236 lines
7.8 KiB
Python
236 lines
7.8 KiB
Python
# SPDX-FileCopyrightText: 2023 John Park for Adafruit
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
# Hexboard seven key modal note/chord pad for MIDI instruments
|
|
# Runs on QT Py RP2040
|
|
# (other QT Pys should work, but the BOOT button is handy for initiating configuration)
|
|
|
|
import time
|
|
import board
|
|
from digitalio import DigitalInOut, Pull
|
|
import keypad
|
|
import neopixel
|
|
import rainbowio
|
|
import usb_midi
|
|
import adafruit_midi
|
|
from adafruit_midi.note_on import NoteOn
|
|
from adafruit_midi.note_off import NoteOff
|
|
|
|
button = DigitalInOut(board.BUTTON)
|
|
button.pull = Pull.UP
|
|
|
|
num_switches = 7
|
|
leds = neopixel.NeoPixel(board.A0, num_switches, brightness=0.7)
|
|
leds.fill(rainbowio.colorwheel(5))
|
|
leds.show()
|
|
|
|
# root_picked = False
|
|
note = 0
|
|
root = 0 # defaults to a C
|
|
|
|
# lists of modal intervals (relative to root). Customize these if you want other scales/keys
|
|
major = (0, 2, 4, 5, 7, 9, 11)
|
|
minor = (0, 2, 3, 5, 7, 8, 10)
|
|
dorian = (0, 2, 3, 5, 7, 9, 10)
|
|
phrygian = (0, 1, 3, 5, 7, 8, 10)
|
|
lydian = (0, 2, 4, 6, 7, 9, 11)
|
|
mixolydian = (0, 2, 4, 5, 7, 9, 10)
|
|
locrian = (0, 1, 3, 5, 6, 8, 10)
|
|
|
|
modes = []
|
|
modes.append(major)
|
|
modes.append(minor)
|
|
modes.append(dorian)
|
|
modes.append(phrygian)
|
|
modes.append(lydian)
|
|
modes.append(mixolydian)
|
|
modes.append(locrian)
|
|
|
|
octv = 4
|
|
mode = 0 # default to major scale
|
|
play_chords = True # default to play chords
|
|
pre_notes = modes[mode] # initial mapping
|
|
keymap = (4, 3, 5, 0, 2, 6, 1) # physical to logical key mapping
|
|
|
|
# Key chart | logical |Interval chart example
|
|
# 6 1 | 6 7 | 9 11
|
|
# 5 0 2 | 3 4 5 | 4 5 7
|
|
# 4 3 | 0 1 | 0 2
|
|
|
|
# MIDI Setup
|
|
midi_usb_channel = 1 # change this to your desired MIDI out channel, 1-16
|
|
midi_usb = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=midi_usb_channel-1)
|
|
|
|
# Keyswitch setup
|
|
keyswitch_pins = (board.A3, board.A2, board.SDA, board.SCL, board.TX, board.RX, board.A1)
|
|
keyswitches = keypad.Keys(keyswitch_pins, value_when_pressed=False, pull=True)
|
|
|
|
def pick_mode():
|
|
print("Choose mode...")
|
|
mode_picked = False
|
|
# pylint: disable=global-statement
|
|
global mode
|
|
while not mode_picked:
|
|
# pylint: disable=redefined-outer-name
|
|
keyswitch = keyswitches.events.get() # check for key events
|
|
if keyswitch:
|
|
if keyswitch.pressed:
|
|
mode = keymap.index(keyswitch.key_number) # bottom left key is 0/major
|
|
print("Mode is:", mode)
|
|
if keyswitch.released:
|
|
mode_picked = True
|
|
leds.fill(rainbowio.colorwheel(8))
|
|
leds.show()
|
|
pick_octave()
|
|
|
|
def pick_octave():
|
|
print("Choose octave...")
|
|
octave_picked = False
|
|
# pylint: disable=global-statement
|
|
global octv
|
|
while not octave_picked:
|
|
if button.value is False: # pressed
|
|
launch_config()
|
|
time.sleep(0.1)
|
|
# pylint: disable=redefined-outer-name
|
|
keyswitch = keyswitches.events.get() # check for key events
|
|
if keyswitch:
|
|
if keyswitch.pressed:
|
|
octv = keymap.index(keyswitch.key_number) # get remapped position, lower left is 0
|
|
print("Octave is:", octv)
|
|
if keyswitch.released:
|
|
octave_picked = True
|
|
leds.fill(rainbowio.colorwheel(16))
|
|
pick_root()
|
|
|
|
def pick_root():# user selects key in which to play
|
|
print("Choose root note...")
|
|
root_picked = False
|
|
# pylint: disable=global-statement
|
|
global root
|
|
while not root_picked:
|
|
if button.value is False: # pressed
|
|
launch_config()
|
|
time.sleep(0.1)
|
|
# pylint: disable=redefined-outer-name
|
|
keyswitch = keyswitches.events.get() # check for key events
|
|
if keyswitch:
|
|
if keyswitch.pressed:
|
|
root = keymap.index(keyswitch.key_number) # get remapped position, lower left is 0
|
|
print("ksw:", keyswitch.key_number, "keymap index:", root)
|
|
note = pre_notes[root]
|
|
print("note:", note)
|
|
midi_usb.send(NoteOn(note + (12*octv), 120))
|
|
root_notes.clear()
|
|
# pylint: disable=redefined-outer-name
|
|
for mode_interval in range(num_switches):
|
|
root_notes.append(modes[mode][mode_interval] + note)
|
|
print("root note intervals:", root_notes)
|
|
if keyswitch.released:
|
|
note = pre_notes[root]
|
|
midi_usb.send(NoteOff(note + (12*octv), 0))
|
|
root_picked = True
|
|
leds.fill(0x0)
|
|
leds[3] = rainbowio.colorwheel(12)
|
|
leds[4] = rainbowio.colorwheel(5)
|
|
leds.show()
|
|
pick_chords()
|
|
|
|
def pick_chords():
|
|
print("Choose chords vs. single notes...")
|
|
chords_picked = False
|
|
# pylint: disable=global-statement
|
|
global play_chords
|
|
while not chords_picked:
|
|
if button.value is False: # pressed
|
|
launch_config()
|
|
time.sleep(0.1)
|
|
# pylint: disable=redefined-outer-name
|
|
keyswitch = keyswitches.events.get() # check for key events
|
|
if keyswitch:
|
|
if keyswitch.pressed:
|
|
if keyswitch.key_number == 4:
|
|
play_chords = True
|
|
print("Chords are on")
|
|
chords_picked = True
|
|
playback_led_colors()
|
|
if keyswitch.key_number == 3:
|
|
play_chords = False
|
|
print("Chords are off")
|
|
chords_picked = True
|
|
playback_led_colors()
|
|
|
|
# create the interval list based on root key and mode that's been picked in variable
|
|
root_notes = []
|
|
for mode_interval in range(num_switches):
|
|
root_notes.append(modes[mode][mode_interval] + note)
|
|
print("---Hexpad---")
|
|
print("\nRoot note intervals:", root_notes)
|
|
|
|
key_colors = (18, 10, 18, 26, 26, 18, 10)
|
|
|
|
def playback_led_colors():
|
|
for i in range(num_switches):
|
|
leds[i]=(rainbowio.colorwheel(key_colors[i]))
|
|
leds.show()
|
|
time.sleep(0.1)
|
|
|
|
playback_led_colors()
|
|
|
|
# MIDI Note Message Functions
|
|
def send_note_on(note_num):
|
|
if play_chords is True:
|
|
note_num = root_notes[note_num] + (12*octv)
|
|
midi_usb.send(NoteOn(note_num, 120))
|
|
midi_usb.send(NoteOn(note_num + modes[mode][2], 80))
|
|
midi_usb.send(NoteOn(note_num + modes[mode][4], 60))
|
|
midi_usb.send(NoteOn(note_num+12, 80))
|
|
else:
|
|
note_num = root_notes[note_num] + (12*octv)
|
|
midi_usb.send(NoteOn(note_num, 120))
|
|
|
|
|
|
def send_note_off(note_num):
|
|
if play_chords is True:
|
|
note_num = root_notes[note_num] + (12*octv)
|
|
midi_usb.send(NoteOff(note_num, 0))
|
|
midi_usb.send(NoteOff(note_num + modes[mode][2], 0))
|
|
midi_usb.send(NoteOff(note_num + modes[mode][4], 0))
|
|
midi_usb.send(NoteOff(note_num+12, 0))
|
|
else:
|
|
note_num = root_notes[note_num] + (12*octv)
|
|
midi_usb.send(NoteOff(note_num, 0))
|
|
|
|
def send_midi_panic():
|
|
for x in range(128):
|
|
midi_usb.send(NoteOff(x, 0))
|
|
|
|
def launch_config():
|
|
print("-launching config-")
|
|
send_midi_panic()
|
|
leds.fill(rainbowio.colorwheel(5))
|
|
leds.show()
|
|
pick_mode()
|
|
|
|
send_midi_panic() # turn off any stuck notes at startup
|
|
|
|
|
|
while True:
|
|
keyswitch = keyswitches.events.get() # check for key events
|
|
if keyswitch:
|
|
keyswitch_number=keyswitch.key_number
|
|
if keyswitch.pressed:
|
|
note_picked = keymap.index(keyswitch.key_number)
|
|
send_note_on(note_picked)
|
|
leds[keyswitch_number]=(rainbowio.colorwheel(10))
|
|
|
|
leds.show()
|
|
if keyswitch.released:
|
|
note_picked = keymap.index(keyswitch.key_number)
|
|
send_note_off(note_picked)
|
|
leds[keyswitch_number]=(rainbowio.colorwheel(key_colors[keyswitch_number]))
|
|
leds.show()
|
|
|
|
if button.value is False: # pressed
|
|
launch_config()
|
|
time.sleep(0.1)
|