Creating a .wav file from analog input

Hi everyone,

I am new here and fairly new to coding. I am working on a project which takes a .wav file and performs an FFT for analysis. I decided to use an Adafruit Metro M4 Express board to record the .wav file and I have had some trouble getting things to work.

I am currently attempting to save the analog data in a temporary file on the SD and then have the code read this data and write it to the .wav file but I cant seem to figure out how to get the code to take what's in the .txt and send it to the .wav file.

I originally tried to send the analog data directly to the .wav but I could not get it to work. I also heard that sending data directly to the .wav is not the best idea.

If anyone has any suggestions on how to do this I would really appreciate it.

my code for this section of the project is here:

#include <SD.h>
#include <SPI.h>
File wavFile;
File tempFile;
int datatemp = 0;
const char* fileName = "test1.wav";
const char TempfileName = "datatemp.txt";
const int chipSelect = 10;
int data = 0;
int buttonState = 0;
const int buttonPin = 6; 
const int ledPin1 = 9;    
const int ledPin2 = 13;
const int pulsePin = 7; // Input signal connected to Pin 7 of Arduino
int pulseHigh; // Integer variable to capture High time of the incoming pulse
int pulseLow; // Integer variable to capture Low time of the incoming pulse
float pulseTotal; // Float variable to capture Total time of the incoming pulse
char data1 = data;
int MIN_DATA_VALUE;
int MAX_DATA_VALUE;
/// The first 4 byte of a wav file should be the characters "RIFF" */
char chunkID[4] = {'R', 'I', 'F', 'F'};
/// 36 + SubChunk2Size
uint32_t chunkSize = 36; // You Don't know this until you write your data but at a minimum it is 36 for an empty file
/// "should be characters "WAVE"
char format[4] = {'W', 'A', 'V', 'E'};
/// " This should be the letters "fmt ", note the space character
char subChunk1ID[4] = {'f', 'm', 't', ' '};
///: For PCM == 16, since audioFormat == uint16_t
uint32_t subChunk1Size = 16;
///: For PCM this is 1, other values indicate compression
uint16_t audioFormat = 1;
///: Mono = 1, Stereo = 2, etc.
uint16_t numChannels = 1;
///: Sample Rate of file
uint32_t sampleRate = 44100;
///: SampleRate * NumChannels * BitsPerSample/8
uint32_t byteRate = 44100 * 2;
///: The number of byte for one frame NumChannels * BitsPerSample/8
uint16_t blockAlign = 2;
///: 8 bits = 8, 16 bits = 16
uint16_t bitsPerSample = 16;
///: Contains the letters "data"
char subChunk2ID[4] = {'d', 'a', 't', 'a'};
///: == NumSamples * NumChannels * BitsPerSample/8  i.e. number of byte in the data.
uint32_t subChunk2Size = 0; // You Don't know this until you write your data
unsigned long previousMillis = 0;
const long interval = 10000;
// *** from final
int writeWavHeader()
{
  wavFile.write(chunkID,4);
  wavFile.write((byte*)&chunkSize,4);
  wavFile.write(format,4);
  wavFile.write(subChunk1ID,4);
  wavFile.write((byte*)&subChunk1Size,4);
  wavFile.write((byte*)&audioFormat,2);
  wavFile.write((byte*)&numChannels,2);
  wavFile.write((byte*)&sampleRate,4);
  wavFile.write((byte*)&byteRate,4);
  wavFile.write((byte*)&blockAlign,2);
  wavFile.write((byte*)&bitsPerSample,2);
  wavFile.write(subChunk2ID,4);
  wavFile.write((byte*)&subChunk2Size,4);
  
  return 0;
}
int writeDataToWavFile(int signal)
{
  tempFile = SD.open(datatemp)
    if (tempFile) {
      while(datatemp.Available()){
        int signal = Serial.read
        readint += signal
        delay(3)
      }
    }
  int16_t sampleValue = map(signal, MIN_DATA_VALUE, MAX_DATA_VALUE,-32767,32767);
  subChunk2Size += numChannels * bitsPerSample/8;
  wavFile.seek(40);
  wavFile.write((byte*)&subChunk2Size,4);
  wavFile.seek(4);
  chunkSize = 36 + subChunk2Size;
  wavFile.write((byte*)&chunkSize,4);
  wavFile.seek(wavFile.size()-1);
  wavFile.write((byte*)&sampleValue,2);
  return 0;
}

