From 9c57c04a0019193499cc941eda8838efdc30e977 Mon Sep 17 00:00:00 2001 From: Andrey Salomatin Date: Sat, 11 Apr 2026 00:50:28 +0200 Subject: [PATCH] Drum Rack example --- firmware/README.md | 1 + .../examples/nanogrid-drum-rack/README.md | 20 ++ firmware/examples/nanogrid-drum-rack/main.py | 210 ++++++++++++++++++ .../examples/nanogrid-drum-rack/manifest.py | 4 + 4 files changed, 235 insertions(+) create mode 100644 firmware/examples/nanogrid-drum-rack/README.md create mode 100644 firmware/examples/nanogrid-drum-rack/main.py create mode 100644 firmware/examples/nanogrid-drum-rack/manifest.py diff --git a/firmware/README.md b/firmware/README.md index 85cd35c..9a6213b 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -16,6 +16,7 @@ Custom micropython build for the Olimex RP2350B-XL. Comes with some features ena 9. Run `make BOARD=OLIMEX_PICO2_XL submodules` to install dependencies 10. Run `make BOARD=OLIMEX_PICO2_XL clean` to remove previous build artefacts (if any) 11. Run `make BOARD=OLIMEX_PICO2_XL` to build the firmware +12. To rebuild: `make BOARD=OLIMEX_PICO2_XL clean && make BOARD=OLIMEX_PICO2_XL` ## flashing diff --git a/firmware/examples/nanogrid-drum-rack/README.md b/firmware/examples/nanogrid-drum-rack/README.md new file mode 100644 index 0000000..a4a24ca --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/README.md @@ -0,0 +1,20 @@ +# MIDI Drum Rack example + +Press to send a MIDI note in the range from C1 to D#2. Buttons are +ordered in the same way the pads are in Ableton Drum Rack: with the USB +port poiting from the player left-to-right, bottom-to-top. + + +## build + +1. Make a micropython build with the libraries added: + +```bash +cd $MICROPYTHON_PATH/ports/rp2 +make BOARD=OLIMEX_PICO2_XL clean && make V=1 BOARD=OLIMEX_PICO2_XL FROZEN_MANIFEST={path-to-neogrid-project}/firmware/examples/nanogrid-drum-rack/manifest.py +``` + +2. Flush the board with the resulting uf2 (see + [firmware/README.md](/firmware/README.md)) + +3. Copy the `main.py` to the board diff --git a/firmware/examples/nanogrid-drum-rack/main.py b/firmware/examples/nanogrid-drum-rack/main.py new file mode 100644 index 0000000..53bd603 --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/main.py @@ -0,0 +1,210 @@ +# MicroPython USB MIDI example +# +# This example demonstrates creating a custom MIDI device. +# +# To run this example: +# +# 1. Make sure `usb-device-midi` is installed via: mpremote mip install usb-device-midi +# +# 2. Run the example via: mpremote run midi_example.py +# +# 3. mpremote will exit with an error after the previous step, because when the +# example runs the existing USB device disconnects and then re-enumerates with +# the MIDI interface present. At this point, the example is running. +# +# 4. To see output from the example, re-connect: mpremote connect PORTNAME +# +# +# MIT license; Copyright (c) 2023-2024 Angus Gratton +import usb.device +from usb.device.midi import MIDIInterface +import time + +from machine import Pin + +import array, time +import rp2 + +NUM_COL = 4 +NUM_ROW = 4 +NUM_LEDS = NUM_COL * NUM_ROW +PIN_NUM = 16 +brightness = 0.2 + +@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24) +def ws2812(): + T1 = 2 + T2 = 5 + T3 = 3 + wrap_target() + label("bitloop") + out(x, 1) .side(1) [T3 - 1] + jmp(not_x, "do_zero") .side(0) [T1 - 1] + jmp("bitloop") .side(0) [T2 - 1] + label("do_zero") + nop() .side(1) [T2 - 1] + wrap() + +def init_leds(): + # Create the StateMachine with the ws2812 program, outputting on pin + sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM)) + + # Start the StateMachine, it will wait for data on its FIFO. + sm.active(1) + + # Display a pattern on the LEDs via an array of LED RGB values. + ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + + ########################################################################## + def show_all(): + dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)]) + for i, c in enumerate(ar): + r = int(((c >> 8) & 0xFF) * brightness) + g = int(((c >> 16) & 0xFF) * brightness) + b = int((c & 0xFF) * brightness) + dimmer_ar[i] = (g<<16) + (r<<8) + b + sm.put(dimmer_ar, 8) + + def set_led(i, color): + row = i // NUM_COL + col = i - row * NUM_COL + new_col = NUM_COL - col - 1 + new_row = NUM_ROW - row - 1 if new_col % 2 == 0 else row + ar[new_col * NUM_ROW + new_row] = (color[1]<<16) + (color[0]<<8) + color[2] + + return (set_led, show_all) + +class MIDIExample(MIDIInterface): + # Very simple example event handler functions, showing how to receive note + # and control change messages sent from the host to the device. + # + # If you need to send MIDI data to the host, then it's fine to instantiate + # MIDIInterface class directly. + + def on_open(self): + super().on_open() + print("Device opened by host") + + def on_note_on(self, channel, pitch, vel): + print(f"RX Note On channel {channel} pitch {pitch} velocity {vel}") + + def on_note_off(self, channel, pitch, vel): + print(f"RX Note Off channel {channel} pitch {pitch} velocity {vel}") + + def on_control_change(self, channel, controller, value): + print(f"RX Control channel {channel} controller {controller} value {value}") + + +m = MIDIExample() +# Remove builtin_driver=True if you don't want the MicroPython serial REPL available. +usb.device.get().init(m, builtin_driver=True) + +print("Waiting for USB host to configure the interface...") + +while not m.is_open(): + time.sleep_ms(100) + +# TX constants +CHANNEL = 0 +PITCH_C1 = 37 +CONTROLLER = 64 + +control_val = 0 + +def init_btns(): + row_pins = [ + Pin(0, Pin.OUT), + Pin(1, Pin.OUT), + Pin(2, Pin.OUT), + Pin(3, Pin.OUT), + ] + + col_pins = [ + Pin(32, Pin.IN, Pin.PULL_UP), + Pin(33, Pin.IN, Pin.PULL_UP), + Pin(34, Pin.IN, Pin.PULL_UP), + Pin(35, Pin.IN, Pin.PULL_UP), + ] + + pressed_keys = set() + + # reset cols + for pin in row_pins: + pin.value(1) + + def loop(): + pressed_keys.clear() + for row, row_pin in enumerate(row_pins): + # set only active pin low, keep rest high + row_pin.value(0) + for col, col_pin in enumerate(col_pins): + key = ( + (NUM_ROW - row - 1) + + (NUM_COL - col - 1) * NUM_ROW + ) + if not col_pin.value(): + pressed_keys.add(key) + row_pin.value(1) + return set(pressed_keys) + + return loop + +btn_loop = init_btns() +turned_on = set() + +(set_led, show_all) = init_leds() + +# for i in range(NUM_LEDS): +# set_led(i, CYAN) +# for j in range(NUM_LEDS): +# if i != j: +# set_led(j, BLACK) +# show_all() +# time.sleep(2) +# + +CYAN = (0, 255, 255) +BLACK = (0, 0, 0) + +print("Starting loop...") +while m.is_open(): + time.sleep_ms(5) + # print(f"TX Note On channel {CHANNEL} pitch {PITCH_C1}") + # m.note_on(CHANNEL, PITCH_C1) # Velocity is an optional third argument + # time.sleep(0.5) + # print(f"TX Note Off channel {CHANNEL} pitch {PITCH_C1}") + # m.note_off(CHANNEL, PITCH_C1) + # time.sleep(1) + # print(f"TX Control channel {CHANNEL} controller {CONTROLLER} value {control_val}") + # m.control_change(CHANNEL, CONTROLLER, control_val) + # control_val += 1 + # if control_val == 0x7F: + # control_val = 0 + # time.sleep(1) + + pressed = btn_loop() + + to_turn_on = pressed.difference(turned_on) + to_turn_off = turned_on.difference(pressed) + + if len(to_turn_on): + print("to_turn_on", to_turn_on) + + if len(to_turn_off): + print("to_turn_off", to_turn_off) + + + for key in to_turn_on: + m.note_on(CHANNEL, PITCH_C1 + key - 1) + set_led(key, CYAN) + turned_on.add(key) + + for key in to_turn_off: + m.note_off(CHANNEL, PITCH_C1 + key - 1) + turned_on.remove(key) + set_led(key, BLACK) + + show_all() + +print("USB host has reset device, example done.") + diff --git a/firmware/examples/nanogrid-drum-rack/manifest.py b/firmware/examples/nanogrid-drum-rack/manifest.py new file mode 100644 index 0000000..e53101e --- /dev/null +++ b/firmware/examples/nanogrid-drum-rack/manifest.py @@ -0,0 +1,4 @@ +metadata(version="0.1.0") +include("$(PORT_DIR)/boards/manifest.py") +require("usb-device") +require("usb-device-midi")