feat(stereo): 新增不同型号的小爱音箱组立体声应用
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::app::master::run_master;
|
||||
use crate::app::slave::run_slave;
|
||||
use crate::net::protocol::ChannelRole;
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
|
||||
pub async fn run_stereo() -> Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("用法: {} [master|slave] [left|right]", args[0]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mode = &args[1];
|
||||
let role = if args[2].to_lowercase() == "left" {
|
||||
ChannelRole::Left
|
||||
} else {
|
||||
ChannelRole::Right
|
||||
};
|
||||
|
||||
if mode == "master" {
|
||||
run_master(role).await
|
||||
} else {
|
||||
run_slave(role).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::audio::codec::OpusCodec;
|
||||
use crate::audio::config::AudioConfig;
|
||||
use crate::audio::player::AudioPlayer;
|
||||
use crate::net::discovery::Discovery;
|
||||
use crate::net::network::{ControlConnection, MasterNetwork};
|
||||
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
|
||||
use crate::utils::alsa::AlsaRedirector;
|
||||
use crate::utils::sync::now_us;
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub const SERVER_TCP_PORT: u16 = 53531;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SlaveSession {
|
||||
udp_addr: SocketAddr,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
pub async fn run_master(master_role: ChannelRole) -> Result<()> {
|
||||
println!("--- 主节点模式 ({}) ---", master_role.to_string());
|
||||
|
||||
// 0. 设置 ALSA 重定向
|
||||
println!("🔥 启动中,请稍等...");
|
||||
let _alsa_guard = AlsaRedirector::new()?;
|
||||
|
||||
// 1. 设置网络 (UDP + TCP)
|
||||
let network = MasterNetwork::setup(SERVER_TCP_PORT).await?;
|
||||
let audio_socket = network.audio_socket().clone_inner();
|
||||
|
||||
// 2. 启动服务发现广播
|
||||
Discovery::start_broadcast(SERVER_TCP_PORT).await?;
|
||||
|
||||
println!("✅ 服务已启动,等待连接...");
|
||||
|
||||
let shutdown_flag = Arc::new(AtomicBool::new(false));
|
||||
let slaves = Arc::new(Mutex::new(Vec::<SlaveSession>::new()));
|
||||
|
||||
// 3. 启动连接监听任务
|
||||
let slaves_clone = slaves.clone();
|
||||
let audio_socket_clone = audio_socket.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match network.accept().await {
|
||||
Ok((control_conn, client_addr)) => {
|
||||
let slaves_for_session = slaves_clone.clone();
|
||||
let audio_socket_for_session = audio_socket_clone.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_master_session(
|
||||
control_conn,
|
||||
audio_socket_for_session,
|
||||
slaves_for_session,
|
||||
client_addr.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("❌ 会话错误: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Accept 错误: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 音频处理主循环
|
||||
let config = AudioConfig::music();
|
||||
let encode_config = AudioConfig {
|
||||
channels: 1,
|
||||
vbr: true,
|
||||
..AudioConfig::music()
|
||||
};
|
||||
let player = AudioPlayer::new(&AudioConfig {
|
||||
channels: 2,
|
||||
playback_device: "plug:original_default".into(),
|
||||
..config.clone()
|
||||
})?;
|
||||
|
||||
let mut raw_buf = vec![0u8; config.frame_size * 2 * 2];
|
||||
let mut pcm_out = vec![0i16; config.frame_size * 2];
|
||||
let mut left_pcm = vec![0i16; config.frame_size];
|
||||
let mut right_pcm = vec![0i16; config.frame_size];
|
||||
let mut opus_out = vec![0u8; 1500];
|
||||
let mut seq = 0u32;
|
||||
|
||||
let delay_us = 200_000;
|
||||
let frame_duration_us =
|
||||
(config.frame_size as f64 / config.sample_rate as f64 * 1_000_000.0) as u128;
|
||||
|
||||
let mut stream_start_ts = 0;
|
||||
let mut stream_start_seq = 0;
|
||||
|
||||
let shutdown_flag_clone = shutdown_flag.clone();
|
||||
let audio_loop = async move {
|
||||
loop {
|
||||
if shutdown_flag_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 打开 FIFO
|
||||
let mut fifo = match tokio::fs::File::open(AlsaRedirector::fifo_path()).await {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
if shutdown_flag_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 每个新流开始时,重置编码器状态以避免残留音频导致爆音
|
||||
let mut left_encoder = OpusCodec::new(&encode_config)?;
|
||||
let mut right_encoder = OpusCodec::new(&encode_config)?;
|
||||
|
||||
loop {
|
||||
if shutdown_flag_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 从 FIFO 读取
|
||||
if let Err(_) = fifo.read_exact(&mut raw_buf).await {
|
||||
break; // FIFO 关闭,重新打开
|
||||
}
|
||||
|
||||
let active_slaves = {
|
||||
let s = slaves.lock().await;
|
||||
if s.is_empty() { None } else { Some(s.clone()) }
|
||||
};
|
||||
|
||||
let now = now_us();
|
||||
if stream_start_ts == 0 {
|
||||
stream_start_ts = now;
|
||||
stream_start_seq = seq;
|
||||
}
|
||||
|
||||
// 计算该帧应当播放的基准时间(相对于流开始)
|
||||
let target_ts = stream_start_ts
|
||||
+ ((seq - stream_start_seq) as u128 * frame_duration_us)
|
||||
+ delay_us;
|
||||
|
||||
// 提取 PCM 数据
|
||||
for i in 0..config.frame_size {
|
||||
left_pcm[i] = i16::from_le_bytes([raw_buf[i * 4], raw_buf[i * 4 + 1]]);
|
||||
right_pcm[i] = i16::from_le_bytes([raw_buf[i * 4 + 2], raw_buf[i * 4 + 3]]);
|
||||
}
|
||||
|
||||
if let Some(slaves_list) = active_slaves {
|
||||
// 情况 1: 有从节点,进行网络传输,并本地构造静音声道回放
|
||||
|
||||
// 1. 检查各声道是否有从节点需要
|
||||
let needs_left = slaves_list.iter().any(|s| s.role == ChannelRole::Left);
|
||||
let needs_right = slaves_list.iter().any(|s| s.role == ChannelRole::Right);
|
||||
|
||||
// 2. 编码需要的声道
|
||||
let mut left_bytes = None;
|
||||
let mut right_bytes = None;
|
||||
|
||||
if needs_left {
|
||||
let len = left_encoder.encode(&left_pcm, &mut opus_out)?;
|
||||
let packet = AudioPacket {
|
||||
seq,
|
||||
timestamp: target_ts,
|
||||
data: opus_out[..len].to_vec(),
|
||||
};
|
||||
left_bytes = Some(postcard::to_allocvec(&packet)?);
|
||||
}
|
||||
|
||||
if needs_right {
|
||||
let len = right_encoder.encode(&right_pcm, &mut opus_out)?;
|
||||
let packet = AudioPacket {
|
||||
seq,
|
||||
timestamp: target_ts,
|
||||
data: opus_out[..len].to_vec(),
|
||||
};
|
||||
right_bytes = Some(postcard::to_allocvec(&packet)?);
|
||||
}
|
||||
|
||||
// 3. 发送给对应的从节点
|
||||
for slave in &slaves_list {
|
||||
let bytes = match slave.role {
|
||||
ChannelRole::Left => left_bytes.as_ref(),
|
||||
ChannelRole::Right => right_bytes.as_ref(),
|
||||
};
|
||||
if let Some(b) = bytes {
|
||||
let _ = audio_socket.send_to(b, slave.udp_addr).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 将非本节点的声道置为静音
|
||||
for i in 0..config.frame_size {
|
||||
match master_role {
|
||||
ChannelRole::Left => {
|
||||
pcm_out[i * 2] = left_pcm[i];
|
||||
pcm_out[i * 2 + 1] = 0;
|
||||
}
|
||||
ChannelRole::Right => {
|
||||
pcm_out[i * 2] = 0;
|
||||
pcm_out[i * 2 + 1] = right_pcm[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 等待播放
|
||||
let now = now_us();
|
||||
if now < target_ts {
|
||||
let wait = target_ts - now;
|
||||
if wait > 1000 {
|
||||
tokio::time::sleep(Duration::from_micros(wait as u64)).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 情况 2: 没有从节点,本地立体声播放
|
||||
for i in 0..config.frame_size {
|
||||
pcm_out[i * 2] = left_pcm[i];
|
||||
pcm_out[i * 2 + 1] = right_pcm[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 统一写入播放器 (始终是立体声)
|
||||
if let Err(_) = player.write(&pcm_out) {
|
||||
if shutdown_flag_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
seq += 1;
|
||||
}
|
||||
|
||||
// 重置流计时
|
||||
stream_start_ts = 0;
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
res = audio_loop => {
|
||||
if let Err(e) = res {
|
||||
eprintln!("❌ 音频循环错误: {:?}", e);
|
||||
}
|
||||
},
|
||||
_ = shutdown_signal() => {
|
||||
// 设置退出标志,通知音频循环停止
|
||||
shutdown_flag.store(true, Ordering::Relaxed);
|
||||
},
|
||||
}
|
||||
|
||||
// 显式清理
|
||||
println!("👋 正在退出...");
|
||||
AlsaRedirector::cleanup();
|
||||
|
||||
// 强制退出
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
/// 监听系统退出信号 (SIGINT, SIGTERM, SIGQUIT)
|
||||
async fn shutdown_signal() {
|
||||
let mut sigint = signal(SignalKind::interrupt()).expect("无法注册 SIGINT 处理器");
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("无法注册 SIGTERM 处理器");
|
||||
let mut sigquit = signal(SignalKind::quit()).expect("无法注册 SIGQUIT 处理器");
|
||||
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => {},
|
||||
_ = sigterm.recv() => {},
|
||||
_ = sigquit.recv() => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理主节点与从节点的会话
|
||||
async fn handle_master_session(
|
||||
mut control: ControlConnection,
|
||||
audio_socket: Arc<tokio::net::UdpSocket>,
|
||||
slaves: Arc<Mutex<Vec<SlaveSession>>>,
|
||||
client_tcp_addr: String,
|
||||
) -> Result<()> {
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
// 握手
|
||||
let pkt = control.recv_packet(&mut buf).await?;
|
||||
let slave_role = match pkt {
|
||||
ControlPacket::ClientIdentify { role } => role,
|
||||
_ => return Err(anyhow!("无效的握手协议")),
|
||||
};
|
||||
|
||||
let stereo = ControlPacket::ServerHello {
|
||||
udp_port: audio_socket.local_addr()?.port(),
|
||||
};
|
||||
control.send_packet(&stereo).await?;
|
||||
|
||||
// 等待 UDP 打洞/确认
|
||||
let mut buf = [0u8; 128];
|
||||
let (_, client_udp_addr) = audio_socket.recv_from(&mut buf).await?;
|
||||
|
||||
println!(
|
||||
"✅ 从节点已连接: {} {}",
|
||||
client_tcp_addr,
|
||||
slave_role.to_string(),
|
||||
);
|
||||
|
||||
// 添加到从节点列表
|
||||
let session = SlaveSession {
|
||||
udp_addr: client_udp_addr,
|
||||
role: slave_role,
|
||||
};
|
||||
{
|
||||
let mut s = slaves.lock().await;
|
||||
s.push(session.clone());
|
||||
}
|
||||
|
||||
// 分离 TCP 读写,处理控制消息和心跳
|
||||
let (mut tcp_rx, mut tcp_tx) = control.split();
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
loop {
|
||||
match tcp_rx.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
if let Ok(ControlPacket::Ping { client_ts, seq }) = postcard::from_bytes(&buf[..n])
|
||||
{
|
||||
let pong = ControlPacket::Pong {
|
||||
client_ts,
|
||||
server_ts: now_us(),
|
||||
seq,
|
||||
};
|
||||
if tcp_tx
|
||||
.write_all(&postcard::to_allocvec(&pong).unwrap())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"❌ 从节点已断开: {} {}",
|
||||
client_tcp_addr,
|
||||
slave_role.to_string(),
|
||||
);
|
||||
|
||||
// 从列表中移除
|
||||
{
|
||||
let mut s = slaves.lock().await;
|
||||
s.retain(|x| x.udp_addr != client_udp_addr);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod entry;
|
||||
pub mod master;
|
||||
pub mod slave;
|
||||
@@ -0,0 +1,191 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::audio::codec::OpusCodec;
|
||||
use crate::audio::config::AudioConfig;
|
||||
use crate::audio::player::AudioPlayer;
|
||||
use crate::net::discovery::Discovery;
|
||||
use crate::net::network::SlaveNetwork;
|
||||
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
|
||||
use crate::utils::jitter_buffer::JitterBuffer;
|
||||
use crate::utils::sync::{ClockSync, now_us};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
|
||||
/// 运行从节点模式
|
||||
pub async fn run_slave(role: ChannelRole) -> Result<()> {
|
||||
println!("--- 从节点模式 ({}) ---", role.to_string());
|
||||
|
||||
loop {
|
||||
match handle_connection(role.clone()).await {
|
||||
Err(e) => {
|
||||
eprintln!("❌ {:?}", e);
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(role: ChannelRole) -> Result<()> {
|
||||
// 1. 发现主节点
|
||||
println!("🔍 正在扫描主节点...");
|
||||
let (master_ip, master_tcp_port) = Discovery::discover_master().await?;
|
||||
let master_tcp_addr = format!("{}:{}", master_ip, master_tcp_port);
|
||||
|
||||
// 2. 建立 TCP 连接
|
||||
println!("🔥 发现主节点: {}", master_tcp_addr);
|
||||
let network = SlaveNetwork::connect(master_tcp_addr.parse()?).await?;
|
||||
let (mut control, audio) = network.split();
|
||||
|
||||
// 3. 身份认证
|
||||
control
|
||||
.send_packet(&ControlPacket::ClientIdentify { role: role.clone() })
|
||||
.await?;
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let pkt = control.recv_packet(&mut buf).await?;
|
||||
let server_udp_port = match pkt {
|
||||
ControlPacket::ServerHello { udp_port } => udp_port,
|
||||
_ => return Err(anyhow!("身份认证应答异常")),
|
||||
};
|
||||
|
||||
// 4. UDP 打洞
|
||||
audio
|
||||
.punch(format!("{}:{}", master_ip, server_udp_port).parse()?)
|
||||
.await?;
|
||||
|
||||
// 5. 初始化音频与同步组件
|
||||
let config = AudioConfig {
|
||||
channels: 1,
|
||||
..AudioConfig::music()
|
||||
};
|
||||
let player = AudioPlayer::new(&config)?;
|
||||
let mut codec = OpusCodec::new(&config)?;
|
||||
let mut jitter = JitterBuffer::new(50_000, 3);
|
||||
let clock = Arc::new(Mutex::new(ClockSync::new(100)));
|
||||
|
||||
// 用于通知主循环 TCP 已断开的消息通道
|
||||
let (disconnect_tx, mut disconnect_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
// 6. 分离 TCP 读写
|
||||
let (mut tcp_rx, mut tcp_tx) = control.split();
|
||||
let clock_updater = clock.clone();
|
||||
let d_tx_ping = disconnect_tx.clone();
|
||||
let d_tx_pong = disconnect_tx.clone();
|
||||
|
||||
// 定时发送 Ping (心跳 & 时间同步)
|
||||
let _sync_handle = tokio::spawn(async move {
|
||||
let mut seq = 0;
|
||||
loop {
|
||||
let t1 = now_us();
|
||||
let msg = ControlPacket::Ping { client_ts: t1, seq };
|
||||
let data = postcard::to_allocvec(&msg).unwrap();
|
||||
if tcp_tx.write_all(&data).await.is_err() {
|
||||
let _ = d_tx_ping.send(()).await; // 通知主线程 TCP 失败
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
seq += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// 接收 Pong
|
||||
tokio::spawn(async move {
|
||||
let mut buf = [0u8; 1024];
|
||||
loop {
|
||||
match tcp_rx.read(&mut buf).await {
|
||||
Ok(n) if n > 0 => {
|
||||
if let Ok(ControlPacket::Pong {
|
||||
client_ts,
|
||||
server_ts,
|
||||
..
|
||||
}) = postcard::from_bytes(&buf[..n])
|
||||
{
|
||||
let t4 = now_us();
|
||||
clock_updater.lock().await.update(client_ts, server_ts, t4);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let _ = d_tx_pong.send(()).await; // TCP 断开
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 接收音频数据包 (UDP)
|
||||
let (audio_tx, mut audio_rx) = mpsc::channel(100);
|
||||
let audio_socket = audio.clone_inner();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = [0u8; 2048];
|
||||
loop {
|
||||
if let Ok((len, _)) = audio_socket.recv_from(&mut buf).await {
|
||||
if let Ok(packet) = postcard::from_bytes::<AudioPacket>(&buf[..len]) {
|
||||
if audio_tx.send(packet).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 8. 播放主循环
|
||||
println!("✅ 主节点已连接,音频串流中...");
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut last_seq: Option<u32> = None;
|
||||
let mut last_packet_time = now_us();
|
||||
|
||||
loop {
|
||||
// 检查 TCP 是否已断开
|
||||
if let Ok(_) = disconnect_rx.try_recv() {
|
||||
return Err(anyhow!("主节点已断开: {}", master_tcp_addr));
|
||||
}
|
||||
|
||||
// 填充 Jitter Buffer
|
||||
while let Ok(pkt) = audio_rx.try_recv() {
|
||||
let now = now_us();
|
||||
// 如果超过 500ms 没有收到包,认为是新流开始,重置状态
|
||||
if now - last_packet_time > 500_000 {
|
||||
jitter.clear();
|
||||
last_seq = None;
|
||||
codec = OpusCodec::new(&config)?;
|
||||
let _ = player.prepare();
|
||||
}
|
||||
last_packet_time = now;
|
||||
jitter.push(pkt);
|
||||
}
|
||||
|
||||
let now = now_us();
|
||||
let current_server_time = clock.lock().await.to_server_time(now);
|
||||
|
||||
if let Some((seq, data)) = jitter.pop_frame(current_server_time) {
|
||||
if let Some(last) = last_seq {
|
||||
let loss_count = seq.wrapping_sub(last) as i32 - 1;
|
||||
if loss_count > 0 {
|
||||
// 1. 优先尝试 FEC 恢复最近丢失的那一帧
|
||||
// Opus 的 FEC 数据存储在当前包(data)中,用于恢复“前一帧”
|
||||
if let Ok(len) = codec.decode_fec(&data, &mut pcm_buf) {
|
||||
let _ = player.write(&pcm_buf[..len]);
|
||||
}
|
||||
|
||||
// 2. 如果丢包超过 1 帧,剩下的帧只能靠丢包补偿(PLC)
|
||||
for _ in 0..(loss_count - 1) {
|
||||
if let Ok(len) = codec.decode_loss(&mut pcm_buf) {
|
||||
let _ = player.write(&pcm_buf[..len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
last_seq = Some(seq);
|
||||
|
||||
// 3. 正常解码当前帧
|
||||
let len = codec.decode(&data, &mut pcm_buf)?;
|
||||
player.write(&pcm_buf[..len])?;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user