How to record audio and save it to wav file using Electret Mic Amp - MAX9814?

A file is a file and the file extensions just help the OS to "link" applications that can deal with a particular sort of file and such a file in an abstract way.

The WAV header is there to tell a playback application how (speed, bytes per sample, how many bytes to read from the file, ...) to play the raw PCM data.
So once you have the raw PCM data you'd already have everything needed to play it back.

Your graph above shows just very few samples (about 12.5 milliseconds) so not a lot to deal with let alone to hear if played back.

Icic alright, i think i understand the overall hehehe. But is it correct that i was able to get the raw PCM data just from the ADCDMAGen3 lib? The graph that I obtained came from just the ADCDMAGEN3 lib without any changes. The only changes is i put the Serial.println.

When you did that you only printed the first of 1024 samples.
To get all the data of one sampling go you’d need to do this

  for (int i=0; i < SAMPLES_IN_BUFFER; i++)
    Serial.println(samples[i]);

Ahh okay, thanks, ill try when i got home.

Hi @ScruffR, Thanks for the help. I managed to get the samples using this code

for (int i=0; i < SAMPLES_IN_BUFFER; i++)
   Serial.println(samples[i]);

Just an update, I manage to write the file inside my sd card using this example. Thanks @shanevanj .

However, I tried to understand the code in this thread and I only understand it a little bit.

This is what i have done.

// Simple ADC DMA example

// Repository: https://github.com/rickkas7/ADCDMAGen3_RK
// License: MIT (free for use in open or closed source products)

#include "Particle.h"
#include "ADCDMAGen3_RK.h"
#include "adc_hal.h"
#include "gpio_hal.h"
#include "pinmap_hal.h"
#include "pinmap_impl.h"
#include <SdFat.h>

void buttonHandler(system_event_t event, int data);

SdFat sd;
File myFile;
int fileCount=0;

int button=D2;
int buttonstate;

const int SPI_CS = A5;
const size_t SAMPLES_IN_BUFFER = 1024;
const size_t SAMPLE_FREQ = 16000; // Hz
const unsigned long MAX_RECORDING_LENGTH_MS = 10000;

enum State { STATE_WAITING, STATE_CONNECT, STATE_RUNNING, STATE_FINISH };
State state = STATE_WAITING;

unsigned long recordingStart;

// Print debug message to USB serial
SerialLogHandler logHandler;

// Works threaded or not
SYSTEM_THREAD(ENABLED);

// This is where the samples are stored
static nrf_saadc_value_t buffer0[SAMPLES_IN_BUFFER];
static nrf_saadc_value_t buffer1[SAMPLES_IN_BUFFER];
static nrf_saadc_value_t *bufferReady = 0;

ADCDMAGen3 adc;

void myBufferCallback(nrf_saadc_value_t *buf, size_t size);
int result = 0;

void setup() {
	// Optional, just for testing so I can see the logs below
	// waitFor(Serial.isConnected, 10000);
	pinMode(button, INPUT);
	Serial.begin(9600);

	if(sd.begin(SPI_CS, SPI_FULL_SPEED))
	//the sd is underlined.
		Serial.println("SD initialised");
	else
		Serial.println("failed to open card");

	ret_code_t err = adc
		.withSampleFreqHz(SAMPLE_FREQ)
		.withDoubleBuffer(SAMPLES_IN_BUFFER, buffer0, buffer1)
		.withSamplePin(A0)
		.withBufferCallback(myBufferCallback)
		.init();

	Log.info("adc.init %lu", err);

	adc.start();
}

