Restting dsp::LadderFilter

I have a polyphonic audio engine where each voice has a LadderFilter.

Before each voice plays, I call setCutoffFrequencyHz and reset the filter, but that doesn’t seem to reset the value smoothers inside the filter.

So whenever one of the polyphonic voices is reused, the filter will not play correctly in case the frequency has been changed since the last iteration on that voice, due to the value smoothers doing their thing on the frequency. This is most apparent when I use ADSR to control the cutoff, making the filter seems to behave randomly.

I don’t mind replacing LadderFilter in case it doesn’t play well with polyphonics but was wondering if there is a simple solution for this.

i had this same problem. i found it was because the ladder filter’s reset method also clears out the internal state. i fixed it by exposing the smoothers from the filter object

diff --git a/modules/juce_dsp/widgets/juce_LadderFilter.h b/modules/juce_dsp/widgets/juce_LadderFilter.h
index d1b21122d..af99183ab 100644
--- a/modules/juce_dsp/widgets/juce_LadderFilter.h
+++ b/modules/juce_dsp/widgets/juce_LadderFilter.h
@@ -119,6 +119,7 @@ protected:
     //==============================================================================
     SampleType processSample (SampleType inputValue, size_t channelToUse) noexcept;
     void updateSmoothers() noexcept;
+    SmoothedValue<SampleType> cutoffTransformSmoother, scaledResonanceSmoother;^M

 private:
     //==============================================================================
@@ -134,7 +135,6 @@ private:
     std::vector<std::array<SampleType, numStates>> state;
     std::array<SampleType, numStates> A;

-    SmoothedValue<SampleType> cutoffTransformSmoother, scaledResonanceSmoother;
     SampleType cutoffTransformValue, scaledResonanceValue;

     LookupTableTransform<SampleType> saturationLUT { [] (SampleType x) { return std::tanh (x); },

and then in my code doing this instead of calling filter.reset() directly when i’m recycling a voice:

filter.cutoffTransformSmoother.setCurrentAndTargetValue(filter.cutoffTransformSmoother.getTargetValue());
filter.scaledResonanceSmoother.setCurrentAndTargetValue(filter.scaledResonanceSmoother.getTargetValue());
1 Like

Thanks, that does help in terms of audio, since I’m no longer getting that annoying “oomph” effect.

The VU meter still picks up on something. I can see that it behaves differently for recycled voices compared to new voices. For a fraction of a second right at the beginning of a note there is a fast flicker suggesting that the smoothers are active, even though it’s not audible.

Hi @duvrin,

can you help me understand what the expected behaviour would be in your use case? I’ve played with this a bit, and when calling LadderFilter::reset() the smoothers are already being reset the way modosc suggested. So right after calling setCutoffFrequencyHz() and then reset() the smoothers should not be doing any smoothing.

However the filter state is also cleared entirely, which means that there will be a 5 sample long fade-in transient when using the filter this way. Maybe if you are trying to play a voice in a continuous loop and hoping to hear a constant sound, I could imagine those 5 samples being audible.

In this case I would wonder if calling reset() is even needed at all.

Hi Attila

It seems that you’re right. Looks like the error in my case is because of an ADSR misuse.
My use case is having a slow attack filter, so the 5 sample edge effect is not an issue.

Perhaps I’m overlooking something basic here? This how I implement juce::ADSR for filtering:

  • on a new/recycled note init, call adsr.reset() followed by adsr.noteOn()
  • also during init, call setCutoffFrequencyHz() and then reset()
  • on each processBlock(), create a temp buffer the size of block, fill it with 1s and apply the adsr to that buffer after calling adsr.setParameters() with the current parameters which stay constant for that purpose.
  • next, take the first sample from the temp buffer, and multiply it by the frequency used during init (meaning to call setCutoffFrequencyHz() again with applied frequency, only true in case ADSR is actually on)
  • finally call ladderFilter.process(processContext);

This produces different results for “out of the box” vs recycled voices. There is a whole lot more going on but I believe nothing is related to the filter/adsr. If the above steps should work in theory I’ll recheck that statement.

This sounds sufficiently complicated that I can’t quite piece together what the code exactly is, and what sound is expected to come out of this.

So just a few thoughts

  • I don’t know what the meaning of multiplying the first sample of the buffer with the frequency is. But if you are calling setCutoffFrequencyHz() more than once with different parameters, and LadderFilter::reset() only once, then there will be smoothing between those two different values.
  • The first sample of the temp buffer after having applied the adsr to it, will be very close to 0. So multiplying with this value may introduce lots of randomness.

The meaning of multiplying the first sample is a bypass around calling setCutoffFrequencyHz() for each and every sample in the audio buffer, which I think is overkill for filter ADSR (I won’t do that for volume ADSR). So instead, I call setCutoffFrequencyHz() just once per processBlock with the value I extracted from that dummy buffer, which I just fill with 1s and apply the ADSR on it.

But that’s a good point you raise about the set/reset call ratio. Definately something I need to consider.

another thought - this might be easier if the filter code was unrolled (or whatever you call it when you convert from a recurrence relation) so the state’s not necessary?

Somehow by adding another call to setCutoffFrequencyHz() during voiceOff clean up (the function that gets called on midi noteOff message to stop the voice), it solved my problems and the voices are now consistent. I don’t ask how and why…

But I realize my filter ADSR assignment is a bit hacky. Can you guys share how you implement ADSR values for LadderFilter? No need to consider multiple voices, just a plain implemntaion of juce::ADSR class.