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'\
|
'\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"
|
'\xc0\xc0' >"$G/functions/hid.usb1/report_desc"
|
||||||
|
|
||||||
# # -- UAC2 Audio
|
# ---------- UAC2 function – speaker + mic, 2×48 kHz stereo ---------
|
||||||
# mkdir -p $G/functions/uac2.usb0
|
mkdir -p "$G/functions/uac2.usb0"
|
||||||
# echo 48000 > $G/functions/uac2.usb0/c_srate
|
cfg="$G/functions/uac2.usb0"
|
||||||
# echo 2 > $G/functions/uac2.usb0/c_ssize
|
echo 48000 >"$cfg/s_spk/freq"
|
||||||
# echo 2 > $G/functions/uac2.usb0/p_chmask
|
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 -----------------------------
|
# ----------------------- configuration -----------------------------
|
||||||
mkdir -p "$G/configs/c.1/strings/0x409"
|
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
|
# 6) Finally bind to first available UDC
|
||||||
ln -s $G/functions/hid.usb0 $G/configs/c.1/
|
ln -s $G/functions/hid.usb0 $G/configs/c.1/
|
||||||
ln -s $G/functions/hid.usb1 $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
|
# 7) Finally bind to first available UDC
|
||||||
echo "$UDC" > "$G/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)}
|
ORIG_USER=${SUDO_USER:-$(id -un)}
|
||||||
|
|
||||||
echo "==> 1a. Base packages"
|
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
|
if ! command -v yay >/dev/null 2>&1; then
|
||||||
echo "==> 1b. installing yay from AUR ..."
|
echo "==> 1b. installing yay from AUR ..."
|
||||||
sudo -u "$ORIG_USER" bash -c '
|
sudo -u "$ORIG_USER" bash -c '
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
|
// server/src/audio.rs
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use gstreamer as gst;
|
use gstreamer as gst;
|
||||||
use gstreamer_app as gst_app;
|
use gstreamer_app as gst_app;
|
||||||
|
use gst::prelude::*;
|
||||||
use lesavka_common::lesavka::AudioPacket;
|
use lesavka_common::lesavka::AudioPacket;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, error, warn};
|
||||||
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";
|
|
||||||
|
|
||||||
|
/// “Speaker” stream coming **from** the remote host (UAC2‑gadget playback
|
||||||
|
/// endpoint) **towards** the client.
|
||||||
pub struct AudioStream {
|
pub struct AudioStream {
|
||||||
_pipe: gst::Pipeline,
|
_pipeline: gst::Pipeline,
|
||||||
inner: ReceiverStream<Result<AudioPacket, Status>>,
|
inner: ReceiverStream<Result<AudioPacket, Status>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for AudioStream {
|
impl Stream for AudioStream {
|
||||||
@ -26,51 +28,90 @@ impl Stream for AudioStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub async fn eye_ear(
|
impl Drop for AudioStream {
|
||||||
// dev: &str,
|
fn drop(&mut self) {
|
||||||
// id: u32,
|
let _ = self._pipeline.set_state(gst::State::Null);
|
||||||
// ) -> 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");
|
|
||||||
|
|
||||||
// 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(
|
pub async fn eye_ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
|
||||||
// gst_app::AppSinkCallbacks::builder()
|
// NB: one *logical* speaker → id==0. A 2nd logical stream could be
|
||||||
// .new_sample(move |s| {
|
// added later (for multi‑channel) without changing the client.
|
||||||
// let samp = s.pull_sample().map_err(|_| gst::FlowError::Eos)?;
|
gst::init().context("gst init")?;
|
||||||
// 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()
|
|
||||||
// );
|
|
||||||
|
|
||||||
// pipe.set_state(gst::State::Playing)?;
|
/*──────────── pipeline description ────────────
|
||||||
// Ok(AudioStream{ _pipe: pipe, inner: ReceiverStream::new(rx) })
|
*
|
||||||
// }
|
* 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 {
|
Ok(AudioStream {
|
||||||
_pipe: gst::Pipeline::new(),
|
_pipeline: pipeline,
|
||||||
inner: ReceiverStream::new(rx), // always empty
|
inner: ReceiverStream::new(rx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,15 +186,17 @@ impl Relay for Handler {
|
|||||||
&self,
|
&self,
|
||||||
req: Request<MonitorRequest>,
|
req: Request<MonitorRequest>,
|
||||||
) -> Result<Response<Self::CaptureAudioStream>, Status> {
|
) -> Result<Response<Self::CaptureAudioStream>, Status> {
|
||||||
// let id = req.into_inner().id;
|
// Only one speaker stream for now; both 0/1 → same ALSA dev.
|
||||||
// let dev = match id { 0 => "/dev/lesavka_l_eye",
|
let _id = req.into_inner().id;
|
||||||
// 1 => "/dev/lesavka_r_eye",
|
// Allow override (`LESAVKA_ALSA_DEV=hw:2,0` for debugging).
|
||||||
// _ => return Err(Status::invalid_argument("monitor id must be 0 or 1")) };
|
let dev = std::env::var("LESAVKA_ALSA_DEV")
|
||||||
// let s = audio::eye_ear(dev, id)
|
.unwrap_or_else(|_| "hw:UAC2Gadget,0".into());
|
||||||
// .await
|
|
||||||
// .map_err(|e| Status::internal(format!("{e:#}")))?;
|
let s = audio::eye_ear(&dev, 0)
|
||||||
// Ok(Response::new(Box::pin(s)))
|
.await
|
||||||
Err(Status::unimplemented("GC311 has no embedded audio"))
|
.map_err(|e| Status::internal(format!("{e:#}")))?;
|
||||||
|
|
||||||
|
Ok(Response::new(Box::pin(s)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*────────────── USB-reset RPC ───────────*/
|
/*────────────── USB-reset RPC ───────────*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user