void loop() {

buttonstate=digitalRead(button);

if(buttonstate = HIGH){
State state = STATE_CONNECT;
}
else
{
	switch(state){
		case STATE_WAITING:
		//// Waiting for the user to press the tactile switch
		break;

		case STATE_CONNECT:
			char fileName[128];
			snprintf(fileName, sizeof(fileName), "rec%04d.pcm", fileCount+1);
		if(myFile.open(fileName, O_RDWR | O_CREAT | O_TRUNC))
		{
			fileCount++;
			digitalWrite(D7,HIGH);
			Serial.printlnf("Writing to %s", fileName);
		  	recordingStart = millis();

			if (bufferReady) 
			{
				int16_t *samples = (int16_t *)bufferReady;
				bufferReady = 0;
			}
			state = STATE_RUNNING;
		}
		else 
		{
			Serial.printlnf("opening %s for write failed", fileName);
			state = STATE_WAITING;
	    }
		break;

		case STATE_RUNNING:
		//how to continue
		break;


		case STATE_FINISH:
		//how to continue
		break;
	}
}
}

void myBufferCallback(nrf_saadc_value_t *buf, size_t size) {
	// This gets executed after each sample buffer has been read.
	// Note: This is executed in interrupt context, so beware of what you do here!

	// We just remember the buffer and handle it from loop
	bufferReady = buf;
}

Am I on the right track? Is there anything wrong so far? How do I continue this since I'm not using Photon. What I want to change is to record the audio for 10 seconds when the tactile button is pressed. Then the data will be converted into wav file and sent to the sd card. Once again, thank you very much.:grin:

I have had the time to cobble together some code but wasn’t able to test yet.
This also creates the WAV header

#include <SdFat.h>
#include <ADCDMAGen3_RK.h>

//SYSTEM_THREAD(ENABLED);

SerialLogHandler logHandler(LOG_LEVEL_WARN, { // Logging level for non-application messages
    { "app", LOG_LEVEL_INFO } // Logging level for application messages
});
// WAV header spec information:
//https://web.archive.org/web/20140327141505/https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
//http://www.topherlee.com/software/pcm-tut-wavformat.html
typedef struct wav_header {
  // RIFF Header
  char     riff_header[4];   // Contains "RIFF"
  uint32_t wav_size;         // Size of the wav portion of the file, which follows the first 8 bytes. File size - 8
  char     wave_header[4];   // Contains "WAVE"
    
  // Format Header
  char     fmt_header[4];    // Contains "fmt " (includes trailing space)
  uint32_t fmt_chunk_size;   // Should be 16 for PCM
  uint16_t audio_format;     // Should be 1 for PCM. 3 for IEEE Float
  uint16_t num_channels;
  uint32_t sample_rate;
  uint32_t byte_rate;        // Number of bytes per second. sample_rate * num_channels * Bytes Per Sample
  uint16_t sample_alignment; // num_channels * Bytes Per Sample
  uint16_t bit_depth;        // Number of bits per sample
    
  // Data
  char     data_header[4];   // Contains "data"
  uint32_t data_bytes;       // Number of bytes in data. Number of samples * num_channels * sample byte size
  // uint8_t bytes[];        // Remainder of wave file is bytes
} WavHeader_t;

const  int        SD_SS                   = A2; 
const  int        SAMPLE_PIN              = A0;
const  int        SAMPLE_CHANNELS         =     1; // 1 .. mono, 2 .. stereo     
const  size_t     SAMPLE_FREQ             = 16000; // Hz
const  size_t     SAMPLES_IN_BUFFER       =  1024;
const  uint32_t   MAX_RECORDING_LENGTH_MS = 30000; // milliseconds

nrf_saadc_value_t *bufferReady            = 0;
nrf_saadc_value_t buffer0[SAMPLES_IN_BUFFER];
nrf_saadc_value_t buffer1[SAMPLES_IN_BUFFER];

int               fileCount               = 0;
uint32_t          recordingStart;
enum              State { STATE_WAITING, STATE_CONNECT, STATE_RUNNING, STATE_FINISH };
State             state                   = STATE_WAITING;

SdFat             sd;
File              myFile;
WavHeader_t       wh;

ADCDMAGen3 adc;

void myBufferCallback(nrf_saadc_value_t *buf, size_t size);

