Host capture gain works on punktfunk/1, and boosting no longer hard-clips #229

Merged
enricobuehler merged 1 commits from worktree-audio-soft-limit-gain into main 2026-08-14 17:20:53 +00:00
Owner

Closes a gap a field report walked into: PUNKTFUNK_AUDIO_GAIN had two defects that compounded.

The two defects

It existed only on the GameStream plane. On native punktfunk/1 it silently did nothing. That mattered more than it looks, because WASAPI loopback is tapped upstream of the endpoint's master volume — turning the host's speaker slider up does not change the level a client receives either. Between the two, there was no host-side way at all to lift a quiet desktop mix on the protocol that matters.

Where it did apply, it was (s * gain).clamp(-1.0, 1.0) — a hard clip. Flat-topping a waveform is a first-derivative discontinuity, which radiates harsh high-order harmonics, so anyone who pushed past roughly 1.5× heard gross distortion long before reaching the level they were chasing. A user report of "+18 dB and everything warbles" is the expected output of that line, not a fault downstream of it.

What this does

punktfunk_core::audio::apply_gain replaces the clamp with a tanh soft knee above 0.7 (≈ −3.1 dBFS), chosen for three properties:

  1. C¹-continuous at the knee — the shaped branch's slope where it meets the linear one is exactly 1, so there is no corner in the transfer curve and the onset of limiting is not itself an audible event.
  2. Bounded by construction — asymptotic to 1.0, and ±inf maps to ±1.0, so no sample can leave out of range.
  3. Odd-symmetric — benign odd-harmonic distortion, no DC offset.

It is a memoryless waveshaper, not a lookahead limiter, so it costs zero added latency in the realtime encode path. That is the trade that makes it acceptable here.

capture_gain() is now shared by both planes, and rejects the two values that are always typos: non-positive (would invert or mute) and above 8.0/+18 dB (capped, and logged).

Measured transfer — material below ≈−15 dBFS gets the full requested boost linearly; only near-full-scale peaks bend; nothing exceeds 0 dBFS:

input +6 dB +12 dB +18 dB
−20 dBFS −14.0 (6.0) −8.0 (12.0) −2.0 (18.0)
−12 dBFS −6.0 (6.0) −0.6 (11.4) −0.0 (12.0)
−4.4 dBFS −0.2 (4.3) −0.0 (4.4) −0.0 (4.4)

Scope — deliberately not a compressor

This buys headroom, not loudness. It cannot close a peak-to-loudness gap against already-limited broadcast content; that needs a compressor with a real time constant, which this deliberately is not, and the docs say so. The requester asked for makeup gain to match TV-app loudness — that half is held pending an actual LUFS measurement, since compressing game audio flattens the explosion-vs-footstep range and is a much larger surface.

Notes for review

  • Unity is a no-op inside apply_gain itself, not merely at the call sites, so the default wire stays byte-for-byte identical and a future caller that forgets to gate cannot quietly bend every peak. A test locks this.
  • SOFT_LIMIT_KNEE is excluded from cbindgen. It is host-side capture processing no C embedder can act on, and exporting it would add a bare #define against the config's own R21 prefix rule. Verified by regenerating include/punktfunk_core.hbyte-identical, ABI 19 untouched.
  • No CHANGELOG entry: there is no Unreleased section and v0.28.1 is already tagged, so opening one is a convention call left to the maintainer. It does want a line when the next release is cut, since an env var changed semantics and gained a second protocol.

Verification

Both call sites are cfg(linux/windows), so a macOS check compiles stubs and proves nothing; macOS host clippy is also already red on main for unrelated pre-existing pf-encode reasons. Gated on Linux amd64 in punktfunk-rust-ci:latest, confirmed non-vacuous (Compiling punktfunk-host present):

  • cargo fmt --check (core + host) — clean
  • cargo clippy --all-targets -p punktfunk-core -- -D warnings — clean
  • cargo clippy --all-targets -p punktfunk-host -- -D warnings — clean
  • cargo test -p punktfunk-core — 215/215 local, 51 audio:: on Linux
  • include/punktfunk_core.h — 0 lines of drift

New tests cover: unity bit-exactness, transparency below the knee, boundedness under absurd gain (incl. ±inf), monotonicity, odd symmetry, and slope continuity across the knee.

