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.
Platforms: Windows, Mac OS X, Linux
- This tutorial assumes you have already completed Tutorial: Simple synthesis (noise) and Tutorial: Playing sound files. If not, you should look at these first.
Download the demo project for this tutorial here: tutorial_looping_audio_sample_buffer.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 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: Playing sound files 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
MainComponent.cpp file within the
Source directory 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:
- 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)
- 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 for example of how to load files on a background thread in this way.
- 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
- : Notice that we shut down the audio system for the AudioAppComponent object each time we open a new file. This is to avoid some of the multithreading issues hinted at already. Once the audio system is shut down, there is no danger that our
getNextAudioBlock()function will be called on the audio thread while we are still within the call to the
buttonClicked()function (which will have called this
openButtonClicked()function from the message thread).
- : Here we create the AudioFormatReader object using the AudioFormatManager object. Notice that we store this in a ScopedPointer object as we need to manage this object ourselves. (In Tutorial: Playing sound files we pass the AudioFormatReader object to the AudioFormatReaderSource object to manage for for us.) This operation may fail to create the reader object, object, therefore we have to check that the
readerpointer is not a
nullptrvalue on the next line.
- : This is where we calculate the duration of the sound file by dividing the length of the file in samples by its sample rate. We check that this is less that 2 seconds on the next line.
- : Here we resize the AudioSampleBuffer object by calling the AudioSampleBuffer::setSize() function using the number of channels and length from the AudioFormatReader object.
- : This reads the audio data from the AudioFormatReader object into our AudioSampleBuffer
fileBuffermember using the AudioFormatReader::read() function. The arguments are:
- [5.1]: The destination start sample in the AudioSampleBuffer object where the data will start to be written.
- [5.2]: The number of samples to read.
- [5.3]: The start samples in the AudioFormatReader object where reading will start.
- [5.4]: For stereo (or other two-channel) files this flag indicates whether to read the left channel.
- [5.5]: For stereo files this flag indicates whether to read the right channel.
- : We need to store the most recent read position within our buffer as we play it. This resets our
positionmember to zero.
- : This starts the audio system back up again. Here we have an opportunity to use the number of channels in the sound file to try and configure our audio device with the same number of channels.
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):
- : The
outputSamplesRemainingvariable 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.
- : We also take a copy of the AudioSourceChannelInfo::startSample value to use as our offset within the destination buffer.
- : Here we calculate how many samples are left in the buffer from which we are reading.
- : For this pass of the
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.
- : For each output channel we use the AudioSampleBuffer::copyFrom() function to copy sections of data from one channel of one buffer to a channel of another buffer. Here we specify the destination channel index.
- [12.1]: This is the sample offset within the destination buffer.
- [12.2]: This is the source AudioSampleBuffer object from which to copy.
- [12.3]: This is the channel index of the source buffer. In case the source buffer has fewer channels than our destination buffer we use this modulo calculation. For example, a mono source buffer will mean that this always results in zero, copying the same data to each of the output channels.
- [12.4]: This is the position to start reading from in the source buffer.
- [12.5]: The number of samples to read that we calculated earlier.
- : Now deduct the number of samples we just processed from the
- : Increment the
outputSamplesOffsetby the same amount in case we have another pass of the
- : Offset our
positionmember by the same amount too.
- : Finally, check if the
positionmember reached the end of the
fileBufferAudioSampleBuffer object and reset it to zero to form the loop if necessary.
- Add a level slider to control the audio playback level of the audio file (see Tutorial: Synthesis with level control). 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
openButtonClicked() functions could be running at the same time in different threads. Here are some examples:
- Let's say that the application is already playing an audio file and the user clicks the Open... button and chooses a new file. Suppose the audio thread interrupts this function between  and . The buffer has been resized but the data hasn't been written to the buffer. The buffer may still contain audio data from the previous file but it depends whether the memory for the buffer needed to be moved when it was resized. In any case we'll probably get a glitch.
- It's possible that the
getNextAudioBlock()function could be interrupted by code in the
openButtonClicked()function. Suppose this happens just after  and that the
openButtonClicked()function has just reached . 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:
- 2 channels
- 2 seconds
- 44,100 samples
- 4 bytes-per-sample (using the
Multiply these together and the result is: 2 x 2 x 44100 x 4 = 705600
In this tutorial we have introduced:
- How to read audio data directly from a sound file.
- How to copy the data into a buffer for playback.
- The basis for simple sampler applications and synthesisers using wavetable buffers.
- Some of the potential multithreading issues that exist in audio applications.
Generated on Fri Jan 12 2018 09:51:15 for JUCE by 1.8.13