Audio Waveform Test

Since I recently started studying some JUCE tutorials and demos, I wanted to create a base JUCE code framework for audio ideas and implementations. I choose the way for multichannel implementations (juce::dsp::ProcessorDuplicator). I created a simple audio player to see audio waveforms with play and load buttons and a gain slider. I set the filter range (DSPSetup.h) 22kHz to get out of the audible range. Later, I will use the same structure to play around some DSP ideas. The amount of relevant docs, tutorials, and demos are very high on the internet. You can download AudioWaveformTest.zip here (zipped). I just kept declarations and definitions in the header files for readability and organizing files.

AudioThumbnailComponent.h

#pragma once

#include <JuceHeader.h>

//==============================================================================
class AudioThumbnailComponent final : public juce::Component,
    public juce::ChangeBroadcaster,
    private juce::ChangeListener,
    private juce::Timer
{
public:
    AudioThumbnailComponent(juce::AudioDeviceManager& adm, juce::AudioFormatManager& afm)
        : audioDeviceManager(adm),
        thumbnailCache(5),
        thumbnail(128, afm, thumbnailCache)
    {
        thumbnail.addChangeListener(this);
    }

    ~AudioThumbnailComponent() override
    {
        thumbnail.removeChangeListener(this);
    }

    void paint(juce::Graphics& g) override
    {
        g.fillAll(juce::Colours::lightblue);  // Thumbnail block background colour
        g.setColour(juce::Colours::darkgrey); // Waveform colour

        if (thumbnail.getTotalLength() > 0.0)
        {
            thumbnail.drawChannels(g, getLocalBounds().reduced(2), 0.0, thumbnail.getTotalLength(), 1.0f);

            g.setColour(juce::Colours::red);  // Play line colour
            g.fillRect(static_cast<float> (playLinePosition * getWidth()), 0.0f, 1.0f, static_cast<float> (getHeight()));
        }
        else
        {
            g.setColour(juce::Colours::black);
            g.drawFittedText("No audio file loaded.\nPlease click the \"Load File...\" button.", getLocalBounds(),
                juce::Justification::centred, 2);
        }
    }

    void setCurrentURL(const juce::URL& u)
    {
        if (currentURL == u) return;

        currentURL = u;
        thumbnail.setSource(makeInputSource(u).release());
    }

    juce::URL getCurrentURL() const
    {
        return currentURL;
    }

    void setTransportSource(juce::AudioTransportSource* newSource)
    {
        transportSource = newSource;

        struct ResetCallback final : public juce::CallbackMessage
        {
            ResetCallback(AudioThumbnailComponent& o) : owner(o) {}
            void messageCallback() override { owner.reset(); }

            AudioThumbnailComponent& owner;
        };

        (new ResetCallback(*this))->post();
    }

    void timerCallback() override
    {
        if (transportSource != nullptr)
        {
            playLinePosition = transportSource->getCurrentPosition() / thumbnail.getTotalLength();
            repaint();
        }
    }

private:
    juce::AudioDeviceManager& audioDeviceManager;
    juce::AudioThumbnailCache thumbnailCache;
    juce::AudioThumbnail thumbnail;
    juce::AudioTransportSource* transportSource = nullptr;

    juce::URL currentURL;
    double playLinePosition = 0.0;

    //==============================================================================
    void changeListenerCallback(ChangeBroadcaster*) override
    {
        repaint();
    }

    void reset()
    {
        playLinePosition = 0.0;
        repaint();

        if (transportSource == nullptr)
            stopTimer();
        else
            startTimerHz(25);
    }

    void mouseDrag(const juce::MouseEvent& e) override
    {
        if (transportSource != nullptr)
        {
            const juce::ScopedLock sl(audioDeviceManager.getAudioCallbackLock());

            transportSource->setPosition((juce::jmax(static_cast<double> (e.x), 0.0) / getWidth())
                * thumbnail.getTotalLength());
        }
    }
    //==============================================================================

    // Helpers
    //==============================================================================
    inline std::unique_ptr<juce::InputSource> makeInputSource(const juce::URL& url)
    {
        if (const auto doc = juce::AndroidDocument::fromDocument(url))
            return std::make_unique<juce::AndroidDocumentInputSource>(doc);

    #if ! JUCE_IOS
        if (url.isLocalFile())
            return std::make_unique<juce::FileInputSource>(url.getLocalFile());
    #endif

        return std::make_unique<juce::URLInputSource>(url);
    }
    //==============================================================================
};

