LadderFilter artefacts

I am experiencing some crackling when using LadderFilter, I assume I’m probably doing something wrong.

Here’s an example PIP of my code, switching it for LPF and passing in a saw wave to be processed also produces some “warbling” with lower cutoff frequencies, neither the warbling or the crackling are desirable artefacts and I doubt they should be present, hence my thinking that I’ve got something amiss.

 /*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

  name:             ladderTest

  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, 
                    juce_audio_processors, juce_audio_utils, juce_core, juce_data_structures, juce_dsp, 
                    juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
  exporters:        xcode_mac

  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

  type:             AudioProcessor
  mainClass:        MyPlugin

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once


//==============================================================================
class MyPlugin  : public AudioProcessor
{
public:
    //==============================================================================
    MyPlugin()
        : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                           .withOutput ("Output", AudioChannelSet::stereo())),
        apvts(*this, nullptr, getName(), createParameterLayout())
    {
    }

    ~MyPlugin()
    {
    }

	AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
	{
	    std::vector<std::unique_ptr<RangedAudioParameter>> params;

	    auto hpe = std::make_unique<AudioParameterBool>("hp_enabled", "High Pass Enabled", false, "",
	                                                    [](bool value, int maxLen) {
	                                                        return String(value ? "On" : "Off");
	                                                    });
	    highpassEnabled = hpe.get();
	    params.push_back(std::move(hpe));

	    auto hpc = std::make_unique<AudioParameterFloat>("hp_cutoff", "High Pass Cutoff",
	                                                     NormalisableRange<float>(50.0f, 22000.0f, 0.1f), 50.0f, "");
	    highpassCutoff = hpc.get();
	    params.push_back(std::move(hpc));

	    auto hpr = std::make_unique<AudioParameterFloat>("hp_res", "High Pass Resonance", 0.0f, 1.0f, 0.0f);
	    highpassRes = hpr.get();
	    params.push_back(std::move(hpr));

	    auto hpd = std::make_unique<AudioParameterFloat>("hp_drive", "High Pass Drive", 1.0f, 11.0f, 1.0f);
	    highpassDrive = hpd.get();
	    params.push_back(std::move(hpd));

	    auto hps = std::make_unique<AudioParameterBool>("hp_slope", "High Pass Slope", false, "",
	                                                    [](bool value, int maxLen) {
	                                                        return String(value ? "24dB" : "12dB");
	                                                    });
	    highpassSlope = hps.get();
	    params.push_back(std::move(hps));

        return { params.begin(), params.end() };
	}

    //==============================================================================
    void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
	    const dsp::ProcessSpec processSpec {
	        sampleRate,
	        static_cast<juce::uint32>(samplesPerBlock),
	        static_cast<juce::uint32>(getTotalNumInputChannels())
	    };

	    highpassLadder.prepare(processSpec);
	    highpassLadder.reset();
    }

    void releaseResources() override
    {
    }

    void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        ScopedNoDenormals noDenormals;
        auto totalNumInputChannels  = getTotalNumInputChannels();
        auto totalNumOutputChannels = getTotalNumOutputChannels();

        for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
            buffer.clear (i, 0, buffer.getNumSamples());

	    highpassLadder.setMode(*highpassSlope ? dsp::LadderFilter<float>::Mode::HPF24 : dsp::LadderFilter<float>::Mode::HPF12);
	    highpassLadder.setEnabled(*highpassEnabled);
	    highpassLadder.setDrive(*highpassDrive);
	    highpassLadder.setCutoffFrequencyHz(*highpassCutoff);
	    highpassLadder.setResonance(*highpassRes);

	    dsp::AudioBlock<float> block(buffer);
	    const dsp::ProcessContextReplacing<float> processContext { block };

	    highpassLadder.process(processContext);
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                        { return false;   }

    //==============================================================================
    const String getName() const override                  { return "ladderTest"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return 1; }
    int getCurrentProgram() override                       { return 0; }
    void setCurrentProgram (int) override                  {}
    const String getProgramName (int) override             { return {}; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override
    {
        // You should use this method to store your parameters in the memory block.
        // You could do that either as raw data, or use the XML or ValueTree classes
        // as intermediaries to make it easy to save and load complex data.
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        // You should use this method to restore your parameters from this memory block,
        // whose contents will have been created by the getStateInformation() call.
    }

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        // This is the place where you check if the layout is supported.
        // In this template code we only support mono or stereo.
        if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
            && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
            return false;

        // This checks if the input layout matches the output layout
        if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
            return false;

        return true;
    }

private:
    //==============================================================================

    AudioProcessorValueTreeState apvts;
    AudioParameterFloat* highpassCutoff;
    AudioParameterFloat* highpassRes;
    AudioParameterFloat* highpassDrive;
    AudioParameterBool*  highpassEnabled;
    AudioParameterBool*  highpassSlope;
    dsp::LadderFilter<float> highpassLadder;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyPlugin)
};

Any help would be appreciated.

Pretty sure it’s this classic pitfall:

Before you start creating an array of states, have a look at ProcessorDuplicator

1 Like

I kind of assumed given this method exists that LadderFilter was taking care of multiple channels by itself.

So now my question is, if I’m using ProcessorDuplicator, I’d need something like:

ProcessorDuplicator<dsp::LadderFilter<float>, ???> duplicator;

what do I put in place of ??? and how do I set the state for the LadderFilter inside the ProcessDuplicator?

I’ve used the IIRFilter successfully with ProcessorDuplicator before with no problem, I understood how that works, but I’m a bit stumped with LadderFilter.

Having a look at the process method for LadderFilter

        for (size_t n = 0; n < numSamples; ++n)
        {
            updateSmoothers();

            for (size_t ch = 0; ch < numChannels; ++ch)
                outputBlock.getChannelPointer (ch)[n] = processSample (inputBlock.getChannelPointer (ch)[n], ch);
        }

it’s looping over samples first and then channels inside, processSample uses a separate state per channel, so my assumption seems on the face of it correct.

From the source code it looks like the LadderFilter is taking care about multichannel states itself.

Edit: @richie what a timing :wink:

This is called in the prepare method:

void setNumChannels (size_t newValue)   { state.resize (newValue); }
1 Like

Like that card game for kids: SNAP!

So I wonder what else is it that I’m doing wrong, because there is no way that the crackle and warble effects I hear are something that a real Moog filter would do! :joy:

I found the error:

ighpassLadder.setMode(*highpassSlope ? dsp::LadderFilter<float>::Mode::HPF24 : dsp::LadderFilter<float>::Mode::HPF12);

This method calls reset() which sets the states to zero. You only should set this when you change the type, not each time you process a block

2 Likes

Brilliant! Thank you. Such a simple mistake.

JUCE team could we get something added to the documentation to note that calling this will reset the filter and should not be done from processBlock? It is sort of obvious when you think about it, but a little note would help eedjits like me in future :wink:

1 Like