Tutorial: Looping audio using the AudioSampleBuffer class (advanced)

This tutorial shows how to play and loop audio stored in an AudioSampleBuffer object using thread-safe techniques. A technique for loading the audio data on a background thread is also introduced.

Level: Advanced

Platforms: Windows, Mac OS X, Linux

Classes: ReferenceCountedObject, ReferenceCountedArray, Thread,

Getting started

This tutorial leads on from Tutorial: Looping audio using the AudioSampleBuffer class. If you haven't done so already, you should read that tutorial first.

Download the demo project for this tutorial here: tutorial_looping_audio_sample_buffer_advanced.zip. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: Getting started with the Projucer.

The demo project

The demo project implements similar behaviour to the demo project from Tutorial: Looping audio using the AudioSampleBuffer class. It allows the user to open an audio file that is loaded into a buffer and played in a loop. One major difference in this tutorial is that the audio system is kept running, rather than shutting it down each time we browse for a file. This is achieved by using some helpful classes for communicating between threads in a thread-safe manner.

Thread-safe techniques

You should recall in Tutorial: Looping audio using the AudioSampleBuffer class how we solved the potential problem of the audio thread and the message thread accessing potentially incomplete or corrupted data. Just before we browsed for a file we shut down the audio system. Then, once a file was selected, we opened the file and restarted the audio system. This is clearly an impractical and cumbersome method in a real application!

Reference-counted objects

The ReferenceCountedObject class is a useful tool for passing messages and data between threads. Here, we store our AudioSampleBuffer object and the playback position in a ReferenceCountedObject class. To help with debugging, and to help illustrate how the class works, we also include name member (although this isn't strictly necessary for the class to function):

class ReferenceCountedBuffer : public ReferenceCountedObject
{
public:
ReferenceCountedBuffer (const String& nameToUse,
int numChannels,
int numSamples)
: position (0),
name (nameToUse),
buffer (numChannels, numSamples)
{
DBG (String ("Buffer named '") + name + "' constructed. numChannels = " + String (numChannels) + ", numSamples = " + String (numSamples));
}
~ReferenceCountedBuffer()
{
DBG (String ("Buffer named '") + name + "' destroyed");
}
AudioSampleBuffer* getAudioSampleBuffer()
{
return &buffer;
}
int position;
private:
String name;
};

The typedef at the top of the class is an important part in implementing a ReferenceCountedObject subclass. Rather than storing our ReferenceCountedBuffer object in a raw pointer, we store it in a ReferenceCountedBuffer::Ptr type. It is this that manages the reference count of the object (incrementing and decrementing as necessary) and its lifetime (deleting the object when the reference count reaches zero). We can also store an array of ReferenceCountedBuffer objects using the ReferenceCountedArray class.

In our MainContentComponent class we store both an array and a single instance:

ReferenceCountedBuffer::Ptr currentBuffer;

The buffers member keeps hold of our buffers in the array until we are absolutely sure they are no longer needed by the audio thread. The currentBuffer member holds the currently selected buffer.

Implementing the background thread

Our MainContentComponent class inherits from the Thread class:

class MainContentComponent : public AudioAppComponent,
private Thread
{
//...

This is used to implement our background thread. Our overridden Thread::run() function is as follows:

void run() override
{
while (! threadShouldExit())
{
checkForBuffersToFree();
wait (500);
}
}

Here, we check whether there are any buffers to be freed, then our thread waits for 500ms or to be woken up (using the Thread::notify() function). Essentially, this means that the check will occur at least every 500ms. The checkForBuffersToFree() function searches through our buffers array to see if any buffers can be freed:

void checkForBuffersToFree()
{
for (int i = buffers.size(); --i >= 0;) // [1]
{
ReferenceCountedBuffer::Ptr buffer (buffers.getUnchecked (i)); // [2]
if (buffer->getReferenceCount() == 2) // [3]
buffers.remove (i);
}
}
  • [1]: It is useful to remember to iterate over the array in reverse in these situations. It is easier to avoid corrupting the array index access if we remove items as we iterate over the array.
  • [2]: This retains a copy of a buffer at the specified index.
  • [3]: If the reference count at this point is equal to 2 then we know that the audio thread can't be using the buffer and we can remove it from the array. One of these two references will be in the buffers and the other will be in the local buffer variable. The removed buffer will delete itself as the buffer variable goes out of scope (as this will be the last remaining reference).

Of course, we need to start the thread as our application starts, which we do in our MainContentComponent constructor:

startThread();

Opening the file

Our openButtonClicked() function is similar to the openButtonClicked() function from Tutorial: Looping audio using the AudioSampleBuffer class with some minor differences:

void openButtonClicked()
{
FileChooser chooser ("Select a Wave file shorter than 2 seconds to play...",
File::nonexistent,
"*.wav");
if (chooser.browseForFileToOpen())
{
const File file (chooser.getResult());
ScopedPointer<AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader != nullptr)
{
const double duration = reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(),
reader->numChannels,
reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, reader->lengthInSamples, 0, true, true);
currentBuffer = newBuffer;
buffers.add (newBuffer);
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
}
}

Here the differences are that we:

  • Allocate a new instance of our ReferenceCountedBuffer class.
  • Read the audio data into the AudioSampleBuffer object that it contains.
  • Make it the current buffer.
  • Add it to our array of buffers.

To clear the current buffer we can just set its value to nullptr:

void clearButtonClicked()
{
currentBuffer = nullptr;
}

Playing the buffer

Our getNextAudioBlock() function is similar to the getNextAudioBlock() function from Tutorial: Looping audio using the AudioSampleBuffer class except we need to access our current ReferenceCountedBuffer object and the AudioSampleBuffer object it contains.

void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
{
ReferenceCountedBuffer::Ptr retainedCurrentBuffer (currentBuffer); // [4]
if (retainedCurrentBuffer == nullptr) // [5]
{
bufferToFill.clearActiveBufferRegion();
return;
}
AudioSampleBuffer* currentAudioSampleBuffer (retainedCurrentBuffer->getAudioSampleBuffer()); // [6]
int position = retainedCurrentBuffer->position; // [7]
const int numInputChannels = currentAudioSampleBuffer->getNumChannels();
const int numOutputChannels = bufferToFill.buffer->getNumChannels();
int outputSamplesRemaining = bufferToFill.numSamples;
int outputSamplesOffset = 0;
while (outputSamplesRemaining > 0)
{
int bufferSamplesRemaining = currentAudioSampleBuffer->getNumSamples() - position;
int samplesThisTime = jmin (outputSamplesRemaining, bufferSamplesRemaining);
for (int channel = 0; channel < numOutputChannels; ++channel)
{
bufferToFill.buffer->copyFrom (channel,
bufferToFill.startSample + outputSamplesOffset,
*currentAudioSampleBuffer,
channel % numInputChannels,
position,
samplesThisTime);
}
outputSamplesRemaining -= samplesThisTime;
outputSamplesOffset += samplesThisTime;
position += samplesThisTime;
if (position == currentAudioSampleBuffer->getNumSamples())
position = 0;
}
retainedCurrentBuffer->position = position; // [8]
}

The important changes are:

  • [4]: We retain a copy of the currentBuffer member. After this point in the function it doesn't matter if the currentBuffer member is changed on another thread since we have taken a local copy.
  • [5]: We output silence if the currentBuffer member when we took a copy.
  • [6]: We access the AudioSampleBuffer object within the ReferenceCountedBuffer object.
  • [7]: We get the current playback position for the buffer.
  • [8]: After modifying the current playback position, we store it back in the ReferenceCountedBuffer object.

This algorithm ensures that ReferenceCountedBuffer objects aren't deleted on the the audio thread. It is not a good idea to allocate or free memory on the audio thread. The ReferenceCountedBuffer objects will only be deleted on our background thread.

Reading the audio on the background thread

Our application still reads the audio data on the message thread. This is not ideal since this blocks the message thread and large files could take some time to load. In fact, we can also use our background thread to perform this task.

Passing the file path to the background thread

First, add the following member to the MainContentComponent class:

String chosenPath;

Now change the openButtonClicked() function to swap the full path of the file into this member:

void openButtonClicked()
{
FileChooser chooser ("Select a Wave file shorter than 2 seconds to play...",
File::nonexistent,
"*.wav");
if (chooser.browseForFileToOpen())
{
const File file (chooser.getResult());
String path (file.getFullPathName());
swapVariables (chosenPath, path);
notify();
}
}

Here we also wake up the background thread since we are going to call a function on the background thread to open the file.

Accessing the path from the background thread

Our run() function should be updated as follows:

void run() override
{
while (! threadShouldExit())
{
checkForPathToOpen();
checkForBuffersToFree();
wait (500);
}
}

The checkForPathToOpen() function checks the chosenPath member by swapping it into a local variable:

void checkForPathToOpen()
{
String pathToOpen;
swapVariables (pathToOpen, chosenPath);
if (pathToOpen.isNotEmpty())
{
const File file (pathToOpen);
ScopedPointer<AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader != nullptr)
{
const double duration = reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(), reader->numChannels, reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, reader->lengthInSamples, 0, true, true);
currentBuffer = newBuffer;
buffers.add (newBuffer);
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
}
}

If the pathToOpen variable is an empty string then we know there isn't a new file to open. The remainder of the code in this function should be familiar to you.

Run the application again and it should still function correctly.

Note
The final code for this section can be found in the MainComponent_02.cpp file within the Source directory of the demo project.

Summary

This tutorial has introduced some useful techniques for passing data between threads, especially in an audio application. After reading this tutorial you should be able to:

  • Implement a subclass of the ReferenceCountedObject class.
  • Maintain the lifetime of a ReferenceCountedObject in a multi-threaded application.
  • Implement a background thread to perform tasks such as deleting objects that are no longer needed and performing file reading operations.

See also