playback: only attenuate a gain with no peak

A peak is the only headroom check the gain code has. When the server sends a gain without one (Navidrome does this for Opus R128 gains, which carry no peak tags by design), a positive gain was applied unchecked and the 16-bit sink saturated.

Cap a peakless gain at unity instead: attenuation still applies, but a boost needs a peak to prove it fits. The cap is unchanged when a peak is present.

Update the tests that expected a peakless boost, and cover the positive preamp that can no longer lift a peakless track and the attenuation that still applies.
This commit is contained in:
austinried
2026-10-18 13:35:27 +00:00
parent 746e2319b9
commit 87dbabb496
5 changed files with 43 additions and 14 deletions
@@ -31,7 +31,8 @@ internal fun replayGainLinearGain(
settings: ReplayGainSettings,
): Float {
// gonic sends a peak of 0 when it only has album gain, so a non-positive or non-finite peak means
// "no peak data" and is ignored rather than treated as silence.
// "no peak data" and is ignored rather than treated as silence. A boost cannot be checked for
// headroom without a peak, so a peakless track is only ever attenuated.
val trackPeak = gain.trackPeak?.takeIf { it.isFinite() && it > 0f }
val albumPeak = gain.albumPeak?.takeIf { it.isFinite() && it > 0f }
val (requestedDb, peak) =
@@ -42,7 +43,8 @@ internal fun replayGainLinearGain(
}
val gainDb = requestedDb?.takeIf { it.isFinite() } ?: return 1f
val requested = gainDb + settings.preampDb
val applied = peak?.let { min(requested, maxSafeGainDb(it)) } ?: requested
val maxGainDb = peak?.let(::maxSafeGainDb) ?: 0f
val applied = min(requested, maxGainDb)
return 10f.pow(applied / 20f).takeIf { it.isFinite() && it > 0f } ?: 1f
}