Equaliser

 

Key Features:

  • Done with JUCE framework by using filter processor duplicator (IIR filter, juce::dsp::ProcessorDuplicator)
  • Response curve (logarithmic), spectrum, audio waveform, peak and RMS level meters
  • Volume and low-high-band pass (with 0.707 Q value) and two peak filters for simple modification test purposes
  • Mouse draggable playing position bar on audio waveform window
  • 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
  • Better knob adjustments
  • Spectrum setup structure should be improved
  • Some tweaks for dB level measurements

 

Download Link:

  • StandaloneEQTest.zip  (zipped)
  • Only audio player and response curve code published here to keep simple

 

References:

 

 

ResponseCurveComponent.h

#pragma once

#include <JuceHeader.h>

class ResponseCurveComponent : public juce::Component, public juce::Timer
{
public:
    ResponseCurveComponent()
    {        
        init();
        startTimerHz(25);
    }
    ~ResponseCurveComponent() = default;

    void init()
    {
        generateLogSpacedFrequencies();

        lowCutData.gainDB = 0.0f;
        lowCutData.freq = 10.0f;
        lowCutData.Q = 0.7f;

        midData.gainDB = 0.0f;
        midData.freq = 1000.0f;
        midData.Q = 0.7f;

        highMidData.gainDB = 0.0f;
        highMidData.freq = 3000.0f;
        highMidData.Q = 0.7f;

        highCutData.gainDB = 0.0f;
        highCutData.freq = 20000.0f;
        highCutData.Q = 0.7f;
    }

    void paint(juce::Graphics& g) override
    {
        auto bounds = getLocalBounds();

        g.setColour(juce::Colours::black);
        g.fillRect(bounds);
       
        // Create the Response Curve Path & Fill Path 
        g.setColour(juce::Colours::white); // Curve color

        juce::Path responseCurve;
        juce::Path fillArea; // Path for the filled area
        bool firstPoint = true;

        float zeroDB_Y = juce::jmap<float>(0.0f, -12.0f, 12.0f, bounds.getHeight(), 0);

        for (int i = 0; i < numPoints; ++i)
        {
            float f = frequencies[i];
            float gainDB = frequencyGainValues[i];
            
            if (std::isinf(gainDB) || std::isnan(gainDB))
            {
                // Set gainDB outside visual context
                gainDB = -20.0f;
            }

            // Convert frequency (log scale) to X pixel position
            float x = juce::jmap<float>(std::log10(f), std::log10(20.0f), std::log10(20000.0f), 2, getWidth() - 2);

            // Convert dB gain to Y pixel position (center = 0 dB)
            float y = juce::jmap<float>(gainDB, -12.0f, 12.0f, bounds.getHeight() - 2, 2);   

            if (firstPoint)
            {
                responseCurve.startNewSubPath(x, y);
                fillArea.startNewSubPath(x, zeroDB_Y); // Start from 0 dB line
                fillArea.lineTo(x, y);
                firstPoint = false;
            }
            else
            {
                responseCurve.lineTo(x, y);
                fillArea.lineTo(x, y);
            }
        }

        // Close the fill area path by connecting to 0 dB line
        fillArea.lineTo(getWidth() - 2.0f, zeroDB_Y);
        fillArea.closeSubPath();

        // === Fill the area between the curve and the 0 dB line ===
        g.setColour(juce::Colours::lightblue.withAlpha(0.8f)); // Semi-transparent blue fill
        g.fillPath(fillArea);

        // === Draw the EQ response curve ===
        g.setColour(juce::Colours::white);
        g.strokePath(responseCurve, juce::PathStrokeType(2.0f));
    }

    void resized() override
    {
        const auto bounds = getLocalBounds().toFloat();
    }

    void timerCallback() override
    {
        updateFrequencyGainValuesForDraw();
        repaint();
    }

    //==============================================================================

    enum FilterType
    {
        LowCut,
        Mid,
        HighMid,
        HighCut 
    };

    struct FilterData
    {
        float freq;
        float Q;
        float gainDB;
    };

    void updateLowCutData(const float f0)
    {
        lowCutData.freq = f0;
        lowCutData.gainDB = 0.0f;
        lowCutData.Q = 0.7f;
    }

    void updateMidData(const float f0, const float gainDB, const float Q)
    {
        midData.freq = f0;
        midData.gainDB = gainDB;
        midData.Q = Q;
    }

