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!
