Tutorial: Looping audio using the AudioSampleBuffer class

Table of Contents


This tutorial shows how to play and loop audio stored in an AudioSampleBuffer object. This is a useful basis for sampling applications that manipulate recorded audio data.

Level: Intermediate

Platforms: Windows, macOS, Linux

Classes: AudioBuffer, AudioFormatReader, AudioAppComponent

Getting started

Note
This tutorial assumes you have already completed Tutorial: Build a white noise generator and Tutorial: Build an audio player. If not, you should look at these 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 for this tutorial allows the user to open a sound file, read the whole file into an AudioSampleBuffer object, and play it in a loop. In Tutorial: Build an audio player we played sound files using an AudioFormatReaderSource object connected to an AudioTransportSource object to play the sound. Looping is possible using this method by enabling enabling the AudioFormatReaderSource object's looping flag — using the AudioFormatReaderSource::setLooping() function.

All of the code relevant to the discussion in this tutorial is in the MainContentComponent class of the demo project.

Loading sample data into memory

There are many cases where it is probably better to use the built-in classes for sound file playback. There may be occasions where you need to do this yourself and this tutorial gives an introduction to some of the techniques. Sampler applications commonly load the sound file data into memory like this, especially when the sounds are relatively short (see the SamplerSound class for an example). Synthesising sounds can also be achieved by storing a wavetable in an AudioSampleBuffer object and looping it at an appropriate rate to produce the required musical pitch. This is explored in Tutorial: Wavetable synthesis.

This tutorial also highlights some of the potential multi-threading issues you may encounter when combining access to files, and audio processing on the audio thread. Some of these problems seem simple on the surface but often require carefully applied techniques in order to avoid crashes and audio glitches. These techniques are explored further in Tutorial: Looping audio using the AudioSampleBuffer class (advanced).

Why the length limit?

The demo project limits the length of the sound file you can load to less than 2 seconds. This limit is rather arbitrary, but this is broadly for two reasons:

  1. If the whole file is very large then your computer might run out of physical memory. In a real application, of course, you would be able to use a much higher limit. A 2-second stereo audio file, at a sample rate of 44.1kHz, loaded into an AudioSampleBuffer object, will only occupy 705,600 bytes of memory. (See notes)
  2. Loading even quite short files doesn't take a trivial amount of time.

Regarding point 1: if we exceed the amount of physical memory the computer has, it may start to use virtual memory (that is, secondary storage such as a hard drive). This rather defeats the purpose of loading the data into memory in the first place! Of course the operation may just fail on some devices if it runs out of memory.

Regarding point 2: we keep the example simple by loading the audio data directly in, after the FileChooser::browseForFileToOpen() function has returned the file selected by the user. This means that the message thread will be blocked until all of the audio has been read in from disk into the AudioSampleBuffer object. Even with short sounds we should really do this on a background thread to keep the user interface as responsive as possible for the user. For long sounds the delay and unresponsiveness will be very noticeable. Adding another (background) thread would add to the complexity of this example. See Tutorial: Looping audio using the AudioSampleBuffer class (advanced) for example of how to load files on a background thread in this way.

Exercise
To keep it simple, demo project doesn't report an error if you try to load a longer file — it just fails. Adding error reporting like this is left for you as an additional exercise.

Reading the sound file

When the user clicks the Open... button they are presented with a file chooser. The whole file is then read into an AudioSampleBuffer member fileBuffer in our MainContentComponent class.

void openButtonClicked()
{
shutdownAudio(); // [1]
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)); // [2]
if (reader.get() != nullptr)
{
auto duration = reader->lengthInSamples / reader->sampleRate; // [3]
if (duration < 2)
{
fileBuffer.setSize (reader->numChannels, (int) reader->lengthInSamples); // [4]
reader->read (&fileBuffer, // [5]
0, // [5.1]
(int) reader->lengthInSamples, // [5.2]
0, // [5.3]
true, // [5.4]
true); // [5.5]
position = 0; // [6]
setAudioChannels (0, reader->numChannels); // [7]
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
}
}

Processing the audio

In the getNextAudioBlock() function the appropriate number of samples is read from our fileBuffer AudioSampleBuffer member and written out the the AudioSampleBuffer object in the AudioSourceChannelInfo struct.

While reading the audio data from the file we keep track of the current read position using the position member (being careful to update it after all the channels of the audio have been processed for the specified block of samples):

void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
{
auto numInputChannels = fileBuffer.getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
auto outputSamplesRemaining = bufferToFill.numSamples; // [8]
auto outputSamplesOffset = bufferToFill.startSample; // [9]
while (outputSamplesRemaining > 0)
{
auto bufferSamplesRemaining = fileBuffer.getNumSamples() - position; // [10]
auto samplesThisTime = jmin (outputSamplesRemaining, bufferSamplesRemaining); // [11]
for (auto channel = 0; channel < numOutputChannels; ++channel)
{
bufferToFill.buffer->copyFrom (channel, // [12]
outputSamplesOffset, // [12.1]
fileBuffer, // [12.2]
channel % numInputChannels, // [12.3]
position, // [12.4]
samplesThisTime); // [12.5]
}
outputSamplesRemaining -= samplesThisTime; // [13]
outputSamplesOffset += samplesThisTime; // [14]
position += samplesThisTime; // [15]
if (position == fileBuffer.getNumSamples())
position = 0; // [16]
}
}
Exercise
Add a level slider to control the audio playback level of the audio file (see Tutorial: Control audio levels). You can use the AudioSampleBuffer::applyGain() or AudioSampleBuffer::applyGainRamp() functions to apply the gain to the data in an AudioSampleBuffer object.

Multithreading issues

As discussed previously, this tutorial avoids multithreading issues by shutting down and restarting audio each time the user clicks the Open... button. But what if we didn't do this — what could happen? There many things that could go wrong, all of which have to do with the fact that both the getNextAudioBlock() and openButtonClicked() functions could be running at the same time in different threads. Here are some examples:

Warning
There are a number of other things that could go wrong. You may be familiar with using a critical section to synchronise memory access between threads. This is just one possible solution but care should be taken using a critical section in audio code as it can lead to priority inversion which could cause audio drop outs. We look at a solution that avoids critical sections in Tutorial: Looping audio using the AudioSampleBuffer class (advanced).

Notes

Two seconds of stereo audio at 44.1kHz would use 705,600 bytes in an AudioSampleBuffer object because there are:

Multiply these together and the result is: 2 x 2 x 44100 x 4 = 705600

Summary

In this tutorial we have introduced:

See also