AudioPlayerComponent.h

juce::dsp::ProcessorDuplicator

#pragma once

#include "AudioThumbnailComponent.h"
#include "DSPSetup.h"

//==============================================================================
class AudioPlayerComponent final : public juce::Component,
    public juce::ChangeBroadcaster,
    private juce::TimeSliceThread,
    private juce::ChangeListener,
    private juce::Value::Listener
{
public:
    AudioPlayerComponent() : juce::TimeSliceThread("Audio Player Thread"),
        thumbnailComp(audioDeviceManager, formatManager)
    {
        formatManager.registerBasicFormats();
        audioDeviceManager.addAudioCallback(&audioSourcePlayer);

#ifndef JUCE_DEMO_RUNNER
        audioDeviceManager.initialiseWithDefaultDevices(0, 2);
#endif

        init();
        startThread();

        setOpaque(true);

        addAndMakeVisible(loadButton);
        addAndMakeVisible(playButton);
        addAndMakeVisible(gainSlider);

        loadButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightblue);
        loadButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);

        playButton.setButtonText("Play");
        playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
        playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);

        gainSlider.setRange(0, 100, 1);
        gainSlider.setValue(100);

        loadButton.onClick = [this] { openFile(); };
        playButton.onClick = [this] { togglePlay(); };
        gainSlider.onValueChange = [this] { updateGain(); };

        addAndMakeVisible(thumbnailComp);
        thumbnailComp.addChangeListener(this);
    }

    ~AudioPlayerComponent() override
    {
        signalThreadShouldExit();
        stop();
        audioDeviceManager.removeAudioCallback(&audioSourcePlayer);
        waitForThreadToExit(10000);

        audioSourcePlayer.setSource(nullptr);
        playState.removeListener(this);
    }

    void paint(juce::Graphics& g) override
    {
        g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
        g.fillRect(getLocalBounds());
    }

    void resized() override
    {
        // When the main component's fized size is 900 x 280
        playButton.setBounds(20, 210, 160, 50);
        loadButton.setBounds(220, 210, 160, 50);
        gainSlider.setBounds(440, 210, 420, 40);
        thumbnailComp.setBounds(16, 15, 850, 170);
    }

    void init()
    {
        if (transportSource.get() == nullptr)
        {
            transportSource.reset(new juce::AudioTransportSource());
            transportSource->addChangeListener(this);

            if (readerSource != nullptr)
            {
                if (auto* device = audioDeviceManager.getCurrentAudioDevice())
                {
                    transportSource->setSource(readerSource.get(), juce::roundToInt(device->getCurrentSampleRate()), this, reader->sampleRate);

                    getThumbnailComponent().setTransportSource(transportSource.get());
                }
            }
        }

        audioSourcePlayer.setSource(nullptr);
        dspSetup.reset();

        if (dspSetup.get() == nullptr)
        {
            dspSetup.reset(new DSPAudioBlocks(*transportSource));
        }

        audioSourcePlayer.setSource(dspSetup.get());
    }

    void togglePlay()
    {
        if (playState.getValue())
        {
            stop();           
        }
        else
        {
            play();    
        }
    }

    void play()
    {
        if (readerSource == nullptr) return;

        if (transportSource->getCurrentPosition() >= transportSource->getLengthInSeconds() || transportSource->getCurrentPosition() < 0)
        {
            transportSource->setPosition(0);
        }

        transportSource->start();
        playState = true;

        playButton.setButtonText("Stop");
        playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightsalmon);
    }

    void stop()
    {
        playState = false;

        if (transportSource.get() != nullptr)
        {
            transportSource->stop();
            transportSource->setPosition(0);

            playButton.setButtonText("Play");
            playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
        }
    }

    void updateGain()
    {
        // Gain slider's range is set between 0 and 100
        audioSourcePlayer.setGain(static_cast<float>(gainSlider.getValue()) / 100.0f);
    }

    AudioThumbnailComponent& getThumbnailComponent()
    {
        return thumbnailComp;
    }

    void openFile()
    {
        //stop(); It can be activated if the current play to be stopped once the file folder is opened

        if (fileChooser != nullptr) return;

        if (!juce::RuntimePermissions::isGranted(juce::RuntimePermissions::readExternalStorage))
        {
            SafePointer<AudioPlayerComponent> safeThis(this);
            juce::RuntimePermissions::request(juce::RuntimePermissions::readExternalStorage,
                [safeThis](bool granted) mutable
                {
                    if (safeThis != nullptr && granted)
                    {
                        safeThis->openFile();
                    }
                });
            return;
        }

        fileChooser.reset(new juce::FileChooser("Select an audio file...", juce::File(), "*.wav;*.mp3;*.aif"));

        fileChooser->launchAsync(juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles,
            [this](const juce::FileChooser& fc) mutable
            {
                if (fc.getURLResults().size() > 0)
                {
                    const auto u = fc.getURLResult();

                    if (!loadURL(u))
                    {
                        auto options = juce::MessageBoxOptions().withIconType(juce::MessageBoxIconType::WarningIcon)
                            .withTitle("Error loading file")
                            .withMessage("Unable to load audio file")
                            .withButton("OK");
                        messageBox = juce::NativeMessageBox::showScopedAsync(options, nullptr);
                    }
                    else
                    {
                        thumbnailComp.setCurrentURL(u);
                    }
                }

                fileChooser = nullptr;
            }, nullptr);
    }

    bool loadURL(const juce::URL& fileToPlay)
    {
        stop();

        audioSourcePlayer.setSource(nullptr);
        getThumbnailComponent().setTransportSource(nullptr);
        transportSource.reset();
        readerSource.reset();

        auto source = makeInputSource(fileToPlay);

        if (source == nullptr) return false;

        auto stream = rawToUniquePtr(source->createInputStream());

        if (stream == nullptr) return false;

        reader = rawToUniquePtr(formatManager.createReaderFor(std::move(stream)));

        if (reader == nullptr) return false;

        readerSource.reset(new juce::AudioFormatReaderSource(reader.get(), false));

        init();
        resized();

        return true;
    }

