code.py
  1  # SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries
  2  # SPDX-License-Identifier: MIT
  3  
  4  # Pico RP2040 Mechanical MIDI Modal Keyboard
  5  # 7x3 mech keyboard
  6  # Each key sends MIDI NoteOn / NoteOff message over USB
  7  # Can be any scale/mode
  8  # Key combo sends MIDI panic (see bottom section of code)
  9  
 10  import time
 11  import board
 12  from digitalio import DigitalInOut, Direction, Pull
 13  import usb_midi
 14  import adafruit_midi
 15  from adafruit_midi.note_on import NoteOn
 16  from adafruit_midi.note_off import NoteOff
 17  from adafruit_debouncer import Debouncer
 18  
 19  print("---Pico MIDI Modal Mech Keyboard---")
 20  
 21  MIDI_CHANNEL = 1  # pick your MIDI channel here
 22  
 23  midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=MIDI_CHANNEL-1)
 24  
 25  def send_midi_panic():
 26      print("All MIDI notes off")
 27      for x in range(128):
 28          midi.send(NoteOff(x, 0))
 29  
 30  led = DigitalInOut(board.LED)
 31  led.direction = Direction.OUTPUT
 32  led.value = True
 33  
 34  num_keys = 21
 35  
 36  # list of pins to use (skipping GP15 on Pico because it's funky)
 37  pins = (
 38      board.GP0,
 39      board.GP1,
 40      board.GP2,
 41      board.GP3,
 42      board.GP4,
 43      board.GP5,
 44      board.GP6,
 45      board.GP7,
 46      board.GP8,
 47      board.GP9,
 48      board.GP10,
 49      board.GP11,
 50      board.GP12,
 51      board.GP13,
 52      board.GP14,
 53      board.GP16,
 54      board.GP17,
 55      board.GP18,
 56      board.GP19,
 57      board.GP20,
 58      board.GP21,
 59  )
 60  
 61  
 62  keys = []
 63  for pin in pins:
 64      tmp_pin = DigitalInOut(pin)
 65      tmp_pin.pull = Pull.UP
 66      keys.append(Debouncer(tmp_pin))
 67  
 68  root_notes = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59)  # used during config
 69  note_numbers = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
 70                  60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
 71                  72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83)
 72  note_names = ("C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
 73                "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
 74                "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",)
 75  scale_root = root_notes[0]  # default if nothing is picked
 76  root_picked = False  # state of root selection
 77  mode_picked = False  # state of mode selection
 78  mode_choice = 0
 79  
 80  
 81  #  ----- User selection of the root note ----- #
 82  print("Pick the root using top twelve keys, then press bottom right key to enter:")
 83  print(". . . . . . .")
 84  print(". . . . . o o")
 85  print("o o o o o o .")
 86  
 87  while not root_picked:
 88      for i in range(12):
 89          keys[i].update()
 90          if keys[i].fell:
 91              scale_root = root_notes[i]
 92              midi.send(NoteOn(root_notes[i], 120))
 93              print("Root is", note_names[i])
 94          if keys[i].rose:
 95              midi.send(NoteOff(root_notes[i], 0))
 96      keys[20].update()
 97  
 98      if keys[20].rose:
 99          root_picked = True
100          print("Root picked.\n")
101  
102  #  lists of mode intervals relative to root
103  major = ( 0, 2, 4, 5, 7, 9, 11 )
104  minor = ( 0, 2, 3, 5, 7, 8, 10 )
105  dorian = ( 0, 2, 3, 5, 7, 9, 10 )
106  phrygian = ( 0, 1, 3, 5, 7, 8, 10 )
107  lydian = (0 , 2, 4, 6, 7, 9, 11 )
108  mixolydian = ( 0, 2, 4, 5, 7, 9, 10)
109  locrian = ( 0, 1, 3, 5, 6, 8, 10)
110  
111  modes = []
112  modes.append(major)
113  modes.append(minor)
114  modes.append(dorian)
115  modes.append(phrygian)
116  modes.append(lydian)
117  modes.append(mixolydian)
118  modes.append(locrian)
119  
120  mode_names = ("Major/Ionian",
121                "Minor/Aeolian",
122                "Dorian",
123                "Phrygian",
124                "Lydian",
125                "Mixolydian",
126                "Locrian")
127  
128  intervals = list(mixolydian)  # intervals for Mixolydian by default
129  
130  print("Pick the mode with top seven keys, then press bottom right key to enter:")
131  print(". . . . . . .")
132  print("o o o o o o o")
133  print("o o o o o o .")
134  
135  while not mode_picked:
136      for i in range(7):
137          keys[i].update()
138          if keys[i].fell:
139              mode_choice = i
140              print(mode_names[mode_choice], "mode")
141              for j in range(7):
142                  intervals[j] = modes[i][j]
143              # play the scale
144              for k in range(7):
145                  midi.send(NoteOn(scale_root+intervals[k], 120))
146                  note_index = note_numbers.index(scale_root+intervals[k])
147                  print(note_names[note_index])
148                  time.sleep(0.15)
149                  midi.send(NoteOff(scale_root+intervals[k], 0))
150                  time.sleep(0.15)
151              midi.send(NoteOn(scale_root+12, 120))
152              note_index = note_numbers.index(scale_root+12)
153              print(note_names[note_index], "\n")
154              time.sleep(0.15)
155              midi.send(NoteOff(scale_root+12, 0))
156              time.sleep(0.15)
157  
158      keys[20].update()
159      if keys[20].rose:
160          print(mode_names[mode_choice], "mode picked.\n")
161          mode_picked = True
162  
163  scale = []  # create the base scale
164  for i in range(7):
165      scale.append(scale_root + intervals[i])
166  
167  midi_notes = []  # build the list with three octaves
168  for k in range(7):
169      midi_notes.append(scale[k]+24)
170  for l in range(7):
171      midi_notes.append(scale[l]+12)
172  for m in range(7):
173      midi_notes.append(scale[m])
174  
175  led.value = False
176  print("Ready, set, play!")
177  
178  
179  while True:
180  
181      for i in range(num_keys):
182          keys[i].update()
183          if keys[i].fell:
184              try:
185                  midi.send(NoteOn(midi_notes[i], 120))
186                  note_index = note_numbers.index(midi_notes[i])
187                  print("MIDI NoteOn:", note_names[note_index])
188              except ValueError:  # deals w six key limit
189                  pass
190  
191          if keys[i].rose:
192              try:
193                  midi.send(NoteOff(midi_notes[i], 0))
194                  note_index = note_numbers.index(midi_notes[i])
195                  print("MIDI NoteOff:", note_names[note_index])
196              except ValueError:
197                  pass
198  
199      #  Key combo for MIDI panic
200      # . o o o o o .
201      # o o o . o o o
202      # . o o o o o .
203  
204      if (not keys[0].value and
205              not keys[6].value
206              and not keys[10].value
207              and not keys[14].value
208              and not keys[20].value):
209          send_midi_panic()
210          time.sleep(1)