use blit_remote::{AUDIO_FRAME_CODEC_OPUS, S2C_AUDIO_FRAME};
use opus::{Application, Channels, Encoder as OpusEncoder};
use std::collections::VecDeque;
use std::io::BufRead;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc;
fn pdeathsig_hook() -> impl FnMut() -> std::io::Result<()> {
|| unsafe {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
}
const FRAME_SAMPLES: usize = 960;
const CHANNELS: usize = 2;
const FRAME_FLOATS: usize = FRAME_SAMPLES * CHANNELS;
const MAX_OPUS_PACKET: usize = 4000;
pub const DEFAULT_BITRATE: i32 = 64_000;
const RING_CAPACITY: usize = 10;
const HEAL_COOLDOWN: Duration = Duration::from_secs(1);
const MAX_HEALS: u32 = 5;
const HEAL_WINDOW: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct OpusFrame {
pub timestamp: u32,
pub data: Vec<u8>,
}
pub struct AudioPipeline {
dbus_child: Child,
pipewire_child: Child,
wireplumber_child: Option<Child>,
pipewire_pulse_child: Child,
pw_cat_child: Child,
opus_rx: mpsc::Receiver<OpusFrame>,
ring: VecDeque<OpusFrame>,
pub runtime_dir: PathBuf,
alive: bool,
bitrate_tx: tokio::sync::watch::Sender<i32>,
encoder_alive: Arc<AtomicBool>,
dbus_address: String,
opus_tx: mpsc::Sender<OpusFrame>,
verbose: bool,
epoch: Instant,
last_heal: Option<Instant>,
first_heal_at: Option<Instant>,
heals: u32,
}
const PIPEWIRE_CONF_TEMPLATE: &str = r#"
context.properties = {
core.daemon = true
core.name = pipewire-0
default.clock.rate = 48000
}
context.spa-libs = {
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
{ name = libpipewire-module-protocol-native }
{ name = libpipewire-module-access }
{ name = libpipewire-module-client-node }
{ name = libpipewire-module-adapter }
{ name = libpipewire-module-link-factory }
{ name = libpipewire-module-metadata }
{ name = libpipewire-module-spa-node-factory }
]
context.objects = [
{ factory = adapter
args = {
factory.name = support.null-audio-sink
node.name = blit-sink
media.class = Audio/Sink
object.linger = true
audio.position = [ FL FR ]
audio.rate = 48000
monitor.channel-volumes = true
monitor.passthrough = true
}
}
]
"#;
const WIREPLUMBER_CONF_TEMPLATE: &str = r#"
wireplumber.profiles = {
main = {
support.dbus = disabled
support.portal-permissionstore = disabled
support.reserve-device = disabled
# hardware.audio stays enabled — its policy.node links streams to sinks.
hardware.bluetooth = disabled
hardware.video-capture = disabled
monitor.alsa = disabled
monitor.alsa.reserve-device = disabled
monitor.bluez = disabled
monitor.bluez.midi = disabled
monitor.bluez.seat-monitoring = disabled
monitor.libcamera = disabled
monitor.v4l2 = disabled
}
}
"#;
fn find_program(name: &str) -> Option<PathBuf> {
let path_var = std::env::var("PATH").unwrap_or_default();
for dir in path_var.split(':') {
let candidate = Path::new(dir).join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub fn pipewire_available() -> bool {
missing_pipewire_binaries().is_empty()
}
pub fn missing_pipewire_binaries() -> Vec<&'static str> {
["pipewire", "pipewire-pulse", "pw-cat", "dbus-daemon"]
.into_iter()
.filter(|name| find_program(name).is_none())
.collect()
}
fn wait_for_socket(path: &Path, timeout: std::time::Duration) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if path.exists() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
false
}
fn lookup_sink_serial(runtime_dir: &Path) -> Option<String> {
let pipewire_remote = runtime_dir.join("pipewire-0");
let output = Command::new("pw-cli")
.args(["ls", "Node"])
.env("PIPEWIRE_REMOTE", &pipewire_remote)
.env("XDG_RUNTIME_DIR", runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let mut serial = None;
let mut in_blit_sink = false;
for line in text.lines() {
if line.contains("node.name") && line.contains("blit-sink") {
in_blit_sink = true;
} else if in_blit_sink && line.contains("object.serial") {
serial = line.split('"').nth(1).map(|s| s.to_string());
break;
} else if line.starts_with('\t') && line.contains("id ") {
in_blit_sink = false;
}
}
if serial.is_some() {
return serial;
}
let mut current_serial = None;
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("object.serial = \"") {
current_serial = rest.strip_suffix('"').map(|s| s.to_string());
}
if trimmed.contains("node.name") && trimmed.contains("blit-sink") {
return current_serial;
}
}
None
}
impl AudioPipeline {
pub fn spawn(
runtime_dir: &Path,
instance_id: u16,
bitrate: i32,
verbose: bool,
epoch: Instant,
) -> Result<Self, String> {
let audio_dir = runtime_dir.join(format!("blit-audio-{instance_id}"));
if audio_dir.exists() {
let _ = std::fs::remove_dir_all(&audio_dir);
}
std::fs::create_dir_all(&audio_dir)
.map_err(|e| format!("failed to create audio runtime dir: {e}"))?;
let conf_dir = audio_dir.join("pipewire");
std::fs::create_dir_all(&conf_dir)
.map_err(|e| format!("failed to create PipeWire config dir: {e}"))?;
let conf_path = conf_dir.join("pipewire.conf");
std::fs::write(&conf_path, PIPEWIRE_CONF_TEMPLATE)
.map_err(|e| format!("failed to write PipeWire config: {e}"))?;
let mut dbus_child = unsafe {
Command::new("dbus-daemon")
.args(["--session", "--print-address=1", "--nofork"])
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
.map_err(|e| format!("failed to start dbus-daemon: {e}"))?
};
let dbus_stdout = dbus_child
.stdout
.take()
.ok_or("dbus-daemon stdout missing")?;
let mut dbus_reader = std::io::BufReader::new(dbus_stdout);
let mut dbus_address = String::new();
dbus_reader
.read_line(&mut dbus_address)
.map_err(|e| format!("failed to read dbus-daemon address: {e}"))?;
let dbus_address = dbus_address.trim();
if dbus_address.is_empty() {
let _ = dbus_child.kill();
return Err("dbus-daemon exited without printing an address".into());
}
let mut pipewire_child = match unsafe {
Command::new("pipewire")
.env("XDG_CONFIG_HOME", &audio_dir)
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
let _ = dbus_child.kill();
let _ = dbus_child.wait();
return Err(format!("failed to start pipewire: {e}"));
}
};
let pw_socket = audio_dir.join("pipewire-0");
if !wait_for_socket(&pw_socket, std::time::Duration::from_secs(2)) {
if matches!(pipewire_child.try_wait(), Ok(Some(_))) {
let _ = dbus_child.kill();
let _ = dbus_child.wait();
return Err("pipewire exited before creating its socket".into());
}
}
let mut wireplumber_child = if find_program("wireplumber").is_some() {
let wp_conf_dir = audio_dir.join("wireplumber").join("wireplumber.conf.d");
let _ = std::fs::create_dir_all(&wp_conf_dir);
let _ = std::fs::write(wp_conf_dir.join("99-blit.conf"), WIREPLUMBER_CONF_TEMPLATE);
let child = unsafe {
Command::new("wireplumber")
.env("PIPEWIRE_REMOTE", audio_dir.join("pipewire-0"))
.env("XDG_CONFIG_HOME", &audio_dir)
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
};
match child {
Ok(c) => {
std::thread::sleep(std::time::Duration::from_millis(250));
Some(c)
}
Err(e) => {
if verbose {
eprintln!("[audio] failed to start wireplumber: {e}");
}
None
}
}
} else {
None
};
let mut pipewire_pulse_child = match unsafe {
Command::new("pipewire-pulse")
.env("PIPEWIRE_REMOTE", audio_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err(format!("failed to start pipewire-pulse: {e}"));
}
};
let pulse_socket = audio_dir.join("pulse").join("native");
if !wait_for_socket(&pulse_socket, std::time::Duration::from_secs(2))
&& matches!(pipewire_pulse_child.try_wait(), Ok(Some(_)))
{
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err("pipewire-pulse exited before creating its socket".into());
}
let target = lookup_sink_serial(&audio_dir).unwrap_or_else(|| "blit-sink".to_string());
if verbose {
eprintln!("[audio] pw-cat target: {target}");
}
let pw_cat_child = match unsafe {
Command::new("pw-cat")
.args([
"--record",
"--rate",
"48000",
"--format",
"f32",
"--channels",
"2",
"--target",
&target,
"-", ])
.env("PIPEWIRE_REMOTE", audio_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", dbus_address)
.env("XDG_RUNTIME_DIR", &audio_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
let _ = pipewire_pulse_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
let _ = pipewire_pulse_child.wait();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err(format!("failed to start pw-cat: {e}"));
}
};
if verbose {
eprintln!(
"[audio] spawned dbus={} pipewire={} pipewire-pulse={} pw-cat={} dir={}",
dbus_child.id(),
pipewire_child.id(),
pipewire_pulse_child.id(),
pw_cat_child.id(),
audio_dir.display(),
);
}
let mut pw_cat_child = pw_cat_child;
let pw_cat_stdout = match pw_cat_child.stdout.take() {
Some(s) => s,
None => {
let _ = pw_cat_child.kill();
let _ = pipewire_pulse_child.kill();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.kill();
}
let _ = pipewire_child.kill();
let _ = dbus_child.kill();
let _ = pw_cat_child.wait();
let _ = pipewire_pulse_child.wait();
if let Some(ref mut wp) = wireplumber_child {
let _ = wp.wait();
}
let _ = pipewire_child.wait();
let _ = dbus_child.wait();
return Err("pw-cat stdout missing".into());
}
};
let (opus_tx, opus_rx) = mpsc::channel::<OpusFrame>(RING_CAPACITY * 2);
let opus_tx_keep = opus_tx.clone();
let bitrate = if bitrate > 0 {
bitrate
} else {
DEFAULT_BITRATE
};
let (bitrate_tx, bitrate_rx) = tokio::sync::watch::channel(bitrate);
let encoder_alive = Arc::new(AtomicBool::new(true));
let encoder_alive_clone = encoder_alive.clone();
let verbose_copy = verbose;
tokio::spawn(async move {
let result = reader_encoder_task(
pw_cat_stdout,
opus_tx,
bitrate,
verbose_copy,
epoch,
bitrate_rx,
)
.await;
encoder_alive_clone.store(false, Ordering::Release);
if let Err(e) = result
&& verbose_copy
{
eprintln!("[audio] reader/encoder task exited: {e}");
}
});
Ok(Self {
dbus_child,
pipewire_child,
wireplumber_child,
pipewire_pulse_child,
pw_cat_child,
opus_rx,
ring: VecDeque::with_capacity(RING_CAPACITY),
runtime_dir: audio_dir,
alive: true,
bitrate_tx,
encoder_alive,
dbus_address: dbus_address.to_string(),
opus_tx: opus_tx_keep,
verbose,
epoch,
last_heal: None,
first_heal_at: None,
heals: 0,
})
}
pub fn poll_frames(&mut self) -> Vec<OpusFrame> {
let mut new_frames = Vec::new();
while let Ok(frame) = self.opus_rx.try_recv() {
if self.ring.len() >= RING_CAPACITY {
self.ring.pop_front();
}
self.ring.push_back(frame.clone());
new_frames.push(frame);
}
new_frames
}
pub fn ring_frames(&self) -> impl Iterator<Item = &OpusFrame> {
self.ring.iter()
}
pub fn is_alive(&mut self) -> bool {
if !self.alive {
return false;
}
if matches!(self.pipewire_child.try_wait(), Ok(Some(_)))
|| matches!(self.dbus_child.try_wait(), Ok(Some(_)))
{
self.alive = false;
return false;
}
let wp_dead = self
.wireplumber_child
.as_mut()
.is_some_and(|wp| matches!(wp.try_wait(), Ok(Some(_))));
let pulse_dead = matches!(self.pipewire_pulse_child.try_wait(), Ok(Some(_)));
let encoder_dead = !self.encoder_alive.load(Ordering::Acquire);
let pw_cat_dead = matches!(self.pw_cat_child.try_wait(), Ok(Some(_)));
let needs_heal = wp_dead || pulse_dead || encoder_dead || pw_cat_dead;
if !needs_heal {
return true;
}
let now = Instant::now();
let can_heal = self
.last_heal
.is_none_or(|t| now.duration_since(t) >= HEAL_COOLDOWN);
if !can_heal {
return true;
}
if self
.first_heal_at
.is_none_or(|t| now.duration_since(t) > HEAL_WINDOW)
{
self.first_heal_at = Some(now);
self.heals = 0;
}
self.heals += 1;
if self.heals > MAX_HEALS {
eprintln!(
"[audio] too many sub-process restarts ({}), giving up",
self.heals
);
self.alive = false;
return false;
}
self.last_heal = Some(now);
if wp_dead {
eprintln!("[audio] wireplumber died, restarting");
self.restart_wireplumber();
}
if pulse_dead {
eprintln!("[audio] pipewire-pulse died, restarting");
self.restart_pipewire_pulse();
}
if encoder_dead || pw_cat_dead {
eprintln!("[audio] pw-cat/encoder died, restarting");
if !self.restart_pw_cat() {
self.alive = false;
return false;
}
}
true
}
pub fn shutdown(&mut self) {
self.alive = false;
let _ = self.pw_cat_child.kill();
let _ = self.pipewire_pulse_child.kill();
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.kill();
}
let _ = self.pipewire_child.kill();
let _ = self.dbus_child.kill();
let _ = self.pw_cat_child.wait();
let _ = self.pipewire_pulse_child.wait();
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.wait();
}
let _ = self.pipewire_child.wait();
let _ = self.dbus_child.wait();
let _ = std::fs::remove_dir_all(&self.runtime_dir);
}
fn restart_wireplumber(&mut self) {
if let Some(ref mut wp) = self.wireplumber_child {
let _ = wp.kill();
let _ = wp.wait();
}
let child = unsafe {
Command::new("wireplumber")
.env("PIPEWIRE_REMOTE", self.runtime_dir.join("pipewire-0"))
.env("XDG_CONFIG_HOME", &self.runtime_dir)
.env("DBUS_SESSION_BUS_ADDRESS", &self.dbus_address)
.env("XDG_RUNTIME_DIR", &self.runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if self.verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
};
match child {
Ok(c) => {
self.wireplumber_child = Some(c);
}
Err(e) => {
eprintln!("[audio] failed to restart wireplumber: {e}");
self.wireplumber_child = None;
}
}
}
fn restart_pipewire_pulse(&mut self) {
let _ = self.pipewire_pulse_child.kill();
let _ = self.pipewire_pulse_child.wait();
let _ = std::fs::remove_dir_all(self.runtime_dir.join("pulse"));
match unsafe {
Command::new("pipewire-pulse")
.env("PIPEWIRE_REMOTE", self.runtime_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", &self.dbus_address)
.env("XDG_RUNTIME_DIR", &self.runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(if self.verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => {
self.pipewire_pulse_child = c;
}
Err(e) => {
eprintln!("[audio] failed to restart pipewire-pulse: {e}");
}
}
}
fn restart_pw_cat(&mut self) -> bool {
let _ = self.pw_cat_child.kill();
let _ = self.pw_cat_child.wait();
let target =
lookup_sink_serial(&self.runtime_dir).unwrap_or_else(|| "blit-sink".to_string());
let mut new_child = match unsafe {
Command::new("pw-cat")
.args([
"--record",
"--rate",
"48000",
"--format",
"f32",
"--channels",
"2",
"--target",
&target,
"-",
])
.env("PIPEWIRE_REMOTE", self.runtime_dir.join("pipewire-0"))
.env("DBUS_SESSION_BUS_ADDRESS", &self.dbus_address)
.env("XDG_RUNTIME_DIR", &self.runtime_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(if self.verbose {
Stdio::inherit()
} else {
Stdio::null()
})
.pre_exec(pdeathsig_hook())
.spawn()
} {
Ok(c) => c,
Err(e) => {
eprintln!("[audio] failed to restart pw-cat: {e}");
return false;
}
};
let stdout = match new_child.stdout.take() {
Some(s) => s,
None => {
let _ = new_child.kill();
return false;
}
};
self.pw_cat_child = new_child;
let encoder_alive = Arc::new(AtomicBool::new(true));
self.encoder_alive = encoder_alive.clone();
let opus_tx = self.opus_tx.clone();
let bitrate = *self.bitrate_tx.borrow();
let bitrate_rx = self.bitrate_tx.subscribe();
let verbose = self.verbose;
let epoch = self.epoch;
tokio::spawn(async move {
let result =
reader_encoder_task(stdout, opus_tx, bitrate, verbose, epoch, bitrate_rx).await;
encoder_alive.store(false, Ordering::Release);
if let Err(e) = result
&& verbose
{
eprintln!("[audio] reader/encoder task exited: {e}");
}
});
if self.verbose {
eprintln!("[audio] restarted pw-cat (target={target}) and encoder");
}
true
}
pub fn set_bitrate(&self, bitrate: i32) {
let _ = self.bitrate_tx.send(bitrate);
}
pub fn pulse_server_path(&self) -> String {
let pulse_dir = self.runtime_dir.join("pulse");
format!("unix:{}", pulse_dir.join("native").display())
}
pub fn pipewire_remote_path(&self) -> String {
self.runtime_dir
.join("pipewire-0")
.to_string_lossy()
.into_owned()
}
}
impl Drop for AudioPipeline {
fn drop(&mut self) {
self.shutdown();
}
}
pub fn msg_audio_frame(frame: &OpusFrame) -> Vec<u8> {
let mut msg = Vec::with_capacity(1 + 4 + 1 + frame.data.len());
msg.push(S2C_AUDIO_FRAME);
msg.extend_from_slice(&frame.timestamp.to_le_bytes());
msg.push(AUDIO_FRAME_CODEC_OPUS);
msg.extend_from_slice(&frame.data);
msg
}
async fn reader_encoder_task(
stdout: std::process::ChildStdout,
tx: mpsc::Sender<OpusFrame>,
bitrate: i32,
verbose: bool,
epoch: Instant,
mut bitrate_rx: tokio::sync::watch::Receiver<i32>,
) -> Result<(), String> {
let mut reader = tokio::process::ChildStdout::from_std(stdout)
.map_err(|e| format!("failed to convert pw-cat stdout to async: {e}"))?;
let mut encoder = OpusEncoder::new(48000, Channels::Stereo, Application::Audio)
.map_err(|e| format!("failed to create Opus encoder: {e}"))?;
encoder
.set_bitrate(opus::Bitrate::Bits(bitrate))
.map_err(|e| format!("failed to set Opus bitrate: {e}"))?;
let mut current_bitrate = bitrate;
if verbose {
eprintln!("[audio] encoder ready, bitrate={bitrate} bps");
}
let mut pcm_buf = vec![0f32; FRAME_FLOATS];
let mut byte_buf = vec![0u8; FRAME_FLOATS * 4]; let mut byte_offset = 0usize;
let mut opus_out = vec![0u8; MAX_OPUS_PACKET];
loop {
if bitrate_rx.has_changed().unwrap_or(false) {
let new_bitrate = *bitrate_rx.borrow_and_update();
if new_bitrate != current_bitrate {
if let Err(e) = encoder.set_bitrate(opus::Bitrate::Bits(new_bitrate)) {
if verbose {
eprintln!("[audio] failed to update bitrate to {new_bitrate}: {e}");
}
} else {
if verbose {
eprintln!(
"[audio] bitrate updated: {current_bitrate} -> {new_bitrate} bps"
);
}
current_bitrate = new_bitrate;
}
}
}
let needed = (FRAME_FLOATS * 4) - byte_offset;
let n = reader
.read(&mut byte_buf[byte_offset..byte_offset + needed])
.await
.map_err(|e| format!("pipe read error: {e}"))?;
if n == 0 {
return Ok(());
}
byte_offset += n;
while byte_offset >= FRAME_FLOATS * 4 {
for (i, sample) in pcm_buf.iter_mut().enumerate() {
let off = i * 4;
*sample = f32::from_le_bytes([
byte_buf[off],
byte_buf[off + 1],
byte_buf[off + 2],
byte_buf[off + 3],
]);
}
let encoded_len = match encoder.encode_float(&pcm_buf, &mut opus_out) {
Ok(len) => len,
Err(e) => {
if verbose {
eprintln!("[audio] Opus encode error, skipping frame: {e}");
}
let consumed = FRAME_FLOATS * 4;
byte_buf.copy_within(consumed..byte_offset, 0);
byte_offset -= consumed;
continue;
}
};
let frame = OpusFrame {
timestamp: epoch.elapsed().as_millis() as u32,
data: opus_out[..encoded_len].to_vec(),
};
match tx.try_send(frame) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
}
Err(mpsc::error::TrySendError::Closed(_)) => {
return Ok(());
}
}
let consumed = FRAME_FLOATS * 4;
byte_buf.copy_within(consumed..byte_offset, 0);
byte_offset -= consumed;
}
}
}