Record on SD using AudioStream

Status
Not open for further replies.

Paso

Member
Hi guys,
I'm an Italian maker with a decent/good background in Arduino and C++. In the past I used Teensy to realize several projects, the last one was a sort of answering machine, made with Teensy 3.6. The project worked very well, but I found the approach to audio recording really bad compared to the one used for audio play. So I started writing a hereditary AudioStream class for recording WAV files on SD cards. The library is based on the example file Recorder.ino.

My setup:
- Teensy4.0 with SD card reader soldered under the board using pins 34-39
- microSDHC Class10 UHS-I Transcend 16GB
- Visual Studio Code wtih Platformio

I'm sure the setup is working properly, as the Recorder.ino sketch works.

The problem: after recording, the data block of the wav file is a long succession of 0.

My files:
record_sd_wav.h
Code:
#ifndef record_sd_wav_h_
#define record_sd_wav_h_

#include "Arduino.h"
#include "AudioStream.h"
#include "SD.h"

#if defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__)

#define STATE_STOP 13
#define STATE_REC 14

class AudioRecordSdWav : public AudioStream
{
public:
    AudioRecordSdWav(void) : AudioStream(1, inputQueueArray) { begin(); }
    void begin(void);
    bool record(const char *filename);
    bool isRecording(void) { return state_record == STATE_REC; }
    void stop(void);
    virtual void update(void);

private:
    audio_block_t *inputQueueArray[1];

    void write_header(void);
    // wav header
    uint32_t ChunkSize;
    uint32_t Subchunk1Size = 16; //const ?
    uint AudioFormat = 1;        //const ?
    uint NumChannels = 1;
    uint32_t SampleRate = AUDIO_SAMPLE_RATE;
    uint BitsPerSample = 16;                                            //const ?
    uint32_t ByteRate = SampleRate * NumChannels * (BitsPerSample / 8); // samplerate x channels x (bitspersample / 8)
    uint BlockAlign = NumChannels * BitsPerSample / 8;
    uint32_t Subchunk2Size = 0L;
    uint32_t recByteSaved = 0L;

    File wavfile;
    uint8_t state_record;
    audio_block_t *blocklist[2];
    uint8_t buffer[512]; // __attribute__((aligned(4))); // I've tried both with and without
    uint8_t i_block;     // index block
};

#endif
#endif

record_sd_wav.cpp
Code:
#include "Arduino.h"
#include "record_sd_wav.h"
#include "spi_interrupt.h"

#if defined(__IMXRT1062__) || defined(__MK66FX1M0__) || defined(__MK64FX512__)

void AudioRecordSdWav::begin(void)
{
	state_record = STATE_STOP;
	recByteSaved = 0;
	i_block = 0;

	if (blocklist[0])
	{
		release(blocklist[0]);
		blocklist[0] = NULL;
	}
	if (blocklist[1])
	{
		release(blocklist[1]);
		blocklist[1] = NULL;
	}
}

bool AudioRecordSdWav::record(const char *filename)
{
	stop();
	AudioStartUsingSPI();
	__disable_irq();
	wavfile = SD.open(filename, FILE_WRITE);
	__enable_irq();
	if (!wavfile)
	{
		AudioStopUsingSPI();
		return false;
	}
	state_record = STATE_REC;
	recByteSaved = 0;
	i_block = 0;
	return true;
};

void AudioRecordSdWav::stop(void)
{
	__disable_irq();
	if (state_record != STATE_STOP)
	{
		write_header();
		wavfile.close();
		release(blocklist[0]);
		release(blocklist[1]);
		__enable_irq();
		AudioStopUsingSPI();
		state_record = STATE_STOP;
	}
	else
	{
		__enable_irq();
	}
}