void setup()
{
  pinMode(ledPin1, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(pulsePin, INPUT);
  Serial.begin(9600);
  Serial.println("starting project");
   if (SD.begin(chipSelect))
    {
    Serial.println("SD card is present");
    } 
    else
    {
    Serial.println("SD card missing");
    while(1);  //wait here forever
    }
    writeWavHeader();
    wavFile.close();
}
void loop() 
{
       buttonState = digitalRead(buttonPin);
  if (buttonState == LOW) {
    digitalWrite(ledPin1, HIGH);
    delay(500);
    digitalWrite(ledPin1, LOW);
    delay(500);
    digitalWrite(ledPin1, HIGH);
    delay(500);
    digitalWrite(ledPin1, LOW);
    delay(500);
    digitalWrite(ledPin1, HIGH);
    delay(500);
    digitalWrite(ledPin1, LOW);
    delay(500);
    digitalWrite(ledPin1, HIGH);
    Serial.println("start of for sect");
          // writeWavHeader();
   for (int i = 0; i <= 500; i++) {  
    SD.open(fileName, FILE_WRITE);
    if (wavFile);
      int data = analogRead(A3);
        Serial.println(data);
        writeDataToWavFile(data);        
        }
    wavFile.close(); 
    digitalWrite(ledPin1, LOW);
    Serial.print("done");
     }
}

Sorry if this code is sort of a horror show. I'm not much of a coder. more of a hardware guy.

There is no way that code compiles.

If loop(), you have a for() loop that cycles 500 times, calling writeDataToWavFile() each iteration, but inside that function, you open a file from the SD card and try to read the file. I don't think you want to do that 500 times. You also open fileName every time through the for() loop which is unnecessary.

At the very least, tidy up your code using Ctrl-T in the IDE. Besides making it easier to read, it will show you a few things.
Things like this:

That trailing semicolon terminates the if() statement so everything after that will always be executed.

If you are just starting out with coding, I'd start a lot smaller and build my way up. Learn how to detect when a button is pressed, not if a button is pressed with the State Change Detection example in the IDE (Examples->02.digital->State Change Detection)

Then learn how to read/write to as SD card. There are lots of examples for the SD library as well.

Once you have all the pieces working, then start to connect it all together.

Why do it all on an Arduino?

There is a app called Audacity (multi platform) that will do all that for you. It will even do an FFT if you want.

When you know what you are looking at with an FFT then you can think about doing it with some sort of Arduino which is a much harder task.

1 Like

Thanks for the replies.

I should clarify, I am not entirely new to coding and I know this project seems like a lot but the functionality is really quite simple. I am essentially looking to make a device that writes a 3 second .wav file to an SD card. It's essentially a one button recorder. This is a very small section of the code of a much larger code that is functional. I know it doesn’t compile and I really just need hep trying to get the analog data to go to a buffer of some sort and then to the .wav when I execute the writetowave function. I thought I might be able to save the data in a .txt and then have a way of moving the data stored in the .txt into a .wav file. I was unable to get it to works so I really only need help with that. I’ve had a pretty tough time finding any good examples of writing a .wav but I know it’s possible.

The previous code, which was attempting to write directly to the .wav file may have a few bits in this code that didn’t get cut out. Like I said, sorry for the horror show. If there is a way to have the analog data record direct to the .wav file that would also be really helpful but I haven’t gotten it to work yet. I have pretty solid guidance from my professors and the guys in the lab, but I wanted to check in here to see if anyone had experience making .wav files with an Arduino.

I should also mention that I already have a complete working FFT code that I created in python. I am not using the Arduino to do the FFT (I don’t even know if it can). I just thought it might help if you all knew a little more about the end goals for this project. That aspect of this project is on an entirely different device. I am only looking to use the Arduino to do the recording aspect of this project.

The Audacity website has a nice little Introduction to Digital Audio. You need to sample the analog at a known sample rate.

But before worrying about the sample rate, make sure you're getting "good data". You should be able to see the difference in silence (or "near silence) and louder sounds. Since the ADC can't read negative voltages the input should be biased and silence should read about half of the ADC range. You''ll need to subtract-out the silence for a WAV file, except 8-bit WAV files are biased and only hold positive sample values.

To "look at" the raw audio data you can run the Analog Read Serial Example (but take-out the delay).

When you look at the sample values they will "look random", even with a constant tone because you are sampling a wave.

I don't see why you need a temporary file. But if you have enough RAM (I don't know anything about that processor) you may want to make a buffer. The only thing special about a WAV file is the header.

You can write the header first. Normally, you have to go-back and fill-in the size field when you stop recording but you know in advance that you're going to record 3 seconds and you know the sample rate and bit depth so you know the number of bytes in advance.

Do you have a way of reading/writing the SD card on your computer? That will be very helpful.

Audacity can read cand play your file from the SD card to confirm it's good. It can also open a "raw" file without the WAV header, which is no problem if you know the bit depth and sample rate, and the other details.

Audacity can also generate a tone that you can save on the SD card, or you could record & save "real audio" or whatever. That would be a handy to test/debug the FFT.

In fact, you may not need a WAV file if it's only going to be read by the microcontroller, which already knows the data-format.

You can try writing a WAV file using the Arduino (a sine or square wave) with software only before trying to capture the real audio data. Then open in Audacity to verify it.

Make sure that your FFT library runs on the Metro. I think the libraries are processor-specific.

Here is an example that works with the Adafruit Clue. I use Audacity to create other types of audio files from the raw .wav format.

// working perfectly, 4/7/2024
/*
  This example reads audio data from the Clue on-board PDM microphone
  and saves to a QSPI flash file audio0n.dat
  Record start/stop using A and B buttons, file names are consecutively numbered.
  // 2Mb flash = 2097152 bytes, 4096 512 byte blocks
*/

#include <Adafruit_Arcada.h>

uint32_t buttons, last_buttons;

//up buffer size to reduce sampling glitches

#define SAMPLES 1024
Adafruit_Arcada arcada;
#include <PDM.h>

// buffer to store samples, each sample is 16-bits
int16_t sampleBuffer[SAMPLES];

// number of samples read
volatile int samplesRead;

File file;
char outputFile[40] = {0};

void setup() {

  Serial.begin(115200);
  while (!Serial) yield();

  // configure the data receive callback
  PDM.onReceive(onPDMdata);

  if (!arcada.arcadaBegin()) {
    while (1);
  }
  //Arcada_FilesystemType
  arcada.filesysBegin(ARCADA_FILESYS_QSPI);

  // Start TFT and fill black
  arcada.displayBegin();

  // Turn on backlight
  arcada.setBacklight(255);
  arcada.display->setTextWrap(false);
  arcada.display->fillScreen(ARCADA_BLACK);
  arcada.display->setTextColor(ARCADA_GREEN);
  arcada.display->setTextSize(2);
  arcada.display->println("Audio Recorder");

  // initialize PDM with:
  // - one channel (mono mode)
  // - a 16 kHz sample rate
  // larger buffer
  PDM.setBufferSize(2 * SAMPLES); //bytes!
  if (!PDM.begin(1, 16000)) {
    arcada.display->println("PDM failure");
    while (1) yield();
  }
  // set the gain, defaults to 20
  PDM.setGain(45);

  arcada.display->println("A/B start/stop");
  delay(300); //wait for microphone to settle
}
int nframes = 0; //frame count, max 4000 on QSPI flash
int filenum = 0; //file number
int recording = 0; //run/stop mode
void loop() {

  int bytes_written = 0;

  buttons = arcada.variantReadButtons();
  if (buttons != last_buttons) {
    last_buttons = buttons;

    if (buttons & ARCADA_BUTTONMASK_A) {
      arcada.display->println("starting");
      recording = 1;
      snprintf(outputFile, sizeof(outputFile), "/audio%02d.dat", filenum); //generate a name
      file = arcada.open(outputFile, O_CREAT | O_WRITE);
      if (!file) {
        arcada.display->println("output file open failure");
        while (1);
      }
      else { //display output file name
        arcada.display->println(outputFile);
      }
    } //button A pressed
  } //buttons
  //      int x, avg = 0;
  while (recording) {
    // wait for samples to be read
    if (samplesRead) {
      bytes_written = file.write((char *)sampleBuffer, 2 * SAMPLES);  //check bytes_written for out of space
      nframes++;
      samplesRead = 0;
    }//samples read
    // check buttons for stop
    buttons = arcada.variantReadButtons();
    if (buttons != last_buttons) {
      last_buttons = buttons;
      if (buttons & ARCADA_BUTTONMASK_B) {
        recording = 0;
      }
    }
    //close file if done, out of space or button B pushed.

    if (bytes_written < 2 * SAMPLES || recording == 0) {
      file.close();
      arcada.display->print("Stop: ");
      arcada.display->println(bytes_written);
      recording = 0;
      filenum++; //next file name
    }
  }// while recording
} //loop

void onPDMdata() {
  // query the number of bytes available
  int bytesAvailable = PDM.available();

  // read into the sample buffer
  PDM.read(sampleBuffer, bytesAvailable);

  // 16-bit, 2 bytes per sample
  samplesRead = bytesAvailable / 2;
}

