Files

196 lines
7.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 │
│ [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:**
```bash
# 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):
```bash
# Arch
pacman -S swh-plugins
# Ubuntu / Debian
apt install swh-plugins
# Fedora
dnf install swh-plugins
```
Verify the rubberband LADSPA plugin is present:
```bash
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
```bash
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
```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()`.