Skip to content

Add look-ahead brickwall limiter and fix planar DSP indexing#468

Merged
alnitak merged 5 commits into
alnitak:mainfrom
Kunstderfug:codex/brickwall-limiter-upstream
May 21, 2026
Merged

Add look-ahead brickwall limiter and fix planar DSP indexing#468
alnitak merged 5 commits into
alnitak:mainfrom
Kunstderfug:codex/brickwall-limiter-upstream

Conversation

@Kunstderfug
Copy link
Copy Markdown
Contributor

Summary

  • replace the limiter core with a true look-ahead brickwall/maximizer path
  • interpret Threshold as input drive while Output Ceiling remains the final peak target
  • stereo-link limiter gain reduction and use SoLoud planar buffer indexing
  • fix compressor planar buffer indexing
  • preserve the configured limiter ceiling through the final engine output path
  • add standalone limiter correctness tests and refresh the example lockfile package version

Why

The previous limiter smoothed attack after computing the required reduction, which could let transients leak above the configured ceiling. The filter buffers are planar, so interleaved indexing could also cross-feed channels and break stereo behavior.

Validation

  • ./test/run_limiter_test.sh: 18/18 assertions passed
  • flutter analyze: no issues found
  • git diff --check origin/main..HEAD: no whitespace errors

Original limiter was a feed-forward soft-knee compressor that smoothed
the gain envelope after computing the required reduction, which let
transients exceed the output ceiling. New implementation uses a
look-ahead delay buffer (length = ATTACK_TIME ms), stereo-linked peak
detection, backward-scan envelope ramping for instant attack, one-pole
release smoothing, and a hard ceiling clamp as a safety net.

Public parameter IDs and ranges are unchanged so existing Dart callers
keep working. ATTACK_TIME's semantic shifts from envelope time-constant
to look-ahead window length; for typical values (1 ms) the user-facing
behaviour is the same.
Tests exercise the limiter without linking the SoLoud engine — minimal
SoLoud base-class stubs let limiter.cpp compile and run in isolation.

Coverage:
  1. Ceiling honored on a full-scale 1-sample impulse (the case the
     original limiter leaks because of attack smoothing)
  2. Ceiling honored on a +6 dB sustained sine
  3. Sub-threshold input passes through sample-accurately (after the
     look-ahead delay)
  4. Stereo image preserved under asymmetric limiting (stereo-link
     verification — original tracked L/R independently and broke this)
  5. Fuzz across parameter combinations: ceiling guarantee always holds
  6. Pre-emptive ramp-down: gain reaches reqGain BEFORE the offending
     sample arrives at the output, instead of catching up after it
SoLoud passes audio in PLANAR layout: channel ch occupies the contiguous
range [ch*aBufferSize, ch*aBufferSize + aSamples). The default
FilterInstance::filter loops over channels calling
filterChannel(aBuffer + ch*aBufferSize, ...) and Freeverb confirms it
with 'inputR = aSampleData + aStride'.

The old limiter and the new one both used aBuffer[i*channels + ch]
(interleaved). The old limiter masked the bug because per-channel
envelopes change slowly, scrambling samples just smeared the audio
slightly. The new look-ahead limiter copies samples into a delay ring
and reads them back ~1 ms later — wrong-order samples come out as
heavy distortion.

Now uses aBuffer[i + ch*aBufferSize] for read/write. Internal delay
ring stays frame-major (interleaved) for cache locality of the
per-frame loop.

Tests rewritten to use planar layout and a new regression test feeds
1 kHz into L and 7 kHz into R (both sub-threshold), then verifies each
output channel matches its own delayed input — would have caught this
on day one.
@alnitak
Copy link
Copy Markdown
Owner

alnitak commented May 18, 2026

Hi @Kunstderfug, this is a great PR! Thank you.

If you feel good about this, I can turn this PR ready for review and merge it.

@Kunstderfug
Copy link
Copy Markdown
Contributor Author

Thank you! Let's try it!

@alnitak alnitak marked this pull request as ready for review May 21, 2026 08:42
@alnitak alnitak merged commit 43ec61c into alnitak:main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants