Bendik Aagaard LynghaugandClaude Opus 4.8 784cc41ad4
Release / build (x86_64, ubuntu-latest, /sccache, x86_64-unknown-linux-gnu) (push) Successful in 2m52s
Release / build (aarch64, aarch64, /var/lib/gitea-runner/sccache, aarch64-unknown-linux-gnu) (push) Successful in 5m18s
Release / update-aur (push) Successful in 27s
Packaging: release CI (both arch, sccache, AUR, Arch registry), PKGBUILD, MIT LICENSE
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLUwWE2KmFPzhKaf67tWbx
2026-09-13 17:17:26 +02: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                     │
│                        [pitch → lpL/lpR → reverb]            │
│                    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 — rubberband Semitones port (requires ladspa-rubberband)
  • Cutoff filter — lowpass_iir + highpass_iir (swh-plugins), Cutoff Frequency port:
    • Negative value → lowpass (kill highs), positive → highpass (kill lows), 0 → flat
  • Reverb — plate Dry/wet mix port (swh-plugins): 0=dry → 1=wet
  • Volume (fader) — wpctl set-volume on the stream node
  • Tempo — varispeed in audio engine; rubberband compensates pitch automatically

Dependencies

System

Required:

# Arch
pacman -S pipewire wireplumber pipewire-audio ladspa rubberband

# Ubuntu / Debian
apt install pipewire wireplumber ladspa-sdk rubberband-ladspa

# Fedora
dnf install pipewire wireplumber ladspa rubberband-ladspa

Required (cutoff + reverb):

# Arch
pacman -S swh-plugins

# Ubuntu / Debian
apt install swh-plugins

# Fedora
dnf install swh-plugins

Verify the rubberband LADSPA plugin is present:

python3 -c "
import ctypes; lib = ctypes.CDLL('/usr/lib/ladspa/ladspa-rubberband.so')
lib.ladspa_descriptor.restype = ctypes.c_void_p
class D(ctypes.Structure):
    _fields_ = [('uid',ctypes.c_ulong),('label',ctypes.c_char_p),('props',ctypes.c_int),('name',ctypes.c_char_p)]
i=0
while True:
    d=lib.ladspa_descriptor(i)
    if not d: break
    print(ctypes.cast(d,ctypes.POINTER(D)).contents.label.decode()); i+=1
"
# should include: 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. Start 4 PipeWire playback streams, each targeting its filter-chain sink
  2. Load libpipewire-module-filter-chain for each deck via pw-cli load-module (pitch → lpL/lpR → reverb)
  3. Wait 800ms for PipeWire to settle, then resolve node IDs via pw-dump
  4. Explicitly link each chain's output to the default sink via pw-link -L
  5. 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 (rubberband Semitones port)
, / . Tempo ±1% (varispeed; rubberband compensates pitch)
- / = Cutoff ±500 (negative=LP kills highs, positive=HP kills lows, 0=flat)
o / p Reverb wet ±5% (requires swh-plugins)
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().

S
Description
Terminal DJ deck: multi-deck audio player with beat-sync, pitch/tempo stretch, and effects, low-latency to PipeWire
Readme MIT
116 KiB
v0.1.0
Latest
2026-09-13 15:17:26 +00:00
Languages
Rust 99.3%
Shell 0.7%