Tutorial: Looping audio using the AudioSampleBuffer class (advanced)

Table of Contents


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, macOS, Linux

Classes: ReferenceCountedObject, ReferenceCountedArray, Thread, AudioBuffer

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: PIP | ZIP. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: Projucer Part 1: 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)
: 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 = 0;
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 (auto i = buffers.size(); --i >= 0;) // [1]
{
ReferenceCountedBuffer::Ptr buffer (buffers.getUnchecked (i)); // [2]
if (buffer->getReferenceCount() == 2) // [3]
buffers.remove (i);
}
}

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())
{
auto file = chooser.getResult();
std::unique_ptr<AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader.get() != nullptr)
{
auto duration = reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(),
reader->numChannels,
(int) reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, (int) 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:

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;
}
auto* currentAudioSampleBuffer = retainedCurrentBuffer->getAudioSampleBuffer(); // [6]
auto position = retainedCurrentBuffer->position; // [7]
auto numInputChannels = currentAudioSampleBuffer->getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
auto outputSamplesRemaining = bufferToFill.numSamples;
auto outputSamplesOffset = 0;
while (outputSamplesRemaining > 0)
{
auto bufferSamplesRemaining = currentAudioSampleBuffer->getNumSamples() - position;
auto samplesThisTime = jmin (outputSamplesRemaining, bufferSamplesRemaining);
for (auto 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:

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())
{
auto file = chooser.getResult();
auto path = file.getFullPathName();
chosenPath.swapWith (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;
pathToOpen.swapWith (chosenPath);
if (pathToOpen.isNotEmpty())
{
File file (pathToOpen);
std::unique_ptr<AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader.get() != nullptr)
{
auto duration = reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(), reader->numChannels, (int) reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, (int) 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 LoopingAudioSampleBufferAdvancedTutorial_02.h file 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:

See also