# Mixon 8 Pro jog-display protocol for a Rust implementation ## 1. Scope and confidence This guide describes MIDI 1.0 display feedback. Your audio engine remains responsible for playback, tempo, pitch, keylock DSP, and elapsed/remaining time. The messages below change visual feedback only. Evidence labels used here and in the fixtures: | Label | Meaning | | --- | --- | | `replayed` | Sent independently with Serato/djay closed; user confirmed the physical result | | `captured` | Recorded in application output with matching observed values; exact packet not independently replayed | | `mapping` | Supported by the installed official djay mapping | | `inferred` | Generalized from the observed protocol; hardware verification remains | Tests used the left physical jog, including logical decks 1 and 3. Do not treat the four-deck encoding pattern as independent confirmation of decks 2 and 4. ## 2. MIDI transport - Observed macOS CoreMIDI destination: `Reloop Mixon 8 Pro`. - Observed USB VID/PID: `0x26AD` / `0xAEF5`; the destination name and IDs remained the same after moving the rear connection from USB A to USB B. - Use your existing MIDI output transport. These messages worked through CoreMIDI; the display did not require a separate display-driver API in our tests. - Send the complete byte arrays below. They include MIDI status bytes and, for SysEx, `F0` and `F7`. Do not add USB-MIDI cable/CIN framing when using a MIDI API. - Incoming event decoding below assumes your MIDI library has already framed complete MIDI messages. A raw-byte parser must separately handle running status, fragmented SysEx, and interleaved real-time bytes. - MIDI Monitor must spy on **output to destinations** to see host display writes. Messages labeled **From Reloop Mixon 8 Pro** alone mostly show controller input. CoreMIDI accepting a send is not a hardware acknowledgement. No acknowledgement, readback, or capability-negotiation protocol has been established. ## 3. Deck addressing and byte conventions All packet literals in this document are hexadecimal. Values in descriptions are decimal unless prefixed with `0x`. | Logical deck | SysEx deck byte | Note On status | CC status | Physical jog | | --- | --- | --- | --- | --- | | 1 | `01` | `90` | `B0` | Left | | 2 | `02` | `91` | `B1` | Right, inferred | | 3 | `03` | `92` | `B2` | Left | | 4 | `04` | `93` | `B3` | Right, inferred | MIDI channel numbers in most monitors are 1-based; the low status nibble is 0-based. For these deck messages it is `deck - 1`. ### Numeric SysEx envelope ```text F0 00 20 7F DD FF N0 N1 ... F7 │ │ └─ one hexadecimal nibble per MIDI data byte, high to low │ └──── field ID └─────── logical deck 01..04 ``` Preserve the observed `00 20 7F` prefix. No checksum was present in these numeric packets. Six-nibble packets are 13 bytes total; eight-nibble packets are 15 bytes. Each payload byte is `00..0F`. This is **not** ordinary 7-bit MIDI integer packing, ASCII text, or little-endian bytes. ## 4. Output fields | Field | Encoding | Evidence | | --- | --- | --- | | BPM | SysEx field `01`, 6 nibbles, `bpm_centi + 5` | Deck 1 replayed at 125.0; captured at 81.0 and 69.0 | | Pitch percentage | SysEx field `02`, 6 nibbles, `0x800000 + pitch_centi_percent` | Deck 1 replayed at ±2.5%; captured at ±8.0% | | Display time | SysEx field `04`, 8 nibbles, `0x80000000 + milliseconds` | Deck 1 83,400 ms and animation replayed; deck 3 33,300 ms replayed | | Pitch range | `[90 + deck - 1, 0E, range_percent]` | Deck 1 16 replayed; 8 captured | | Needle | `[B0 + deck - 1, 2B, position]` | Deck 1 animation using 1..88 replayed | | Blue track-progress ring | `[B0 + deck - 1, 07, position]` | Deck 1 animation using 1..88 replayed; mapping maximum 88 | | Keylock off | `[90 + deck - 1, 0D, 3F]` | Deck 1 replayed | | Keylock on | `[90 + deck - 1, 0D, 41]` | Deck 1 replayed | | Track duration | SysEx field `03`, 8 nibbles, `0x80000000 + milliseconds` | Captured only; omit from the initial integration path | ### BPM and signed pitch Prefer integer units at the packet-builder boundary: - `bpm_centi = 12_500` means 125.00 BPM; encoded value is `12_505`. - `pitch_centi_percent = 250` means +2.50%; encoded value is `0x8000FA`. - `pitch_centi_percent = -250` means −2.50%; encoded value is `0x7FFF06`. The `+5` BPM bias reproduces our Serato captures and the successful replay. The exact rounding behavior between tenths is unverified. Read-only inspection of djay's BPM sender suggested it rounds to tenths and sends a multiple of ten; the bias should be treated as a working Serato convention, not a universal rule. If converting from floats, reject non-finite values and choose a documented rounding policy before entering the encoder. Do signed pitch arithmetic before converting to an unsigned payload; casting a negative percentage first is wrong. The display's BPM should be whatever your engine intends to show. For ordinary tempo scaling, `effective_bpm = base_bpm * (1 + pitch_percent / 100)` gives the observed 75 BPM → 81 BPM at +8%, and → 69 BPM at −8%. ### Time and elapsed/remaining mode Your software chooses which nonnegative millisecond value to send: ```text elapsed: max(0, playback_position_ms) remaining: max(0, track_duration_ms - playback_position_ms) ``` Both observed elapsed and remaining packets retain the leading `8` nibble. **Bit 31 does not identify remaining mode in these captures.** Preserve the observed offset; negative time, its sign representation, and the full numerical range have not been tested. A conservative encoder can accept `0..0x0FFF_FFFF` milliseconds, preserving the observed leading nibble. This API range is not a claim that every value can be displayed physically. For left-deck TIME mode, Serato additionally sent: | Mode | Accompanying output | | --- | --- | | Elapsed | `94 46 00` | | Remaining | `94 46 7F` | Those notes accompanied both toggle directions, but their independent effect on the display label versus button LED has not been isolated. The djay mapping uses base-channel note `46` with 63/65 and reversed boolean polarity for elapsed mode. **Do not include either scheme as a verified label command.** Send numeric time normally and keep optional label experiments separate. The successful animation also sent `94 46 00`; its individual effect was not tested. ### Needle and blue ring These are independent CC fields. Updating time does not animate either one. The successful demo used: ```text needle_position = 1 + floor(frac(revolutions) * 88) progress_position = min(88, 1 + floor(clamp(track_fraction, 0, 1) * 88)) ``` Here `frac(x) = x - floor(x)`, including for negative motion. Serato's needle wrapped approximately every 1.8 seconds, consistent with 33⅓ RPM. The standalone demo used `revolutions = elapsed_seconds / 1.8` and updated at 40 Hz. For your DJ software, derive phase and track fraction from engine state so seek, pause, pitch changes, and scratching can be reflected. Absolute needle orientation, reverse behavior, segment zero, and exact end-of-track semantics remain untested. The formulas are demo conventions that visibly worked, not calibrated geometry. For zero/unknown duration, define your UI policy explicitly; no clear/blank command is established, so do not divide by zero or invent a blanking packet. ### Keylock and range Keylock symbol velocities are decimal 63/65 (`3F`/`41`), not ordinary 0/127 button LED values. This command does not enable time stretching or pitch correction. Apply the user's keylock setting in your DSP and send its visual state separately. Pitch range uses the decimal percentage directly: 16% → `10`, 8% → `08`. Only 16% has been independently replayed. Do not infer support for every possible MIDI data-byte value or assume this output changes the fader's input scaling. ## 5. Hardware deck and USB indicators With Serato/djay closed, pressing left DECK 1/3 changed the physical indicator from deck 1 to deck 3. The controller sent: ```text 92 08 7F deck 3 active 90 08 00 deck 1 inactive ``` There was no accompanying host output. A later deck 3 time packet worked: ```text F0 00 20 7F 03 04 08 00 00 00 08 02 01 04 F7 33,300 ms ``` Maintain independent states for all four logical decks. A reasonable inferred input decoder recognizes note `08` on channels 1..4, nonzero Note On as active and zero-velocity Note On / Note Off as inactive. Only the transition above was physically correlated. Do not confuse it with input from unrelated channels. Track the two sides separately: 1/3 left and 2/4 right. On an active report, set that side's active deck and repaint it. On an inactive report, clear selection only if that exact deck is still active; this prevents a late deck 1 off message from undoing the earlier deck 3 on message. Start with unknown selection until observed or otherwise established by your application. No selection query has been identified. The USB indicator showed **2** once the rear computer connection and channel source were both set to USB B, without a running DJ application or test sender. During the earlier USB A connection, changing the channel source alone left the indicator at 1. Let the hardware handle this indicator; no host override command has been established. Channel-source changes included CC `22` (decimal 34): ```text B2 22 5F B2 22 1F ``` These are raw reports, not decoded absolute USB identifiers. Their meaning appears relative to the connected host, but is unverified. Do not equate `1F` with USB A and `5F` with USB B. Preserve the raw values if useful for later investigation. ## 6. Minimal Rust packet builders These examples take integer units and return full MIDI messages. Bounds are conservative application limits, not measured physical display limits. ```rust fn sysex(deck: u8, field: u8, value: u32, digits: usize) -> Result, &'static str> { if !(1..=4).contains(&deck) || ![6, 8].contains(&digits) || field >= 128 { return Err("invalid deck, field or width"); } if digits == 6 && value > 0xFF_FFFF { return Err("value exceeds six nibbles"); } let mut bytes = vec![0xF0, 0x00, 0x20, 0x7F, deck, field]; for nibble in (0..digits).rev() { bytes.push(((value >> (nibble * 4)) & 0xF) as u8); } bytes.push(0xF7); Ok(bytes) } fn bpm_packet(deck: u8, bpm_centi: u32) -> Result, &'static str> { if bpm_centi > 99_900 { return Err("BPM outside chosen 0..999 limit"); } sysex(deck, 0x01, bpm_centi + 5, 6) } fn pitch_packet(deck: u8, centi_percent: i32) -> Result, &'static str> { if !(-10_000..=10_000).contains(¢i_percent) { return Err("pitch outside chosen -100..100 percent limit"); } sysex(deck, 0x02, (0x80_0000_i32 + centi_percent) as u32, 6) } fn time_packet(deck: u8, ms: u32) -> Result, &'static str> { if ms > 0x0FFF_FFFF { return Err("time exceeds chosen range"); } sysex(deck, 0x04, 0x8000_0000 + ms, 8) } fn deck_message(deck: u8, kind: u8, number: u8, value: u8) -> Result<[u8; 3], &'static str> { if !(1..=4).contains(&deck) || ![0x90, 0xB0].contains(&kind) || number >= 128 || value >= 128 { return Err("invalid deck message"); } Ok([kind + deck - 1, number, value]) } // Examples, ready for your MIDI transport's send(&[u8]) equivalent: // bpm_packet(1, 12_500)?; // pitch_packet(1, -250)?; // time_packet(3, 33_300)?; // deck_message(1, 0x90, 0x0E, 16)?; // range 16% // deck_message(1, 0x90, 0x0D, 65)?; // keylock symbol on // deck_message(1, 0xB0, 0x2B, 23)?; // needle position // deck_message(1, 0xB0, 0x07, 44)?; // blue progress position ``` Use a dedicated wrapper to constrain needle/progress positions to the tested 1..88 interval. The generic short-message builder only validates MIDI syntax. The JSON fixtures provide decimal byte arrays and hexadecimal equivalents for language-independent tests. They distinguish replay evidence from captures. ## 7. State, scheduling, and reconnects Recommended integration behavior (software design, not additional protocol facts): 1. Keep a display snapshot per logical deck: effective BPM, signed pitch percent, range, selected numerical time, needle phase, track progress, and keylock. 2. Serialize MIDI output on a worker/event-loop thread. Keep allocation, device discovery, and MIDI I/O out of the real-time audio callback. Publish engine snapshots to the worker using your existing thread communication mechanism. 3. A 40 Hz tick worked for a single deck's three animated fields. Start there, coalesce stale snapshots, and send only changed encoded values. This is a tested starting point, not a measured maximum throughput or four-deck guarantee. 4. Update BPM, range, pitch, and keylock on changes. Stop synthetic motion when paused; scratching/seeking can still change position. On track load or seek, repaint all affected fields together from one consistent snapshot. 5. Cache the last successfully submitted bytes per `(deck, field)`. Update a cache entry only after the transport accepts its packet. If a send fails, preserve unsent changes for retry or invalidate the deck and repaint. 6. On a deck-selection report, force a full repaint of that logical deck. Inactive-deck buffering is unverified; do not rely on old hidden writes. 7. On disconnect, stop sending to the stale endpoint. Re-enumerate on reconnect, clear the output cache and selection assumptions, and repaint when routing is established. A changed connection must not be hidden by a stale "unchanged" cache. Check your transport's error result; do not assume a callback returning normally means success if the API reports errors another way. 8. Keep experimental duration, time-label, end warning, and initialization messages out of routine output until separately validated. No keepalive requirement, reset packet, or blank-display sequence has been established. In our tests Serato/djay were closed to prevent competing display writers. An initial integration should likewise have one application owning display updates. ## 8. Startup and remaining hardware checks **Cold-start initialization is unresolved.** Our display writes succeeded after Serato had already initialized the controller. No handshake sequence has been isolated, and no claim is made that it is either required or unnecessary. The 26-byte controller-originated SysEx in the first capture is not established as a display write or a packet that custom software should echo. Before considering the integration complete, verify: - Power-cycle the controller with DJ applications closed, connect your program, and send a known-good time packet. If it fails, capture both MIDI directions while launching a supported application and isolate what enables display writes. - Test both right-hand decks and a full repaint after 1↔3 and 2↔4 switches. - Test reconnects on both rear USB ports, source selection, and cache invalidation. - Check BPM rounding near tenth boundaries, range values your software supports, progress endpoints, needle orientation/reverse motion, and zero-duration policy. - Isolate elapsed/remaining label behavior if your UI needs to change that label. - Independently test duration field `03` and any end-of-track warning before use. No firmware flashing, patched driver, or authentication bypass was used in the successful display tests. ## 9. Optional replay using the existing macOS helper The parent folder's `mixon-display-test` accepts a JSON list of complete MIDI byte arrays. Its existing fixture files can be replayed directly: ```sh cd .. ./mixon-display-test --packets bpm-pitch-demo.json ./mixon-display-test --packets negative-pitch-demo.json ./mixon-display-test --packets deck3-time-demo.json ./mixon-display-test --packets keylock-on.json ./mixon-display-test --packets keylock-off.json ./mixon-display-test --animate 20 ``` Run from this document's directory before `cd ..`; close other display writers. Select deck 3 physically for its time test. `--animate` targets deck 1 and is a synthetic demo, not a transport-synchronized player. `golden-packets.json` has metadata around each packet and is not directly a sender input file. To replay an individual fixture, write `[fixture.bytes]` as JSON, then pass that file to `--packets`. Capture-only fixtures have not all been physically replayed. Keep that distinction when reporting your own verification results.