Skip to content

fix: resolve review findings (audio-thread safety, robustness)#19

Merged
yves-vogl merged 4 commits into
mainfrom
fix/review-findings
Jul 16, 2026
Merged

fix: resolve review findings (audio-thread safety, robustness)#19
yves-vogl merged 4 commits into
mainfrom
fix/review-findings

Conversation

@yves-vogl

Copy link
Copy Markdown
Contributor

Summary

Three review findings from the 2026-07-15 suite-wide review sweep, each with its own regression test and its own commit:

  • Fixes BandCompressor: limiter ballistics freeze (not track input) while disabled, contradicting documented no-pop-on-re-enable guarantee #12 — BandCompressor's High-band limiter froze its internal ballistics (BallisticsFilter envelope) while limiterEnabled == false, because toggling juce::dsp::Limiter's own context.isBypassed short-circuits to a copy-through before the envelope update in both JUCE 8.0.14 Limiter::process() and its two internal Compressor::process() calls — contradicting the documented "no pop on re-enable" guarantee. The limiter now always runs at full strength into a preallocated scratch buffer, splicing the limited result back into the output only when enabled, so re-enabling resumes from a state consistent with the live input.
  • Fixes TriptychEngine: unsmoothed 0/1 Mute/Solo gain causes audible click on toggle mid-playback #13 — Per-band Mute/Solo resolved to a bare block-rate 0.0f/1.0f gain multiplier, producing an audible click at the block boundary on toggle mid-playback (independent of, and in addition to, each band's compressor/limiter envelopes already staying continuous underneath). The resolved gain is now ramped through a juce::SmoothedValue per band, matching every other real-time-varying scalar in this engine.
  • Fixes TriptychEngine::process: block larger than prepared capacity leaves excess samples as unprocessed dry passthrough #14 — A host-supplied block larger than the capacity established in prepareToPlay() left the excess samples as unprocessed dry passthrough (bypassing every stage, including the master Output trim) instead of being fully processed. TriptychEngine::process() now chunks any oversized block into <=-capacity pieces, each run through the full signal chain via a new processChunk().

Each fix has a dedicated Catch2 regression test (verified to fail against the pre-fix code and pass against the fix): tests/BandCompressorTests.cpp (#12), tests/MuteSoloAndLimiterTests.cpp (#13), tests/EngineTests.cpp (#14). Suite is 42/42 green locally (39 existing + 3 new). docs/architecture.md, CLAUDE.md, and CHANGELOG.md (## [Unreleased]### Fixed) updated accordingly.

JUCE 8.0.14 Limiter/Compressor/Gain internals were read directly from ~/.cache/CPM/juce/c074/modules/juce_dsp/widgets/ to confirm the context.isBypassed short-circuit behaviour before implementing the #12 fix.

Test plan

  • cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug && cmake --build build --target Tests Triptych_Standalone --parallel 3 — builds clean
  • ctest --test-dir build --output-on-failure — 42/42 passed
  • Each new regression test manually verified to FAIL when reverting only its corresponding source fix (confirms the test actually exercises the bug, not just the happy path)

🤖 Generated with Claude Code

yves-vogl and others added 4 commits July 16, 2026 00:39
juce::dsp::Limiter::process() (and each of its two internal Compressors)
short-circuits to a plain copyFrom() as its first statement when
context.isBypassed, skipping the BallisticsFilter envelope update
entirely. Toggling that flag to represent "disabled" therefore froze
the limiter's gain-reduction state instead of keeping it continuous,
so re-enabling could resume from a stale pre-disable envelope rather
than one consistent with the current input - the opposite of the
documented no-pop-on-re-enable guarantee.

BandCompressor now always runs the limiter at full strength into a
preallocated scratch buffer and only splices the limited result back
into the output when enabled, so its ballistics genuinely track the
live signal at all times.

Fixes #12

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tail

TriptychEngine::process() clamped numSamples to the per-band buffer
capacity established in prepare() and only ever touched that first
capacity-sized region of the host's buffer. Samples beyond it were
never read or written at all - silently passing raw dry input through
untouched, including bypassing the master Output trim, whenever a host
called process() with more samples than it declared via
prepareToPlay()'s maximumExpectedSamplesPerBlock (offline bounce/render
passes commonly do this).

process() now loops over the requested block in <= capacity-sized
chunks, running each through the full signal chain via a new
processChunk() (crossovers, band compressors, Mute/Solo gate, output
trim) - previously the whole body of process() itself. Channel-count
mismatches are still defensively clamped rather than chunked, since a
host violating its own negotiated bus layout is not a realistic
scenario the way an oversized block is.

Fixes #14

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TriptychEngine::process() resolved Mute/Solo to a bare block-rate
0.0f/1.0f multiplier applied uniformly across the whole block, so
toggling mid-playback produced a hard step discontinuity at whatever
sample happened to land on the block boundary - audible as a click.
This was independent of (and in addition to) each band's own
compressor/limiter envelopes already staying continuous underneath:
the gain step itself was never smoothed.

The resolved gain is now ramped through a juce::SmoothedValue per band
(lowGainSmoothed/midGainSmoothed/highGainSmoothed, linear, the same
smoothingTimeSeconds used for every other real-time-varying scalar in
this engine), filled into a per-sample ramp buffer once per chunk and
consumed by the summing loop instead of a bare per-block constant.

Fixes #13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yves-vogl
yves-vogl merged commit b863d69 into main Jul 16, 2026
2 checks passed
@yves-vogl
yves-vogl deleted the fix/review-findings branch July 16, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment