Reading SD not fast enough for 44.1 kHz music player

Hello,

I’m stuck trying to build an audio player that reads from SD card (INPUT) and plays music onto a speaker (OUTPUT). I am using an UNO, a R2R ladder DAC and some buffers and filter to get the best out of it. The DAC is fed by a shift register - if possible at a speed of 44100 Hz (1 Byte / 22,5 µs).

To make the program run fluently time tInput (from SD) must be less than time tOutput (from shift register).

For the output I’m using an altered shiftOut function that uses portManipulation and arduinos timer0 which is set to the appropriate timing. Outputting one byte takes less than 10 µs with the altered shiftOut this way, so its pretty fast.

For reading the SD card I use SPI and I tried using a simple read command on my SD object to get the byte value that i wanted to shift out.

shift_out(&PORTB, dataPin, clockPin, latchPin, MSBFIRST, audioFile.read());

But it was not fast enough. The song played slower than it should because the reading took too long and elongated the timer ISR, decreasing the output frequency.

Anyone here ever had this problem before? Is there a way to read the SD card faster? (I’m already using SdFat lib, but it’s still not fast enough).

Nice regards!

edit: Spend the last couple of hours trying to make it work by reading the bytes from the SD card to a buffer instead of one by one, because I thought having the overhead after every single byte is the reason for the big latency. And I actually successfully have increased the output speed by a large factor doing so, BUT this way the output speed somehow becomes dependent on the size of the buffer and I am not able to adjust to 44.1 kHz precisely enough.

You do realize that specialized hardware for playing audio exists, right?

The Arduino is a microcontroller: it was designed to drive LEDs, motors, read sensors, buttons, and the like, it is just not suited for audio.

Pieter

Anyone here ever had this problem before?

Everyone.

Is there a way to read the SD card faster?

The basic read no, but a buffer is the right idea.

BUT this way the output speed somehow becomes dependent on the size of the buffer and I am not able to adjust to 44.1 kHz precisely enough.

You need to send the samples to the D/A as an interrupt task controlled by a timer. Then in the interim you read the SD card and make sure the buffer is topped up.

Hi!

thanks for your answers.

PieterP:
You do realize that specialized hardware for playing audio exists, right?

Since I already started this project with the arduino I will stick to it. The results aren’t that bad for now. Just out of curiosity though, do you mind to specify at little bit, so I know better the next time? I am not so conversant with this whole audio subject yet.

Grumpy_Mike:
You need to send the samples to the D/A as an interrupt task controlled by a timer. Then in the interim you read the SD card and make sure the buffer is topped up.

Yes that’s what I did. But it took me several hours to figure out how to make the output frequency solely dependent on the timer settings. Because changing the size of the buffer made it faster when being increased and slower when being reduced. But the matter has resolved itself - here’s how I archived the desired effect, if someone is interested.

#include <BlockDriver.h>
#include <FreeStack.h>
#include <MinimumSerial.h>
#include <SdFat.h>
#include <SdFatConfig.h>
#include <SysCall.h>
#include <SPI.h>

SdFat sd;
SdFile audioFile;
uint8_t buf1[1000];
int i = 0;
int j = 0;
int u = 1;

void setup() {
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);

    
  Serial.begin(9600);
  SPI.begin();
  sd.begin(4, SPI_FULL_SPEED);

  audioFile.open("tune.wav");

    audioFile.read(buf1, sizeof(buf1));
    
/* timer setup yada yada


... */
   
   
}

ISR(TIMER0_COMPA_vect) 
{   
    
    shift_out(&PORTB, dataPin, clockPin, latchPin, MSBFIRST, buf[j++]);
    
    if(j == 1000)
    {
      j = 0;
      i++;
    }
    
    

}

void loop()
{   

    if(u == i)
    {
      audioFile.read(buf1, sizeof(buf1));
      u++;
    }
}

The variable i is incremented whenever one full buffer is being shifted out and u is incremented when one full buffer has been filled. This way read fill the array and then the mainLoop waits until it’s been shifted before reading the next chunk of bytes. Only requirement here is that the buffer size is big enough so that tRead < tShift. Because the overhead takes the largest portion of time to be read and the actual data bytes less than a microsecond, this will be the case at one point, since shifting out one byte takes a few microseconds.

Cheers

Well that is not what I said.
You should not read a whole buffer at a time, you should only read enough to top up the buffer, I am surprised that works. However given the distortion a home made R/2R will give you maybe you don't notice it.

OP: I'm trying to chop down the largest tree in the forest with a herring, but I'm not having much luck. Does anyone know how to chop down a tree with a herring?

Guru: No, a herring is a terrible tool for chopping down a tree. Get an axe. They work much better.

OP: Well, I've already got the herring so I'm just going to stick with that. Now, can one of you make it so I can cut down a tree with it...

SMDH

Now, can one of you make it so I can cut down a tree with it...

You just need a herring aid.

Thanks for the funny replys. I wired a pair of headphones to it and the audio signal actually sounds pretty undistorted IMO. But yeah since it's only a 8 Bit R2R quality must not be perfect anyway.

Grumpy_Mike:
You should not read a whole buffer at a time, you should only read enough to top up the buffer

I didn't know you could top up the buffer. The reason I did it this way is because I measured the timing with micros() beforehand and it showed that reading 1 Byte from the SD took about 900 µs and reading 1000 Byte took about 1400 µs, so I thought the overhead takes about 900 µs. That's why I read in chunks - as many bytes at once as I can, though I am aware that there will be a very short period of time in which I read unupdated data this way. But the bigger the buffer the more negligible the effect becomes. Is there a certain command to top up the buffer? And would it be as fast?

Is there a certain command to top up the buffer?

No you have to see how many bytes you need to read and then read them into the buffer. When the buffer is long enough making it longer does not produce any improvement.

But yeah since it’s only a 8 Bit R2R quality must not be perfect anyway.

I didn’t mean the fact that it is only 8 bits, I meant that most home made R2R ladders do not use resistors of close enough tolerances to warrant 8 bits. In other words it will not be a monotonic output. That means that the analogue output is not guaranteed to be higher for a higher digital input. This occurs around transitions between say 0111 1111 and 1000 0000, or any time you get a new binary bit to the right of the bit pattern.

Grumpy_Mike:
No you have to see how many bytes you need to read and then read them into the buffer. When the buffer is long enough making it longer does not produce any improvement.

But the side effect of this would be that I have to have the overhead whenever I fill up again, no? E.g. I shifted 1/4 of the buffer, then I fill it up again, producing the overhead delay, which means 4 times the delay then when I wait until its empty. While the output surely would be more "right", I would take more runtime (+ finding out how many bytes need to be filled, + jumping to the right position in the buffer) and I'd have less time for something else, say another task. At this point an axe might be better to chop that tree I guess. I had a look around and I might treat myself with an ESP32. What do you think?

Grumpy_Mike:
I didn't mean the fact that it is only 8 bits, I meant that most home made R2R ladders do not use resistors of close enough tolerances to warrant 8 bits. In other words it will not be a monotonic output. That means that the analogue output is not guaranteed to be higher for a higher digital input. This occurs around transitions between say 0111 1111 and 1000 0000, or any time you get a new binary bit to the right of the bit pattern.

Ah, I understand what you mean. Well I used some SMD resistors that I got from my local university. They are 10k and 20k and have no rings on them, so I'm unable to find out their tolerances. But what you say makes sense, never thought of that.

But the side effect of this would be that I have to have the overhead whenever I fill up again, no?

What you can do is to set a trigger point, when the buffer reaches a certain degree of emptiness, say only a quarter left, you then add the equivalence in bytes of three quarters of the buffer.