void AudioRecordSdWav::update(void)
{
	audio_block_t *block;
	block = receiveReadOnly();
	if (!block)
		return;

	if (state_record == STATE_STOP) // or !wavfile)
	{
		release(block);
		return;
	}
	blocklist[i_block] = block;
	i_block = (i_block + 1) % 2;
	if (!i_block)
	{
		memcpy(buffer + 0x000, blocklist[0]->data, 256);
		memcpy(buffer + 0x100, blocklist[1]->data, 256);
		// only to debug
		for (int i = 0; i < 8; i++)
		{
			Serial.print(buffer[i], HEX);
			Serial.print(" ");
		}
		Serial.println();
		// end only to debug
		__disable_irq();
		wavfile.write(buffer, 512);
		recByteSaved += 512;
		release(blocklist[0]);
		release(blocklist[1]);
		__enable_irq();
	}
}
void AudioRecordSdWav::write_header(void)
{
	Subchunk2Size = recByteSaved;
	ChunkSize = Subchunk2Size + 36;
	wavfile.seek(0);
	// "RIFF" chunck descriptor
	wavfile.write("RIFF"); // ChunkID
	wavfile.write(ChunkSize & 0xff);
	wavfile.write((ChunkSize >> 8) & 0xff);
	wavfile.write((ChunkSize >> 16) & 0xff);
	wavfile.write((ChunkSize >> 24) & 0xff);
	wavfile.write("WAVE"); // Format
	// "fmt" sub-chunck
	wavfile.write("fmt "); // Subchunk1ID
	wavfile.write(Subchunk1Size & 0xff);
	wavfile.write((Subchunk1Size >> 8) & 0xff);
	wavfile.write((Subchunk1Size >> 16) & 0xff);
	wavfile.write((Subchunk1Size >> 24) & 0xff);
	wavfile.write(AudioFormat & 0xff);
	wavfile.write((AudioFormat >> 8) & 0xff);
	wavfile.write(NumChannels & 0xff);
	wavfile.write((NumChannels >> 8) & 0xff);
	wavfile.write(SampleRate & 0xff);
	wavfile.write((SampleRate >> 8) & 0xff);
	wavfile.write((SampleRate >> 16) & 0xff);
	wavfile.write((SampleRate >> 24) & 0xff);
	wavfile.write(ByteRate & 0xff);
	wavfile.write((ByteRate >> 8) & 0xff);
	wavfile.write((ByteRate >> 16) & 0xff);
	wavfile.write((ByteRate >> 24) & 0xff);
	wavfile.write(BlockAlign & 0xff);
	wavfile.write((BlockAlign >> 8) & 0xff);
	wavfile.write(BitsPerSample & 0xff);
	wavfile.write((BitsPerSample >> 8) & 0xff);
	// "data" sub-chunk
	wavfile.write("data"); // Subchunk2ID
	wavfile.write(Subchunk2Size & 0xff);
	wavfile.write((Subchunk2Size >> 8) & 0xff);
	wavfile.write((Subchunk2Size >> 16) & 0xff);
	wavfile.write((Subchunk2Size >> 24) & 0xff);
}
#endif

main.h
Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
//#include <SD_t3.h> // I've tried both with and without
#include <SerialFlash.h>

#include "record_sd_wav.h"

//AudioInputI2S rec_source;
AudioSynthWaveformSine rec_source;
AudioRecordSdWav sdWav;

AudioOutputI2S out_i2s; // <- Why do I need this?

AudioConnection patchCord(rec_source, sdWav);

