chore: 初始化测试场景
This commit is contained in:
Generated
+1
@@ -541,6 +541,7 @@ dependencies = [
|
||||
"alsa",
|
||||
"anyhow",
|
||||
"opus",
|
||||
"parking_lot",
|
||||
"postcard",
|
||||
"serde",
|
||||
"tokio",
|
||||
|
||||
@@ -21,6 +21,7 @@ anyhow = "1.0"
|
||||
tokio = { version = "1.48", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
postcard = { version = "1.0", features = ["alloc", "use-std"] }
|
||||
parking_lot = "0.12.5"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
alsa = "0.11"
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
build-server:
|
||||
cargo build --release --bin server --features app
|
||||
clear && cargo build --release --bin server --features app
|
||||
|
||||
build-client:
|
||||
docker run --rm -v $(shell pwd):/app idootop/open-xiaoai-runtime:oh2p \
|
||||
clear && docker run --rm -v $(shell pwd):/app idootop/open-xiaoai-runtime:oh2p \
|
||||
cargo build --target armv7-unknown-linux-gnueabihf --release --bin client --features app
|
||||
|
||||
run-server:
|
||||
cargo run --bin server --features app
|
||||
target/release/server
|
||||
|
||||
# 部署到小爱音箱(调试自用)
|
||||
deploy:
|
||||
dd if=target/armv7-unknown-linux-gnueabihf/release/client \
|
||||
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.153 "dd of=/data/client"
|
||||
dd if=target/armv7-unknown-linux-gnueabihf/release/client \
|
||||
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/client"
|
||||
@@ -0,0 +1,224 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::audio::codec::OpusCodec;
|
||||
use crate::audio::config::AudioConfig;
|
||||
use crate::audio::player::AudioPlayer;
|
||||
use crate::audio::recorder::AudioRecorder;
|
||||
use crate::net::discovery::Discovery;
|
||||
use crate::net::network::{AudioSocket, ClientNetwork, ControlConnection};
|
||||
use crate::net::protocol::{AudioPacket, ControlPacket, DeviceInfo, RpcResult};
|
||||
use anyhow::{Context, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub struct Client {
|
||||
info: DeviceInfo,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(model: &str, mac: &str, version: u32) -> Self {
|
||||
Self {
|
||||
info: DeviceInfo {
|
||||
model: model.to_string(),
|
||||
mac: mac.to_string(),
|
||||
version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<()> {
|
||||
println!("正在寻找服务端...");
|
||||
let (server_ip, server_port) = Discovery::discover_server().await?;
|
||||
let server_addr = SocketAddr::new(server_ip, server_port);
|
||||
println!("发现服务端: {}", server_addr);
|
||||
|
||||
let network = ClientNetwork::connect(server_addr).await?;
|
||||
let mut control = network.into_control();
|
||||
|
||||
// 握手
|
||||
control
|
||||
.send_packet(&ControlPacket::ClientIdentify {
|
||||
info: self.info.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
match control.recv_packet().await? {
|
||||
ControlPacket::IdentifyOk => println!("认证成功"),
|
||||
p => return Err(anyhow::anyhow!("认证失败: {:?}", p)),
|
||||
}
|
||||
|
||||
let (stop_tx, _) = broadcast::channel::<()>(1);
|
||||
let audio_socket = Arc::new(AudioSocket::bind().await?);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
packet = control.recv_packet() => {
|
||||
let packet = packet?;
|
||||
match packet {
|
||||
ControlPacket::StartRecording { config } => {
|
||||
println!("开始录音: {:?}", config);
|
||||
let socket = audio_socket.clone();
|
||||
let mut stop_rx = stop_tx.subscribe();
|
||||
let server_addr = control.peer_addr()?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_recording(config, socket, server_addr, stop_rx).await {
|
||||
eprintln!("录音出错: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
ControlPacket::StopRecording => {
|
||||
println!("停止录音");
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
ControlPacket::StartPlayback { config } => {
|
||||
println!("开始播放: {:?}", config);
|
||||
let socket = audio_socket.clone();
|
||||
let mut stop_rx = stop_tx.subscribe();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_playback(config, socket, stop_rx).await {
|
||||
eprintln!("播放出错: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
ControlPacket::StopPlayback => {
|
||||
println!("停止播放");
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
ControlPacket::RpcRequest { id, method, args } => {
|
||||
println!("收到 RPC 请求: {} {:?}", method, args);
|
||||
let result = handle_rpc(&method, args).await;
|
||||
control.send_packet(&ControlPacket::RpcResponse { id, result }).await?;
|
||||
}
|
||||
ControlPacket::Ping => {
|
||||
control.send_packet(&ControlPacket::Pong).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_recording(
|
||||
config: AudioConfig,
|
||||
socket: Arc<AudioSocket>,
|
||||
server_addr: SocketAddr,
|
||||
mut stop_rx: broadcast::Receiver<()>,
|
||||
) -> Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let recorder = AudioRecorder::new(&config)?;
|
||||
let mut codec = OpusCodec::new(&config)?;
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut opus_buf = vec![0u8; 4096];
|
||||
|
||||
loop {
|
||||
if stop_rx.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
let n = recorder.read(&mut pcm_buf)?;
|
||||
if n > 0 {
|
||||
let opus_len = codec.encode(&pcm_buf[..n], &mut opus_buf)?;
|
||||
let packet = AudioPacket {
|
||||
data: opus_buf[..opus_len].to_vec(),
|
||||
};
|
||||
socket.send_packet(&packet, server_addr).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
println!("当前系统不支持 ALSA 录音,模拟发送音频数据...");
|
||||
loop {
|
||||
if stop_rx.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
let packet = AudioPacket {
|
||||
data: vec![0u8; 10],
|
||||
};
|
||||
socket.send_packet(&packet, server_addr).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_playback(
|
||||
config: AudioConfig,
|
||||
socket: Arc<AudioSocket>,
|
||||
mut stop_rx: broadcast::Receiver<()>,
|
||||
) -> Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let player = AudioPlayer::new(&config)?;
|
||||
let mut codec = OpusCodec::new(&config)?;
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut udp_buf = vec![0u8; 4096];
|
||||
|
||||
loop {
|
||||
if stop_rx.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
// 这里简单处理,UDP 接收可能阻塞。实际建议加超时或 select
|
||||
let (packet, _) = socket.recv_packet(&mut udp_buf).await?;
|
||||
let pcm_len = codec.decode(&packet.data, &mut pcm_buf)?;
|
||||
player.write(&pcm_buf[..pcm_len])?;
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
println!("当前系统不支持 ALSA 播放,模拟接收音频数据...");
|
||||
let mut udp_buf = vec![0u8; 4096];
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = stop_rx.recv() => break,
|
||||
res = socket.recv_packet(&mut udp_buf) => {
|
||||
let _ = res?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_rpc(method: &str, args: Vec<String>) -> RpcResult {
|
||||
if method == "shell" && !args.is_empty() {
|
||||
let cmd = &args[0];
|
||||
println!("执行 Shell: {}", cmd);
|
||||
|
||||
// 模拟执行
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("sh").arg("-c").arg(cmd).output();
|
||||
match output {
|
||||
Ok(out) => RpcResult {
|
||||
stdout: String::from_utf8_lossy(&out.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).to_string(),
|
||||
code: out.status.code().unwrap_or(0),
|
||||
},
|
||||
Err(e) => RpcResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: e.to_string(),
|
||||
code: -1,
|
||||
},
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
RpcResult {
|
||||
stdout: format!("模拟执行 {} 成功", cmd),
|
||||
stderr: "".to_string(),
|
||||
code: 0,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RpcResult {
|
||||
stdout: "".to_string(),
|
||||
stderr: "未知方法或参数错误".to_string(),
|
||||
code: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::app::client::core::Client;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_client() -> Result<()> {
|
||||
println!("Hello, Client!");
|
||||
Ok(())
|
||||
// 模拟从系统获取信息
|
||||
let model = "XiaoAi-V2-Simulated";
|
||||
let mac = "00:11:22:33:44:55";
|
||||
let version = 1;
|
||||
|
||||
let client = Client::new(model, mac, version);
|
||||
client.run().await
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod core;
|
||||
pub mod entry;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod stereo;
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
use crate::audio::codec::OpusCodec;
|
||||
use crate::audio::config::AudioConfig;
|
||||
use crate::audio::wav::{WavReader, WavWriter};
|
||||
use crate::net::discovery::Discovery;
|
||||
use crate::net::network::{AudioSocket, ControlConnection, ServerNetwork};
|
||||
use crate::net::protocol::{AudioPacket, ControlPacket, DeviceInfo, RpcResult};
|
||||
use anyhow::{Context, Result};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
|
||||
pub struct RpcManager {
|
||||
next_id: AtomicU32,
|
||||
pending: Mutex<HashMap<u32, oneshot::Sender<RpcResult>>>,
|
||||
}
|
||||
|
||||
impl RpcManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU32::new(1),
|
||||
pending: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_id(&self) -> (u32, oneshot::Receiver<RpcResult>) {
|
||||
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.pending.lock().insert(id, tx);
|
||||
(id, rx)
|
||||
}
|
||||
|
||||
pub fn fulfill(&self, id: u32, result: RpcResult) {
|
||||
if let Some(tx) = self.pending.lock().remove(&id) {
|
||||
let _ = tx.send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub info: DeviceInfo,
|
||||
pub control: Arc<tokio::sync::Mutex<ControlConnection>>,
|
||||
pub addr: SocketAddr,
|
||||
pub rpc: Arc<RpcManager>,
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
sessions: Arc<Mutex<HashMap<SocketAddr, Arc<Session>>>>,
|
||||
audio_socket: Arc<AudioSocket>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
audio_socket: Arc::new(AudioSocket::bind().await?),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>, port: u16) -> Result<()> {
|
||||
let network = ServerNetwork::setup(port).await?;
|
||||
println!("服务端启动在: {}", network.local_addr()?);
|
||||
|
||||
// 启动广播
|
||||
Discovery::start_broadcast(port).await?;
|
||||
|
||||
loop {
|
||||
let (control, addr) = network.accept().await?;
|
||||
let server = self.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = server.handle_connection(control, addr).await {
|
||||
eprintln!("连接 {} 出错: {}", addr, e);
|
||||
}
|
||||
server.sessions.lock().remove(&addr);
|
||||
println!("客户端 {} 断开连接", addr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
&self,
|
||||
mut control: ControlConnection,
|
||||
addr: SocketAddr,
|
||||
) -> Result<()> {
|
||||
println!("新客户端连接: {}", addr);
|
||||
|
||||
// 握手认证
|
||||
let info = match control.recv_packet().await? {
|
||||
ControlPacket::ClientIdentify { info } => info,
|
||||
p => return Err(anyhow::anyhow!("预期的握手包,收到: {:?}", p)),
|
||||
};
|
||||
|
||||
println!(
|
||||
"客户端识别: {} ({}) v{}",
|
||||
info.model, info.mac, info.version
|
||||
);
|
||||
control.send_packet(&ControlPacket::IdentifyOk).await?;
|
||||
|
||||
let rpc = Arc::new(RpcManager::new());
|
||||
let control = Arc::new(tokio::sync::Mutex::new(control));
|
||||
let session = Arc::new(Session {
|
||||
info,
|
||||
control: control.clone(),
|
||||
addr,
|
||||
rpc: rpc.clone(),
|
||||
});
|
||||
|
||||
self.sessions.lock().insert(addr, session.clone());
|
||||
|
||||
// 处理控制消息循环
|
||||
loop {
|
||||
let mut ctrl = control.lock().await;
|
||||
let packet = ctrl.recv_packet().await?;
|
||||
drop(ctrl); // 释放锁以允许发送
|
||||
|
||||
match packet {
|
||||
ControlPacket::RpcResponse { id, result } => {
|
||||
rpc.fulfill(id, result);
|
||||
}
|
||||
ControlPacket::Ping => {
|
||||
control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::Pong)
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给外部调用的方法
|
||||
pub async fn call_shell(&self, addr: SocketAddr, cmd: &str) -> Result<RpcResult> {
|
||||
let session = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get(&addr)
|
||||
.cloned()
|
||||
.context("未找到 Session")?;
|
||||
let (id, rx) = session.rpc.alloc_id();
|
||||
session
|
||||
.control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::RpcRequest {
|
||||
id,
|
||||
method: "shell".to_string(),
|
||||
args: vec![cmd.to_string()],
|
||||
})
|
||||
.await?;
|
||||
Ok(rx.await?)
|
||||
}
|
||||
|
||||
pub async fn start_recording(&self, addr: SocketAddr, config: AudioConfig) -> Result<()> {
|
||||
let session = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get(&addr)
|
||||
.cloned()
|
||||
.context("未找到 Session")?;
|
||||
|
||||
// 发送开始录音指令
|
||||
session
|
||||
.control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::StartRecording {
|
||||
config: config.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let audio_socket = self.audio_socket.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = save_audio_to_wav(audio_socket, config, "temp/recorded.wav").await {
|
||||
eprintln!("保存录音失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_recording(&self, addr: SocketAddr) -> Result<()> {
|
||||
let session = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get(&addr)
|
||||
.cloned()
|
||||
.context("未找到 Session")?;
|
||||
session
|
||||
.control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::StopRecording)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_playback(&self, addr: SocketAddr, config: AudioConfig) -> Result<()> {
|
||||
let session = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get(&addr)
|
||||
.cloned()
|
||||
.context("未找到 Session")?;
|
||||
|
||||
session
|
||||
.control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::StartPlayback {
|
||||
config: config.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let audio_socket = self.audio_socket.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = stream_wav_to_client(audio_socket, addr, config, "temp/test.wav").await
|
||||
{
|
||||
eprintln!("推流失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_playback(&self, addr: SocketAddr) -> Result<()> {
|
||||
let session = self
|
||||
.sessions
|
||||
.lock()
|
||||
.get(&addr)
|
||||
.cloned()
|
||||
.context("未找到 Session")?;
|
||||
session
|
||||
.control
|
||||
.lock()
|
||||
.await
|
||||
.send_packet(&ControlPacket::StopPlayback)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_sessions(&self) -> Vec<SocketAddr> {
|
||||
self.sessions.lock().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_audio_to_wav(
|
||||
socket: Arc<AudioSocket>,
|
||||
config: AudioConfig,
|
||||
path: &str,
|
||||
) -> Result<()> {
|
||||
std::fs::create_dir_all("temp")?;
|
||||
let mut writer = WavWriter::create(path, config.sample_rate, config.channels)?;
|
||||
let mut codec = OpusCodec::new(&config)?;
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut udp_buf = vec![0u8; 4096];
|
||||
|
||||
println!("正在录制到 {}...", path);
|
||||
|
||||
// 这里需要一个停止机制,目前简单起见,如果一段时间没收到包就停止,或者通过全局状态
|
||||
// 为简单演示,我们录制 100 个包
|
||||
for _ in 0..100 {
|
||||
let (packet, _) = socket.recv_packet(&mut udp_buf).await?;
|
||||
let pcm_len = codec.decode(&packet.data, &mut pcm_buf)?;
|
||||
writer.write_samples(&pcm_buf[..pcm_len])?;
|
||||
}
|
||||
|
||||
writer.finalize()?;
|
||||
println!("录制完成: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_wav_to_client(
|
||||
socket: Arc<AudioSocket>,
|
||||
target: SocketAddr,
|
||||
config: AudioConfig,
|
||||
path: &str,
|
||||
) -> Result<()> {
|
||||
let mut reader = WavReader::open(path)?;
|
||||
let mut codec = OpusCodec::new(&config)?;
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut opus_buf = vec![0u8; 4096];
|
||||
|
||||
println!("正在从 {} 推流...", path);
|
||||
|
||||
loop {
|
||||
let n = reader.read_samples(&mut pcm_buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let opus_len = codec.encode(&pcm_buf[..n], &mut opus_buf)?;
|
||||
let packet = AudioPacket {
|
||||
data: opus_buf[..opus_len].to_vec(),
|
||||
};
|
||||
socket.send_packet(&packet, target).await?;
|
||||
|
||||
// 控制发送频率,约 20ms 一帧
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
}
|
||||
|
||||
println!("推流结束");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,57 @@
|
||||
use crate::app::server::core::Server;
|
||||
use crate::audio::config::AudioConfig;
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn run_server() -> Result<()> {
|
||||
println!("Hello, Server!");
|
||||
Ok(())
|
||||
let server = Arc::new(Server::new().await?);
|
||||
let s = server.clone();
|
||||
|
||||
// 运行服务器
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = s.run(53531).await {
|
||||
eprintln!("Server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待一个客户端连接并进行演示
|
||||
println!("等待客户端连接以进行功能演示...");
|
||||
loop {
|
||||
let sessions = server.get_sessions();
|
||||
if !sessions.is_empty() {
|
||||
let addr = sessions[0];
|
||||
println!("开始对 {} 进行功能测试...", addr);
|
||||
|
||||
// 1. 测试 RPC
|
||||
println!("测试 RPC: echo hello");
|
||||
let res = server.call_shell(addr, "echo hello").await?;
|
||||
println!("RPC 结果: {:?}", res);
|
||||
|
||||
// 2. 测试录音
|
||||
println!("测试录制 2 秒音频...");
|
||||
server.start_recording(addr, AudioConfig::voice()).await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
server.stop_recording(addr).await?;
|
||||
println!("录制结束,请检查 temp/recorded.wav");
|
||||
|
||||
// 3. 测试播放 (如果 temp/test.wav 存在)
|
||||
if std::path::Path::new("temp/test.wav").exists() {
|
||||
println!("测试播放 temp/test.wav...");
|
||||
server.start_playback(addr, AudioConfig::voice()).await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
server.stop_playback(addr).await?;
|
||||
println!("播放结束");
|
||||
} else {
|
||||
println!("跳过播放测试 (temp/test.wav 不存在)");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// 保持运行
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod core;
|
||||
pub mod entry;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#![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 = if args[1].to_lowercase() == "master" {
|
||||
"主节点"
|
||||
} else {
|
||||
"从节点"
|
||||
};
|
||||
|
||||
let role = if args[2].to_lowercase() == "left" {
|
||||
ChannelRole::Left
|
||||
} else {
|
||||
ChannelRole::Right
|
||||
};
|
||||
|
||||
println!("🚗 当前为: {} {}", mode, role.to_string());
|
||||
|
||||
if mode == "主节点" {
|
||||
run_master(role).await
|
||||
} else {
|
||||
run_slave(role).await
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
#![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<()> {
|
||||
// 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 = 100_000; // 100ms 基础延迟
|
||||
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;
|
||||
}
|
||||
|
||||
// 计算该帧应当播放的目标时间
|
||||
// target_ts = 数据包发送时间 + 播放延迟
|
||||
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 {
|
||||
// 小于 1ms,直接播放,让播放时机稍微早一点点
|
||||
}
|
||||
}
|
||||
} 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 xiao = ControlPacket::ServerHello {
|
||||
udp_port: audio_socket.local_addr()?.port(),
|
||||
};
|
||||
control.send_packet(&xiao).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(())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod entry;
|
||||
pub mod master;
|
||||
pub mod slave;
|
||||
@@ -1,199 +0,0 @@
|
||||
#![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::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<()> {
|
||||
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 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_millis(200)).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 role_str = role.to_string();
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"/usr/sbin/tts_play.sh \"主节点已连接,{}\" >/dev/null 2>&1",
|
||||
role_str
|
||||
))
|
||||
.status()
|
||||
.await;
|
||||
});
|
||||
|
||||
// 9. 播放主循环
|
||||
let mut pcm_buf = vec![0i16; config.frame_size];
|
||||
let mut last_seq: Option<u32> = None;
|
||||
|
||||
loop {
|
||||
// 检查 TCP 是否已断开
|
||||
if let Ok(_) = disconnect_rx.try_recv() {
|
||||
return Err(anyhow!("主节点已断开: {}", master_tcp_addr));
|
||||
}
|
||||
|
||||
// 接收数据包
|
||||
if let Ok(pkt) = audio_rx.try_recv() {
|
||||
let now = now_us();
|
||||
let current_server_time = clock.lock().await.to_server_time(now);
|
||||
|
||||
// 检查包是否迟到(目标时间已过)
|
||||
if current_server_time > pkt.timestamp {
|
||||
let late_ms = (current_server_time - pkt.timestamp) / 1000;
|
||||
if late_ms > 50 {
|
||||
// 迟到超过 50ms,直接丢弃
|
||||
continue;
|
||||
}
|
||||
// 轻微迟到(<50ms),尝试播放
|
||||
}
|
||||
|
||||
last_seq = Some(pkt.seq);
|
||||
|
||||
// 精确等待到播放时间
|
||||
loop {
|
||||
let now = now_us();
|
||||
let current_server_time_precise = clock.lock().await.to_server_time(now);
|
||||
|
||||
if current_server_time_precise >= pkt.timestamp {
|
||||
break;
|
||||
}
|
||||
|
||||
let wait_us = (pkt.timestamp - current_server_time_precise) as u64;
|
||||
|
||||
if wait_us > 1000 {
|
||||
tokio::time::sleep(Duration::from_micros(wait_us as u64)).await;
|
||||
} else {
|
||||
// 小于 1ms,直接播放,让播放时机稍微早一点点
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 解码并播放
|
||||
if let Ok(len) = codec.decode(&pkt.data, &mut pcm_buf) {
|
||||
let _ = player.write(&pcm_buf[..len]);
|
||||
}
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_micros(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum AudioScene {
|
||||
Music,
|
||||
Voice,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AudioConfig {
|
||||
// ALSA 设备参数,用于录音和播放
|
||||
pub capture_device: String,
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod codec;
|
||||
pub mod config;
|
||||
pub mod player;
|
||||
pub mod recorder;
|
||||
pub mod wav;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||
|
||||
pub struct WavWriter {
|
||||
writer: BufWriter<File>,
|
||||
data_size: u32,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
impl WavWriter {
|
||||
pub fn create(path: &str, sample_rate: u32, channels: u16) -> Result<Self> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
// Header placeholder
|
||||
writer.write_all(&[0u8; 44])?;
|
||||
|
||||
Ok(Self {
|
||||
writer,
|
||||
data_size: 0,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_samples(&mut self, samples: &[i16]) -> Result<()> {
|
||||
for &sample in samples {
|
||||
self.writer.write_all(&sample.to_le_bytes())?;
|
||||
self.data_size += 2;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> Result<()> {
|
||||
self.writer.flush()?;
|
||||
let mut file = self.writer.into_inner()?;
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
|
||||
let file_size = 36 + self.data_size;
|
||||
let byte_rate = self.sample_rate * self.channels as u32 * 2;
|
||||
let block_align = self.channels * 2;
|
||||
|
||||
let mut header = [0u8; 44];
|
||||
header[0..4].copy_from_slice(b"RIFF");
|
||||
header[4..8].copy_from_slice(&file_size.to_le_bytes());
|
||||
header[8..12].copy_from_slice(b"WAVE");
|
||||
header[12..16].copy_from_slice(b"fmt ");
|
||||
header[16..20].copy_from_slice(&16u32.to_le_bytes());
|
||||
header[20..22].copy_from_slice(&1u16.to_le_bytes()); // PCM
|
||||
header[22..24].copy_from_slice(&self.channels.to_le_bytes());
|
||||
header[24..28].copy_from_slice(&self.sample_rate.to_le_bytes());
|
||||
header[28..32].copy_from_slice(&byte_rate.to_le_bytes());
|
||||
header[32..34].copy_from_slice(&block_align.to_le_bytes());
|
||||
header[34..36].copy_from_slice(&16u16.to_le_bytes()); // bits per sample
|
||||
header[36..40].copy_from_slice(b"data");
|
||||
header[40..44].copy_from_slice(&self.data_size.to_le_bytes());
|
||||
|
||||
file.write_all(&header)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WavReader {
|
||||
reader: BufReader<File>,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
pub data_size: u32,
|
||||
}
|
||||
|
||||
impl WavReader {
|
||||
pub fn open(path: &str) -> Result<Self> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut header = [0u8; 44];
|
||||
reader.read_exact(&mut header)?;
|
||||
|
||||
if &header[0..4] != b"RIFF" || &header[8..12] != b"WAVE" {
|
||||
return Err(anyhow::anyhow!("Not a WAV file"));
|
||||
}
|
||||
|
||||
let channels = u16::from_le_bytes([header[22], header[23]]);
|
||||
let sample_rate = u32::from_le_bytes([header[24], header[25], header[26], header[27]]);
|
||||
let data_size = u32::from_le_bytes([header[40], header[41], header[42], header[43]]);
|
||||
|
||||
Ok(Self {
|
||||
reader,
|
||||
sample_rate,
|
||||
channels,
|
||||
data_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_samples(&mut self, samples: &mut [i16]) -> Result<usize> {
|
||||
let mut bytes = vec![0u8; samples.len() * 2];
|
||||
let n = self.reader.read(&mut bytes)?;
|
||||
let sample_count = n / 2;
|
||||
for i in 0..sample_count {
|
||||
samples[i] = i16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]);
|
||||
}
|
||||
Ok(sample_count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,5 @@ async fn main() -> Result<()> {
|
||||
{
|
||||
xiao::app::client::entry::run_client().await?;
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
eprintln!("Only support Linux");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
#[cfg(feature = "app")]
|
||||
{
|
||||
xiao::app::stereo::entry::run_stereo().await?;
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
eprintln!("Only support Linux");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,13 +10,13 @@ pub const DISCOVERY_PORT: u16 = 53530;
|
||||
pub struct Discovery;
|
||||
|
||||
impl Discovery {
|
||||
/// 主节点:启动广播,告知从节点自己的 TCP 端口
|
||||
/// 服务端:启动广播,告知客户端自己的 TCP 端口
|
||||
pub async fn start_broadcast(tcp_port: u16) -> Result<()> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.set_broadcast(true)?;
|
||||
|
||||
let target: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?;
|
||||
let msg = postcard::to_allocvec(&ControlPacket::ServerHello { udp_port: tcp_port })?;
|
||||
let msg = postcard::to_allocvec(&ControlPacket::ServerHello { tcp_port })?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -28,17 +28,17 @@ impl Discovery {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从节点:监听广播,发现主节点的 IP 和 TCP 端口
|
||||
pub async fn discover_master() -> Result<(IpAddr, u16)> {
|
||||
/// 客户端:监听广播,发现服务端的 IP 和 TCP 端口
|
||||
pub async fn discover_server() -> Result<(IpAddr, u16)> {
|
||||
let socket = UdpSocket::bind(format!("0.0.0.0:{}", DISCOVERY_PORT)).await?;
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
loop {
|
||||
let (len, addr) = socket.recv_from(&mut buf).await?;
|
||||
if let Ok(ControlPacket::ServerHello { udp_port }) =
|
||||
if let Ok(ControlPacket::ServerHello { tcp_port }) =
|
||||
postcard::from_bytes::<ControlPacket>(&buf[..len])
|
||||
{
|
||||
return Ok((addr.ip(), udp_port));
|
||||
return Ok((addr.ip(), tcp_port));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ impl AudioSocket {
|
||||
Ok((packet, addr))
|
||||
}
|
||||
|
||||
pub async fn punch(&self, target: SocketAddr) -> Result<()> {
|
||||
self.socket.send_to(&[0u8; 1], target).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clone_inner(&self) -> Arc<UdpSocket> {
|
||||
self.socket.clone()
|
||||
}
|
||||
@@ -56,16 +51,20 @@ impl ControlConnection {
|
||||
|
||||
pub async fn send_packet(&mut self, packet: &ControlPacket) -> Result<()> {
|
||||
let bytes = postcard::to_allocvec(packet)?;
|
||||
let len = bytes.len() as u32;
|
||||
self.stream.write_u32(len).await?;
|
||||
self.stream.write_all(&bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recv_packet(&mut self, buf: &mut [u8]) -> Result<ControlPacket> {
|
||||
let len = self.stream.read(buf).await?;
|
||||
if len == 0 {
|
||||
return Err(anyhow::anyhow!("连接已关闭"));
|
||||
pub async fn recv_packet(&mut self) -> Result<ControlPacket> {
|
||||
let len = self.stream.read_u32().await? as usize;
|
||||
if len > 10 * 1024 * 1024 {
|
||||
return Err(anyhow::anyhow!("Packet too large: {}", len));
|
||||
}
|
||||
let packet = postcard::from_bytes(&buf[..len])?;
|
||||
let mut buf = vec![0u8; len];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
let packet = postcard::from_bytes(&buf)?;
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
@@ -77,19 +76,21 @@ impl ControlConnection {
|
||||
) {
|
||||
self.stream.into_split()
|
||||
}
|
||||
|
||||
pub fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.stream.peer_addr().context("Failed to get peer addr")
|
||||
}
|
||||
}
|
||||
|
||||
/// 主节点网络管理器
|
||||
pub struct MasterNetwork {
|
||||
/// 服务端网络管理器
|
||||
pub struct ServerNetwork {
|
||||
listener: TcpListener,
|
||||
audio: AudioSocket,
|
||||
}
|
||||
|
||||
impl MasterNetwork {
|
||||
impl ServerNetwork {
|
||||
pub async fn setup(port: u16) -> Result<Self> {
|
||||
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
||||
let audio = AudioSocket::bind().await?;
|
||||
Ok(Self { listener, audio })
|
||||
Ok(Self { listener })
|
||||
}
|
||||
|
||||
pub async fn accept(&self) -> Result<(ControlConnection, SocketAddr)> {
|
||||
@@ -97,30 +98,29 @@ impl MasterNetwork {
|
||||
Ok((ControlConnection::new(stream), addr))
|
||||
}
|
||||
|
||||
pub fn audio_socket(&self) -> &AudioSocket {
|
||||
&self.audio
|
||||
pub fn local_addr(&self) -> Result<SocketAddr> {
|
||||
self.listener
|
||||
.local_addr()
|
||||
.context("Failed to get local addr")
|
||||
}
|
||||
}
|
||||
|
||||
/// 从节点网络管理器
|
||||
pub struct SlaveNetwork {
|
||||
/// 客户端网络管理器
|
||||
pub struct ClientNetwork {
|
||||
control: ControlConnection,
|
||||
audio: AudioSocket,
|
||||
}
|
||||
|
||||
impl SlaveNetwork {
|
||||
pub async fn connect(master_addr: SocketAddr) -> Result<Self> {
|
||||
let stream = TcpStream::connect(master_addr)
|
||||
impl ClientNetwork {
|
||||
pub async fn connect(server_addr: SocketAddr) -> Result<Self> {
|
||||
let stream = TcpStream::connect(server_addr)
|
||||
.await
|
||||
.context(format!("无法连接到主节点 TCP 地址: {}", master_addr))?;
|
||||
let audio = AudioSocket::bind().await?;
|
||||
.context(format!("无法连接到服务端 TCP 地址: {}", server_addr))?;
|
||||
Ok(Self {
|
||||
control: ControlConnection::new(stream),
|
||||
audio,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn split(self) -> (ControlConnection, AudioSocket) {
|
||||
(self.control, self.audio)
|
||||
pub fn into_control(self) -> ControlConnection {
|
||||
self.control
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,59 @@
|
||||
use crate::audio::config::AudioConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum ChannelRole {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl ChannelRole {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
ChannelRole::Left => "左声道".to_string(),
|
||||
ChannelRole::Right => "右声道".to_string(),
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DeviceInfo {
|
||||
pub model: String,
|
||||
pub mac: String,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum ControlPacket {
|
||||
// 发现协议
|
||||
// 服务发现
|
||||
ServerHello {
|
||||
udp_port: u16, // UDP 音频流端口
|
||||
tcp_port: u16,
|
||||
},
|
||||
// 握手协议
|
||||
// 握手与认证
|
||||
ClientIdentify {
|
||||
role: ChannelRole,
|
||||
info: DeviceInfo,
|
||||
},
|
||||
// 时间同步 (持续进行)
|
||||
Ping {
|
||||
client_ts: u128,
|
||||
seq: u32,
|
||||
IdentifyOk,
|
||||
|
||||
// 音频控制
|
||||
StartRecording {
|
||||
config: AudioConfig,
|
||||
},
|
||||
Pong {
|
||||
client_ts: u128,
|
||||
server_ts: u128,
|
||||
seq: u32,
|
||||
StopRecording,
|
||||
StartPlayback {
|
||||
config: AudioConfig,
|
||||
},
|
||||
StopPlayback,
|
||||
|
||||
// RPC
|
||||
RpcRequest {
|
||||
id: u32,
|
||||
method: String,
|
||||
args: Vec<String>,
|
||||
},
|
||||
RpcResponse {
|
||||
id: u32,
|
||||
result: RpcResult,
|
||||
},
|
||||
|
||||
// 心跳
|
||||
Ping,
|
||||
Pong,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RpcResult {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub code: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AudioPacket {
|
||||
pub seq: u32, // 序列号,用于丢包检测
|
||||
pub timestamp: u128, // 目标播放时间 (主节点时间)
|
||||
pub data: Vec<u8>, // Opus 编码数据
|
||||
pub data: Vec<u8>, // Opus 编码数据
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user