mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
feat: UAC USB microphone passthrough
- Add UAC1 gadget function (ConfigFS) with optimized endpoint config - Browser mic capture with Opus encoding via WebCodecs AudioEncoder - WebSocket audio transport (Opus 64kbps, 100x bandwidth reduction vs raw PCM) - aplay subprocess for reliable PCM playback to USB gadget - Settings toggle + ActionBar mic button (hidden when UAC disabled) - Dynamic PCM device resolution (/proc/asound) - c_chmask=0 + req_number=4 fixes DWC3 composite isochronous endpoint issue
This commit is contained in:
@@ -11,6 +11,8 @@ pub mod monitor;
|
||||
pub mod recovery;
|
||||
pub mod streamer;
|
||||
pub mod types;
|
||||
pub mod uac_streamer;
|
||||
pub mod uac_websocket;
|
||||
|
||||
pub use capture::{AudioCapturer, AudioConfig, AudioFrame};
|
||||
pub use controller::AudioController;
|
||||
|
||||
210
src/audio/uac_streamer.rs
Normal file
210
src/audio/uac_streamer.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use std::io::Write;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Kill aplay after this much idle time (no incoming audio frames).
|
||||
const IDLE_CLOSE_TIMEOUT_MS: u64 = 2000;
|
||||
|
||||
/// Configuration for the UAC playback stream.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UacPlaybackConfig {
|
||||
pub device_name: String,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
}
|
||||
|
||||
impl Default for UacPlaybackConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_name: crate::otg::uac::find_uac_pcm_device()
|
||||
.unwrap_or_else(crate::otg::uac::uac_pcm_device),
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UacPcmFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub duration_ms: u32,
|
||||
}
|
||||
|
||||
/// Writes PCM to the UAC gadget via `aplay` subprocess — same
|
||||
/// mechanism as the successful manual test: `ffmpeg | aplay hw:0,0`.
|
||||
#[derive(Clone)]
|
||||
pub struct UacPlaybackWriter {
|
||||
pcm_sender: mpsc::Sender<UacPcmFrame>,
|
||||
stop_tx: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
impl UacPlaybackWriter {
|
||||
pub fn start(config: UacPlaybackConfig) -> Result<Self> {
|
||||
let (pcm_sender, pcm_receiver) = mpsc::channel::<UacPcmFrame>(64);
|
||||
let (stop_tx, stop_rx) = watch::channel(false);
|
||||
|
||||
let device = config.device_name;
|
||||
let rate = config.sample_rate;
|
||||
let ch = config.channels;
|
||||
|
||||
let thread_device = device.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("uac-aplay".into())
|
||||
.spawn(move || {
|
||||
Self::playback_loop(&thread_device, rate, ch, pcm_receiver, stop_rx);
|
||||
info!("UAC aplay thread stopped");
|
||||
})
|
||||
.map_err(|e| AppError::Internal(format!("spawn: {e}")))?;
|
||||
|
||||
info!("UAC aplay writer started on {device}");
|
||||
Ok(Self { pcm_sender, stop_tx })
|
||||
}
|
||||
|
||||
pub async fn write(&self, frame: UacPcmFrame) -> Result<()> {
|
||||
self.pcm_sender.send(frame).await
|
||||
.map_err(|_| AppError::Internal("UAC channel closed".into()))
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let _ = self.stop_tx.send(true);
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────
|
||||
|
||||
fn spawn_aplay(device: &str, rate: u32, ch: u16) -> Option<(Child, Box<dyn Write + Send>)> {
|
||||
let mut cmd = Command::new("aplay");
|
||||
cmd.arg("-D").arg(device)
|
||||
.arg("-f").arg("S16_LE")
|
||||
.arg("-r").arg(rate.to_string())
|
||||
.arg("-c").arg(ch.to_string())
|
||||
.arg("-") // stdin
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::inherit()); // → journalctl
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(mut child) => {
|
||||
let stdin = child.stdin.take()?;
|
||||
info!("aplay spawned pid={}", child.id());
|
||||
Some((child, Box::new(stdin)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("aplay spawn failed: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn kill_aplay(mut child: Child, stdin: Box<dyn Write + Send>) {
|
||||
drop(stdin); // close pipe → EOF for aplay
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
fn playback_loop(
|
||||
device: &str,
|
||||
rate: u32,
|
||||
ch: u16,
|
||||
mut receiver: mpsc::Receiver<UacPcmFrame>,
|
||||
mut stop_rx: watch::Receiver<bool>,
|
||||
) {
|
||||
let idle_timeout = Duration::from_millis(IDLE_CLOSE_TIMEOUT_MS);
|
||||
let mut aplay: Option<(Child, Box<dyn Write + Send>)> = None;
|
||||
let mut last_write = std::time::Instant::now();
|
||||
let mut frame_count: u64 = 0;
|
||||
let mut byte_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
// ── wait for frame ──────────────────────────
|
||||
let need_timeout = aplay.is_some()
|
||||
&& last_write.elapsed() >= idle_timeout;
|
||||
let deadline = if need_timeout || aplay.is_none() {
|
||||
Some(std::time::Instant::now() + Duration::from_millis(200))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let frame = loop {
|
||||
if *stop_rx.borrow() { break None; }
|
||||
match receiver.try_recv() {
|
||||
Ok(f) => break Some(f),
|
||||
Err(mpsc::error::TryRecvError::Disconnected) => break None,
|
||||
Err(mpsc::error::TryRecvError::Empty) => {}
|
||||
}
|
||||
if let Some(dl) = deadline {
|
||||
if std::time::Instant::now() >= dl {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
};
|
||||
|
||||
if *stop_rx.borrow() {
|
||||
break;
|
||||
}
|
||||
|
||||
match frame {
|
||||
Some(f) => {
|
||||
last_write = std::time::Instant::now();
|
||||
|
||||
// Ensure aplay is alive
|
||||
if aplay.is_none() {
|
||||
aplay = Self::spawn_aplay(device, rate, ch);
|
||||
}
|
||||
|
||||
if let Some((child, stdin)) = aplay.as_mut() {
|
||||
// Check child health
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
warn!("aplay died: {status}");
|
||||
aplay = Self::spawn_aplay(device, rate, ch);
|
||||
if aplay.is_none() { continue; }
|
||||
}
|
||||
Ok(None) => {} // alive
|
||||
Err(e) => {
|
||||
warn!("aplay wait error: {e}");
|
||||
aplay = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((child, stdin)) = aplay.as_mut() {
|
||||
match stdin.write_all(&f.data) {
|
||||
Ok(()) => {
|
||||
let _ = stdin.flush();
|
||||
frame_count += 1;
|
||||
byte_count += f.data.len() as u64;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("aplay write error: {e}");
|
||||
// aplay died — reap and restart
|
||||
if let Some((c, s)) = aplay.take() {
|
||||
Self::kill_aplay(c, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Timeout — kill aplay
|
||||
if let Some((c, s)) = aplay.take() {
|
||||
Self::kill_aplay(c, s);
|
||||
}
|
||||
if *stop_rx.borrow() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if let Some((c, s)) = aplay.take() {
|
||||
Self::kill_aplay(c, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/audio/uac_websocket.rs
Normal file
133
src/audio/uac_websocket.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use audiopus::coder::Decoder as OpusDecoder;
|
||||
use audiopus::{Channels, SampleRate};
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::uac_streamer::{UacPcmFrame, UacPlaybackWriter};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Binary protocol header for UAC audio input.
|
||||
///
|
||||
/// 0x03 — message type (reverse audio / microphone passthrough)
|
||||
/// timestamp — u32 LE (milliseconds, for future sync)
|
||||
/// duration — u16 LE (frame duration in ms, typically 20)
|
||||
/// sequence — u32 LE (frame counter, for loss detection)
|
||||
/// data_len — u32 LE (Opus payload length in bytes)
|
||||
const UAC_AUDIO_HEADER_SIZE: usize = 15;
|
||||
const UAC_AUDIO_MSG_TYPE: u8 = 0x03;
|
||||
|
||||
/// Accept incoming Opus audio frames over WebSocket and route them
|
||||
/// to the UAC playback writer.
|
||||
pub async fn handle_uac_audio_ws(
|
||||
mut ws: WebSocket,
|
||||
playback: Arc<UacPlaybackWriter>,
|
||||
) {
|
||||
// Create an Opus decoder: 48kHz stereo → PCM S16LE.
|
||||
let mut decoder = match OpusDecoder::new(SampleRate::Hz48000, Channels::Stereo) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Failed to create Opus decoder: {}", e);
|
||||
let _ = ws.send(Message::Close(None)).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("UAC audio WebSocket connected (mic passthrough)");
|
||||
|
||||
while let Some(msg) = ws.recv().await {
|
||||
let msg = match msg {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!("UAC WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::Binary(data) => {
|
||||
if data.len() < UAC_AUDIO_HEADER_SIZE {
|
||||
warn!(
|
||||
"UAC audio frame too short: {} bytes (min {})",
|
||||
data.len(),
|
||||
UAC_AUDIO_HEADER_SIZE
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let msg_type = data[0];
|
||||
if msg_type == 0x04 {
|
||||
// Raw PCM passthrough — no Opus decode needed.
|
||||
// Useful for testing and for clients that encode locally.
|
||||
let duration = u16::from_le_bytes([data[5], data[6]]);
|
||||
let data_len = u32::from_le_bytes([data[11], data[12], data[13], data[14]]) as usize;
|
||||
if data.len() < UAC_AUDIO_HEADER_SIZE + data_len {
|
||||
warn!("UAC PCM frame truncated");
|
||||
continue;
|
||||
}
|
||||
let pcm_bytes = &data[UAC_AUDIO_HEADER_SIZE..UAC_AUDIO_HEADER_SIZE + data_len];
|
||||
if let Err(e) = playback
|
||||
.write(super::uac_streamer::UacPcmFrame {
|
||||
data: pcm_bytes.to_vec(),
|
||||
duration_ms: duration as u32,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("Failed to send UAC PCM frame: {}", e);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if msg_type != UAC_AUDIO_MSG_TYPE {
|
||||
warn!("UAC unknown msg type: 0x{msg_type:02x}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let duration = u16::from_le_bytes([data[5], data[6]]);
|
||||
let data_len = u32::from_le_bytes([data[11], data[12], data[13], data[14]]) as usize;
|
||||
|
||||
if data.len() < UAC_AUDIO_HEADER_SIZE + data_len {
|
||||
warn!("UAC audio frame truncated");
|
||||
continue;
|
||||
}
|
||||
|
||||
let opus_payload = &data[UAC_AUDIO_HEADER_SIZE..UAC_AUDIO_HEADER_SIZE + data_len];
|
||||
|
||||
let frame_samples = (48000u32 * duration as u32 / 1000) as usize * 2; // 2 channels
|
||||
let mut pcm_i16 = vec![0i16; frame_samples];
|
||||
|
||||
match decoder.decode(Some(opus_payload), &mut pcm_i16, false) {
|
||||
Ok(decoded) => {
|
||||
// Convert i16 → bytes (S16LE interleaved)
|
||||
let pcm_bytes: Vec<u8> = pcm_i16[..decoded]
|
||||
.iter()
|
||||
.flat_map(|s| s.to_le_bytes())
|
||||
.collect();
|
||||
if let Err(e) = playback
|
||||
.write(UacPcmFrame {
|
||||
data: pcm_bytes,
|
||||
duration_ms: duration as u32,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("Failed to send UAC PCM frame: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Opus decode error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Ping(_) | Message::Pong(_) => {}
|
||||
Message::Close(_) => {
|
||||
debug!("UAC audio WebSocket closing");
|
||||
break;
|
||||
}
|
||||
Message::Text(_) => {
|
||||
// Ignore text messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("UAC audio WebSocket disconnected");
|
||||
}
|
||||
Reference in New Issue
Block a user