void setup()
{
    AudioMemory(60);
    rec_source.amplitude(1);
    rec_source.frequency(432);

    Serial.begin(115200);
    while (!Serial)
    {
    }
    // Initialize the SD card
    while (!(SD.begin(BUILTIN_SDCARD)))
    {
        Serial.println("Unable to access the SD card");
        delay(500);
    }
    Serial.println("Start recording");
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWriteFast(LED_BUILTIN, HIGH);
    Serial.println(sdWav.record("test.wav"));
    delay(3000);
    sdWav.stop();
    digitalWriteFast(LED_BUILTIN, LOW);
    Serial.println("Stop recording");
    delay(1000);
    // print wav file on Serial Monitor
    File wavfile = SD.open("test.wav");
    Serial.println("header:");
    for (int i = 0; i < 44; i++)
    {
        int value = wavfile.read();
        if (value < 16)
            Serial.print("0");
        Serial.print(value, HEX);
        if (i % 4 == 3)
            Serial.println();
        else
            Serial.print(" ");
    }
    Serial.println("\ndata:");
    for (int i = 0; i < 20; i++)
    {
        int value = wavfile.read();
        if (value < 16)
            Serial.print("0");
        Serial.print(value, HEX);
        if (i % 4 == 3)
            Serial.println();
        else
            Serial.print(" ");
    }
    Serial.println("\n\n");
}
bool var = true;
void loop()
{
    delay(100);
}

Part of Serial Monitor output:
Code:
2D 80 D5 80 F5 81 92 83 
58 7F 4F 7E CE 7C D1 7A 
6E 81 D8 82 B9 84 14 87 
7E 7D B3 7B 73 79 BB 76 
E1 83 B 86 A8 88 BC 8B 
Stop recording
header:
52 49 46 46
24 6E 04 00
57 41 56 45
66 6D 74 20
10 00 00 00
01 00 01 00
44 AC 00 00
88 58 01 00
02 00 10 00
64 61 74 61
00 6E 04 00

data:
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00

If I connect the sine with a DIY I2S DAC (with PCM5100) it feels great and if I print in the serial monitor part of the recorder buffer you can see that the values are not 0, so I think it's a saving problem on the SD card, but the header saves it correctly, so what can it be?
I also realized that if I don't declare an output, my custom class doesn't update it, why?

Thanks a lot.
 
For your last question:

I also realized that if I don't declare an output, my custom class doesn't update it, why?

You will see the same with USB. The library need at least an input or output (best is I2s) - it uses it as a timer to be able to call the internal update() (i.e. your AudioRecordSdWav::update(void) ) in the right intervals.
 
My friend successfully tried the sketch with Teensy 3.6 and microSDHC Class6 Traxdata 4GB.
Part of Serial Monitor output:
Code:
header:
52 49 46 46
24 8C 05 00
57 41 56 45
66 6D 74 20
10 00 00 00
01 00 01 00
55 AC 00 00
AA 58 01 00
02 00 10 00
64 61 74 61
00 8C 05 00

data:
80 7E 74 7F
EF 7F EC 7F
6E 7F 73 7E
00 7D 12 7B
AE 78 D4 75
Unlike the previous output, the sampling fequency is 44117 Hz instead of 44100Hz. As a result, the sample size increased from 3.3s to 4.1s. In this second case the frequency is perfect at 432Hz.

I'm getting more and more confused.
 
Thank you Frank for your always quick response. But I'd already read the documentation, and what I don't understand is something else.
How can it be that a powerful and precise board like Teensy 4.0 can't write on a high performance SD card, while a "worse" board with a bad SD card can do it with perfect frequency? Also recording a longer sample (I know, delay() is not very precise)
 
Oh I'm sure it can (sorry, havn't looked at your sourcecode).
@Others: Where is the problem?

Maybe you can upload a working sketch here (as zip) - makes it easier for us!
 
I don't see how zipping it makes any difference. Anyway, I did.
View attachment 19532

How gracious of you to help us in our search for your bug!
However, it displays this on a Teensy 4:

Code:
8B 87 70 8A C5 8D 8B 91 

34 76 F4 72 47 6F 2C 6B 

53 8C EC 8F EE 93 5B 98 

DE 70 EE 6C 9A 68 DC 63 

71 44 F6 4A 34 51 21 57 

Stop recording

header:

52 49 46 46

24 30 06 00

57 41 56 45

66 6D 74 20

10 00 00 00

01 00 01 00

44 AC 00 00

