AudioPlayQueue playBuffer() taking 2 - 3ms / old blocks not being freed

Status
Not open for further replies.

djex81

Active member
Hello all. I've come across an interesting problem. I'm working on a kick drum generator that uses a sine wavetable. I've been having issues with timing and narrowed it down to AudioPlayQueue playBuffer() taking 2 - 3 milliseconds to complete. Since playBuffer() is in a loop to process the length of the kick drum playBuffer() occurs many times adding up to over 300ms of time used on playBuffer(). I have simplified my code below and have confirmed the code below produces the issue I am having. I am using a Teensy 3.2 clocked to 96mhz. My AudioMemory is set to 48.

Code:
int16_t buffer[AUDIO_BUFFER_SIZE];

while(triggered)
{
	if (sampleCount >= numSamples)
	{
		triggered = false;
		break;
	}
	
	if (bufferCount == AUDIO_BUFFER_SIZE) //128
	{
		int16_t* bufM = audioPlayQueueMono.getBuffer();
		memcpy(bufM, &buffer[0], AUDIO_BUFFER_SIZE << 1);

		audioPlayQueueMono.playBuffer();

		bufferCount = 0;
	}
	
	buffer[bufferCount] = sample;
	
	bufferCount++;
        sampleCount++;
}

I noticed that for some reason old blocks are not being cleared out of the queue. Below is output from a timer of playBuffer() in milliseconds and the corresponding AudioMemoryUsed(). As you can see the first 32 memory blocks complete in 0ms but then after the queue is full at 32 it begins to take 2 - 3 ms for each playBuffer() call. I understand once the queue is full it waits for a block to become free but shouldn't blocks become free before hitting the max of 32? At no point does it ever manage to free any blocks from the queue. I'm not sure what to try next.

Code:
playBuffer() millis: 0 AudioMemoryUsed: 1
playBuffer() millis: 0 AudioMemoryUsed: 2
playBuffer() millis: 0 AudioMemoryUsed: 3
playBuffer() millis: 0 AudioMemoryUsed: 4
playBuffer() millis: 0 AudioMemoryUsed: 5
playBuffer() millis: 0 AudioMemoryUsed: 6
playBuffer() millis: 0 AudioMemoryUsed: 7
playBuffer() millis: 0 AudioMemoryUsed: 8
playBuffer() millis: 0 AudioMemoryUsed: 9
playBuffer() millis: 0 AudioMemoryUsed: 10
playBuffer() millis: 0 AudioMemoryUsed: 11
playBuffer() millis: 0 AudioMemoryUsed: 12
playBuffer() millis: 0 AudioMemoryUsed: 13
playBuffer() millis: 0 AudioMemoryUsed: 14
playBuffer() millis: 0 AudioMemoryUsed: 15
playBuffer() millis: 0 AudioMemoryUsed: 16
playBuffer() millis: 0 AudioMemoryUsed: 17
playBuffer() millis: 0 AudioMemoryUsed: 17
playBuffer() millis: 0 AudioMemoryUsed: 18
playBuffer() millis: 0 AudioMemoryUsed: 19
playBuffer() millis: 0 AudioMemoryUsed: 20
playBuffer() millis: 0 AudioMemoryUsed: 21
playBuffer() millis: 0 AudioMemoryUsed: 21
playBuffer() millis: 0 AudioMemoryUsed: 22
playBuffer() millis: 0 AudioMemoryUsed: 23
playBuffer() millis: 0 AudioMemoryUsed: 24
playBuffer() millis: 0 AudioMemoryUsed: 24
playBuffer() millis: 0 AudioMemoryUsed: 25
playBuffer() millis: 0 AudioMemoryUsed: 26
playBuffer() millis: 0 AudioMemoryUsed: 27
playBuffer() millis: 0 AudioMemoryUsed: 27
playBuffer() millis: 0 AudioMemoryUsed: 28
playBuffer() millis: 0 AudioMemoryUsed: 29
playBuffer() millis: 0 AudioMemoryUsed: 30
playBuffer() millis: 0 AudioMemoryUsed: 31
playBuffer() millis: 0 AudioMemoryUsed: 31
playBuffer() millis: 0 AudioMemoryUsed: 32
playBuffer() millis: 1 AudioMemoryUsed: 32
playBuffer() millis: 2 AudioMemoryUsed: 32
playBuffer() millis: 3 AudioMemoryUsed: 32
playBuffer() millis: 3 AudioMemoryUsed: 32
playBuffer() millis: 3 AudioMemoryUsed: 32
playBuffer() millis: 3 AudioMemoryUsed: 32
playBuffer() millis: 3 AudioMemoryUsed: 32
...
 
