Audio Overhaul

This commit is contained in:
Brad Stein 2025-06-29 22:57:54 -05:00
parent 6241715c57
commit 0a44f36f5d
4 changed files with 129 additions and 65 deletions

View File

@ -93,11 +93,15 @@ printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00'\
'\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\
'\xc0\xc0' >"$G/functions/hid.usb1/report_desc"
# # -- UAC2 Audio
# mkdir -p $G/functions/uac2.usb0
# echo 48000 > $G/functions/uac2.usb0/c_srate
# echo 2 > $G/functions/uac2.usb0/c_ssize
# echo 2 > $G/functions/uac2.usb0/p_chmask
# ---------- UAC2 function speaker + mic, 2×48kHz stereo ---------
mkdir -p "$G/functions/uac2.usb0"
cfg="$G/functions/uac2.usb0"
echo 48000 >"$cfg/s_spk/freq"
echo 2 >"$cfg/s_spk/chs" # L+R
echo 16 >"$cfg/s_spk/strm_maxpacket"
echo 48000 >"$cfg/c_mic/freq"
echo 2 >"$cfg/c_mic/chs"
echo 16 >"$cfg/c_mic/strm_maxpacket"
# ----------------------- configuration -----------------------------
mkdir -p "$G/configs/c.1/strings/0x409"
@ -107,8 +111,12 @@ echo "Config 1" > "$G/configs/c.1/strings/0x409/configuration"
# 6) Finally bind to first available UDC
ln -s $G/functions/hid.usb0 $G/configs/c.1/
ln -s $G/functions/hid.usb1 $G/configs/c.1/
# ln -s $G/functions/uac2.usb0 $G/configs/c.1/
ln -sf "$cfg" "$G/configs/c.1/"
# 7) Finally bind to first available UDC
echo "$UDC" > "$G/UDC"
echo "[lesavka-core] 🎉 gadget ready on $UDC (keyboard: hidg0, mouse: hidg1)"
echo "[lesavka-core] 🎉 gadget ready"
echo "⌨️ hidg0 gadget ready on controller $UDC"
echo "🖱️ hidg1 gadget ready on controller $UDC"
echo "🎧 UAC2 gadget ready on controller $UDC"
echo "🎤 UAC2 gadget ready on controller $UDC"

View File

@ -4,7 +4,20 @@ set -euo pipefail
ORIG_USER=${SUDO_USER:-$(id -un)}
echo "==> 1a. Base packages"
sudo pacman -Syq --needed --noconfirm git rustup protobuf gcc pipewire pipewire-pulse tailscale base-devel gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav
sudo pacman -Syq --needed --noconfirm git \
rustup \
protobuf \
gcc \
pipewire \
pipewire-pulse \
tailscale \
base-devel \
gstreamer \
gst-plugins-base \
gst-plugins-good \
gst-plugins-bad \
gst-plugins-ugly \
gst-libav
if ! command -v yay >/dev/null 2>&1; then
echo "==> 1b. installing yay from AUR ..."
sudo -u "$ORIG_USER" bash -c '

View File

