#[cfg(feature = "voice-playback")]
use super::audio_buffer::voice_output_prebuffer_frames;
use super::dave::VoiceDaveOutboundPayload;
use super::opus::VoicePlaybackDecodeState;
#[cfg(feature = "voice-playback")]
use super::playback::voice_output_buffer_size;
use super::rtp::build_voice_rtp_packet;
use super::runtime::stop_voice_connection_task;
use super::*;
#[cfg(feature = "voice-playback")]
use crate::support::audio_output::{f32_sample_to_i16, f32_sample_to_u8, f32_sample_to_u16};
fn requested_voice() -> CurrentVoiceConnectionState {
CurrentVoiceConnectionState {
self_mute: true,
..CurrentVoiceConnectionState::test(Id::new(1), Id::new(10))
}
}
fn voice_state(user_id: u64, channel_id: Option<Id<ChannelMarker>>) -> VoiceStateInfo {
VoiceStateInfo {
session_id: Some("voice-session".to_owned()),
..VoiceStateInfo::test(Id::new(1), channel_id, Id::new(user_id))
}
}
fn voice_server() -> VoiceServerInfo {
VoiceServerInfo {
guild_id: Some(Id::new(1)),
channel_id: None,
endpoint: Some("voice.example.com".to_owned()),
token: "secret-token".to_owned(),
}
}
#[test]
fn voice_runtime_assembles_local_voice_session() {
let mut state = VoiceRuntimeState::default();
assert_eq!(
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10)))),
None
);
assert_eq!(
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice()))),
None
);
assert_eq!(
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10))
))),
None
);
let action = state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
match action {
Some(VoiceRuntimeAction::Connect(session)) => {
assert_eq!(session.scope, VoiceScope::Guild(Id::new(1)));
assert_eq!(session.channel_id, Id::new(10));
assert_eq!(session.user_id, Id::new(10));
assert_eq!(session.endpoint, "voice.example.com");
}
other => panic!("expected connect action, got {other:?}"),
}
}
#[test]
fn voice_runtime_restores_active_sources_only_for_the_latest_failed_selection() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
let connection_id = match state.apply(VoiceRuntimeEvent::VoiceServer(voice_server())) {
Some(VoiceRuntimeAction::Connect(session)) => session.connection_id,
action => panic!("voice server should start a connection, got {action:?}"),
};
let requested_sources = VoiceAudioSources {
input: Some("new-mic".to_owned()),
output: Some("new-speaker".to_owned()),
};
state.apply(VoiceRuntimeEvent::AudioSourcesChanged(
requested_sources.clone(),
));
let selection = state.audio_source_selection();
state.apply(VoiceRuntimeEvent::AudioSourcesApplyFailed {
connection_id,
generation: selection.generation.saturating_sub(1),
requested_sources: requested_sources.clone(),
active_sources: VoiceAudioSources::default(),
message: "stale failure".to_owned(),
});
assert_eq!(state.audio_source_selection().sources, requested_sources);
state.apply(VoiceRuntimeEvent::AudioSourcesApplyFailed {
connection_id,
generation: selection.generation,
requested_sources: requested_sources.clone(),
active_sources: VoiceAudioSources::default(),
message: "current failure".to_owned(),
});
assert_eq!(
state.audio_source_selection().sources,
VoiceAudioSources::default()
);
}
#[test]
fn voice_runtime_capture_gate_requires_allowed_active_unmuted_voice() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
let mut requested = requested_voice();
requested.allow_microphone_transmit = true;
requested.noise_suppression = true;
requested.self_mute = false;
requested.microphone_volume = VoiceVolumePercent::new(40);
requested.voice_output_volume = VoiceVolumePercent::new(65);
state.apply(VoiceRuntimeEvent::Requested(Some(requested)));
assert_eq!(state.capture_gate(), None);
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: true,
transmit_enabled: true,
use_voice_activity: true,
noise_suppression: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::new(40),
})
);
assert_eq!(
state.playback_gate(),
Some(VoicePlaybackGate {
enabled: true,
volume: VoiceVolumePercent::new(65),
})
);
requested.self_mute = true;
state.apply(VoiceRuntimeEvent::Requested(Some(requested)));
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: false,
transmit_enabled: false,
use_voice_activity: true,
noise_suppression: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::new(40),
})
);
assert_eq!(
state.playback_gate(),
Some(VoicePlaybackGate {
enabled: true,
volume: VoiceVolumePercent::new(65),
})
);
requested.self_deaf = true;
state.apply(VoiceRuntimeEvent::Requested(Some(requested)));
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: false,
transmit_enabled: false,
use_voice_activity: true,
noise_suppression: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::new(40),
})
);
assert_eq!(
state.playback_gate(),
Some(VoicePlaybackGate {
enabled: false,
volume: VoiceVolumePercent::new(65),
})
);
requested.self_mute = false;
requested.allow_microphone_transmit = false;
requested.self_deaf = false;
state.apply(VoiceRuntimeEvent::Requested(Some(requested)));
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: false,
transmit_enabled: false,
use_voice_activity: true,
noise_suppression: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::new(40),
})
);
assert_eq!(
state.playback_gate(),
Some(VoicePlaybackGate {
enabled: true,
volume: VoiceVolumePercent::new(65),
})
);
let mut other_channel = requested;
other_channel.channel_id = Id::new(11);
other_channel.allow_microphone_transmit = true;
state.apply(VoiceRuntimeEvent::Requested(Some(other_channel)));
assert_eq!(state.capture_gate(), None);
assert_eq!(state.playback_gate(), None);
}
#[test]
#[cfg(feature = "voice-playback")]
fn voice_runtime_push_to_talk_transmits_only_while_pressed() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
let mut requested = requested_voice();
requested.allow_microphone_transmit = true;
requested.self_mute = false;
state.apply(VoiceRuntimeEvent::Requested(Some(requested)));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
state.apply(VoiceRuntimeEvent::PushToTalkEnabledChanged(true));
let released_gate = state.capture_gate().expect("capture gate exists");
assert!(released_gate.capture_enabled);
assert!(!released_gate.transmit_enabled);
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: true,
transmit_enabled: false,
use_voice_activity: false,
noise_suppression: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
})
);
state.apply(VoiceRuntimeEvent::PushToTalkPressed(true));
assert_eq!(
state.capture_gate(),
Some(VoiceCaptureGate {
capture_enabled: true,
transmit_enabled: true,
use_voice_activity: false,
noise_suppression: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
})
);
state.apply(VoiceRuntimeEvent::PushToTalkPressed(false));
assert!(
!state
.capture_gate()
.expect("capture gate exists")
.transmit_enabled
);
}
#[test]
fn voice_runtime_ignores_other_user_voice_state() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
assert_eq!(
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
99,
Some(Id::new(10))
))),
None
);
}
#[test]
fn voice_runtime_closes_on_leave() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
assert_eq!(
state.apply(VoiceRuntimeEvent::Requested(None)),
Some(VoiceRuntimeAction::Close)
);
}
#[test]
fn voice_runtime_respects_connection_end_outcome() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
let connected = state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
let Some(VoiceRuntimeAction::Connect(session)) = connected else {
panic!("expected initial voice connect action, got {connected:?}");
};
let reconnected = state.apply(session.connection_ended_event(VoiceConnectionEnd::Reconnect));
let Some(VoiceRuntimeAction::Connect(active)) = reconnected else {
panic!("recoverable end should reconnect, got {reconnected:?}");
};
assert_eq!(
state.apply(active.connection_ended_event(VoiceConnectionEnd::Stop)),
None
);
assert_eq!(
state.apply(VoiceRuntimeEvent::VoiceServer(voice_server())),
None
);
assert!(matches!(
state.apply(VoiceRuntimeEvent::ManualRetry(requested_voice())),
Some(VoiceRuntimeAction::Connect(_))
));
}
#[test]
fn voice_runtime_limits_reconnects_and_resets_after_success() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
let connected = state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
let Some(VoiceRuntimeAction::Connect(mut active)) = connected else {
panic!("expected initial voice connect action, got {connected:?}");
};
for _ in 0..super::runtime::MAX_VOICE_RECONNECT_ATTEMPTS {
let reconnected = state.apply(active.connection_ended_event(VoiceConnectionEnd::Reconnect));
let Some(VoiceRuntimeAction::Connect(next)) = reconnected else {
panic!("retry within the limit should reconnect, got {reconnected:?}");
};
active = next;
}
assert_eq!(
state.apply(active.connection_ended_event(VoiceConnectionEnd::Reconnect)),
None
);
let manual_retry = state.apply(VoiceRuntimeEvent::ManualRetry(requested_voice()));
assert!(
matches!(manual_retry, Some(VoiceRuntimeAction::Connect(_))),
"manual retry should reset the reconnect limit, got {manual_retry:?}"
);
let mut rotated = voice_server();
rotated.token = "rotated-token".to_owned();
let reset = state.apply(VoiceRuntimeEvent::VoiceServer(rotated));
let Some(VoiceRuntimeAction::Connect(mut active)) = reset else {
panic!("new voice session should reset the retry limit, got {reset:?}");
};
assert_eq!(
state.apply(active.connection_established_event()),
None,
"a healthy connection should reset consecutive retries"
);
for _ in 0..super::runtime::MAX_VOICE_RECONNECT_ATTEMPTS {
let reconnected = state.apply(active.connection_ended_event(VoiceConnectionEnd::Reconnect));
let Some(VoiceRuntimeAction::Connect(next)) = reconnected else {
panic!("retry after a healthy connection should reconnect, got {reconnected:?}");
};
active = next;
}
}
#[test]
fn voice_runtime_ignores_stale_end_after_server_token_rotation() {
let mut state = VoiceRuntimeState::default();
state.apply(VoiceRuntimeEvent::CurrentUserReady(Some(Id::new(10))));
state.apply(VoiceRuntimeEvent::Requested(Some(requested_voice())));
state.apply(VoiceRuntimeEvent::VoiceState(voice_state(
10,
Some(Id::new(10)),
)));
let connected = state.apply(VoiceRuntimeEvent::VoiceServer(voice_server()));
let Some(VoiceRuntimeAction::Connect(previous)) = connected else {
panic!("expected initial voice connect action, got {connected:?}");
};
let mut rotated = voice_server();
rotated.token = "rotated-token".to_owned();
let replaced = state.apply(VoiceRuntimeEvent::VoiceServer(rotated));
let Some(VoiceRuntimeAction::Connect(replacement)) = replaced else {
panic!("token rotation should replace the voice task, got {replaced:?}");
};
assert_ne!(previous.connection_id, replacement.connection_id);
assert_eq!(
state.apply(previous.connection_ended_event(VoiceConnectionEnd::Stop)),
None
);
assert!(matches!(
state.apply(replacement.connection_ended_event(VoiceConnectionEnd::Reconnect)),
Some(VoiceRuntimeAction::Connect(_))
));
}
#[test]
fn voice_close_codes_follow_reconnect_policy() {
assert_eq!(voice_close_action(4013), VoiceCloseAction::Resume);
assert_eq!(voice_close_action(4015), VoiceCloseAction::Resume);
assert_eq!(voice_close_action(4006), VoiceCloseAction::Reconnect);
assert_eq!(voice_close_action(4009), VoiceCloseAction::Reconnect);
for code in [4014, 4021, 4022] {
assert_eq!(voice_close_action(code), VoiceCloseAction::Stop);
}
}
#[test]
fn voice_gateway_session_debug_redacts_secrets() {
let session = VoiceGatewaySession {
connection_id: 0,
scope: VoiceScope::Guild(Id::new(1)),
channel_id: Id::new(10),
user_id: Id::new(20),
session_id: "secret-session".to_owned(),
endpoint: "voice.example.com".to_owned(),
token: "secret-token".to_owned(),
};
let debug = format!("{session:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("secret-session"));
assert!(!debug.contains("secret-token"));
}
#[test]
fn voice_state_debug_redacts_session_id() {
let state = voice_state(10, Some(Id::new(10)));
let debug = format!("{state:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("voice-session"));
}
#[test]
fn voice_session_description_reuses_only_the_same_transport_key_and_mode() {
let current = VoiceSessionDescription {
mode: "aead_xchacha20_poly1305_rtpsize".to_owned(),
secret_key: vec![1, 2, 3],
dave_protocol_version: Some(1),
video_codec: None,
};
for (next, expected) in [
(
VoiceSessionDescription {
dave_protocol_version: Some(2),
..current.clone()
},
true,
),
(
VoiceSessionDescription {
secret_key: vec![4, 5, 6],
..current.clone()
},
false,
),
(
VoiceSessionDescription {
mode: "aead_aes256_gcm_rtpsize".to_owned(),
..current.clone()
},
false,
),
] {
assert_eq!(current.uses_same_transport(&next), expected, "{next:?}");
}
}
#[test]
fn voice_dave_state_tracks_speaking_ssrc_mapping() {
let session = VoiceGatewaySession {
connection_id: 0,
scope: VoiceScope::Guild(Id::new(1)),
channel_id: Id::new(10),
user_id: Id::new(20),
session_id: "voice-session".to_owned(),
endpoint: "voice.example.com".to_owned(),
token: "voice-token".to_owned(),
};
let mut state = VoiceDaveState::new(&session);
state.record_speaking_state(VoiceSpeakingState {
user_id: Some(30),
ssrc: Some(1234),
speaking: Some(1),
});
assert_eq!(state.ssrc_user_ids.get(&1234), Some(&30));
assert_eq!(state.user_id_for_ssrc(1234), Some(Id::new(30)));
assert_eq!(state.user_id_for_ssrc(9999), None);
assert!(state.known_user_ids.contains(&30));
}
#[test]
fn voice_dave_active_drops_non_dave_payloads() {
let session = test_voice_gateway_session();
let mut state = VoiceDaveState::new(&session);
state.reinit(1).expect("DAVE session should initialize");
assert_eq!(
state.unwrap_media_payload_for_ssrc(1234, b"plain-opus"),
VoiceMediaPayload::DaveUnexpectedPlain { payload_len: 10 }
);
}
#[test]
fn voice_speaking_uses_microphone_bit_only() {
assert!(!voice_speaking_microphone_active(0));
assert!(voice_speaking_microphone_active(1));
assert!(!voice_speaking_microphone_active(2));
assert!(voice_speaking_microphone_active(5));
}
#[test]
fn voice_speaking_tracker_keeps_local_and_remote_activity_separate() {
let remote_user = Id::new(30);
let local_user = Id::new(20);
let mut tracker = VoiceSpeakingTracker::new(local_user);
let now = Instant::now();
assert_eq!(tracker.record_remote(local_user, true, now), None);
assert!(tracker.remote_deadlines.is_empty());
assert_eq!(tracker.record_remote(remote_user, true, now), Some(true));
assert_eq!(
tracker.record_remote(remote_user, true, now + VOICE_REMOTE_SPEAKING_TTL / 2),
None
);
assert!(
tracker
.expire_remote(now + VOICE_REMOTE_SPEAKING_TTL)
.is_empty()
);
assert_eq!(
tracker.expire_remote(now + VOICE_REMOTE_SPEAKING_TTL + VOICE_REMOTE_SPEAKING_TTL / 2),
vec![remote_user]
);
assert_eq!(tracker.record_remote(remote_user, false, now), None);
assert_eq!(tracker.record_remote(remote_user, true, now), Some(true));
assert_eq!(tracker.record_remote(remote_user, false, now), Some(false));
assert_eq!(tracker.record_local(true), Some(true));
assert_eq!(tracker.record_local(true), None);
assert_eq!(tracker.clear_all(), vec![local_user]);
}
#[cfg(feature = "voice-playback")]
#[test]
fn local_speaking_follows_microphone_activity_and_emits_only_edges() {
let quiet = vec![100i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let voice_activity_gate = VoiceCaptureGate {
capture_enabled: true,
transmit_enabled: true,
use_voice_activity: true,
noise_suppression: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
};
assert!(voice_microphone_frame_is_active(
voice_activity_gate,
&mut VoiceMicrophoneGateState::default(),
&normal,
));
assert!(!voice_microphone_frame_is_active(
voice_activity_gate,
&mut VoiceMicrophoneGateState::default(),
&quiet,
));
assert!(voice_microphone_frame_is_active(
VoiceCaptureGate {
use_voice_activity: false,
..voice_activity_gate
},
&mut VoiceMicrophoneGateState::default(),
&quiet,
));
assert!(!voice_microphone_frame_is_active(
VoiceCaptureGate {
transmit_enabled: false,
..voice_activity_gate
},
&mut VoiceMicrophoneGateState::default(),
&normal,
));
let (speaking_tx, mut speaking_rx) = mpsc::unbounded_channel();
let mut local_speaking = false;
publish_local_speaking_edge(&speaking_tx, &mut local_speaking, false);
publish_local_speaking_edge(&speaking_tx, &mut local_speaking, true);
publish_local_speaking_edge(&speaking_tx, &mut local_speaking, true);
publish_local_speaking_edge(&speaking_tx, &mut local_speaking, false);
publish_local_speaking_edge(&speaking_tx, &mut local_speaking, false);
assert_eq!(speaking_rx.try_recv(), Ok(true));
assert_eq!(speaking_rx.try_recv(), Ok(false));
assert_eq!(
speaking_rx.try_recv(),
Err(mpsc::error::TryRecvError::Empty)
);
}
#[test]
fn voice_dave_outbound_opus_fails_closed_unless_ready() {
let mut state = VoiceDaveState::new(&test_voice_gateway_session());
assert_eq!(
state.prepare_outbound_opus(b"opus-frame"),
VoiceDaveOutboundPayload::Plain(b"opus-frame".to_vec())
);
state.protocol_version = NonZeroU16::new(1);
assert_eq!(
state.prepare_outbound_opus(b"opus-frame"),
VoiceDaveOutboundPayload::Blocked(VoiceOutboundSendBlockReason::DaveOutboundMissingSession)
);
state.reinit(1).expect("DAVE session should initialize");
assert_eq!(
state.prepare_outbound_opus(b"opus-frame"),
VoiceDaveOutboundPayload::Blocked(VoiceOutboundSendBlockReason::DaveOutboundNotReady)
);
state.reinit(0).expect("DAVE should disable cleanly");
assert_eq!(
state.prepare_outbound_opus(b"opus-frame"),
VoiceDaveOutboundPayload::Plain(b"opus-frame".to_vec())
);
}
#[test]
fn dave_media_detection_requires_magic_marker() {
assert!(!looks_like_dave_media_frame(b"opus-frame"));
let mut payload = vec![0u8; DAVE_MIN_SUPPLEMENTAL_BYTES];
let marker_start = payload.len() - DAVE_MAGIC_MARKER.len();
payload[marker_start..].copy_from_slice(&DAVE_MAGIC_MARKER);
assert!(looks_like_dave_media_frame(&payload));
}
#[test]
fn voice_playback_frame_uses_only_playable_media_payloads() {
let header = RtpHeader {
has_padding: false,
marker: false,
payload_type: DISCORD_VOICE_PAYLOAD_TYPE,
sequence: 7,
timestamp: 8,
ssrc: 9,
authenticated_header_len: 12,
encrypted_extension_body_len: 0,
payload_offset: 12,
};
let mapped_user_id = Id::new(41);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::Plain(b"opus".to_vec()),
&header,
Some(mapped_user_id),
),
Some(VoicePlaybackFrame {
ssrc: 9,
user_id: Some(mapped_user_id),
sequence: 7,
timestamp: 8,
opus: b"opus".to_vec(),
})
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::Plain(b"unmapped-opus".to_vec()),
&header,
None,
),
Some(VoicePlaybackFrame {
ssrc: 9,
user_id: None,
sequence: 7,
timestamp: 8,
opus: b"unmapped-opus".to_vec(),
})
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveDecrypted {
user_id: 42,
opus: b"dave-opus".to_vec(),
},
&header,
Some(mapped_user_id),
),
Some(VoicePlaybackFrame {
ssrc: 9,
user_id: Some(Id::new(42)),
sequence: 7,
timestamp: 8,
opus: b"dave-opus".to_vec(),
})
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveUnexpectedPlain { payload_len: 4 },
&header,
Some(mapped_user_id),
),
None
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveMissingUser { payload_len: 4 },
&header,
Some(mapped_user_id),
),
None
);
}
fn test_playback_frame(ssrc: u32, user_id: Option<u64>, sequence: u16) -> VoicePlaybackFrame {
test_playback_frame_with_timestamp(
ssrc,
user_id,
sequence,
u32::from(sequence) * DISCORD_OPUS_TIMESTAMP_INCREMENT,
)
}
fn test_playback_frame_with_timestamp(
ssrc: u32,
user_id: Option<u64>,
sequence: u16,
timestamp: u32,
) -> VoicePlaybackFrame {
VoicePlaybackFrame {
ssrc,
user_id: user_id.map(Id::new),
sequence,
timestamp,
opus: vec![sequence as u8],
}
}
#[test]
fn voice_playout_buffer_reorders_nearby_packets() {
let now = Instant::now();
let mut buffer = VoicePlaybackPlayoutBuffer::default();
assert!(buffer.push(test_playback_frame(9, Some(42), 12), now));
assert!(buffer.push(test_playback_frame(9, Some(42), 10), now));
assert_eq!(buffer.next_frame(now), None);
assert!(buffer.push(test_playback_frame(9, Some(42), 11), now));
assert_eq!(
buffer.next_frame(now + VOICE_PLAYBACK_FRAME_DURATION),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
10
)))
);
assert_eq!(
buffer.next_frame(now + VOICE_PLAYBACK_FRAME_DURATION * 2),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
11
)))
);
assert_eq!(
buffer.next_frame(now + VOICE_PLAYBACK_FRAME_DURATION * 3),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
12
)))
);
}
#[test]
fn voice_playout_buffer_schedules_packets_by_rtp_timestamp_delta() {
struct Case {
name: &'static str,
timestamp_step: u32,
step_duration: Duration,
early_duration: Duration,
}
for case in [
Case {
name: "20ms Discord packet",
timestamp_step: DISCORD_OPUS_TIMESTAMP_INCREMENT,
step_duration: VOICE_PLAYBACK_FRAME_DURATION,
early_duration: Duration::from_millis(10),
},
Case {
name: "10ms Abaddon packet",
timestamp_step: 480,
step_duration: Duration::from_millis(10),
early_duration: Duration::from_millis(5),
},
] {
let now = Instant::now();
let playout_start = now + VOICE_PLAYBACK_JITTER_BUFFER_DELAY;
let mut buffer = VoicePlaybackPlayoutBuffer::default();
let timestamps = [
case.timestamp_step * 10,
case.timestamp_step * 11,
case.timestamp_step * 12,
];
assert!(buffer.push(
test_playback_frame_with_timestamp(9, Some(42), 10, timestamps[0]),
now
));
assert!(buffer.push(
test_playback_frame_with_timestamp(9, Some(42), 11, timestamps[1]),
now
));
assert!(buffer.push(
test_playback_frame_with_timestamp(9, Some(42), 12, timestamps[2]),
now
));
assert_eq!(
buffer.next_frame(playout_start),
Some(VoicePlayoutFrame::Audio(
test_playback_frame_with_timestamp(9, Some(42), 10, timestamps[0])
)),
"{} should emit the first frame at playout start",
case.name
);
assert_eq!(
buffer.next_frame(playout_start + case.early_duration),
None,
"{} should wait for the RTP timestamp delta",
case.name
);
assert_eq!(
buffer.next_frame(playout_start + case.step_duration),
Some(VoicePlayoutFrame::Audio(
test_playback_frame_with_timestamp(9, Some(42), 11, timestamps[1])
)),
"{} should emit the second frame after its timestamp delta",
case.name
);
assert_eq!(
buffer.next_frame(playout_start + case.step_duration * 2),
Some(VoicePlayoutFrame::Audio(
test_playback_frame_with_timestamp(9, Some(42), 12, timestamps[2])
)),
"{} should keep the same timestamp cadence",
case.name
);
}
}
#[test]
fn voice_playout_buffer_emits_packet_loss_for_missing_sequence() {
let cases = [
(
"20ms",
DISCORD_OPUS_TIMESTAMP_INCREMENT,
VOICE_PLAYBACK_FRAME_DURATION,
),
("10ms", 480, Duration::from_millis(10)),
];
for (name, timestamp_step, step_duration) in cases {
let now = Instant::now();
let playout_start = now + VOICE_PLAYBACK_JITTER_BUFFER_DELAY;
let mut buffer = VoicePlaybackPlayoutBuffer::default();
let first = test_playback_frame_with_timestamp(9, Some(42), 10, timestamp_step * 10);
let third = test_playback_frame_with_timestamp(9, Some(42), 12, timestamp_step * 12);
assert!(buffer.push(first.clone(), now));
assert!(buffer.push(third.clone(), now));
assert!(buffer.push(
test_playback_frame_with_timestamp(9, Some(42), 13, timestamp_step * 13),
now
));
assert_eq!(
buffer.next_frame(playout_start),
Some(VoicePlayoutFrame::Audio(first)),
"{name}"
);
assert_eq!(
buffer.next_frame(playout_start + step_duration),
Some(VoicePlayoutFrame::PacketLoss {
ssrc: 9,
user_id: Some(Id::new(42)),
sequence: 11,
timestamp_step,
}),
"{name}"
);
assert_eq!(
buffer.next_frame(playout_start + step_duration * 2),
Some(VoicePlayoutFrame::Audio(third)),
"{name}"
);
}
}
#[test]
fn voice_playout_buffer_drops_stale_packets_after_playout_advances() {
let now = Instant::now();
let mut buffer = VoicePlaybackPlayoutBuffer::default();
assert!(buffer.push(test_playback_frame(9, Some(42), 7), now));
assert!(buffer.push(test_playback_frame(9, Some(42), 8), now));
assert!(buffer.push(test_playback_frame(9, Some(42), 9), now));
assert_eq!(
buffer.next_frame(now + VOICE_PLAYBACK_FRAME_DURATION),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
7
)))
);
assert_eq!(
buffer.next_frame(now + VOICE_PLAYBACK_FRAME_DURATION * 2),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
8
)))
);
assert!(!buffer.push(test_playback_frame(9, Some(42), 7), now));
}
#[test]
fn voice_decoded_samples_mix_same_tick_frames() {
let mixed =
mix_voice_decoded_samples(&[vec![0.5, 0.25, -0.5, -0.25], vec![0.5, -0.25, 0.5, -0.75]])
.expect("same-tick decoded frames should mix");
let gain = 1.0 / 2.0f32.sqrt();
assert_voice_sample_near(mixed[0], 1.0 * gain);
assert_voice_sample_near(mixed[1], 0.0);
assert_voice_sample_near(mixed[2], 0.0);
assert_voice_sample_near(mixed[3], -gain);
}
#[test]
fn voice_decode_state_outputs_one_poll_quantum_per_mix() {
let mut state = VoicePlaybackDecodeState::default();
let poll_samples =
VOICE_PLAYBACK_POLL_SAMPLES_PER_CHANNEL * usize::from(DISCORD_VOICE_CHANNELS);
state.push_decoded_samples(1, vec![1.0; poll_samples * 2]);
state.push_decoded_samples(2, vec![0.5; poll_samples]);
let first = state
.next_pending_mix()
.expect("first poll should mix pending samples");
let second = state
.next_pending_mix()
.expect("second poll should drain 20ms frame remainder");
assert_eq!(first.len(), poll_samples);
assert_eq!(second.len(), poll_samples);
assert!(state.next_pending_mix().is_none());
}
#[test]
fn voice_decode_state_applies_participant_settings_before_final_output_limit() {
let mut state = VoicePlaybackDecodeState::default();
let poll_samples =
VOICE_PLAYBACK_POLL_SAMPLES_PER_CHANNEL * usize::from(DISCORD_VOICE_CHANNELS);
state.replace_participant_playback_settings(HashMap::from([
(
Id::new(10),
VoiceParticipantPlaybackSettings {
volume: VoiceParticipantVolumePercent::new(200),
muted: false,
},
),
(
Id::new(11),
VoiceParticipantPlaybackSettings {
muted: true,
..VoiceParticipantPlaybackSettings::default()
},
),
]));
state.push_decoded_samples_for_user(1, Some(Id::new(10)), vec![0.75; poll_samples]);
state.push_decoded_samples_for_user(2, Some(Id::new(11)), vec![1.0; poll_samples]);
let mixed = state
.next_pending_mix()
.expect("audible participant should produce a mix");
assert!(mixed.iter().all(|sample| (*sample - 1.5).abs() < 0.0001));
#[cfg(feature = "voice-playback")]
{
let reduced_after_participant_boost = apply_voice_playback_gain_and_limit(mixed[0], 0.5);
assert_voice_sample_near(reduced_after_participant_boost, 0.75);
let boosted_quieter_peak = apply_voice_playback_gain_and_limit(0.75, 2.0);
let boosted_louder_peak = apply_voice_playback_gain_and_limit(1.0, 2.0);
assert!(boosted_quieter_peak < boosted_louder_peak);
assert!(boosted_louder_peak <= VOICE_SOFT_LIMIT_CEILING);
}
}
#[test]
fn voice_post_process_reduces_alternating_high_frequency_noise() {
let mut post_process = VoicePlaybackPostProcess::default();
let mut samples = vec![1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0];
post_process.process(&mut samples);
assert!(samples[2].abs() < 1.0);
assert!(samples[4].abs() < 1.0);
assert!(samples[6].abs() < 1.0);
}
#[cfg(feature = "voice-playback")]
#[test]
fn extra_output_channels_use_converted_silence() {
let mut u8_output = [0u8; 4];
write_voice_output_frame(&mut u8_output, 0.5, -0.5, 1.0, f32_sample_to_u8);
assert_eq!(
u8_output,
[
f32_sample_to_u8(0.5),
f32_sample_to_u8(-0.5),
f32_sample_to_u8(0.0),
f32_sample_to_u8(0.0)
]
);
let mut u16_output = [0u16; 4];
write_voice_output_frame(&mut u16_output, 0.5, -0.5, 1.0, f32_sample_to_u16);
assert_eq!(
u16_output,
[
f32_sample_to_u16(0.5),
f32_sample_to_u16(-0.5),
f32_sample_to_u16(0.0),
f32_sample_to_u16(0.0),
]
);
let mut i16_output = [1i16; 4];
write_voice_output_frame(&mut i16_output, 0.5, -0.5, 1.0, f32_sample_to_i16);
assert_eq!(
i16_output,
[f32_sample_to_i16(0.5), f32_sample_to_i16(-0.5), 0, 0]
);
}
fn assert_voice_sample_near(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < 0.0001,
"expected {actual} to be close to {expected}"
);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_audio_buffer_resamples_non_48khz_output_clock() {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
tx.try_send(vec![0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0])
.expect("decoded samples should queue");
let stats = Arc::new(VoiceAudioOutputStats::default());
stats
.queued_frames
.store(VOICE_AUDIO_OUTPUT_PREBUFFER_FRAMES, Ordering::Relaxed);
let mut buffer = VoiceAudioBuffer::new(rx, 24_000, stats);
buffer.begin_output(0);
assert_eq!(buffer.next_stereo_frame(), Some([0.0, 0.0]));
assert_eq!(buffer.next_stereo_frame(), Some([2.0, 2.0]));
let faded = buffer
.next_stereo_frame()
.expect("resampled underrun should fade from the last frame");
assert!(faded[0] < 2.0 && faded[0] > 0.0);
assert_eq!(faded[0], faded[1]);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_audio_buffer_fades_short_underruns() {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
tx.try_send(vec![1.0, -1.0])
.expect("decoded samples should queue");
let stats = Arc::new(VoiceAudioOutputStats::default());
stats.record_pcm_enqueue(1);
stats
.queued_frames
.store(VOICE_AUDIO_OUTPUT_PREBUFFER_FRAMES, Ordering::Relaxed);
let mut buffer = VoiceAudioBuffer::new(rx, DISCORD_VOICE_SAMPLE_RATE, Arc::clone(&stats));
buffer.begin_output(0);
assert_eq!(buffer.next_stereo_frame(), Some([1.0, -1.0]));
let faded = buffer
.next_stereo_frame()
.expect("underrun should produce a short fade tail");
assert!(faded[0] < 1.0 && faded[0] > 0.0);
assert!(faded[1] > -1.0 && faded[1] < 0.0);
assert_eq!(stats.output_underruns.load(Ordering::Relaxed), 1);
assert_eq!(stats.recent_pcm_underruns.load(Ordering::Relaxed), 1);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_output_prebuffer_includes_one_device_callback() {
let cases = [
(4_096, 48_000, 6_976),
(96_000, 48_000, 98_880),
(4_096, 192_000, 3_904),
(4_096, 24_000, 11_072),
];
for (callback_frames, output_sample_rate, expected) in cases {
assert_eq!(
voice_output_prebuffer_frames(callback_frames, output_sample_rate),
expected
);
}
let (tx, rx) = std::sync::mpsc::sync_channel(1);
tx.try_send(vec![1.0, -1.0])
.expect("decoded samples should queue");
let stats = Arc::new(VoiceAudioOutputStats::default());
let mut buffer = VoiceAudioBuffer::new(rx, DISCORD_VOICE_SAMPLE_RATE, Arc::clone(&stats));
stats.queued_frames.store(6_975, Ordering::Relaxed);
buffer.begin_output(4_096);
assert_eq!(buffer.next_stereo_frame(), None);
stats.queued_frames.store(6_976, Ordering::Relaxed);
buffer.begin_output(4_096);
assert_eq!(buffer.next_stereo_frame(), Some([1.0, -1.0]));
}
#[test]
fn remote_speaking_activity_ignores_silence_and_unplayable_media() {
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::Plain(DISCORD_OPUS_SILENCE_FRAME.to_vec()),
));
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveDecrypted {
user_id: 42,
opus: DISCORD_OPUS_SILENCE_FRAME.to_vec(),
},
));
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveUnexpectedPlain { payload_len: 4 },
));
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveMissingUser { payload_len: 4 },
));
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveNotReady {
user_id: 42,
payload_len: 4,
},
));
assert!(!voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveDecryptFailed {
user_id: 42,
message: "failed".to_owned(),
},
));
assert!(voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::Plain(b"opus".to_vec()),
));
assert!(voice_media_payload_counts_as_remote_activity(
&VoiceMediaPayload::DaveDecrypted {
user_id: 42,
opus: b"opus".to_vec(),
},
));
}
#[test]
fn microphone_sensitivity_filters_quiet_pcm_frames() {
let quiet = vec![100i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let loud = vec![4000i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
assert!(voice_pcm_frame_reaches_sensitivity(
&quiet,
MicrophoneSensitivityDb::new(-60),
));
assert!(!voice_pcm_frame_reaches_sensitivity(
&quiet,
MicrophoneSensitivityDb::new(-30),
));
assert!(voice_pcm_frame_reaches_sensitivity(
&normal,
MicrophoneSensitivityDb::default(),
));
assert!(voice_pcm_frame_reaches_sensitivity(
&loud,
MicrophoneSensitivityDb::new(-20),
));
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_gate_hangover_keeps_short_quiet_gaps_open() {
let quiet = vec![100i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let mut gate = VoiceMicrophoneGateState::default();
assert!(gate.allows_frame(&normal, MicrophoneSensitivityDb::default()));
for _ in 0..VOICE_MIC_GATE_HANGOVER_FRAMES {
assert!(gate.allows_frame(&quiet, MicrophoneSensitivityDb::default()));
}
assert!(!gate.allows_frame(&quiet, MicrophoneSensitivityDb::default()));
gate.allows_frame(&normal, MicrophoneSensitivityDb::default());
gate.reset();
assert!(!gate.allows_frame(&quiet, MicrophoneSensitivityDb::default()));
}
#[test]
fn voice_volume_scales_i16_pcm_frame() {
let mut frame = vec![1000, -1000, i16::MAX, i16::MIN];
let limited =
apply_voice_microphone_gain_and_limit(&mut frame, VoiceVolumePercent::new(50).gain());
assert_eq!(frame, vec![500, -500, 16384, -16384]);
assert_eq!(limited, 0);
let mut boosted = vec![1000, -1000, 20_000, -20_000];
let limited =
apply_voice_microphone_gain_and_limit(&mut boosted, VoiceVolumePercent::new(200).gain());
assert_eq!(boosted[0..2], [2000, -2000]);
assert!(boosted[2] < i16::MAX);
assert!(boosted[3] > i16::MIN);
assert_eq!(boosted[2], -boosted[3]);
assert_eq!(limited, 2);
}
#[test]
fn voice_microphone_protection_soft_limits_extreme_samples() {
let mut frame = vec![1000, -1000, i16::MAX, i16::MIN];
let limited = apply_voice_microphone_gain_and_limit(&mut frame, 1.0);
assert_eq!(frame[0], 1000);
assert_eq!(frame[1], -1000);
assert!(frame[2] < i16::MAX);
assert!(frame[3] > i16::MIN);
assert!((i32::from(frame[2]) + i32::from(frame[3])).abs() <= 1);
assert_eq!(limited, 2);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_microphone_conditioning_combines_gain_before_soft_limiting() {
let mut frame = vec![1000, 12_000, 20_000, -12_000, -20_000];
let mut microphone_gate = VoiceMicrophoneGateState::default();
let mut transmit_stats = VoiceUdpTransmitStats::default();
condition_voice_microphone_frame(
&mut frame,
VoiceCaptureGate {
capture_enabled: true,
transmit_enabled: true,
use_voice_activity: true,
noise_suppression: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::new(200),
},
&mut microphone_gate,
&mut transmit_stats,
);
assert_eq!(frame[0], 3000);
assert!(frame[1] < frame[2]);
assert!(frame[2] < i16::MAX);
assert_eq!(frame[1], -frame[3]);
assert_eq!(frame[2], -frame[4]);
assert_eq!(transmit_stats.limited_samples, 4);
}
#[test]
fn voice_microphone_overload_detects_dense_clipping_not_single_peaks() {
let mut normal_loud = vec![8_000i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
normal_loud[0] = i16::MAX;
normal_loud[1] = i16::MIN + 1;
assert!(!voice_microphone_frame_is_overloaded(&normal_loud));
let mut below_threshold = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in below_threshold
.iter_mut()
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES - 1)
{
*sample = i16::MAX;
}
assert!(!voice_microphone_frame_is_overloaded(&below_threshold));
let mut overloaded = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in overloaded
.iter_mut()
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
assert!(voice_microphone_frame_is_overloaded(&overloaded));
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_gate_blanks_handling_noise_envelope() {
let mut gate = VoiceMicrophoneGateState::default();
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let mut handling_noise = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
handling_noise[0] = i16::MAX;
handling_noise[1] = i16::MIN + 1;
for sample in handling_noise
.iter_mut()
.skip(2)
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES - 2)
{
*sample = i16::MAX;
}
let overload_decision = gate
.overload_decision(&handling_noise)
.expect("handling-noise frame should be blanked");
assert_eq!(
overload_decision.kind,
VoiceMicrophoneOverloadKind::HandlingNoise
);
assert_eq!(overload_decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
for _ in 0..VOICE_MIC_HANDLING_NOISE_SUPPRESSION_FRAMES {
let recovery_decision = gate
.overload_decision(&normal)
.expect("handling-noise envelope should be blanked");
assert_eq!(
recovery_decision.kind,
VoiceMicrophoneOverloadKind::Recovery
);
assert_eq!(recovery_decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
}
assert!(gate.overload_decision(&normal).is_none());
gate.overload_decision(&handling_noise);
gate.reset();
assert!(gate.overload_decision(&normal).is_none());
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_gate_ramps_after_non_handling_transient() {
let mut gate = VoiceMicrophoneGateState::default();
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let mut transient = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in transient
.iter_mut()
.take(VOICE_MIC_OVERLOAD_SEVERE_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
let overload_decision = gate
.overload_decision(&transient)
.expect("transient frame should be attenuated");
assert_eq!(
overload_decision.kind,
VoiceMicrophoneOverloadKind::Transient
);
assert_eq!(overload_decision.gain, VOICE_MIC_OVERLOAD_TRANSIENT_GAIN);
let mut previous_gain = overload_decision.gain;
for frame_index in 0..VOICE_MIC_OVERLOAD_RECOVERY_FRAMES {
let recovery_decision = gate
.overload_decision(&normal)
.expect("transient recovery should be ramped");
assert_eq!(
recovery_decision.kind,
VoiceMicrophoneOverloadKind::Recovery
);
if frame_index == 0 {
assert!(
(recovery_decision.gain - VOICE_MIC_OVERLOAD_RECOVERY_START_GAIN).abs()
< f32::EPSILON
);
}
assert!(recovery_decision.gain > previous_gain);
assert!(recovery_decision.gain <= 1.0);
previous_gain = recovery_decision.gain;
}
assert!(gate.overload_decision(&normal).is_none());
}
#[test]
fn voice_microphone_overload_gain_keeps_shouted_frame_audible() {
let mut shouted = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in shouted
.iter_mut()
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
let gain = voice_microphone_overload_gain(&shouted)
.expect("clipped shouted frame should be gain-reduced");
apply_voice_microphone_gain_and_limit(&mut shouted, gain);
assert_eq!(gain, VOICE_MIC_OVERLOAD_ATTENUATION_GAIN);
assert!(shouted.iter().any(|sample| *sample > 0));
assert!(
shouted
.iter()
.all(|sample| i32::from(*sample).abs() < i32::from(i16::MAX))
);
}
#[test]
fn voice_microphone_blanks_clipped_frames_except_handling_noise() {
let mut sparse_clip = vec![2000i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in sparse_clip
.iter_mut()
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES - 2)
{
*sample = i16::MAX;
}
let mut attenuated = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in attenuated
.iter_mut()
.take(VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
let mut handling_noise = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
handling_noise[0] = i16::MAX;
handling_noise[1] = i16::MIN + 1;
let cases = [
("sparse unclassified clip", sparse_clip, None, true),
(
"attenuated clip",
attenuated,
Some(VoiceMicrophoneOverloadKind::Attenuated),
true,
),
(
"handling noise",
handling_noise,
Some(VoiceMicrophoneOverloadKind::HandlingNoise),
false,
),
(
"clean frame",
vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES],
None,
false,
),
];
for (name, frame, expected_kind, needs_blank) in cases {
let raw_decision = voice_microphone_overload_decision(&frame);
assert_eq!(
raw_decision.map(|decision| decision.kind),
expected_kind,
"{name}"
);
assert_eq!(
voice_microphone_clipped_frame_needs_blank(&frame, raw_decision),
needs_blank,
"{name}"
);
}
}
#[test]
fn voice_microphone_handling_noise_uses_adjacent_delta_without_dense_clipping() {
let mut handling_noise = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
handling_noise[0] = 22_000;
handling_noise[1] = -20_001;
let decision = voice_microphone_overload_decision(&handling_noise)
.expect("large adjacent delta should classify handling noise");
assert_eq!(decision.kind, VoiceMicrophoneOverloadKind::HandlingNoise);
assert_eq!(decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
assert_eq!(voice_microphone_clipped_sample_count(&handling_noise), 0);
}
#[test]
fn voice_microphone_overload_promotes_sparse_clipped_transients_to_handling_noise() {
for (name, second_sample, min_delta, max_delta) in [
(
"impulse",
-3_233,
VOICE_MIC_OVERLOAD_IMPULSE_DELTA,
VOICE_MIC_HANDLING_NOISE_DELTA,
),
(
"step",
i16::MAX,
VOICE_MIC_OVERLOAD_CLIPPED_STEP_DELTA,
VOICE_MIC_OVERLOAD_IMPULSE_DELTA,
),
] {
let mut frame = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
frame[0] = i16::MAX;
frame[1] = second_sample;
let decision = voice_microphone_overload_decision(&frame)
.unwrap_or_else(|| panic!("clipped {name} should be gain-reduced"));
assert_eq!(
decision.kind,
VoiceMicrophoneOverloadKind::HandlingNoise,
"{name}"
);
assert_eq!(decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN, "{name}");
let max_adjacent_delta = voice_microphone_max_adjacent_delta(&frame);
assert!(max_adjacent_delta >= min_delta, "{name}");
assert!(max_adjacent_delta < max_delta, "{name}");
assert!(
voice_microphone_clipped_sample_count(&frame) < VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES,
"{name}"
);
}
}
#[test]
fn voice_microphone_overload_gain_keeps_sub_extreme_same_polarity_clip_audible() {
let mut clipped = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in clipped
.iter_mut()
.take(VOICE_MIC_OVERLOAD_EXTREME_CLIPPED_SAMPLES - 1)
{
*sample = i16::MAX;
}
assert_eq!(
voice_microphone_overload_gain(&clipped),
Some(VOICE_MIC_OVERLOAD_TRANSIENT_GAIN)
);
}
#[test]
fn voice_microphone_overload_gain_blanks_extreme_same_polarity_clip() {
let mut clipped = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in clipped
.iter_mut()
.take(VOICE_MIC_OVERLOAD_EXTREME_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
let decision = voice_microphone_overload_decision(&clipped)
.expect("extreme clipped frame should be blanked");
assert_eq!(decision.kind, VoiceMicrophoneOverloadKind::HandlingNoise);
assert_eq!(decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
assert_eq!(
voice_microphone_clipped_sample_count(&clipped),
VOICE_MIC_OVERLOAD_EXTREME_CLIPPED_SAMPLES
);
}
#[test]
fn voice_identify_payload_matches_expected_shape() {
let session = VoiceGatewaySession {
connection_id: 0,
scope: VoiceScope::Guild(Id::new(1)),
channel_id: Id::new(10),
user_id: Id::new(20),
session_id: "voice-session".to_owned(),
endpoint: "voice.example.com".to_owned(),
token: "voice-token".to_owned(),
};
let payload: Value = serde_json::from_str(&voice_identify_payload(&session))
.expect("voice identify payload is valid JSON");
assert_eq!(payload["op"].as_u64(), Some(0));
assert_eq!(payload["d"]["server_id"].as_str(), Some("1"));
assert_eq!(payload["d"]["user_id"].as_str(), Some("20"));
assert_eq!(payload["d"]["channel_id"].as_str(), Some("10"));
assert_eq!(payload["d"]["session_id"].as_str(), Some("voice-session"));
assert_eq!(payload["d"]["token"].as_str(), Some("voice-token"));
assert_eq!(
payload["d"]["max_dave_protocol_version"].as_u64(),
Some(u64::from(davey::DAVE_PROTOCOL_VERSION))
);
let heartbeat: Value = serde_json::from_str(&voice_heartbeat_payload(42))
.expect("voice heartbeat payload is valid JSON");
assert_eq!(heartbeat["op"].as_u64(), Some(3));
assert_eq!(heartbeat["d"]["seq_ack"].as_i64(), Some(42));
let resume: Value = serde_json::from_str(&voice_resume_payload(&session, 43))
.expect("voice resume payload is valid JSON");
assert_eq!(resume["op"].as_u64(), Some(7));
assert_eq!(resume["d"]["server_id"].as_str(), Some("1"));
assert_eq!(resume["d"]["channel_id"].as_str(), Some("10"));
assert_eq!(resume["d"]["session_id"].as_str(), Some("voice-session"));
assert_eq!(resume["d"]["token"].as_str(), Some("voice-token"));
assert_eq!(resume["d"]["seq_ack"].as_i64(), Some(43));
let mut heartbeat_ack = VoiceHeartbeatAckState::default();
assert!(heartbeat_ack.mark_sent());
assert!(!heartbeat_ack.mark_sent());
heartbeat_ack.mark_acknowledged();
assert!(heartbeat_ack.mark_sent());
}
#[test]
fn voice_gateway_url_normalizes_endpoint() {
assert_eq!(
voice_gateway_url("voice.example.com:2048/").as_deref(),
Ok("wss://voice.example.com:2048/?v=9")
);
assert_eq!(
voice_gateway_url("wss://voice.example.com").as_deref(),
Ok("wss://voice.example.com/?v=9")
);
assert_eq!(
voice_gateway_url("https://voice.example.com").as_deref(),
Ok("wss://voice.example.com/?v=9")
);
assert_eq!(
voice_gateway_url(" /").expect_err("empty endpoint should be rejected"),
"voice endpoint is empty"
);
}
#[test]
fn voice_ready_payload_parses_udp_transport_fields() {
let payload = json!({
"op": 2,
"d": {
"ssrc": 0x01020304u32,
"ip": "203.0.113.10",
"port": 50000u64,
"modes": [
"aead_xchacha20_poly1305_rtpsize",
"aead_aes256_gcm_rtpsize"
],
},
});
let ready = parse_voice_ready_payload(&payload).expect("ready payload should parse");
assert_eq!(ready.ssrc, 0x01020304);
assert_eq!(ready.ip, "203.0.113.10");
assert_eq!(ready.port, 50000);
assert_eq!(
choose_encryption_mode(&ready.modes).as_deref(),
Ok(AEAD_AES256_GCM_RTPSIZE)
);
}
#[test]
fn udp_discovery_and_select_protocol_match_expected_shapes() {
let packet = udp_discovery_request(0x01020304);
assert_eq!(packet.len(), UDP_DISCOVERY_PACKET_LEN);
assert_eq!(
&packet[..8],
&[0x00, 0x01, 0x00, 0x46, 0x01, 0x02, 0x03, 0x04]
);
assert!(packet[8..].iter().all(|byte| *byte == 0));
let mut response = [0u8; UDP_DISCOVERY_PACKET_LEN];
response[0..2].copy_from_slice(&2u16.to_be_bytes());
response[2..4].copy_from_slice(&70u16.to_be_bytes());
response[4..8].copy_from_slice(&0x01020304u32.to_be_bytes());
response[8..21].copy_from_slice(b"203.0.113.10\0");
response[72..74].copy_from_slice(&50000u16.to_be_bytes());
let discovered = parse_udp_discovery_response(&response, 0x01020304)
.expect("discovery response should parse");
assert_eq!(
discovered,
DiscoveredVoiceAddress {
address: "203.0.113.10".to_owned(),
port: 50000,
}
);
let payload: Value = serde_json::from_str(&voice_select_protocol_payload(
&discovered,
AEAD_XCHACHA20_POLY1305_RTPSIZE,
))
.expect("select protocol payload should be valid JSON");
assert_eq!(payload["op"].as_u64(), Some(1));
assert_eq!(payload["d"]["protocol"].as_str(), Some("udp"));
assert_eq!(
payload["d"]["data"]["address"].as_str(),
Some("203.0.113.10")
);
assert_eq!(payload["d"]["data"]["port"].as_u64(), Some(50000));
assert_eq!(
payload["d"]["data"]["mode"].as_str(),
Some(AEAD_XCHACHA20_POLY1305_RTPSIZE)
);
}
#[test]
fn udp_keepalive_packet_round_trips_little_endian_counter() {
let packet = udp_keepalive_packet(0x01020304);
assert_eq!(packet, [0x04, 0x03, 0x02, 0x01, 0, 0, 0, 0]);
assert_eq!(parse_udp_keepalive_response(&packet), Some(0x01020304));
assert_eq!(parse_udp_keepalive_response(&packet[..7]), None);
}
#[tokio::test]
async fn voice_udp_keepalive_sends_initial_counter() {
let receiver = UdpSocket::bind("127.0.0.1:0")
.await
.expect("receiver should bind");
let sender = Arc::new(
UdpSocket::bind("127.0.0.1:0")
.await
.expect("sender should bind"),
);
sender
.connect(
receiver
.local_addr()
.expect("receiver should have an address"),
)
.await
.expect("sender should connect");
let keepalive = tokio::spawn(run_voice_udp_keepalive(sender));
let mut packet = [0u8; UDP_KEEPALIVE_PACKET_LEN];
let received = timeout(Duration::from_secs(1), receiver.recv(&mut packet))
.await
.expect("keepalive should arrive")
.expect("receiver should read the keepalive");
keepalive.abort();
assert_eq!(received, UDP_KEEPALIVE_PACKET_LEN);
assert_eq!(parse_udp_keepalive_response(&packet), Some(0));
}
#[test]
fn voice_session_description_parses_mode_and_redacts_secret() {
let payload = json!({
"op": 4,
"d": {
"mode": AEAD_XCHACHA20_POLY1305_RTPSIZE,
"secret_key": (0u8..32).collect::<Vec<_>>(),
"dave_protocol_version": 1,
},
});
let description =
parse_voice_session_description(&payload).expect("session description should parse");
let debug = format!("{description:?}");
assert_eq!(description.mode, AEAD_XCHACHA20_POLY1305_RTPSIZE);
assert_eq!(description.secret_key.len(), 32);
assert_eq!(description.dave_protocol_version, Some(1));
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("31"));
}
#[test]
fn rtp_header_parses_minimal_and_extended_packets() {
let packet = [
0x80, 0x78, 0x12, 0x34, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
];
let header = parse_rtp_header(&packet).expect("RTP header should parse");
assert_eq!(
header,
RtpHeader {
has_padding: false,
marker: false,
payload_type: 0x78,
sequence: 0x1234,
timestamp: 0x01020304,
ssrc: 0x05060708,
authenticated_header_len: 12,
encrypted_extension_body_len: 0,
payload_offset: 12,
}
);
let mut extended = vec![0x91, 0x78, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
extended.extend_from_slice(&0x11223344u32.to_be_bytes());
extended.extend_from_slice(&0x1000u16.to_be_bytes());
extended.extend_from_slice(&1u16.to_be_bytes());
extended.extend_from_slice(&0x55667788u32.to_be_bytes());
let header = parse_rtp_header(&extended).expect("extended RTP header should parse");
assert_eq!(header.authenticated_header_len, 20);
assert_eq!(header.encrypted_extension_body_len, 4);
assert_eq!(header.payload_offset, 24);
}
#[test]
fn rtp_decrypts_aead_rtpsize_modes_and_strips_extension_body_and_padding() {
let key = [7u8; 32];
let nonce_suffix = [1, 2, 3, 4];
let mut header = vec![0xb0, 0x78, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9];
header.extend_from_slice(&0x1000u16.to_be_bytes());
header.extend_from_slice(&1u16.to_be_bytes());
let plaintext = [
b"ext!".as_slice(),
b"opus-frame".as_slice(),
[0, 0, 3].as_slice(),
]
.concat();
for mode in [AEAD_AES256_GCM_RTPSIZE, AEAD_XCHACHA20_POLY1305_RTPSIZE] {
let mut packet = header.clone();
packet.extend(encrypt_test_rtp_payload(
mode,
&key,
&header,
&plaintext,
nonce_suffix,
));
packet.extend_from_slice(&nonce_suffix);
let rtp_header = parse_rtp_header(&packet).expect("RTP header should parse");
let decryptor = VoiceRtpDecryptor::new(mode, &key).expect("decryptor should build");
assert!(rtp_header.has_padding);
let decrypted = decryptor
.decrypt_packet(&packet, &rtp_header)
.expect("RTP payload should decrypt");
assert_eq!(decrypted.encrypted_extension_body_len, 4);
assert_eq!(decrypted.media_payload, b"opus-frame");
}
}
#[test]
fn outbound_rtp_packet_builder_sets_header_and_advances_state() {
let mut state = VoiceOutboundRtpState {
sequence: u16::MAX,
timestamp: u32::MAX - 100,
ssrc: 0x01020304,
};
let packet = state
.packetize(&DISCORD_OPUS_SILENCE_FRAME)
.expect("RTP packet should build");
let header = parse_rtp_header(&packet).expect("RTP header should parse");
assert_eq!(packet[0], 0x80);
assert_eq!(header.payload_type, DISCORD_VOICE_PAYLOAD_TYPE);
assert_eq!(header.sequence, u16::MAX);
assert_eq!(header.timestamp, u32::MAX - 100);
assert_eq!(header.ssrc, 0x01020304);
assert_eq!(header.payload_offset, RTP_HEADER_MIN_LEN);
assert_eq!(&packet[header.payload_offset..], DISCORD_OPUS_SILENCE_FRAME);
assert_eq!(state.sequence, 0);
assert_eq!(
state.timestamp,
(u32::MAX - 100).wrapping_add(DISCORD_OPUS_TIMESTAMP_INCREMENT)
);
assert_eq!(
build_voice_rtp_packet(1, 2, 3, &[]).expect_err("empty payload should fail"),
"voice RTP packet requires a non-empty Opus payload"
);
}
#[test]
fn outbound_rtp_encrypts_aead_rtpsize_modes_for_decrypt_round_trip() {
let key = [9u8; 32];
let nonce_suffix = [4, 3, 2, 1];
let packet =
build_voice_rtp_packet(7, 960, 42, b"opus-frame").expect("RTP packet should build");
for mode in [AEAD_AES256_GCM_RTPSIZE, AEAD_XCHACHA20_POLY1305_RTPSIZE] {
let encryptor = VoiceRtpEncryptor::new(mode, &key).expect("encryptor should build");
let encrypted = encryptor
.encrypt_packet(&packet, nonce_suffix)
.expect("RTP payload should encrypt");
let header = parse_rtp_header(&encrypted).expect("encrypted RTP header should parse");
let decryptor = VoiceRtpDecryptor::new(mode, &key).expect("decryptor should build");
let decrypted = decryptor
.decrypt_packet(&encrypted, &header)
.expect("RTP payload should decrypt");
assert_eq!(
&encrypted[encrypted.len() - RTP_AEAD_NONCE_SUFFIX_BYTES..],
nonce_suffix
);
assert_eq!(header.sequence, 7);
assert_eq!(header.timestamp, 960);
assert_eq!(header.ssrc, 42);
assert_eq!(decrypted.media_payload, b"opus-frame");
}
}
#[test]
fn opus_encoder_encodes_decodable_20ms_stereo_frame() {
let mut encoder = VoiceOpusEncode::new().expect("Opus encoder should build");
let pcm = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let opus = encoder
.encode_20ms_i16(&pcm)
.expect("20 ms stereo frame should encode");
assert!(!opus.is_empty());
let mut decoder = OpusDecoder::new(DISCORD_VOICE_SAMPLE_RATE, Channels::Stereo)
.expect("Opus decoder should build");
let mut decoded = vec![0.0f32; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let samples_per_channel = decoder
.decode_float(&opus, &mut decoded, false)
.expect("encoded Opus should decode");
assert_eq!(samples_per_channel, DISCORD_OPUS_FRAME_SAMPLES_PER_CHANNEL);
assert_eq!(
encoder
.encode_20ms_i16(&pcm[..pcm.len() - 1])
.expect_err("short frame should fail"),
format!(
"voice Opus encoder expected {} interleaved stereo samples, got {}",
DISCORD_OPUS_20MS_STEREO_SAMPLES,
DISCORD_OPUS_20MS_STEREO_SAMPLES - 1
)
);
}
#[test]
fn system_audio_opus_encoder_encodes_decodable_20ms_stereo_frame() {
let mut encoder =
VoiceOpusEncode::new_system_audio().expect("system audio Opus encoder should build");
let pcm = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let opus = encoder
.encode_20ms_i16(&pcm)
.expect("system audio frame should encode");
let mut decoder = OpusDecoder::new(DISCORD_VOICE_SAMPLE_RATE, Channels::Stereo)
.expect("Opus decoder should build");
let mut decoded = vec![0.0f32; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let samples_per_channel = decoder
.decode_float(&opus, &mut decoded, false)
.expect("system audio Opus should decode");
assert_eq!(samples_per_channel, DISCORD_OPUS_FRAME_SAMPLES_PER_CHANNEL);
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_input_conversion_produces_20ms_stereo_frames() {
let mono = vec![0.5f32; DISCORD_OPUS_FRAME_SAMPLES_PER_CHANNEL];
let stereo = voice_input_f32_to_stereo_i16(&mono, 1);
assert_eq!(stereo.len(), DISCORD_OPUS_20MS_STEREO_SAMPLES);
assert_eq!(stereo[0], stereo[1]);
assert!(stereo[0] > 0);
let interleaved = voice_input_i16_to_stereo_i16(&[1, 2, 3, 4, 5, 6], 3);
assert_eq!(interleaved, vec![1, 2, 4, 5]);
let unsigned = voice_input_u8_to_stereo_i16(&[0, 255], 2);
assert_eq!(unsigned, vec![i16::MIN, 32512]);
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_pcm_frames_resample_44100_to_48000() {
let (tx, mut rx) = tokio::sync::mpsc::channel(4);
let stats = Arc::new(VoiceMicrophoneCaptureStats::default());
let mut frames = VoiceMicrophonePcmFrames::new(tx, Arc::clone(&stats), 44_100);
let input_frames = 883;
let mut samples = Vec::with_capacity(input_frames * DISCORD_VOICE_CHANNELS_USIZE);
for index in 0..input_frames {
samples.push(index as i16);
samples.push(-(index as i16));
}
frames.push_stereo_samples(&samples);
let frame = rx
.try_recv()
.expect("resampled 20 ms frame should be queued");
assert_eq!(frame.samples.len(), DISCORD_OPUS_20MS_STEREO_SAMPLES);
assert_eq!(frame.samples[0], 0);
assert_eq!(frame.samples[1], 0);
assert!(frame.samples[frame.samples.len() - 2] > 870);
assert!(frame.samples[frame.samples.len() - 1] < -870);
assert!(rx.try_recv().is_err());
assert_eq!(stats.queued_frames.load(Ordering::Relaxed), 1);
assert_eq!(stats.dropped_frames.load(Ordering::Relaxed), 0);
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_pcm_frames_count_full_queue_drops() {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let stats = Arc::new(VoiceMicrophoneCaptureStats::default());
let mut frames =
VoiceMicrophonePcmFrames::new(tx, Arc::clone(&stats), DISCORD_VOICE_SAMPLE_RATE);
let samples = vec![1i16; DISCORD_OPUS_20MS_STEREO_SAMPLES * 2];
frames.push_stereo_samples(&samples);
assert_eq!(
rx.try_recv()
.expect("first frame should queue")
.samples
.len(),
DISCORD_OPUS_20MS_STEREO_SAMPLES
);
assert_eq!(stats.queued_frames.load(Ordering::Relaxed), 1);
assert_eq!(stats.dropped_frames.load(Ordering::Relaxed), 1);
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_freshness_policy_bounds_queue_depth_and_frame_age() {
let now = Instant::now();
let (tx, mut rx) = tokio::sync::mpsc::channel(VOICE_MIC_PCM_FRAME_QUEUE);
let initial = VoiceMicrophoneFrame {
samples: vec![0],
captured_at: now - Duration::from_millis(320),
};
for index in 1u8..=15 {
tx.try_send(VoiceMicrophoneFrame {
samples: vec![i16::from(index)],
captured_at: now - Duration::from_millis(u64::from(15 - index) * 20),
})
.expect("backlog frame should queue");
}
let (selected, dropped) = select_fresh_voice_microphone_frame(initial, &mut rx, now);
let selected = selected.expect("a fresh frame should remain");
assert_eq!(selected.samples, vec![13]);
assert_eq!(dropped, 13);
assert_eq!(rx.len().saturating_add(1), VOICE_MIC_MAX_LIVE_FRAMES);
assert!(now.saturating_duration_since(selected.captured_at) <= VOICE_MIC_MAX_FRAME_AGE);
let (_tx, mut rx) = tokio::sync::mpsc::channel(1);
let boundary = VoiceMicrophoneFrame {
samples: vec![50],
captured_at: now - VOICE_MIC_MAX_FRAME_AGE,
};
let (selected, dropped) = select_fresh_voice_microphone_frame(boundary, &mut rx, now);
assert_eq!(
selected.expect("boundary frame should remain live").samples,
vec![50]
);
assert_eq!(dropped, 0);
let (_tx, mut rx) = tokio::sync::mpsc::channel(1);
let stale = VoiceMicrophoneFrame {
samples: vec![99],
captured_at: now - VOICE_MIC_MAX_FRAME_AGE - Duration::from_millis(1),
};
let (selected, dropped) = select_fresh_voice_microphone_frame(stale, &mut rx, now);
assert!(selected.is_none());
assert_eq!(dropped, 1);
}
#[cfg(feature = "voice-playback")]
#[tokio::test]
async fn voice_child_tasks_waits_for_udp_transmit_shutdown() {
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
let mut child_tasks = VoiceChildTasks::default();
child_tasks.udp_transmit = Some(tokio::spawn(async move {
sleep(Duration::from_millis(10)).await;
let _ = done_tx.send(());
}));
child_tasks.shutdown_all().await;
done_rx
.await
.expect("shutdown should await UDP transmit completion");
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_udp_transmit_reports_only_failures_to_the_gateway() {
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
publish_voice_udp_transmit_failure(Ok(()), &failure_tx);
assert_eq!(failure_rx.try_recv(), Err(mpsc::error::TryRecvError::Empty));
publish_voice_udp_transmit_failure(Err("closed voice websocket".to_owned()), &failure_tx);
assert_eq!(
failure_rx
.try_recv()
.expect("fatal transmit errors should reach the gateway"),
"closed voice websocket"
);
}
#[tokio::test]
async fn voice_runtime_stops_connection_task_by_closing_gate_channels() {
let (audio_sources_tx, mut audio_sources_rx) = watch::channel(VoiceAudioSourceSelection {
generation: 0,
sources: VoiceAudioSources::default(),
});
let (capture_gate_tx, mut capture_gate_rx) = mpsc::unbounded_channel();
let (playback_gate_tx, mut playback_gate_rx) = mpsc::unbounded_channel();
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
let mut connection_task = Some(tokio::spawn(async move {
assert!(audio_sources_rx.changed().await.is_err());
assert!(capture_gate_rx.recv().await.is_none());
assert!(playback_gate_rx.recv().await.is_none());
let _ = done_tx.send(());
}));
let mut connection_session = None;
let mut audio_sources_tx = Some(audio_sources_tx);
let mut capture_gate_tx = Some(capture_gate_tx);
let mut playback_gate_tx = Some(playback_gate_tx);
let stopped_session = stop_voice_connection_task(
&mut connection_task,
&mut connection_session,
&mut audio_sources_tx,
&mut capture_gate_tx,
&mut playback_gate_tx,
"test voice connection stop",
)
.await;
assert!(stopped_session.is_none());
done_rx
.await
.expect("connection task should finish after gate channels close");
assert!(connection_task.is_none());
assert!(audio_sources_tx.is_none());
assert!(capture_gate_tx.is_none());
assert!(playback_gate_tx.is_none());
}
#[test]
fn voice_audio_source_watch_retains_only_the_latest_selection() {
let (audio_sources_tx, audio_sources_rx) = watch::channel(VoiceAudioSourceSelection {
generation: 0,
sources: VoiceAudioSources::default(),
});
audio_sources_tx.send_replace(VoiceAudioSourceSelection {
generation: 1,
sources: VoiceAudioSources {
input: Some("mic-1".to_owned()),
output: None,
},
});
audio_sources_tx.send_replace(VoiceAudioSourceSelection {
generation: 2,
sources: VoiceAudioSources {
input: Some("mic-2".to_owned()),
output: Some("speaker-2".to_owned()),
},
});
assert_eq!(
audio_sources_rx.borrow().clone(),
VoiceAudioSourceSelection {
generation: 2,
sources: VoiceAudioSources {
input: Some("mic-2".to_owned()),
output: Some("speaker-2".to_owned()),
},
}
);
}
#[tokio::test]
async fn voice_runtime_requests_speaking_cleanup_after_connection_task_failure() {
let mut connection_task = Some(tokio::spawn(async {
panic!("simulated voice connection task failure");
}));
let expected_session = test_voice_gateway_session();
let mut connection_session = Some(expected_session.clone());
let mut audio_sources_tx = None;
let mut capture_gate_tx = None;
let mut playback_gate_tx = None;
let stopped_session = stop_voice_connection_task(
&mut connection_task,
&mut connection_session,
&mut audio_sources_tx,
&mut capture_gate_tx,
&mut playback_gate_tx,
"test failed voice connection stop",
)
.await;
assert_eq!(stopped_session, Some(expected_session));
assert!(connection_task.is_none());
assert!(connection_session.is_none());
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_pcm_drain_clears_backlog_before_reenable() {
let (tx, mut rx) = tokio::sync::mpsc::channel(VOICE_MIC_PCM_FRAME_QUEUE);
let now = Instant::now();
tx.try_send(VoiceMicrophoneFrame {
samples: vec![10],
captured_at: now,
})
.expect("first frame should queue");
tx.try_send(VoiceMicrophoneFrame {
samples: vec![20],
captured_at: now,
})
.expect("second frame should queue");
drain_voice_microphone_pcm_queue(&mut rx);
assert!(matches!(
rx.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
));
}
#[cfg(feature = "voice-playback")]
#[test]
fn microphone_capture_stats_track_callback_size_and_clipping() {
let stats = VoiceMicrophoneCaptureStats::default();
record_voice_input_chunk(960, 2, &stats);
record_voice_input_chunk(480, 2, &stats);
record_voice_input_pcm_stats(&[0, i16::MAX, i16::MIN + 1, 120], &stats);
assert_eq!(stats.chunks.load(Ordering::Relaxed), 2);
assert_eq!(stats.frames.load(Ordering::Relaxed), 720);
assert_eq!(voice_microphone_min_callback_frames(&stats), 240);
assert_eq!(stats.max_callback_frames.load(Ordering::Relaxed), 480);
assert_eq!(stats.peak_sample.load(Ordering::Relaxed), 32767);
assert_eq!(stats.clipped_samples.load(Ordering::Relaxed), 2);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_input_config_prefers_mono_then_sample_format() {
assert!(voice_input_channel_rank(1) < voice_input_channel_rank(2));
assert!(
voice_input_sample_format_rank(cpal::SampleFormat::F32)
< voice_input_sample_format_rank(cpal::SampleFormat::I16)
);
assert!(
voice_input_sample_format_rank(cpal::SampleFormat::I16)
< voice_input_sample_format_rank(cpal::SampleFormat::U16)
);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_input_buffer_size_requests_small_supported_fixed_buffer() {
let supported = cpal::SupportedBufferSize::Range { min: 128, max: 960 };
assert_eq!(
voice_input_buffer_size(&supported),
cpal::BufferSize::Fixed(VOICE_MIC_PREFERRED_BUFFER_FRAMES)
);
assert_eq!(
voice_input_buffer_size(&cpal::SupportedBufferSize::Unknown),
cpal::BufferSize::Default
);
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_output_buffer_size_requests_bounded_low_latency_buffer() {
let cases = [
(
true,
cpal::SupportedBufferSize::Range {
min: 128,
max: 8_192,
},
cpal::BufferSize::Fixed(VOICE_PULSE_OUTPUT_BUFFER_FRAMES),
),
(
true,
cpal::SupportedBufferSize::Range {
min: 4_096,
max: 8_192,
},
cpal::BufferSize::Fixed(4_096),
),
(
true,
cpal::SupportedBufferSize::Range { min: 128, max: 960 },
cpal::BufferSize::Fixed(960),
),
(
true,
cpal::SupportedBufferSize::Unknown,
cpal::BufferSize::Default,
),
(
false,
cpal::SupportedBufferSize::Range {
min: 128,
max: 8_192,
},
cpal::BufferSize::Default,
),
];
for (use_low_latency_pulse_audio, supported, expected) in cases {
assert_eq!(
voice_output_buffer_size(use_low_latency_pulse_audio, &supported),
expected
);
}
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_speaking_payload_matches_expected_shape() {
let on: Value = serde_json::from_str(&voice_speaking_payload(1234, true))
.expect("speaking-on payload should be JSON");
assert_eq!(on["op"].as_u64(), Some(u64::from(VOICE_OP_SPEAKING)));
assert_eq!(on["d"]["speaking"].as_u64(), Some(1));
assert_eq!(on["d"]["delay"].as_u64(), Some(0));
assert_eq!(on["d"]["ssrc"].as_u64(), Some(1234));
let off: Value = serde_json::from_str(&voice_speaking_payload(1234, false))
.expect("speaking-off payload should be JSON");
assert_eq!(off["d"]["speaking"].as_u64(), Some(0));
}
#[test]
fn fake_outbound_noops_when_capture_gate_is_closed() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 10);
let rtp = state.rtp;
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("send should no-op"),
VoiceOutboundSendOutcome::Noop
);
assert!(state.events().is_empty());
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, 10);
state.set_capture_gate(true, true);
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("muted send should no-op"),
VoiceOutboundSendOutcome::Noop
);
assert!(state.events().is_empty());
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, 10);
}
#[test]
fn fake_outbound_blocks_dave_active_plaintext_fallback() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 10);
state.set_capture_gate(true, false);
state.set_dave_active(true);
let rtp = state.rtp;
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("DAVE block should be reported"),
VoiceOutboundSendOutcome::Blocked(VoiceOutboundSendBlockReason::DaveOutboundUnsupported)
);
assert!(state.events().is_empty());
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, 10);
}
#[test]
fn fake_outbound_uses_dave_outbound_policy_before_transport_encrypt() {
let mut dave = VoiceDaveState::new(&test_voice_gateway_session());
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 30);
state.set_capture_gate(true, false);
assert_eq!(
state
.send_opus_frame_with_dave(b"opus-frame", &mut dave)
.expect("DAVE inactive frame should send"),
VoiceOutboundSendOutcome::Sent
);
assert_fake_packet(
AEAD_AES256_GCM_RTPSIZE,
&state.events()[1],
7,
960,
b"opus-frame",
30u32.to_be_bytes(),
true,
);
let mut dave = VoiceDaveState::new(&test_voice_gateway_session());
dave.reinit(1).expect("DAVE session should initialize");
let mut blocked = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 30);
blocked.set_capture_gate(true, false);
let rtp = blocked.rtp;
assert_eq!(
blocked
.send_opus_frame_with_dave(b"opus-frame", &mut dave)
.expect("DAVE not-ready frame should block"),
VoiceOutboundSendOutcome::Blocked(VoiceOutboundSendBlockReason::DaveOutboundNotReady)
);
assert!(blocked.events().is_empty());
assert_eq!(blocked.rtp, rtp);
assert_eq!(blocked.nonce_suffix, 30);
}
#[test]
fn fake_outbound_sends_encrypted_packets_without_live_io() {
for mode in [AEAD_AES256_GCM_RTPSIZE, AEAD_XCHACHA20_POLY1305_RTPSIZE] {
let mut state = fake_outbound_state(mode, 0x01020304);
state.set_capture_gate(true, false);
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("first frame should send"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(state.events().len(), 2);
assert_eq!(
state.events()[0],
VoiceOutboundSendEvent::Speaking {
speaking: true,
ssrc: 42,
}
);
assert_fake_packet(
mode,
&state.events()[1],
7,
960,
b"opus-frame",
[1, 2, 3, 4],
true,
);
assert_eq!(state.rtp.sequence, 8);
assert_eq!(state.rtp.timestamp, 960);
assert_eq!(state.nonce_suffix, 0x01020305);
state.advance_media_clock_frames(1);
assert_eq!(
state
.send_opus_frame(b"next-frame")
.expect("second frame should send"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(state.events().len(), 3);
assert_fake_packet(
mode,
&state.events()[2],
8,
1920,
b"next-frame",
[1, 2, 3, 5],
false,
);
assert_eq!(state.rtp.sequence, 9);
assert_eq!(state.rtp.timestamp, 1920);
assert_eq!(state.nonce_suffix, 0x01020306);
}
}
#[test]
fn fake_outbound_media_clock_and_talkspurt_marker_are_independent() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 10);
state.set_capture_gate(true, false);
state.advance_media_clock_frames(10);
assert_eq!(state.rtp.sequence, 7);
assert_eq!(state.rtp.timestamp, 10_560);
assert_eq!(state.nonce_suffix, 10);
assert_eq!(
state
.send_opus_frame(b"first-talkspurt")
.expect("first talkspurt should send"),
VoiceOutboundSendOutcome::Sent
);
let first_header = fake_packet_header(&state.events()[1]);
assert!(first_header.marker);
assert_eq!(first_header.timestamp, 10_560);
assert_eq!(state.rtp.sequence, 8);
assert_eq!(state.rtp.timestamp, 10_560);
assert_eq!(state.nonce_suffix, 11);
state.advance_media_clock_frames(1);
assert_eq!(
state
.send_opus_frame(b"same-talkspurt")
.expect("continued talkspurt should send"),
VoiceOutboundSendOutcome::Sent
);
let continued_header = fake_packet_header(&state.events()[2]);
assert!(!continued_header.marker);
assert_eq!(continued_header.timestamp, 11_520);
assert_eq!(
state.stop_speaking().expect("talkspurt should stop"),
VoiceOutboundSendOutcome::Sent
);
state.advance_media_clock_frames(50);
assert_eq!(
state
.send_opus_frame(b"next-talkspurt")
.expect("next talkspurt should send"),
VoiceOutboundSendOutcome::Sent
);
let next_header = fake_packet_header(
state
.events()
.last()
.expect("next talkspurt should queue a packet"),
);
assert!(next_header.marker);
assert_eq!(next_header.timestamp, 59_520);
}
#[test]
fn trailing_silence_is_paced_and_can_be_cancelled() {
let mut tail = VoiceTrailingSilence::default();
tail.start(true);
for index in 0..DISCORD_TRAILING_SILENCE_FRAMES {
assert_eq!(
tail.take_frame(),
Some(index + 1 == DISCORD_TRAILING_SILENCE_FRAMES)
);
}
assert_eq!(tail.take_frame(), None);
tail.start(true);
assert_eq!(tail.take_frame(), Some(false));
tail.cancel();
assert_eq!(tail.take_frame(), None);
tail.start(false);
assert_eq!(tail.take_frame(), None);
}
#[test]
fn microphone_capture_time_advances_rtp_clock_across_missing_frames() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 10);
let started_at = Instant::now();
let mut previous_frame_at = None;
advance_voice_media_clock(&mut state, &mut previous_frame_at, started_at);
assert_eq!(state.rtp.timestamp, 1920);
advance_voice_media_clock(
&mut state,
&mut previous_frame_at,
started_at + Duration::from_millis(20),
);
assert_eq!(state.rtp.timestamp, 2880);
advance_voice_media_clock(
&mut state,
&mut previous_frame_at,
started_at + Duration::from_millis(120),
);
assert_eq!(state.rtp.timestamp, 7680);
assert_eq!(state.rtp.sequence, 7);
assert_eq!(state.nonce_suffix, 10);
}
#[test]
fn fake_outbound_stop_paces_finite_silence_then_speaking_off() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 20);
state.set_capture_gate(true, false);
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("frame should send"),
VoiceOutboundSendOutcome::Sent
);
for index in 0..DISCORD_TRAILING_SILENCE_FRAMES {
state.advance_media_clock_frames(1);
assert_eq!(
state
.send_trailing_silence_frame_with_dave_payload(
VoiceDaveOutboundPayload::Plain(DISCORD_OPUS_SILENCE_FRAME.to_vec()),
index + 1 == DISCORD_TRAILING_SILENCE_FRAMES,
)
.expect("one trailing silence frame should send"),
VoiceOutboundSendOutcome::Sent
);
let expected_event_count =
index + 3 + usize::from(index + 1 == DISCORD_TRAILING_SILENCE_FRAMES);
assert_eq!(state.events().len(), expected_event_count);
assert_fake_packet(
AEAD_AES256_GCM_RTPSIZE,
&state.events()[index + 2],
8 + index as u16,
1920 + index as u32 * DISCORD_OPUS_TIMESTAMP_INCREMENT,
&DISCORD_OPUS_SILENCE_FRAME,
(21 + index as u32).to_be_bytes(),
false,
);
}
assert_eq!(
state.events()[DISCORD_TRAILING_SILENCE_FRAMES + 2],
VoiceOutboundSendEvent::Speaking {
speaking: false,
ssrc: 42,
}
);
assert_eq!(state.rtp.sequence, 13);
assert_eq!(state.rtp.timestamp, 5760);
assert_eq!(state.nonce_suffix, 26);
}
#[test]
fn fake_outbound_stop_sends_speaking_off_when_capture_gate_closes() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 20);
state.set_capture_gate(true, false);
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect("frame should send"),
VoiceOutboundSendOutcome::Sent
);
let event_count = state.events().len();
let rtp = state.rtp;
let nonce_suffix = state.nonce_suffix;
state.set_capture_gate(true, true);
assert_eq!(
state
.stop_speaking()
.expect("muted stop should send speaking off"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(state.events().len(), event_count + 1);
assert_eq!(
state.events()[event_count],
VoiceOutboundSendEvent::Speaking {
speaking: false,
ssrc: 42,
}
);
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, nonce_suffix);
state.speaking = true;
state.set_capture_gate(false, false);
assert_eq!(
state
.stop_speaking()
.expect("disallowed stop should send speaking off"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(state.events().len(), event_count + 2);
assert_eq!(
state.events()[event_count + 1],
VoiceOutboundSendEvent::Speaking {
speaking: false,
ssrc: 42,
}
);
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, nonce_suffix);
}
#[test]
fn fake_outbound_trailing_silence_uses_dave_policy() {
let mut dave = VoiceDaveState::new(&test_voice_gateway_session());
dave.reinit(1).expect("DAVE session should initialize");
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, 20);
state.set_capture_gate(true, false);
state.speaking = true;
let rtp = state.rtp;
assert_eq!(
state
.send_trailing_silence_frame_with_dave(&mut dave, false)
.expect("DAVE not-ready silence should still send speaking off"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(
state.events(),
&[VoiceOutboundSendEvent::Speaking {
speaking: false,
ssrc: 42,
}]
);
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, 20);
assert!(!state.speaking);
}
#[test]
fn fake_outbound_nonce_exhaustion_fails_without_state_change() {
let mut state = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, u32::MAX);
state.set_capture_gate(true, false);
let rtp = state.rtp;
assert_eq!(
state
.send_opus_frame(b"opus-frame")
.expect_err("exhausted nonce should fail"),
"voice RTP nonce suffix exhausted"
);
assert!(state.events().is_empty());
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, u32::MAX);
let mut stopping = fake_outbound_state(AEAD_AES256_GCM_RTPSIZE, u32::MAX - 2);
stopping.set_capture_gate(true, false);
stopping.speaking = true;
let rtp = stopping.rtp;
assert_eq!(
stopping
.stop_speaking()
.expect("stop should still clear speaking"),
VoiceOutboundSendOutcome::Sent
);
assert_eq!(
stopping.events(),
&[VoiceOutboundSendEvent::Speaking {
speaking: false,
ssrc: 42,
}]
);
assert_eq!(stopping.rtp, rtp);
assert_eq!(stopping.nonce_suffix, u32::MAX - 2);
assert!(!stopping.speaking);
}
#[test]
fn rtp_header_rejects_malformed_packets() {
assert_eq!(
parse_rtp_header(&[0; 11]).expect_err("short packet should fail"),
"RTP packet is too short"
);
let packet = [0x40, 0x78, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
assert_eq!(
parse_rtp_header(&packet).expect_err("wrong version should fail"),
"RTP packet has unsupported version"
);
}
#[test]
fn rtp_header_rejects_rtcp_reports_before_payload_type_masking() {
let local_ssrc = 0x0000_f5e7u32;
let mut receiver_report = vec![0x80, 0xc9, 0, 7];
receiver_report.extend_from_slice(&local_ssrc.to_be_bytes());
receiver_report.extend_from_slice(&[0, 0, 0, 0]);
assert!(looks_like_rtcp_packet(&receiver_report));
assert_eq!(rtcp_sender_ssrc(&receiver_report), Some(local_ssrc));
assert_eq!(
parse_rtp_header(&receiver_report).expect_err("RTCP should not parse as RTP"),
"RTP parser received RTCP packet"
);
let sender_report = [0x80, 0xc8, 0, 12, 0, 0, 0xf5, 0xe7, 0, 0, 0, 0];
assert!(looks_like_rtcp_packet(&sender_report));
assert_eq!(
parse_rtp_header(&sender_report).expect_err("RTCP should not parse as RTP"),
"RTP parser received RTCP packet"
);
}
fn fake_outbound_state(mode: &str, nonce_suffix: u32) -> VoiceOutboundSendState {
VoiceOutboundSendState::new(
mode,
&[9u8; 32],
VoiceOutboundRtpState {
sequence: 7,
timestamp: 960,
ssrc: 42,
},
nonce_suffix,
)
.expect("fake outbound state should build")
}
fn test_voice_gateway_session() -> VoiceGatewaySession {
VoiceGatewaySession {
connection_id: 0,
scope: VoiceScope::Guild(Id::new(1)),
channel_id: Id::new(10),
user_id: Id::new(20),
session_id: "voice-session".to_owned(),
endpoint: "voice.example.com".to_owned(),
token: "voice-token".to_owned(),
}
}
fn assert_fake_packet(
mode: &str,
event: &VoiceOutboundSendEvent,
sequence: u16,
timestamp: u32,
expected_payload: &[u8],
nonce_suffix: [u8; RTP_AEAD_NONCE_SUFFIX_BYTES],
marker: bool,
) {
let VoiceOutboundSendEvent::Packet { bytes } = event else {
panic!("expected fake packet event, got {event:?}");
};
let packet_bytes = bytes.as_slice();
let header = parse_rtp_header(packet_bytes).expect("fake RTP header should parse");
let decryptor = VoiceRtpDecryptor::new(mode, &[9u8; 32]).expect("decryptor should build");
let decrypted = decryptor
.decrypt_packet(packet_bytes, &header)
.expect("fake RTP packet should decrypt");
let actual_nonce_suffix = &packet_bytes[packet_bytes.len() - RTP_AEAD_NONCE_SUFFIX_BYTES..];
assert_eq!(actual_nonce_suffix, nonce_suffix.as_slice());
assert_eq!(header.marker, marker);
assert_eq!(header.sequence, sequence);
assert_eq!(header.timestamp, timestamp);
assert_eq!(header.ssrc, 42);
assert_eq!(decrypted.media_payload, expected_payload);
}
fn fake_packet_header(event: &VoiceOutboundSendEvent) -> RtpHeader {
let VoiceOutboundSendEvent::Packet { bytes } = event else {
panic!("expected fake packet event, got {event:?}");
};
parse_rtp_header(bytes).expect("fake RTP header should parse")
}
fn encrypt_test_rtp_payload(
mode: &str,
key: &[u8],
aad: &[u8],
plaintext: &[u8],
nonce_suffix: [u8; RTP_AEAD_NONCE_SUFFIX_BYTES],
) -> Vec<u8> {
match mode {
AEAD_AES256_GCM_RTPSIZE => {
let cipher = Aes256Gcm::new_from_slice(key).expect("test key is valid");
let mut nonce = [0u8; 12];
nonce[..RTP_AEAD_NONCE_SUFFIX_BYTES].copy_from_slice(&nonce_suffix);
cipher
.encrypt(
AesGcmNonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad,
},
)
.expect("test payload encrypts")
}
AEAD_XCHACHA20_POLY1305_RTPSIZE => {
let cipher = XChaCha20Poly1305::new_from_slice(key).expect("test key is valid");
let mut nonce = [0u8; 24];
nonce[..RTP_AEAD_NONCE_SUFFIX_BYTES].copy_from_slice(&nonce_suffix);
cipher
.encrypt(
XNonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad,
},
)
.expect("test payload encrypts")
}
other => panic!("unsupported test mode: {other}"),
}
}