Synchronising audio data read/reload

I have the following situation:

I have a straightforward SampleReader object that plays back an audio snippet by copying frames of audio from a pre-loaded AudioBuffer (containing the audio data) into the host’s bufferToFill on the audio thread.

At the same time, I have a SampleLoader object that might, at any time, load new data into said AudioBuffer on another thread, or delete that data.

Of course, I need to synchronise the two such that SampleLoader doesn’t overwrite/delete the audio data while the SampleReader is reading it.

If the SampleLoader is currently manipulating the data, the SampleReader should simply return from the audio callback and do nothing.

Obviously, the SampleReader is not allowed to do any locking or other non-realtime-safe operation.

What is the recommended JUCE way to do this these days?

  • use std::mutex::lock() in the SampleLoader and std::mutex::try_lock() in the SampleReader?
  • use juce::SpinLock::enter() in the SampleLoader and juce::SpinLock::tryEnter() in the SampleReader?
  • something else?

I might have one hundred of those SampleReaders going on simultaneously, so whatever try-lock-ish operation they do in their audio callback, it will be called a lot.

Cheers,
Timur

An alternative to this whole try-lock approach is a reference-counted buffer, as in this tutorial: https://docs.juce.com/master/tutorial_looping_audio_sample_buffer_advanced.html

However, this deals with a simple app that only plays one audio file at a time. Does anyone has experience with this approach in a pro application that deals with many buffers simultaneously?

What about an atomic flag that you set up in the SampleReader as high during reading, and low after finishing, so SampleLoader only access when that flag is low and just return when it’s high? Then you should only need to check it each time you try to load new audio into the AudioBuffer.

Yes, except that you also need to handle the case if the flag is high as the SampleLoader tries to modify the buffer. For example, by waiting in the SampleLoader until it becomes low.

And at that point you have exactly the juce::SpinLock approach, except that you have implemented the spinlock yourself.

Or did I not understand your suggestion correctly?

1 Like

Okay I just read the other way around in an unidirectional manner (that your SL would just return if the SR was reading).

At the same time, I have a SampleLoader object that might, at any time, load new data into said AudioBuffer on another thread, or delete that data.

I’m using the reference counted buffer aproach to load files in a background thread and grant my voices access to those files to be played in the audio thread.

This is cobbled up very quickly and I’m sure there’s a pretty immediate reason why it’s totally flawed - but who knows…

(1) Implement a DoubleBuffer class that has write access to both buffers and read access only to the “front” buffer, plus a “flip” method, that switches the front and back buffer.
At cruise speed, the audio thread reads happily from all the “front” buffers.

Our dream is to have the flip method called only from the audio thread

(2) Now when the sound writer needs to update a sound, it writes audio to the “back” buffer of say DoubleBuffer no. 101, then enqueues a message using a lock free queue , maybe this one … something like:


Message msg {Messages::flip, 101};

messageQueue.enqueue(msg);

(3) before reading/writing audio buffers, the audio thread dequeues a message and calls the flip() method on the corresponding DoubleBuffer .


Message msg;

if(messageQueue.try_dequeue(msg))

{

switch (msg.type) {

case Messages::flip:

{

AudioBuffers[msg.index].flip();

…

I guess You can dequeue more than one incoming message, but since the audio callback is called never less often than every few milliseconds I think you can go with it unless you have to flush all the sounds at once, then you need to find your “compromise point”.

Final remarks

  • this approach could be considered a big waste of memory.

  • there’s still a possible “click” problem when a sound gets read half from a buffer and half from another - an audio smoother for every sound is maybe not economical, maybe you might have a shorter array of smoothers that attach themselves to changing sounds based on what is triggered in the dequeue part of the audio thread

Hope this has been someway helpful.