Audio Library Play Latency

Status
Not open for further replies.

jcarruthers

Well-known member
So my quest is to try and reduce the latency of the PlayRAW function whilst playing via A14/DAC.

It takes 3ms to open a file from the SD card — and my use for this is to play samples repeatedly.

Looking at the code it's not doing anything clever to avoid opening the same file on a second call — so I presume I could add something like "if it's the same file as last time then don't open and just move the offset to 0"

This may save some time?
 
Last edited:
So my quest is to try and reduce the latency of the PlayRAW function whilst playing via A14/DAC.

It takes 3ms to open a file from the SD card — and my use for this is to play samples repeatedly.

Looking at the code it's not doing anything clever to avoid opening the same file on a second call — so I presume I could add something like "if it's the same file as last time then don't open and just move the offset to 0"

This may save some time?


the consensus seems to be that 3ms = 2.9ms, ie due to the audio buffer. sounds plausible, but i don't know. in this case, as noted, a smaller buffer (64/32/16) would be one obvious way to try minimizing the latency. that might or might not be more or less easy, i haven't looked into it. this is from an older thread where Paul suggested that

If you're imagining I actually test the library with anything other than 128 sample blocks, the answer would be no. If "supported" means I think everything ought to work, again no. Most things probably will work, but some of the input or output objects probably don't probably adjust to other block sizes. But if "supported" means I'd accept pull requests to fix such problems, if the fixes didn't negatively impact performance at 128, then sure.

if on the other hand the 3ms are due to opening the file, there's the story that "opening-by-index" (rather than by filename) might improve things. that might or might not be the case. see here, for instance: https://forum.pjrc.com/threads/2794...es-by-index-instead-of-name-to-reduce-latency

other than that, "pre fetching" the data might be another option. chances are there's more, somebody else will have to chime in.

that said. 3ms is already pretty good. i'd be surprised if that's noticeable. though "seeking" back to the start might indeed improve things .. worth trying at least.


edit: i note in the other thread you say "I am getting around 10ms" (total) latency (i suppose). so did those other 7ms already go away, or how did you disentangle the various factors?
 
Last edited:
Just doing and "rawfile = SD.open("file.raw") takes 3ms.

So for sure that is adding to the delay.

why, sure it adds, it was just wondering what happened to the the figure of 10ms.

anyways, i'd imagine some combination of pause() and seek() rather than stop() and play() might well improve things, none of which are part of the API atm of course but aren't difficult add. other than that, smaller buffer and open-by-index might be another/complementary way, as mentioned; it certainly would be a useful addition considering what can happen when people try to put a zillion file names into a String array (cf RadioMusic).
 
I'll start by expanding play(char filename) to also just play() — and add a load(char filename) that doesn't play.
 
Cheers. I didn't have time to look at this yesterday.

The more I think about it the more sense it makes to follow SD card conventions and have open() close()

Then play() and pause() don't do anything with the file side of things.
 
well, there's not much to be learned from issue 109 but I was thinking maybe this might be a good occasion to fix / add some of the stuff that's missing from play_sd_wav / sd_raw . most of it is easily implemented but I'd hesitate to do a pull-request without some prior discussion.

off the top of my head that would include :

seek(const char *filename filename, uint32_t pos)
pause()
resume(uint32_t pos)
positionBytes()
lengthBytes()


re: latency, open/close would be one way, I guess, but as a generic solution i don't know. at least, it probably would break most existing user programs which expect play( ) to also open the file.

my idea would be to maybe leave play ( ) more or less alone, just have it check for the filename, somewhat like so (untested) :

Code:
bool AudioPlaySdWav :: play(const char *filename)
{
	
	AudioStartUsingSPI();
	__disable_irq();

    if (wavfile) {

        if (!strcmp(wavfile.name(), filename)) // = same file
        {	
        	// does this need to release the audio blocks?
        	wavfile.seek(0x0);
		__enable_irq();
		}
        else  // =  file is different
        {
        	stop();
	        wavfile = SD.open(filename);
	        __enable_irq();
	        if (!wavfile) { 
			AudioStopUsingSPI();
			return false;
		}
        }
    }
    else 
    {
    	stop(); 
    	wavfile = SD.open(filename);
	    __enable_irq();
	        if (!wavfile) { 
			AudioStopUsingSPI();
			return false;
		}
    }
 	
	buffer_length = 0;
	buffer_offset = 0;
	state_play = STATE_STOP;
	data_length = 20;
	header_offset = 0;
	state = STATE_PARSE1;
	return true;
}

this would need adjustments to consume(), too, ie so it doesn't close the file when the end is reached but merely goes into STATE_STOP or some other state which could be added (STATE_PAUSE).

in that case, an additional function called resume(uint32_t pos) (or similar), which wouldn't need to be passed the filename might be a good idea; at least on the level of the API it would be more semantically transparent: ie a wavfile can only be resumed if it's already been playing. (I guess the same could be argued for open first, only then play)
 
True, best not to break the current model.

Maybe then add an open(filename) and a play() — alter play(filename) to use the open(filename) if there isn't a file already loaded.
 
We already have a playing state - so that is covered. Don't think there is a need to make a differentiation between stopped and paused.