private:
    AudioThumbnailComponent thumbnailComp;
    juce::TextButton loadButton{ "Load File" }, playButton{ "Play" };
    juce::Slider gainSlider;

    std::unique_ptr<juce::FileChooser> fileChooser;
    juce::ScopedMessageBox messageBox;

    juce::AudioDeviceManager audioDeviceManager;
    juce::AudioFormatManager formatManager;
    juce::AudioSourcePlayer audioSourcePlayer;

    juce::Value playState{ juce::var(false) };

    std::unique_ptr<juce::AudioFormatReader> reader;
    std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
    std::unique_ptr<juce::AudioTransportSource> transportSource;
    std::unique_ptr<DSPAudioBlocks> dspSetup;

    //==============================================================================
    void changeListenerCallback(ChangeBroadcaster*) override
    {
        if (playState.getValue() && !transportSource->isPlaying())
        {
            stop();
        }
    }

    void valueChanged(juce::Value& v) override
    {
    }
    //==============================================================================

    // Helpers
    //==============================================================================
    inline std::unique_ptr<juce::InputSource> makeInputSource(const juce::URL& url)
    {
        if (const auto doc = juce::AndroidDocument::fromDocument(url))
            return std::make_unique<juce::AndroidDocumentInputSource>(doc);

#if ! JUCE_IOS
        if (url.isLocalFile())
            return std::make_unique<juce::FileInputSource>(url.getLocalFile());
#endif

        return std::make_unique<juce::URLInputSource>(url);
    }

    template <typename T>
    std::unique_ptr<T> rawToUniquePtr(T* ptr)
    {
        return std::unique_ptr<T>(ptr);
    }
    //==============================================================================
};

 