void setup() {
  Serial.begin(9600);
  System.on(button_click, buttonHandler);
  pinMode(D7, OUTPUT);
	
  ret_code_t err = adc
	.withSampleFreqHz(SAMPLE_FREQ)
	.withDoubleBuffer(SAMPLES_IN_BUFFER, buffer0, buffer1)
	.withSamplePin(A0)
	.withBufferCallback(myBufferCallback)
	.init();
  Log.info("adc.init %lu", err);

  if (sd.begin(SD_SS, SPI_FULL_SPEED)) 
    Log.info("SD initialised");
  else
    Log.warn("failed to open card");
}

void loop() {
  switch(state) {
	case STATE_WAITING:
	  // Waiting for the user to press the SETUP button. The setup button handler
	  // will bump the state into STATE_CONNECT
	break;

	case STATE_CONNECT:
	{
      char fileName[128];
      snprintf(fileName, sizeof(fileName), "rec%04d.wav", fileCount+1);
      if (myFile.open(fileName, O_RDWR | O_CREAT | O_TRUNC)) {
    	strcpy(wh.riff_header, "RIFF");
  	    strcpy(wh.wave_header, "WAVE");
  	    strcpy(wh.fmt_header , "fmt ");
	    strcpy(wh.data_header, "data");
        wh.fmt_chunk_size   = 16;
        wh.audio_format     = 1; 
        wh.num_channels     = SAMPLE_CHANNELS;
        wh.bit_depth        = 16;
        wh.sample_rate      = SAMPLE_FREQ;
        wh.sample_alignment = wh.num_channels * wh.bit_depth / 8; 
        wh.byte_rate        = wh.sample_rate * wh.sample_alignment; 
        wh.data_bytes       = 0;
        wh.wav_size         = sizeof(wh) - 8;
        if (myFile.write((uint8_t *)&wh, sizeof(wh)) < sizeof(wh)) {
  		  Log.warn("error writing WAV header");
  		  myFile.close();
		  state = STATE_WAITING;
          return;
		}
        
        fileCount++;
        digitalWrite(D7, HIGH);
	    Log.info("Writing to %s", fileName);
		recordingStart = millis();
        adc.start();
        state = STATE_RUNNING;
      }
      else {
	    Log.warn("opening %s for write failed", fileName);
		state = STATE_WAITING;
	  }
	}
    break;

	case STATE_RUNNING:
      if (bufferReady) {
	    int16_t *samples = (int16_t*)bufferReady;
	    bufferReady = 0;
	    for (int i = 0; i < SAMPLES_IN_BUFFER; i++) {
	      samples[i] -= 2048;  // transpose 0 .. +4095 to -2048 .. +2047 
	      samples[i] <<= 4;    // 12bit -> 16bit
	    }
        int count = myFile.write((uint8_t *)samples, SAMPLES_IN_BUFFER * 2);
        wh.wav_size   += count;
		wh.data_bytes += count;
		if (count == SAMPLES_IN_BUFFER * 2)
		  Log.trace("%d samples written", count/2);
		else {
  		  Log.warn("error writing %d", count/2);
		  state = STATE_FINISH;
		}
      }
      if (millis() - recordingStart >= MAX_RECORDING_LENGTH_MS) {
		state = STATE_FINISH;
	  }
	break;

	case STATE_FINISH:
		adc.stop();
		Log.info("stopping");
		
        myFile.seek(0);
        if (myFile.write((uint8_t *)&wh, sizeof(wh)) < sizeof(wh))
  		  Log.warn("error writing WAV header");

		myFile.close();

		digitalWrite(D7, LOW);
		state = STATE_WAITING;
		break;
	}
}

void buttonHandler(system_event_t event, int data) {
	switch(state) {
	case STATE_WAITING:
		state = STATE_CONNECT;
		break;

	case STATE_RUNNING:
		state = STATE_FINISH;
		break;
	}
}

void myBufferCallback(nrf_saadc_value_t *buf, size_t size) {
  bufferReady = buf;
}

You can start/stop a recording with the MODE button and via MAX_RECORDING_LENGTH_MS (currently 30 seconds) you can limit the length of a recording.

2 Likes

Wah thank you very much! I’ll let u know whether can work or not hehehe.

Hi @ScruffR, i changed the SD_SS into A5 because the SPI_SS of Argon is A5 pin. Furthermore, I changed the code from

