use super::dave::VoiceDaveOutboundPayload;
use super::rtp::build_voice_rtp_packet;
use super::runtime::stop_voice_connection_task;
use super::*;
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: Id::new(1),
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.guild_id, 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_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.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 {
enabled: 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 {
enabled: false,
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 {
enabled: false,
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 {
enabled: false,
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]
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_reconnects_after_matching_connection_end() {
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:?}");
};
assert_eq!(
state.apply(session.connection_ended_event()),
Some(VoiceRuntimeAction::Connect(session))
);
}
#[test]
fn voice_gateway_session_debug_redacts_secrets() {
let session = VoiceGatewaySession {
guild_id: 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_dave_state_tracks_speaking_ssrc_mapping() {
let session = VoiceGatewaySession {
guild_id: 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_expires_remote_speakers_and_tracks_local_edges() {
let mut tracker = VoiceSpeakingTracker::default();
let remote_user = Id::new(30);
let local_user = Id::new(20);
let now = Instant::now();
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(local_user), vec![local_user]);
}
#[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(VoiceFakeSendBlockReason::DaveOutboundMissingSession)
);
state.reinit(1).expect("DAVE session should initialize");
assert_eq!(
state.prepare_outbound_opus(b"opus-frame"),
VoiceDaveOutboundPayload::Blocked(VoiceFakeSendBlockReason::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 {
payload_type: DISCORD_VOICE_PAYLOAD_TYPE,
sequence: 7,
timestamp: 8,
ssrc: 9,
authenticated_header_len: 12,
encrypted_extension_body_len: 0,
payload_offset: 12,
};
assert_eq!(
voice_playback_frame(&VoiceMediaPayload::Plain(b"opus".to_vec()), &header),
Some(VoicePlaybackFrame {
ssrc: 9,
user_id: None,
sequence: 7,
timestamp: 8,
opus: b"opus".to_vec(),
})
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveDecrypted {
user_id: 42,
opus: b"dave-opus".to_vec(),
},
&header,
),
Some(VoicePlaybackFrame {
ssrc: 9,
user_id: Some(42),
sequence: 7,
timestamp: 8,
opus: b"dave-opus".to_vec(),
})
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveUnexpectedPlain { payload_len: 4 },
&header,
),
None
);
assert_eq!(
voice_playback_frame(
&VoiceMediaPayload::DaveMissingUser { payload_len: 4 },
&header,
),
None
);
}
fn test_playback_frame(ssrc: u32, user_id: Option<u64>, sequence: u16) -> VoicePlaybackFrame {
VoicePlaybackFrame {
ssrc,
user_id,
sequence,
timestamp: u32::from(sequence) * DISCORD_OPUS_TIMESTAMP_INCREMENT,
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),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
10
)))
);
assert_eq!(
buffer.next_frame(now),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
11
)))
);
assert_eq!(
buffer.next_frame(now),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
12
)))
);
}
#[test]
fn voice_playout_buffer_emits_packet_loss_for_missing_sequence() {
let now = Instant::now();
let mut buffer = VoicePlaybackPlayoutBuffer::default();
assert!(buffer.push(test_playback_frame(9, Some(42), 10), now));
assert!(buffer.push(test_playback_frame(9, Some(42), 12), now));
assert!(buffer.push(test_playback_frame(9, Some(42), 13), now));
assert_eq!(
buffer.next_frame(now),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
10
)))
);
assert_eq!(
buffer.next_frame(now),
Some(VoicePlayoutFrame::PacketLoss {
ssrc: 9,
user_id: Some(42),
sequence: 11,
})
);
assert_eq!(
buffer.next_frame(now),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
12
)))
);
}
#[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),
Some(VoicePlayoutFrame::Audio(test_playback_frame(
9,
Some(42),
7
)))
);
assert_eq!(
buffer.next_frame(now),
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_decoded_samples_clamp_mixed_peaks() {
let mixed = mix_voice_decoded_samples(&[vec![1.0, 1.0], vec![1.0, 1.0]])
.expect("same-tick decoded frames should mix");
assert_eq!(mixed, vec![1.0, 1.0]);
}
#[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, 1.0, -1.0, voice_sample_to_u8);
assert_eq!(
u8_output,
[255, 0, voice_sample_to_u8(0.0), voice_sample_to_u8(0.0)]
);
let mut u16_output = [0u16; 4];
write_voice_output_frame(&mut u16_output, 1.0, -1.0, voice_sample_to_u16);
assert_eq!(
u16_output,
[
u16::MAX,
0,
voice_sample_to_u16(0.0),
voice_sample_to_u16(0.0),
]
);
let mut i16_output = [1i16; 4];
write_voice_output_frame(&mut i16_output, 1.0, -1.0, voice_sample_to_i16);
assert_eq!(i16_output, [i16::MAX, i16::MIN + 1, 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 mut buffer = VoiceAudioBuffer::new(rx, 24_000);
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 mut buffer = VoiceAudioBuffer::new(rx, DISCORD_VOICE_SAMPLE_RATE);
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);
}
#[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];
apply_voice_volume_to_i16_frame(&mut frame, VoiceVolumePercent::new(50));
assert_eq!(frame, vec![500, -500, 16384, -16384]);
}
#[test]
fn voice_microphone_protection_soft_limits_extreme_samples() {
let mut frame = vec![1000, -1000, i16::MAX, i16::MIN];
let limited = protect_voice_microphone_frame(&mut frame);
assert_eq!(frame[0], 1000);
assert_eq!(frame[1], -1000);
assert!(frame[2] < i16::MAX);
assert!(frame[3] > i16::MIN);
assert_eq!(frame[2], -frame[3]);
assert_eq!(limited, 2);
}
#[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(&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_sparse_clipped_unclassified_frame() {
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 raw_decision = voice_microphone_overload_decision(&sparse_clip);
assert!(raw_decision.is_none());
assert!(voice_microphone_clipped_frame_needs_blank(
&sparse_clip,
raw_decision,
));
}
#[test]
fn voice_microphone_blanks_clipped_non_handling_frame() {
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 raw_decision = voice_microphone_overload_decision(&attenuated);
assert_eq!(
raw_decision.map(|decision| decision.kind),
Some(VoiceMicrophoneOverloadKind::Attenuated)
);
assert!(voice_microphone_clipped_frame_needs_blank(
&attenuated,
raw_decision,
));
}
#[test]
fn voice_microphone_keeps_handling_noise_on_gate_path() {
let mut handling_noise = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
handling_noise[0] = i16::MAX;
handling_noise[1] = i16::MIN + 1;
let raw_decision = voice_microphone_overload_decision(&handling_noise);
assert_eq!(
raw_decision.map(|decision| decision.kind),
Some(VoiceMicrophoneOverloadKind::HandlingNoise)
);
assert!(!voice_microphone_clipped_frame_needs_blank(
&handling_noise,
raw_decision,
));
}
#[test]
fn voice_microphone_does_not_blank_unclipped_unclassified_frame() {
let normal = vec![1500i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
let raw_decision = voice_microphone_overload_decision(&normal);
assert!(raw_decision.is_none());
assert!(!voice_microphone_clipped_frame_needs_blank(
&normal,
raw_decision,
));
}
#[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_impulse_to_handling_noise() {
let mut impulse = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
impulse[0] = i16::MAX;
impulse[1] = -3_233;
let decision = voice_microphone_overload_decision(&impulse)
.expect("clipped impulse should be gain-reduced");
assert_eq!(decision.kind, VoiceMicrophoneOverloadKind::HandlingNoise);
assert_eq!(decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
let max_adjacent_delta = voice_microphone_max_adjacent_delta(&impulse);
assert!(max_adjacent_delta >= VOICE_MIC_OVERLOAD_IMPULSE_DELTA);
assert!(max_adjacent_delta < VOICE_MIC_HANDLING_NOISE_DELTA);
assert!(
voice_microphone_clipped_sample_count(&impulse) < VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES
);
}
#[test]
fn voice_microphone_overload_promotes_sparse_clipped_step_to_handling_noise() {
let mut impulse = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
impulse[0] = i16::MAX;
impulse[1] = i16::MAX;
let decision =
voice_microphone_overload_decision(&impulse).expect("clipped step should be gain-reduced");
assert_eq!(decision.kind, VoiceMicrophoneOverloadKind::HandlingNoise);
assert_eq!(decision.gain, VOICE_MIC_HANDLING_NOISE_GAIN);
let max_adjacent_delta = voice_microphone_max_adjacent_delta(&impulse);
assert!(max_adjacent_delta >= VOICE_MIC_OVERLOAD_CLIPPED_STEP_DELTA);
assert!(max_adjacent_delta < VOICE_MIC_OVERLOAD_IMPULSE_DELTA);
assert!(
voice_microphone_clipped_sample_count(&impulse) < VOICE_MIC_OVERLOAD_MIN_CLIPPED_SAMPLES
);
}
#[test]
fn voice_microphone_overload_gain_keeps_severe_same_polarity_clip_audible() {
let mut clipped = vec![0i16; DISCORD_OPUS_20MS_STEREO_SAMPLES];
for sample in clipped
.iter_mut()
.take(VOICE_MIC_OVERLOAD_SEVERE_CLIPPED_SAMPLES)
{
*sample = i16::MAX;
}
assert_eq!(
voice_microphone_overload_gain(&clipped),
Some(VOICE_MIC_OVERLOAD_TRANSIENT_GAIN)
);
}
#[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 {
guild_id: 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))
);
}
#[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 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 {
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() {
let key = [7u8; 32];
let nonce_suffix = [1, 2, 3, 4];
let mut header = vec![0x90, 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()].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");
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
)
);
}
#[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.len(), DISCORD_OPUS_20MS_STEREO_SAMPLES);
assert_eq!(frame[0], 0);
assert_eq!(frame[1], 0);
assert!(frame[frame.len() - 2] > 870);
assert!(frame[frame.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").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")]
#[tokio::test]
async fn microphone_pcm_recv_returns_frames_in_fifo_order() {
let (tx, mut rx) = tokio::sync::mpsc::channel(VOICE_MIC_PCM_FRAME_QUEUE);
tx.try_send(vec![1]).expect("first frame should queue");
tx.try_send(vec![2]).expect("second frame should queue");
tx.try_send(vec![3]).expect("third frame should queue");
assert_eq!(rx.recv().await, Some(vec![1]));
assert_eq!(rx.recv().await, Some(vec![2]));
assert_eq!(rx.recv().await, Some(vec![3]));
}
#[cfg(feature = "voice-playback")]
#[test]
fn voice_transmit_pacer_delays_queued_frames_to_20ms_slots() {
let mut pacer = VoiceTransmitPacer::default();
let start = Instant::now();
assert_eq!(pacer.delay_before_send(start), None);
assert_eq!(
pacer.delay_before_send(start),
Some(VOICE_PLAYBACK_FRAME_DURATION)
);
assert_eq!(
pacer.delay_before_send(start + VOICE_PLAYBACK_FRAME_DURATION),
Some(VOICE_PLAYBACK_FRAME_DURATION)
);
assert_eq!(
pacer.delay_before_send(start + VOICE_PLAYBACK_FRAME_DURATION * 4),
None
);
}
#[cfg(feature = "voice-playback")]
#[tokio::test]
async fn voice_transmit_pacer_delay_stops_when_gate_disables() {
let (gate_tx, mut gate_rx) = watch::channel(VoiceCaptureGate {
enabled: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
});
let wait = tokio::spawn(async move {
wait_voice_transmit_pacer_delay(Duration::from_secs(60), &mut gate_rx).await
});
gate_tx
.send(VoiceCaptureGate {
enabled: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
})
.expect("gate receiver should still be alive");
let outcome = timeout(Duration::from_millis(100), wait)
.await
.expect("gate disable should interrupt pacer delay")
.expect("pacer wait task should finish");
assert_eq!(outcome, VoiceTransmitPacerDelayOutcome::GateChanged);
}
#[cfg(feature = "voice-playback")]
#[tokio::test]
async fn voice_transmit_pacer_delay_drops_frame_on_disable_then_reenable() {
let (gate_tx, mut gate_rx) = watch::channel(VoiceCaptureGate {
enabled: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
});
gate_tx
.send(VoiceCaptureGate {
enabled: false,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
})
.expect("gate receiver should still be alive");
gate_tx
.send(VoiceCaptureGate {
enabled: true,
microphone_sensitivity: MicrophoneSensitivityDb::default(),
microphone_volume: VoiceVolumePercent::default(),
})
.expect("gate receiver should still be alive");
let outcome = timeout(
Duration::from_millis(100),
wait_voice_transmit_pacer_delay(Duration::from_secs(60), &mut gate_rx),
)
.await
.expect("any gate change should interrupt pacer delay");
assert_eq!(outcome, VoiceTransmitPacerDelayOutcome::GateChanged);
assert!(gate_rx.borrow().enabled);
}
#[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");
}
#[tokio::test]
async fn voice_runtime_stops_connection_task_by_closing_gate_channels() {
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!(capture_gate_rx.recv().await.is_none());
assert!(playback_gate_rx.recv().await.is_none());
let _ = done_tx.send(());
}));
let mut capture_gate_tx = Some(capture_gate_tx);
let mut playback_gate_tx = Some(playback_gate_tx);
stop_voice_connection_task(
&mut connection_task,
&mut capture_gate_tx,
&mut playback_gate_tx,
"test voice connection stop",
)
.await;
done_rx
.await
.expect("connection task should finish after gate channels close");
assert!(connection_task.is_none());
assert!(capture_gate_tx.is_none());
assert!(playback_gate_tx.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);
tx.try_send(vec![10]).expect("first frame should queue");
tx.try_send(vec![20]).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_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"),
VoiceFakeSendOutcome::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"),
VoiceFakeSendOutcome::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"),
VoiceFakeSendOutcome::Blocked(VoiceFakeSendBlockReason::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"),
VoiceFakeSendOutcome::Sent
);
assert_fake_packet(
AEAD_AES256_GCM_RTPSIZE,
&state.events()[1],
7,
960,
42,
b"opus-frame",
30u32.to_be_bytes(),
);
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"),
VoiceFakeSendOutcome::Blocked(VoiceFakeSendBlockReason::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"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(state.events().len(), 2);
assert_eq!(
state.events()[0],
VoiceFakeOutboundEvent::Speaking {
speaking: true,
ssrc: 42,
}
);
assert_fake_packet(
mode,
&state.events()[1],
7,
960,
42,
b"opus-frame",
[1, 2, 3, 4],
);
assert_eq!(state.rtp.sequence, 8);
assert_eq!(state.rtp.timestamp, 1920);
assert_eq!(state.nonce_suffix, 0x01020305);
assert_eq!(
state
.send_opus_frame(b"next-frame")
.expect("second frame should send"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(state.events().len(), 3);
assert_fake_packet(
mode,
&state.events()[2],
8,
1920,
42,
b"next-frame",
[1, 2, 3, 5],
);
assert_eq!(state.rtp.sequence, 9);
assert_eq!(state.rtp.timestamp, 2880);
assert_eq!(state.nonce_suffix, 0x01020306);
}
}
#[test]
fn fake_outbound_stop_sends_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"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(
state.stop_speaking().expect("stop should send silence"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(state.events().len(), DISCORD_TRAILING_SILENCE_FRAMES + 3);
for index in 0..DISCORD_TRAILING_SILENCE_FRAMES {
assert_fake_packet(
AEAD_AES256_GCM_RTPSIZE,
&state.events()[index + 2],
8 + index as u16,
1920 + index as u32 * DISCORD_OPUS_TIMESTAMP_INCREMENT,
42,
&DISCORD_OPUS_SILENCE_FRAME,
(21 + index as u32).to_be_bytes(),
);
}
assert_eq!(
state.events()[DISCORD_TRAILING_SILENCE_FRAMES + 2],
VoiceFakeOutboundEvent::Speaking {
speaking: false,
ssrc: 42,
}
);
assert_eq!(state.rtp.sequence, 13);
assert_eq!(state.rtp.timestamp, 6720);
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"),
VoiceFakeSendOutcome::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"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(state.events().len(), event_count + 1);
assert_eq!(
state.events()[event_count],
VoiceFakeOutboundEvent::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"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(state.events().len(), event_count + 2);
assert_eq!(
state.events()[event_count + 1],
VoiceFakeOutboundEvent::Speaking {
speaking: false,
ssrc: 42,
}
);
assert_eq!(state.rtp, rtp);
assert_eq!(state.nonce_suffix, nonce_suffix);
}
#[test]
fn fake_outbound_stop_uses_dave_policy_for_silence_frames() {
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
.stop_speaking_with_dave(&mut dave)
.expect("DAVE not-ready silence should still send speaking off"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(
state.events(),
&[VoiceFakeOutboundEvent::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"),
VoiceFakeSendOutcome::Sent
);
assert_eq!(
stopping.events(),
&[VoiceFakeOutboundEvent::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) -> VoiceFakeOutboundSendState {
VoiceFakeOutboundSendState::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 {
guild_id: 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: &VoiceFakeOutboundEvent,
sequence: u16,
timestamp: u32,
ssrc: u32,
expected_payload: &[u8],
nonce_suffix: [u8; RTP_AEAD_NONCE_SUFFIX_BYTES],
) {
let VoiceFakeOutboundEvent::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.sequence, sequence);
assert_eq!(header.timestamp, timestamp);
assert_eq!(header.ssrc, ssrc);
assert_eq!(decrypted.media_payload, expected_payload);
}
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}"),
}
}