Thanks a lot for the replies. This has been really helpful. I think I have a better understanding of what I need to do to get this thing working.

I understand why I'd bias the signal but how would I do that? would I be adding a 'DC' bias to the signal? if it is DC bias, would it need to be at 2.5 volts?

I was only trying to make a temporary file since I wasn't able to get the data to write directly from the analog pin. I was able to figure out that part earlier today with one of the guys in the lab but now I'm not so sure I will need it!

I did have two other questions about the write function in the code; after the very last line, is there anything I need to add to signify that its 'done'? I feel like I may be missing something here but I really don't know what. The other question was about the min/ max data values. What exactly is determining these values? Should they be preset to an amount based on my frequency range?

Thanks again for the assistance. A different version of the code is below

#include <SD.h>
#include <SPI.h>

const int chipSelect = 10;
int MIN_DATA_VALUE = 0;
int MAX_DATA_VALUE = 1024;

// from .wav writing file
/// The first 4 byte of a wav file should be the characters "RIFF" */
char chunkID[4] = {'R', 'I', 'F', 'F'};
/// 36 + SubChunk2Size
uint32_t chunkSize = 36; // You Don't know this until you write your data but at a minimum it is 36 for an empty file
/// "should be characters "WAVE"
char format[4] = {'W', 'A', 'V', 'E'};
/// " This should be the letters "fmt ", note the space character
char subChunk1ID[4] = {'f', 'm', 't', ' '};
///: For PCM == 16, since audioFormat == uint16_t
uint32_t subChunk1Size = 16;
///: For PCM this is 1, other values indicate compression
uint16_t audioFormat = 1;
///: Mono = 1, Stereo = 2, etc.
uint16_t numChannels = 1;
///: Sample Rate of file
uint32_t sampleRate = 44100;
///: SampleRate * NumChannels * BitsPerSample/8
uint32_t byteRate = 44100 * 2;
///: The number of byte for one frame NumChannels * BitsPerSample/8
uint16_t blockAlign = 2;
///: 8 bits = 8, 16 bits = 16
uint16_t bitsPerSample = 16;
///: Contains the letters "data"
char subChunk2ID[4] = {'d', 'a', 't', 'a'};
///: == NumSamples * NumChannels * BitsPerSample/8  i.e. number of byte in the data.
uint32_t subChunk2Size = 2116800; // You Don't know this until you write your data

File wavFile;
const char* filename = "fre110.wav";


void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  // wait for Serial Monitor to connect. Needed for native USB port boards only:
  while (!Serial);

  Serial.print("Initializing SD card...");

  if (!SD.begin(10)) {
    Serial.println("initialization failed.");
    while (true);
  }

  Serial.println("initialization done.");

if (!SD.begin(10))
    while (1);

  wavFile = SD.open("freq110.wav", FILE_WRITE);

  if (!wavFile)
    while (1);
}

void writeWavHeader()
{
   wavFile.write(chunkID,4);
   wavFile.write((byte*)&chunkSize,4);
   wavFile.write(format,4);
   wavFile.write(subChunk1ID,4);
   wavFile.write((byte*)&subChunk1Size,4);
   wavFile.write((byte*)&audioFormat,2);
   wavFile.write((byte*)&numChannels,2);
   wavFile.write((byte*)&sampleRate,4);
   wavFile.write((byte*)&byteRate,4);
   wavFile.write((byte*)&blockAlign,2);
   wavFile.write((byte*)&bitsPerSample,2);
   wavFile.write(subChunk2ID,4);
   wavFile.write((byte*)&subChunk2Size,4);
}

void loop() 
{
  int analogdata = analogRead(A2);
  Serial.println(analogdata);
  writeDataToWavFile(analogdata);
  delay(500);
}

 void writeDataToWavFile(int analogdata)
{
  int16_t sampleValue = map(analogdata, MIN_DATA_VALUE, MAX_DATA_VALUE, -32767, 32767);

  subChunk2Size += numChannels * bitsPerSample/8;
  wavFile.seek(40);
  wavFile.write((byte*)&subChunk2Size,4);

  wavFile.seek(4);
  chunkSize = 36 + subChunk2Size;
  wavFile.write((byte*)&chunkSize,4);

  wavFile.seek(wavFile.size()-1);
  wavFile.write((byte*)&sampleValue,2);
}

Thanks again!

Yes, usually half of the analog reference voltage. Typical line audio input circuit for 5V Arduino, with AREF = 5V:

Capture

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.