Files
One-KVM/src/otg/uac.rs
2026-07-26 11:59:58 +08:00

154 lines
5.4 KiB
Rust

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 1.0 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())?;
// 16-bit S16LE.
write_file(&func_path.join("p_ssize"), "2")?;
// One decibel per step. The kernel default is 1/256 dB, which creates
// 25,600 control values and triggers a UAC volume-range warning.
write_file(&func_path.join("p_volume_res"), "256")?;
// 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")?;
write_file(&func_path.join("c_volume_present"), "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
}