feat(stereo): 新增不同型号的小爱音箱组立体声应用
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
const FIFO_PATH: &str = "/tmp/stereo_out.fifo";
|
||||
const REAL_ASOUND_CONF: &str = "/etc/asound.conf";
|
||||
const TEMP_ASOUND_CONF: &str = "/tmp/asound.stereo.conf";
|
||||
|
||||
/// ALSA 音频重定向器,用于拦截系统音频输出到 FIFO 管道
|
||||
pub struct AlsaRedirector;
|
||||
|
||||
impl AlsaRedirector {
|
||||
pub fn new() -> Result<Self> {
|
||||
Self::cleanup(); // 确保环境干净
|
||||
|
||||
let original_conf = fs::read_to_string(REAL_ASOUND_CONF).unwrap_or_default();
|
||||
|
||||
if !original_conf.contains("pcm.original_default") {
|
||||
// 重命名原有的 default 逻辑,插入拦截器
|
||||
let mut new_conf = original_conf.replace("pcm.!default", "pcm.original_default");
|
||||
new_conf.push_str(&format!(
|
||||
"\npcm.!default {{ type plug slave {{ pcm \"stereo_interceptor\" format S16_LE rate 48000 channels 2 }} }}\n\
|
||||
pcm.stereo_interceptor {{ type file slave.pcm \"null\" file \"{}\" format \"raw\" }}\n",
|
||||
FIFO_PATH
|
||||
));
|
||||
|
||||
fs::write(TEMP_ASOUND_CONF, new_conf)?;
|
||||
|
||||
// 挂载覆盖 /etc/asound.conf
|
||||
let status = Command::new("mount")
|
||||
.arg("--bind")
|
||||
.arg(TEMP_ASOUND_CONF)
|
||||
.arg(REAL_ASOUND_CONF)
|
||||
.status()
|
||||
.context("执行 mount 命令失败")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow::anyhow!("挂载 asound.conf 失败"));
|
||||
}
|
||||
|
||||
Self::restart_applications();
|
||||
}
|
||||
|
||||
// 创建 FIFO 管道
|
||||
let _ = Command::new("mkfifo").arg(FIFO_PATH).status();
|
||||
let _ = Command::new("chmod").arg("666").arg(FIFO_PATH).status();
|
||||
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
pub fn cleanup() {
|
||||
let _ = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("umount -l {} >/dev/null 2>&1", REAL_ASOUND_CONF))
|
||||
.status();
|
||||
let _ = fs::remove_file(TEMP_ASOUND_CONF);
|
||||
let _ = fs::remove_file(FIFO_PATH);
|
||||
Self::restart_applications();
|
||||
}
|
||||
|
||||
pub fn fifo_path() -> &'static str {
|
||||
FIFO_PATH
|
||||
}
|
||||
|
||||
pub fn restart_applications() {
|
||||
// 重启媒体播放器
|
||||
let _ = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("/etc/init.d/mediaplayer restart >/dev/null 2>&1")
|
||||
.status();
|
||||
// 重启蓝牙
|
||||
let _ = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("/etc/init.d/bluetooth restart >/dev/null 2>&1")
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AlsaRedirector {
|
||||
fn drop(&mut self) {
|
||||
Self::cleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
use crate::net::protocol::AudioPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OrderedPacket {
|
||||
seq: u32,
|
||||
timestamp: u128,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
// 仅针对序列号进行比较,处理回绕逻辑
|
||||
impl PartialEq for OrderedPacket {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.seq == other.seq
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for OrderedPacket {}
|
||||
|
||||
impl PartialOrd for OrderedPacket {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for OrderedPacket {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// 使用 wrapping_sub 处理 u32 回绕 (Rollover)
|
||||
let diff = self.seq.wrapping_sub(other.seq) as i32;
|
||||
diff.cmp(&0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JitterBuffer {
|
||||
// 使用 Reverse 将 BinaryHeap 变为小顶堆,避免手动实现大量 Trait
|
||||
buffer: BinaryHeap<Reverse<OrderedPacket>>,
|
||||
last_played_seq: Option<u32>,
|
||||
pub target_delay_us: u128,
|
||||
min_packets: usize, // 最小缓冲包数,防止微小抖动
|
||||
}
|
||||
|
||||
impl JitterBuffer {
|
||||
pub fn new(target_delay_us: u128, min_packets: usize) -> Self {
|
||||
Self {
|
||||
buffer: BinaryHeap::with_capacity(32), // 预分配初始容量
|
||||
last_played_seq: None,
|
||||
target_delay_us,
|
||||
min_packets,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, packet: AudioPacket) {
|
||||
// 1. 处理序列号回绕的丢包逻辑
|
||||
if let Some(last) = self.last_played_seq {
|
||||
let diff = packet.seq.wrapping_sub(last) as i32;
|
||||
if diff <= 0 {
|
||||
return; // 这是一个延迟到达的旧包,直接丢弃
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.push(Reverse(OrderedPacket {
|
||||
seq: packet.seq,
|
||||
timestamp: packet.timestamp,
|
||||
data: packet.data,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn pop_frame(&mut self, current_time: u128) -> Option<(u32, Vec<u8>)> {
|
||||
// 2. 预缓冲逻辑:如果包量太少,先不播放,等待填充
|
||||
if self.buffer.len() < self.min_packets && self.last_played_seq.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 3. 检查堆顶元素
|
||||
if let Some(Reverse(pkt)) = self.buffer.peek() {
|
||||
// 判断是否到达播放时间(考虑目标延迟)
|
||||
if current_time >= pkt.timestamp + (self.target_delay_us / 1000) {
|
||||
let Reverse(pkt) = self.buffer.pop().unwrap();
|
||||
self.last_played_seq = Some(pkt.seq);
|
||||
return Some((pkt.seq, pkt.data));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 如果缓冲区堆积过大,可以主动跳帧以降低延迟
|
||||
pub fn shrink_to_fit_latency(&mut self, max_size: usize) {
|
||||
while self.buffer.len() > max_size {
|
||||
self.buffer.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.buffer.clear();
|
||||
self.last_played_seq = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod alsa;
|
||||
pub mod jitter_buffer;
|
||||
pub mod sync;
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 获取当前微秒级时间戳
|
||||
pub fn now_us() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("时间倒流")
|
||||
.as_micros()
|
||||
}
|
||||
|
||||
/// 时钟同步管理器,用于计算主从节点间的时钟偏移
|
||||
pub struct ClockSync {
|
||||
offsets: VecDeque<i128>,
|
||||
pub current_offset: i128,
|
||||
window_size: usize,
|
||||
}
|
||||
|
||||
impl ClockSync {
|
||||
pub fn new(window_size: usize) -> Self {
|
||||
Self {
|
||||
offsets: VecDeque::with_capacity(window_size),
|
||||
current_offset: 0,
|
||||
window_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新时钟偏移估计
|
||||
pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) {
|
||||
let rtt = (client_recv_ts - client_send_ts) as i128;
|
||||
// 基础过滤:如果 RTT 过大则忽略 (例如局域网内 > 100ms)
|
||||
if rtt > 100_000 {
|
||||
return;
|
||||
}
|
||||
|
||||
// 时钟偏移 = 主节点时间 - 从节点时间
|
||||
// 假设主节点收到 Ping 的时间点在 (发送时间 + 接收时间) / 2
|
||||
let estimated_server_time = server_ts as i128 + rtt / 2;
|
||||
let offset = estimated_server_time - client_recv_ts as i128;
|
||||
|
||||
self.offsets.push_back(offset);
|
||||
if self.offsets.len() > self.window_size {
|
||||
self.offsets.pop_front();
|
||||
}
|
||||
|
||||
// 计算中位数偏移,以增强抗干扰能力
|
||||
let mut sorted: Vec<i128> = self.offsets.iter().cloned().collect();
|
||||
sorted.sort_unstable();
|
||||
if !sorted.is_empty() {
|
||||
self.current_offset = sorted[sorted.len() / 2];
|
||||
}
|
||||
}
|
||||
|
||||
/// 将本地时间转换为服务器(主节点)时间
|
||||
pub fn to_server_time(&self, client_time: u128) -> u128 {
|
||||
(client_time as i128 + self.current_offset) as u128
|
||||
}
|
||||
|
||||
/// 将服务器(主节点)时间转换为本地时间
|
||||
pub fn to_client_time(&self, server_time: u128) -> u128 {
|
||||
(server_time as i128 - self.current_offset) as u128
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user