Skip to content

Fix Input Boost wrapping around instead of clipping#3834

Open
mcfnord wants to merge 1 commit into
jamulussoftware:mainfrom
mcfnord:fix-input-boost-overflow
Open

Fix Input Boost wrapping around instead of clipping#3834
mcfnord wants to merge 1 commit into
jamulussoftware:mainfrom
mcfnord:fix-input-boost-overflow

Conversation

@mcfnord

@mcfnord mcfnord commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Note

📡 STAND BY FOR AN LLM-AUTHORED MESSAGE.

Input Boost multiplies each sample and narrows the int result back to int16_t with a plain cast, so a boosted sample that passes full scale wraps to the opposite polarity instead of clipping.

// src/client.cpp, CClient::ProcessAudioDataIntern
vecsStereoSndCrd[j + 1] = static_cast<int16_t> ( iInputBoost * vecsStereoSndCrd[j + 1] );
vecsStereoSndCrd[j]     = static_cast<int16_t> ( iInputBoost * vecsStereoSndCrd[j] );

iInputBoost is 2..10 (the "2x".."10x" dropdown), so the product routinely exceeds int16_t. Wraparound is a full-amplitude sign flip, which is considerably more audible than the clipping a user would expect from a gain control.

The invariant this breaks is written down 20 lines below

The pan block in the same function documents exactly when the plain cast is safe:

// note that the gain is always <= 1, therefore a simple cast is
// ok since we never can get an overload

and the mono mixdown block, where sums can overload, says:

// note that we need the Float2Short for stereo pan mode

Input Boost is the only gain > 1 in the send path, and the only one of the three that does not saturate.

Measured

1 kHz sine at −10 dBFS, 4800 samples. "Sign-flipped" means the current cast returned a value of opposite polarity to the true product.

boost wraps above samples over full scale sign-flipped max error
2x −6.0 dBFS 0 / 4800 0 0
4x −12.0 dBFS 2200 / 4800 2200 (100%) 65424 LSB
10x −20.0 dBFS 3800 / 4800 2200 (the rest wrap twice) 63760 LSB

The threshold is 32767 / iInputBoost. At 10x — the setting a user with a quiet microphone is most likely to reach for — anything above −20 dBFS inverts.

Performance

This is a per-sample loop, so: x86-64, gcc 13.3, -O3, 128-sample frame (2.67 ms), best of 7 runs × 3M reps, loops in a separate translation unit, memcpy baseline subtracted.

variant ns/frame
current plain cast 108.6
Float2Short (this PR) 1111.7
integer std::min/std::max clamp 1102.3

That is +1000 ns on a 2 670 000 ns budget, or +0.037%, and only on the path where the user has actually enabled the setting (iInputBoost != 1; the default is 1, which skips the loop entirely). The ratio is large because the plain cast vectorizes to pmullw and neither clamped form reaches packssdw on gcc 13 — but the absolute cost is not perceptible.

I measured the integer clamp as the alternative and it comes out the same, so I chose Float2Short for consistency with the rest of the function rather than for speed. Happy to switch if you prefer the integer form.

Caveat: timings are x86-64 only; I have not measured ARM.

Side note, not fixed here

SignalLevelMeter.Update runs after the boost, and CStereoSignalLevelMeter::Update measures negative peaks only (a deliberate speed optimization, every third sample). A positive sample that wraps becomes a large negative and reads as a peak; a negative sample that wraps becomes positive and is missed. So the level meter is not a dependable warning for this, in either direction. That seemed like a separate discussion, so I left it alone.

History

Introduced with the feature in #1222 (2021-04-01, e5bc010) and untouched since except by clang-format. I could not find any existing issue, PR, or discussion about it.

CHANGELOG: Bugfix: Input Boost now clips instead of wrapping around when the boosted signal exceeds full scale.

The Input Boost setting (2x..10x) multiplied each sample and narrowed the
int result back to int16_t with a plain cast, so any sample boosted past
full scale wrapped to the opposite polarity rather than clipping.

Use Float2Short, the same saturating helper already used twice in this
function, so boosted peaks clip instead of inverting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant