Audio Analyser
Key Features:
- Done with JUCE framework by using filter processor duplicator (IIR filter, juce::dsp::ProcessorDuplicator)
- Audio thumbnail, linear spectrum and stereo correlation windows, peak and RMS level meters
- Volume, low pass filter, and stereo width sliders for simple modification test purpose
- Mouse draggable playing position bar
- Second Order Butterworth low pass filter with 0.707 Q value for a quick coefficient calculation
- Smooth levelling applied to stereo correlation and peak-RMS level meters
- Mostly fixed hard-coded GUI style for components
Improvement Areas:
- General refactoring to reduce the complexity
- Spectrum scaling would be improved (to logarithmic)
- DSP setup structure should be improved
- Smooth levelling should be applied to spectrum section
- Duration, sample rate, and bit depth values should be added
Download Link:
- AudioAnalyserTest.zip (zipped)
- Only DSP and stereo correlation code published here to keep simple
References:
- JUCE Tutorial: Visualise the frequencies of a signal in real time
- Drawing Audio Thumbnail
- Stereo Width Control
- Drawing Level Meters

DSPSetup.h
#pragma once
#include "SpectrumComponent.h"
#include "LevelMeterComponent.h"
#include "StereoCorrelationComponent.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 = secondOrderButterworthCoefficients(spec.sampleRate, 20000.0f);
filterProcessor.prepare(spec);
gainProcessor.prepare(spec);
gainProcessor.setGainLinear(1.0f);
}
void process(const juce::dsp::ProcessContextReplacing<float>& context)
{
juce::ScopedNoDenormals noDenormals;
processStereoWidth(context);
filterProcessor.process(context);
gainProcessor.process(context);
}
void reset()
{
filterProcessor.reset();
gainProcessor.reset();
stereoWidth = 1.0f;
}
//==============================================================================
void updateLowPass(const float cutoffValue)
{
if (cutoffValue < 1.0f) return;
*filterProcessor.state = secondOrderButterworthCoefficients(sampleRate, cutoffValue);
}
void updateGain(const float volumeLevel)
{
gainProcessor.setGainLinear(juce::jlimit(0.0f, 1.0f, volumeLevel));
}
void updateStereoWidth(const float widthValue)
{
stereoWidth = widthValue;
}
void processStereoWidth(const juce::dsp::ProcessContextReplacing<float>& context)
{
auto& block = context.getOutputBlock(); // mutable access !!!
if (block.getNumChannels() < 2) return;
auto* leftChannel = block.getChannelPointer(0);
auto* rightChannel = block.getChannelPointer(1);
auto numSamples = block.getNumSamples();
for (size_t sample = 0; sample < numSamples; ++sample)
{
const float mid = (leftChannel[sample] + rightChannel[sample]) * 0.5f;
const float side = (leftChannel[sample] - rightChannel[sample]) * 0.5f * stereoWidth;
leftChannel[sample] = mid + side;
rightChannel[sample] = mid - side;
}
}
//==============================================================================
//==============================================================================
// FOR LOW PASS COEFFICIENTS
static std::array<float, 6> secondOrderButterworthCoefficients(const float sampleRate, const float frequency)
{
// where pi = 3.14159f and Q = 0.70711f
const float n = 1 / std::tan(3.14159f * frequency / sampleRate);
const float nSquared = n * n;
const float invQ = 1 / 0.70711f;
const float c = 1 / (1 + invQ * n + nSquared); // scaling factor
// { b0, b1, b2, a0, a1, a2 }
return { c, 2 * c, c, 1, 2 * c * (1 - nSquared), c * (1 - invQ * n + nSquared) };
}
//==============================================================================
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;
float sampleRate = 0.0;
float stereoWidth = 1.0f; // wider if > 1, narrow if < 1 - mono is 0
};
//==============================================================================
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.CalculateFrequencyBins(sampleRate);
//==============================================================================
//==============================================================================
// 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);
//==============================================================================
//==============================================================================
// STEREO CORRELATION METER SETUP
stereoCorrelation.setSmoothingCorrelation(sampleRate, 0.3f, 1.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);
//==============================================================================
//==============================================================================
// FOR STEREO CORRELATION
stereoCorrelation.calculateStereoCorrelation(*bufferToFill.buffer);
//==============================================================================
}
void updateLowPass(const float cutoffValue)
{
juce::ScopedLock audioLock(audioCallbackLock);
this->processor.updateLowPass(cutoffValue);
}
void updateGain(const float volumeLevel)
{
juce::ScopedLock audioLock(audioCallbackLock);
this->processor.updateGain(volumeLevel);
}
void updateStereoWidth(const float widthValue)
{
juce::ScopedLock audioLock(audioCallbackLock);
this->processor.updateStereoWidth(widthValue);
}
SpectrumComponent spectrum;
LevelMeterComponent leftPeakMeter, rightPeakMeter, leftRMSMeter, rightRMSMeter;
StereoCorrelationComponent stereoCorrelation;
private:
juce::CriticalSection audioCallbackLock;
juce::AudioSource* inputSource;
};StereoCorrelationComponent.h
#pragma once
#include <JuceHeader.h>
#include <juce_graphics/juce_graphics.h>
class StereoCorrelationComponent : public juce::Component, public juce::Timer
{
public:
StereoCorrelationComponent() :
gradient(), grill(), stereoCorrelationValue(0.0f), smoothCorreleationValue()
{
startTimerHz(25);
}
~StereoCorrelationComponent() = default;
void paint(juce::Graphics& g) override
{
auto bounds = getLocalBounds();
g.setColour(juce::Colours::black);
g.fillRect(bounds);
const auto scaledX = juce::jmap(stereoCorrelationValue, -1.0f, 1.0f, 0.0f, static_cast<float>(getWidth()));
// Calculate the center position of the stick
const int stickWidth = 6;
const int stickX = static_cast<int>(scaledX - stickWidth / 2.0f);
// Calculate the stick color using linear interpolation
juce::Colour stickColor;
if (stereoCorrelationValue <= 0.0f)
{
// Blend from red (-1) to yellow (0)
stickColor = juce::Colours::red.interpolatedWith(juce::Colours::yellow, (stereoCorrelationValue + 1.0f) / 1.0f);
}
else
{
// Blend from yellow (0) to green (+1)
stickColor = juce::Colours::yellow.interpolatedWith(juce::Colours::green, stereoCorrelationValue / 1.0f);
}
g.setColour(stickColor);
g.fillRect(juce::Rectangle<int>(stickWidth, getHeight()).withCentre({ stickX, getHeight() / 2 }));
}
void paintOverChildren(::juce::Graphics& g) override
{
g.drawImage(grill, getLocalBounds().toFloat());
}
void timerCallback() override
{
repaint();
}
//==============================================================================
void setSmoothingCorrelation
(
const double sampleRate,
const float rampLengthInSecs,
const float bottomLevelValue
)
{
smoothCorreleationValue.reset(sampleRate, rampLengthInSecs);
smoothCorreleationValue.setCurrentAndTargetValue(bottomLevelValue);
}
void calculateStereoCorrelation(const juce::AudioBuffer<float>& buffer)
{
smoothCorreleationValue.skip(buffer.getNumSamples());
const int numSamples = buffer.getNumSamples();
const int numChannels = buffer.getNumChannels();
if (numChannels < 2 || numSamples == 0)
{
// Ensure the buffer has at least two channels for stereo correlation
// Return 0 if there's not enough data to calculate
smoothCorreleationValue.setTargetValue(0.0f);
}
const float* leftChannel = buffer.getReadPointer(0); // Left channel
const float* rightChannel = buffer.getReadPointer(1); // Right channel
float sumLR = 0.0; // Sum of left * right
float sumLL = 0.0; // Sum of left * left
float sumRR = 0.0; // Sum of right * right
for (int i = 0; i < numSamples; ++i)
{
float leftSample = leftChannel[i];
float rightSample = rightChannel[i];
sumLR += leftSample * rightSample; // Cross-product
sumLL += leftSample * leftSample; // Left energy
sumRR += rightSample * rightSample; // Right energy
}
// Compute the correlation coefficient
const float denominator = std::sqrt(sumLL * sumRR); // Magnitudes
if (denominator == 0.0f)
{
smoothCorreleationValue.setCurrentAndTargetValue(1.0f); // Avoid division by zero
}
const float correlation = sumLR / denominator;
if (correlation < smoothCorreleationValue.getCurrentValue())
{
smoothCorreleationValue.setTargetValue(correlation);
}
else
{
smoothCorreleationValue.setCurrentAndTargetValue(correlation);
}
// Clamp the result to the range [-1, 1]
stereoCorrelationValue = juce::jlimit(-1.0f, 1.0f, smoothCorreleationValue.getCurrentValue());
}
//==============================================================================
private:
juce::ColourGradient gradient;
juce::Image grill;
float stereoCorrelationValue = 0.0f;
juce::LinearSmoothedValue<float> smoothCorreleationValue;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StereoCorrelationComponent)
};