@ -1,18 +1,20 @@
// server/src/audio.rs
#![forbid(unsafe_code)]
use anyhow::Context;
use futures_util::Stream;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gst::prelude::*;
use lesavka_common::lesavka::AudioPacket;
use tokio_stream::wrappers::ReceiverStream;
use tonic::Status;
use tracing::{debug, trace};
use gst::prelude::*;
const EAR_ID: [&str; 2] = ["l", "r"];
const PIPE: &str = "appsrc name=audsrc is-live=true do-timestamp=true ! aacparse ! queue ! appsink name=asink emit-signals=true";
use tracing::{debug, error, warn};
/// “Speaker” stream coming **from** the remote host (UAC2gadget playback
/// endpoint) **towards** the client.
pub struct AudioStream {
_pipe: gst::Pipeline,
_pipeline: gst::Pipeline,
inner: ReceiverStream<Result<AudioPacket, Status>>,
}
@ -26,51 +28,90 @@ impl Stream for AudioStream {
}
}
// pub async fn eye_ear(
// dev: &str,
// id: u32,
// ) -> anyhow::Result<AudioStream> {
// let ear = EAR_ID[id as usize];
// gst::init().context("gst init")?;
// let desc = format!(
// "v4l2src device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
// video/mpegts,systemstream=true,packetsize=188 ! \
// tsdemux name=d d.audio_0 ! queue ! \
// aacparse ! queue ! \
// appsink name=asink emit-signals=true max-buffers=64 drop=true"
// );
// let pipe: gst::Pipeline = gst::parse::launch(&desc)?
// .downcast()
// .expect("pipeline");
impl Drop for AudioStream {
fn drop(&mut self) {
let _ = self._pipeline.set_state(gst::State::Null);
}
}
// let sink = pipe.by_name("asink").expect("asink").downcast::<gst_app::AppSink>().unwrap();
// let (tx, rx) = tokio::sync::mpsc::channel(8192);
/*───────────────────────────────────────────────────────────────────────────*/
/* eye_ear() capture from ALSA (“speaker”) and push AAC AUs via gRPC */
/*───────────────────────────────────────────────────────────────────────────*/
// sink.set_callbacks(
// gst_app::AppSinkCallbacks::builder()
// .new_sample(move |s| {
// let samp = s.pull_sample().map_err(|_| gst::FlowError::Eos)?;
// let buf = samp.buffer().ok_or(gst::FlowError::Error)?;
// let map = buf.map_readable().map_err(|_| gst::FlowError::Error)?;
// let pts = buf.pts().unwrap_or(gst::ClockTime::ZERO).nseconds()/1_000;
// let pkt = AudioPacket { id, pts, data: map.as_slice().to_vec() };
// debug!(target:"lesavka_server::audio",
// eye = id, bytes = pkt.data.len(), pts = pkt.pts,
// "⬆️ pushed audio sample ear-{ear}");
// let _ = tx.try_send(Ok(pkt));
// Ok(gst::FlowSuccess::Ok)
// }).build()
// );
pub async fn eye_ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
// NB: one *logical* speaker → id==0. A 2nd logical stream could be
// added later (for multichannel) without changing the client.
gst::init().context("gst init")?;
// pipe.set_state(gst::State::Playing)?;
// Ok(AudioStream{ _pipe: pipe, inner: ReceiverStream::new(rx) })
// }
/*──────────── pipeline description ────────────
*
* ALSA (UAC2 gadget) AAC+ADTS AppSink
* raw 48kHz AU/ADTS
* alsasrc voaacenc appsink
*
*/
let desc = format!(
"alsasrc name=audsrc device=\"{alsa_dev}\" do-timestamp=true ! \
audio/x-raw,channels=2,rate=48000 ! \
voaacenc bitrate=192000 ! aacparse ! queue ! \
appsink name=asink emit-signals=true max-buffers=64 drop=true"
);
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
.downcast()
.expect("pipeline");
let sink: gst_app::AppSink = pipeline
.by_name("asink")
.expect("asink")
.downcast()
.expect("appsink");
let (tx, rx) = tokio::sync::mpsc::channel(8192);
/*──────────── callbacks ────────────*/
sink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample(move |s| {
let sample = s.pull_sample().map_err(|_| gst::FlowError::Eos)?;
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
static CNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n < 10 || n % 300 == 0 {
debug!("🔊 eyeear #{n}: {}bytes", map.len());
}
let pts_us = buffer
.pts()
.unwrap_or(gst::ClockTime::ZERO)
.nseconds() / 1_000;
// push nonblocking; drop oldest on overflow
if tx.try_send(Ok(AudioPacket {
id,
pts: pts_us,
data: map.as_slice().to_vec(),
})).is_err() {
static DROPS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let d = DROPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if d % 300 == 0 {
warn!("🔊💔 dropped {d} audio AUs (client too slow)");
}
}
Ok(gst::FlowSuccess::Ok)
})
.build(),
);
pipeline.set_state(gst::State::Playing)
.context("starting audio pipeline")?;
pub async fn eye_ear(_dev: &str, _id: u32) -> anyhow::Result<AudioStream> {
use tokio_stream::StreamExt;
let (_tx, rx) = tokio::sync::mpsc::channel(1);
Ok(AudioStream {
_pipe: gst::Pipeline::new(),
inner: ReceiverStream::new(rx), // always empty
_pipeline: pipeline,
inner: ReceiverStream::new(rx),
})
}

View File

@ -186,15 +186,17 @@ impl Relay for Handler {
&self,
req: Request<MonitorRequest>,
) -> Result<Response<Self::CaptureAudioStream>, Status> {
// let id = req.into_inner().id;
// let dev = match id { 0 => "/dev/lesavka_l_eye",
// 1 => "/dev/lesavka_r_eye",
// _ => return Err(Status::invalid_argument("monitor id must be 0 or 1")) };
// let s = audio::eye_ear(dev, id)
// .await
// .map_err(|e| Status::internal(format!("{e:#}")))?;
// Ok(Response::new(Box::pin(s)))
Err(Status::unimplemented("GC311 has no embedded audio"))
// Only one speaker stream for now; both 0/1 → same ALSA dev.
let _id = req.into_inner().id;
// Allow override (`LESAVKA_ALSA_DEV=hw:2,0` for debugging).
let dev = std::env::var("LESAVKA_ALSA_DEV")
.unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
let s = audio::eye_ear(&dev, 0)
.await
.map_err(|e| Status::internal(format!("{e:#}")))?;
Ok(Response::new(Box::pin(s)))
}
/*────────────── USB-reset RPC ───────────*/