Search code examples
c++audioarduinowavrecording

How to convert analog input readings from Arduino to .WAV from sketch


I am tring to build an audio recorder with arduino. Right now the setup has a microphone recording an input an outputing to an analog input of an Arduino uno.

How can I convert a (timestamp,data) reading into .WAV? I know there is more libraries that are able to do this but I am tring to undertand the structure of .WAV and would like to be able to script this my self in C\C++.

Thanks for the Attention!


Solution

  • Step one: define wav header vairables

    /// 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
    

    There are some assumptions made here like quality of audio. The above the assumes CD quality audio 16-bit, 44.1kHz. If you need something else you will need to amend the relevant values.

    You will also need to be aware if your Arduino is actually regularly recording samples or if this is essentially a sonification of data.

    Step 2: Open Wav File.

    #include <SPI.h>
    #include <SD.h>
    
    File wavFile;
    const char* filename = "data.wav";
    
    
    void setup()
    {
    
      Serial.begin(9600);
      while (!Serial);
    
      if (!SD.begin(4))
        while (1);
    
      wavFile = SD.open(filename, FILE_WRITE);
    
    
      if (!wavFile)
        while (1);
    
    
    }
    

    Step 3: Write Header to Wav File.

    Given the way that the SD Library is organised, you have to cast the elements of your header to byte if they are not already char. Have to is perhaps a little strong, but it certainly makes it easier to maintain the correct byte width for each element of the header without fudging things too much.

    It makes sense to wrap this in its own function.

    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);
    }
    

    Step 4: Add Data to Wav File

    The wav header tells us that chunkSize and subChunk2Size are dependant on the amount of data in the file. As such, these will grow and will have to altered if you append data to the file.

    There are a fe ways you can got about adding data

    • add one sample at a time
    • add a buffer of data of a fixed size
    • store all data and add it all at the end

    What method you choose will depend on your implementation which we don't have much info on. For the sake of argument, let's write one sample at a time. This approach should demonstrate what is required and the cons (regularly jumping around the file, lots of repetition) should be immediately apparent.

    void writeDataToWavFile(int data)
    {
      int16_t sampleValue = map(data, 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);
    }
    

    MIN_DATA_VALUE and MAX_DATA_VALUE are something you will have to define. This all assumes you're working with integer data. If it is floating point then some adjustments will need to be made.

    Altogether.

    #include <SPI.h>
    #include <SD.h>
    
    int MIN_DATA_VALUE;
    int MAX_DATA_VALUE;
    File wavFile;
    const char* filename = "data.wav";
    
    /// 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
    
    
    
    void setup()
    {
    
      Serial.begin(9600);
      while (!Serial);
    
      if (!SD.begin(4))
        while (1);
    
      wavFile = SD.open(filename, FILE_WRITE);
    
    
      if (!wavFile)
        while (1);
    
      writeWavHeader();
    
    }
    
    void loop()
    {
      int data = getSomeData();
      writeDataToWavFile(data);
    }
    
    void writeWavHeader()
    {
       wavFile.seek(0);
       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 writeDataToWavFile(int data)
    {
      int16_t sampleValue = map(data, 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);
    }
    

    In sum

    • you need to know the min / max values of your data or constrain them.
    • you have to ensure you are recording at a regular interval or or you will get artefacts.