diff --git a/releases/33_drumdrum/CMakeLists.txt b/releases/33_drumdrum/CMakeLists.txt index 8ca736f..3f3edf2 100644 --- a/releases/33_drumdrum/CMakeLists.txt +++ b/releases/33_drumdrum/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(drumdrum midi_sysex.cpp monome_mext.c grid_ui.cpp + midi_host.cpp ) # Warnings: catch accidental double-precision and other issues diff --git a/releases/33_drumdrum/README.md b/releases/33_drumdrum/README.md index 6b02774..11bf084 100644 --- a/releases/33_drumdrum/README.md +++ b/releases/33_drumdrum/README.md @@ -4,13 +4,14 @@ A DFAM-style 8-step sequencer for the [Music Thing Modular Workshop System Compu drumdrum gives you a dual-VCO pitch sequencer with per-step velocity, white noise, step triggers, and end-of-cycle triggers — the core building blocks of a DFAM-style percussion voice, all from a single program card. Sequence data is randomised on every reset, so you can roll the dice on a new pattern any time. -You can drive the sequencer three ways, all sharing the same state: +You can drive the sequencer four ways, all sharing the same state: - **The card itself** — three knobs, the switch, and the six panel LEDs (always available). - **A Monome Grid** (16×8) plugged into the front USB jack for hands-on visual editing. +- **A [Music Thing 8mu](https://www.musicthing.co.uk/8mu)** plugged into the front USB jack for fader-based control of all 8 steps at once. - **A browser editor** in Chrome/Edge over WebMIDI when the card is connected to a computer. -The card decides between Grid and browser at boot from the USB-C cable orientation: peripheral plugged in → Grid mode, computer plugged in → browser mode. Power-cycle to switch. Panel knobs and switch keep working in all modes. +The card decides between host mode (Grid / 8mu) and device mode (browser) at boot from the USB-C cable orientation: peripheral plugged in → host mode, computer plugged in → browser mode. Within host mode, Grid vs 8mu is auto-detected from the device's USB class — no configuration needed. Power-cycle to switch between host and device. Panel knobs and switch keep working in all modes. ## Controls @@ -98,6 +99,34 @@ row 7 │ ▓▓▓▓▓▓▓▓ steps 1..8 ▓▓▓▓▓▓▓▓ │ Edits made on the Grid persist — the panel knob will not overwrite them unless you actually turn it (the knob has a "must move to take over" guard so a parked knob can't silently clobber Grid changes). +## Music Thing 8mu mode + +Plug a [Music Thing 8mu](https://www.musicthing.co.uk/8mu) into the card's front USB-C jack and the eight faders become live edit controls for the eight sequencer steps. Same auto-detection as the Grid — the firmware notices it's a class-compliant USB MIDI device (rather than CDC) and routes accordingly. Panel knobs and switch keep working in parallel. + +### Default mapping + +The 8mu's factory faders send CC 34–41, which is what the firmware listens for out of the box. Buttons and the second velocity bank need configuration in the [8mu web editor](https://www.musicthing.co.uk/8mu) — point each control at the CC numbers below. + +| CC | Source | Effect | +|---------|------------------|-----------------------------------------------------| +| **34–41** | faders (factory) | Step pitches 1–8 (raw 7-bit value), or step velocities (`value × 2`) when in velocity-edit mode | +| **50–57** | faders (alt bank) | Step velocities 1–8, regardless of edit mode | +| **28** | fader | Edit cursor (0 → step 1, 127 → step 8) | +| **22** | button (press) | Toggle pitch ↔ velocity edit mode | +| **23** | button (press) | Toggle play/pause | +| **24** | button (press) | Reset to step 1 | + +Buttons act on the rising edge (CC value crossing ≥ 64), so a press registers once even if your button sends a release event afterwards. + +A typical 8mu setup uses banks: bank 1 = pitches (factory faders + buttons mapped to CC 22–24); bank 2 = velocities (faders re-mapped to CC 50–57, same buttons); bank 3 = edit cursor + transport (one fader on CC 28, buttons on CC 22–24). + +### Notes + +- The 8mu sends data only when something changes, so step parameters stay at whatever they were until you actually move a fader. Toggling pitch ↔ velocity mode does not reset values; just sweep the fader for the step you want to change. +- Pitch changes are 7-bit (0–127, the full MIDI range, 1:1 with fader position). Velocity changes are 8-bit (0–254, computed as `CC × 2`). +- The 4 buttons and the 6-axis accelerometer (CC 42–49) beyond CC 22–24 are ignored in this firmware. Only CC messages are recognised — note-on / note-off / sysex are dropped. +- 8mu and Grid can't be plugged into the front jack at the same time; pick one per session. + ## Browser editor (WebMIDI) When you plug the card into a computer instead of a Grid, it appears as a USB MIDI device named **DrumDrum**. Open `editor.html` in Chrome or Edge — it's a single self-contained file with no build step: @@ -262,15 +291,15 @@ Flash the resulting `drumdrum.uf2` to the Workshop Computer by holding BOOT whil ## Technical Details - **Core 0** runs the sequencer and audio DSP in `ProcessSample()` at 48 kHz in interrupt context. Pure integer arithmetic, no float, no division. -- **Core 1** owns the USB stack — TinyUSB host (Monome Grid via the vendored mext serial protocol) or device (USB MIDI for the browser editor), decided once at boot from the USB-C CC pins via `USBPowerState()`. -- All three control surfaces share a single `SharedState` struct (`shared_state.h`); cross-core writes are atomic on the M0+, no locks needed. `tickEpoch` is the cross-core "something changed" signal. +- **Core 1** owns the USB stack — TinyUSB host (Monome Grid via the vendored mext serial protocol, or 8mu via a small in-tree class-compliant USB MIDI host driver since TinyUSB 0.18 doesn't ship one) or device (USB MIDI for the browser editor), decided once at boot from the USB-C CC pins via `USBPowerState()`. +- All four control surfaces (panel, Grid, 8mu, browser editor) share a single `SharedState` struct (`shared_state.h`); cross-core writes are atomic on the M0+, no locks needed. `tickEpoch` is the cross-core "something changed" signal. - White noise via xorshift32 PRNG, seeded from the hardware timer on each boot. - CV Out 1 uses EEPROM-calibrated `CVOutMIDINote()` for accurate 1V/oct tracking. - Audio Out 2 approximates 1V/oct on the 12-bit audio DAC (~28.4 DAC units/semitone, uncalibrated). - System clock set to 144 MHz to reduce ADC tonal artifacts; all code copied to RAM (`copy_to_ram`) to eliminate flash cache jitter. - 150 ms boot mute holds audio + pulse outputs at zero so DAC settling and the first trigger don't click. -**Source files:** `main.cpp` (sequencer + audio + role select), `shared_state.h` (cross-core data), `usb_core1.cpp` (USB task pump), `tusb_config.h` + `usb_descriptors.c` (TinyUSB), `monome_mext.c/h` (Grid serial protocol, vendored from MLRws), `grid_ui.cpp/h` (Grid layout + key dispatch), `midi_sysex.cpp/h` (browser-protocol parser/encoder), `editor.html` (browser editor). +**Source files:** `main.cpp` (sequencer + audio + role select), `shared_state.h` (cross-core data), `usb_core1.cpp` (USB task pump), `tusb_config.h` + `usb_descriptors.c` (TinyUSB), `monome_mext.c/h` (Grid serial protocol, vendored from MLRws), `grid_ui.cpp/h` (Grid layout + key dispatch), `midi_host.cpp/h` (in-tree class-compliant USB MIDI host driver for 8mu), `midi_sysex.cpp/h` (browser-protocol parser/encoder), `editor.html` (browser editor). ## License diff --git a/releases/33_drumdrum/drumdrum.uf2 b/releases/33_drumdrum/drumdrum.uf2 index 6bb6922..700bf02 100644 Binary files a/releases/33_drumdrum/drumdrum.uf2 and b/releases/33_drumdrum/drumdrum.uf2 differ diff --git a/releases/33_drumdrum/editor.html b/releases/33_drumdrum/editor.html index 50b93bb..30c20eb 100644 --- a/releases/33_drumdrum/editor.html +++ b/releases/33_drumdrum/editor.html @@ -11,9 +11,13 @@ html, body { margin: 0; padding: 0; background: #f3f1ea; height: 100%; } body { font-family: 'Space Grotesk', system-ui, sans-serif; display: flex; align-items: center; justify-content: center; - min-height: 100vh; padding: 28px; } + min-height: 100vh; padding: 28px; + -webkit-text-size-adjust: 100%; } #root { width: 100%; max-width: 1180px; } @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.45 } } + @media (max-width: 720px) { + body { padding: 10px; align-items: flex-start; } + } @@ -35,6 +39,18 @@ } function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } +// Track viewport width so layouts can collapse below a phone-sized breakpoint. +function useIsMobile(bp = 720) { + const get = () => typeof window !== 'undefined' && window.innerWidth < bp; + const [m, setM] = React.useState(get); + React.useEffect(() => { + const on = () => setM(get()); + window.addEventListener('resize', on); + return () => window.removeEventListener('resize', on); + }, []); + return m; +} + // ─── SysEx protocol (matches midi_sysex.h on the firmware) ─────────── const SYSEX_MFR = 0x7D; const SYSEX_SET_PITCH = 0x01; @@ -211,6 +227,7 @@ const p = usePattern(); const [sel, setSel] = React.useState(0); const c = theme(dark); + const m = useIsMobile(); React.useEffect(() => { document.body.style.background = c.bg; }, [dark]); @@ -257,67 +274,84 @@ return (
-
- - - -
); } // ─── Header ────────────────────────────────────────────────────────── -function Header({ p, c, dark, setDark }) { +function Header({ p, c, m, dark, setDark }) { return ( -
+
-
Workshop Computer · DFAM-style sequencer
-
drumdrum.
-
- - - +
+ +
+ + +
); } -function Status({ c, conn }) { +function Status({ c, conn, m }) { const tones = { connected: { bg: c.okBg, bd: c.okBd, fg: c.ok }, searching: { bg: c.warnBg, bd: c.warnBd, fg: c.warn }, disconnect: { bg: c.badBg, bd: c.badBd, fg: c.bad }, }; const t = tones[conn.tone] || tones.searching; + // On mobile the connected label includes the long port name; trim to keep + // the badge from forcing the row wider than the viewport. + const label = (m && conn.tone === 'connected') ? 'Connected' : conn.label; return (
- {conn.label} + {label}
); } @@ -343,14 +377,14 @@ ); } -function Transport({ p, c }) { +function Transport({ p, c, m }) { return ( ); } // ─── Step detail ───────────────────────────────────────────────────── -function StepDetail({ p, c, sel, setSel }) { +function StepDetail({ p, c, m, sel, setSel }) { const pitch = p.pitch[sel]; const vel = p.velocity[sel]; return (
-
- -
- step - {String(sel + 1).padStart(2, '0')} +
+
+ +
+ step + {String(sel + 1).padStart(2, '0')} +
-
- setSel((sel + p.seqLength - 1) % p.seqLength)}>← - setSel((sel + 1) % p.seqLength)}>→ - ←/→ +
+ setSel((sel + p.seqLength - 1) % p.seqLength)}>← + setSel((sel + 1) % p.seqLength)}>→ + {!m && ( + ←/→ + )}
- p.setStepPitch(sel, v)} /> - p.setStepVel(sel, v)} /> + p.setStepPitch(sel, v)} /> + p.setStepVel(sel, v)} />
); } -function NavBtn({ c, children, onClick }) { +function NavBtn({ c, m, children, onClick }) { return ( ); } -function PitchEditor({ c, pitch, onChange }) { +function PitchEditor({ c, m, pitch, onChange }) { return (
@@ -619,20 +680,23 @@ {noteName(pitch)} · {pitch}
- - + +
); } -function PianoStrip({ c, value, onChange }) { - const center = clamp(value, 18, 108); +function PianoStrip({ c, m, value, onChange }) { + // Show fewer keys on mobile so each one is large enough to tap. + const span = m ? 24 : 36; + const half = Math.floor(span / 2); + const center = clamp(value, half, 127 - half); const startOct = Math.floor(center / 12) - 1; - const startNote = startOct * 12; - const notes = Array.from({ length: 36 }, (_, i) => startNote + i); + const startNote = clamp(startOct * 12, 0, 128 - span); + const notes = Array.from({ length: span }, (_, i) => startNote + i); return (
@@ -668,7 +732,7 @@ ); } -function VelocityEditor({ c, vel, onChange }) { +function VelocityEditor({ c, m, vel, onChange }) { return (
@@ -679,7 +743,7 @@
- +
); } @@ -731,7 +795,7 @@ ); } -function BigSlider({ c, min, max, value, onChange }) { +function BigSlider({ c, m, min, max, value, onChange }) { const ref = React.useRef(null); const dragging = React.useRef(false); const set = (e) => { @@ -750,21 +814,25 @@ e.currentTarget.releasePointerCapture?.(e.pointerId); }; const t = (value - min) / (max - min); + const trackH = m ? 12 : 8; + const handleW = m ? 22 : 14; + const handlePad = m ? 6 : 4; return (
@@ -773,7 +841,15 @@ } // ─── Footer ────────────────────────────────────────────────────────── -function Footer({ c }) { +function Footer({ c, m }) { + if (m) { + return ( +
+ SysEx · 7D · MIDI thru WebMIDI +
+ ); + } return (
+#include + +// ── Driver-private state ───────────────────────────────────────────── +// Only one MIDI device tracked at a time. The Workshop Computer's front +// jack is a single port; a hub could in principle bring more, but v1 +// only listens to the first MIDI device that mounts. +static volatile bool s_connected = false; +static uint8_t s_dev_addr = 0; +static uint8_t s_ep_in = 0; // bulk-IN endpoint address (with direction bit) +static uint16_t s_ep_in_size = 64; // bulk-IN max packet size +static uint8_t s_last_itf = TUSB_INDEX_INVALID_8; // highest interface we claimed + +// Per-button rising-edge state (CC value <64 → ≥64 = press). +static uint8_t s_btn22_prev = 0; +static uint8_t s_btn23_prev = 0; +static uint8_t s_btn24_prev = 0; + +// USB-MIDI Event Packets are 4 bytes each. 64 bytes = up to 16 events +// per IN transfer, which is plenty: an 8mu sweeps a fader at maybe +// 100 Hz max. +static uint8_t s_rx_buf[64]; + +// ── Helpers ────────────────────────────────────────────────────────── +static inline bool button_press_edge(uint8_t* prev, uint8_t value) { + bool press = (value >= 64) && (*prev < 64); + *prev = value; + return press; +} + +static void handle_cc(uint8_t cc, uint8_t v) { + bool changed = false; + if (cc >= 34 && cc <= 41) { + const uint8_t i = (uint8_t)(cc - 34); + if (gState.midiHostVelocityMode) { + gState.velocity[i] = (uint8_t)(v << 1); + } else { + gState.pitch[i] = v; + } + changed = true; + } else if (cc >= 50 && cc <= 57) { + gState.velocity[cc - 50] = (uint8_t)(v << 1); + changed = true; + } else if (cc == 28) { + uint8_t step = (uint8_t)((v * 8u) >> 7); + if (step > 7) step = 7; + gState.editStep = step; + changed = true; + } else if (cc == 22) { + if (button_press_edge(&s_btn22_prev, v)) { + gState.midiHostVelocityMode ^= 1; + changed = true; + } + } else if (cc == 23) { + if (button_press_edge(&s_btn23_prev, v)) { + gState.playing ^= 1; + changed = true; + } + } else if (cc == 24) { + if (button_press_edge(&s_btn24_prev, v)) { + // Mirrors the Pulse In 2 reset: jump to step 1. currentStep + // is normally written only by Core 0, but a single-byte + // store from Core 1 is atomic on the M0+ — and the audio + // ISR's only "reaction" is to display/play step 0, which + // is exactly what we want. + gState.currentStep = 0; + changed = true; + } + } + if (changed) gState.tickEpoch++; +} + +static void rearm_rx(void) { + if (!s_connected || s_ep_in == 0) return; + if (!usbh_edpt_claim(s_dev_addr, s_ep_in)) return; + uint16_t const len = (s_ep_in_size > sizeof(s_rx_buf)) + ? (uint16_t)sizeof(s_rx_buf) : s_ep_in_size; + if (!usbh_edpt_xfer(s_dev_addr, s_ep_in, s_rx_buf, len)) { + // claim is rolled back inside usbh_edpt_xfer on failure + } +} + +static void clear_state(void) { + s_connected = false; + s_dev_addr = 0; + s_ep_in = 0; + s_ep_in_size = 64; + s_last_itf = TUSB_INDEX_INVALID_8; + s_btn22_prev = 0; + s_btn23_prev = 0; + s_btn24_prev = 0; +} + +// ── Public API ─────────────────────────────────────────────────────── +extern "C" void midi_host_init(void) { + clear_state(); +} + +// ── TinyUSB class-driver callbacks ─────────────────────────────────── +// +// Driver lifecycle: +// driver_init — once at host stack init. +// driver_open — when a matching interface descriptor arrives +// during enumeration. We claim two interfaces +// (Audio-Control + MIDIStreaming) by parsing +// through to the bulk endpoints and opening them. +// driver_set_config — once the device is in CONFIGURED state. We +// kick off the first IN read here, then signal +// the host stack we're done with this interface. +// driver_xfer_cb — every time a bulk-IN packet completes. +// driver_close — on disconnect. + +static bool driver_init(void) { + clear_state(); + return true; +} + +static bool driver_deinit(void) { + clear_state(); + return true; +} + +static bool driver_open(uint8_t rhport, uint8_t dev_addr, + tusb_desc_interface_t const* itf_desc, uint16_t max_len) { + (void)rhport; + + // We only claim Audio-class interfaces. The descriptor parser at + // usbh.c:1681 groups Audio-Control (subclass 1) + MIDIStreaming + // (subclass 3) together via assoc_itf_count=2 when CFG_TUH_MIDI=1. + // Some devices skip Audio-Control entirely, so accept either entry. + if (itf_desc->bInterfaceClass != TUSB_CLASS_AUDIO) return false; + if (itf_desc->bInterfaceSubClass != 1 /* AUDIO_SUBCLASS_CONTROL */ && + itf_desc->bInterfaceSubClass != 3 /* AUDIO_SUBCLASS_MIDI_STREAMING */) { + return false; + } + + // Only one MIDI device at a time. + if (s_connected) return false; + + uint8_t const* p_desc = (uint8_t const*)itf_desc; + uint8_t const* p_end = p_desc + max_len; + + // Walk forward to the MIDIStreaming interface. If we entered on + // MIDIStreaming directly, the very first iteration matches. + // Stash its interface number so set_config can tell the host stack + // to resume past our claimed group — see driver_set_config below. + uint8_t ms_itf_num = TUSB_INDEX_INVALID_8; + while (p_desc < p_end) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + tusb_desc_interface_t const* itf = (tusb_desc_interface_t const*)p_desc; + if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && + itf->bInterfaceSubClass == 3 /* MIDIStreaming */) { + ms_itf_num = itf->bInterfaceNumber; + p_desc = tu_desc_next(p_desc); + break; + } + } + p_desc = tu_desc_next(p_desc); + } + if (ms_itf_num == TUSB_INDEX_INVALID_8) return false; + + // Now scan for the bulk endpoints. Stop at the next interface. + uint8_t ep_in = 0, ep_out = 0; + uint16_t ep_in_size = 64; + while (p_desc < p_end) { + uint8_t const dt = tu_desc_type(p_desc); + if (dt == TUSB_DESC_INTERFACE) { + break; + } else if (dt == TUSB_DESC_ENDPOINT) { + tusb_desc_endpoint_t const* ep = (tusb_desc_endpoint_t const*)p_desc; + if (ep->bmAttributes.xfer == TUSB_XFER_BULK) { + if (tu_edpt_dir(ep->bEndpointAddress) == TUSB_DIR_IN) { + if (!tuh_edpt_open(dev_addr, ep)) return false; + ep_in = ep->bEndpointAddress; + ep_in_size = tu_edpt_packet_size(ep); + } else { + if (!tuh_edpt_open(dev_addr, ep)) return false; + ep_out = ep->bEndpointAddress; + } + } + } + p_desc = tu_desc_next(p_desc); + } + if (ep_in == 0) return false; // need at least an IN endpoint + (void)ep_out; // OUT not used in v1 (no SysEx writes back to 8mu) + + s_dev_addr = dev_addr; + s_ep_in = ep_in; + s_ep_in_size = ep_in_size; + s_last_itf = ms_itf_num; + return true; +} + +static bool driver_set_config(uint8_t dev_addr, uint8_t itf_num) { + (void)itf_num; + s_connected = true; + rearm_rx(); + // Tell the host stack this driver has finished configuring. The + // stack's loop advances `itf_num++` and resumes searching for the + // next driver from there (usbh.c:1740). When our driver claimed + // both AudioControl + MIDIStreaming, both interface numbers map + // back to us in dev->itf2drv[], so we MUST return the highest + // claimed interface number — passing TUSB_INDEX_INVALID_8 (0xFF) + // would wrap to 0, find our driver again, and recurse until the + // stack overflows. The TinyUSB header note for IAD-binding drivers + // says exactly this: "should return itf_num + 1 when complete". + usbh_driver_set_config_complete(dev_addr, s_last_itf); + return true; +} + +static bool driver_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, + xfer_result_t result, uint32_t xferred_bytes) { + (void)dev_addr; + if (ep_addr != s_ep_in) { + // OUT-completion or stray callback — ignore. + return true; + } + if (result == XFER_RESULT_SUCCESS) { + // Parse 32-bit USB-MIDI Event Packets. Each packet: + // byte 0: (cable_num << 4) | code_index_number + // bytes 1..3: MIDI message bytes + // 8mu has only one cable, but we accept any cable index. + for (uint32_t i = 0; i + 4 <= xferred_bytes; i += 4) { + uint8_t const cin = (uint8_t)(s_rx_buf[i] & 0x0F); + if (cin == MIDI_CIN_CONTROL_CHANGE) { + handle_cc((uint8_t)(s_rx_buf[i + 2] & 0x7F), + (uint8_t)(s_rx_buf[i + 3] & 0x7F)); + } + // Other CINs (note on/off, sysex, pitch bend, etc.) are + // silently dropped — 8mu's button defaults could be notes, + // but our v1 protocol uses CC 22-24 as documented. + } + } + // Always re-arm: a stalled or aborted xfer should still try again. + rearm_rx(); + return true; +} + +static void driver_close(uint8_t dev_addr) { + if (s_dev_addr == dev_addr) { + clear_state(); + } +} + +// Aggregate-initialised in field order to avoid the C++ designated-init +// extension. Match the order of usbh_class_driver_t in usbh_pvt.h: +// name, init, deinit, open, set_config, xfer_cb, close. +static usbh_class_driver_t const s_drivers[] = { + { + "MIDI", + driver_init, + driver_deinit, + driver_open, + driver_set_config, + driver_xfer_cb, + driver_close, + }, +}; + +extern "C" usbh_class_driver_t const* usbh_app_driver_get_cb(uint8_t* driver_count) { + *driver_count = (uint8_t)(sizeof(s_drivers) / sizeof(s_drivers[0])); + return s_drivers; +} diff --git a/releases/33_drumdrum/midi_host.h b/releases/33_drumdrum/midi_host.h new file mode 100644 index 0000000..f594a3e --- /dev/null +++ b/releases/33_drumdrum/midi_host.h @@ -0,0 +1,28 @@ +#pragma once + +// Minimal class-compliant USB MIDI host for Music Thing 8mu support. +// +// Pico SDK 2.2.0 ships TinyUSB 0.18, which does not include a MIDI host +// class driver — only a descriptor-parser hint enabled via CFG_TUH_MIDI. +// We register our own driver via TinyUSB's usbh_app_driver_get_cb() +// extension point and read 32-bit USB-MIDI Event Packets directly off +// the bulk-IN endpoint. CC messages are dispatched into gState. +// +// CC mapping (channel-agnostic, edge-detected on buttons): +// 34..41 faders step pitches (or velocities when edit mode = 1) +// 50..57 faders step velocities (always) +// 28 fader edit cursor (0..7) +// 22 button (press) toggle pitch ↔ velocity edit mode +// 23 button (press) toggle play/pause +// 24 button (press) reset to step 1 +// all others ignored + +#ifdef __cplusplus +extern "C" { +#endif + +void midi_host_init(void); + +#ifdef __cplusplus +} +#endif diff --git a/releases/33_drumdrum/shared_state.h b/releases/33_drumdrum/shared_state.h index 93bb050..82f5269 100644 --- a/releases/33_drumdrum/shared_state.h +++ b/releases/33_drumdrum/shared_state.h @@ -27,6 +27,7 @@ struct SharedState { uint8_t editStep; // 0..7 selected step for editing uint8_t currentStep; // 0..7 playback position uint8_t playing; // 0/1 playback enable + uint8_t midiHostVelocityMode; // 0=8mu faders edit pitch, 1=velocity uint32_t tickEpoch; // ++ on every step advance (cross-core signal) }; diff --git a/releases/33_drumdrum/tusb_config.h b/releases/33_drumdrum/tusb_config.h index 0adb18d..4be59e8 100644 --- a/releases/33_drumdrum/tusb_config.h +++ b/releases/33_drumdrum/tusb_config.h @@ -28,7 +28,7 @@ extern "C" { #define CFG_TUD_MIDI_TX_BUFSIZE 128 #define CFG_TUD_MIDI_EP_BUFSIZE 64 -// ── Host stack (Monome Grid over CDC + FTDI) ───────────────── +// ── Host stack (Monome Grid over CDC + FTDI, Music Thing 8mu over MIDI) ───── #define CFG_TUH_ENUMERATION_BUFSIZE 256 #define CFG_TUH_HUB 1 #define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) @@ -40,6 +40,13 @@ extern "C" { #define CFG_TUH_MSC 0 #define CFG_TUH_VENDOR 0 +// TinyUSB 0.18 (shipped with Pico SDK 2.2.0) has no MIDI host class driver, +// only a one-block descriptor-parser hint that groups class-compliant USB +// MIDI's Audio-Control + MIDIStreaming interfaces under a single driver +// (usbh.c:1681). CFG_TUH_MIDI=1 turns that hint on; the actual driver is +// our own minimal one in midi_host.cpp, registered via usbh_app_driver_get_cb. +#define CFG_TUH_MIDI 1 + // Modern Monome Grids assert DTR/RTS on enumeration; older FTDI-based // units expect 115200 8N1. Both are mext-protocol grids on the wire. #define CFG_TUH_CDC_LINE_CONTROL_ON_ENUM 0x03 diff --git a/releases/33_drumdrum/usb_core1.cpp b/releases/33_drumdrum/usb_core1.cpp index d438112..837ec18 100644 --- a/releases/33_drumdrum/usb_core1.cpp +++ b/releases/33_drumdrum/usb_core1.cpp @@ -19,6 +19,7 @@ #include "midi_sysex.h" #include "monome_mext.h" #include "grid_ui.h" +#include "midi_host.h" #include "tusb.h" #include "bsp/board_api.h" @@ -26,10 +27,16 @@ volatile uint8_t gUsbHostMode = 0; -static void run_grid_loop(void) +static void run_host_loop(void) { board_init(); + // Both host paths coexist: a Grid plugged in fires CDC mount cbs + // into mext, an 8mu (or any class-compliant USB MIDI device) fires + // our app-registered MIDI driver. Whichever shows up wins; the + // other path stays idle. mext_task() drives tuh_task(), which is + // what dispatches the MIDI driver's xfer callbacks. mext_init(MEXT_TRANSPORT_HOST, 0); + midi_host_init(); tusb_init(); grid_ui_init(); while (true) { @@ -52,7 +59,7 @@ static void run_device_loop(void) extern "C" void core1_entry(void) { if (gUsbHostMode) { - run_grid_loop(); // never returns + run_host_loop(); // never returns } else { run_device_loop(); // never returns } diff --git a/releases/README.md b/releases/README.md index 96a27de..30c757b 100644 --- a/releases/README.md +++ b/releases/README.md @@ -26,7 +26,7 @@ | 30_cirpy_wavetable | Wavetable oscillator that using wavetables from Plaits, Braids, and Microwave, | 0.1
Functional but WIP | Circuit Python | @todbot / Tod Kurt | | 31_esp | A MS-20-style External Signal Processor that includes a preamp, bandpass filter, envelope follower, gate, and 1v/oct pitch outs. | 1.0
Released | C++(ComputerCard) | Ben Regnier | | 32_vink | Dual delay loops with sigmoid saturation for Jaap Vink / Roland Kayn style feedback patching | 1.1
Functional | C++(ComputerCard) | Ben Regnier | -| 33_drumdrum | DFAM-style 8-step sequencer
[Web editor](https://mohoyt.com/drumdrum.html) | 1.1.0
Functional but WIP | C++ (ComputerCard) | Moses Hoyt | +| 33_drumdrum | DFAM-style 8-step sequencer
[Web editor](https://mohoyt.com/drumdrum.html) | 1.2.0
Functional but WIP | C++ (ComputerCard) | Moses Hoyt | | 37_compulidean | Generative Euclidean drum + sample player. | (see source repo)
Functional, but WIP | C++/Arduino, with vscode+platformio. | Tristan Rowley | | 38_od | Loopable chaotic Lorenz attractor trajectories and zero-crossings as CV and pulses, with sensitivity to initial conditions. | 1.0
Released | MicroPython | M. John Mills | | 39_knots | Six-engine oscillator firmware for the Music Thing Workshop System | 0.2
Released | C++ (RPi Pico SDK / ComputerCard) | Jeff Fletcher |