Tutorial: Draw audio waveforms

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

Getting started

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

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:

The demo project showing its initial state

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

The demo project showing a file opened and displayed using the AudioThumbnail class

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.

Setting up the AudioThumbnail

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.

Additional objects

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.

Initialising the objects

In the initialiser list for the MainContentComponent constructor we set up these objects:

MainContentComponent()
    : state (Stopped),
      thumbnailCache (5), // [4]
      thumbnail (512, formatManager, thumbnailCache) // [5]
{
  • [4]: The AudioThumbnailCache objects must be constructed with the number of thumbnails to store.
  • [5]: The AudioThumbnail object itself needs to be constructed by telling it how many source samples will be used to create a single thumbnail sample. This governs the resolution of the low resolution version. The other two arguments are the AudioFormatManager and AudioThumbnailCache objects, as discussed above.

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]

Responding to changes

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();
}

Opening the file

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());
            }
        }
    });
}

Performing the drawing

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.

Adding a time position marker

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.

Adding a timer

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.

linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram