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:

 

References:

 

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