mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-11-01 18:30:51 +08:00
154 lines
5.4 KiB
Rust
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
|
|
}
|