This tutorial introduces the display of audio waveforms using the AudioThumbnail class. This provides an easy way of drawing any number of waveforms within your audio applications.
LEVEL: Intermediate
PLATFORMS: Windows, macOS, Linux
CLASSES: AudioThumbnail, AudioThumbnailCache, AudioFormatReader, ChangeListener
This tutorial leads on from Tutorial: Build an audio player, which you should have read and understood first. It also assumes that you are familiar with the Graphics class and the Component::paint() function for performing drawing within a component (see Tutorial: The Graphics class).
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 presents three buttons in the same way as Tutorial: Build an audio player (for opening, playing, and stopping a sound file).
There is also a rectangular area where the waveform from the sound file can be drawn. In its default state (with no sound file loaded) the application looks like this:

Once a sound file is loaded, the application looks like this:

Drawing an audio waveform, especially for long files, generally involves storing a low resolution version of the audio data in a format that makes drawing the waveform efficient and also clear to the user. The AudioThumbnail class handles this low resolution version for you and it is created and updated when needed.
The first important point is that the AudioThumbnail class is not a subclass of the Component class. The AudioThumbnail class is used to perform the drawing of the audio waveform within the paint() function of another Component object. The code below shows how to add this functionality based on the demo project from Tutorial: Build an audio player.
In our MainContentComponent class we need to add two members: an AudioThumbnailCache object and an AudioThumbnail object. The AudioThumbnailCache class is used to cache the necessary low resolution version of one or more audio files. This means, for example, if we close a file, open a new file, then return to open the first file, the AudioThumbnailCache will still contain the low resolution version of the first file and won't need to rescan and recalculate the data. Another useful feature is that AudioThumbnailCache objects can be shared between different instances of the AudioThumbnail class
juce::TextButton openButton;
juce::TextButton playButton;
juce::TextButton stopButton;
std::unique_ptr<juce::FileChooser> chooser;
juce::AudioFormatManager formatManager; // [3]
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource;
TransportState state;
juce::AudioThumbnailCache thumbnailCache; // [1]
juce::AudioThumbnail thumbnail; // [2]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
If statically allocated objects like this are used, it is important that the AudioThumbnailCache object [1] is listed before the AudioThumbnail object [2] since it is passed as an argument to the AudioThumbnail constructor. It is also important that the AudioFormatManager object [3] is listed before the AudioThumbnail object for the same reason.
In the initialiser list for the MainContentComponent constructor we set up these objects:
MainContentComponent()
: state (Stopped),
thumbnailCache (5), // [4]
thumbnail (512, formatManager, thumbnailCache) // [5]
{
The AudioThumbnail class is also a type of ChangeBroadcaster class. We can register as a listener for changes [6](in our MainContentComponent constructor). These changes will be when the AudioThumbnail object has changed such that we need to update our drawing of the waveform.
thumbnail.addChangeListener (this); // [6]
In our changeListenerCallback() function we need to determine whether the change is being broadcasted from the AudioTransportSource object or the AudioThumbnail object:
void changeListenerCallback (juce::ChangeBroadcaster* source) override
{
if (source == &transportSource)
transportSourceChanged();
if (source == &thumbnail)
thumbnailChanged();
}
The transportSourceChanged() function just contains our original code for responding to changes in the AudioTransportSource object:
void transportSourceChanged()
{
changeState (transportSource.isPlaying() ? Playing : Stopped);
}
If it is the AudioThumbnail object that has changed, we call the Component::repaint() function. This will cause our paint() function to be called during the next screen drawing operation:
void thumbnailChanged()
{
repaint();
}
When we open the sound file successfully we also need to pass the file to the AudioThumbnail object [7] within a FileInputSource object.
void openButtonClicked()
{
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file 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 {})
{
auto* reader = formatManager.createReaderFor (file);
if (reader != nullptr)
{
auto newSource = std::make_unique<juce::AudioFormatReaderSource> (reader, true);
transportSource.setSource (newSource.get(), 0, nullptr, reader->sampleRate);
playButton.setEnabled (true);
thumbnail.setSource (new juce::FileInputSource (file)); // [7]
readerSource.reset (newSource.release());
}
}
});
}
In our paint() function, first we calculate the rectangle into which we will draw. Then we check how many channels the AudioThumbnail object contains, which tells us whether we have a file loaded or not:
void paint (juce::Graphics& g) override
{
juce::Rectangle<int> thumbnailBounds (10, 100, getWidth() - 20, getHeight() - 120);
if (thumbnail.getNumChannels() == 0)
paintIfNoFileLoaded (g, thumbnailBounds);
else
paintIfFileLoaded (g, thumbnailBounds);
}
If we have no file loaded then we display the message No File Loaded by passing our paintIfNoFileLoaded() function the Graphics object and the bounds rectangle:
void paintIfNoFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::darkgrey);
g.fillRect (thumbnailBounds);
g.setColour (juce::Colours::white);
g.drawFittedText ("No File Loaded", thumbnailBounds, juce::Justification::centred, 1);
}
The important part is next. If we do have a file loaded we can draw the waveform:
void paintIfFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::white);
g.fillRect (thumbnailBounds);
g.setColour (juce::Colours::red); // [8]
thumbnail.drawChannels (g, // [9]
thumbnailBounds,
0.0, // start time
thumbnail.getTotalLength(), // end time
1.0f); // vertical zoom
}
This covers all the basic points for using an AudioThumbnail object.
Exercise: In practice you will commonly want to display only certain regions of the sound files. It should be clear from the AudioThumbnail::drawChannels() function how simple this is to implement using JUCE. Try modifying the code to display only a specific region of the file.
In this section we will walk you through adding a vertical line to the display that will show the current time position of the file playback.
First of all we need to add the Timer class to our list of base classes [10]:
class MainContentComponent : public juce::AudioAppComponent,
public juce::ChangeListener,
private juce::Timer // [10]
{
public:
Then we need to make the timer callback repaint our component. Make sure this code is added to the private section as you will notice we inherited privately from the Timer class:
void timerCallback() override
{
repaint();
}
In the MainContentComponent constructor we need to start the timer [11]--- every 40ms should be sufficient:
startTimer (40); // [11]
}
Exercise: In fact you could delay starting the timer, by starting it once the file is successfully opened.