Skip to the content
PCBKitNew board
2 layers96.6 × 135 mmODJController, the printed circuit board in 3D

ODJController

primewildy/ODJ

Experiments in DJ controller software.

Size
96.6 × 135 mm
Copper layers
2
Thickness
1.6 mm
Parts
42
Nets
49
Pads
186
Tracks, vias
526, 11
Pours
1
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
24 May 2026
Updated
14 Jun 2026

Overview

ODJController is an open-source printed circuit board by primewildy, from the primewildy/ODJ repository on GitHub. It is a 2-layer board of 96.6 × 135 mm with 42 parts on 49 nets, routed with 526 tracks and 11 vias.

Its parts include 32 connectors, 4 resistors, 3 capacitors and 3 ICs. It belongs with the RP2040 boards of this library.

README

README.md

ODJ

A from-scratch DJ controller for Linux, written in Rust. Two decks, sub-3ms output latency on PipeWire, MIDI controller support, in-app BPM + beat-grid

  • key detection, harmonic-mix filtering, and a phase vocoder for key-locked tempo control.

ODJ UI

Built as a side project; on the road to driving a custom microcontroller hardware controller (RP2040 / ESP32-S3) speaking USB MIDI.

Status

Working end-to-end as a two-deck mixing tool you can DJ a set on. Tested with an AKAI LPD8 + homebrew ODJ Controller (hardware/) against a library of ~550 mp3/flac tracks. The breadboard prototype of the controller (Deck A: jog, transport buttons, 3-band EQ + volume + pitch faders, Play LED) is fully working. A two-deck carrier PCB (hardware/pcb/) is ready for fab — single Pico, JST-XH plug-in panel connectors for all controls, plug-in CJMCU-4051 mux modules. A 3D-printable faceplate (hardware/enclosure/, parametric OpenSCAD) matches it. Cue/PFL output works via a USB DAC dongle with PipeWire routing handled automatically by start-dj.sh.

