Hi all,
I’m developing a stereo imaging and modulation plugin that features a step sequencer with 4 tabs (Pan, Volume, Width, Pitch), each with up to 64 steps supporting per-step shapes, multiband values, mirror/invert flags etc. The plugin supports MIDI keyswitch-triggered preset switching, the user stores different sequencer configurations as “motions” and triggers them via MIDI notes during live performance.
The problem: There’s a perceptible audio gap/artifact when switching between motions during playback. I’ve narrowed it down to message thread latency during the switch.
Current architecture:
Motion states are stored internally as std::unique_ptr<juce::XmlElement>. When a MIDI keyswitch fires:
-
Audio thread: Detects the note, starts a 4ms
SmoothedValuefade-out to silence, pushes note to a lock-free FIFO, wakes the message thread viatriggerAsyncUpdate() -
Message thread (in
handleAsyncUpdate):-
Auto-saves the current motion:
apvts.copyState()+ serialises ~3,500 step data fields into the ValueTree as children, then converts to XML -
Loads the new motion:
ValueTree::fromXml()on the stored XML (~3,600 children), builds a HashMap for step data extraction, iterates all children to apply ~96 global params viasetValueNotifyingHost() -
Calls
forceParamSync()(sets an atomic flag)
-
-
Audio thread: Detects the flag, waits for gate < 0.001, resets DSP state (IIR filters, analysis accumulators, delay lines are preserved), snaps all
SmoothedValueinstances, starts 8ms fade-in
The step data itself was recently moved out of APVTS (it was ~3,584 registered parameters causing listener storms). Step data now lives as a plain StepData[64] array on the processor, written by the UI thread under a SpinLock and read directly by the audio thread. Only ~96 global params remain in APVTS.
The bottleneck: The message thread work between fade-out and fade-in takes ~15-20ms:
-
serializeStepDataInto(): appends ~3,500 ValueTree children (~5ms) -
ValueTree::fromXml(): parses stored XML with ~3,600 elements (~5ms) -
deserializeStepDataInto(): HashMap construction from ~3,600 entries (~3ms) -
Global param iteration + backward compat scans (~3ms)
This creates a ~20ms silence gap that’s clearly audible during rhythmic playback.
What I’m planning: Cache step data as raw StepData[64] arrays directly in each motion slot (simple memcpy on save/load), and store only the ~96 global params in a lightweight ValueTree, eliminating all XML parsing and large ValueTree construction from the hot path. This should bring message thread work down to ~1-2ms.
My questions:
-
Is there a better pattern for fast preset switching in JUCE plugins? Plugins like Stutter Edit 2 and CableGuys’ ShaperBox achieve near-instant switches. Are they likely doing the state swap entirely on the audio thread, or is message-thread dispatch with minimal work the standard approach?
-
For the ~96 global params that go through APVTS: Is
setValueNotifyingHost()in a loop the fastest way to apply them, or is there a faster bulk-update path that batches the listener notifications? I’ve already moved fromreplaceState()to differential application (only touching params that actually changed). -
Any concerns with storing the “authoritative” step sequencer state as a plain C struct array on the processor (written by message thread under SpinLock, read locklessly by audio thread), rather than as APVTS parameters? This eliminated the 3,584-parameter overhead but I want to make sure I’m not missing an edge case with host state save/recall, currently I serialise the array into the APVTS ValueTree in
getStateInformation()and deserialise insetStateInformation(). -
triggerAsyncUpdate()dispatch latency: I’m seeing 1-5ms between the audio thread callingtriggerAsyncUpdateand the message thread enteringhandleAsyncUpdate. Is this typical? Is there any way to reduce this, or is moving more of the switch logic to the audio thread the only path to sub-5ms transitions?
Running JUCE 8 on Windows (VS2022), targeting VST3. Any insights from people who’ve built similar performance-oriented switching would be hugely appreciated.
Thanks!