    void updateHighMidData(const float f0, const float gainDB, const float Q)
    {
        highMidData.freq = f0;
        highMidData.gainDB = gainDB;
        highMidData.Q = Q;
    }

    void updateHighCutData(const float f0)
    {
        highCutData.freq = f0;
        highCutData.gainDB = 0.0f;
        highCutData.Q = 0.7f;
    }

    void updateFrequencyGainValuesForDraw()
    {
        const size_t size = frequencies.size();

        for (size_t i = 0; i < size; ++i)
        {
            // Clean previous data
            frequencyGainValues[i] = 0.0f;

            frequencyGainValues[i] += computeLowCutGain(frequencies[i], lowCutData.freq, 0.7f);
            frequencyGainValues[i] += computeGainAtFrequency(frequencies[i], midData.freq, midData.gainDB, midData.Q);
            frequencyGainValues[i] += computeGainAtFrequency(frequencies[i], highMidData.freq, highMidData.gainDB, highMidData.Q);
            frequencyGainValues[i] += computeHighCutGain(frequencies[i], highCutData.freq, 0.7f);
        }
    }

    float computeGainAtFrequency(float f, float f0, float gainValue, float Q)
    {
        // Compute EQ response at a given frequency f by center frequency f0
        const float linearGainFactor = std::pow(10.0f, gainValue / 20.0f);

        // Use log-based frequency ratio instead of linear difference
        float logRatio = std::log(f / f0) / std::log(1 + 1.0f / Q);

        // Symmetrical gain calculation
        float gain = 1 + (linearGainFactor - 1) / (1 + logRatio * logRatio);

        // Clamp the result between -20 and +20 dB
        gain = std::clamp(gain, -20.0f, 20.0f);

        return 20.0f * std::log10(gain); // Convert back to dB
    }

    float computeLowCutGain(float f, float f0, float Q)
    {
        if (f == 0.0f) return -100.0f; // Prevent log(0) issues, set a deep cut at DC

        // Frequency ratio
        float frequencyRatio = f0 / f; // f0 is cutoff frequency

        // Compute gain for high-pass filter
        float gain = 1.0f / std::sqrt(1.0f + std::pow(frequencyRatio, 2.0f * Q));
        float gainDB = 20.0f * std::log10(gain);

        if (std::isinf(gainDB) || std::isnan(gainDB))
        {
            gainDB = -100.0f; // Extreme low frequencies should be deeply cut
        }

        return gainDB;
    }

    float computeHighCutGain(float f, float f0, float Q)
    {
        if (f == 0.0f) return 0.0f; // No attenuation at DC for a high-cut filter

        float frequencyRatio = f0 / f; 

        // Compute gain for high-cut (low-pass) filter
        float gain = 1.0f / std::sqrt(1.0f + std::pow(frequencyRatio, -2.0f * Q));
        float gainDB = 20.0f * std::log10(gain);

        if (std::isinf(gainDB) || std::isnan(gainDB))
        {
            gainDB = -100.0f; 
        }

        return gainDB;
    }

    void generateLogSpacedFrequencies(float minFreq = 1.0f, float maxFreq = 20000.0f)
    {
        frequencies.clear();
        frequencyGainValues.clear();

        for (int i = 0; i < numPoints; ++i)
        {
            float fraction = static_cast<float>(i) / (numPoints - 1);
            float freq = minFreq * std::pow(maxFreq / minFreq, fraction); // Log scale
            frequencies.push_back(freq);
            frequencyGainValues.push_back(0.0f);
        }
    }

    void generateLinearSpacedFrequencies(float minFreq = 1.0f, float maxFreq = 20000.0f)
    {
        frequencies.clear();
        frequencyGainValues.clear();

        for (int i = 0; i < numPoints; ++i)
        {
            float freq = minFreq + i * (maxFreq - minFreq) / (numPoints - 1); // Linear scale
            frequencies.push_back(freq);
            frequencyGainValues.push_back(0.0f);
        }
    }
//==============================================================================
private:
    std::vector<float> frequencies;
    std::vector<float> frequencyGainValues;

    int numPoints = 1000;

    FilterData lowCutData;
    FilterData midData;
    FilterData highMidData;
    FilterData highCutData;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ResponseCurveComponent)
};

 

AudioplayerComponent.h

#pragma once

#include "AudioThumbnailComponent.h"
#include "SpectrumComponent.h"
#include "LevelMeterComponent.h"
#include "ResponseCurveComponent.h"
#include "UIHelper.h"


