pre pipewire-rs effects

This commit is contained in:
Bendik Aagaard Lynghaug
2026-03-06 20:38:46 +01:00
parent 4fec224769
commit 35f6343637
5 changed files with 95 additions and 66 deletions
+71 -54
View File
@@ -14,8 +14,8 @@ use std::{
thread,
};
use anyhow::{anyhow, Result};
use crossbeam_channel::{bounded, Receiver, Sender};
use anyhow::{Result, anyhow};
use crossbeam_channel::{Receiver, Sender, bounded};
use symphonia::core::{
audio::{AudioBufferRef, Signal},
codecs::DecoderOptions,
@@ -54,10 +54,20 @@ pub enum AudioStatus {
/// PipeWire object ID of the stream node (for wpctl volume)
stream_node_id: u32,
},
Position { deck: DeckId, position: u64 },
Vu { deck: DeckId, l: f32, r: f32 },
Position {
deck: DeckId,
position: u64,
},
Vu {
deck: DeckId,
l: f32,
r: f32,
},
TrackEnded(DeckId),
Error { deck: DeckId, msg: String },
Error {
deck: DeckId,
msg: String,
},
}
// ─── Per-deck internal state ──────────────────────────────────────────────────
@@ -65,7 +75,7 @@ pub enum AudioStatus {
struct DeckState {
id: DeckId,
playing: bool,
pcm: Vec<f32>, // interleaved stereo f32
pcm: Vec<f32>, // interleaved stereo f32
sample_rate: u32,
read_pos: usize, // byte index into pcm (always even)
cue_pos: usize,
@@ -84,7 +94,7 @@ impl DeckState {
}
}
// ─── Public handle ──────────────────────────────────────────────────────────
// ─── Public handle ─────────────────────────────────────────────────────
pub struct AudioEngine {
pub cmd_tx: Sender<AudioCmd>,
@@ -108,7 +118,7 @@ impl AudioEngine {
}
}
// ─── Engine thread ─────────────────────────────────────────────────────────
// ─── Engine thread ──────────────────────────────────────────────────────
fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) -> Result<()> {
use pipewire::{context::ContextRc, main_loop::MainLoopRc, stream::StreamRc};
@@ -155,19 +165,20 @@ fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) ->
.process(move |stream: &pipewire::stream::Stream, _| {
let mut ds_guard = ds_cb.lock();
let ds = ds_guard.as_mut().unwrap();
if let Some(mut buf) = stream.dequeue_buffer() {
let datas = buf.datas_mut();
if datas.is_empty() { return; }
if datas.is_empty() {
return;
}
let data = &mut datas[0];
let chunk = data.chunk_mut();
let n_frames = (chunk.size() / 8) as usize; // f32 stereo = 8 bytes
let out_bytes = data.data().unwrap();
let n_frames = out_bytes.len() / 8; // f32 stereo
let out = unsafe {
std::slice::from_raw_parts_mut(
out_bytes.as_ptr() as *mut f32,
n_frames * 2,
)
std::slice::from_raw_parts_mut(out_bytes.as_ptr() as *mut f32, n_frames * 2)
};
let mut vu_l = 0.0f32;
@@ -176,18 +187,20 @@ fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) ->
if ds.playing && !ds.pcm.is_empty() {
for frame in 0..n_frames {
if ds.read_pos + 1 >= ds.pcm.len() {
// End of track
ds.playing = false;
out[frame * 2] = 0.0;
out[frame * 2] = 0.0;
out[frame * 2 + 1] = 0.0;
let _ = status_tx_cb.try_send(AudioStatus::TrackEnded(ds.id));
continue;
}
let l = ds.pcm[ds.read_pos];
let r = ds.pcm[ds.read_pos + 1];
ds.read_pos += 2;
out[frame * 2] = l;
out[frame * 2] = l;
out[frame * 2 + 1] = r;
vu_l = vu_l.max(l.abs());
vu_r = vu_r.max(r.abs());
}
@@ -195,18 +208,26 @@ fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) ->
out.fill(0.0);
}
*chunk.offset_mut() = 0;
*chunk.stride_mut() = 8;
*chunk.size_mut() = (n_frames * 8) as u32;
{
let chunk = data.chunk_mut();
*chunk.offset_mut() = 0;
*chunk.stride_mut() = 8;
*chunk.size_mut() = (n_frames * 8) as u32;
}
let pos = (ds.read_pos / 2) as u64;
let _ = status_tx_cb.try_send(AudioStatus::Position { deck: ds.id, position: pos });
let _ = status_tx_cb.try_send(AudioStatus::Vu { deck: ds.id, l: vu_l, r: vu_r });
let _ = status_tx_cb.try_send(AudioStatus::Position {
deck: ds.id,
position: pos,
});
let _ = status_tx_cb.try_send(AudioStatus::Vu {
deck: ds.id,
l: vu_l,
r: vu_r,
});
}
})
.register()?;
// Connect the stream with F32LE stereo format
.register()?; // Connect the stream with F32LE stereo format
let pod_bytes = make_f32_stereo_pod()?;
let mut params = [pipewire::spa::pod::Pod::from_bytes(&pod_bytes).unwrap()];
stream.connect(
@@ -235,14 +256,11 @@ fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) ->
Ok(())
}
fn handle_cmd(
cmd: AudioCmd,
states: &[Arc<Mutex<DeckState>>],
status_tx: &Sender<AudioStatus>,
) {
fn handle_cmd(cmd: AudioCmd, states: &[Arc<Mutex<DeckState>>], status_tx: &Sender<AudioStatus>) {
match cmd {
AudioCmd::Load { deck, path } => {
let name = path.file_name()
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
@@ -269,7 +287,10 @@ fn handle_cmd(
});
}
Err(e) => {
let _ = status_tx.send(AudioStatus::Error { deck, msg: e.to_string() });
let _ = status_tx.send(AudioStatus::Error {
deck,
msg: e.to_string(),
});
}
}
}
@@ -333,8 +354,8 @@ fn decode_file(path: &PathBuf) -> Result<(Vec<f32>, u32)> {
let sr = track.codec_params.sample_rate.unwrap_or(44100);
let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2);
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())?;
let mut decoder =
symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?;
let track_id = track.id;
let mut pcm: Vec<f32> = Vec::new();
@@ -346,7 +367,9 @@ fn decode_file(path: &PathBuf) -> Result<(Vec<f32>, u32)> {
Err(symphonia::core::errors::Error::ResetRequired) => break,
Err(e) => return Err(e.into()),
};
if packet.track_id() != track_id { continue; }
if packet.track_id() != track_id {
continue;
}
let decoded = decoder.decode(&packet)?;
push_samples(&mut pcm, decoded, channels);
@@ -373,11 +396,11 @@ fn push_samples(out: &mut Vec<f32>, buf: AudioBufferRef, channels: usize) {
}
match buf {
AudioBufferRef::F32(b) => push!(b, 1.0_f32),
AudioBufferRef::F64(b) => push!(b, 1.0_f32),
AudioBufferRef::S16(b) => push!(b, 1.0 / 32768.0_f32),
AudioBufferRef::S32(b) => push!(b, 1.0 / 2147483648.0_f32),
AudioBufferRef::U8(b) => push!(b, 1.0 / 128.0_f32),
AudioBufferRef::F32(b) => push!(b, 1.0_f32),
AudioBufferRef::F64(b) => push!(b, 1.0_f32),
AudioBufferRef::S16(b) => push!(b, 1.0 / 32768.0_f32),
AudioBufferRef::S32(b) => push!(b, 1.0 / 2147483648.0_f32),
AudioBufferRef::U8(b) => push!(b, 1.0 / 128.0_f32),
_ => {}
}
}
@@ -385,11 +408,11 @@ fn push_samples(out: &mut Vec<f32>, buf: AudioBufferRef, channels: usize) {
// ─── PipeWire format pod helper ───────────────────────────────────────────────
fn make_f32_stereo_pod() -> Result<Vec<u8>> {
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{object, property, Value};
use pipewire::spa::param::ParamType;
use pipewire::spa::param::audio::AudioFormat;
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::spa::param::ParamType;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{Value, object, property};
use pipewire::spa::utils::SpaTypes;
let bytes = PodSerializer::serialize(
@@ -397,16 +420,10 @@ fn make_f32_stereo_pod() -> Result<Vec<u8>> {
&Value::Object(object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
let bytes = PodSerializer::serialize(
std::io::Cursor::new(Vec::new()),
&Value::Object(object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
property!(FormatProperties::MediaType, Id, MediaType::Audio),
property!(FormatProperties::MediaType, Id, MediaType::Audio),
property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
property!(FormatProperties::AudioFormat, Id, AudioFormat::F32LE),
property!(FormatProperties::AudioRate, Int, 44100i32),
property!(FormatProperties::AudioFormat, Id, AudioFormat::F32LE),
property!(FormatProperties::AudioRate, Int, 44100i32),
property!(FormatProperties::AudioChannels, Int, 2i32),
)),
)?
@@ -414,4 +431,4 @@ fn make_f32_stereo_pod() -> Result<Vec<u8>> {
.into_inner();
Ok(bytes)
}
}