DSPSetup. h

#pragma once

#include <JuceHeader.h>

//==============================================================================
struct DSPProcessBase
{
    void prepare(const juce::dsp::ProcessSpec& spec)
    {
        // 22kHz chosen not to affect audible range
        multiProcessor.state = juce::dsp::IIR::Coefficients<float>::makeLowPass(spec.sampleRate, 22000.0);
        multiProcessor.prepare(spec);
    }

    void process(const juce::dsp::ProcessContextReplacing<float>& context)
    {
        multiProcessor.process(context);
    }

    void reset()
    {
        multiProcessor.reset();
    }

    // IIR::Filter chosen as a mono processor type and process would be in the "float" numeric domain   
    juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> multiProcessor;
};

//==============================================================================
struct DSPAudioBlocks final : public juce::AudioSource,
    public juce::dsp::ProcessorWrapper<DSPProcessBase>,
    private juce::ChangeListener
{
    DSPAudioBlocks(AudioSource& input) : inputSource(&input)
    {
    }

    void prepareToPlay(int blockSize, double sampleRate) override
    {
        inputSource->prepareToPlay(blockSize, sampleRate);
        this->prepare({ sampleRate, (juce::uint32)blockSize, 2 });
    }

    void releaseResources() override
    {
        inputSource->releaseResources();
    }

    void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
    {
        if (bufferToFill.buffer == nullptr)
        {
            jassertfalse;
            return;
        }

        inputSource->getNextAudioBlock(bufferToFill);

        juce::dsp::AudioBlock<float> block(*bufferToFill.buffer, (size_t)bufferToFill.startSample);

        juce::ScopedLock audioLock(audioCallbackLock);
        this->process(juce::dsp::ProcessContextReplacing<float>(block));
    }

    void changeListenerCallback(juce::ChangeBroadcaster*) override
    {
        juce::ScopedLock audioLock(audioCallbackLock);
    }

    juce::CriticalSection audioCallbackLock;
    juce::AudioSource* inputSource;
};

MainComponent.h

#pragma once

#include "AudioPlayerComponent.h"

class MainComponent : public juce::Component
{
public:
    MainComponent()
    {
        setOpaque(true);

        addAndMakeVisible(audioPlayer);

        setSize(900, 280);
    }

    ~MainComponent() = default;

    void paint(juce::Graphics& g) override
    {
        g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
    }

    void resized() override
    {
        audioPlayer.setBounds(getLocalBounds());
    }

private:
    AudioPlayerComponent audioPlayer;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
};

Main.cpp

#include "MainComponent.h"

//==============================================================================
class AudioWaveformApplication  : public juce::JUCEApplication
{
public:
    //==============================================================================
    AudioWaveformApplication() {}

    const juce::String getApplicationName() override       { return "Audio Waveform Test" ; }
    const juce::String getApplicationVersion() override    { return "test version"; }
    bool moreThanOneInstanceAllowed() override             { return true; }

    //==============================================================================
    void initialise (const juce::String& commandLine) override
    {
        mainWindow.reset (new MainWindow (getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr; // (deletes our window)
    }

    //==============================================================================
    void systemRequestedQuit() override
    {
        quit();
    }

    class MainWindow    : public juce::DocumentWindow
    {
    public:
        MainWindow (juce::String name)
            : DocumentWindow (name,
                              juce::Desktop::getInstance().getDefaultLookAndFeel()
                                                          .findColour (juce::ResizableWindow::backgroundColourId),
                              DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar (true);
            setContentOwned (new MainComponent(), true);

           #if JUCE_IOS || JUCE_ANDROID
            setFullScreen (true);
           #else
            setResizable(false, false);
            setResizeLimits(900, 320, 900, 320);
            centreWithSize (getWidth(), getHeight());
           #endif

            setVisible (true);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };

private:
    std::unique_ptr<MainWindow> mainWindow;
};

//==============================================================================

START_JUCE_APPLICATION (AudioWaveformApplication)