class AudioPlayerComponent final : public juce::Component,
    public juce::AudioSource,
    public juce::ChangeBroadcaster,
    private juce::TimeSliceThread,
    private juce::ChangeListener
{
public:
    AudioPlayerComponent() :
        juce::TimeSliceThread("Audio Player Thread"),
        thumbnailComponent(audioDeviceManager, formatManager)
    {
        // Initialize audio device manager
        audioDeviceManager.initialiseWithDefaultDevices(0, 2);

        // Register audio formats
        formatManager.registerBasicFormats();

        // Add the audio callback
        audioDeviceManager.addAudioCallback(&audioSourcePlayer);

        init();
        startThread();

        addAndMakeVisible(loadButton);
        addAndMakeVisible(playButton);
        addAndMakeVisible(byPassButton);
        addAndMakeVisible(volumeSlider);

        loadButton.setColour(juce::TextButton::buttonColourId,
            getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
        loadButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);

        playButton.setButtonText("Play");
        playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
        playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);

        byPassButton.setButtonText("Bypass Off");
        byPassButton.setColour(juce::TextButton::buttonColourId,
            getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
        byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);

        volumeSlider.setLookAndFeel(&customVolumeKnob);
        customVolumeKnob.setThumbnailRatio(0.7f);
        volumeSlider.setSliderStyle(juce::Slider::Rotary);
        volumeSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 35, 13);
        volumeSlider.setRange(0, 100, 1);
        volumeSlider.setValue(100);
        //volumeSlider.setPopupDisplayEnabled(true, true, this);

        loadButton.onClick = [this] { openFile(); };
        playButton.onClick = [this] { togglePlay(); };
        byPassButton.onClick = [this] { toggleByPass(); };
        volumeSlider.onValueChange = [this] { updateGain(); };

        addAndMakeVisible(thumbnailComponent);
        thumbnailComponent.addChangeListener(this);       

        //==============================================================================       
        // LOW CUT FILTER
        createVerticalSlider(lowCutFreq, 10, 10, 300, 1);  
        createByPassToggle(lowCutByPass, isLowCutOn);
      
        lowCutFreq.onValueChange = [this] { updateLowCutFrequency(); };
        lowCutByPass.onClick = [this] { toggleLowCutByPass(); };

        // MID FILTER
        createRotarySlider(midGain, 0.0f, -12.0f, 12.0f, 0.1f);
        createRotarySlider(midFreq, 1000, 20.0f, 20000.0f, 1.0f);
        createRotarySlider(midQ, 0.7f, 0.1f, 20.0f, 0.1f);
        createByPassToggle(midByPass, isMidCutOn);

        midGain.onValueChange = [this] { updateMidFilterGainLevel(); };
        midFreq.onValueChange = [this] { updateMidFilterFrequency(); };
        midQ.onValueChange = [this] { updateMidFilterQ(); };               
        midByPass.onClick = [this] { toggleMidByPass(); };

        // HIGH MID FILTER
        createRotarySlider(highMidGain, 0.0f, -12.0f, 12.0f, 0.1f);
        createRotarySlider(highMidFreq, 3000, 20.0f, 20000.0f, 1.0f);
        createRotarySlider(highMidQ, 0.7f, 0.1f, 20.0f, 0.1f);
        createByPassToggle(highMidByPass, isHighMidCutOn);

        highMidFreq.onValueChange = [this] { updateHighMidFilterFrequency(); };
        highMidQ.onValueChange = [this] { updateHighMidFilterQ(); };
        highMidGain.onValueChange = [this] { updateHighMidFilterGainLevel(); };
        highMidByPass.onClick = [this] { toggleHighMidByPass(); };

        // HIGH CUT FILTER
        createVerticalSlider(highCutFreq, 20000, 3000, 20000, 1);
        createByPassToggle(highCutByPass, isHighCutOn);

        highCutFreq.onValueChange = [this] { updateHighCutFrequency(); };
        highCutByPass.onClick = [this] { toggleHighCutByPass(); };
        //==============================================================================
    }

    ~AudioPlayerComponent() override
    {
        signalThreadShouldExit();
        stop();
        audioDeviceManager.removeAudioCallback(&audioSourcePlayer);
        waitForThreadToExit(10000);

        audioSourcePlayer.setSource(nullptr);
    }

    //==============================================================================
    // INITILIASING SOURCES
    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());
                }
            }
        }

        // Disconnect any previous audio source
        audioSourcePlayer.setSource(nullptr);
       
        // Set up filters in the processor chain
        auto* device = audioDeviceManager.getCurrentAudioDevice();
        const double sampleRate = device->getCurrentSampleRate();
        *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, 10.0f, 0.7f);
        *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 1000.0f, 0.7f, 1.0f);
        *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 5000.0f, 0.7f, 1.0f);
        *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, 20000.0f, 0.7f);

        setFilterData(lowCutData, 0.0f, 1.0f, 10.0f, 0.7f);
        setFilterData(midData, 0.0f, 1.0f, 1000.0f, 0.7f);
        setFilterData(highMidData, 0.0f, 1.0f, 3000.0f, 0.7f);
        setFilterData(highCutData, 0.0f, 1.0f, 20000.0f, 0.7f);
                
        // Reconnect processorChain as the audio source
        audioSourcePlayer.setSource(this);

        // After loading any file
        volumeSlider.setValue(100);

        lowCutFreq.setValue(10.0f);
        lowCutByPass.setButtonText("off");
        isLowCutOn = false;
        
        midFreq.setValue(1000.0f);
        midQ.setValue(0.7f);
        midGain.setValue(0.0f);
        midByPass.setButtonText("off");
        isMidCutOn = false;

        highMidFreq.setValue(3000.0f);
        highMidQ.setValue(0.7f);
        highMidGain.setValue(0.0f);
        highMidByPass.setButtonText("off");
        isHighMidCutOn = false;

        highCutFreq.setValue(20000.0f);
        highCutByPass.setButtonText("off");
        isHighCutOn = false;

        byPassButton.setButtonText("Bypass Off");
        byPassButton.setColour(juce::TextButton::buttonColourId,
            getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
        byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
        byPassState = false;
    }

    void setSpectrumAnalyser(SpectrumComponent* analyser)
    {
        spectrumAnalyser = analyser;
    }

    void setLevelMeter(LevelMeterComponent* leftPeak, 
        LevelMeterComponent* rightPeak, 
        LevelMeterComponent* leftRMS, 
        LevelMeterComponent* rightRMS)
    {
        leftPeakMeter = leftPeak;
        rightPeakMeter = rightPeak;
        leftRMSMeter = leftRMS;
        rightRMSMeter = rightRMS;
    }

    void setResponseCurve(ResponseCurveComponent* _responseCurve)
    {
        responseCurve = _responseCurve;
    }
    //==============================================================================
       
    //==============================================================================
    // PREPARE AND PROCESS
    void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override 
    {
        juce::dsp::ProcessSpec spec{ sampleRate, (juce::uint32)samplesPerBlockExpected, 2 };

        lowCutProcessor.prepare(spec);
        midProcessor.prepare(spec);
        highMidProcessor.prepare(spec);
        highCutProcessor.prepare(spec);
       
        // Setup filters with default values
        *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, 10.0f, 0.7f);
        *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 1000.0f, 0.7f, 1.0f);
        *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, 3000.0f, 0.7f, 1.0f);
        *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, 20000.0f, 0.7f);

        setFilterData(lowCutData, 0.0f, 1.0f, 10.0f, 0.7f);
        setFilterData(midData, 0.0f, 1.0f, 1000.0f, 0.7f);
        setFilterData(highMidData, 0.0f, 1.0f, 3000.0f, 0.7f);
        setFilterData(highCutData, 0.0f, 1.0f, 20000.0f, 0.7f);

        gainProcessor.prepare(spec);

        if (transportSource)
        {
            transportSource->prepareToPlay(samplesPerBlockExpected, sampleRate);
        }        

        //==============================================================================
        // SPECTRUM SETUP
        if (spectrumAnalyser)
        {
            spectrumAnalyser->CalculateFFTIndices(sampleRate);
            spectrumAnalyser->setSmoothingLevel(sampleRate, 0.065f);
        }        
        //==============================================================================

        //==============================================================================
        // LEVEL METERS SETUP 
        if (leftPeakMeter)
        {
            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 getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
    {
        juce::ScopedLock audioLock(audioCallbackLock);

        juce::ScopedNoDenormals noDenormals;
      
        transportSource->getNextAudioBlock(bufferToFill);       
        juce::dsp::AudioBlock<float> block(*bufferToFill.buffer);        
        juce::dsp::ProcessContextReplacing<float> context(block);
                     
        lowCutProcessor.process(context);
        midProcessor.process(context);        
        highMidProcessor.process(context);
        highCutProcessor.process(context);

        gainProcessor.process(context);

        //==============================================================================
        // FOR LEVEL METERS   
        if (leftPeakMeter)
        {
            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 SPECTRUM ANALYSER
        if (spectrumAnalyser)
        {
            spectrumAnalyser->getNextAudioBlock(bufferToFill); // For modified data
        }       
        //==============================================================================        
    }

    void releaseResources() override
    {
        transportSource->releaseResources();
    }
    //==============================================================================
  
    //==============================================================================
    // PAINT AND RESIZE
    void paint(juce::Graphics& g) override
    {
        g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
        g.fillRect(getLocalBounds());
    }

    void resized() override
    {
        // Hardcoded for test purposes, the main frame is fixed size
        playButton.setBounds(20, 245, 80, 30);
        loadButton.setBounds(120, 245, 80, 30);
        byPassButton.setBounds(280, 245, 100, 30);
        volumeSlider.setBounds(795, 435, 70, 70);
        
        thumbnailComponent.setBounds(437, 245, 314, 60);
       
        lowCutFreq.setBounds(50, 315, 50, 150);
        lowCutByPass.setBounds(60, 290, 32, 22);

        midGain.setBounds(140, 313, 65, 65);
        midFreq.setBounds(140, 373, 65, 65);
        midQ.setBounds(140, 430, 68, 65);
        midByPass.setBounds(158, 290, 32, 22);

        highMidGain.setBounds(218, 313, 65, 65);
        highMidFreq.setBounds(218, 373, 65, 65);
        highMidQ.setBounds(218, 430, 63, 65);
        highMidByPass.setBounds(236, 290, 32, 22);

        highCutFreq.setBounds(304, 315, 50, 150);
        highCutByPass.setBounds(314, 290, 32, 22);        
    }
    //==============================================================================
    
    //==============================================================================
    // PLAY AND STOP
    void togglePlay()
    {
        if (playState)
            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::darkorange);
        playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);
    }

    void stop()
    {
        playState = false;

        if (transportSource.get() != nullptr)
        {
            transportSource->stop();
            transportSource->setPosition(0);

            playButton.setButtonText("Play");
            playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
            playButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);
        }
    }
    //==============================================================================

    //==============================================================================
    // UPDATE PARAMETERS
    enum FilterType
    {
        LowCut,
        Mid,
        HighMid,
        HighCut 
    };

    struct FilterData
    {
        float freq;
        float Q;
        float gainFactor;
        float gainDB;
    };

    void createRotarySlider(juce::Slider& slider, float peakValue, float minRange, float maxRange, float step)
    {
        addAndMakeVisible(slider);

        slider.setLookAndFeel(&customFilterKnob);
        customFilterKnob.setThumbnailRatio(0.7f);

        slider.setAlwaysOnTop(true);
        slider.setSliderStyle(juce::Slider::Rotary);
        slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 10, 10);
        slider.setRange(minRange, maxRange, step);
        slider.setValue(peakValue);
        slider.setPopupDisplayEnabled(true, true, this);
    }

    void createVerticalSlider(juce::Slider& slider, int peakValue, int minRange, int maxRange, int step)
    {
        addAndMakeVisible(slider);

        slider.setLookAndFeel(&customSlider);
        customSlider.setThumbnailRatio(0.9f);

        slider.setAlwaysOnTop(true);
        slider.setSliderStyle(juce::Slider::LinearVertical);
        slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 10, 10);
        slider.setRange(minRange, maxRange, step);
        slider.setValue(peakValue);
        slider.setPopupDisplayEnabled(true, true, this);
    }

    void createByPassToggle(juce::TextButton& byPassButton, bool byPassState)
    {
        addAndMakeVisible(byPassButton);
        byPassButton.setLookAndFeel(&customTextButton);
        byPassButton.setClickingTogglesState(true); 
        byPassButton.setButtonText("off"); 
        byPassState = false;
    }

    void setFilterData(FilterData& data, float gainDB, float gainFactor, float freq, float Q)
    {
        data.gainDB = gainDB;
        data.gainFactor = gainFactor;
        data.freq = freq;
        data.Q = Q;
    }

    void updateGain()
    {
        juce::ScopedLock audioLock(audioCallbackLock);

        // Gain slider's range is set between 0 and 100
        const float volumeLevel = static_cast<float>(volumeSlider.getValue()) / 100.0f;
        gainProcessor.setGainLinear(juce::jlimit(0.0f, 1.0f, volumeLevel));
    }

    void updateFilterParameters(const FilterType type, const FilterData & data)
    {
        juce::ScopedLock audioLock(audioCallbackLock);

        auto* device = audioDeviceManager.getCurrentAudioDevice();
        const double sampleRate = device->getCurrentSampleRate();
       
        switch (type)
        {
        case LowCut:
            *lowCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass(sampleRate, data.freq, 0.7f);           
            break;
        case Mid:
            *midProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, data.freq, data.Q, data.gainFactor);
            responseCurve->updateMidData(data.freq, data.gainDB, data.Q);
            break;
        case HighMid:
            *highMidProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter(sampleRate, data.freq, data.Q, data.gainFactor);
            responseCurve->updateHighMidData(data.freq, data.gainDB, data.Q);
            break;
        case HighCut:
            *highCutProcessor.state = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass(sampleRate, data.freq, 0.7f);            
            break;
        default:
            break;
        }
    }

    // BYPASS UPDATE
    void toggleLowCutByPass()
    {
        isLowCutOn = lowCutByPass.getToggleState();
        lowCutByPass.setButtonText(isLowCutOn ? "on" : "off");
        updateLowCutFrequency();
    }

    void toggleMidByPass()
    {
        isMidCutOn = midByPass.getToggleState();
        midByPass.setButtonText(isMidCutOn ? "on" : "off");
        updateMidFilterGainLevel();
    }

    void toggleHighMidByPass()
    {
        isHighMidCutOn = highMidByPass.getToggleState();
        highMidByPass.setButtonText(isHighMidCutOn ? "on" : "off");
        updateHighMidFilterGainLevel();
    }
    
    void toggleHighCutByPass()
    {
        isHighCutOn = highCutByPass.getToggleState();
        highCutByPass.setButtonText(isHighCutOn ? "on" : "off");
        updateHighCutFrequency();
    }

    void toggleByPass()
    {        
        if (!byPassState)
        {    
            byPassButton.setButtonText("Bypass On");
            byPassButton.setColour(juce::TextButton::buttonColourId, juce::Colours::lightgreen);
            byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::black);

            byPassState = true;
        }
        else
        {
            byPassButton.setButtonText("Bypass Off");
            byPassButton.setColour(juce::TextButton::buttonColourId,
                getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId).darker());
            byPassButton.setColour(juce::TextButton::textColourOffId, juce::Colours::white);

            byPassState = false;
        }
           
        updateLowCutFrequency();
        updateMidFilterGainLevel();
        updateHighMidFilterGainLevel();
        updateHighCutFrequency();
    }

    // LOW CUT FILTER
    void updateLowCutFrequency()
    {
        auto& data = lowCutData;
        
        if (isLowCutOn && !byPassState)  data.freq = lowCutFreq.getValue();
        else                             data.freq = 10.0f;

        updateFilterParameters(FilterType::LowCut, data);
        responseCurve->updateLowCutData(lowCutFreq.getValue());
    }

    // MID FILTER
    void updateMidFilterGainLevel()
    {
        auto& data = midData;
        data.gainDB = midGain.getValue();

        if (isMidCutOn && !byPassState)   data.gainFactor = std::pow(10.0f, midGain.getValue() / 20.0f);
        else                              data.gainFactor = 1.0f;

        updateFilterParameters(FilterType::Mid, data);
    }

    void updateMidFilterFrequency()
    {
        auto & data = midData;
        data.freq = midFreq.getValue();
        updateFilterParameters(FilterType::Mid, data);
    }

    void updateMidFilterQ()
    {
        auto & data = midData;
        data.Q = midQ.getValue();
        updateFilterParameters(FilterType::Mid, data);
    } 

    // HIGH MID FILTER
    void updateHighMidFilterGainLevel()
    {
        auto& data = highMidData;
        data.gainDB = highMidGain.getValue();

        if (isHighMidCutOn && !byPassState)  data.gainFactor = std::pow(10.0f, highMidGain.getValue() / 20.0f);
        else                                 data.gainFactor = 1.0f;
       
        updateFilterParameters(FilterType::HighMid, data);
    }

    void updateHighMidFilterFrequency()
    {
        auto& data = highMidData;
        data.freq = highMidFreq.getValue();
        updateFilterParameters(FilterType::HighMid, data);
    }

    void updateHighMidFilterQ()
    {
        auto& data = highMidData;
        data.Q = highMidQ.getValue();
        updateFilterParameters(FilterType::HighMid, data);
    }

    // HIGH CUT FILTER
    void updateHighCutFrequency()
    {
        auto& data = highCutData;

        if (isHighCutOn && !byPassState)  data.freq = highCutFreq.getValue();
        else                              data.freq = 20000.0f;

        updateFilterParameters(FilterType::HighCut, data);
        responseCurve->updateHighCutData(highCutFreq.getValue());
    }
    //==============================================================================

    AudioThumbnailComponent& getThumbnailComponent()
    {
        return thumbnailComponent;
    }

    //==============================================================================
    // OPEN AND LOAD 
    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;"));

        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))
                    {
                        thumbnailComponent.setCurrentURL(u);
                    }
                    else
                    {
                        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);
                    }
                }

                fileChooser = nullptr;
            }, nullptr);
    }

    bool loadURL(const juce::URL& fileToPlay)
    {
        stop();

        audioSourcePlayer.setSource(nullptr);
        getThumbnailComponent().setTransportSource(nullptr);
        transportSource.reset();
        readerSource.reset();
       
        auto source = std::make_unique<juce::URLInputSource>(fileToPlay);

        // Create stream and ensure proper ownership transfer 
        auto stream = std::unique_ptr<juce::InputStream>(source->createInputStream());

        if (!stream) return false;

        // Create reader for stream to transfer ownership
        reader = std::unique_ptr<juce::AudioFormatReader>(formatManager.createReaderFor(std::move(stream)));

        if (!reader) return false;

        // Reset readerSource with the new reader
        readerSource.reset(new juce::AudioFormatReaderSource(reader.get(), false));

        init();
        resized();

        return true;
    }
    //==============================================================================

    //==============================================================================
    void changeListenerCallback(ChangeBroadcaster* source) override
    {
        if (thumbnailComponent.isEnded())
        {
            stop();
        }

        if (source == &thumbnailComponent)
        {
            repaint();
        }
    }
    //==============================================================================

