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-volumeon the stream node - Pitch/Tempo params —
pw-cli set-param <node-id> Props '{ params = [...] }'
Dependencies
System
# 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:
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
cargo build --release
./target/release/djdeck
Logs → /tmp/djdeck.log. Set RUST_LOG=debug for verbose output.
On startup djdeck will:
- Load
libpipewire-module-filter-chainfor each deck (rubberband LADSPA) - Start 4 PipeWire playback streams, each targeting its filter-chain sink
- Wait 800ms for PipeWire to settle, then resolve node IDs via
pw-dump - 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/<n>/play |
Play | |
/deck/<n>/pause |
Pause | |
/deck/<n>/stop |
Stop | |
/deck/<n>/seek |
f 0.0–1.0 |
Seek to position |
/deck/<n>/fader |
f 0.0–1.0 |
Set volume (wpctl) |
/deck/<n>/pitch |
f semitones |
Set pitch (rubberband) |
/deck/<n>/tempo |
f multiplier |
Set tempo (rubberband) |
/deck/<n>/cue/set |
Mark cue point | |
/deck/<n>/cue/goto |
Jump to cue point |
<n> = 0 (A), 1 (Z), 2 (S), 3 (X)
Outgoing (port 9001)
| Address | Args |
|---|---|
/deck/<n>/loaded |
s track name |
/deck/<n>/position |
f 0.0–1.0 |
/deck/<n>/vu |
f f L R |
Inspecting the graph
# 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 <filter-node-id> Props '{ params = ["pitch:Pitch scale" "1.0594"] }'
# Volume on stream node
wpctl set-volume <stream-node-id> 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().
Description
Languages
Rust
100%