mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +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:
@@ -47,6 +47,7 @@ pub struct OtgGadgetManager {
|
||||
hid_instance: u8,
|
||||
msd_instance: u8,
|
||||
network_instance: u8,
|
||||
uac_instance: u8,
|
||||
functions: Vec<Box<dyn GadgetFunction>>,
|
||||
bound_udc: Option<String>,
|
||||
created_by_us: bool,
|
||||
@@ -73,6 +74,7 @@ impl OtgGadgetManager {
|
||||
hid_instance: 0,
|
||||
msd_instance: 0,
|
||||
network_instance: 0,
|
||||
uac_instance: 0,
|
||||
functions: Vec::with_capacity(4),
|
||||
bound_udc: None,
|
||||
created_by_us: false,
|
||||
@@ -148,6 +150,14 @@ impl OtgGadgetManager {
|
||||
Ok(func_clone)
|
||||
}
|
||||
|
||||
pub fn add_uac(&mut self, sample_rate: u32, channels: u8) -> Result<super::uac::UacFunction> {
|
||||
let func = super::uac::UacFunction::new(self.uac_instance, sample_rate, channels)?;
|
||||
let func_clone = func.clone();
|
||||
self.add_function(Box::new(func))?;
|
||||
self.uac_instance += 1;
|
||||
Ok(func_clone)
|
||||
}
|
||||
|
||||
fn add_function(&mut self, func: Box<dyn GadgetFunction>) -> Result<()> {
|
||||
self.functions.push(func);
|
||||
Ok(())
|
||||
|
||||
@@ -18,6 +18,8 @@ pub mod report_desc;
|
||||
pub mod self_check;
|
||||
#[cfg(unix)]
|
||||
pub mod service;
|
||||
#[cfg(unix)]
|
||||
pub mod uac;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use manager::{wait_for_hid_devices, OtgGadgetManager};
|
||||
@@ -26,7 +28,9 @@ pub use msd::{MsdFunction, MsdLunConfig};
|
||||
#[cfg(unix)]
|
||||
pub use network::NetworkFunction;
|
||||
#[cfg(unix)]
|
||||
pub use service::{HidDevicePaths, OtgNetworkStatus, OtgRuntimeHealth, OtgService};
|
||||
pub use service::{HidDevicePaths, OtgNetworkStatus, OtgRuntimeHealth, OtgService, UacConfig};
|
||||
#[cfg(unix)]
|
||||
pub use uac::UacFunction;
|
||||
|
||||
/// List USB Device Controller names exposed by sysfs.
|
||||
pub fn list_udc_devices() -> Vec<String> {
|
||||
|
||||
@@ -12,6 +12,36 @@ use crate::config::{
|
||||
};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Configuration for the USB Audio Class (UAC) gadget function.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct UacConfig {
|
||||
/// Enable the virtual USB microphone.
|
||||
pub enabled: bool,
|
||||
/// Sample rate in Hz (e.g. 48000).
|
||||
pub sample_rate: u32,
|
||||
/// Number of channels (1=mono, 2=stereo).
|
||||
pub channels: u8,
|
||||
}
|
||||
|
||||
impl UacConfig {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.sample_rate != 0 && (self.sample_rate < 8000 || self.sample_rate > 384000) {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"UAC sample rate {} out of range (8000-384000)",
|
||||
self.sample_rate
|
||||
)));
|
||||
}
|
||||
if self.channels != 0 && self.channels > 8 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"UAC channel count {} out of range (1-8)",
|
||||
self.channels
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HidDevicePaths {
|
||||
pub keyboard: Option<PathBuf>,
|
||||
@@ -62,6 +92,7 @@ pub(crate) struct OtgDesiredState {
|
||||
pub msd_enabled: bool,
|
||||
pub msd_lun_capacity: u8,
|
||||
pub network: OtgNetworkConfig,
|
||||
pub uac_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for OtgDesiredState {
|
||||
@@ -74,6 +105,7 @@ impl Default for OtgDesiredState {
|
||||
msd_enabled: false,
|
||||
msd_lun_capacity: 1,
|
||||
network: OtgNetworkConfig::default(),
|
||||
uac_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,8 +115,10 @@ impl OtgDesiredState {
|
||||
hid: &HidConfig,
|
||||
msd: &MsdConfig,
|
||||
network: &OtgNetworkConfig,
|
||||
uac: &UacConfig,
|
||||
) -> Result<Self> {
|
||||
network.validate()?;
|
||||
uac.validate()?;
|
||||
let hid_functions = if hid.backend == HidBackend::Otg {
|
||||
let functions = hid.constrained_otg_functions();
|
||||
Some(functions)
|
||||
@@ -93,7 +127,8 @@ impl OtgDesiredState {
|
||||
};
|
||||
|
||||
hid.validate_otg_functions()?;
|
||||
let needs_udc = hid_functions.is_some() || msd.enabled || network.enabled;
|
||||
let needs_udc =
|
||||
hid_functions.is_some() || msd.enabled || network.enabled || uac.enabled;
|
||||
let udc = if needs_udc {
|
||||
hid.otg_udc
|
||||
.as_ref()
|
||||
@@ -111,6 +146,7 @@ impl OtgDesiredState {
|
||||
msd_enabled: msd.enabled,
|
||||
msd_lun_capacity: 1,
|
||||
network: network.clone(),
|
||||
uac_enabled: uac.enabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,6 +169,7 @@ struct OtgServiceState {
|
||||
pub msd_enabled: bool,
|
||||
pub msd_lun_capacity: u8,
|
||||
pub network: OtgNetworkConfig,
|
||||
pub uac_enabled: bool,
|
||||
pub configured_udc: Option<String>,
|
||||
pub hid_paths: Option<HidDevicePaths>,
|
||||
pub hid_functions: Option<OtgHidFunctions>,
|
||||
@@ -150,6 +187,7 @@ impl Default for OtgServiceState {
|
||||
msd_enabled: false,
|
||||
msd_lun_capacity: 1,
|
||||
network: OtgNetworkConfig::default(),
|
||||
uac_enabled: false,
|
||||
configured_udc: None,
|
||||
hid_paths: None,
|
||||
hid_functions: None,
|
||||
@@ -215,6 +253,7 @@ impl OtgService {
|
||||
hid: &HidConfig,
|
||||
msd: &MsdConfig,
|
||||
network: &OtgNetworkConfig,
|
||||
uac: &UacConfig,
|
||||
) -> Result<()> {
|
||||
if !self.recovery_checked.load(Ordering::SeqCst) {
|
||||
if let Err(error) = NetworkBridgeRuntime::recover_stale_transaction() {
|
||||
@@ -226,7 +265,7 @@ impl OtgService {
|
||||
}
|
||||
let previous = self.desired.read().await.clone();
|
||||
let desired = self
|
||||
.desired_from_config_preserving_runtime(hid, msd, network)
|
||||
.desired_from_config_preserving_runtime(hid, msd, network, uac)
|
||||
.await?;
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
@@ -269,8 +308,9 @@ impl OtgService {
|
||||
hid: &HidConfig,
|
||||
msd: &MsdConfig,
|
||||
network: &OtgNetworkConfig,
|
||||
uac: &UacConfig,
|
||||
) -> Result<OtgDesiredState> {
|
||||
let mut desired = OtgDesiredState::from_config(hid, msd, network)?;
|
||||
let mut desired = OtgDesiredState::from_config(hid, msd, network, uac)?;
|
||||
desired.msd_lun_capacity = self.desired.read().await.msd_lun_capacity;
|
||||
Ok(desired)
|
||||
}
|
||||
@@ -305,10 +345,11 @@ impl OtgService {
|
||||
let desired = self.desired.read().await.clone();
|
||||
|
||||
debug!(
|
||||
"Reconciling OTG gadget: HID={}, MSD={}, NET={}, UDC={:?}",
|
||||
"Reconciling OTG gadget: HID={}, MSD={}, NET={}, UAC={}, UDC={:?}",
|
||||
desired.hid_enabled(),
|
||||
desired.msd_enabled,
|
||||
desired.network_enabled(),
|
||||
desired.uac_enabled,
|
||||
desired.udc
|
||||
);
|
||||
|
||||
@@ -320,6 +361,7 @@ impl OtgService {
|
||||
&& state.msd_enabled == desired.msd_enabled
|
||||
&& state.msd_lun_capacity == desired.msd_lun_capacity
|
||||
&& state.network == desired.network
|
||||
&& state.uac_enabled == desired.uac_enabled
|
||||
&& state.configured_udc == desired.udc
|
||||
&& state.hid_functions == desired.hid_functions
|
||||
&& state.keyboard_leds_enabled == desired.keyboard_leds
|
||||
@@ -359,6 +401,7 @@ impl OtgService {
|
||||
state.msd_enabled = false;
|
||||
state.msd_lun_capacity = 1;
|
||||
state.network = OtgNetworkConfig::default();
|
||||
state.uac_enabled = false;
|
||||
state.configured_udc = None;
|
||||
state.hid_paths = None;
|
||||
state.hid_functions = None;
|
||||
@@ -393,6 +436,20 @@ impl OtgService {
|
||||
);
|
||||
|
||||
let mut hid_paths = None;
|
||||
// Add UAC BEFORE HID so the isochronous endpoint gets a
|
||||
// lower hardware endpoint number. DWC3 seems to have
|
||||
// trouble with isochronous transfers on higher-numbered
|
||||
// endpoints when they follow interrupt endpoints.
|
||||
let _uac_func = if desired.uac_enabled {
|
||||
let sample_rate: u32 = 48000;
|
||||
let channels: u8 = 2;
|
||||
Some(manager.add_uac(sample_rate, channels).map_err(|e| {
|
||||
AppError::Internal(format!("Failed to add UAC function: {e}"))
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(hid_functions) = desired.hid_functions.clone() {
|
||||
let mut paths = HidDevicePaths {
|
||||
udc: Some(udc.clone()),
|
||||
@@ -537,6 +594,7 @@ impl OtgService {
|
||||
state.msd_enabled = desired.msd_enabled;
|
||||
state.msd_lun_capacity = desired.msd_lun_capacity;
|
||||
state.network = desired.network.clone();
|
||||
state.uac_enabled = desired.uac_enabled;
|
||||
state.configured_udc = Some(udc);
|
||||
state.hid_paths = hid_paths;
|
||||
state.hid_functions = desired.hid_functions;
|
||||
@@ -642,6 +700,7 @@ mod tests {
|
||||
&HidConfig::default(),
|
||||
&MsdConfig::default(),
|
||||
&OtgNetworkConfig::default(),
|
||||
&UacConfig::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
148
src/otg/uac.rs
Normal file
148
src/otg/uac.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::configfs::{create_dir, create_symlink, remove_dir, write_file};
|
||||
use super::function::GadgetFunction;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// USB Audio Class 2.0 (UAC1) gadget function.
|
||||
///
|
||||
/// Creates a virtual USB microphone that the USB host sees as a standard
|
||||
/// USB audio input device. Audio written to the PCM playback device on the
|
||||
/// gadget side appears as microphone input on the host.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UacFunction {
|
||||
name: String,
|
||||
sample_rate: u32,
|
||||
channels: u8,
|
||||
}
|
||||
|
||||
impl UacFunction {
|
||||
/// Create a new UAC1 function instance.
|
||||
///
|
||||
/// `instance` is a zero-based index to avoid name collisions
|
||||
/// (e.g. `uac2.usb0`).
|
||||
pub fn new(instance: u8, sample_rate: u32, channels: u8) -> Result<Self> {
|
||||
if sample_rate == 0 || sample_rate > 384_000 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"invalid UAC sample rate: {sample_rate}"
|
||||
)));
|
||||
}
|
||||
if channels == 0 || channels > 8 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"invalid UAC channel count: {channels}"
|
||||
)));
|
||||
}
|
||||
Ok(Self {
|
||||
name: format!("uac1.usb{instance}"),
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
fn function_path(&self, gadget_path: &Path) -> PathBuf {
|
||||
gadget_path.join("functions").join(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl GadgetFunction for UacFunction {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn create(&self, gadget_path: &Path) -> Result<()> {
|
||||
let func_path = self.function_path(gadget_path);
|
||||
create_dir(&func_path)?;
|
||||
|
||||
// Playback direction (gadget → host): the controlled machine sees
|
||||
// this as a microphone/line-in input.
|
||||
let chmask: u32 = (1u32 << self.channels) - 1;
|
||||
write_file(&func_path.join("p_chmask"), &chmask.to_string())?;
|
||||
write_file(&func_path.join("p_srate"), &self.sample_rate.to_string())?;
|
||||
write_file(&func_path.join("p_ssize"), "2")?; // 16-bit S16LE
|
||||
// UAC1 does not need p_hs_bint — Windows has native built-in
|
||||
// UAC1 drivers and handles isochronous streaming automatically.
|
||||
|
||||
// Only enable playback direction (gadget → host = mic).
|
||||
// Disabling capture saves one isochronous endpoint.
|
||||
write_file(&func_path.join("c_chmask"), "0")?;
|
||||
|
||||
// req_number=4: explicitly allocate 4 USB requests for the
|
||||
// isochronous endpoint. Default (0 = auto) may not be enough
|
||||
// for composite gadgets on DWC3.
|
||||
let _ = write_file(&func_path.join("req_number"), "4");
|
||||
|
||||
debug!(
|
||||
"UAC1 function {} created: {}ch {}Hz",
|
||||
&self.name, self.channels, self.sample_rate
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn link(&self, config_path: &Path, gadget_path: &Path) -> Result<()> {
|
||||
let func_path = self.function_path(gadget_path);
|
||||
let link_path = config_path.join(&self.name);
|
||||
create_symlink(&func_path, &link_path)?;
|
||||
debug!("UAC1 function {} linked into configuration", &self.name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unlink(&self, config_path: &Path) -> Result<()> {
|
||||
let link_path = config_path.join(&self.name);
|
||||
if link_path.exists() {
|
||||
std::fs::remove_file(&link_path).map_err(|e| {
|
||||
AppError::Internal(format!(
|
||||
"Failed to unlink UAC1 function {}: {}",
|
||||
&self.name, e
|
||||
))
|
||||
})?;
|
||||
debug!("UAC1 function {} unlinked", &self.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup(&self, gadget_path: &Path) -> Result<()> {
|
||||
let func_path = self.function_path(gadget_path);
|
||||
if func_path.exists() {
|
||||
remove_dir(&func_path).map_err(|e| {
|
||||
AppError::Internal(format!(
|
||||
"Failed to remove UAC1 function {}: {}",
|
||||
&self.name, e
|
||||
))
|
||||
})?;
|
||||
info!("UAC1 function {} removed", &self.name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the ALSA PCM device name that the kernel assigns to a UAC1
|
||||
/// gadget after binding. The device appears as a playback-only PCM on
|
||||
/// the gadget side.
|
||||
pub fn uac_pcm_device() -> String {
|
||||
// The kernel assigns the card name based on the gadget name.
|
||||
// The PCM name is typically "playback" for UAC1.
|
||||
"hw:UAC1Gadget,0".to_string()
|
||||
}
|
||||
|
||||
/// Resolve the actual PCM device name for a UAC1 playback device
|
||||
/// by scanning /proc/asound/ for the gadget audio card.
|
||||
pub fn find_uac_pcm_device() -> Option<String> {
|
||||
for entry in std::fs::read_dir("/proc/asound").ok()? {
|
||||
let entry = entry.ok()?;
|
||||
let name = entry.file_name();
|
||||
let name = name.to_str()?;
|
||||
if !name.starts_with("card") {
|
||||
continue;
|
||||
}
|
||||
let card_path = entry.path().join("id");
|
||||
if let Ok(id) = std::fs::read_to_string(&card_path) {
|
||||
if id.trim().starts_with("UAC1Gadget") || id.trim().starts_with("gadget") {
|
||||
let card_num = name.strip_prefix("card")?;
|
||||
return Some(format!("hw:{card_num},0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user