Pirkle's WDFTunableButterLPF3 instantiation, no sound out from processAudioBlock()?

I might be missing something completely trivial here, but it’s driving me crazy. I’m trying to implement one of the WDF filters from Pirkle’s book, the documentation for which is here

private:
    WDFTunableButterLPF3 lpfLeft, lpfRight;
    juce::AudioParameterFloat * cutoffFreqParameter;

cutoffFreqParameter is attached to a GUI slider.

The createWDF() is called in the class constructor, and I call the reset() in the processor’s prepareToPlay().

void AnalogFiltersAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    lpfLeft.reset(sampleRate);
    lpfRight.reset(sampleRate);
    lpfLeft.setUsePostWarping(true);
    lpfRight.setUsePostWarping(true);
}

This is my processBlock() code:

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

    auto fc = cutoffFreqParameter->get();
    lpfLeft.setFilterFc((double)fc);
    lpfRight.setFilterFc((double)fc);

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

    auto currentOutChannels = getNumOutputChannels();
    for (int channel = 0; channel < currentOutChannels; ++channel)
    {
        auto* channelData = buffer.getWritePointer (channel);
        auto numSamples = buffer.getNumSamples();
        for (auto sample = 0; sample < numSamples; sample++) {
            float xn, yn;
            xn = channelData[sample];
            if (channel == 1) // right channel
                yn = (float)lpfRight.processAudioSample((double)xn);
            else
                yn = (float)lpfLeft.processAudioSample((double)xn);
            channelData[sample] = yn;
        }
    }
}

When I bypass the plugin, I get sound out in the AudioPluginHost as expected, but as soon as I turn the plugin on, even when the cutoff is set to 20,000Hz on the slider, there is no sound out.

I’ve checked setFilterFc() with the debugger and confirmed that the filter objects are receiving the expected value for cutoff. When I break at channelData[sample] = yn; I see that yn is significantly smaller in magnitude than xn (practically non-zero).

I can’t tell whether I’m misunderstanding how to use the WDFTunableButterLPF3 class or if I’m messing up somewhere in my processBlock() loops. There shouldn’t be any issue of saving the inner loop state as far as I can tell, because I believe each WDFTunableButterLPF3 tracks that internally.

UPDATE: Here’s some more confusing tests and results in the AudioPluginHost:

Audio Input directly into Audio Output, buggy plugin not added to host yet: clean audio.
Buggy plugin added to host, nothing connected to inputs or outputs: choppy audio, even though it isn’t hooked up.
Audio Input connected to buggy plugin, outputs of plugin connected to Audio Ouptut: no sound??
Audio Input and Audio Output disconnected again: choppy audio.
Buggy plugin deleted from host: clean audio.

shouldn’t be clearing the buffer if you need to filter it.

Good point. Deleting that did fix the issue of stuttering, and now if the plugin is loaded in the host the audio is still clean. But still when I try and pass audio through the plugin directly there’s no sound out.

I printed out some examples of xn and yn in the debugging log, and was not able to get a sample of yn greater than 0.002f. Maybe I’m not using processAudioSample() properly.

UPDATE tried swapping out my WDFTunableButterLPF3 objects with the very similar WDFButterLPF3 class and that runs fine, so it’s definitely an issue with the class.

Okay, I figured it out, and thought I’d leave the solution here in case anybody else ever gets stuck trying to use this.

In fxobjects.h from Pirkle’s site, this is the code for the function WDFTunableButterLPF3::setFilterFc():

/** parameter setter for fc */
void setFilterFc(double fc_Hz)
{
	if (useFrequencyWarping)
	{
		double arg = (kPi*fc_Hz) / sampleRate;
		fc_Hz = fc_Hz*(tan(arg) / arg);
	}

	seriesAdaptor_L1.setComponentValue(L1_norm / fc_Hz);
	parallelAdaptor_C1.setComponentValue(C1_norm / fc_Hz);
	seriesTerminatedAdaptor_L2.setComponentValue(L2_norm / fc_Hz);
}

The problem is that setComponentValue() does not update the A and B coefficients of the parallel and series adaptors, respectively. And those coefficients are what control the reflected summations and consequently the filter cutoff.
There are probably better ways to fix this, but since the coefficients get called in the WdfAdaptorBase::initialize() functions, I just re-initialized the chain at the end of the cutoff-setting function:

/** parameter setter for fc */
void setFilterFc(double fc_Hz)
{
    if (useFrequencyWarping)
    {
	    double arg = (kPi*fc_Hz) / sampleRate;
	    fc_Hz = fc_Hz*(tan(arg) / arg);
    }

    seriesAdaptor_L1.setComponentValue(L1_norm / fc_Hz);
    parallelAdaptor_C1.setComponentValue(C1_norm / fc_Hz);
    seriesTerminatedAdaptor_L2.setComponentValue(L2_norm / fc_Hz);

    /* re-initialize */
    seriesAdaptor_L1.initializeAdaptorChain(); // changed impedances impact A and B coefficients of adaptors
}

i know you solved your issue already, but another thing you could do is getting rid of this if statement, because you’re looping through the channels anyway and rather just directly process channelData[sample] = (float)lpf[channel].processAudioSample((double)xn); considering that you make your filter an std::vector. that would also be neat because it makes it flexible for all possible channel counts

Good point! Thanks for the tip!

This is clearing the buffers of any output channels that don’t have matching input channels.

I’m not sure the exact reason for this (my guess is that perhaps if there is no input channel, then the output buffer may be full of nonsense data), and I think it may not always be necessary depending on how you write your DSP code.

The loop is starting from the totalNumInputChannels and since the buffers are 0 indexed that means it would start after the last input channel, and then running while i < totalNumOutputChannels.

So in a stereo → stereo plugin totalNumInputChannels == totalNumOutputChannels and the for loop never even executes.