|
| 1 | +# SPDX-FileCopyrightText: 2023 John Park for Adafruit |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +# Hexboard seven key modal note/chord pad for MIDI instruments |
| 5 | +# Runs on QT Py RP2040 |
| 6 | +# (other QT Pys should work, but the BOOT button is handy for initiating configuration) |
| 7 | + |
| 8 | +import time |
| 9 | +import board |
| 10 | +from digitalio import DigitalInOut, Pull |
| 11 | +import keypad |
| 12 | +import neopixel |
| 13 | +import rainbowio |
| 14 | +import usb_midi |
| 15 | +import adafruit_midi |
| 16 | +from adafruit_midi.note_on import NoteOn |
| 17 | +from adafruit_midi.note_off import NoteOff |
| 18 | + |
| 19 | +button = DigitalInOut(board.BUTTON) |
| 20 | +button.pull = Pull.UP |
| 21 | + |
| 22 | +num_switches = 7 |
| 23 | +leds = neopixel.NeoPixel(board.A0, num_switches, brightness=0.7) |
| 24 | +leds.fill(rainbowio.colorwheel(5)) |
| 25 | +leds.show() |
| 26 | + |
| 27 | +# root_picked = False |
| 28 | +note = 0 |
| 29 | +root = 0 # defaults to a C |
| 30 | + |
| 31 | +# lists of modal intervals (relative to root). Customize these if you want other scales/keys |
| 32 | +major = (0, 2, 4, 5, 7, 9, 11) |
| 33 | +minor = (0, 2, 3, 5, 7, 8, 10) |
| 34 | +dorian = (0, 2, 3, 5, 7, 9, 10) |
| 35 | +phrygian = (0, 1, 3, 5, 7, 8, 10) |
| 36 | +lydian = (0, 2, 4, 6, 7, 9, 11) |
| 37 | +mixolydian = (0, 2, 4, 5, 7, 9, 10) |
| 38 | +locrian = (0, 1, 3, 5, 6, 8, 10) |
| 39 | + |
| 40 | +modes = [] |
| 41 | +modes.append(major) |
| 42 | +modes.append(minor) |
| 43 | +modes.append(dorian) |
| 44 | +modes.append(phrygian) |
| 45 | +modes.append(lydian) |
| 46 | +modes.append(mixolydian) |
| 47 | +modes.append(locrian) |
| 48 | + |
| 49 | +octv = 4 |
| 50 | +mode = 0 # default to major scale |
| 51 | +play_chords = True # default to play chords |
| 52 | +pre_notes = modes[mode] # initial mapping |
| 53 | +keymap = (4, 3, 5, 0, 2, 6, 1) # physical to logical key mapping |
| 54 | + |
| 55 | +# Key chart | logical |Interval chart example |
| 56 | +# 6 1 | 6 7 | 9 11 |
| 57 | +# 5 0 2 | 3 4 5 | 4 5 7 |
| 58 | +# 4 3 | 0 1 | 0 2 |
| 59 | + |
| 60 | +# MIDI Setup |
| 61 | +midi_usb_channel = 1 # change this to your desired MIDI out channel, 1-16 |
| 62 | +midi_usb = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=midi_usb_channel-1) |
| 63 | + |
| 64 | +# Keyswitch setup |
| 65 | +keyswitch_pins = (board.A3, board.A2, board.SDA, board.SCL, board.TX, board.RX, board.A1) |
| 66 | +keyswitches = keypad.Keys(keyswitch_pins, value_when_pressed=False, pull=True) |
| 67 | + |
| 68 | +def pick_mode(): |
| 69 | + print("Choose mode...") |
| 70 | + mode_picked = False |
| 71 | + # pylint: disable=global-statement |
| 72 | + global mode |
| 73 | + while not mode_picked: |
| 74 | + # pylint: disable=redefined-outer-name |
| 75 | + keyswitch = keyswitches.events.get() # check for key events |
| 76 | + if keyswitch: |
| 77 | + if keyswitch.pressed: |
| 78 | + mode = keymap.index(keyswitch.key_number) # bottom left key is 0/major |
| 79 | + print("Mode is:", mode) |
| 80 | + if keyswitch.released: |
| 81 | + mode_picked = True |
| 82 | + leds.fill(rainbowio.colorwheel(8)) |
| 83 | + leds.show() |
| 84 | + pick_octave() |
| 85 | + |
| 86 | +def pick_octave(): |
| 87 | + print("Choose octave...") |
| 88 | + octave_picked = False |
| 89 | + # pylint: disable=global-statement |
| 90 | + global octv |
| 91 | + while not octave_picked: |
| 92 | + if button.value is False: # pressed |
| 93 | + launch_config() |
| 94 | + time.sleep(0.1) |
| 95 | + # pylint: disable=redefined-outer-name |
| 96 | + keyswitch = keyswitches.events.get() # check for key events |
| 97 | + if keyswitch: |
| 98 | + if keyswitch.pressed: |
| 99 | + octv = keymap.index(keyswitch.key_number) # get remapped position, lower left is 0 |
| 100 | + print("Octave is:", octv) |
| 101 | + if keyswitch.released: |
| 102 | + octave_picked = True |
| 103 | + leds.fill(rainbowio.colorwheel(16)) |
| 104 | + pick_root() |
| 105 | + |
| 106 | +def pick_root():# user selects key in which to play |
| 107 | + print("Choose root note...") |
| 108 | + root_picked = False |
| 109 | + # pylint: disable=global-statement |
| 110 | + global root |
| 111 | + while not root_picked: |
| 112 | + if button.value is False: # pressed |
| 113 | + launch_config() |
| 114 | + time.sleep(0.1) |
| 115 | + # pylint: disable=redefined-outer-name |
| 116 | + keyswitch = keyswitches.events.get() # check for key events |
| 117 | + if keyswitch: |
| 118 | + if keyswitch.pressed: |
| 119 | + root = keymap.index(keyswitch.key_number) # get remapped position, lower left is 0 |
| 120 | + print("ksw:", keyswitch.key_number, "keymap index:", root) |
| 121 | + note = pre_notes[root] |
| 122 | + print("note:", note) |
| 123 | + midi_usb.send(NoteOn(note + (12*octv), 120)) |
| 124 | + root_notes.clear() |
| 125 | + # pylint: disable=redefined-outer-name |
| 126 | + for mode_interval in range(num_switches): |
| 127 | + root_notes.append(modes[mode][mode_interval] + note) |
| 128 | + print("root note intervals:", root_notes) |
| 129 | + if keyswitch.released: |
| 130 | + note = pre_notes[root] |
| 131 | + midi_usb.send(NoteOff(note + (12*octv), 0)) |
| 132 | + root_picked = True |
| 133 | + leds.fill(0x0) |
| 134 | + leds[3] = rainbowio.colorwheel(12) |
| 135 | + leds[4] = rainbowio.colorwheel(5) |
| 136 | + leds.show() |
| 137 | + pick_chords() |
| 138 | + |
| 139 | +def pick_chords(): |
| 140 | + print("Choose chords vs. single notes...") |
| 141 | + chords_picked = False |
| 142 | + # pylint: disable=global-statement |
| 143 | + global play_chords |
| 144 | + while not chords_picked: |
| 145 | + if button.value is False: # pressed |
| 146 | + launch_config() |
| 147 | + time.sleep(0.1) |
| 148 | + # pylint: disable=redefined-outer-name |
| 149 | + keyswitch = keyswitches.events.get() # check for key events |
| 150 | + if keyswitch: |
| 151 | + if keyswitch.pressed: |
| 152 | + if keyswitch.key_number == 4: |
| 153 | + play_chords = True |
| 154 | + print("Chords are on") |
| 155 | + chords_picked = True |
| 156 | + playback_led_colors() |
| 157 | + if keyswitch.key_number == 3: |
| 158 | + play_chords = False |
| 159 | + print("Chords are off") |
| 160 | + chords_picked = True |
| 161 | + playback_led_colors() |
| 162 | + |
| 163 | +# create the interval list based on root key and mode that's been picked in variable |
| 164 | +root_notes = [] |
| 165 | +for mode_interval in range(num_switches): |
| 166 | + root_notes.append(modes[mode][mode_interval] + note) |
| 167 | +print("---Hexpad---") |
| 168 | +print("\nRoot note intervals:", root_notes) |
| 169 | + |
| 170 | +key_colors = (18, 10, 18, 26, 26, 18, 10) |
| 171 | + |
| 172 | +def playback_led_colors(): |
| 173 | + for i in range(num_switches): |
| 174 | + leds[i]=(rainbowio.colorwheel(key_colors[i])) |
| 175 | + leds.show() |
| 176 | + time.sleep(0.1) |
| 177 | + |
| 178 | +playback_led_colors() |
| 179 | + |
| 180 | +# MIDI Note Message Functions |
| 181 | +def send_note_on(note_num): |
| 182 | + if play_chords is True: |
| 183 | + note_num = root_notes[note_num] + (12*octv) |
| 184 | + midi_usb.send(NoteOn(note_num, 120)) |
| 185 | + midi_usb.send(NoteOn(note_num + modes[mode][2], 80)) |
| 186 | + midi_usb.send(NoteOn(note_num + modes[mode][4], 60)) |
| 187 | + midi_usb.send(NoteOn(note_num+12, 80)) |
| 188 | + else: |
| 189 | + note_num = root_notes[note_num] + (12*octv) |
| 190 | + midi_usb.send(NoteOn(note_num, 120)) |
| 191 | + |
| 192 | + |
| 193 | +def send_note_off(note_num): |
| 194 | + if play_chords is True: |
| 195 | + note_num = root_notes[note_num] + (12*octv) |
| 196 | + midi_usb.send(NoteOff(note_num, 0)) |
| 197 | + midi_usb.send(NoteOff(note_num + modes[mode][2], 0)) |
| 198 | + midi_usb.send(NoteOff(note_num + modes[mode][4], 0)) |
| 199 | + midi_usb.send(NoteOff(note_num+12, 0)) |
| 200 | + else: |
| 201 | + note_num = root_notes[note_num] + (12*octv) |
| 202 | + midi_usb.send(NoteOff(note, 0)) |
| 203 | + |
| 204 | +def send_midi_panic(): |
| 205 | + for x in range(128): |
| 206 | + midi_usb.send(NoteOff(x, 0)) |
| 207 | + |
| 208 | +def launch_config(): |
| 209 | + print("-launching config-") |
| 210 | + send_midi_panic() |
| 211 | + leds.fill(rainbowio.colorwheel(5)) |
| 212 | + leds.show() |
| 213 | + pick_mode() |
| 214 | + |
| 215 | +send_midi_panic() # turn off any stuck notes at startup |
| 216 | + |
| 217 | + |
| 218 | +while True: |
| 219 | + keyswitch = keyswitches.events.get() # check for key events |
| 220 | + if keyswitch: |
| 221 | + keyswitch_number=keyswitch.key_number |
| 222 | + if keyswitch.pressed: |
| 223 | + note_picked = keymap.index(keyswitch.key_number) |
| 224 | + send_note_on(note_picked) |
| 225 | + leds[keyswitch_number]=(rainbowio.colorwheel(10)) |
| 226 | + |
| 227 | + leds.show() |
| 228 | + if keyswitch.released: |
| 229 | + note_picked = keymap.index(keyswitch.key_number) |
| 230 | + send_note_off(note_picked) |
| 231 | + leds[keyswitch_number]=(rainbowio.colorwheel(key_colors[keyswitch_number])) |
| 232 | + leds.show() |
| 233 | + |
| 234 | + if button.value is False: # pressed |
| 235 | + launch_config() |
| 236 | + time.sleep(0.1) |
0 commit comments