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:
+159
-31
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user