//! Listener-side stereo pan law. //! //! Capture, Opus, and the network stay mono. These helpers are used only after a //! peer has been decoded locally, just before the playout mix is written to the //! stereo playback bus. /// Clamp and compute constant-power pan gains for `pan` in `[-1.0, 1.0]`. /// /// - `-1.0` is hard left `(1, 0)` /// - `0.0` is center `(sqrt(1/2), sqrt(1/2))` /// - `1.0` is hard right `(0, 1)` pub fn pan_gains(pan: f32) -> (f32, f32) { let pan = pan.clamp(-1.0, 1.0); let theta = (pan + 1.0) * std::f32::consts::FRAC_PI_4; (theta.cos(), theta.sin()) } /// Gains used by the legacy-compatible playback mixer. /// /// The pure law above is constant-power. The existing application, however, was /// mono and users heard the full old mono signal in both ears. Scaling by sqrt(2) /// makes `pan = 0` exactly dual-mono `(1, 1)`, preserving the default sound while /// still following the same equal-power curve as a peer is moved away from center. pub fn playback_pan_gains(pan: f32) -> (f32, f32) { let (left, right) = pan_gains(pan); ( left * std::f32::consts::SQRT_2, right * std::f32::consts::SQRT_2, ) } #[cfg(test)] mod tests { use super::*; const EPS: f32 = 1.0e-6; #[test] fn hard_left_and_right_are_endpoints() { assert_eq!(pan_gains(-1.0), (1.0, 0.0)); let (l, r) = pan_gains(1.0); assert!( l.abs() < EPS, "left at hard-right should be zero-ish, got {l}" ); assert!( (r - 1.0).abs() < EPS, "right at hard-right should be one, got {r}" ); } #[test] fn center_is_equal_and_power_preserving() { let (l, r) = pan_gains(0.0); assert!((l - r).abs() < EPS); assert!((l - std::f32::consts::FRAC_1_SQRT_2).abs() < EPS); assert!(((l * l + r * r) - 1.0).abs() < EPS); } #[test] fn gains_move_monotonically() { let pans = [-1.0, -0.5, 0.0, 0.5, 1.0]; let mut prev_l = f32::INFINITY; let mut prev_r = f32::NEG_INFINITY; for pan in pans { let (l, r) = pan_gains(pan); assert!( l <= prev_l + EPS, "left gain must not rise as pan moves right" ); assert!( r >= prev_r - EPS, "right gain must not fall as pan moves right" ); prev_l = l; prev_r = r; } } #[test] fn playback_center_preserves_legacy_dual_mono() { let (l, r) = playback_pan_gains(0.0); assert!((l - 1.0).abs() < EPS); assert!((r - 1.0).abs() < EPS); } #[test] fn input_is_clamped() { assert_eq!(pan_gains(-9.0), pan_gains(-1.0)); assert_eq!(pan_gains(9.0), pan_gains(1.0)); } }