[Beginner] Lowpass filter not working properly

Hello, I’m a CS student and as part of a research study, my group and I had the task of creating an audio plug-in despite our little experience in C++ and absolutely no experience at all with the JUCE framework.

When making the filters, we quickly focused on the DSP module with the help of tutorials, especially the videos concerning the IIR filters from The Audio Programmer and the equalizer realized by Matkat music on the freeCodeCamp YTB channel.
But each time we encountered a problem where the lowpass filter does its job only in the range [0,~3000] and does nothing after.
We tried to find the source of the problem with our research tutor and we thought that there was maybe a conflict with another code that acts on the sound like the delay but for the moment we have no other track.

That’s why we decided to ask the question on this forum to perhaps have some leads that could help us solve our problem, this is the first time I post on a forum, I hope I formulated my problem correctly and thanks in advance for your answers.

You mean that if you set the cutoff frequency to less than 3000 Hz, the lowpass filter works and filters out the higher frequencies, but if you set the cutoff to higher than 3000 Hz, it no longer filters anything?

That’s not what is supposed to happen and you most likely have a bug in your code somewhere. (A typical mistake is that people use a single filter for both left and right channels, so make sure each channel has its own filter, or that at least the filter is setup to handle two channels.)

1 Like

For the channels in my processBlock I used the AudioBlock since when I did some research I did read some stuff about the channels, maybe I didn’t implement it correcly at all

This is what I did in my processBlock

void RunDubDelayAudioProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    int totalNumInputChannels = getTotalNumInputChannels();
    const int totalNumOutputChannels = getTotalNumOutputChannels();
    const int numSamples = buffer.getNumSamples();

    int dpr, dpw;
    
    if(auto* playHead = getPlayHead()){
        AudioPlayHead::CurrentPositionInfo info;
        playHead->getCurrentPosition(info);
        currentBPM.store(info.bpm);
        if(info.ppqPosition == 0) delayBuffer.clear();
    }

    // In case we have more outputs than inputs, this code clears any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    // This is here to avoid people getting screaming feedback
    // when they first compile a plugin, but obviously you don't need to keep
    // this code if your algorithm always overwrites all the output channels.
    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear(i, 0, buffer.getNumSamples());

    auto chainSettings = getChainSettings(apvts);

    updatePeakFilter(chainSettings);

    auto lowPassCoefficients = dsp::FilterDesign<float>::designIIRLowpassHighOrderButterworthMethod(chainSettings.lowPassFreq,
                                                                                                    getSampleRate(),
                                                                                                    2 * (chainSettings.lowPassSlope + 1));

    auto& leftLowPass = leftChain.get<ChainPositions::LowPass>();
    auto& rightLowPass = rightChain.get<ChainPositions::LowPass>();
    updateCutFilter(leftLowPass, lowPassCoefficients, chainSettings.lowPassSlope);
    updateCutFilter(rightLowPass, lowPassCoefficients, chainSettings.lowPassSlope);

    auto highPassCoefficients = dsp::FilterDesign<float>::designIIRHighpassHighOrderButterworthMethod(chainSettings.highPassFreq,
                                                                                                      getSampleRate(),
                                                                                                      2 * (chainSettings.highPassSlope + 1));

    auto& lefthighPass = leftChain.get<ChainPositions::HighPass>();
    auto& righthighPass = rightChain.get<ChainPositions::HighPass>();
    updateCutFilter(lefthighPass, highPassCoefficients, chainSettings.highPassSlope);
    updateCutFilter(righthighPass, highPassCoefficients, chainSettings.highPassSlope);

    dsp::AudioBlock<float> block(buffer);

    auto leftBlock = block.getSingleChannelBlock(0);
    auto rightBlock = block.getSingleChannelBlock(1);

    dsp::ProcessContextReplacing<float> leftContext(leftBlock);
    dsp::ProcessContextReplacing<float> rightContext(rightBlock);

    leftChain.process(leftContext);
    rightChain.process(rightContext);
    /*--------*/

    jassert (totalNumOutputChannels <= 2); // mono or stereo out

I will assume that I did it wrong and I’m gonna try to properly define two channels or setup a filter than can handle two channels. Thanks already.

maybe you accidently used your plugin on a mono track. because the way your code is setup right now that would not work in mono. when you are doing:

auto rightBlock = block.getSingleChannelBlock(1);

you assume that a 2nd channel always exists.
alternatively you can use a channel loop