Built features:

  • Two decks, cpal/PipeWire output at 256-frame buffers (~5.8 ms latency end-to-end; drops to ~2.9 ms at 128 if you run master only).
  • Optional second cpal stream for PFL / cue monitoring on a separate output device (e.g. a USB DAC). Headphone bus with its own volume and a Pioneer-style CUE↔MASTER blend so you can mix the cued decks with the main mix in your headphones.
  • Symphonia-based decode (mp3, flac, wav, aac/m4a).
  • Phase-vocoder key-lock (with identity phase-locking for cleaner highs)
    • vinyl-style coupled-pitch mode per deck.
  • 3-band EQ per deck (low 250 Hz shelf, mid 1 kHz bell, high 4 kHz shelf, ±25..+6 dB).
  • Per-deck play-envelope (5 ms fade) — no click on transitions.
  • BPM + beat-grid analysis (pure-Rust spectral flux + autocorr + brute force phase-aligned refinement, ~0.05 BPM precision).
  • Downbeat detection via beat_this exported to ONNX and run in-process from Rust via ort (CUDA EP). Red 4-bar lines on the beat grid; auto-snap of the playhead and cue point to the first downbeat on load. Optional — falls back to i%4==0 if the ONNX file isn't present.
  • Live stem separation via HTDemucs ( MansfieldPlumbing/Demucs_v4_TRT ONNX) on the GPU through ort + TensorRT + FP16. ~7 s/track on an RTX 4060 Laptop. Three per-deck rotary knobs (DRUMS / VOCALS / INSTRUMENTS), 3-colour overlayed stem waveforms, in-memory session cache (not persisted to disk — too big). Tracks load and play immediately; the mixer transparently switches from the single buffer to the 4-stem mix when the worker finishes.
  • Musical key detection (Krumhansl-Schmuckler) in Camelot notation.
  • Auto Sync (BPM match) + auto Beat Align (phase-snap on play start and on every manual grid edit — so nudging a wonky grid against a known-good reference deck audibly converges step-by-step).
  • Pioneer-style CUE state machine including the "Cue Play" commit.
  • Hot cues — 8 slots per deck with the same set-vs-jump-vs-preview semantics as CUE. Jumps are beat-quantised when Q is on (defers the teleport to the next beat so sloppy press timing still lands cleanly). Right-click a slot to label it, pick a colour, or delete it. Persisted per-track to .track-meta. Coloured tick markers appear on both waveforms.
  • Manual beat-grid editing — Grid Adjust tab in the source rail. Nudge ±1/±10 ms, skip ±1/2/4/8 beats, ½× / 2× BPM, set downbeat at playhead, reset. Locked by default each session so a stray click can't mangle a grid. Overrides persist to .track-meta; the manual grid wins over the analyser's refined output on every subsequent load.
  • Loops — beat-quantised IN/OUT, one-shot 4-beat auto-loop, ½ / ×2 length, graceful Exit (plays out the current iteration), live "recording" highlight from IN to the playhead.
  • Per-deck FX — beat-synced Echo (Type B, beat-picker UI) and Schroeder Reverb (Type A, continuous Time knob). Post-EQ, pre-fader so the cue bus tap is the wet signal.
  • Auto-mix — armed state pre-loads a candidate next track on the idle deck; active blend ramps gains and drum-stem gates over the outgoing track's last 16 bars. Any deck-affecting user input cancels in-flight.
  • Vinyl-style temporary push/pull nudge while held.
  • Paused jog = audible scrub (forward + reverse). Click-to-seek on either waveform.
  • Sortable track list (Title / Artist / Genre / Key / BPM / Length / Plays), favourites, harmonic compatibility filter, background analysis worker with disk persistence (and a catch_unwind around the DSP so a single pathological track can't kill the worker).
  • Session history — every track played for ≥30 s is logged to .history. History tab groups by 2-hour gap into sessions; one-click setlist export to clipboard.
  • Settings UI — audio/cue/MIDI device pickers, music-dir path, per-deck startup defaults. Persisted under XDG ~/.config/odj/.
  • System theme — light / dark follows the OS (gdbus + gsettings fallback); custom palette tokens for accents, stems and hot cues.
  • LPD8 hardcoded mapping (pads + 8 knobs) with on-screen mirroring.
  • Homebrew hardware controller prototype under hardware/ — RP2040
    • optical encoder + arcade buttons + 74HC4051 analog mux. Class- compliant USB MIDI; sysex-triggered reboot for one-command re-flash (hardware/flash.sh). Two-deck carrier PCB (one Pico, both decks, JST-XH plug-in headers, CJMCU-4051 mux modules) generated from a SKiDL netlist (hardware/pcb/odj_controller.py) → KiCad → Gerbers. Parametric 3D-printed faceplate in hardware/enclosure/.

See DESIGN.md for architecture and TODO.md for roadmap.

Setup

Beyond cargo + the system audio stack, two of the bigger features need out-of-band model files. The app runs fine without either — they're gracefully optional — but you'll be missing red downbeat lines and stem controls until they're in place.

All caches live under ~/.cache/dj/ (override with $XDG_CACHE_HOME).

Models — runtime cache layout

PathWhat it isSizeHow to get it
~/.cache/dj/model_final0.onnxbeat_this downbeat model~85 MBrun scripts/export_beat_this_onnx.py in a Python venv that has beat_this installed, then cp model_final0.onnx ~/.cache/dj/
~/.cache/dj/htdemucs.onnxHTDemucs stem-separation model246 MBcurl -L -o ~/.cache/dj/htdemucs.onnx https://huggingface.co/MansfieldPlumbing/Demucs_v4_TRT/resolve/main/demucsv4.onnx
~/.cache/dj/trt-engines/TensorRT engine cache~190 MB after first buildcreated automatically on first stem separation (~15 min compile, cached after)

Both files are runtime-loaded; nothing is bundled into the binary.

TensorRT — for the fast stem path

The stems crate uses ort's TensorRT execution provider. That needs TensorRT 10.x libs (libnvinfer.so.10, libnvinfer_plugin.so.10, libnvonnxparser.so.10) discoverable on LD_LIBRARY_PATH. The easiest way: extract them from PyPI's tensorrt-cu12-libs wheel:

sh
curl -L -o /tmp/trt.whl \
    https://pypi.nvidia.com/tensorrt-cu12-libs/tensorrt_cu12_libs-10.16.1.11-py3-none-manylinux_2_28_x86_64.whl
mkdir -p stem-spike/trt-libs && unzip -q /tmp/trt.whl -d stem-spike/trt-libs

start-dj.sh picks them up from stem-spike/trt-libs/tensorrt_libs/ automatically.

CUDA — what ort needs at runtime

ort ships against CUDA 12 but ships a CUDA EP that's flexible. On Arch with system CUDA 13.2 you can either:

  • Install CUDA 12 alongside (e.g. via the matching cuda-12.x AUR package), or
  • Use PyTorch's bundled CUDA-12 libs from a venv (set up via either spike script; PyTorch pulls them as nvidia-* packages).

start-dj.sh auto-prepends stem-spike/venv/lib/python*/site-packages/nvidia/*/lib/ to LD_LIBRARY_PATH if it exists, so the venv path "just works".

If neither is in place, ort silently falls back to CPU (stems take ~80 s/track instead of ~7 s; downbeat detection still works in seconds).

Music

--music-dir music by default (relative to CWD). Drop your library in there; first launch pre-analyses every track in the background (BPM, beat grid, downbeats, key) and caches results to <music-dir>/.analysis-cache.

Running

The convenient way (handles PipeWire routing for the cue stream and restores your normal audio setup on exit):

text
./start-dj.sh

That's a thin wrapper around cargo run --release with sensible flags and some PipeWire choreography (see docs/notes/pipewire_routing.md). Or run the binary directly:

text
cargo run --release -- [--device <cpal-name>] [--cue-device <cpal-name>] \
                      [--midi <port-substring>] [--music-dir <path>]

Defaults / behaviour:

  • --device unset → uses the cpal pipewire device for the master out. Override to target a specific sink directly. When the cpal device name is pipewire, the PIPEWIRE_NODE env var (if set) is honoured.
  • --cue-device unset → no cue stream is opened; deck cue toggles have no audible effect. Set this to a cpal device name to enable PFL monitoring. If cpal can't find that name in its ALSA enumeration (common for USB DACs on PipeWire systems), it falls back to looking up a PipeWire sink whose name contains the same substring and routes the cue stream to it via PIPEWIRE_NODE — so e.g. --cue-device "KT USB" finds alsa_output.usb-…KT_USB_Audio… without needing the full name. The two streams have independent clocks; drift is irrelevant for cue since master and cue are heard on separate transducers.
  • --midi "ODJ" is what start-dj.sh passes; matches the homebrew controller's USB-MIDI port name. The binary's default is "LPD8" for legacy dev with an AKAI pad; either substring works. Set to "" to disable MIDI entirely.
  • --music-dir music (relative to CWD). Audio files in this directory (non-recursive) populate the in-app track list.

Per-build controller quirks (e.g. a backward-wired fader) go in controls.toml in the working directory — see controls.toml.example. It's read at startup; currently supports invert_cc = [...] to flip specific MIDI CCs. Gitignored since it's hardware-specific.

The first launch with a populated music directory will spend a few minutes in the background pre-analysing every track (BPM + beat grid + key). Progress shows in the top bar. Cache file <music-dir>/.analysis-cache makes subsequent launches instant.

Controls

Keyboard

  • space Deck A play/pause toggle
  • c (hold) Deck A CUE (Pioneer state machine; press while paused at cue starts preview, release returns to cue)
  • b Deck B play/pause toggle
  • n (hold) Deck B CUE

LPD8 (factory PROG-1 defaults)

Layout — top row pads/knobs = Deck A, bottom row = Deck B:

text
[K1 K2 K3 K4]    [Pad 5 Pad 6 Pad 7 Pad 8]    ← Deck A
[K5 K6 K7 K8]    [Pad 1 Pad 2 Pad 3 Pad 4]    ← Deck B
PadDeckFunction
5APlay / pause toggle
6ACUE (Pioneer state machine, hold for preview)
7APull (slower while held — vinyl push/pull)
8APush (faster while held)
1BPlay / pause toggle
2BCUE
3BPull
4BPush
KnobDeckFunction
K1APitch (CC 64 = unity, ±8% range)
K2AVolume (linear 0..1)
K3AEQ high (CC 64 = flat, ±25..+6 dB)
K4AEQ low
K5BPitch
K6BVolume
K7BEQ high
K8BEQ low

If your LPD8 sends different note/CC numbers, every incoming MIDI message is logged to stderr — read them off and edit the match arms in src/midi.rs.

Project layout

text
/
├── Cargo.toml          # workspace + binary
├── start-dj.sh         # launch wrapper — PipeWire routing for the cue
│                       #   stream, auto-restores your normal setup
├── src/
│   ├── main.rs         # eframe entry, CLI parse, MIDI thread spawn
│   └── midi.rs         # LPD8 / ODJ Controller MIDI input + jog handler
├── crates/
│   ├── control/        # DeckCommand enum, MusicalKey, ring buffer types
│   ├── decode/         # symphonia → TrackBuffer
│   ├── analysis/       # BPM + beat grid + downbeats (ONNX) + key
│   ├── stems/          # HTDemucs ONNX wrapper
│   ├── audio/          # cpal stream(s), mixer, EQ, FX, phase vocoder
│   └── ui/             # eframe app — see crate-level breakdown in
│                       #   DESIGN.md §4 (auto_mix, grid_edit, history,
│                       #   palette, persistence, settings, theme, fonts)
└── hardware/
    ├── firmware/       # Pico SDK C project (breadboard Deck A)
    ├── pcb/            # two-deck carrier — SKiDL netlist + KiCad
    │   ├── odj_controller.py   # SKiDL → odj_controller.net
    │   └── odj.pretty/         # project-local footprints (Pico,
    │                           #   CJMCU-4051 mux module)
    ├── enclosure/      # 3D-printable faceplate — parametric OpenSCAD
    ├── SCHEMATIC.md    # Deck-A breadboard wiring reference
    └── flash.sh        # sysex-reboot auto-flash workflow

Acknowledgements

Beat-detection pipeline drew on the standard MIR cookbook (spectral flux

  • autocorrelation, with phase-aligned brute-force refinement). Key detection uses the Krumhansl-Kessler profiles. Pioneer CDJ CUE behaviour was researched against the CDJ-3000 / CDJ-2000NXS instruction manuals.

Parts

42
QtyValueFootprintReferences
40RResistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_HorizontalR1, R2, R3, R4
2100nFCapacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mmC1, C2
2CJMCU_4051odj:CJMCU_4051U2, U3
110uFCapacitor_THT:CP_Radial_D5.0mm_P2.50mmC3
1A-CUEConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ18
1A-ENCODERConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ25
1A-EQ_HIGHConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ3
1A-EQ_LOWConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ1
1A-EQ_MIDConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ2
1A-HPCUEConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ19
1A-PITCHConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ5
1A-PLAYConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ17
1A-SPARE5Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ6
1A-SPARE6Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ7
1A-SPARE7Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ8
1A-SYNCConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ23
1A-VOLUMEConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ4
1A_HPCUE-LEDConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ28
1A_PLAY-LEDConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ27
1B-CUEConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ21
1B-ENCODERConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ26
1B-EQ_HIGHConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ11
1B-EQ_LOWConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ9
1B-EQ_MIDConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ10
1B-HPCUEConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ22
1B-PITCHConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ13
1B-PLAYConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ20
1B-SPARE5Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ14
1B-SPARE6Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ15
1B-SPARE7Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ16
1B-SYNCConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ24
1B-VOLUMEConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ12
1B_HPCUE-LEDConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ30
1B_PLAY-LEDConnector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_VerticalJ29
1I2C-EXPConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ32
1RPi_Picoodj:RPi_Pico_THTU1
1SPARE-GPIOConnector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_VerticalJ31

Questions

How big is the ODJController PCB?

It measures 96.6 × 135 mm and has 2 copper layers, 1.6 mm thick.

What parts does the ODJController use?

42 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the ODJController KiCad files?

From the primewildy/ODJ repository on GitHub, at the commit this page was made from: the board is hardware/pcb/ODJController/ODJController.kicad_pcb and its schematic hardware/pcb/ODJController/ODJController.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the ODJController design in my own project?

Its repository states no licence, so its author keeps all rights: ask primewildy before reusing the design.

Similar boards

  • 2 layers30 × 50 mmModuleV400, the printed circuit board in 3D

    ModuleV400

    stuartpittaway/diyBMSv4

    Version 4 of the diyBMS

    42 parts1.1k
  • 2 layers139.7 × 64.8 mmSprig console, the printed circuit board in 3D

    Sprig console

    hackclub/sprig

    🍃 Learn to code by making games in a JavaScript web-based game editor.

    42 parts1.1kMIT
  • 2 layers90.5 × 50 mmMain control board, the printed circuit board in 3D

    Main control board

    jamro/tiny-engineer

    A small desktop robot that brings an AI coding assistant to life, physically acting out when it reads files, thinks, writes code, and finishes work.

    42 parts1.1k
  • 2 layers67.6 × 30.5 mmRgb to hdmi, the printed circuit board in 3D

    Rgb to hdmi

    hoglet67/RGBtoHDMI

    Bare-metal Raspberry Pi project that provides pixel-perfect sampling of Retro Computer RGB/YUV video and conversion to HDMI

    42 parts1kGPL-3.0
  • 2 layers190 × 80 mmPcb shield, the printed circuit board in 3D

    Pcb shield

    hackclub/OnBoard

    💡 Join 1,000 teenagers and make your first PCB with a $100 grant!

    42 parts915
  • 2 layers98 × 80 mmMai io, the printed circuit board in 3D

    Mai io

    whowechina/mai_pico

    Pico controller for Maimai

    42 parts475
  • 2 layers50.8 × 90.4 mmOBOGS PANEL, the printed circuit board in 3D

    OBOGS PANEL

    jrsteensen/OpenHornet

    OpenHornet 1:1 F/A-18C Simulator Repository

    42 parts431