diff --git a/src/app.rs b/src/app.rs index b660720..7dc770d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,6 +29,7 @@ use crate::{ osc::{self, OscAppCmd}, tui, }; +use std::path::PathBuf; pub struct App { pub decks: Vec, @@ -38,10 +39,11 @@ pub struct App { audio: AudioEngine, chains: Vec, // one per deck, in order osc_rx: crossbeam_channel::Receiver, + audio_dir: PathBuf, } impl App { - pub fn new() -> Result { + pub fn new(audio_dir: Option) -> Result { // 1. Load filter-chain modules for all 4 decks tracing::info!("Loading PipeWire filter-chain modules…"); let mut chains = Vec::new(); @@ -79,6 +81,8 @@ impl App { .map(|i| Deck::new(DeckId::from_index(i).unwrap())) .collect(); + let audio_dir = audio_dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); + Ok(Self { decks, focused_deck: 0, @@ -86,6 +90,7 @@ impl App { audio, chains, osc_rx, + audio_dir, }) } @@ -312,7 +317,7 @@ impl App { fn open_selector(&mut self, idx: usize) { 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)); } } diff --git a/src/audio.rs b/src/audio.rs index ee1dcf0..9977fa1 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -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, // interleaved stereo f32 + pcm: Vec, // 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, @@ -108,7 +118,7 @@ impl AudioEngine { } } -// ─── Engine thread ───────────────────────────────────────────────────────── +// ─── Engine thread ────────────────────────────────────────────────────── fn engine_thread(cmd_rx: Receiver, status_tx: Sender) -> Result<()> { use pipewire::{context::ContextRc, main_loop::MainLoopRc, stream::StreamRc}; @@ -155,19 +165,20 @@ fn engine_thread(cmd_rx: Receiver, status_tx: Sender) -> .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, status_tx: Sender) -> 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, status_tx: Sender) -> 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, status_tx: Sender) -> Ok(()) } -fn handle_cmd( - cmd: AudioCmd, - states: &[Arc>], - status_tx: &Sender, -) { +fn handle_cmd(cmd: AudioCmd, states: &[Arc>], status_tx: &Sender) { 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, 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 = Vec::new(); @@ -346,7 +367,9 @@ fn decode_file(path: &PathBuf) -> Result<(Vec, 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, 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, buf: AudioBufferRef, channels: usize) { // ─── PipeWire format pod helper ─────────────────────────────────────────────── fn make_f32_stereo_pod() -> Result> { - 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> { &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> { .into_inner(); Ok(bytes) -} \ No newline at end of file +} diff --git a/src/file_selector.rs b/src/file_selector.rs index 7f52294..9990bd3 100644 --- a/src/file_selector.rs +++ b/src/file_selector.rs @@ -19,9 +19,8 @@ pub struct FileSelector { } impl FileSelector { - pub fn new(deck: DeckId) -> Self { - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let all_files = cache::get_audio_files(&cwd).unwrap_or_default(); + pub fn new(deck: DeckId, audio_dir: &PathBuf) -> Self { + let all_files = cache::get_audio_files(audio_dir).unwrap_or_default(); let filtered = all_files.clone(); let mut ls = ListState::default(); if !filtered.is_empty() { ls.select(Some(0)); } diff --git a/src/main.rs b/src/main.rs index 509436a..49b2c85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod osc; mod tui; use anyhow::Result; +use std::path::PathBuf; #[tokio::main] async fn main() -> Result<()> { @@ -23,6 +24,13 @@ async fn main() -> Result<()> { 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 } diff --git a/src/tui.rs b/src/tui.rs index 95f3666..726e4c6 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -109,7 +109,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) { .borders(Borders::ALL) .title(title) .border_style(border_style) - .style(Style::default().bg(Color::Black)); + .style(Style::default().bg(Color::Indexed(0))); f.render_widget(outer, area); 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 f.render_widget( 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) .label(""), rows[1], @@ -154,7 +154,7 @@ fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) { Gauge::default() .gauge_style(Style::default() .fg(if fader_pct > 10 { Color::White } else { Color::DarkGray }) - .bg(Color::Black)) + .bg(Color::Indexed(0))) .percent(fader_pct.min(100)) .label(format!("VOL {:3}%", fader_pct)), 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 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(rp)).bg(Color::Black)).percent(rp.min(100)).label("R"), ra); + 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::Indexed(0))).percent(rp.min(100)).label("R"), ra); } fn draw_help(f: &mut Frame, area: Rect) { @@ -225,7 +225,7 @@ fn draw_help(f: &mut Frame, area: Rect) { .block(Block::default() .borders(Borders::TOP) .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); }