Files
peerspeak/src/audio/pan.rs
T
molluskandClaude Opus 4.8 d0a16cb8b9 style: apply cargo fmt across the crate (A20)
The repo never enforced rustfmt, so formatting had drifted broadly. This is a
single mechanical `cargo fmt` pass over the whole crate (no behavioral change;
lib suite green, 493 passed). Going forward fmt should be enforced (planned CI
fmt --check step). Part of the 0.6.1 hygiene pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:11:44 -04:00

93 lines
2.8 KiB
Rust

//! 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));
}
}