private:
    juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> lowCutProcessor;
    juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> midProcessor;
    juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> highMidProcessor;
    juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>> highCutProcessor;

    FilterData lowCutData;
    FilterData midData;
    FilterData highMidData;
    FilterData highCutData;

    juce::dsp::Gain<float> gainProcessor;

    AudioThumbnailComponent thumbnailComponent;
    ResponseCurveComponent* responseCurve = nullptr;
    SpectrumComponent* spectrumAnalyser = nullptr;
    LevelMeterComponent* leftPeakMeter = nullptr;
    LevelMeterComponent* rightPeakMeter = nullptr;
    LevelMeterComponent* leftRMSMeter = nullptr;
    LevelMeterComponent* rightRMSMeter = nullptr;

    std::unique_ptr<juce::FileChooser> fileChooser;
    juce::ScopedMessageBox messageBox;

    juce::AudioDeviceManager audioDeviceManager;
    juce::AudioFormatManager formatManager;
    juce::AudioSourcePlayer audioSourcePlayer;
    
    std::unique_ptr<juce::AudioFormatReader> reader;
    std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
    std::unique_ptr<juce::AudioTransportSource> transportSource;

    juce::CriticalSection audioCallbackLock;

    CustomLookAndFeel customVolumeKnob;
    CustomLookAndFeel customFilterKnob;
    CustomLookAndFeel customSlider;
    CustomLookAndFeel customTextButton;

    //==============================================================================    
    // LABEL SECTION 
    juce::TextButton loadButton{ "Load" }, playButton{ "Play" }, byPassButton{ "Bypass Off" };

    bool playState = false;
    bool byPassState = false;

    juce::Slider volumeSlider;
    juce::Slider lowCutFreq, highCutFreq;
    juce::Slider midFreq, midQ, midGain;
    juce::Slider highMidFreq, highMidQ, highMidGain;

    juce::TextButton lowCutByPass;
    juce::TextButton midByPass;
    juce::TextButton highMidByPass;
    juce::TextButton highCutByPass;

    bool isLowCutOn = false;
    bool isMidCutOn = false;
    bool isHighMidCutOn = false;
    bool isHighCutOn = false;
    //============================================================================== 
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPlayerComponent)
};