Native low-latency PipeWire capture for -p (drop pw-record subprocess)
-p now connects a real capture stream via the pipewire crate and pushes mono f32 straight into the render channel from the RT process callback, instead of spawning pw-record and parsing its s16 stdout. It captures the default sink monitor (what you hear), negotiates F32 at the graph's native rate/channels, and takes a --latency <frames> quantum hint (default 256) so you can trade CPU/xrun-risk for lower latency. libpipewire becomes a build+runtime dependency: CI installs the dev headers + clang, and the PKGBUILD depends on libpipewire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLUwWE2KmFPzhKaf67tWbx
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3dbe5c4ddd
commit
b230e6829a
+52
-65
@@ -17,7 +17,8 @@ use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
mod pipewire_capture;
|
||||
|
||||
const EMPTY_CELL: Cell = Cell { character: ' ', filled: false };
|
||||
const BLOCK_CELL: Cell = Cell { character: '█', filled: true };
|
||||
@@ -121,7 +122,7 @@ fn handle_terminal_resize(
|
||||
long_about = "Spectrust is a PCM spectrum analyzer that visualizes audio in your terminal.\n\
|
||||
It can read from stdin, a file, or capture directly from PipeWire.\n\n\
|
||||
Examples:\n\
|
||||
- Direct PipeWire capture: spectrust -p\n\
|
||||
- Direct PipeWire capture (native, low-latency): spectrust -p\n\
|
||||
- From audio file: spectrust -i audio.pcm\n\
|
||||
- From stdin: pw-record --raw - | spectrust\n\n\
|
||||
Runtime controls:\n\
|
||||
@@ -133,9 +134,12 @@ struct Cli {
|
||||
#[arg(short, long, help = "Target frames per second", default_value_t = 60)]
|
||||
fps: u16,
|
||||
|
||||
#[arg(short, long, help = "Use PipeWire to capture audio directly")]
|
||||
#[arg(short, long, help = "Capture audio directly via native low-latency PipeWire (default sink monitor)")]
|
||||
pipewire: bool,
|
||||
|
||||
#[arg(long, help = "PipeWire capture quantum in frames — lower is lower latency (more CPU/xrun risk)", default_value_t = 256)]
|
||||
latency: u32,
|
||||
|
||||
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
||||
input: Option<String>,
|
||||
|
||||
@@ -184,77 +188,60 @@ fn main() {
|
||||
|
||||
let _ = out.flush();
|
||||
|
||||
let reader: Box<dyn Read + Send + 'static> = if cli.pipewire {
|
||||
let spawn_result = Command::new("pw-record")
|
||||
.args([
|
||||
"--format=s16",
|
||||
&format!("--rate={}", cli.rate),
|
||||
&format!("--channels={}", cli.channels),
|
||||
"--raw",
|
||||
"-",
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn();
|
||||
match spawn_result {
|
||||
Ok(mut process) => {
|
||||
Box::new(process.stdout.take().expect("failed to capture pw-record stdout"))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
let _ = execute!(out, LeaveAlternateScreen);
|
||||
let _ = disable_raw_mode();
|
||||
eprintln!("error: pw-record not found — install PipeWire or pipe audio via stdin.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = execute!(out, LeaveAlternateScreen);
|
||||
let _ = disable_raw_mode();
|
||||
eprintln!("error: failed to start pw-record: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else if let Some(filename) = &cli.input {
|
||||
Box::new(File::open(filename).expect("failed to open input file"))
|
||||
} else {
|
||||
Box::new(std::io::stdin())
|
||||
};
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Vec<f32>>();
|
||||
let _audio_thread = thread::spawn(move || {
|
||||
let mut reader = reader;
|
||||
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
||||
loop {
|
||||
match reader.read(&mut input_buffer) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
let mut samples = Vec::with_capacity(n / (num_channels * (bit_depth / 8)));
|
||||
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
||||
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
||||
let mut frame_sum = 0.0f32;
|
||||
let mut valid_channels = 0u32;
|
||||
for _ in 0..num_channels {
|
||||
match cursor.read_i16::<LittleEndian>() {
|
||||
Ok(sample) => {
|
||||
frame_sum += sample as f32;
|
||||
valid_channels += 1;
|
||||
|
||||
if cli.pipewire {
|
||||
// Native low-latency capture: pushes mono f32 straight into `tx`.
|
||||
if let Err(e) = pipewire_capture::spawn(tx, cli.rate, cli.channels, cli.latency) {
|
||||
let _ = execute!(out, LeaveAlternateScreen);
|
||||
let _ = disable_raw_mode();
|
||||
eprintln!("error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
let reader: Box<dyn Read + Send + 'static> = if let Some(filename) = &cli.input {
|
||||
Box::new(File::open(filename).expect("failed to open input file"))
|
||||
} else {
|
||||
Box::new(std::io::stdin())
|
||||
};
|
||||
|
||||
let _audio_thread = thread::spawn(move || {
|
||||
let mut reader = reader;
|
||||
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
||||
loop {
|
||||
match reader.read(&mut input_buffer) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
let mut samples = Vec::with_capacity(n / (num_channels * (bit_depth / 8)));
|
||||
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
||||
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
||||
let mut frame_sum = 0.0f32;
|
||||
let mut valid_channels = 0u32;
|
||||
for _ in 0..num_channels {
|
||||
match cursor.read_i16::<LittleEndian>() {
|
||||
Ok(sample) => {
|
||||
frame_sum += sample as f32;
|
||||
valid_channels += 1;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
if valid_channels > 0 {
|
||||
samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
||||
}
|
||||
}
|
||||
if valid_channels > 0 {
|
||||
samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
||||
if !samples.is_empty() && tx.send(samples).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !samples.is_empty() && tx.send(samples).is_err() {
|
||||
break;
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let max_width: u16 = 1000;
|
||||
let max_height: u16 = 1000;
|
||||
|
||||
Reference in New Issue
Block a user