88 58 01 00

02 00 10 00

64 61 74 61

00 30 06 00


data:

2A 4A 9B 43

CB 3C C0 35

81 2E 14 27

83 1F D2 17

0A 10 33 08
 
No reader. My own dev-board - it's just a socket on the board.
You're writing in the update() method. That means you have only 2.8ms to write. If the SD Card decides to stall (due to internal wear-levelling or other magical things SD use to do) you'll have more than that 2.8ms. You know that SD-Cards have a internal CPU?
Maybe it does not work for you becaue of that(?) - I used a brandnew card.
Anyway, mid-term you will not have much fun with writing in update().

Use google, you'll find many threads about this behaviour. Worst-case it can be several hundred milliseconds. You problems will start with only 1-2 millisconds...

Using the queue is the _perfect_ way to do it. Together with large AudioMemory (as large as possible - if still not enough use extra arrays)
 
Last edited:
No reader. My own dev-board - it's just a socket on the board.
So you use pins 34-39 to manage the SD card too?

I measured the write times on the microSD (practically new) and although they are very variable, they never reach the millisecond.
Here's how I made the measurement:
Code:
uint32_t m = micros();
__disable_irq();
wavfile.write(buffer, 512);
recByteSaved += 512;
release(blocklist[0]);
release(blocklist[1]);
__enable_irq();
uint32_t mm = micros();
Serial.println(mm - m);

Also, doesn't the __disable_irq() function disable all interrupts, avoiding writing problems? Or do I only have 2.8ms anyway? Why?

Anyway, today I managed to recover another SD card ( ScanDisk Industrial 8GB, which one did you use?)and magically with this one it works. It remains for me to understand why times are so different between teensy 4.0 and teensy 3.6.
 
If it takes longer you'll just loose Audio data and this makes your recording worthless.
I don't mind if you don't believe me that it can take a long time. It's your program, not mine, just do what you want :)

It does not matter that you see short times.You'll see it sooner or later. The brand of the card does not matter that much (all are from china, only a few manufacturers)
Some brands may be better - but even they the use different chips for the "same" cards.

You'll have 2.8ms because after 2.8ms the buffers are full and the interrupts are called. If you stop the interrupts longer, you loose data.
 
Preamble: I'm trying to understand, I don't want to impose my sketch on anyone, or say that it's the best.

Can you confirm that you use pins 34-39 (same pins, same hardware buffer, I think)?
I'm not interested in the brand of SD card, but the technical characteristics. I would probably be able to understand as much as possible which are the factors that determine a correct functioning and which are not.

You say that if it takes more than 2.8ms the interrupts are called (ok, I believe you), but are they called even if I use the __disable_irq() function ? If yes, why if I use queue and write in the loop shouldn't the interrupts be called damaging the writing?

If you stop the interrupts longer, you loose data.

Are you sure about this? In industrial anthropomorphic robots interrupts are queued according to priority, but they never "jump", they are just put on hold (I know, in the audio field it's not a good thing). Isn't it the same on Teensy?
 
Yes, I'm sure about this. 1. there is no queue, only a flag. 2. where should the new data be stored? magical memory/ram from deep space? :)
You're delaying the interrupt. If it misses one call because the "old" one is still running you'll loose data.

Edit: Yes i'm using these pins - hey I used your sketch - it would not work with other pins - right?

Edit: To explain it a bit better:

You'll delay the update() of the whole library. If the delay is too long, all other updates() will be in trouble.
As an experiment, open one of the examples with audible output, and add a delay(3) to one (does not matter which) of the update()-functions used.
This will crash soon, maybe at the 2nd call of the update() chain and you will hear nothing. If it is only sometimes longer than 2.8 ms then it can run, but the sound will be distorted - in case of a output, the DMA will send old, not updated data to the speaker - in case of an input it outputs old data to the following objects.
 
Last edited:
Status
Not open for further replies.
Back
Top