//! Replay-gain target curve, applied browser-side via Web Audio.
//!
//! Goal: equalize playback loudness across recordings from clients
//! with very different microphone levels (Watch ~-38 dB mean, desktop
//! ~-27 dB) without modifying the audio file. The server computes one
//! non-negative gain value per recording from a `volumedetect` pass;
//! the browser applies it through a `GainNode`.

/// Target mean volume in dBFS. Recordings below this get amplified
/// up to this level; recordings already at or above it get gain 0.
/// Matches the EBU R128 broadcast loudness ballpark for speech and
/// was validated against the 2026-04-23 PoC.
pub const TARGET_MEAN_DB: f64 = -16.0;

/// Peak ceiling in dBFS. Caps amplification so the loudest sample
/// never exceeds this — leaves a 1 dB headroom against digital
/// clipping. Combined with the target-mean check, the chosen gain is
/// the minimum of the two budgets.
pub const PEAK_CEILING_DB: f64 = -1.0;

/// Compute the playback-gain in dB for a recording with the given
/// `volumedetect` measurements. Always non-negative — we never
/// attenuate; if the recording is already at or above the target it
/// gets gain 0 dB.
///
/// **Caller contract:** both inputs must be finite (`is_finite()`).
/// `f64::NEG_INFINITY` (silent file) propagates to `+Inf` through the
/// arithmetic and would make `serde_json` refuse to serialize the
/// resulting `Loudness` struct. The worker gates on finiteness and
/// stores `loudness: None` for silent recordings instead.
pub fn compute_gain_db(mean_db: f64, max_db: f64) -> f64 {
    let to_target = TARGET_MEAN_DB - mean_db;
    let headroom = PEAK_CEILING_DB - max_db;
    to_target.min(headroom).max(0.0)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A very dynamic source: low average level (mean=-30) but loud
    /// peaks (max=-3). The headroom budget caps the gain even though
    /// the average alone would allow much more boost.
    #[test]
    fn headroom_limits_gain() {
        // to_target = -16 - (-30) = +14
        // headroom  = -1  - (-3)  = +2  ← tighter
        assert_eq!(compute_gain_db(-30.0, -3.0), 2.0);
    }

    /// A quiet, well-controlled source: low average (mean=-22),
    /// moderate peaks (max=-15). Plenty of headroom — the target
    /// budget is the limiting factor.
    #[test]
    fn target_limits_gain() {
        // to_target = -16 - (-22) = +6   ← tighter
        // headroom  = -1  - (-15) = +14
        assert_eq!(compute_gain_db(-22.0, -15.0), 6.0);
    }

    /// Already-loud source (mean=-12, well above target). Both
    /// budgets contradict each other, but the `max(0.0)` floor makes
    /// the result 0 — we never attenuate.
    #[test]
    fn clamps_to_zero_when_already_loud() {
        // to_target = -16 - (-12) = -4   (would attenuate)
        // headroom  = -1  - (-2)  = +1
        // min       = -4
        // max(0)    = 0
        assert_eq!(compute_gain_db(-12.0, -2.0), 0.0);
    }
}