Closes a gap a field report walked into: `PUNKTFUNK_AUDIO_GAIN` had two defects that compounded. ## The two defects **It existed only on the GameStream plane.** On native `punktfunk/1` it silently did nothing. That mattered more than it looks, because **WASAPI loopback is tapped upstream of the endpoint's master volume** — turning the host's speaker slider up does not change the level a client receives either. Between the two, there was *no host-side way at all* to lift a quiet desktop mix on the protocol that matters. **Where it did apply, it was `(s * gain).clamp(-1.0, 1.0)`** — a hard clip. Flat-topping a waveform is a first-derivative discontinuity, which radiates harsh high-order harmonics, so anyone who pushed past roughly 1.5× heard gross distortion long before reaching the level they were chasing. A user report of "+18 dB and everything warbles" is the expected output of that line, not a fault downstream of it. ## What this does `punktfunk_core::audio::apply_gain` replaces the clamp with a `tanh` soft knee above `0.7` (≈ −3.1 dBFS), chosen for three properties: 1. **C¹-continuous at the knee** — the shaped branch's slope where it meets the linear one is exactly 1, so there is no corner in the transfer curve and the onset of limiting is not itself an audible event. 2. **Bounded by construction** — asymptotic to 1.0, and ±inf maps to ±1.0, so no sample can leave out of range. 3. **Odd-symmetric** — benign odd-harmonic distortion, no DC offset. It is a *memoryless waveshaper*, not a lookahead limiter, so it costs **zero added latency** in the realtime encode path. That is the trade that makes it acceptable here. `capture_gain()` is now shared by both planes, and rejects the two values that are always typos: non-positive (would invert or mute) and above `8.0`/+18 dB (capped, and logged). Measured transfer — material below ≈−15 dBFS gets the **full** requested boost linearly; only near-full-scale peaks bend; nothing exceeds 0 dBFS: | input | +6 dB | +12 dB | +18 dB | |---|---|---|---| | −20 dBFS | −14.0 (6.0) | −8.0 (12.0) | −2.0 (18.0) | | −12 dBFS | −6.0 (6.0) | −0.6 (11.4) | −0.0 (12.0) | | −4.4 dBFS | −0.2 (4.3) | −0.0 (4.4) | −0.0 (4.4) | ## Scope — deliberately not a compressor This buys **headroom, not loudness**. It cannot close a peak-to-loudness gap against already-limited broadcast content; that needs a compressor with a real time constant, which this deliberately is not, and the docs say so. The requester asked for makeup gain to match TV-app loudness — that half is held pending an actual LUFS measurement, since compressing game audio flattens the explosion-vs-footstep range and is a much larger surface. ## Notes for review - **Unity is a no-op inside `apply_gain` itself**, not merely at the call sites, so the default wire stays byte-for-byte identical and a future caller that forgets to gate cannot quietly bend every peak. A test locks this. - **`SOFT_LIMIT_KNEE` is excluded from cbindgen.** It is host-side capture processing no C embedder can act on, and exporting it would add a bare `#define` against the config's own R21 prefix rule. Verified by regenerating `include/punktfunk_core.h` — **byte-identical, ABI 19 untouched**. - No `CHANGELOG` entry: there is no `Unreleased` section and v0.28.1 is already tagged, so opening one is a convention call left to the maintainer. It does want a line when the next release is cut, since an env var changed semantics *and* gained a second protocol. ## Verification Both call sites are `cfg(linux/windows)`, so a macOS check compiles stubs and proves nothing; macOS host clippy is also already red on main for unrelated pre-existing `pf-encode` reasons. Gated on Linux amd64 in `punktfunk-rust-ci:latest`, confirmed non-vacuous (`Compiling punktfunk-host` present): - `cargo fmt --check` (core + host) — clean - `cargo clippy --all-targets -p punktfunk-core -- -D warnings` — clean - `cargo clippy --all-targets -p punktfunk-host -- -D warnings` — clean - `cargo test -p punktfunk-core` — 215/215 local, 51 `audio::` on Linux - `include/punktfunk_core.h` — 0 lines of drift New tests cover: unity bit-exactness, transparency below the knee, boundedness under absurd gain (incl. ±inf), monotonicity, odd symmetry, and slope continuity across the knee.
enricobuehler added 1 commit 2026-08-14 17:20:14 +00:00
feat(audio): capture gain on punktfunk/1, and a soft knee instead of the clamp that made boosting a trap
android / android (pull_request) Failing after 1m5s
apple / swift (pull_request) Successful in 1m57s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m37s
ci / web (pull_request) Successful in 1m58s
ci / bun-nix (pull_request) Successful in 41s
ci / rust-arm64 (pull_request) Failing after 3m56s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m50s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m41s
ci / rust (pull_request) Successful in 21m14s
6eb5edaff4
`PUNKTFUNK_AUDIO_GAIN` had two defects that compounded.

It existed only on the GameStream plane, so on native `punktfunk/1` it silently did
nothing — and since WASAPI loopback is tapped UPSTREAM of the endpoint's master volume,
turning the host's speaker slider up does not change the level a client receives either.
Between the two there was no host-side way at all to lift a quiet desktop mix on the
protocol that matters.

And where it did apply it was `(s * gain).clamp(-1.0, 1.0)` — a hard clip. Flat-topping a
waveform is a first-derivative discontinuity, which radiates harsh high-order harmonics, so
any operator who pushed past roughly 1.5x heard gross distortion long before reaching the
level they were chasing. A field report of "+18 dB and everything warbles" is the expected
output of that line, not a fault anywhere downstream of it.

`punktfunk_core::audio::apply_gain` replaces the clamp with a tanh soft knee above 0.7
(~-3.1 dBFS), chosen for three properties: C1-continuous where the branches meet (slope 1
on both sides, so the onset of limiting is not itself an audible event), bounded by
construction (asymptotic to 1.0, and +-inf maps to +-1.0, so nothing leaves out of range),
and odd-symmetric (benign harmonics, no DC). It is a memoryless waveshaper, so it costs
zero latency in the realtime encode path.

Unity is a no-op inside `apply_gain` itself, not merely at the call sites, so the default
wire stays byte-for-byte identical and a future caller that forgets to gate cannot quietly
bend every peak. `capture_gain` is now shared by both planes and rejects the two values
that are always typos: non-positive (would invert or mute) and above 8.0/+18 dB (capped,
and said out loud).

This buys headroom, NOT loudness. It cannot close a peak-to-loudness gap against
already-limited broadcast content; that needs a compressor with a real time constant, which
this deliberately is not, and the docs say so.

`SOFT_LIMIT_KNEE` is excluded from cbindgen: it is host-side capture processing that no C
embedder can act on, and exporting it would add a bare `#define` against the config's own
R21 rule. Verified by regenerating `include/punktfunk_core.h` — byte-identical, ABI 19
untouched.
enricobuehler scheduled this pull request to auto merge when all checks succeed 2026-08-14 17:20:49 +00:00
enricobuehler merged commit 4676d20dc1 into main 2026-08-14 17:20:53 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#229