for (int i = 0; i &lt; SAMPLES_IN_BUFFER; i++)

to

for (unsigned int i = 0; i < SAMPLES_IN_BUFFER; i++) 

because the error on this was comparison between signed and unsigned integer expressions.
I don't know whether this will affect the code or not.

When I tried to run it, I pressed the mode button. The terminal gives me this message

0000151191 [app] WARN: opening rec0001.pcm for write failed

So from this, I know that the program failed on this part

if (myFile.open(fileName, O_RDWR | O_CREAT | O_TRUNC))

May I know what is the meaning of this part so that I can try to fix it? There are a lot of things in the code that you gave me I don't understand. After the whole thing is done, do u mind if I send you the codes that I'm not too sure so you can help me understand? If too troublesome, I'll try to find out myself. But now, I cannot figure out on how to fix this. This is very difficult for a beginner :sweat_smile:

Thank you so much for the help and time!

A5 is only the default pin which would be used if not stated otherwise.
An SPI slave can use whichever pin it wants - that's how multiple SPI devices can share the same bus by dedicateing one unique SlaveSelect pin for each of them.

I guess this was rather a warning not an error and it wouldn't affect the code unless you had 2+ billion samples - however, it's good style to resolve warnings nonetheless.

Have you tried the funcionality of your SD first?
When you have multiple parts that need to interact, it's best to first test each of them individually with the most simple test code possible and only after that start putting them together gradually.
This makes determining the cause of eventual problems much easier.

Unfortunately I have no MAX9814 board, otherwise I'd try that code with the exact same setup you have.

But as it comes to the code, sure you can ask about anything.

yap I did. I tried to use the example

and I can make my txt file inside my SD card. For my mic, I also did what you told me to do and manage to get the raw data inside the terminal.

Sorry for the question, but have you reinserted the SD card properly?
Just pop it out and in again to make doubly sure :wink:

I guess I need build a test rig with some kind of mock-up mic

Yap i did.

I guess I need build a test rig with some kind of mock-up mic

Thank you so much for trying to help me. Sorry if i'm a slow learner hahaha.

I just noticed something. This code doesn't appear in my terminal at all :sweat_smile:

  if (sd.begin(SD_SS, SPI_FULL_SPEED)) 
    Log.info("SD initialised");
  else
    Log.warn("failed to open card");
}

It doesn't show SD initialised or failed to open card. I'll try to fix the hardware. If i make the SPI_SS is A2, meaning I put the CS on the module to A2 right?

That should not really make any difference.

You get neither of these messages but you get the output of Log.info("adc.init %lu", err);?

I got a lot of these code

0000015765 [comm.protocol] INFO: message id 9 complete with code 0.00
0000015767 [comm.protocol] INFO: rcv'd message type=13
0000015869 [system] INFO: Cloud connected
0000046869 [comm.protocol] INFO: message id 13 complete with code 0.00
0000046869 [comm.protocol] INFO: rcv'd message type=13
0000077671 [comm.protocol] INFO: message id 14 complete with code 0.00
0000077673 [comm.protocol] INFO: rcv'd message type=13
0000108375 [comm.protocol] INFO: message id 15 complete with code 0.00
0000108375 [comm.protocol] INFO: rcv'd message type=13

Then if I don't press the MODE button, it will keep increasing the rcv'd id...
Then if I pressed the MODE button, it will just show this

0000173223 [app] WARN: opening rec0001.pcm for write failed

I don't know what is those comm.protocol and system :frowning:

Okay so i tried to implement the ReadWrite.ino to the code you gave me. Amazingly, I managed to get the pcm file. IMPROVEMENT! I used Audacity to play the PCM and it works well. The changes that I made was changing the SS pin, make it into the A2 SPI pin. I think the library did it for me. Then I added this.

