trimixxx0
Run your own emulated TriMixxx deck (trimixxx0, a Raspberry Pi 4 under QEMU) with `pi-qemu deck` to test deck software without the hardware - up in ~3 s from a snapshot of main's latest, screenshots o
SKILL.md.claude/skills/trimixxx0/SKILL.md
GitHubtrimixxx0: your own emulated deck
pi-qemu deck gives you a deck of your own: its own copy of the card, ssh
port, ssh alias and control socket. It is headless and silent. It starts in
about 3 s from a snapshot, already booted with Mixxx running; a cold boot
takes about 7 s. That snapshot, the golden, is main's latest: it is made
again whenever main changes what goes onto a deck, so a new deck runs what
main has now, and your own changes are what you deploy on top. Other agents
may be running theirs at the same time, and the person at the laptop may be
running their own (with windows). Yours touches none of them.
pi-qemu is the one tool for decks (pi-qemu help, pi-qemu deck,
pi-qemu deck VERB --help). Every verb names its deck: your instance's NAME,
or --host ALIAS for a real deck. There is no default deck. For anything
not covered here (running a card of your own, USB sticks, troubleshooting),
read pi-qemu/README.md.
Run it from your checkout (or worktree): pi-qemu deploys the code of the checkout it runs in, and finds the built QEMU and the golden card in the main checkout by itself. In a git worktree, read "Working from a git worktree" below first.
Rules
- Pick one NAME and only ever touch that instance. Make it unique to
your task, e.g.
fix-sync-meter.- Never
pkillqemu, pi-qemu or docker. - Never
deck rm|kill|stop|downa NAME you did not create. - Never
deck goldenorimage build trimixxx0unless asked: they replace the golden pair everyone's instances start from.image goldenis fine: it only ever makes it from main's code.
- Never
- Never
--hosta real deck (trimixxx-pi,trimixxx-pi-2,trimixxx2, ...) unless the person asked you to, for that deck. - No sound on the Mac, ever. Never pass
--audio speakers. To check audio, record it to a WAV file and analyse the file (see below). - Reuse, do not restart.
deck up NAMEon a running instance only reports it, so call it at the start of every step. To change how it runs (--fresh,--boot,-- --audio ...),rmit first: on a running instanceuprefuses options rather than ignore them. - Clean up, or say what is left. See "Before you finish". Not optional.
Start (or reuse) your deck
pi-qemu deck list # "no instances", or what runs (yours may already)
pi-qemu deck up NAME # ~3 s from the golden snapshot; reports it if running
The instance lives in ~/.pi-qemu/instances/NAME/. Each one needs 2 GB of
RAM; if list already shows about 4 running, ask before starting another.
- Main's latest first.
upends by saying where its card came from (from 5f0b01a of 2026-10-16 15:20: main's latest). When main has just moved on,upfirst waits for the golden to be made again: a few minutes, more when much of Mixxx is rebuilt, with its steps as they go. Give it a long timeout (10 min, or run it in the background); if your shell gives up first, runupagain: it waits for the same remake. When the golden can't be made (main doesn't build, the main checkout isn't at main, a remake that looks stuck),upsays so and starts from the last golden: say so in your results, and tell the nightman (or Sam) about a stuck one; stopping it is theirs. - A stopped deck may start fresh. If main moved on while your deck was
stopped, its next
upstarts it from the new golden, saying first what it loses: what you deployed, files you put on it, its saved state. Deploy your changes again then. A running deck is never replaced (uponly says it is behind); a deck fromup --from CARDnever is. up NAME --fresh: start again from the golden snapshot; your card's changes are lost.up NAME --boot: boot instead of restoring (~7 s), e.g. to test the boot itself. Thendeck ready NAME.up NAME --from CARD: boot a card of your own (a clone of it), e.g. a release card.stop NAME: suspend (~5 s; leaves a ~1 GB snapshot until the nextup, which resumes it in ~3 s, on a new ssh port thatdeck sshfollows; unless main moved on meanwhile: then it starts fresh, as above).up NAME --window: also shows its screen and panel on the Mac, but only when the person wants to watch.
Look at it
pi-qemu deck shot NAME "$TMPDIR/NAME-1.png" # the 1280x800 screen, headless; Read the PNG
pi-qemu deck leds NAME # the S3's LEDs as JSON: play, cue, loopIn/Out, ringA/ringB
pi-qemu deck status NAME # Pi running, S3 link, ssh port, run dir
pi-qemu deck ssh NAME 'tail -50 /tmp/mixxx/mixxx.log' # Mixxx (also stderr.log; .1 = previous run)
pi-qemu deck ssh NAME 'tail -f /tmp/mixxx/stderr.log' # the same lines as they happen, without times
pi-qemu deck ssh NAME 'journalctl -b -u trimixxx-launchd -n 50'
mixxx.log is written in batches (Mixxx flushes it at critical), so its
last minutes may not be there yet; stderr.log has every line at once. To
time something, stamp stderr.log as it grows (a tail -F into a small
Python loop on the deck) and read that.
Put screenshots in a temp or scratch directory, not in the repo. Take one after anything you expect to change the screen, and look at it before you conclude. The deck's clock is UK time (Europe/London), which may differ from the Mac's: mind the offset when matching its logs with output on the Mac.
Poke it like a DJ
These go through the virtual S3 as MIDI on the Pi's UART, exactly as the real
board sends them, on trimixxx0's wiring (mixxx_config/units/trimixxx0.json):
pi-qemu deck press NAME play # press+release, 80 ms; `press NAME cue 600` holds
pi-qemu deck down NAME cue # ... `deck up NAME cue` releases
pi-qemu deck jog NAME 3240 # +clockwise, 12960 ticks per turn
pi-qemu deck touch NAME on # platter touch (scratch); `touch NAME off`
pi-qemu deck browse NAME -2 # the track encoder, + is up (50 at most at once: more loses detents)
pi-qemu deck press NAME push # encoder push: opens the library, then activates the selection
pi-qemu deck tempo NAME center # fader 0..16383, 8192 (center) the middle
pi-qemu deck midi NAME 90 3C 7F # raw bytes, for anything else
pi-qemu deck stick NAME insert SAM3 # a rekordbox stick image, from archives/sticks/ (read-only); `stick NAME list`
pi-qemu deck stick NAME insert /some/dir # a folder as a FAT32 stick, nothing copied (or a .stick file)
pi-qemu deck stick NAME insert-rw X.img # an image file the deck may write to ("Record set")
pi-qemu deck input NAME set.wav # the sound card's line input hears it (16-bit stereo 44.1 kHz); none: silence
Controls: tempo-range keylock loop8 loop4 loop-double loop-halve back hotcue1 hotcue2 hotcue3 hotcue4 slip sort play cue loop-in loop-out reloop push jog-touch. The S3's MIDI contract is firmwares/trimixxx-midi/lib/PiLink/MidiMap.hpp.
The touchscreen, with xdotool on the deck's X display (a faithful touch: Qt gets its taps as pointer events; a hover it cannot make, by design):
pi-qemu deck tap NAME 130 331 # a fingertip on that pixel
pi-qemu deck longpress NAME 500 300 # held 800 ms, past the long press
pi-qemu deck swipe NAME 100 300 700 300 # left to right: BACK
pi-qemu deck flick NAME 500 500 500 200 # a kinetic scroll
pi-qemu deck key NAME Down Return # X key events
pi-qemu deck where NAME # the pointer's position
- A copy of a real stick (a DJ's files on an external drive): insert
its folder, or a
.stickfile that also gives the real stick's layout, boot sector and size. See "A folder as a stick" inpi-qemu/README.md. - The line input (what "Record set" records) is on the same card as the
output,
hw:CARD=CODEC,DEV=0, as on the real UCA222. A deck restored from a golden snapshot older than QEMU patch 20 has no input:deck up NAME --boot. It hears silence again afterdeck stopandup: give it the file again. "A stick the deck writes to" and "The line input" inpi-qemu/README.mdhave the rest. - The Bluetooth receiver (the home list's row): QEMU has no radio, so
its page says so.
pi_config/bluetooth/bluez-standin.pyplays BlueZ in bluetoothd's place, with devices that pair, connect, play, seek, take the deck's PLAY and skips, or go out of range, driven with busctl; its log says what the deck asked:pi_config/bluetooth/README.md, "On an emulated deck". - A slow stick:
stick NAME speed ID 4M(or4M/300for reads a second too,fullto lift it), before or while it is in. Sticks range from ~3 MB/s to over 100, so try what you change at a few speeds.stick NAME reads ID [reset]says what the Pi read off a folder stick, by file. - Load a track:
stick NAME insert SAM3,press NAME push, then browse and push until it loads, with a screenshot between steps. With no track loaded, most presses change nothing visible. - Check that bytes reached the Pi:
pi-qemu deck ssh NAME 'sudo grep ^3: /proc/tty/driver/ttyAMA': the S3 is on UART3 (^0:, the PL011, on a card from before the move).rx:counts what the S3 sent. It grows by 6 perpress(note on and note off), 3 perdown,up,touchorbrowsestep, 6 pertempo, and 3 per 63 ticks ofjog.tx:is what Mixxx sent back to the S3 (the LEDs). It growing after a press shows Mixxx handled it.
Several decks on one network (Pro DJ Link)
Decks started with the same --link NET share a network on their eth0s, as
CDJs in a booth share a switch: they see each other over Pro DJ Link, browse
each other's sticks, and load and play each other's tracks. Without --link
a deck's eth0 is on an empty switch.
pi-qemu deck up NAME-a --link NAME-net # says: NAME-a: player 4 at 169.254.x.y, MAC on link NAME-net
pi-qemu deck up NAME-b --link NAME-net # NAME-b: player 3 (it saw NAME-a holding 4)
pi-qemu link devices NAME-net # the players heard on it: number, name, address, MAC, deck
pi-qemu deck stick NAME-a insert SANDISK-E02C # a rekordbox stick in NAME-a...
pi-qemu deck press NAME-b push # ...is "4 <label>" in NAME-b's library: push, browse, push to load
pi-qemu deck link NAME-a # what it announces, who else is on its link
pi-qemu deck link NAME-a unplug # pull its cable (the Pi sees its link go down); plug puts it back
pi-qemu link capture NAME-net out.pcap 30 # every frame on the link for 30 s, for Wireshark or `prolink pcap`
- A link's name is global on the Mac, like a deck's: name it after your
task, put only your own decks on it, and never
--linka name another agent uses (pi-qemu link listshows every link and its decks). up --linktakes about 15-35 s: a restored deck takes its own MAC, waits its turn on the link, and claims a player number;upwaits for that and prints it. Decks on one link may be started in parallel.- The link stays with the deck through
stop/up;up NAME --link none(on a stopped deck) takes it off. - A plain stick (no rekordbox export, e.g. a folder of MP3s) is served as a
CDJ serves one: a CDJ's LINK shows its FOLDER category, and another deck
lists it as a Folders source. A rekordbox stick such as
SANDISK-E02C(read-only, nothing copied) is served with its whole library. rmyour decks when done: each takes its socket off the link with it.
A CDJ on your deck's link
An emulated CDJ-2000NXS, on Pioneer's own firmware, can join the link: a
"real" CDJ to test Pro DJ Link against. pi-qemu/README.md, "Emulated CDJs",
has the rest.
pi-qemu cdj build # once per checkout (~1.5 min); in a worktree, after prepare
pi-qemu cdj firmware ~/Downloads/C2KNXS.UPD # once per Mac: into archives/ (git-ignored), never in git
pi-qemu cdj up NAME-cdj --link NAME-net # ~25 s: "NAME-cdj: player 1 at 169.254.x.y, ... on link NAME-net"
pi-qemu link devices NAME-net 10 # 10 s: with a deck there, a CDJ re-claims and keep-alives up to ~5 s apart
pi-qemu cdj shot NAME-cdj cdj.png # its 480x234 screen; `cdj press NAME-cdj link` a key
pi-qemu cdj lamps NAME-cdj # the lamps its firmware lit, by name: PLAY_PAUSE, MASTER, ...
pi-qemu cdj rm NAME-cdj # when done: `deck rm` doesn't know CDJs
cdj window NAME-cdj (or up --window) opens its screen and panel, lit as
its firmware lights it: for Sam, not for an agent, which reads cdj shot and
cdj lamps.
- Expensive: a CDJ takes about two cores from boot on, idle or not (one
with
--dsp-model), and its logs grow ~0.5 GB an hour untilcdj rm. It runs at a real NXS's speed (keep-alives every 2.0 s), but a loaded track plays only withcdj up --dsp-model(the emulator's behavioural DSP: beats at the track's tempo, no audio). It then plays the moment it loads, with no cue and no PLAY, and with no master on the link it takes master; CUE does nothing in play until it has been stopped and cued once. - Centre its TEMPO slider before testing tempo or sync: an emulated NXS
says pitch -100% until its slider first moves. From
cdj2000-emulator/,.venv/bin/python -m tools.cdj_main.panel_control --port P analog 3 32768, then the same withanalog 2 32768(P:endpoints.panel_portin~/.pi-qemu/cdj/NAME/run/run.json).analog 2 Vmoves the slider: 0 is -10%, 65535 +10%.mixxx/lib/prolink/emu-captures/README.mdnames what else an emulated CDJ does differently. - In the crew, a minion's CDJs are
BRANCH-aandBRANCH-b, as its decks areBRANCHandBRANCH-b: the guard refuses other names,crew resources --for cdjcounts them, andcrew cleanremoves them. On one link a deck and a CDJ can't share a name, soBRANCH-bis one or the other there. - Never put its firmware or its screenshots in git: both are Pioneer's. The website's few, shown with a disclaimer, live in usr-ein/static-resources.
Deploy your own code onto it
pi-qemu deck deploy NAME config # 005: mixxx_config: mapping, scripts, skin, fonts, mixxx.cfg (~10 s)
pi-qemu deck deploy NAME mixxx # 004: the Mixxx fork: Docker arm64 build, swaps /usr/bin/mixxx
pi-qemu deck deploy NAME system # 002: pi_config: units, session, splash, eth0, hotspot, dj-usb
pi-qemu deck deploy NAME launcher # 003: trimixxx-launcher
pi-qemu deck deploy NAME ttymidi # 001: the serial<->MIDI bridge
pi-qemu deck deploy NAME # every step, in number order
The steps are pi-qemu/deploy/NNN_name.sh (on the Mac, reaching the deck as
ssh deck) and NNN_name.pi.sh (on the deck, as root); lib.sh there has
their contract. Name a step as mixxx, 004 or 004_mixxx. A new step is
a new number. A step that changed the boot flags has the deck reboot before
the next one; one that restarted the MIDI bridge or the launcher has the
session restarted at the end (Mixxx only opens their ports at start). After
a session restart deploy waits until Mixxx is ready and says
NAME: Mixxx is ready (sound open, the S3 connected). That means Mixxx is
running with its sound output and the deck's controller open, not that a
track plays; a screenshot is meaningful straight away. deck ready NAME
checks the same on its own.
Typical times: config 10 s, 75 s the first time in a worktree), plus any wait behind
another agent's Mixxx build. system ~20 s, mixxx ~25 s for a changed
file or two (ttymidi, launcher and mixxx leave the deck
alone when their new binary is the same as the deck's.
ttymidi, launcher and mixxx build in Docker. deploy stops early if
Docker's disk has less than 6 GB free. Freeing it is the person's call: tell
them, do not prune.
Mixxx builds go by content: each checkout's build tree keeps its own copy of
the sources, and a build recompiles exactly the files whose content changed
since that tree's last build (deploy's output says how many:
tree-sync: N change(s)). File timestamps, git checkouts and other agents'
builds do not affect it.
For a shell or script of your own that uses plain ssh/scp:
eval "$(pi-qemu deck env NAME)" makes ssh NAME and ssh deck reach it.
Working from a git worktree
Agents usually work in their own git worktree (the root CLAUDE.md has the
whole workflow). For the deck:
pi-qemu worktree preparefirst. A new worktree's submodules (mixxx/, itslib/prolink,mixxx_config/ttymidi) are empty directories.preparemakes them linked worktrees of the main checkout's repositories, in seconds, without downloading anything.deployruns it by itself, but you need it before editing Mixxx. Nevergit submodule update: its clones die with the worktree, and any commits in them with it.- Changing Mixxx:
git -C mixxx switch -c NAMEbefore committing there. The branch and its commits are then in the main checkout'smixxx/too. Commit the bump in this repo as well (git add mixxx). deck deploy NAME mixxxbuilds your worktree's Mixxx in its own Docker build tree, then swaps it onto your deck.- The first build in a worktree starts from a copy of the main checkout's tree and recompiles only what differs (~75 s in all).
- After that, each build recompiles only what changed, by content (~25 s).
- Mixxx builds queue: one compiles at a time across all agents, which keeps Docker's memory in bounds.
- Changing the S3's MIDI table (
firmwares/trimixxx-midi/lib/PiLink/MidiMap.hpp) or pi-qemu itself: the virtual S3 is compiled into pi-qemu, so build your own,pi-qemu/build.sh app, and run it by its path,pi-qemu/app/build/pi-qemu deck ..., for every call that should use it. Thendeck rmanddeck upyour deck so it runs on it. Your build never makes everyone's golden again (main's does, on PATH): to try a change to how it is made, give it a scratch one,PI_QEMU_CACHE=/tmp/x PI_QEMU_INSTANCES=/tmp/x/i, seeded with clones of.cache/stockand.cache/base(cp -c).
When ssh does not answer
pi-qemu deck console NAME 'systemctl --failed' 'ip -br a' 'dmesg | tail -30' # the serial console
~/.pi-qemu/instances/NAME/card.run/console.log has the boot from the first
kernel line after up --boot. After a restore it only has what came after
the restore.
If ready says Mixxx started before the MIDI bridge restarted, the S3 no
longer reaches it. Do what the message says: restart the session
(sudo systemctl restart getty@tty1), then ready again.
Check audio without sound
Mixxx's own recording of its main mix, fetched to the Mac:
pi-qemu deck record NAME 10 "$TMPDIR/NAME.wav" # 10 s; make the deck play first
Or everything the deck's sound card plays, from QEMU, for a new instance
(rm an old one first): pi-qemu deck up NAME -- --audio "wav:$TMPDIR/NAME.wav";
the file is valid at any moment.
The WAVs are 16-bit stereo at 44.1 kHz. Python's wave module reads them.
Measure the peak, the length of the loud part, and the frequency from zero
crossings.
A tone without Mixxx:
pi-qemu deck ssh NAME 'sudo systemctl stop getty@tty1; timeout 3 speaker-test -D plughw:0,0 -c 1 -r 44100 -t sine -f 440'.
-c 1: one tone on both channels at once (-c 2plays left, then right).- Exit code 124 is
timeoutending it, which is normal. - Stopping the session takes ~5 s. Afterwards bring Mixxx back with
pi-qemu deck ssh NAME 'sudo systemctl start getty@tty1', thenpi-qemu deck ready NAME.
Before you finish
Every time you end your work, whether it succeeded, failed or stopped part way:
- Run
pi-qemu deck list, andpi-qemu cdj listif you ran CDJs. - For each instance you started (
cdj rm NAMEfor a CDJ):- Remove it (
pi-qemu deck rm NAME) when nobody needs to look at it. - Or keep it, and say so in your final message: its name, whether it
is still running or suspended, and the exact command to remove it.
pi-qemu deck stop NAMEsuspends one you will come back to; that costs disk, not RAM, andupresumes it in ~3 s.
- Remove it (
- Working in a git worktree that is going away:
pi-qemu worktree release. It gives back the worktree's Docker build tree (~2 GB) and its submodule checkouts. It refuses while they hold uncommitted work, or commits that no branch has. Leave the worktree in place if the person may still want it, and say so. - Your final message always ends with a line like
Emulated decks: removed fix-sync-meter.orEmulated decks: fix-sync-meter still running - pi-qemu deck rm fix-sync-meter when done.