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
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 the first header file in the Projucer.
If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.
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.
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).
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:
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.
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]
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file shorter than 2 seconds to play...",
juce::File {},
"*.wav");
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;
chooser->launchAsync (chooserFlags, [this] (const juce::FileChooser& fc) {
auto file = fc.getResult();
if (file == juce::File {})
return;
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file)); // [2]
if (reader.get() != nullptr)
{
auto duration = (float) reader->lengthInSamples / reader->sampleRate; // [3]
if (duration < 2)
{
fileBuffer.setSize ((int) 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, (int) reader->numChannels); // [7]
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
});
}
getNextAudioBlock() function will be called on the audio thread while we are still within the call to the Button::onClick lambda function (which will have called this openButtonClicked() function from the message thread).reader pointer is not a nullptr value on the next line.fileBuffer member using the AudioFormatReader::read() function. The arguments are:position member to zero.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 juce::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 = juce::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]
}
}
outputSamplesRemaining variable stores the total number of samples that the getNextAudioBlock() function needs to output taking a copy from the AudioSourceChannelInfo struct. We use this to check if we need to exit the while() loop that starts on the next line.while() loop we need to output the smaller of the remaining samples for this call to the getNextAudioBlock() function and the remaining samples in the buffer --- using the jmin() function. If this is less than the total number of samples for this call to the getNextAudioBlock() function, then there will be one more pass of the while() loop, before exiting.outputSamplesRemaining variable.outputSamplesOffset by the same amount in case we have another pass of the while() loop.position member by the same amount too.position member reached the end of the fileBuffer AudioSampleBuffer object and reset it to zero to form the loop if necessary.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.
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:
getNextAudioBlock() function could be interrupted by code in the openButtonClicked() function. Suppose this happens just after [11]and that the openButtonClicked() function has just reached [4]. The buffer might be resized to be shorter than it was but we already calculated our starting point a few lines earlier. This could lead to a memory access error and the application could crash.getNextAudioBlock() function could be interrupted while calling the AudioSampleBuffer::copyFrom() function. Again depending in the implementation of this we could end up accessing memory that we shouldn't.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).
Two seconds of stereo audio at 44.1kHz would use 705,600 bytes in an AudioSampleBuffer object because there are:
float type)Multiply these together and the result is: 2 x 2 x 44100 x 4 = 705600
In this tutorial we have introduced: