Spectrum Analyser
Key Features:
- Done with JUCE framework by using filter processor duplicator (IIR filter, juce::dsp::ProcessorDuplicator)
- Balanced log-linear spectrum, audio waveform, peak and RMS level meters
- Volume and low-high-band pass filters (with 0.707 Q value) for simple modification test purposes
- Mouse draggable playing position bar on audio waveform window
- Gradual spacing increase at high frequencies on 99 bands spectrum
- Smooth levelling applied to spectrum and peak-RMS level meters
- Mostly fixed hard-coded GUI style for components
Improvement Areas:
- General refactoring to reduce the complexity
- Higher resolution in low frequencies might be preference
- DSP setup structure should be improved
- Stereo correlation might be added
- Some tweaks for dB level measurements
Download Link:
- SpectrumAnalyserTest.zip (zipped)
- Only DSP, spectrum, and level meters code published here to keep simple
References:
- JUCE Tutorial: Visualise the frequencies of a signal in real time
- Drawing Audio Thumbnail
- Drawing Level Meters
DSPSetup.h
#pragma once #include "SpectrumComponent.h" #include "LevelMeterComponent.h" //============================================================================== struct DSPProcessBase { void prepare(const juce::dsp::ProcessSpec& spec) { jassert(spec.sampleRate > 0.0f && spec.sampleRate * 0.5f > 20000.0f); sampleRate = spec.sampleRate; *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, 20000.0f, Q); filterProcessor.prepare(spec); gainProcessor.prepare(spec); gainProcessor.setGainLinear(1.0f); } void process(const juce::dsp::ProcessContextReplacing<float>& context) { juce::ScopedNoDenormals noDenormals; filterProcessor.process(context); gainProcessor.process(context); } void reset() { filterProcessor.reset(); gainProcessor.reset(); } //============================================================================== enum FilterType { LowPass = 1, HighPass = 2, BandPass = 3 }; void updateFilterType(const int type) { switch (type) { case LowPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, filterCutOff, Q); filterType = FilterType::LowPass; break; case HighPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, filterCutOff, Q); filterType = FilterType::HighPass; break; case BandPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeBandPass(sampleRate, filterCutOff, Q); filterType = FilterType::BandPass; break; default: break; } } void updateFilterValue(const float cutoffValue) { if (cutoffValue < 1.0f) return; filterCutOff = cutoffValue; switch (filterType) { case LowPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, filterCutOff, Q); break; case HighPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, filterCutOff, Q); break; case BandPass: *filterProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeBandPass(sampleRate, filterCutOff, Q); break; default: break; } } void updateGain(const float volumeLevel) { gainProcessor.setGainLinear(juce::jlimit(0.0f, 1.0f, volumeLevel)); } //============================================================================== private: // 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>> filterProcessor; juce::dsp::Gain<float> gainProcessor; FilterType filterType = FilterType::LowPass; const float Q = 0.707f; int filterCutOff = 20000; float sampleRate = 0.0f; }; //============================================================================== struct DSPAudioBlocks final : public juce::AudioSource, public juce::dsp::ProcessorWrapper<DSPProcessBase> { DSPAudioBlocks(AudioSource& input) : inputSource(&input) { } void prepareToPlay(int blockSize, double sampleRate) override { inputSource->prepareToPlay(blockSize, sampleRate); this->prepare({ sampleRate, (juce::uint32)blockSize, 2 }); //============================================================================== // SPECTRUM SETUP spectrum.CalculateFFTIndices(sampleRate); spectrum.setSmoothingLevel(sampleRate, 0.1f); //============================================================================== //============================================================================== // LEVEL METERS SETUP leftPeakMeter.setSmoothingLevel(sampleRate, 0.1f, -100.0f); rightPeakMeter.setSmoothingLevel(sampleRate, 0.1f, -100.0f); leftRMSMeter.setSmoothingLevel(sampleRate, 0.5f, -100.0f); rightRMSMeter.setSmoothingLevel(sampleRate, 0.5f, -100.0f); //============================================================================== } 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)); //============================================================================== // FOR SPECTRUM ANALYSER spectrum.getNextAudioBlock(bufferToFill); // For modified data //============================================================================== //============================================================================== // FOR LEVEL METERS leftPeakMeter.setLevelMeter(bufferToFill, LevelMeterComponent::Peak_Level, LevelMeterComponent::Left); rightPeakMeter.setLevelMeter(bufferToFill, LevelMeterComponent::Peak_Level, LevelMeterComponent::Right); leftRMSMeter.setLevelMeter(bufferToFill, LevelMeterComponent::RMS_Level, LevelMeterComponent::Left); rightRMSMeter.setLevelMeter(bufferToFill, LevelMeterComponent::RMS_Level, LevelMeterComponent::Right); //============================================================================== } void updateFilterType(const int filterType) { juce::ScopedLock audioLock(audioCallbackLock); this->processor.updateFilterType(filterType); } void updateFilterValue(const float cutoffValue) { juce::ScopedLock audioLock(audioCallbackLock); this->processor.updateFilterValue(cutoffValue); } void updateGain(const float volumeLevel) { juce::ScopedLock audioLock(audioCallbackLock); this->processor.updateGain(volumeLevel); } SpectrumComponent spectrum; LevelMeterComponent leftPeakMeter, rightPeakMeter, leftRMSMeter, rightRMSMeter; private: juce::CriticalSection audioCallbackLock; juce::AudioSource* inputSource; };
SpectrumComponent.h
#pragma once #include <JuceHeader.h> //============================================================================== class SpectrumComponent : public juce::AudioAppComponent, private juce::Timer { public: SpectrumComponent() : forwardFFT(fftOrder), window(fftSize, juce::dsp::WindowingFunction<float>::blackman) { init(); startTimerHz(25); } ~SpectrumComponent() override { shutdownAudio(); } //============================================================================== void init() { // Target frequencies for FFT targetFrequencies = { 20, 30, 50, 80, 100, 120, 140, 160, 180, 200, 225, 250, 275, 300, 330, 360, 390, 420, 450, 480, 500, 550, 585, 620, 660, 700, 740, 780, 820, 860, 900, 950, 1000, 1060, 1120, 1180, 1250, 1320, 1400, 1500, 1600, 1700, 1800, 1900, 2000, 2120, 2240, 2360, 2500, 2650, 2800, 3000, 3150, 3300, 3500, 3700, 3900, 4100, 4300, 4500, 4750, 5000, 5300, 5600, 5900, 6200, 6500, 6800, 7100, 7400, 7700, 8000, 8350, 8700, 9050, 9400, 9750, 10100, 10500, 10900, 11300, 11700, 12000, 12500, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000 }; // Setting frequency data, spectrum bounds, and bar width frequencyData = { FrequencyData() }; spectrumBounds.setBounds(31, 15, 720, 390); barWidth = spectrumBounds.getWidth() / frequencyData.size(); } void CalculateFFTIndices(const float sampleRate) { for (size_t i = 0; i < frequencyData.size(); ++i) { const int fftIndex = static_cast<float>(fftSize * targetFrequencies[i] / sampleRate); frequencyData[i].fftIndex = fftIndex; } } std::array<int, 99>& getFrequencies() { return targetFrequencies; } //============================================================================== void prepareToPlay(int, double) override {} void releaseResources() override {} void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override { if (bufferToFill.buffer->getNumChannels() > 0) { auto* channelData = bufferToFill.buffer->getReadPointer(0, bufferToFill.startSample); for (auto i = 0; i < bufferToFill.numSamples; ++i) { pushNextSampleIntoFifo(channelData[i]); } } } void paint(juce::Graphics& g) override { } void drawFrame(juce::Graphics& g) { g.setColour(juce::Colours::black); // Background color g.fillRect(spectrumBounds); processSmoothing(); // Draw the rising/falling bars with gradient colors for (size_t i = 0; i < frequencyData.size(); ++i) { const float level = frequencyData[i].frequencyLevel; // Map the level to Y-axis (bar height) const float barHeight = juce::jmap<float>(level, -100.0f, 0.0f, 0.0f, spectrumBounds.getHeight()); const float xPos = spectrumBounds.getX() + i * barWidth; // Define the gradient for the bar juce::ColourGradient gradient( juce::Colours::green, spectrumBounds.getBottomLeft(), juce::Colours::red, spectrumBounds.getTopLeft(), false ); gradient.addColour(0.6, juce::Colours::yellow); // Set the gradient in the graphics context and draw the bar g.setGradientFill(gradient); juce::Rectangle<float> bar(xPos, spectrumBounds.getBottom() - barHeight, barWidth * 0.8f, barHeight); g.fillRect(bar); } } void pushNextSampleIntoFifo(float sample) noexcept { // if the fifo contains enough data, render the next frame if (fifoIndex >= fftSize) { if (!nextFFTBlockReady) { std::fill(fftData.begin(), fftData.end(), 0.0f); std::copy(fifo.begin(), fifo.end(), fftData.begin()); nextFFTBlockReady = true; } fifoIndex = 0; } fifo[fifoIndex++] = sample; } void drawNextFrameOfSpectrum() { // Apply a windowing function to the data and render it window.multiplyWithWindowingTable(fftData.data(), fftSize); forwardFFT.performFrequencyOnlyForwardTransform(fftData.data()); for (size_t i = 0; i < frequencyData.size(); ++i) { // Normalize the FFT magnitude with guard agasint log(0) const auto fftIndex = frequencyData[i].fftIndex; const auto magnitude = std::max(fftData[fftIndex], 1.0e-9f) / static_cast<float>(fftSize); const auto level = std::round(juce::Decibels::gainToDecibels(magnitude)); frequencyData[i].frequencyLevel = level; } } void timerCallback() override { if (nextFFTBlockReady) { drawNextFrameOfSpectrum(); updateSmoothing(); nextFFTBlockReady = false; repaint(); } } //============================================================================== // SMOOTHING SECTION void setSmoothingLevel ( const double sampleRate, const float rampTimeMs ) { for (size_t i = 0; i < frequencyData.size(); ++i) { frequencyData[i].smoothFrequencyLevel.reset(sampleRate, rampTimeMs / 1000.0f); frequencyData[i].smoothFrequencyLevel.setCurrentAndTargetValue(frequencyData[i].frequencyLevel); } } void updateSmoothing() { for (size_t i = 0; i < frequencyData.size(); ++i) { const float gain = 1.1f; // tuning for compensation frequencyData[i].smoothFrequencyLevel.setTargetValue(gain * frequencyData[i].frequencyLevel); } } void processSmoothing() { for (size_t i = 0; i < frequencyData.size(); ++i) { frequencyData[i].frequencyLevel = static_cast<int>(frequencyData[i].smoothFrequencyLevel.getNextValue()); } } //============================================================================== private: juce::dsp::FFT forwardFFT; juce::dsp::WindowingFunction<float> window; static constexpr int fftOrder = 11; static constexpr int fftSize = 2048; std::array<float, fftSize> fifo; std::array<float, fftSize * 2> fftData; int fifoIndex = 0; bool nextFFTBlockReady = false; struct FrequencyData { int fftIndex = 0; int frequencyLevel = -100; juce::LinearSmoothedValue<float> smoothFrequencyLevel = -100.0f; }; std::array<int, 99> targetFrequencies; std::array<FrequencyData, 99> frequencyData; juce::Rectangle<float> spectrumBounds; float barWidth; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SpectrumComponent) };
LevelMeterComponent.h
#pragma once #include <JuceHeader.h> class LevelMeterComponent : public juce::Component, public juce::Timer { public: LevelMeterComponent(): gradient(), grill(), levelMeterValue(-100.0f), smoothLevelMeterValue(-100.0f) { startTimerHz(25); } ~LevelMeterComponent() = default; void paint(juce::Graphics& g) override { auto bounds = getLocalBounds(); g.setColour(juce::Colours::black); g.fillRect(bounds); g.setGradientFill(gradient); const auto scaledY = juce::jmap(levelMeterValue, -100.0f, 3.0f, 0.0f, static_cast<float>(getHeight())); g.fillRect(bounds.removeFromBottom(scaledY)); } void resized() override { const auto bounds = getLocalBounds().toFloat(); gradient = juce::ColourGradient{ juce::Colours::green, bounds.getBottomLeft(), juce::Colours::red, bounds.getTopLeft(), false }; gradient.addColour(0.5, juce::Colours::yellow); } void timerCallback() override { repaint(); } //============================================================================== enum MeterType { Left = 0, Right = 1, Peak_Level, RMS_Level }; void setSmoothingLevel ( const double sampleRate, const float rampLengthInSecs, const float bottomLevelValue ) { smoothLevelMeterValue.reset(sampleRate, rampLengthInSecs); smoothLevelMeterValue.setCurrentAndTargetValue(bottomLevelValue); } void setLevelMeter ( const juce::AudioSourceChannelInfo& bufferToFill, const MeterType meterType, const int channel ) { smoothLevelMeterValue.skip(bufferToFill.buffer->getNumSamples()); float value = 0.0f; switch (meterType) { case Peak_Level: value = bufferToFill.buffer->getMagnitude(channel, (size_t)bufferToFill.startSample, bufferToFill.buffer->getNumSamples()); break; case RMS_Level: value = bufferToFill.buffer->getRMSLevel(channel, (size_t)bufferToFill.startSample, bufferToFill.buffer->getNumSamples()); break; default: // Level meter would not work break; } if (value < smoothLevelMeterValue.getCurrentValue()) { smoothLevelMeterValue.setTargetValue(value); } else { smoothLevelMeterValue.setCurrentAndTargetValue(value); } levelMeterValue = juce::Decibels::gainToDecibels(smoothLevelMeterValue.getCurrentValue()); } //============================================================================== private: juce::ColourGradient gradient; juce::Image grill; float levelMeterValue; juce::LinearSmoothedValue<float> smoothLevelMeterValue; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LevelMeterComponent) };