myFile = sd.open(fileName, FILE_WRITE);
snprintf(fileName, sizeof(fileName), "rec%04d.pcm", fileCount+1);
if (myFile.open(fileName, O_RDWR | O_CREAT | O_TRUNC)

This is the overall code:

#include <SdFat.h>
#include <ADCDMAGen3_RK.h>
#define SD_CS_PIN SS

SYSTEM_THREAD(ENABLED);

// WAV header spec information:
//https://web.archive.org/web/20140327141505/https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
//http://www.topherlee.com/software/pcm-tut-wavformat.html
typedef struct wav_header {
  // RIFF Header
  char     riff_header[4];   // Contains "RIFF"
  uint32_t wav_size;         // Size of the wav portion of the file, which follows the first 8 bytes. File size - 8
  char     wave_header[4];   // Contains "WAVE"
    
  // Format Header
  char     fmt_header[4];    // Contains "fmt " (includes trailing space)
  uint32_t fmt_chunk_size;   // Should be 16 for PCM
  uint16_t audio_format;     // Should be 1 for PCM. 3 for IEEE Float
  uint16_t num_channels;
  uint32_t sample_rate;
  uint32_t byte_rate;        // Number of bytes per second. sample_rate * num_channels * Bytes Per Sample
  uint16_t sample_alignment; // num_channels * Bytes Per Sample
  uint16_t bit_depth;        // Number of bits per sample
    
  // Data
  char     data_header[4];   // Contains "data"
  uint32_t data_bytes;       // Number of bytes in data. Number of samples * num_channels * sample byte size
  // uint8_t bytes[];        // Remainder of wave file is bytes
} WavHeader_t;

SerialLogHandler logHandler;


const  int        SAMPLE_PIN              = A0;
const  size_t     SAMPLE_FREQ             = 16000; // Hz
const  size_t     SAMPLES_IN_BUFFER       = 1024;
const  uint32_t   MAX_RECORDING_LENGTH_MS = 10000; // milliseconds

nrf_saadc_value_t *bufferReady            = 0;
nrf_saadc_value_t buffer0[SAMPLES_IN_BUFFER];
nrf_saadc_value_t buffer1[SAMPLES_IN_BUFFER];

int               fileCount               = 0;
uint32_t          recordingStart;
enum              State { STATE_WAITING, STATE_CONNECT, STATE_RUNNING, STATE_FINISH };
State             state                   = STATE_WAITING;

SdFat             sd;
File              myFile;
WavHeader_t       wh;

ADCDMAGen3 adc;

void myBufferCallback(nrf_saadc_value_t *buf, size_t size);

void setup() {
  Serial.begin(9600);
  System.on(button_click, buttonHandler);
  pinMode(D7, OUTPUT);
	
  ret_code_t err = adc
	.withSampleFreqHz(SAMPLE_FREQ)
	.withDoubleBuffer(SAMPLES_IN_BUFFER, buffer0, buffer1)
	.withSamplePin(A0)
	.withBufferCallback(myBufferCallback)
	.init();
  Log.info("adc.init %lu", err);

  if (sd.begin(SD_CS_PIN)) 
    Log.info("SD initialised");
  else
    Log.warn("failed to open card");
}

void loop() {
  switch(state) {
	case STATE_WAITING:
	  // Waiting for the user to press the SETUP button. The setup button handler
	  // will bump the state into STATE_CONNECT
	break;

	case STATE_CONNECT:
	{
      char fileName[128];
	  myFile = sd.open(fileName, FILE_WRITE); 
      snprintf(fileName, sizeof(fileName), "rec%04d.pcm", fileCount+1);
      if (myFile.open(fileName, O_RDWR | O_CREAT | O_TRUNC)) {
    	strcpy(wh.riff_header, "RIFF");
  	    strcpy(wh.wave_header, "WAVE");
  	    strcpy(wh.fmt_header , "fmt ");
	    strcpy(wh.data_header, "data");
        wh.fmt_chunk_size   = 16;
        wh.audio_format     =  1; 
        wh.num_channels     =  2;
        wh.bit_depth        = 16;
        wh.sample_rate      = SAMPLE_FREQ;
        wh.sample_alignment = wh.num_channels * wh.bit_depth; 
        wh.byte_rate        = wh.sample_rate * wh.sample_alignment; 
        wh.data_bytes       = 0;
        wh.wav_size         = sizeof(wh) - 8;
        if (myFile.write((uint8_t *)&wh, sizeof(wh)) < sizeof(wh)) {
  		  Log.warn("error writing WAV header");
  		  myFile.close();
		  state = STATE_WAITING;
          return;
		}
        
        fileCount++;
        digitalWrite(D7, HIGH);
	    Log.info("Writing to %s", fileName);
		recordingStart = millis();
        adc.start();
        state = STATE_RUNNING;
      }
      else {
	    Log.warn("opening %s for write failed", fileName);
		state = STATE_WAITING;
	  }
	}
    break;

	case STATE_RUNNING:
      if (bufferReady) {
	    int16_t *samples = (int16_t*)bufferReady;
	    bufferReady = 0;
	    for (unsigned int i = 0; i < SAMPLES_IN_BUFFER; i++) {
	      samples[i] -= 2048;  // transpose 0 .. +4095 to -2048 .. +2047 
	      samples[i] <<= 4;    // 12bit -> 16bit
	    }
        int count = myFile.write((uint8_t *)samples, SAMPLES_IN_BUFFER * 2);
        wh.wav_size   += count;
		wh.data_bytes += count;
		if (count == SAMPLES_IN_BUFFER * 2)
		  Log.info("%d samples written", count/2);
		else {
  		  Log.warn("error writing %d", count/2);
		  state = STATE_FINISH;
		}
      }
      if (millis() - recordingStart >= MAX_RECORDING_LENGTH_MS) {
		state = STATE_FINISH;
	  }
	break;

	case STATE_FINISH:
		digitalWrite(D7, LOW);
		adc.stop();
		Log.info("stopping");
		
        myFile.seek(0);
        if (myFile.write((uint8_t *)&wh, sizeof(wh)) < sizeof(wh))
  		  Log.warn("error writing WAV header");

		myFile.close();
		state = STATE_WAITING;
		break;
	}
}

void buttonHandler(system_event_t event, int data) {
	switch(state) {
	case STATE_WAITING:
		state = STATE_CONNECT;
		break;

	case STATE_RUNNING:
		state = STATE_FINISH;
		break;
	}
}

void myBufferCallback(nrf_saadc_value_t *buf, size_t size) {
  bufferReady = buf;
}

So now what should i do to make that file into wav file. I thought this code already has the wav header?

Not sure what went wrong on your side but for me my code above worked as is.

However, I changed rec%04d.pcm to rec%04d.wav since the created file actually now is a WAV file - it was just named wrong.
And to reduce the chatter in the log output I replaced

SerialLogHandler logHandler;

with

SerialLogHandler logHandler(LOG_LEVEL_WARN, { // Logging level for non-application messages
    { "app", LOG_LEVEL_ALL } // Logging level for application messages
});

Wah thank you so much, I did it. I manage to get the wav file. I dont know why mine need to include

	  myFile = sd.open(fileName, FILE_WRITE);

this one though. At least now I got the wav file hehehe. Thank you so much. I might need to ask you some question regarding the codes, but now is already late. Probably tomorrow. Once again, thank you so much. Glad to have someone like you in the community hehehe :laughing:

I just found two errors in the WAV header initialisation.

This should actually be 1 as we are not recording stereo but mono.

and

  wh.sample_alignment = wh.num_channels * wh.bit_depth;` 

should actually be

  wh.sample_alignment = wh.num_channels * wh.bit_depth / 8;

Okay thank you for the info. :grin:

Hi @ScruffR, if i want to change the click button into a tactile switch (pull down resistor), which part of my firmware should i change? Can you briefly explain on how the firmware makes the MODE button becoming the button input?

What I try to do is to add this:

int button = A2;
int buttonstate;

void setup(){
pinMode(button,INPUT);
}

void loop(){
buttonstate = digitalRead(button)
  if(buttonstate = HIGH){
  switch(state) {
	case STATE_WAITING:
		state = STATE_CONNECT;
		break;

	case STATE_RUNNING:
		state = STATE_FINISH;
		break;
  }
}

Something like that. Is it possible? Thank you