audio: extract apply_peer_volume seam + A24 regression tests
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled

A24 (per-peer volume slider has no effect) does not reproduce on current
main from a code trace: the UI slider's EndpointId is the same key the
mixer uses for the incoming jitter frame, and the gain is applied before
EQ/pan/output. Extract the inline per-peer lookup into a pure
apply_peer_volume() seam and add two regression tests:
 - matching key scales the frame (0.5 halves it)
 - mismatched key defaults to unity (guards the key-identity failure mode)

No wire/gossip/identity/PeerState change. The field-reported A24 was most
likely a stale listener build (volume is listener-side); needs a 2-machine
audible re-verify to close.

Co-Authored-By: Codex (gpt-5.5) <noreply@openai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 03:16:06 -04:00
co-authored by Codex Claude Opus 4.8
parent 4dc1bcd546
commit 9a059e1bb8
+39 -5
View File
@@ -294,6 +294,17 @@ fn apply_volume(frame: &mut [i16], vol: f32) {
}
}
/// Apply the listener's per-peer volume for the audio sender id currently being
/// mixed. The map key must be the same `EndpointId` used for the jitter buffer.
fn apply_peer_volume(
frame: &mut [i16],
peer_id: EndpointId,
volumes: &HashMap<EndpointId, f32>,
) {
let vol = volumes.get(&peer_id).copied().unwrap_or(1.0);
apply_volume(frame, vol);
}
/// Normalized RMS level of a frame in `[0.0, 1.0]` (32768 = full scale), for the
/// UI level meter. An empty frame reads as 0.0.
fn frame_level(frame: &[i16]) -> f32 {
@@ -1707,8 +1718,7 @@ async fn run_core_loop(
peer_noise_gates.remove(&peer_id);
}
let vol = current_volumes.get(&peer_id).copied().unwrap_or(1.0);
apply_volume(&mut frame, vol);
apply_peer_volume(&mut frame, peer_id, &current_volumes);
let eq_settings = current_eq
.get(&peer_id)
@@ -2776,9 +2786,9 @@ async fn run_core_loop(
#[cfg(test)]
mod tests {
use super::{
admit_retained, apply_volume, audio_datagram_len_ok, frame_level, mix_frames,
mix_stereo_frames, next_game_change, should_auto_fetch, stereo_to_mono, KnownPeers,
MicLevelMeter, PeerSpeakTicket, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS,
admit_retained, apply_peer_volume, apply_volume, audio_datagram_len_ok, frame_level,
mix_frames, mix_stereo_frames, next_game_change, should_auto_fetch, stereo_to_mono,
KnownPeers, MicLevelMeter, PeerSpeakTicket, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS,
MIC_LEVEL_REPORT_SAMPLES,
};
@@ -3038,6 +3048,30 @@ mod tests {
assert_eq!(frame, vec![2000, -2000]);
}
#[test]
fn peer_volume_map_scales_the_matching_audio_peer_frame() {
let peer = iroh::SecretKey::generate().public();
let other_peer = iroh::SecretKey::generate().public();
let volumes = std::collections::HashMap::from([(peer, 0.5), (other_peer, 2.0)]);
let mut frame = vec![100, -200, 300, -400];
apply_peer_volume(&mut frame, peer, &volumes);
assert_eq!(frame, vec![50, -100, 150, -200]);
}
#[test]
fn peer_volume_map_defaults_to_unity_when_audio_peer_key_is_unmatched() {
let ui_peer = iroh::SecretKey::generate().public();
let audio_peer = iroh::SecretKey::generate().public();
let volumes = std::collections::HashMap::from([(ui_peer, 0.5)]);
let mut frame = vec![100, -200, 300, -400];
apply_peer_volume(&mut frame, audio_peer, &volumes);
assert_eq!(frame, vec![100, -200, 300, -400]);
}
#[test]
fn three_peers_sum_without_saturation() {
let a = vec![10, 20];