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.
This commit is contained in:
ReTeamSpeak
2026-05-12 20:08:23 +09:00
parent d34f5dd952
commit 25af2cb295
6 changed files with 769 additions and 60 deletions
+159 -31
View File
@@ -1,65 +1,193 @@
//! 抖动缓冲
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use super::{AudioError, AudioFrame, AudioResult};
/// 抖动缓冲
pub struct JitterBuffer {
buffer: Vec<Option<AudioFrame>>,
head: usize,
tail: usize,
size: usize,
frames: BTreeMap<u16, BufferedFrame>,
next_output_seq: u16,
capacity: usize,
target_delay_ms: u32,
last_output: Option<Instant>,
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<AudioFrame> {
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<f32>) -> 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);
}
}
+135 -20
View File
@@ -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<Self> {
Ok(Self {
sample_rate,
channels,
})
}
pub fn encode(&mut self, _samples: &[f32]) -> AudioResult<Vec<u8>> {
#[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<Vec<u8>> {
#[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<Self> {
Ok(Self {
sample_rate,
channels,
})
}
pub fn decode(&mut self, _data: &[u8], _fec: bool) -> AudioResult<Vec<f32>> {
#[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<Vec<f32>> {
#[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<Vec<f32>> {
self.decode(data, false)
}
pub fn decode_packet_fec(&mut self, data: &[u8]) -> AudioResult<Vec<f32>> {
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<f32> = (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);
}
}
+14
View File
@@ -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<f32>,
@@ -59,12 +61,24 @@ pub struct AudioFrame {
impl AudioFrame {
pub fn new(sample_rate: u32, channels: u16, samples: Vec<f32>) -> 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()
}
+150 -9
View File
@@ -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<cpal::Stream>,
buffer: Arc<Mutex<Vec<f32>>>,
}
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<Vec<String>> {
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)
}
}
+2
View File
@@ -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::*;
+309
View File
@@ -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<u8>,
}
#[derive(Debug, Clone)]
pub struct WhisperPacket {
pub packet_id: u16,
pub codec: CodecType,
pub channel_targets: Vec<u16>,
pub client_targets: Vec<u16>,
pub audio_data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum VoiceData {
Normal(VoicePacket),
Whisper(WhisperPacket),
}
impl VoicePacket {
pub fn parse(data: &[u8]) -> Result<Self, ProtocolError> {
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<u8> {
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<Self, ProtocolError> {
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<u8> {
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<VoiceData, ProtocolError> {
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<u16>,
client_targets: Vec<u16>,
) -> 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());
}
}