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

Okay basically, what I want to do is to record audio using a MAX9814 electret Mic amp. I manage to get the when i used the code without SDFat. What i want to do is like a recorder. I can record audio when button is pressed, saved it into .wav file, then saved into my sd card. I also can listen to the recording from the wav file.

These are the codes:

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

// Works threaded or not
SYSTEM_THREAD(ENABLED);

// Print debug message to USB serial
SerialLogHandler logHandler;

const size_t SAMPLE_FREQ = 16000; // Hz

const size_t SAMPLES_IN_BUFFER = 1024;

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


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

	Serial.begin(9600);

	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() {

	if (bufferReady) {
		int16_t *samples = (int16_t *)bufferReady;
		bufferReady = 0;

		Serial.println(*samples);

		// Do something with the samples here
		// For the default 12-bit sampling, each index in samples will be 0 - 4095 (inclusive)

	}
}

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









And this is the result that I got. (I got the result by copying the whole terminal, paste it into excel, make line graph). So what i was hoping is to save this as wav file so that i can play the recording.
image

So the

example is a good start to write the data to a file

I have no idea of how to convert to a .wav - sorry can’t help there.

Okay thank you very much. I’ll take a look.

I suspect that equal sign ( = ) should acutally be a opening parenthesis ( ( ).

You'd only need to write the WAV header populated with the correct values (mono, 16bit, 16kHz sampling), convert the unsigned 12 bit samples in signed 16 bit samples (as shown in the other post) and append them to the header and finally go back to the header and update the length field in the WAV header.

1 Like

Alright understood, I'll try it when i got home. Thank you very much. So far am i on the right track?

I will need to read up more about this. I will let u know if i need help. Sorry, I'm not too familiar with IT. What I had experienced before is Arduino, which is much simplier than this hahaha. Once again, thank you @ScruffR

Yup, the code you found in this thread should get you started with a PCM file (which you could play back with software like Audacity without needing to add the WAV header to begin with).

So if i want to make into wav file, i just need to add wav header am i right? When i add this header, it will automatically change my raw file into wav file right?

Why i manage to get the pcm data (raw data) using this only
rickkas7/ADCDMAGen3_RK

The raw data that i got is this
image

The graph in this picture is obtained by using the ADXDMAGen3 library only. I did not use this thread

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