# djdeck Terminal DJ suite — Rust + Ratatui + PipeWire graph DSP. ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Ratatui TUI (16ms loop, crossterm events) │ │ OSC server (UDP :9000 in, :9001 out) │ └──────────┬──────────────────────────────┬───────────────────┘ │ │ AudioCmd channel OscAppCmd channel │ │ ┌──────────▼──────────┐ ┌──────────▼────────────────────┐ │ Audio Engine │ │ App.rs effect dispatch │ │ (PipeWire streams) │ │ fader → wpctl set-volume │ │ Symphonia decode │ │ pitch → pw-cli set-param Props│ │ Raw PCM only │ │ tempo → pw-cli set-param Props│ └──────────┬──────────┘ └───────────────────────────────┘ │ auto-connects to filter-chain sink ▼ ┌──────────────────────────────────────────────────────────────┐ │ PipeWire Graph │ │ │ │ djdeck.deck.a ──► input.djdeck.chain.a │ │ [rubberband LADSPA] │ │ output.djdeck.chain.a ──► default sink │ │ │ │ (same for z, s, x) │ └──────────────────────────────────────────────────────────────┘ ``` Each deck stream outputs raw decoded PCM. The PipeWire graph handles all DSP: - **Pitch shifting** and **time stretching** — rubberband LADSPA via `libpipewire-module-filter-chain` - **Volume (fader)** — `wpctl set-volume` on the stream node - **Pitch/Tempo params** — `pw-cli set-param Props '{ params = [...] }'` ## Dependencies ### System ```bash # Arch pacman -S pipewire wireplumber pipewire-audio ladspa rubberband # Ubuntu / Debian apt install pipewire wireplumber pipewire-audio ladspa-sdk rubberband-ladspa # Fedora dnf install pipewire wireplumber ladspa rubberband-ladspa ``` Verify the rubberband LADSPA plugin is present: ```bash analyseplugin ladspa-rubberband.so # should list labels including: rubberband_pitchshifter_stereo ``` ### Rust ``` pipewire-rs (pipewire = "0.9.2") symphonia (audio decoding) ratatui + crossterm rosc (OSC) tokio, crossbeam-channel, serde_json ``` ## Build & Run ```bash cargo build --release ./target/release/djdeck ``` Logs → `/tmp/djdeck.log`. Set `RUST_LOG=debug` for verbose output. On startup djdeck will: 1. Load `libpipewire-module-filter-chain` for each deck (rubberband LADSPA) 2. Start 4 PipeWire playback streams, each targeting its filter-chain sink 3. Wait 800ms for PipeWire to settle, then resolve node IDs via `pw-dump` 4. Start the TUI and OSC server Node IDs for each deck are shown in the deck panel title: `[pw:stream/filter]`. ## Keyboard Reference | Key | Action | |-----|--------| | `a / z / s / x` | Focus deck A / Z / S / X | | `Shift + A/Z/S/X` | Open file selector for deck | | `Space` | Play / Pause | | `Enter` | Stop (return to start) | | `← / →` | Seek ±1% | | `c` | Set cue point | | `v` | Jump to cue point | | `↑ / ↓` | Fader ±5% (via `wpctl set-volume`) | | `[ / ]` | Pitch ±0.5 semitones (via `pw-cli`, rubberband) | | `, / .` | Tempo ±1% (via `pw-cli`, rubberband) | | `q` | Quit (unloads filter-chain modules) | **File selector:** type to filter, `↑/↓` navigate, `Enter` load, `Esc` cancel. Files indexed from CWD, cached at `$XDG_CACHE_HOME/djdeck/file_index.json`. ## OSC Reference ### Incoming (port 9000) | Address | Args | Description | |---------|------|-------------| | `/deck//play` | | Play | | `/deck//pause` | | Pause | | `/deck//stop` | | Stop | | `/deck//seek` | `f` 0.0–1.0 | Seek to position | | `/deck//fader` | `f` 0.0–1.0 | Set volume (wpctl) | | `/deck//pitch` | `f` semitones | Set pitch (rubberband) | | `/deck//tempo` | `f` multiplier | Set tempo (rubberband) | | `/deck//cue/set` | | Mark cue point | | `/deck//cue/goto` | | Jump to cue point | `` = 0 (A), 1 (Z), 2 (S), 3 (X) ### Outgoing (port 9001) | Address | Args | |---------|------| | `/deck//loaded` | `s` track name | | `/deck//position` | `f` 0.0–1.0 | | `/deck//vu` | `f f` L R | ## Inspecting the graph ```bash # See all djdeck nodes pw-dump | jq '.[] | select(.info.props["node.name"] | startswith("djdeck"))' # Live graph view qpwgraph # or helvum # Check filter-chain params for deck A pw-cli set-param Props '{ params = ["pitch:Pitch scale" "1.0594"] }' # Volume on stream node wpctl set-volume 0.85 ``` ## Adding more effects The effect chain abstraction is in `src/effect_chain.rs`. The filter-chain module `filter.graph` in `EffectChain::load()` accepts multiple nodes wired in series: ``` filter.graph = { nodes = [ { type = ladspa name = pitch plugin = ladspa-rubberband label = rubberband_pitchshifter_stereo ... } { type = ladspa name = eq plugin = lsp-plugins-ladspa label = para_equalizer_x8_stereo ... } ] links = [ { output = "pitch:Output L" input = "eq:Input L" } { output = "pitch:Output R" input = "eq:Input R" } ] } ``` No Rust changes needed — just extend the `args` string in `EffectChain::load()`.