for(auto ch = 0; ch < buffer.getNumChannels(); ++ch)
{
    auto rightBlock = block.getSingleChannelBlock(ch);
1 Like

I’ll look into that thanks !

Or let me point to dsp::ProcessorDuplicator, which does this behind the scenes:

Converts a mono processor class into a multi-channel version by duplicating it and applying multichannel buffers across an array of instances.

When the prepare method is called, it uses the specified number of channels to instantiate the appropriate number of instances, which it then uses in its process() method.

2 Likes

Not sure if that’s your original problem or just an additional one: You are designing your filter coefficient with each call to processBlock which is not what you should do. Designing a filter with this method is a relatively expensive operation and probably involves memory allocation so nothing that should ever be done in a process callback as it can lead to crackles and discontinuities.

You should rather only create new coefficients on actual parameter changes and leave the coefficients untouched otherwise between the blocks.

1 Like

I seen gonna try to correct that thanks !

Sorry for waking up a several weeks old topic, I did try to change the plugin from a mono settings to a stereo settings but unfortunately the problem is still the same with the lowpass filter :

PrepareToPlay :

void RunDubDelayAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{

    // Use this method as the place to do any pre-playback
    // initialisation that you need..
    delayBufferLength = (int)10.0 * (sampleRate);
    if (delayBufferLength < 1)
        delayBufferLength = 1;
    
    delayBuffer.setSize(2, delayBufferLength);
    delayBuffer.clear();
    
    if (MSBPMToggle) setDelayMS();
    else setDelayBPM(paramHolder.delayLengthBPM);
    
    ignoreUnused(samplesPerBlock);
    lastSampleRate = sampleRate;
    //mySynth.setCurrentPlaybackSampleRate(lastSampleRate);

    dsp::ProcessSpec spec;
    spec.maximumBlockSize = samplesPerBlock;
    spec.sampleRate = sampleRate;
    spec.numChannels = getTotalNumOutputChannels();

    lowpassFilter.reset();
    lowpassFilter.prepare(spec);

    highpassFilter.reset();
    highpassFilter.prepare(spec);
}

ProcessBlock :

    int dpr, dpw;

    // This is the place where you'd normally do the guts of your plugin's
    // audio processing...
    // Make sure to reset the state if your inner loop is processing
    // the samples and the outer loop is handling the channels.
    // Alternatively, you can process the samples with the channels
    // interleaved by keeping the same state.
    float* channelData0 = buffer.getWritePointer(0);
    float* channelData1 = buffer.getWritePointer(totalNumInputChannels == 2 ? 1 : 0);
    float* delayData0 = delayBuffer.getWritePointer(0);
    float* delayData1 = delayBuffer.getWritePointer(totalNumInputChannels == 2 ? 1 : 0);

    dpr = delayReadPosition;
    dpw = delayWritePosition;

    for (int i = 0; i < numSamples; ++i) {
        const float in0 = channelData0[i];
        const float in1 = channelData1[i];

        // DRY_WET
        float dw0 = (1 - paramHolder.dryWetMix) * in0 + paramHolder.dryWetMix * delayData0[dpr];
        float dw1 = (1 - paramHolder.dryWetMix) * in1 + paramHolder.dryWetMix * delayData1[dpr];

        // LPHP
        updateFilter();
        //float lphp0 = (lowpassFilter.processSample(dw0) + highpassFilter.processSample(dw0))/2;
        //float lphp1 = (lowpassFilter.processSample(dw1) + highpassFilter.processSample(dw1))/2;
        float lphp0 = lowpassFilter.processSample(highpassFilter.processSample(dw0));
        float lphp1 = lowpassFilter.processSample(highpassFilter.processSample(dw1));

UpdateFilter :

void RunDubDelayAudioProcessor::updateFilter() {
    lowpassFilter.parameters->type = dsp::StateVariableFilter::Parameters<float>::Type::lowPass;
    lowpassFilter.parameters->setCutOffFrequency(lastSampleRate, paramHolder.lpMix);
    highpassFilter.parameters->type = dsp::StateVariableFilter::Parameters<float>::Type::highPass;
    highpassFilter.parameters->setCutOffFrequency(lastSampleRate, paramHolder.hpMix);
}

The parameters where I define my filters in the PluginProcessor.h :

    struct ParamHolder : ParamManager::ParamHolderBase {
        Param<bool> bypass{ this, "Bypass", false };

        Param<float> delayLengthMS { this, "TimeMS", NormalisableRange(0.0f, 10000.0f, 10.0f), 500.0 };
        Param<int> delayLengthBPM { this, "TimeBPM", Range(0, 18), 1 };
        Param<float> feedback { this, "Feedback", NormalisableRange(0.0f, 0.995f, 0.005f), 0.75 };
        
        Param<float> dryWetMix { this, "DryWet", NormalisableRange(0.0f, 1.0f, 0.01f), 0.5 };
        Param<float> panMix { this, "Pan", NormalisableRange(0.0f, 1.0f, 0.01f), 0.5 };
        Param<float> widthMix { this, "Width", NormalisableRange(0.0f, 5.0f, 0.1f), 1.0 };

        Param<float> lpMix{ this, "LowPass", NormalisableRange(20.0f, 20000.0f, 0.01f, 1.f), 20000.0 };
        Param<float> hpMix{ this, "HighPass", NormalisableRange(20.0f, 20000.0f, 0.01f, 1.f), 20.0 };
        
        Param<float> speed { this, "Speed", NormalisableRange(0.0f, 17.0f, 0.01f), 0 };
        Param<float> amnt { this, "Amnt", NormalisableRange(0.0f, 20.0f, 0.01f), 0 };
    } paramHolder;
    ParamManager paramManager { this, paramHolder };

I’m feeling I’m going in circles with this filter, I tried to switch from mono to stereo and tried serial/parallel configurations, I’m starting to think that it’s another functionnality that interfere with the filters but if it is then I don’t know which one.