Adding a Peaking Filter to a Simple Equalizer

Introduction

In the previous article, I explained how to create a simple equalizer with low-cut and high-cut filters.

In this article, I am going to explain how to add a peaking filter to this. The appearance will look like the following:

Prerequisites

You should have already created a simple equalizer as described in the following article.

How to Make a Simple Low-Cut & High-Cut Filter

DSP

Configuring APVTS parameters

First, we will add member variables for the parameters of the peaking filter to ChainSettings, the structure we defined to get the parameters from the apvts object.

struct ChainSettings
{
    float lowCutFreq { 0 }, peakFreq { 0 }, highCutFreq { 0 };
    float lowCutQuality { 0.71f }, peakQuality { 0.71f }, highCutQuality { 0.71f };
    float peakGain { 0 };
};

In addition, add one Filter to the ProcessorChain and a function updatePeakFilter() to update the peaking filter coefficients:

class EqTutorialAudioProcessor  : public juce::AudioProcessor
{
・・・
private:
    using Filter = juce::dsp::IIR::Filter<float>;
    using MonoChain = juce::dsp::ProcessorChain<Filter, Filter, Filter>;
    
    MonoChain leftChain, rightChain;
    
    enum ChainPositions
    {
        LowCut,
        Peak,
        HighCut
    };

    void updateLowCutFilter (const ChainSettings& chainSettings);
    void updatePeakFilter (const ChainSettings& chainSettings);
    void updateHighCutFilter (const ChainSettings& chainSettings);
    
    void updateFilters();
    
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EqTutorialAudioProcessor)
};

Now, let’s edit the cpp file. The parameters managed by the APVTS object were defined in the ParameterLayout function. Therefore, add here the parameters for the peaking filter: cutoff frequency, Q, and gain.

juce::AudioProcessorValueTreeState::ParameterLayout EqTutorialAudioProcessor::createParameterLayout()
{
・・・
    layout.add (std::make_unique<juce::AudioParameterFloat> ("Peak Freq",
                                                             "Peak Freq",
                                                             juce::NormalisableRange<float> (40.f, 18000.f, 1.f, 0.25f),
                                                             1000.f,
                                                             juce::String(),
                                                             juce::AudioProcessorParameter::genericParameter,
                                                             [](float value, int) { return (value < 1000.0f) ?
                                                                 juce::String (value, 0) + " Hz" :
                                                                 juce::String (value / 1000.0f, 1) + " kHz"; },
                                                             nullptr));

    layout.add (std::make_unique<juce::AudioParameterFloat> ("Peak Quality",
                                                             "Peak Quality",
                                                             juce::NormalisableRange<float> (0.1f, 7.f, 0.01f, 0.25f),
                                                             0.71f));

    layout.add (std::make_unique<juce::AudioParameterFloat> ("Peak Gain",
                                                             "Peak Gain",
                                                             juce::NormalisableRange<float> (-24.f, 24.f, 0.1f, 1.f),
                                                             0.0f,
                                                             juce::String(),
                                                             juce::AudioProcessorParameter::genericParameter,
                                                             [](float value, int) { return juce::String (value, 1) + " dB"; },
                                                             nullptr));
・・・
}

Then, add the process of getting the parameters of the peaking filter to getChainSettings(), the function that gets the parameters managed by the apvts object.

ChainSettings getChainSettings (juce::AudioProcessorValueTreeState& apvts)
{
    ChainSettings settings;
    
    settings.lowCutFreq = apvts.getRawParameterValue ("LowCut Freq")->load();
    settings.lowCutQuality = apvts.getRawParameterValue ("LowCut Quality")->load();
    settings.peakFreq = apvts.getRawParameterValue ("Peak Freq")->load();
    settings.peakQuality = apvts.getRawParameterValue ("Peak Quality")->load();
    settings.peakGain = apvts.getRawParameterValue ("Peak Gain")->load();
    settings.highCutFreq = apvts.getRawParameterValue ("HighCut Freq")->load();
    settings.highCutQuality = apvts.getRawParameterValue ("HighCut Quality")->load();

    return settings;
}

Peaking filter coefficients

To calculate the coefficients for the peaking filter, we will use a convenient function called makePeakFilter().

void EqTutorialAudioProcessor::updatePeakFilter (const ChainSettings& chainSettings)
{
    auto peakCoefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (getSampleRate(),
                                                                                 chainSettings.peakFreq,
                                                                                 chainSettings.peakQuality,
                                                                                 juce::Decibels::decibelsToGain (chainSettings.peakGain));
    
    *leftChain.get<ChainPositions::Peak>().coefficients = *peakCoefficients;
    *rightChain.get<ChainPositions::Peak>().coefficients = *peakCoefficients;
}

Finally, add updatePeakFilter() to updateFilters(), a function for updating multiple filter coefficients.

void EqTutorialAudioProcessor::updateFilters()
{
    auto chainSettings = getChainSettings (apvts);
    
    updateLowCutFilter (chainSettings);
    updatePeakFilter (chainSettings);
    updateHighCutFilter (chainSettings);
}

Now, the additional implementation on the DSP side is complete.

UI

Preparing NumberBox and SliderAttachment

Additional implementation on the UI side is even easier. Declare the NumberBox and SliderAttachment for the peaking filter, and define the green color to be applied.

class EqTutorialAudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    EqTutorialAudioProcessor& audioProcessor;
    
    NumberBox lowCutFreqBox;
    NumberBox lowCutQualityBox;
    
    NumberBox peakFreqBox;
    NumberBox peakQualityBox;
    NumberBox peakGainBox;
        
    NumberBox highCutFreqBox;
    NumberBox highCutQualityBox;
    
    using Attachment = juce::AudioProcessorValueTreeState::SliderAttachment;
    
    Attachment lowCutFreqAttachment,
               lowCutQualityAttachment,
               peakFreqAttachment,
               peakQualityAttachment,
               peakGainAttachment,
               highCutFreqAttachment,
               highCutQualityAttachment;

    juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.f, 1.f);
    juce::Colour stGreen = juce::Colour::fromFloatRGBA (0.34f, 0.74f, 0.66f, 1.f);
    juce::Colour yellow = juce::Colour::fromFloatRGBA (1.f, 0.71f, 0.2f, 1.f);
    juce::Colour black = juce::Colour::fromFloatRGBA (0.08f, 0.08f, 0.08f, 1.f);
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EqTutorialAudioProcessorEditor)
};

Next, initialize the Initialize the SliderAttachment object you prepared earlier, and set the color and visibility of the NumberBox as show below:

EqTutorialAudioProcessorEditor::EqTutorialAudioProcessorEditor (EqTutorialAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p),
      lowCutFreqAttachment (audioProcessor.apvts, "LowCut Freq", lowCutFreqBox),
      lowCutQualityAttachment (audioProcessor.apvts, "LowCut Quality", lowCutQualityBox),
      peakFreqAttachment (audioProcessor.apvts, "Peak Freq", peakFreqBox),
      peakQualityAttachment (audioProcessor.apvts, "Peak Quality", peakQualityBox),
      peakGainAttachment (audioProcessor.apvts, "Peak Gain", peakGainBox),
      highCutFreqAttachment (audioProcessor.apvts, "HighCut Freq", highCutFreqBox),
      highCutQualityAttachment (audioProcessor.apvts, "HighCut Quality", highCutQualityBox)

{
    setSize (400, 200);
    addMouseListener (this, true);
    
    lowCutFreqBox.setColour (juce::Slider::textBoxTextColourId, blue);
    lowCutQualityBox.setColour (juce::Slider::textBoxTextColourId, blue.darker(0.3));
    
    peakFreqBox.setColour (juce::Slider::textBoxTextColourId, stGreen);
    peakQualityBox.setColour (juce::Slider::textBoxTextColourId, stGreen.darker(0.3));
    peakGainBox.setColour (juce::Slider::textBoxTextColourId, stGreen.darker(0.3));

    highCutFreqBox.setColour (juce::Slider::textBoxTextColourId, yellow);
    highCutQualityBox.setColour (juce::Slider::textBoxTextColourId, yellow.darker(0.3));
    
    addAndMakeVisible (lowCutFreqBox);
    addAndMakeVisible (lowCutQualityBox);
    addAndMakeVisible (peakFreqBox);
    addAndMakeVisible (peakQualityBox);
    addAndMakeVisible (peakGainBox);
    addAndMakeVisible (highCutFreqBox);
    addAndMakeVisible (highCutQualityBox);
}

Setting the placement and size

We need to adjust the placement of all the filters to balance the placement between them, since we increased the size of the screen a bit.

void EqTutorialAudioProcessorEditor::resized()
{
    lowCutFreqBox.setBounds(10, getHeight() / 2 - 20, 80, 20);
    lowCutQualityBox.setBounds(10, getHeight() / 2 + 10, 80, 20);
    
    peakFreqBox.setBounds(110, getHeight() / 2 - 20, 80, 20);
    peakQualityBox.setBounds(110, getHeight() / 2 + 10, 80, 20);
    peakGainBox.setBounds(110, getHeight() / 2 + 40, 80, 20);
    
    highCutFreqBox.setBounds(210, getHeight() / 2 - 20, 80, 20);
    highCutQualityBox.setBounds(210, getHeight() / 2 + 10, 80, 20);
}

Setting the lock-on marks

Finally, Add the following implementation so that only selected NumberBox are marked as locked on.

void EqTutorialAudioProcessorEditor::mouseDown (const juce::MouseEvent& event)
{
        if (lowCutFreqBox.getLockedOnState())
        {
            lowCutFreqBox.setLockedOnState (false);
            lowCutFreqBox.repaint();
        }
    
        if (lowCutQualityBox.getLockedOnState())
        {
            lowCutQualityBox.setLockedOnState (false);
            lowCutQualityBox.repaint();
        }
    
        if (peakFreqBox.getLockedOnState())
        {
            peakFreqBox.setLockedOnState (false);
            peakFreqBox.repaint();
        }
    
        if (peakQualityBox.getLockedOnState())
        {
            peakQualityBox.setLockedOnState (false);
            peakQualityBox.repaint();
        }
    
        if (peakGainBox.getLockedOnState())
        {
            peakGainBox.setLockedOnState (false);
            peakGainBox.repaint();
        }
                
        if (highCutFreqBox.getLockedOnState())
        {
            highCutFreqBox.setLockedOnState (false);
            highCutFreqBox.repaint();
        }
    
        if (highCutQualityBox.getLockedOnState())
        {
            highCutQualityBox.setLockedOnState (false);
            highCutQualityBox.repaint();
        } 
}

All implementations are now complete. Let’s build and check it out.

Conclusion

In this article, I explained how to add a peaking filter to the simple equalizer we created in the previous article. I hope this article will be useful for someone else.

Finally, if there are any misinterpretations or better ways to implement this, please let me know in the comments or DM. Thank you for reading to the end!

References

Comments

Copied title and URL