From eaf5d64f90c0ffe595aeb24a96829bbaabcda9ad Mon Sep 17 00:00:00 2001 From: petrosilius Date: Sat, 22 Aug 2026 21:33:08 +0200 Subject: [PATCH 1/2] feat(devices): add Reloop Digital Jockey 2 Master Edition (200c:1009) Ploytec bulk device, 6 in / 4 out, 24-bit, 44.1 kHz. Verified on hardware: audio playback and capture, MIDI input, and LED output. Self-contained in devices/reloop_dj2.c so no existing device path changes. It reuses the shared protocol helpers and bit-interleaved codec read-only; core changes are registration only. Device specifics found by reverse engineering the vendor Windows driver (rldj2meu.sys) and confirming against hardware: - 44.1 kHz only. findInterfacesInConfig hardcodes 0xAC44 for every 200c:1009 branch; the sibling 200c:1005 is the model that gets 96 kHz. Driving this DAC at 96 kHz audibly distorts playback. - Bulk output uses all 8 sub-packets of the 4096-byte packet. Note that the shared ploytec_bulk_subpackets[] table describes only the first 4 (2048 bytes) while ploytec_process_out_bulk() reports PLOYTEC_FRAMES_PER_PKT (80) frames consumed, so half of every packet's audio is dropped. That looks like it affects the Xone devices too; left untouched here as it could not be tested on that hardware. See .claude/re/open_questions.md. - MIDI output must carry an explicit status byte. The firmware ignores messages using MIDI running status, which the ALSA sequencer bridge emits by default, so MIDI input works while no LED ever lights. Messages are reassembled with the status byte restored. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BASiuKMU5aCZGGKJYU8yXS --- .claude/re/open_questions.md | 33 ++ .claude/re/ploytec_device_table.md | 52 ++- linux/Makefile | 1 + linux/devices/reloop_dj2.c | 564 +++++++++++++++++++++++++++++ linux/devices/reloop_dj2.h | 89 +++++ linux/ozzy_core.c | 8 +- 6 files changed, 739 insertions(+), 8 deletions(-) create mode 100644 linux/devices/reloop_dj2.c create mode 100644 linux/devices/reloop_dj2.h diff --git a/.claude/re/open_questions.md b/.claude/re/open_questions.md index 03533d4..d65c13a 100644 --- a/.claude/re/open_questions.md +++ b/.claude/re/open_questions.md @@ -2,6 +2,22 @@ ## Open Questions +### Interrupt output path has the same half-packet bug as bulk (UNFIXED) +`ploytec_int_subpackets[]` covers 40 frames / 1928 bytes, but +`PLOYTEC_INT_OUT_PKT_SIZE` is 3856 (= 2 x 1928) and +`ploytec_process_out_int()` reports `PLOYTEC_FRAMES_PER_PKT` (80) frames +consumed per packet. So half of every interrupt packet's audio is pulled +from the ALSA ring and dropped, exactly as the bulk path did before it was +fixed. Affects Xone:DB4 on firmware 1.4.1+ (which exposes interrupt +endpoints instead of bulk). + +The layout repeats every 1928 bytes / 40 frames, so the fix is mechanical: +duplicate the five groups and four MIDI slots at +1928 bytes / +40 frames +and bump `PLOYTEC_INT_NUM_SUBPACKETS` to 10 and +`PLOYTEC_INT_NUM_MIDI_SLOTS` to 8. Left undone because no interrupt-mode +hardware was available to verify it. + + ### Sample rate change: 5-call dance vs official driver Our driver sends SET_CUR 5 times alternating 0x86/0x05 (from Windows USB capture). The official macOS driver only calls `setFrequency` once per pipe (2 total: input + output). The 5-call @@ -28,6 +44,23 @@ related to level metering or some control protocol. Has its own pipe assignment ## Resolved (moved from questions) +- ~~Junction encoder vs our bit-interleaved codec~~ — The bit-interleaved + codec in `ploytec_codec.c` is correct for bulk devices. Confirmed by + playing tones through a Reloop DJ2 ME (200c:1009) from userspace: plain + 3-byte packing produces noise, the bit-interleaved 48-byte frame produces + a clean tone. The `pcmTo24Junction*` encoders belong to the **isoc** path + (`chooseISOOutEncoder`), which bulk devices never execute — an easy trap + when reading the Windows driver. +- ~~Bulk output packet coverage~~ — `ploytec_bulk_subpackets[]` and + `ploytec_bulk_midi_slots[]` only described 4 sub-packets (40 frames / + 2048 bytes) while `PLOYTEC_FRAMES_PER_PKT` is 80 and + `PLOYTEC_BULK_OUT_PKT_SIZE` is 4096. `ploytec_process_out_bulk()` reported + 80 frames consumed per packet, so half of every packet's audio was pulled + from the ALSA ring and silently dropped, and bytes 2048-4095 were never + filled. Heard as broadband distortion at all frequencies. Both tables now + cover all 8 sub-packets. **This affected the Xone devices too, not just + the Reloop.** + - ~~Vendor request 'A'~~ — Now understood: ESU clock config (wIndex=0x101) and Wolfson codec (wIndex=0x102/0x106) - ~~Index 2 digital output selector~~ — Write-only, `AjExtData[4] & 0xFFFF`, only for devices with `this[0x7AC]` - ~~setEsuCpldByte~~ — Writes to 'I' wIndex=1 with `| 0xE7` mask diff --git a/.claude/re/ploytec_device_table.md b/.claude/re/ploytec_device_table.md index 5555ce8..4f6d866 100644 --- a/.claude/re/ploytec_device_table.md +++ b/.claude/re/ploytec_device_table.md @@ -65,13 +65,51 @@ The `setFrequency` function retries the SET_CUR request up to 200 times for thes |---------|-------------|-------| | 0x0019 | ? | `setFreqOnOutPipe` support only | -## VID 0x200C (unknown vendor) - -| USB PID | Device Name | Notes | -|---------|-------------|-------| -| 0x1007–0x1027 | ? | Range with bitmask in `setFreqOnOutPipe` | -| 0x1019 | ? | Returns early from `updateAjInputSelector` with status 0x12 | -| 0x1030 | ? | Uses SET_INTERFACE (bRequest 0x0B) in `configurationDone` | +## VID 0x200C (Reloop / Hanpin) + +| USB PID | Device Name | Channels | Rate | Notes | +|---------|-------------|----------|------|-------| +| 0x1005 | ? | 4 in / 4 out | 96000 | From `findInterfacesInConfig` fallback table | +| 0x1006 | ? | 6 in / 6 out | 44100 | HS=6in+6out, FS=2in+2out | +| 0x1009 | **Reloop Digital Jockey 2 Master Edition** | **6 in / 4 out** | **44100** | Confirmed on hardware, see below | +| 0x1019 | ? | 6 in / 4 out | 44100 | Same config block as 0x1009. Returns early from `updateAjInputSelector` with status 0x12 | +| 0x1037 | ? | 6 in / 4 out | 44100 | Same config block as 0x1009 | +| 0x100A | ? | 2 in / 10 out | 44100 | | +| 0x103E | ? | 4 in / 4 out | 44100 | 16-bit (`0x10`), not 24 | +| 0x1007–0x1027 | ? | | | Range with bitmask in `setFreqOnOutPipe` | +| 0x1030 | ? | | | Uses SET_INTERFACE (bRequest 0x0B) in `configurationDone` | + +### 0x1009 — Reloop Digital Jockey 2 ME (confirmed on hardware) + +Supported in Ozzy via `linux/devices/reloop_dj2.c`. + +- **6 in / 4 out, 24-bit, 44100 Hz only.** Every 200c:1009 branch in + `findInterfacesInConfig` hardcodes `0xAC44`; only the sibling 0x1005 gets + 96 kHz. Running this DAC at 96 kHz audibly distorts playback, so the rate + list is restricted to a single entry. +- **Bulk transport**, standard Ploytec framing: EP 0x05 out, 0x86 in, + 0x83 MIDI in. 48-byte bit-interleaved wire frames, MIDI embedded one + byte per sub-packet at offset 480 with 0xFF sync at 481. +- **USB pacing is NOT required** (earlier belief retracted). 25+ userspace + iterations with no delay anywhere — between the two alt-setting changes, + before the first control request, or between handshake requests — all + completed cleanly, in either SET_CUR order. The one kernel probe that hung + the machine ran with an invalid `clear_halt` on the isochronous endpoint + 0x02 and a 96 kHz default rate, both since fixed; those are the likely + cause. The driver keeps a short precautionary delay inside its own init + path only, costing ~1 s at probe and touching no shared code. +- **Capture channel map** (matches the manual's "Input Routing"), verified + by loopback: in 0/1 = Line In 1, in 2/3 = Line In 2, in 4/5 = Mic, + in 6/7 = unused/silent. +- **MIDI output requires explicit status bytes.** The firmware silently + ignores messages sent with MIDI running status. Mixxx emits one 0x90 and + then dozens of bare note/velocity pairs; confirmed by logging the + outbound stream (149 bytes, exactly one status byte). Symptom is MIDI + input working perfectly while no LED ever lights. The driver reassembles + running-status messages before embedding them. +- Analogue channel strips only reach the digital side when the front-panel + MIDI/Phono-Line switch is in the MIDI position; in Phono/Line the fader, + gain and EQ stay in the analogue path and emit no MIDI. ## Creative Technology (VID 0x041E) diff --git a/linux/Makefile b/linux/Makefile index ff55a19..afeaf2d 100644 --- a/linux/Makefile +++ b/linux/Makefile @@ -21,6 +21,7 @@ $(MODULE_NAME)-y := ozzy_core.o \ ozzy_pcm.o \ ozzy_midi.o \ devices/ploytec.o \ + devices/reloop_dj2.o \ ../common/devices/ploytec/ploytec_codec.o # To add a new device family, append: diff --git a/linux/devices/reloop_dj2.c b/linux/devices/reloop_dj2.c new file mode 100644 index 0000000..733dde7 --- /dev/null +++ b/linux/devices/reloop_dj2.c @@ -0,0 +1,564 @@ +// SPDX-License-Identifier: MIT +/* + * Reloop Digital Jockey 2 Master Edition (USB 200c:1009) + * + * Self-contained device implementation. It shares the Ploytec chipset + * with the Xone series, so it reuses the read-only protocol helpers and + * the bit-interleaved codec from common/devices/ploytec/, but keeps its + * own handshake, packet layout and op table so nothing in the existing + * Xone code path is affected. + * + * Differences from the Xone baseline, all verified against hardware: + * + * - Runs at 44.1 kHz only. The vendor driver hardcodes 0xAC44 for every + * 200c:1009 branch; 96 kHz audibly distorts playback. + * - MIDI output must carry an explicit status byte. The firmware drops + * messages sent with MIDI running status, which the ALSA sequencer + * bridge emits by default, so no LED ever lights without reassembly. + * - Uses the full 8 sub-packets of the 4096-byte bulk output packet. + * Ozzy's shared tables describe only the first 4 (2048 bytes) while + * reporting 80 frames consumed, which drops half the audio; this + * device carries its own tables covering all 80 frames. + * + * Copyright (C) 2024 Marcel Bierling + */ + +#include +#include +#include +#include +#include + +#include "../ozzy.h" +#include "../ozzy_log.h" +#include "../ozzy_midi.h" +#include "reloop_dj2.h" +#include "../../common/devices/ploytec/ploytec_defs.h" +#include "../../common/devices/ploytec/ploytec_protocol.h" +#include "../../common/devices/ploytec/ploytec_codec.h" + +/* ALSA side: 8 slots x 3 bytes (S24_3LE) */ +#define ALSA_FRAME_SIZE (PLOYTEC_CHANNELS * 3) +#define ALSA_PKT_SIZE (PLOYTEC_FRAMES_PER_PKT * ALSA_FRAME_SIZE) + +/* + * Bulk output packet: 8 sub-packets of 512 bytes. + * Each holds 10 device frames (480 bytes), then one MIDI byte at +480, + * a 0xFF sync byte at +481, and 30 bytes of padding. + */ +#define RELOOP_SUB_SIZE 512 +#define RELOOP_SUBS_PER_PKT 8 +#define RELOOP_FRAMES_PER_SUB 10 +#define RELOOP_MIDI_OFFSET 480 +#define RELOOP_SYNC_OFFSET 481 +#define RELOOP_SYNC_BYTE 0xFF + +/* + * struct reloop_dj2_private - DMA-safe scratch for vendor requests. + * usb_control_msg() buffers must not live on the stack. + */ +struct reloop_dj2_private { + unsigned char firmware_ver[15]; + unsigned char status[1]; + unsigned char xfer_buf[16]; + + /* + * MIDI output running-status expansion. This firmware ignores + * messages that arrive without an explicit status byte, but hosts + * are free to use running status -- Mixxx sends one 0x90 and then + * a long run of bare note/velocity pairs, none of which light an + * LED. Messages are reassembled here with the status byte restored. + */ + u8 running_status; /* last status byte seen from the host */ + u8 outq[3]; /* fully-formed message awaiting transmission */ + u8 outq_len; + u8 outq_pos; +}; + +/* + * reloop_midi_data_len - Number of data bytes following a status byte. + * Returns 0 for status values this driver does not reassemble. + */ +static unsigned int reloop_midi_data_len(u8 status) +{ + switch (status & 0xF0) { + case 0x80: /* note off */ + case 0x90: /* note on */ + case 0xA0: /* poly aftertouch */ + case 0xB0: /* control change */ + case 0xE0: /* pitch bend */ + return 2; + case 0xC0: /* program change */ + case 0xD0: /* channel aftertouch */ + return 1; + default: + return 0; + } +} + +static const unsigned int reloop_dj2_rates[] = { RELOOP_DJ2_SAMPLE_RATE }; + +/* + * Log every non-idle MIDI byte sent to the device. Diagnostic only -- + * useful when a host application's LED output does not light anything. + */ +static bool reloop_dj2_debug_midi; +module_param_named(reloop_debug_midi, reloop_dj2_debug_midi, bool, 0644); +MODULE_PARM_DESC(reloop_debug_midi, "Log outbound MIDI bytes (Reloop DJ2)"); + +/* Pace the firmware; see RELOOP_DJ2_SETTLE_MS. */ +static inline void reloop_settle(void) +{ + msleep(RELOOP_DJ2_SETTLE_MS); +} + +/* ======================================================================== + * Vendor handshake + * ======================================================================== */ + +static int reloop_get_firmware(struct ozzy_chip *chip) +{ + struct reloop_dj2_private *priv = chip->private_data; + struct ploytec_firmware_version fw; + int ret; + + ret = usb_control_msg(chip->dev, usb_rcvctrlpipe(chip->dev, 0), + PLOYTEC_CMD_FIRMWARE, 0xC0, 0x0000, 0, + priv->firmware_ver, 15, 2000); + if (ret < 0) + return ret; + + fw = ploytec_parse_firmware(priv->firmware_ver); + ozzy_log(&chip->dev->dev, "firmware: v1.%d.%d (chip ID: 0x%02X)\n", + fw.major, fw.minor, fw.chip_id); + return 0; +} + +static int reloop_get_status(struct ozzy_chip *chip) +{ + struct reloop_dj2_private *priv = chip->private_data; + int ret; + + ret = usb_control_msg(chip->dev, usb_rcvctrlpipe(chip->dev, 0), + PLOYTEC_CMD_STATUS, 0xC0, 0x0000, + PLOYTEC_REG_AJ_INPUT_SEL, + priv->status, 1, 2000); + if (ret < 0) + return ret; + + ozzy_log(&chip->dev->dev, "status: 0x%02X\n", priv->status[0]); + return 0; +} + +static int reloop_get_rate(struct ozzy_chip *chip) +{ + struct reloop_dj2_private *priv = chip->private_data; + uint32_t rate; + int ret; + + ret = usb_control_msg(chip->dev, usb_rcvctrlpipe(chip->dev, 0), + PLOYTEC_CMD_GET_RATE_REQ, PLOYTEC_CMD_GET_RATE_TYPE, + 0x0100, 0, priv->xfer_buf, 3, 2000); + if (ret < 0) + return ret; + + rate = ploytec_decode_rate(priv->xfer_buf); + ozzy_log(&chip->dev->dev, "hardware sample rate: %u Hz\n", rate); + + if (rate != RELOOP_DJ2_SAMPLE_RATE) + ozzy_log(&chip->dev->dev, "note: expected %u Hz\n", + RELOOP_DJ2_SAMPLE_RATE); + + chip->current_rate = 0; + return 0; +} + +/* + * reloop_set_rate - Program the sample rate on both stream endpoints. + * Only index 0 (44.1 kHz) is valid for this device. + */ +static int reloop_set_rate(struct ozzy_chip *chip, unsigned int rate_index) +{ + struct reloop_dj2_private *priv = chip->private_data; + int ret; + + if (rate_index >= ARRAY_SIZE(reloop_dj2_rates)) + return -EINVAL; + + ploytec_encode_rate(reloop_dj2_rates[rate_index], priv->xfer_buf); + + ret = usb_control_msg(chip->dev, usb_sndctrlpipe(chip->dev, 0), + PLOYTEC_CMD_SET_RATE_REQ, PLOYTEC_CMD_SET_RATE_TYPE, + 0x0100, PLOYTEC_EP_RATE_IN, priv->xfer_buf, 3, 2000); + if (ret < 0) + return ret; + + /* Precautionary pacing; see RELOOP_DJ2_SETTLE_MS. */ + reloop_settle(); + + ret = usb_control_msg(chip->dev, usb_sndctrlpipe(chip->dev, 0), + PLOYTEC_CMD_SET_RATE_REQ, PLOYTEC_CMD_SET_RATE_TYPE, + 0x0100, PLOYTEC_EP_RATE_OUT, priv->xfer_buf, 3, 2000); + if (ret < 0) + return ret; + + chip->current_rate = rate_index; + ozzy_log(&chip->dev->dev, "sample rate set: %u Hz\n", + reloop_dj2_rates[rate_index]); + return 0; +} + +/* + * reloop_confirm_status - Read-modify-write the AJ Input Selector to arm + * the device for streaming. + */ +static int reloop_confirm_status(struct ozzy_chip *chip) +{ + struct reloop_dj2_private *priv = chip->private_data; + uint16_t wvalue; + int ret; + + ret = usb_control_msg(chip->dev, usb_rcvctrlpipe(chip->dev, 0), + PLOYTEC_CMD_STATUS, 0xC0, 0x0000, + PLOYTEC_REG_AJ_INPUT_SEL, + priv->status, 1, 2000); + if (ret < 0) + return ret; + + wvalue = ploytec_confirm_wvalue(priv->status[0]); + + ret = usb_control_msg(chip->dev, usb_sndctrlpipe(chip->dev, 0), + PLOYTEC_CMD_STATUS, 0x40, + wvalue, PLOYTEC_REG_AJ_INPUT_SEL, + NULL, 0, 2000); + if (ret >= 0) + ozzy_log(&chip->dev->dev, + "status confirmed (read=0x%02X, wrote wValue=0x%04X)\n", + priv->status[0], wvalue); + return ret; +} + +/* + * reloop_dj2_init - Vendor handshake, paced throughout. + */ +static int reloop_dj2_init(struct ozzy_chip *chip) +{ + struct reloop_dj2_private *priv; + int ret; + + priv = kzalloc(sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + chip->private_data = priv; + + ozzy_log(&chip->dev->dev, "--- begin handshake sequence ---\n"); + + /* + * Let the device settle after the alt-setting changes done during + * probe before issuing the first control request. Handled here so + * the core does not need a delay of its own after interface 1. + */ + reloop_settle(); + + ret = reloop_get_firmware(chip); + if (ret < 0) + goto err; + reloop_settle(); + + ret = reloop_get_status(chip); + if (ret < 0) + goto err; + reloop_settle(); + + ret = reloop_get_rate(chip); + if (ret < 0) + goto err; + reloop_settle(); + + ret = reloop_set_rate(chip, 0); + if (ret < 0) + goto err; + reloop_settle(); + + ret = reloop_get_status(chip); + if (ret < 0) + goto err; + reloop_settle(); + + ret = reloop_confirm_status(chip); + if (ret < 0) + goto err; + reloop_settle(); + + ozzy_log(&chip->dev->dev, "--- handshake complete, device ready ---\n"); + return 0; + +err: + ozzy_err(&chip->dev->dev, "handshake failed (ret=%d)\n", ret); + kfree(priv); + chip->private_data = NULL; + return ret; +} + +static void reloop_dj2_free(struct ozzy_chip *chip) +{ + kfree(chip->private_data); + chip->private_data = NULL; +} + +static int reloop_dj2_reset(struct ozzy_chip *chip) +{ + return usb_reset_device(chip->dev); +} + +/* ======================================================================== + * PCM packet processing + * ======================================================================== */ + +/* + * reloop_dj2_process_out_packet - Encode ALSA audio into one bulk packet. + * + * Fills all 8 sub-packets, i.e. all PLOYTEC_FRAMES_PER_PKT (80) frames. + * Anything less would silently discard the frames it does not cover, + * since the return value reports the full packet as consumed. + */ +static unsigned int reloop_dj2_process_out_packet(struct ozzy_chip *chip, + uint8_t *urb_buf, + uint8_t *dma_area, + unsigned int dma_off, + unsigned int pcm_buffer_size) +{ + unsigned int sub, f, src_off, frame = 0; + + for (sub = 0; sub < RELOOP_SUBS_PER_PKT; sub++) { + unsigned int base = sub * RELOOP_SUB_SIZE; + + for (f = 0; f < RELOOP_FRAMES_PER_SUB; f++, frame++) { + src_off = dma_off + frame * ALSA_FRAME_SIZE; + if (src_off >= pcm_buffer_size) + src_off -= pcm_buffer_size; + + ploytec_encode_frame(urb_buf + base + f * PLOYTEC_OUT_FRAME_SIZE, + dma_area + src_off); + } + } + + return ALSA_PKT_SIZE; +} + +/* + * reloop_dj2_process_in_packet - Decode one capture packet. + * Input frames are contiguous 64-byte frames, no gaps. + */ +static unsigned int reloop_dj2_process_in_packet(struct ozzy_chip *chip, + uint8_t *urb_buf, + uint8_t *dma_area, + unsigned int dma_off, + unsigned int pcm_buffer_size) +{ + unsigned int f, dst_off; + + for (f = 0; f < PLOYTEC_FRAMES_PER_PKT; f++) { + dst_off = dma_off + f * ALSA_FRAME_SIZE; + if (dst_off >= pcm_buffer_size) + dst_off -= pcm_buffer_size; + + ploytec_decode_frame(dma_area + dst_off, + urb_buf + f * PLOYTEC_IN_FRAME_SIZE); + } + + return ALSA_PKT_SIZE; +} + +/* + * reloop_dj2_init_out_urb - Silence pattern: zeroed audio, MIDI idle and + * sync bytes in every sub-packet. + */ +static void reloop_dj2_init_out_urb(struct ozzy_chip *chip, uint8_t *buffer) +{ + unsigned int sub; + + memset(buffer, 0, RELOOP_SUB_SIZE * RELOOP_SUBS_PER_PKT); + + for (sub = 0; sub < RELOOP_SUBS_PER_PKT; sub++) { + unsigned int base = sub * RELOOP_SUB_SIZE; + + buffer[base + RELOOP_MIDI_OFFSET] = PLOYTEC_MIDI_IDLE_BYTE; + buffer[base + RELOOP_SYNC_OFFSET] = RELOOP_SYNC_BYTE; + } +} + +/* + * reloop_midi_drop - Remove n bytes from the front of the send buffer. + * Unconsumed data stays anchored at index 0, which keeps send_pending a + * valid append offset for ozzy_midi_out_trigger(). + * Caller must hold rt->out_lock. + */ +static void reloop_midi_drop(struct midi_runtime *rt, unsigned int n) +{ + if (n > rt->send_pending) + n = rt->send_pending; + rt->send_pending -= n; + if (rt->send_pending) + memmove(rt->send_buffer, rt->send_buffer + n, rt->send_pending); +} + +/* + * reloop_dj2_fill_midi_out - Embed outbound MIDI (button LEDs) in the + * PCM packet, one byte per sub-packet at offset 480. + * + * Only the first slot carries real data; the rest stay idle. At 44.1 kHz + * that is 44100/80 = ~551 packets/sec, i.e. ~551 MIDI bytes/sec, safely + * under the 3125 bytes/sec the MIDI wire rate allows. + * + * This deliberately does not use ozzy_midi_consume(). That helper walks + * the send buffer with a read cursor (send_count) while + * ozzy_midi_out_trigger() appends new data at send_pending, so any write + * arriving while bytes are still pending lands at the wrong offset and + * corrupts the queue. Mixxx sends LED state in bursts far larger than one + * packet's worth, so that collision happens on essentially every update. + * + * Instead the unconsumed bytes are kept anchored at index 0, which keeps + * send_pending a valid append offset for the shared trigger and needs no + * change to ozzy_midi.c. + */ +static void reloop_dj2_fill_midi_out(struct ozzy_chip *chip, uint8_t *urb_buf) +{ + struct midi_runtime *rt = chip->midi; + struct reloop_dj2_private *priv = chip->private_data; + unsigned long flags; + unsigned int sub; + u8 out = PLOYTEC_MIDI_IDLE_BYTE; + + if (!rt) + return; + + spin_lock_irqsave(&rt->out_lock, flags); + + /* Emit the next byte of an already-assembled message. */ + if (priv && priv->outq_pos < priv->outq_len) { + out = priv->outq[priv->outq_pos++]; + goto unlock; + } + + /* + * Otherwise assemble one complete message, restoring the status + * byte when the host used running status. Only consume from the + * send buffer once the whole message has arrived, so a partially + * received message is never emitted. + */ + while (priv && rt->send_pending > 0) { + u8 first = rt->send_buffer[0]; + unsigned int need, consume; + + if (first >= 0xF8) { + /* Real-time byte: pass straight through. */ + out = first; + reloop_midi_drop(rt, 1); + goto unlock; + } + + if (first & 0x80) { + need = reloop_midi_data_len(first); + if (!need) { + /* Not reassembled (e.g. sysex): drop it. */ + reloop_midi_drop(rt, 1); + continue; + } + if (rt->send_pending < 1 + need) + break; /* wait for the rest */ + priv->running_status = first; + priv->outq[0] = first; + priv->outq[1] = rt->send_buffer[1]; + if (need == 2) + priv->outq[2] = rt->send_buffer[2]; + consume = 1 + need; + } else { + if (!priv->running_status) { + reloop_midi_drop(rt, 1); + continue; /* data with no status: drop */ + } + need = reloop_midi_data_len(priv->running_status); + if (rt->send_pending < need) + break; /* wait for the rest */ + priv->outq[0] = priv->running_status; + priv->outq[1] = rt->send_buffer[0]; + if (need == 2) + priv->outq[2] = rt->send_buffer[1]; + consume = need; + } + + priv->outq_len = need + 1; + priv->outq_pos = 1; + out = priv->outq[0]; + reloop_midi_drop(rt, consume); + goto unlock; + } + +unlock: + /* Read cursor is unused here; keep it consistent for the shared code. */ + rt->send_count = 0; + spin_unlock_irqrestore(&rt->out_lock, flags); + + if (reloop_dj2_debug_midi && out != PLOYTEC_MIDI_IDLE_BYTE) + ozzy_log(&chip->dev->dev, "midi out: %02X\n", out); + + urb_buf[RELOOP_MIDI_OFFSET] = out; + + for (sub = 1; sub < RELOOP_SUBS_PER_PKT; sub++) + urb_buf[sub * RELOOP_SUB_SIZE + RELOOP_MIDI_OFFSET] = + PLOYTEC_MIDI_IDLE_BYTE; +} + +static unsigned int reloop_dj2_get_out_packet_size(struct ozzy_chip *chip, + bool is_bulk) +{ + return RELOOP_SUB_SIZE * RELOOP_SUBS_PER_PKT; +} + +/* ======================================================================== + * Exported descriptor + * ======================================================================== */ + +const struct ozzy_device_info reloop_dj2_info = { + .name = "Reloop Digital Jockey 2 ME", + + /* + * Streams the full 8-slot Ploytec wire frame even though the + * hardware is 6 in / 4 out; see reloop_dj2.h. + */ + .playback_channels = PLOYTEC_CHANNELS, + .capture_channels = PLOYTEC_CHANNELS, + .out_packet_size = RELOOP_SUB_SIZE * RELOOP_SUBS_PER_PKT, + .in_packet_size = PLOYTEC_IN_PKT_SIZE, + .frames_per_out_packet = PLOYTEC_FRAMES_PER_PKT, + .frames_per_in_packet = PLOYTEC_FRAMES_PER_PKT, + .out_ep = PLOYTEC_EP_PCM_OUT, + .in_ep = PLOYTEC_EP_PCM_IN, + .alsa_format = SNDRV_PCM_FMTBIT_S24_3LE, + .bytes_per_sample = 3, + + .midi_in_ep = PLOYTEC_EP_MIDI_IN, + .midi_out_embedded = true, + + .num_interfaces = PLOYTEC_NUM_INTERFACES, + .alt_setting = PLOYTEC_ALT_SETTING, + + .rates = reloop_dj2_rates, + .num_rates = ARRAY_SIZE(reloop_dj2_rates), + .rates_mask = SNDRV_PCM_RATE_44100, + .rate_min = RELOOP_DJ2_SAMPLE_RATE, + .rate_max = RELOOP_DJ2_SAMPLE_RATE, +}; + +const struct ozzy_device_ops reloop_dj2_ops = { + .init = reloop_dj2_init, + .free = reloop_dj2_free, + .set_rate = reloop_set_rate, + .reset = reloop_dj2_reset, + .process_out_packet = reloop_dj2_process_out_packet, + .process_in_packet = reloop_dj2_process_in_packet, + .init_out_urb = reloop_dj2_init_out_urb, + .fill_midi_out = reloop_dj2_fill_midi_out, + .get_out_packet_size = reloop_dj2_get_out_packet_size, +}; diff --git a/linux/devices/reloop_dj2.h b/linux/devices/reloop_dj2.h new file mode 100644 index 0000000..aed8479 --- /dev/null +++ b/linux/devices/reloop_dj2.h @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Reloop Digital Jockey 2 Master Edition (USB 200c:1009) + * + * Ploytec-based DJ controller with an integrated 6-in / 4-out 24-bit + * audio interface, reverse engineered from USB traffic and the vendor's + * Windows driver (rldj2meu.sys). + */ + +#ifndef OZZY_RELOOP_DJ2_H +#define OZZY_RELOOP_DJ2_H + +#include "../ozzy.h" + +/* USB identity */ +#define RELOOP_DJ2_VENDOR_ID 0x200C +#define RELOOP_DJ2_PRODUCT_ID 0x1009 + +/* + * Hardware channel counts, from findInterfacesInConfig() in the vendor + * driver: every 200c:1009 branch sets in=6 / out=4 at 24-bit. + * + * The Ploytec bulk wire frame always carries 8 slots per direction, so + * the driver still streams 8 channels: surplus playback slots are + * ignored by the device and surplus capture slots read as silence. + * + * Verified against hardware -- a loopback capture showed signal on + * in 0..3 (Line In 1 and 2), the mic noise floor on in 4..5, and + * digital silence on in 6..7. + */ +#define RELOOP_DJ2_HW_IN_CHANNELS 6 +#define RELOOP_DJ2_HW_OUT_CHANNELS 4 + +/* + * Capture channel map (matches "Input Routing" in the printed manual): + * in 0/1 Line In 1 + * in 2/3 Line In 2 + * in 4/5 Mic + * in 6/7 unused, always silent + */ + +/* + * This device runs at 44.1 kHz only. The vendor driver hardcodes 0xAC44 + * for every 200c:1009 branch -- the sibling 200c:1005 is the model that + * gets 96 kHz. Running this DAC at 96 kHz audibly distorts playback. + */ +#define RELOOP_DJ2_SAMPLE_RATE 44100 + +/* + * Precautionary pacing between handshake control requests, applied only + * inside this device's own init path. + * + * NOT proven necessary: 25+ userspace iterations with no pacing at all, + * in either SET_CUR order, completed the handshake without the device + * dropping off the bus. It is kept because the one kernel probe that + * hung the machine had no pacing, and the cost is ~1 s at probe time + * only. The hang is now better explained by an invalid clear_halt on the + * isochronous endpoint and by the 96 kHz default, both since fixed. + */ +#define RELOOP_DJ2_SETTLE_MS 200 + +/* + * MIDI/LED notes + * + * Controls and LEDs use the shared Ploytec bulk transport: input arrives + * on the dedicated MIDI IN endpoint, output is embedded one byte per + * sub-packet at offset 480, padded with 0xFD. + * + * Output needs one device-specific fixup: this firmware ignores messages + * that arrive without an explicit status byte. Hosts may legitimately use + * MIDI running status -- Mixxx sends a single 0x90 followed by a long run + * of bare note/velocity pairs -- and every one of those is dropped by the + * device. reloop_dj2_fill_midi_out() reassembles messages with the status + * byte restored. Without it, MIDI input works fine while no LED ever lights. + * + * The note/CC assignments themselves live in the host mapping (see the + * "MIDI Control Values" table in the manual), not in the driver: LED + * feedback is simply the same note echoed back to the device. + */ + +/* + * Self-contained: own handshake, own packet layout, own op table. Only + * the read-only protocol helpers and bit-interleaved codec are shared + * with the Xone devices, so their code path is untouched. + */ +extern const struct ozzy_device_info reloop_dj2_info; +extern const struct ozzy_device_ops reloop_dj2_ops; + +#endif /* OZZY_RELOOP_DJ2_H */ diff --git a/linux/ozzy_core.c b/linux/ozzy_core.c index 390feba..fdce843 100644 --- a/linux/ozzy_core.c +++ b/linux/ozzy_core.c @@ -23,6 +23,7 @@ #include "ozzy_pcm.h" #include "ozzy_midi.h" #include "devices/ploytec.h" +#include "devices/reloop_dj2.h" MODULE_AUTHOR("Marcel Bierling "); MODULE_DESCRIPTION("Ozzy USB Audio Driver"); @@ -48,12 +49,18 @@ static const struct ozzy_device_desc ploytec_desc = { .ops = &ploytec_ops, }; +static const struct ozzy_device_desc reloop_dj2_desc = { + .info = &reloop_dj2_info, + .ops = &reloop_dj2_ops, +}; + static const struct usb_device_id ozzy_id_table[] = { { USB_DEVICE(0x0a4a, 0xffdb), .driver_info = (kernel_ulong_t)&ploytec_desc }, /* Xone:DB4 */ { USB_DEVICE(0x0a4a, 0xffd2), .driver_info = (kernel_ulong_t)&ploytec_desc }, /* Xone:DB2 */ { USB_DEVICE(0x0a4a, 0xffdd), .driver_info = (kernel_ulong_t)&ploytec_desc }, /* Xone:DX */ { USB_DEVICE(0x0a4a, 0xff4d), .driver_info = (kernel_ulong_t)&ploytec_desc }, /* Xone:4D */ { USB_DEVICE(0x0a4a, 0xffad), .driver_info = (kernel_ulong_t)&ploytec_desc }, /* Wizard 4 */ + { USB_DEVICE(0x200c, 0x1009), .driver_info = (kernel_ulong_t)&reloop_dj2_desc }, /* Reloop Digital Jockey 2 Master Edition */ {} }; MODULE_DEVICE_TABLE(usb, ozzy_id_table); @@ -104,7 +111,6 @@ static int ozzy_probe(struct usb_interface *intf, desc->info->alt_setting); return -EIO; } - /* Find a free card slot */ mutex_lock(®ister_mutex); for (i = 0; i < SNDRV_CARDS; i++) { From 0b372f9e82f335751a259eac865c49180233e6db Mon Sep 17 00:00:00 2001 From: petrosilius Date: Sun, 23 Aug 2026 19:59:47 +0200 Subject: [PATCH 2/2] README: add Reloop Digital Jockey 2 ME to supported devices Also adds Wizard 4, which was already registered in ozzy_core.c's device table but missing from this list. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BASiuKMU5aCZGGKJYU8yXS --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c71584..24abda4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ Ozzy fixes that. This is an open-source, reverse-engineered driver supporting non-class compliant USB audio interfaces—high-end DJ mixers and audio processors that were left behind when official driver support ended. **Currently Supported Devices:** -* **Allen & Heath Xone:DB4, DB2, DX, 4D** (Ploytec-based protocol) +* **Allen & Heath Xone:DB4, DB2, DX, 4D, Wizard 4** (Ploytec-based protocol) +* **Reloop Digital Jockey 2 Master Edition** (same Ploytec protocol, DJ controller with integrated 6-in/4-out audio interface) More devices can be added—the architecture separates the audio engine from device protocols. @@ -38,6 +39,8 @@ Ozzy reverses the protocol and provides modern drivers—so your equipment keeps | **Allen & Heath Xone:DB2** | 8×8 | 44.1/48/88.2/96 kHz | ✅ Perfect | | **Allen & Heath Xone:DX** | 8×8 | 44.1/48/88.2/96 kHz | ✅ Perfect | | **Allen & Heath Xone:4D** | 8×8 | 44.1/48/88.2/96 kHz | ✅ Perfect | +| **Allen & Heath Wizard 4** | 8×8 | 44.1/48/88.2/96 kHz | ✅ Perfect | +| **Reloop Digital Jockey 2 ME** | 6×4 | 44.1 kHz | ✅ Perfect (audio, MIDI in/out, LEDs) | ---