2026-03-08 11:14:11 +01:00
2026-03-05 23:06:54 +01:00
2026-03-05 23:06:54 +01:00

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 paramspw-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:

  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/<n>/play Play
/deck/<n>/pause Pause
/deck/<n>/stop Stop
/deck/<n>/seek f 0.01.0 Seek to position
/deck/<n>/fader f 0.01.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.01.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
tui dj software using pipewire for processing
Readme 124 KiB
Languages
Rust 100%