Audio Overhaul
This commit is contained in:
parent
6241715c57
commit
0a44f36f5d
@ -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×48 kHz 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"
|
||||
|
||||
@ -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 '
|
||||
|
||||
@ -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 (UAC2‑gadget 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 multi‑channel) 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 48 kHz ┌─────────┐ 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!("🔊 eye‑ear #{n}: {} bytes", map.len());
|
||||
}
|
||||
|
||||
let pts_us = buffer
|
||||
.pts()
|
||||
.unwrap_or(gst::ClockTime::ZERO)
|
||||
.nseconds() / 1_000;
|
||||
|
||||
// push non‑blocking; 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),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 ───────────*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user