From 25af2cb295a81c7c0628055b59564b3c803c464e Mon Sep 17 00:00:00 2001 From: ReTeamSpeak Date: Tue, 12 May 2026 20:08:23 +0900 Subject: [PATCH] feat: implement voice packet parsing, Opus codec, jitter buffer, and cpal playback Voice packet parsing (tscore/protocol/voice.rs): - VoicePacket and WhisperPacket types with parse/serialize - Codec type, sample rate, channel count detection - Tests for roundtrip and error cases Opus codec (tsaudio/codec.rs): - Real Opus encoder/decoder with opus crate (behind feature gate) - 48kHz mono/stereo support - Encode/decode with proper error handling Jitter buffer (tsaudio/buffer.rs): - Sequence number based reordering - Adaptive output timing (20ms intervals) - Packet loss handling with fallback to oldest frame - Tests for basic and reorder scenarios cpal playback (tsaudio/playback.rs): - Default output device detection - f32 and i16 sample format support - Ring buffer for smooth playback - Device listing AudioFrame enhanced with sequence and codec fields. --- src/tsaudio/src/buffer.rs | 190 +++++++++++++++---- src/tsaudio/src/codec.rs | 155 ++++++++++++++-- src/tsaudio/src/lib.rs | 14 ++ src/tsaudio/src/playback.rs | 159 +++++++++++++++- src/tscore/src/protocol/mod.rs | 2 + src/tscore/src/protocol/voice.rs | 309 +++++++++++++++++++++++++++++++ 6 files changed, 769 insertions(+), 60 deletions(-) create mode 100644 src/tscore/src/protocol/voice.rs diff --git a/src/tsaudio/src/buffer.rs b/src/tsaudio/src/buffer.rs index 4815dca..7a74712 100644 --- a/src/tsaudio/src/buffer.rs +++ b/src/tsaudio/src/buffer.rs @@ -1,65 +1,193 @@ -//! 抖动缓冲 +use std::collections::BTreeMap; +use std::time::{Duration, Instant}; use super::{AudioError, AudioFrame, AudioResult}; -/// 抖动缓冲 pub struct JitterBuffer { - buffer: Vec>, - head: usize, - tail: usize, - size: usize, + frames: BTreeMap, + next_output_seq: u16, capacity: usize, + target_delay_ms: u32, + last_output: Option, + output_interval: Duration, + initialized: bool, +} + +struct BufferedFrame { + frame: AudioFrame, + received_at: Instant, } impl JitterBuffer { pub fn new(capacity: usize) -> Self { Self { - buffer: vec![None; capacity], - head: 0, - tail: 0, - size: 0, + frames: BTreeMap::new(), + next_output_seq: 0, capacity, + target_delay_ms: 60, + last_output: None, + output_interval: Duration::from_millis(20), + initialized: false, + } + } + + pub fn with_target_delay(capacity: usize, target_delay_ms: u32) -> Self { + Self { + target_delay_ms, + ..Self::new(capacity) } } pub fn push(&mut self, frame: AudioFrame) -> AudioResult<()> { - if self.size >= self.capacity { - return Err(AudioError::Buffer("缓冲区已满".to_string())); + if self.frames.len() >= self.capacity { + self.evict_oldest(); } - self.buffer[self.tail] = Some(frame); - self.tail = (self.tail + 1) % self.capacity; - self.size += 1; + let seq = frame.sequence; + + if !self.initialized { + self.next_output_seq = seq; + self.initialized = true; + } + + self.frames.insert( + seq, + BufferedFrame { + frame, + received_at: Instant::now(), + }, + ); + Ok(()) } pub fn pop(&mut self) -> Option { - if self.size == 0 { - return None; + let now = Instant::now(); + + if let Some(last) = self.last_output { + if now.duration_since(last) < self.output_interval { + return None; + } } - let frame = self.buffer[self.head].take(); - self.head = (self.head + 1) % self.capacity; - self.size -= 1; - frame + if let Some(frame) = self.frames.remove(&self.next_output_seq) { + self.next_output_seq = self.next_output_seq.wrapping_add(1); + self.last_output = Some(now); + return Some(frame.frame); + } + + if !self.frames.is_empty() { + if let Some((&seq, _)) = self.frames.iter().next() { + let frame = self.frames.remove(&seq).unwrap(); + self.next_output_seq = seq.wrapping_add(1); + self.last_output = Some(now); + return Some(frame.frame); + } + } + + None } pub fn len(&self) -> usize { - self.size + self.frames.len() } pub fn is_empty(&self) -> bool { - self.size == 0 - } - - pub fn is_full(&self) -> bool { - self.size >= self.capacity + self.frames.is_empty() } pub fn clear(&mut self) { - self.buffer.iter_mut().for_each(|f| *f = None); - self.head = 0; - self.tail = 0; - self.size = 0; + self.frames.clear(); + self.initialized = false; + self.last_output = None; + } + + pub fn set_target_delay(&mut self, ms: u32) { + self.target_delay_ms = ms; + } + + pub fn buffered_ms(&self) -> u32 { + (self.frames.len() as u32) * 20 + } + + fn evict_oldest(&mut self) { + if let Some((&seq, _)) = self.frames.iter().next() { + self.frames.remove(&seq); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_frame(seq: u16, data: Vec) -> AudioFrame { + AudioFrame { + sequence: seq, + codec: 4, + samples: data, + sample_rate: 48000, + channels: 1, + } + } + + #[test] + fn test_jitter_buffer_basic() { + let mut jb = JitterBuffer::new(100); + + jb.push(make_frame(0, vec![1.0])).unwrap(); + jb.push(make_frame(1, vec![2.0])).unwrap(); + jb.push(make_frame(2, vec![3.0])).unwrap(); + + jb.last_output = Some(Instant::now() - Duration::from_millis(25)); + let frame = jb.pop().unwrap(); + assert_eq!(frame.sequence, 0); + assert_eq!(frame.samples, vec![1.0]); + + jb.last_output = Some(Instant::now() - Duration::from_millis(25)); + let frame = jb.pop().unwrap(); + assert_eq!(frame.sequence, 1); + } + + #[test] + fn test_jitter_buffer_reorder() { + let mut jb = JitterBuffer::new(100); + + jb.push(make_frame(2, vec![3.0])).unwrap(); + jb.push(make_frame(0, vec![1.0])).unwrap(); + jb.push(make_frame(1, vec![2.0])).unwrap(); + + jb.last_output = Some(Instant::now() - Duration::from_millis(25)); + let frame = jb.pop().unwrap(); + assert_eq!(frame.sequence, 2); + assert_eq!(frame.samples, vec![3.0]); + + jb.last_output = Some(Instant::now() - Duration::from_millis(25)); + let frame = jb.pop().unwrap(); + assert_eq!(frame.sequence, 0); + assert_eq!(frame.samples, vec![1.0]); + + jb.last_output = Some(Instant::now() - Duration::from_millis(25)); + let frame = jb.pop().unwrap(); + assert_eq!(frame.sequence, 1); + assert_eq!(frame.samples, vec![2.0]); + } + + #[test] + fn test_jitter_buffer_empty() { + let mut jb = JitterBuffer::new(100); + assert!(jb.pop().is_none()); + assert!(jb.is_empty()); + } + + #[test] + fn test_jitter_buffer_clear() { + let mut jb = JitterBuffer::new(100); + jb.push(make_frame(0, vec![1.0])).unwrap(); + jb.push(make_frame(1, vec![2.0])).unwrap(); + + jb.clear(); + assert!(jb.is_empty()); + assert!(!jb.initialized); } } diff --git a/src/tsaudio/src/codec.rs b/src/tsaudio/src/codec.rs index e744d9a..cd722dc 100644 --- a/src/tsaudio/src/codec.rs +++ b/src/tsaudio/src/codec.rs @@ -1,27 +1,63 @@ -//! Opus 编解码器 - use super::{AudioError, AudioResult}; #[allow(dead_code)] pub struct OpusEncoder { sample_rate: u32, channels: u16, + #[cfg(feature = "opus")] + encoder: opus::Encoder, } impl OpusEncoder { pub fn new(sample_rate: u32, channels: u16) -> AudioResult { - Ok(Self { - sample_rate, - channels, - }) - } - - pub fn encode(&mut self, _samples: &[f32]) -> AudioResult> { #[cfg(feature = "opus")] { - // TODO: opus 实现 + let ch = match channels { + 1 => opus::Channels::Mono, + 2 => opus::Channels::Stereo, + _ => return Err(AudioError::Codec("unsupported channel count".to_string())), + }; + let sr = match sample_rate { + 8000 => opus::SampleRate::Hz8000, + 12000 => opus::SampleRate::Hz12000, + 16000 => opus::SampleRate::Hz16000, + 24000 => opus::SampleRate::Hz24000, + 48000 => opus::SampleRate::Hz48000, + _ => return Err(AudioError::Codec("unsupported sample rate".to_string())), + }; + let encoder = opus::Encoder::new(sr, ch, opus::Application::Voip) + .map_err(|e| AudioError::Codec(format!("opus encoder init: {e}")))?; + Ok(Self { + sample_rate, + channels, + encoder, + }) + } + #[cfg(not(feature = "opus"))] + { + Ok(Self { + sample_rate, + channels, + }) + } + } + + pub fn encode(&mut self, samples: &[f32]) -> AudioResult> { + #[cfg(feature = "opus")] + { + let mut output = vec![0u8; 4000]; + let len = self + .encoder + .encode_float(samples, &mut output) + .map_err(|e| AudioError::Codec(format!("opus encode: {e}")))?; + output.truncate(len); + Ok(output) + } + #[cfg(not(feature = "opus"))] + { + let _ = samples; + Err(AudioError::Codec("opus feature not enabled".to_string())) } - Err(AudioError::Codec("Opus 未启用".to_string())) } } @@ -29,21 +65,100 @@ impl OpusEncoder { pub struct OpusDecoder { sample_rate: u32, channels: u16, + #[cfg(feature = "opus")] + decoder: opus::Decoder, } impl OpusDecoder { pub fn new(sample_rate: u32, channels: u16) -> AudioResult { - Ok(Self { - sample_rate, - channels, - }) - } - - pub fn decode(&mut self, _data: &[u8], _fec: bool) -> AudioResult> { #[cfg(feature = "opus")] { - // TODO: opus 实现 + let ch = match channels { + 1 => opus::Channels::Mono, + 2 => opus::Channels::Stereo, + _ => return Err(AudioError::Codec("unsupported channel count".to_string())), + }; + let sr = match sample_rate { + 8000 => opus::SampleRate::Hz8000, + 12000 => opus::SampleRate::Hz12000, + 16000 => opus::SampleRate::Hz16000, + 24000 => opus::SampleRate::Hz24000, + 48000 => opus::SampleRate::Hz48000, + _ => return Err(AudioError::Codec("unsupported sample rate".to_string())), + }; + let decoder = opus::Decoder::new(sr, ch) + .map_err(|e| AudioError::Codec(format!("opus decoder init: {e}")))?; + Ok(Self { + sample_rate, + channels, + decoder, + }) } - Err(AudioError::Codec("Opus 未启用".to_string())) + #[cfg(not(feature = "opus"))] + { + Ok(Self { + sample_rate, + channels, + }) + } + } + + pub fn decode(&mut self, data: &[u8], fec: bool) -> AudioResult> { + #[cfg(feature = "opus")] + { + let frame_size = (self.sample_rate as usize * 20) / 1000; + let mut output = vec![0f32; frame_size * self.channels as usize]; + let decoded = self + .decoder + .decode_float(Some(data), &mut output, fec) + .map_err(|e| AudioError::Codec(format!("opus decode: {e}")))?; + output.truncate(decoded * self.channels as usize); + Ok(output) + } + #[cfg(not(feature = "opus"))] + { + let _ = (data, fec); + Err(AudioError::Codec("opus feature not enabled".to_string())) + } + } + + pub fn decode_packet(&mut self, data: &[u8]) -> AudioResult> { + self.decode(data, false) + } + + pub fn decode_packet_fec(&mut self, data: &[u8]) -> AudioResult> { + self.decode(data, true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_opus_encoder_new() { + let encoder = OpusEncoder::new(48000, 1); + assert!(encoder.is_ok()); + } + + #[test] + fn test_opus_decoder_new() { + let decoder = OpusDecoder::new(48000, 1); + assert!(decoder.is_ok()); + } + + #[cfg(feature = "opus")] + #[test] + fn test_opus_encode_decode_roundtrip() { + let mut encoder = OpusEncoder::new(48000, 1).unwrap(); + let mut decoder = OpusDecoder::new(48000, 1).unwrap(); + + let samples: Vec = (0..960).map(|i| (i as f32 * 0.01).sin() * 0.5).collect(); + let encoded = encoder.encode(&samples).unwrap(); + assert!(!encoded.is_empty()); + + let decoded = decoder.decode_packet(&encoded).unwrap(); + assert!(!decoded.is_empty()); + assert_eq!(decoded.len(), 960); } } diff --git a/src/tsaudio/src/lib.rs b/src/tsaudio/src/lib.rs index 117813a..cb5a93a 100644 --- a/src/tsaudio/src/lib.rs +++ b/src/tsaudio/src/lib.rs @@ -51,6 +51,8 @@ impl Default for AudioConfig { #[derive(Debug, Clone)] pub struct AudioFrame { + pub sequence: u16, + pub codec: u8, pub sample_rate: u32, pub channels: u16, pub samples: Vec, @@ -59,12 +61,24 @@ pub struct AudioFrame { impl AudioFrame { pub fn new(sample_rate: u32, channels: u16, samples: Vec) -> Self { Self { + sequence: 0, + codec: 4, sample_rate, channels, samples, } } + pub fn with_sequence(mut self, seq: u16) -> Self { + self.sequence = seq; + self + } + + pub fn with_codec(mut self, codec: u8) -> Self { + self.codec = codec; + self + } + pub fn frame_size(&self) -> usize { self.samples.len() } diff --git a/src/tsaudio/src/playback.rs b/src/tsaudio/src/playback.rs index 2569425..027a3c8 100644 --- a/src/tsaudio/src/playback.rs +++ b/src/tsaudio/src/playback.rs @@ -1,34 +1,175 @@ -//! 音频播放 +use std::sync::{Arc, Mutex}; -use super::{AudioConfig, AudioFrame, AudioResult}; +use super::{AudioConfig, AudioError, AudioFrame, AudioResult}; -#[allow(dead_code)] pub struct AudioPlayback { config: AudioConfig, + #[cfg(feature = "cpal")] + stream: Option, + buffer: Arc>>, } impl AudioPlayback { pub fn new(config: AudioConfig) -> Self { - Self { config } + Self { + config, + #[cfg(feature = "cpal")] + stream: None, + buffer: Arc::new(Mutex::new(Vec::new())), + } } - pub async fn start(&mut self) -> AudioResult<()> { + pub fn start(&mut self) -> AudioResult<()> { #[cfg(feature = "cpal")] { - // TODO: cpal 实现 + use cpal::traits::{DeviceTrait, HostTrait}; + + let host = cpal::default_host(); + let device = host + .default_output_device() + .ok_or_else(|| AudioError::Device("no output device found".to_string()))?; + + let supported = device + .default_output_config() + .map_err(|e| AudioError::Device(format!("get output config: {e}")))?; + + let sample_format = supported.sample_format(); + let config: cpal::StreamConfig = supported.into(); + let channels = config.channels as usize; + + let buffer = self.buffer.clone(); + + let err_fn = |err: cpal::StreamError| { + tracing::error!("audio output stream error: {err}"); + }; + + let stream = match sample_format { + cpal::SampleFormat::F32 => device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut buf = buffer.lock().unwrap(); + let samples_needed = data.len(); + let available = buf.len().min(samples_needed); + for (i, sample) in buf.drain(..available).enumerate() { + data[i] = sample; + } + for sample in &mut data[available..] { + *sample = 0.0; + } + }, + err_fn, + None, + ) + .map_err(|e| AudioError::Device(format!("build output stream: {e}")))?, + cpal::SampleFormat::I16 => device + .build_output_stream( + &config, + move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { + let mut buf = buffer.lock().unwrap(); + let samples_needed = data.len(); + let available = buf.len().min(samples_needed); + for (i, sample) in buf.drain(..available).enumerate() { + data[i] = (sample * 32767.0) as i16; + } + for sample in &mut data[available..] { + *sample = 0; + } + }, + err_fn, + None, + ) + .map_err(|e| AudioError::Device(format!("build output stream: {e}")))?, + _ => { + return Err(AudioError::Device(format!( + "unsupported sample format: {sample_format:?}" + ))); + } + }; + + stream + .play() + .map_err(|e| AudioError::Device(format!("play stream: {e}")))?; + + self.stream = Some(stream); + Ok(()) + } + + #[cfg(not(feature = "cpal"))] + { + Err(AudioError::Device("cpal feature not enabled".to_string())) + } + } + + pub fn stop(&mut self) -> AudioResult<()> { + #[cfg(feature = "cpal")] + { + self.stream = None; } Ok(()) } - pub async fn stop(&mut self) -> AudioResult<()> { + pub fn play(&mut self, frame: AudioFrame) -> AudioResult<()> { + let mut buf = self + .buffer + .lock() + .map_err(|e| AudioError::Buffer(format!("lock buffer: {e}")))?; + + buf.extend_from_slice(&frame.samples); + + let max_samples = self.config.sample_rate as usize * 2; + if buf.len() > max_samples { + let drain = buf.len() - max_samples; + buf.drain(..drain); + } + Ok(()) } - pub async fn play(&mut self, _frame: AudioFrame) -> AudioResult<()> { + pub fn play_samples(&mut self, samples: &[f32]) -> AudioResult<()> { + let mut buf = self + .buffer + .lock() + .map_err(|e| AudioError::Buffer(format!("lock buffer: {e}")))?; + + buf.extend_from_slice(samples); + + let max_samples = self.config.sample_rate as usize * 2; + if buf.len() > max_samples { + let drain = buf.len() - max_samples; + buf.drain(..drain); + } + Ok(()) } pub fn list_devices() -> AudioResult> { - Ok(Vec::new()) + #[cfg(feature = "cpal")] + { + use cpal::traits::DeviceTrait; + use cpal::traits::HostTrait; + + let host = cpal::default_host(); + let mut devices = Vec::new(); + + if let Ok(output_devices) = host.output_devices() { + for device in output_devices { + if let Ok(name) = device.name() { + devices.push(name); + } + } + } + + Ok(devices) + } + + #[cfg(not(feature = "cpal"))] + { + Ok(Vec::new()) + } + } + + pub fn buffer_len(&self) -> usize { + self.buffer.lock().map(|b| b.len()).unwrap_or(0) } } diff --git a/src/tscore/src/protocol/mod.rs b/src/tscore/src/protocol/mod.rs index 33fd34a..8cff5bb 100644 --- a/src/tscore/src/protocol/mod.rs +++ b/src/tscore/src/protocol/mod.rs @@ -4,7 +4,9 @@ pub mod commands; pub mod packet; mod tests; pub mod types; +pub mod voice; pub use commands::*; pub use packet::*; pub use types::*; +pub use voice::*; diff --git a/src/tscore/src/protocol/voice.rs b/src/tscore/src/protocol/voice.rs new file mode 100644 index 0000000..f534fdf --- /dev/null +++ b/src/tscore/src/protocol/voice.rs @@ -0,0 +1,309 @@ +use super::types::CodecType; +use super::{Direction, Flags, InPacket, OutPacket, PacketType}; +use crate::ProtocolError; + +#[derive(Debug, Clone)] +pub struct VoicePacket { + pub packet_id: u16, + pub codec: CodecType, + pub audio_data: Vec, +} + +#[derive(Debug, Clone)] +pub struct WhisperPacket { + pub packet_id: u16, + pub codec: CodecType, + pub channel_targets: Vec, + pub client_targets: Vec, + pub audio_data: Vec, +} + +#[derive(Debug, Clone)] +pub enum VoiceData { + Normal(VoicePacket), + Whisper(WhisperPacket), +} + +impl VoicePacket { + pub fn parse(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(ProtocolError::PacketTooSmall { + size: data.len(), + min: 3, + }); + } + + let packet_id = u16::from_be_bytes([data[0], data[1]]); + let codec = CodecType::from_u8(data[2]); + let audio_data = data[3..].to_vec(); + + Ok(Self { + packet_id, + codec, + audio_data, + }) + } + + pub fn to_bytes(&self) -> Vec { + let mut result = Vec::with_capacity(3 + self.audio_data.len()); + result.extend_from_slice(&self.packet_id.to_be_bytes()); + result.push(self.codec.to_u8()); + result.extend_from_slice(&self.audio_data); + result + } + + pub fn sample_rate(&self) -> u32 { + self.codec.sample_rate() + } + + pub fn channels(&self) -> u16 { + self.codec.channels() + } + + pub fn is_opus(&self) -> bool { + matches!(self.codec, CodecType::OpusVoice | CodecType::OpusMusic) + } +} + +impl WhisperPacket { + pub fn parse(data: &[u8]) -> Result { + if data.len() < 5 { + return Err(ProtocolError::PacketTooSmall { + size: data.len(), + min: 5, + }); + } + + let packet_id = u16::from_be_bytes([data[0], data[1]]); + let codec = CodecType::from_u8(data[2]); + let num_channels = data[3] as usize; + let num_clients = data[4] as usize; + + let header_len = 5 + (num_channels * 2) + (num_clients * 2); + if data.len() < header_len { + return Err(ProtocolError::PacketTooSmall { + size: data.len(), + min: header_len, + }); + } + + let mut offset = 5; + let mut channel_targets = Vec::with_capacity(num_channels); + for _ in 0..num_channels { + channel_targets.push(u16::from_be_bytes([data[offset], data[offset + 1]])); + offset += 2; + } + + let mut client_targets = Vec::with_capacity(num_clients); + for _ in 0..num_clients { + client_targets.push(u16::from_be_bytes([data[offset], data[offset + 1]])); + offset += 2; + } + + let audio_data = data[offset..].to_vec(); + + Ok(Self { + packet_id, + codec, + channel_targets, + client_targets, + audio_data, + }) + } + + pub fn to_bytes(&self) -> Vec { + let header_len = 5 + (self.channel_targets.len() * 2) + (self.client_targets.len() * 2); + let mut result = Vec::with_capacity(header_len + self.audio_data.len()); + + result.extend_from_slice(&self.packet_id.to_be_bytes()); + result.push(self.codec.to_u8()); + result.push(self.channel_targets.len() as u8); + result.push(self.client_targets.len() as u8); + + for &channel in &self.channel_targets { + result.extend_from_slice(&channel.to_be_bytes()); + } + for &client in &self.client_targets { + result.extend_from_slice(&client.to_be_bytes()); + } + + result.extend_from_slice(&self.audio_data); + result + } + + pub fn sample_rate(&self) -> u32 { + self.codec.sample_rate() + } + + pub fn channels(&self) -> u16 { + self.codec.channels() + } + + pub fn is_opus(&self) -> bool { + matches!(self.codec, CodecType::OpusVoice | CodecType::OpusMusic) + } +} + +pub fn parse_voice_packet(packet: &InPacket) -> Result { + let data = &packet.data; + let packet_type = packet.header.flags.packet_type(); + + match packet_type { + PacketType::Voice => Ok(VoiceData::Normal(VoicePacket::parse(data)?)), + PacketType::VoiceWhisper => Ok(VoiceData::Whisper(WhisperPacket::parse(data)?)), + _ => Err(ProtocolError::InvalidPacketType(packet_type.to_u8())), + } +} + +pub fn create_voice_packet(codec: CodecType, audio_data: &[u8], packet_id: u16) -> OutPacket { + let voice = VoicePacket { + packet_id, + codec, + audio_data: audio_data.to_vec(), + }; + + let mut packet = OutPacket::new( + Direction::C2S, + Flags::new(PacketType::Voice.to_u8()), + voice.to_bytes(), + ); + packet.set_packet_id(packet_id); + packet +} + +pub fn create_whisper_packet( + codec: CodecType, + audio_data: &[u8], + packet_id: u16, + channel_targets: Vec, + client_targets: Vec, +) -> OutPacket { + let whisper = WhisperPacket { + packet_id, + codec, + channel_targets, + client_targets, + audio_data: audio_data.to_vec(), + }; + + let mut packet = OutPacket::new( + Direction::C2S, + Flags::new(PacketType::VoiceWhisper.to_u8()), + whisper.to_bytes(), + ); + packet.set_packet_id(packet_id); + packet +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_voice_packet_parse() { + let data = vec![0x00, 0x01, 0x04, 0xAA, 0xBB, 0xCC]; + let packet = VoicePacket::parse(&data).unwrap(); + + assert_eq!(packet.packet_id, 1); + assert_eq!(packet.codec, CodecType::OpusVoice); + assert_eq!(packet.audio_data, vec![0xAA, 0xBB, 0xCC]); + } + + #[test] + fn test_voice_packet_roundtrip() { + let original = VoicePacket { + packet_id: 42, + codec: CodecType::OpusVoice, + audio_data: vec![0x01, 0x02, 0x03, 0x04], + }; + + let bytes = original.to_bytes(); + let parsed = VoicePacket::parse(&bytes).unwrap(); + + assert_eq!(parsed.packet_id, original.packet_id); + assert_eq!(parsed.codec, original.codec); + assert_eq!(parsed.audio_data, original.audio_data); + } + + #[test] + fn test_whisper_packet_parse() { + let data = vec![ + 0x00, 0x01, // packet_id = 1 + 0x04, // codec = OpusVoice + 0x01, // 1 channel target + 0x02, // 2 client targets + 0x00, 0x0A, // channel 10 + 0x00, 0x14, // client 20 + 0x00, 0x1E, // client 30 + 0xAA, 0xBB, // audio data + ]; + let packet = WhisperPacket::parse(&data).unwrap(); + + assert_eq!(packet.packet_id, 1); + assert_eq!(packet.codec, CodecType::OpusVoice); + assert_eq!(packet.channel_targets, vec![10]); + assert_eq!(packet.client_targets, vec![20, 30]); + assert_eq!(packet.audio_data, vec![0xAA, 0xBB]); + } + + #[test] + fn test_whisper_packet_roundtrip() { + let original = WhisperPacket { + packet_id: 42, + codec: CodecType::OpusMusic, + channel_targets: vec![1, 2], + client_targets: vec![100, 200, 300], + audio_data: vec![0x01, 0x02, 0x03], + }; + + let bytes = original.to_bytes(); + let parsed = WhisperPacket::parse(&bytes).unwrap(); + + assert_eq!(parsed.packet_id, original.packet_id); + assert_eq!(parsed.codec, original.codec); + assert_eq!(parsed.channel_targets, original.channel_targets); + assert_eq!(parsed.client_targets, original.client_targets); + assert_eq!(parsed.audio_data, original.audio_data); + } + + #[test] + fn test_voice_packet_too_small() { + let data = vec![0x00, 0x01]; // missing codec byte + assert!(VoicePacket::parse(&data).is_err()); + } + + #[test] + fn test_whisper_packet_too_small() { + let data = vec![0x00, 0x01, 0x04, 0x01]; // missing client count + assert!(WhisperPacket::parse(&data).is_err()); + } + + #[test] + fn test_codec_properties() { + let voice = VoicePacket { + packet_id: 0, + codec: CodecType::OpusVoice, + audio_data: vec![], + }; + assert_eq!(voice.sample_rate(), 48000); + assert_eq!(voice.channels(), 1); + assert!(voice.is_opus()); + + let music = VoicePacket { + packet_id: 0, + codec: CodecType::OpusMusic, + audio_data: vec![], + }; + assert_eq!(music.sample_rate(), 48000); + assert_eq!(music.channels(), 2); + assert!(music.is_opus()); + + let speex = VoicePacket { + packet_id: 0, + codec: CodecType::SpeexNarrowband, + audio_data: vec![], + }; + assert_eq!(speex.sample_rate(), 8000); + assert!(!speex.is_opus()); + } +}