Initial commit: ReTeamSpeak cross-platform TeamSpeak client
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Failing after 2s
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Failing after 2s
- tscore: Protocol implementation (packets, crypto, connection handshake) - tsaudio: Audio engine (capture, playback, codec, VAD, jitter buffer) - tsdb: SQLite database (identities, bookmarks, messages, settings) - shared: Core types and events - tauri-app: Tauri v2 desktop application with React frontend - docs: SRS, SAD, SDD documentation - CI/CD: GitHub Actions workflow - 32 unit tests passing
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "tsaudio"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "TeamSpeak 音频引擎"
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
opus = { workspace = true, optional = true }
|
||||
cpal = { workspace = true, optional = true }
|
||||
rubato = { version = "0.14", optional = true }
|
||||
crossbeam-channel = "0.5"
|
||||
|
||||
shared = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
full = ["cpal", "opus", "rubato"]
|
||||
@@ -0,0 +1,65 @@
|
||||
//! 抖动缓冲
|
||||
|
||||
use super::{AudioFrame, AudioResult, AudioError};
|
||||
|
||||
/// 抖动缓冲
|
||||
pub struct JitterBuffer {
|
||||
buffer: Vec<Option<AudioFrame>>,
|
||||
head: usize,
|
||||
tail: usize,
|
||||
size: usize,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl JitterBuffer {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
buffer: vec![None; capacity],
|
||||
head: 0,
|
||||
tail: 0,
|
||||
size: 0,
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, frame: AudioFrame) -> AudioResult<()> {
|
||||
if self.size >= self.capacity {
|
||||
return Err(AudioError::Buffer("缓冲区已满".to_string()));
|
||||
}
|
||||
|
||||
self.buffer[self.tail] = Some(frame);
|
||||
self.tail = (self.tail + 1) % self.capacity;
|
||||
self.size += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<AudioFrame> {
|
||||
if self.size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let frame = self.buffer[self.head].take();
|
||||
self.head = (self.head + 1) % self.capacity;
|
||||
self.size -= 1;
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.size == 0
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.size >= self.capacity
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.buffer.iter_mut().for_each(|f| *f = None);
|
||||
self.head = 0;
|
||||
self.tail = 0;
|
||||
self.size = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! 音频采集
|
||||
|
||||
use super::{AudioConfig, AudioFrame, AudioResult, AudioError};
|
||||
|
||||
pub struct AudioCapture {
|
||||
config: AudioConfig,
|
||||
}
|
||||
|
||||
impl AudioCapture {
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> AudioResult<()> {
|
||||
#[cfg(feature = "cpal")]
|
||||
{
|
||||
// TODO: cpal 实现
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&mut self) -> AudioResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn capture(&mut self) -> AudioResult<AudioFrame> {
|
||||
Err(AudioError::Device("未实现".to_string()))
|
||||
}
|
||||
|
||||
pub fn list_devices() -> AudioResult<Vec<String>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Opus 编解码器
|
||||
|
||||
use super::{AudioResult, AudioError};
|
||||
|
||||
pub struct OpusEncoder {
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
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 实现
|
||||
}
|
||||
Err(AudioError::Codec("Opus 未启用".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OpusDecoder {
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
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 实现
|
||||
}
|
||||
Err(AudioError::Codec("Opus 未启用".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//! TeamSpeak 音频引擎
|
||||
|
||||
pub mod capture;
|
||||
pub mod playback;
|
||||
pub mod codec;
|
||||
pub mod vad;
|
||||
pub mod buffer;
|
||||
|
||||
pub use capture::*;
|
||||
pub use playback::*;
|
||||
pub use codec::*;
|
||||
pub use vad::*;
|
||||
pub use buffer::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AudioError {
|
||||
#[error("设备错误: {0}")]
|
||||
Device(String),
|
||||
#[error("编解码器错误: {0}")]
|
||||
Codec(String),
|
||||
#[error("缓冲区错误: {0}")]
|
||||
Buffer(String),
|
||||
#[error("配置错误: {0}")]
|
||||
Config(String),
|
||||
#[error("IO 错误: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type AudioResult<T> = Result<T, AudioError>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
pub bits_per_sample: u16,
|
||||
pub frame_size: usize,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sample_rate: 48000,
|
||||
channels: 1,
|
||||
bits_per_sample: 16,
|
||||
frame_size: 960,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFrame {
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
pub samples: Vec<f32>,
|
||||
}
|
||||
|
||||
impl AudioFrame {
|
||||
pub fn new(sample_rate: u32, channels: u16, samples: Vec<f32>) -> Self {
|
||||
Self { sample_rate, channels, samples }
|
||||
}
|
||||
|
||||
pub fn frame_size(&self) -> usize {
|
||||
self.samples.len()
|
||||
}
|
||||
|
||||
pub fn duration_ms(&self) -> f64 {
|
||||
(self.frame_size() as f64 / self.channels as f64) / (self.sample_rate as f64) * 1000.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioDeviceInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_default: bool,
|
||||
pub sample_rates: Vec<u32>,
|
||||
pub channels: Vec<u16>,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! 音频播放
|
||||
|
||||
use super::{AudioConfig, AudioFrame, AudioResult};
|
||||
|
||||
pub struct AudioPlayback {
|
||||
config: AudioConfig,
|
||||
}
|
||||
|
||||
impl AudioPlayback {
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> AudioResult<()> {
|
||||
#[cfg(feature = "cpal")]
|
||||
{
|
||||
// TODO: cpal 实现
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&mut self) -> AudioResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn play(&mut self, _frame: AudioFrame) -> AudioResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_devices() -> AudioResult<Vec<String>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! 语音活动检测 (VAD)
|
||||
|
||||
/// VAD 状态
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VadState {
|
||||
Silent,
|
||||
Speaking,
|
||||
}
|
||||
|
||||
/// 语音活动检测器
|
||||
pub struct VadDetector {
|
||||
threshold: f32,
|
||||
state: VadState,
|
||||
}
|
||||
|
||||
impl VadDetector {
|
||||
pub fn new(threshold: f32) -> Self {
|
||||
Self {
|
||||
threshold,
|
||||
state: VadState::Silent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect(&mut self, samples: &[f32]) -> VadState {
|
||||
let energy: f32 = samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32;
|
||||
if energy > self.threshold {
|
||||
self.state = VadState::Speaking;
|
||||
} else {
|
||||
self.state = VadState::Silent;
|
||||
}
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn state(&self) -> VadState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn set_threshold(&mut self, threshold: f32) {
|
||||
self.threshold = threshold;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user