I found a sort of work around for now. I set my clock to 24mhz and added in a delayMicroseconds(500) after the playBuffer() call. This fixes the buffer queue and timing issues. However this doesn't seem like the right thing to do? Does this make sense?

I figured I was filling the queue faster than the audio system could process it which lead me to slow things down.
 
You overflowed the queue. I think what you need to do is ensure AudioMemory is 32 or less, so that getBuffer() will block before
the queue can overflow, that will give you flow control. I note the docs in the Audio library tool say "lots of caveats" for this object.
 
You overflowed the queue. I think what you need to do is ensure AudioMemory is 32 or less, so that getBuffer() will block before
the queue can overflow, that will give you flow control. I note the docs in the Audio library tool say "lots of caveats" for this object.

Yeah I figured so. Setting the AudioMemory to 32 or less (without the delay) does not work. getBuffer() does not seem to block properly to stop the queue from overflowing.

See what I'm doing is generating a sine wave from a look up table of values. I have a table of 1024 samples which is 1 cycle and so if I want a sine wave 20 cycles long that will be 20480 samples. Which then means I need to process 160 blocks of 128 samples (20480 / 128 = 160). Adding in the delay of 500 microseconds seems to work ok for now but I noticed any change in code that is in the loop with playBuffer() will throw off the timing and will either overflow the queue or cause audio artifacts because there is to much delay. I then need to manually adjust the delay using trial and error to get it working again. I just wish there was a more elegant way to solve this issue but I can't seem to find one at the moment.
 
I think the whole class needs a good looking at - there are two methods not implemented and the semantics seem
hazy - I presume it should be blocking when the queue is full, and I would like the max queue size to be a parameter
not a fixed constant.

I implemented my own input queue class because I needed sample-by-sample queuing which isn't implemented in
this class, so I'll have a look at this class to see if it can improved easily and if so issue a pull-request.
 
shouldn't blocks become free before hitting the max of 32? At no point does it ever manage to free any blocks from the queue.
It does free blocks, but you immediately consume them again and put them back in the buffer queue. The delay of 2-3ms once it reaches 32 buffers is because at 44100Hz it takes 2.9ms to play the samples in one buffer and therefore it takes 2.9ms to free up each buffer.
Both the getBuffer and playBuffer functions are blocking. getBuffer blocks if it can't provide a free buffer. playBuffer blocks if the queue is full and it has to wait for one buffer to be freed up.
I suspect that you are seeing getBuffer block rather than playBuffer but without seeing your complete code that's only a guess. Another guess is that although you say that "AudioMemory is set to 48", it sure looks like it is actually 32.

Pete
 
It does free blocks, but you immediately consume them again and put them back in the buffer queue. The delay of 2-3ms once it reaches 32 buffers is because at 44100Hz it takes 2.9ms to play the samples in one buffer and therefore it takes 2.9ms to free up each buffer.
Both the getBuffer and playBuffer functions are blocking. getBuffer blocks if it can't provide a free buffer. playBuffer blocks if the queue is full and it has to wait for one buffer to be freed up.
I suspect that you are seeing getBuffer block rather than playBuffer but without seeing your complete code that's only a guess. Another guess is that although you say that "AudioMemory is set to 48", it sure looks like it is actually 32.

Pete

Yeah this is what I figured after thinking about it for a bit. I'm feeding the queue too fast. As for which of the two. getBuffer or playBuffer is blocking I found it is playBuffer that is taking all the time. getBuffer takes 0ms but playBuffer takes 2 - 3ms when the queue reaches the max of 32.

As for the AudioMemory setting I can set it to anything > 32 and it will still max out at 32 because the AudioPlayQueue class in play_queue.h has a max_buffers size constant declared that is set to 32. I've tried manually increasing the max buffer size in order to use more audio memory but it actually makes things worse after a certain amount.
 
