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)
}
}