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 (not optimised)
  • Stereo correlation might be added
  • Some tweaks for dB level measurements

 

Download Link:

 

References:

 

 

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