If you have something else to do when a buffer is not available, you can call available() which will return false until a buffer is available during which time you can accomplish something else. Then proceed with getBuffer and playBuffer and they won't block.

max_buffers size constant declared that is set to 32
Ooopss. Duh :)

I'm almost certain that getBuffer is blocking you. After you have played the last free buffer, you then create your new block of data in your buffer[]. Then you call getBuffer. But there are no free buffers because one is still being played. When, eventually, a buffer has been played and freed, getBuffer returns it to you and you immediately fill that buffer and call playBuffer again. At this point there's no reason for playBuffer to block so it puts that buffer in the queue which fills the queue again.
Follow it through assuming that AudioPlayQueue only has one buffer allocated instead of 32.

Pete
 
Yeah this is what I figured after thinking about it for a bit. I'm feeding the queue too fast.

I've had a play with the class and realized it would make sense for its max buffer to be a settable
parameter, so that it can play nicely with other classes when being used like this.

If you could, say, set the max buffers to 3 then you'd never be able to jump ahead more than about 10ms
and not hog lots of audio buffers. In many situations I think 1 buffer would be plenty.

I think it would be good if available returned the number of buffers available, so you can monitor the queue draining
if you want.

I also think it might work better if the buffer handling was all hidden from the user, and just the (currently unimplemented)
play() methods were the way to put samples in. available() could report sample count rather than buffer count, and there
would be a setMaxSamples() method
 
If you could, say, set the max buffers to 3 then you'd never be able to jump ahead more than about 10ms
and not hog lots of audio buffers. In many situations I think 1 buffer would be plenty.

It seems from my initial tests you would be correct in reducing the max_buffers to limit the amount of time ahead it buffers. I don't want to say for sure that this is the answer since I have more testing to do however setting the max_buffers to anywhere between 3 - 5 so far seems to work well and I do not even need the delayMicroseconds now. It's counter intuitive to set the max lower as one would think you would need more buffer space but in this case we are setting the max_buffer to something significantly lower in order to purposely fill the queue so getBuffer will block just enough time to keep things from backing up. I hope that makes sense.

With max_buffer set to 3 a single playBuffer() now takes 0 - 1 ms. Much better than 2 - 3ms. Setting the max to 1 buffer will not play any audio. Not sure why.
 
I've added a pull request for some improvements to this class:
https://github.com/PaulStoffregen/Audio/pull/410

Added implementations for the two play() methods and created a new one, setMaxBuffers(), intended to be used once
before adding data to the queue, so the number of blocks used can be limited.

Oh awesome. Looked over the changes and they look good. However did you test if using 1 buffer will actually work? When I tested with max_buffers set to 1 no audio would play at all. It may be because I was using a low pass filter also which seems to want at least 2 buffers by default. The reason I mention it is because in your setMaxBuffers function you set it to 1 if less than 1 which may still end up in a broken state if 1 buffer is insufficient.
 
Ah, yes I think you are right, this type of buffering needs at least blocks to work, will fix.
 
Just to alert @djex81, I've built on @MarkT's update to make a version of AudioPlayQueue that has the option not to stall the application if audio blocks or queue entries run out: you can find this at https://github.com/h4yn0nnym0u5e/Audio/tree/bugfix/play_queue_stall for now. I also raised an issue at https://github.com/PaulStoffregen/Audio/issues/415.

There are three changed files (play_queue.cpp and .h, and updates to the documentation in index.html), and a new example in examples/Queues/PlayQueueDemo/PlayQueueDemo.ino. Note I've preserved the old behaviour by default so existing sketches will behave as before; to achieve non-blocking operation, call queue1.setBehaviour(AudioPlayQueue::NON_STALLING) or similar - this needs to be done for each play queue individually. You will then have to deal with the new return values from play(), getBuffer() and playBuffer() in order to complete operations that couldn't be executed due to a lack of resources. However, this will leave your main application with more time to do stuff, rather than burning cycles waiting for the audio system to catch up...

I'd appreciate it if you test it and post your results, good or bad - I'd rather not submit a PR until someone other than me has given it a thrashing!

Cheers

Jonathan
 
Status
Not open for further replies.
Back
Top