pre pipewire-rs effects

This commit is contained in:
2026-03-06 20:38:46 +01:00
parent 4fec224769
commit 35f6343637
5 changed files with 95 additions and 66 deletions

View File

@@ -29,6 +29,7 @@ use crate::{
osc::{self, OscAppCmd}, osc::{self, OscAppCmd},
tui, tui,
}; };
use std::path::PathBuf;
pub struct App { pub struct App {
pub decks: Vec<Deck>, pub decks: Vec<Deck>,
@@ -38,10 +39,11 @@ pub struct App {
audio: AudioEngine, audio: AudioEngine,
chains: Vec<EffectChain>, // one per deck, in order chains: Vec<EffectChain>, // one per deck, in order
osc_rx: crossbeam_channel::Receiver<OscAppCmd>, osc_rx: crossbeam_channel::Receiver<OscAppCmd>,
audio_dir: PathBuf,
} }
impl App { impl App {
pub fn new() -> Result<Self> { pub fn new(audio_dir: Option<PathBuf>) -> Result<Self> {
// 1. Load filter-chain modules for all 4 decks // 1. Load filter-chain modules for all 4 decks
tracing::info!("Loading PipeWire filter-chain modules…"); tracing::info!("Loading PipeWire filter-chain modules…");
let mut chains = Vec::new(); let mut chains = Vec::new();
@@ -79,6 +81,8 @@ impl App {
.map(|i| Deck::new(DeckId::from_index(i).unwrap())) .map(|i| Deck::new(DeckId::from_index(i).unwrap()))
.collect(); .collect();
let audio_dir = audio_dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
Ok(Self { Ok(Self {
decks, decks,
focused_deck: 0, focused_deck: 0,
@@ -86,6 +90,7 @@ impl App {
audio, audio,
chains, chains,
osc_rx, osc_rx,
audio_dir,
}) })
} }
@@ -312,7 +317,7 @@ impl App {
fn open_selector(&mut self, idx: usize) { fn open_selector(&mut self, idx: usize) {
if let Some(id) = DeckId::from_index(idx) { if let Some(id) = DeckId::from_index(idx) {
self.file_selector = Some(FileSelector::new(id)); self.file_selector = Some(FileSelector::new(id, &self.audio_dir));
} }
} }

View File

@@ -14,8 +14,8 @@ use std::{
thread, thread,
}; };
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use crossbeam_channel::{bounded, Receiver, Sender}; use crossbeam_channel::{Receiver, Sender, bounded};
use symphonia::core::{ use symphonia::core::{
audio::{AudioBufferRef, Signal}, audio::{AudioBufferRef, Signal},
codecs::DecoderOptions, codecs::DecoderOptions,
@@ -54,10 +54,20 @@ pub enum AudioStatus {
/// PipeWire object ID of the stream node (for wpctl volume) /// PipeWire object ID of the stream node (for wpctl volume)
stream_node_id: u32, stream_node_id: u32,
}, },
Position { deck: DeckId, position: u64 }, Position {
Vu { deck: DeckId, l: f32, r: f32 }, deck: DeckId,
position: u64,
},
Vu {
deck: DeckId,
l: f32,
r: f32,
},
TrackEnded(DeckId), TrackEnded(DeckId),
Error { deck: DeckId, msg: String }, Error {
deck: DeckId,
msg: String,
},
} }
// ─── Per-deck internal state ────────────────────────────────────────────────── // ─── Per-deck internal state ──────────────────────────────────────────────────
@@ -84,7 +94,7 @@ impl DeckState {
} }
} }
// ─── Public handle ────────────────────────────────────────────────────────── // ─── Public handle ─────────────────────────────────────────────────────
pub struct AudioEngine { pub struct AudioEngine {
pub cmd_tx: Sender<AudioCmd>, 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<()> { fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) -> Result<()> {
use pipewire::{context::ContextRc, main_loop::MainLoopRc, stream::StreamRc}; 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, _| { .process(move |stream: &pipewire::stream::Stream, _| {
let mut ds_guard = ds_cb.lock(); let mut ds_guard = ds_cb.lock();
let ds = ds_guard.as_mut().unwrap(); let ds = ds_guard.as_mut().unwrap();
if let Some(mut buf) = stream.dequeue_buffer() { if let Some(mut buf) = stream.dequeue_buffer() {
let datas = buf.datas_mut(); let datas = buf.datas_mut();
if datas.is_empty() { return; } if datas.is_empty() {
return;
}
let data = &mut datas[0]; 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 out_bytes = data.data().unwrap();
let n_frames = out_bytes.len() / 8; // f32 stereo
let out = unsafe { let out = unsafe {
std::slice::from_raw_parts_mut( std::slice::from_raw_parts_mut(out_bytes.as_ptr() as *mut f32, n_frames * 2)
out_bytes.as_ptr() as *mut f32,
n_frames * 2,
)
}; };
let mut vu_l = 0.0f32; 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() { if ds.playing && !ds.pcm.is_empty() {
for frame in 0..n_frames { for frame in 0..n_frames {
if ds.read_pos + 1 >= ds.pcm.len() { if ds.read_pos + 1 >= ds.pcm.len() {
// End of track
ds.playing = false; ds.playing = false;
out[frame * 2] = 0.0; out[frame * 2] = 0.0;
out[frame * 2 + 1] = 0.0; out[frame * 2 + 1] = 0.0;
let _ = status_tx_cb.try_send(AudioStatus::TrackEnded(ds.id)); let _ = status_tx_cb.try_send(AudioStatus::TrackEnded(ds.id));
continue; continue;
} }
let l = ds.pcm[ds.read_pos]; let l = ds.pcm[ds.read_pos];
let r = ds.pcm[ds.read_pos + 1]; let r = ds.pcm[ds.read_pos + 1];
ds.read_pos += 2; ds.read_pos += 2;
out[frame * 2] = l; out[frame * 2] = l;
out[frame * 2 + 1] = r; out[frame * 2 + 1] = r;
vu_l = vu_l.max(l.abs()); vu_l = vu_l.max(l.abs());
vu_r = vu_r.max(r.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); out.fill(0.0);
} }
{
let chunk = data.chunk_mut();
*chunk.offset_mut() = 0; *chunk.offset_mut() = 0;
*chunk.stride_mut() = 8; *chunk.stride_mut() = 8;
*chunk.size_mut() = (n_frames * 8) as u32; *chunk.size_mut() = (n_frames * 8) as u32;
}
let pos = (ds.read_pos / 2) as u64; 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::Position {
let _ = status_tx_cb.try_send(AudioStatus::Vu { deck: ds.id, l: vu_l, r: vu_r }); deck: ds.id,
position: pos,
});
let _ = status_tx_cb.try_send(AudioStatus::Vu {
deck: ds.id,
l: vu_l,
r: vu_r,
});
} }
}) })
.register()?; .register()?; // Connect the stream with F32LE stereo format
// Connect the stream with F32LE stereo format
let pod_bytes = make_f32_stereo_pod()?; let pod_bytes = make_f32_stereo_pod()?;
let mut params = [pipewire::spa::pod::Pod::from_bytes(&pod_bytes).unwrap()]; let mut params = [pipewire::spa::pod::Pod::from_bytes(&pod_bytes).unwrap()];
stream.connect( stream.connect(
@@ -235,14 +256,11 @@ fn engine_thread(cmd_rx: Receiver<AudioCmd>, status_tx: Sender<AudioStatus>) ->
Ok(()) Ok(())
} }
fn handle_cmd( fn handle_cmd(cmd: AudioCmd, states: &[Arc<Mutex<DeckState>>], status_tx: &Sender<AudioStatus>) {
cmd: AudioCmd,
states: &[Arc<Mutex<DeckState>>],
status_tx: &Sender<AudioStatus>,
) {
match cmd { match cmd {
AudioCmd::Load { deck, path } => { AudioCmd::Load { deck, path } => {
let name = path.file_name() let name = path
.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown") .unwrap_or("unknown")
.to_string(); .to_string();
@@ -269,7 +287,10 @@ fn handle_cmd(
}); });
} }
Err(e) => { 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 sr = track.codec_params.sample_rate.unwrap_or(44100);
let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2); let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2);
let mut decoder = symphonia::default::get_codecs() let mut decoder =
.make(&track.codec_params, &DecoderOptions::default())?; symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?;
let track_id = track.id; let track_id = track.id;
let mut pcm: Vec<f32> = Vec::new(); 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(symphonia::core::errors::Error::ResetRequired) => break,
Err(e) => return Err(e.into()), 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)?; let decoded = decoder.decode(&packet)?;
push_samples(&mut pcm, decoded, channels); push_samples(&mut pcm, decoded, channels);
@@ -385,19 +408,13 @@ fn push_samples(out: &mut Vec<f32>, buf: AudioBufferRef, channels: usize) {
// ─── PipeWire format pod helper ─────────────────────────────────────────────── // ─── PipeWire format pod helper ───────────────────────────────────────────────
fn make_f32_stereo_pod() -> Result<Vec<u8>> { fn make_f32_stereo_pod() -> Result<Vec<u8>> {
use pipewire::spa::pod::serialize::PodSerializer; use pipewire::spa::param::ParamType;
use pipewire::spa::pod::{object, property, Value};
use pipewire::spa::param::audio::AudioFormat; use pipewire::spa::param::audio::AudioFormat;
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType}; 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; use pipewire::spa::utils::SpaTypes;
let bytes = PodSerializer::serialize(
std::io::Cursor::new(Vec::new()),
&Value::Object(object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
let bytes = PodSerializer::serialize( let bytes = PodSerializer::serialize(
std::io::Cursor::new(Vec::new()), std::io::Cursor::new(Vec::new()),
&Value::Object(object!( &Value::Object(object!(

View File

@@ -19,9 +19,8 @@ pub struct FileSelector {
} }
impl FileSelector { impl FileSelector {
pub fn new(deck: DeckId) -> Self { pub fn new(deck: DeckId, audio_dir: &PathBuf) -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let all_files = cache::get_audio_files(audio_dir).unwrap_or_default();
let all_files = cache::get_audio_files(&cwd).unwrap_or_default();
let filtered = all_files.clone(); let filtered = all_files.clone();
let mut ls = ListState::default(); let mut ls = ListState::default();
if !filtered.is_empty() { ls.select(Some(0)); } if !filtered.is_empty() { ls.select(Some(0)); }

View File

@@ -8,6 +8,7 @@ mod osc;
mod tui; mod tui;
use anyhow::Result; use anyhow::Result;
use std::path::PathBuf;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@@ -23,6 +24,13 @@ async fn main() -> Result<()> {
tracing::info!("djdeck starting"); tracing::info!("djdeck starting");
let mut app = app::App::new()?; // Parse command line argument for audio directory
let audio_dir = if std::env::args().len() > 1 {
Some(PathBuf::from(std::env::args().nth(1).unwrap()))
} else {
None
};
let mut app = app::App::new(audio_dir)?;
app.run().await app.run().await
} }

View File

@@ -109,7 +109,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
.borders(Borders::ALL) .borders(Borders::ALL)
.title(title) .title(title)
.border_style(border_style) .border_style(border_style)
.style(Style::default().bg(Color::Black)); .style(Style::default().bg(Color::Indexed(0)));
f.render_widget(outer, area); f.render_widget(outer, area);
let inner = area.inner(ratatui::layout::Margin { horizontal: 1, vertical: 1 }); let inner = area.inner(ratatui::layout::Margin { horizontal: 1, vertical: 1 });
@@ -142,7 +142,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
// Progress // Progress
f.render_widget( f.render_widget(
Gauge::default() Gauge::default()
.gauge_style(Style::default().fg(color).bg(Color::Black)) .gauge_style(Style::default().fg(color).bg(Color::Indexed(0)))
.percent((deck.progress() * 100.0) as u16) .percent((deck.progress() * 100.0) as u16)
.label(""), .label(""),
rows[1], rows[1],
@@ -154,7 +154,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
Gauge::default() Gauge::default()
.gauge_style(Style::default() .gauge_style(Style::default()
.fg(if fader_pct > 10 { Color::White } else { Color::DarkGray }) .fg(if fader_pct > 10 { Color::White } else { Color::DarkGray })
.bg(Color::Black)) .bg(Color::Indexed(0)))
.percent(fader_pct.min(100)) .percent(fader_pct.min(100))
.label(format!("VOL {:3}%", fader_pct)), .label(format!("VOL {:3}%", fader_pct)),
rows[3], rows[3],
@@ -191,8 +191,8 @@ fn draw_vu(f: &mut Frame, vu: (f32, f32), area: Rect, color: Color) {
let lp = (vu.0 * 100.0) as u16; let lp = (vu.0 * 100.0) as u16;
let rp = (vu.1 * 100.0) as u16; let rp = (vu.1 * 100.0) as u16;
f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(lp)).bg(Color::Black)).percent(lp.min(100)).label("L"), la); f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(lp)).bg(Color::Indexed(0))).percent(lp.min(100)).label("L"), la);
f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(rp)).bg(Color::Black)).percent(rp.min(100)).label("R"), ra); f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(rp)).bg(Color::Indexed(0))).percent(rp.min(100)).label("R"), ra);
} }
fn draw_help(f: &mut Frame, area: Rect) { fn draw_help(f: &mut Frame, area: Rect) {
@@ -225,7 +225,7 @@ fn draw_help(f: &mut Frame, area: Rect) {
.block(Block::default() .block(Block::default()
.borders(Borders::TOP) .borders(Borders::TOP)
.border_style(Style::default().fg(Color::DarkGray))) .border_style(Style::default().fg(Color::DarkGray)))
.style(Style::default().bg(Color::Black)); .style(Style::default().bg(Color::Indexed(0)));
f.render_widget(help, area); f.render_widget(help, area);
} }