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

- 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:
ReTeamSpeak
2026-05-12 14:41:54 +09:00
commit ea08823c97
79 changed files with 11386 additions and 0 deletions
+24
View File
@@ -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"]
+65
View File
@@ -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;
}
}
+33
View File
@@ -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())
}
}
+41
View File
@@ -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()))
}
}
+80
View File
@@ -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>,
}
+33
View File
@@ -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())
}
}
+41
View File
@@ -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;
}
}