StateVariableTPTFilter produces NaN after reset() if prepare() not called again

Hi all, I’m building a polyphonic sampler with per-voice filters using juce::dsp::StateVariableTPTFilter. I’m trying to avoid calling prepare() on the audio thread to avoid allocation by pre-preparing all voice filters once during initialization.

I have 32 voices, each with its own filter:

struct Voice {
juce::dsp::StateVariableTPTFilter filter;
// … other voice state
};

Voice voices[32];

What I tried:

In my prepare() method (called from message thread at startup):

void Sampler::prepare(double sampleRate, int blockSize)
{
juce::dsp::ProcessSpec spec{sampleRate, static_castjuce::uint32(blockSize), 1};
for (auto& voice : voices)
{
voice.filter.setType(juce::dsp::StateVariableTPTFilterType::lowpass);
voice.filter.setCutoffFrequency(20000.0f);
voice.filter.setResonance(0.707f);
voice.filter.prepare(spec);
voice.filter.reset();
}
}

In my note trigger (called from audio thread):

void Sampler::triggerSample(int note, float velocity)
{
Voice* voice = findFreeVoice();
voice->filter.reset(); // Clear filter state for new note
// … start voice
}

In my process loop:

float cutoff = calculateCutoff();
voice.filter.setCutoffFrequency(cutoff);
voice.filter.setResonance(0.707f);
float output = voice.filter.processSample(0, input);

With this approach, processSample() returns nan for all inputs.

The only way I got it working was to call prepare() again after reset() on each note trigger:

void Sampler::triggerSample(int note, float velocity)
{
Voice* voice = findFreeVoice();
voice->filter.reset();
voice->filter.prepare({sampleRate, static_castjuce::uint32(blockSize), 1}); // Must re-prepare!
// … start voice
}

This works but allocates on the audio thread, which I’m trying to avoid.

Is this expected behavior? Does reset() invalidate state that prepare() sets up? Is there a way to pre-allocate the filter and only call reset() per-note without getting NaN? Should I be using a different approach for per-voice filters in a polyphonic context?

Environment

  • JUCE 7.x
  • macOS, 48kHz, 512 samples

Thanks for any insight!

This sounds to me that the problem is somewhere else but it’s manifesting in the filter. Or findFreeVoice returns a Voice object that hasn’t been prepared.

reset() simply sets the state of the filter to 0. prepare() allocates memory, calls reset(), and calculates the filter coefficients. The only reason you’d get NaN out of this filter is if a) you never actually called prepare() on it, b) you give it an invalid cutoff frequency at some point, or c) you’re feeding it NaNs as input.

1 Like