Files
djdeck/README.md
2026-03-05 23:06:54 +01:00

167 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <node-id> 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/<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
```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 <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()`.