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)