I reckon it's best not to make any assumptions of what is happening. So play() just plays from wherever you are at*— to make the file go back to the start then you need to do something like seek(0).

Open, play, stop, seek keeps everything very clean.
 
open(const char *filename) makes sense, i guess. generally speaking, sounds as if the question is whether you want the stuff come across like file operations (open, close, etc) or more like a tape machine (pause, resume, etc)


that said, i didn't suggest a lot of assumptions should be made. the reason why i thought pause() and resume() make sense is that as it stands, stop() closes the file, which kind of makes sense (unless one were to add close(), too). playing until the end closes the file, too (instead of say, rewind it) which doesn't make that much sense. either way, play(somefile) will have to reopen the file when it was either A) stopped or B) played until the end.

so some kind of explicit pause() thing makes sense. AFAIS, as far as sd_wav is concerned case B) can only be avoided by changing consume(), so the file won't close. dealing with case A) means either changing the way stop() behaves *and* add close() or simply adding pause(). so i'd say let's add pause().

as for play(filename), yeah, why not. play from wherever things were paused. resume() or just play() (overloaded / without the filename) would still be good to have, i guess, in cases where i don't want to run the check on filename.
 
Last edited:
So as expected, it now gives me a minimum of 6ms latency — maximum of 8.5ms or so.

The latency progressively gets smaller until it gets to 6ms at which point it jumps back to 8.5ms. Presume this is in time with the audio update.

Code:
bool AudioPlaySdRaw::play()
{

	if (!rawfile) {
		Serial.println("no sample loaded");
		return false;
	}

	AudioStartUsingSPI();
	rawfile.seek(0);
	file_offset = 0;
	playing = true;
	return true;
}

Code:
bool AudioPlaySdRaw::open(const char *filename)
{
	stop();
	rawfile = SD.open(filename);
	Serial.println("opening file");
	if (!rawfile) {
		return false;
	}
	//file_name = filename;
	file_size = rawfile.size();
	file_offset = 0;
	Serial.println(file_size);
	return true;
}
 
As seen here:

IMG_5336.jpg
 
The SD Card needs ~1ms to address a block.
The FAT must be read (adressing+reading), then the file (another addressing+reading) - minimum ~2ms

Some cards are a little bit faster, but not much.
 
neat. so how did you deal with the update() function? ie when rawfile.available() is false?

just kick out the rawfile.close()? or are you always retriggering?
 
Just curious, does the prior sample play up until the new sample is played?
Or is there silence that corresponds to the delay?
 
Frank - so am I not gaining anything by not opening every time?

Mxxx - I just commented out the .close()

Linuxgeek - the sample is very short and finishes before it gets triggered again
 
If fat is slowed down by addressing etc - how about copying it from FAT and in to the last few Mb of the SD card using the direct SD read library?
 
Changing AUDIO_BLOCK_SAMPLES to 16 in the core AudioStream.h file gives me a latency of around 1.5ms

Changing to 32, 64 etc does as expected and increases it proportionally.

Is there any way of defining AUDIO_BLOCK_SAMPLES in my main file rather than going and editing the core file?

*EDIT* having it set on 16 caused a weird thing to happen to the output when two samples were played at the time via playRaw1 and playRaw2. A kind of buzzing noise. Setting it on 32 solved that — this gives a latency of around 3ms so I am happy with that.
 
Last edited:
I've been playing with my code a bit more and found that setting a low buffer seems to make the Teensy audio library crash when you hit it with a lot of .play() really quickly. Usually leaving only one of the AudioPlaySdRaw instances working — sometimes taking them both out. (I have two running at the same time)

Not sure why that is or how to diagnose it.

I put a rate limiter in to the code to stop it from happening too often.
 
I've been playing with my code a bit more and found that setting a low buffer seems to make the Teensy audio library crash when you hit it with a lot of .play() really quickly. Usually leaving only one of the AudioPlaySdRaw instances working — sometimes taking them both out. (I have two running at the same time)

Not sure why that is or how to diagnose it.

happens here, too, when i try this w/ a modestly complex app. i was assuming that's simply because you effectively minimize / successively half the time you have left for your application code. like with blocksize = 16, that'll be 363 us, 32 =727 us, etc.
 
Makes sense.

The odd thing is it will often work for a bit and then crash.

And sometimes only on one object.

So I wonder if there is a way to prod it back to life.
 
... found that setting a low buffer seems to make the Teensy audio library crash when you hit it with a lot of .play() really quickly.

The problem might be in the SD library.

First, install Teensyduino 1.24, if you're using an older copy. That'll give you the latest code.

Find the file SD_t3.h and uncomment this:

Code:
// This Teensy 3.x optimized version is a work-in-progress.
// Uncomment this line to use the Teensy version.  Otherwise,
// the normal SD library is used.
//#define USE_TEENSY3_OPTIMIZED_CODE

When you upload again, you should see this increases RAM usage by about 3k, because it caches more than 1 sector.

Please let me know if that makes a difference?
 
Hi Paul,


I did install the latest SD library and uncomment that line —*it certainly improved what I was trying to do before.

I'll double check when I get home.


James
 
Status
Not open for further replies.
Back
Top