code.py
  1  # SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries
  2  # SPDX-License-Identifier: MIT
  3  
  4  #  Grand Central MIDI Knobs
  5  #  for USB MIDI
  6  #  Reads analog inputs, sends out MIDI CC values
  7  #   with Kattni Rembor and Jan Goolsbey for range and hysteresis code
  8  
  9  import time
 10  import board
 11  import busio
 12  from simpleio import map_range
 13  from analogio import AnalogIn
 14  from digitalio import DigitalInOut, Direction
 15  import usb_midi
 16  import adafruit_midi  # MIDI protocol encoder/decoder library
 17  from adafruit_midi.control_change import ControlChange
 18  
 19  
 20  USB_MIDI_channel = 1  # pick your USB MIDI out channel here, 1-16
 21  # pick your classic MIDI channel for sending over UART serial TX/RX
 22  CLASSIC_MIDI_channel = 2
 23  
 24  usb_midi = adafruit_midi.MIDI(
 25      midi_out=usb_midi.ports[1], out_channel=USB_MIDI_channel - 1
 26  )
 27  #  use DIN-5 or TRS MIDI jack on TX/RX for classic MIDI
 28  uart = busio.UART(board.TX, board.RX, baudrate=31250, timeout=0.001)  # initialize UART
 29  classic_midi = adafruit_midi.MIDI(
 30      midi_out=uart, midi_in=uart, out_channel=CLASSIC_MIDI_channel - 1, debug=False
 31  )
 32  
 33  led = DigitalInOut(board.D13)  # activity indicator
 34  led.direction = Direction.OUTPUT
 35  
 36  knob_count = 16  # Set the total number of potentiometers used
 37  
 38  # Create the input objects list for potentiometers
 39  knob = []
 40  for k in range(knob_count):
 41      knobs = AnalogIn(
 42          getattr(board, "A{}".format(k))
 43      )  # get pin # attribute, use string formatting
 44      knob.append(knobs)
 45  
 46  #  assignment of knobs to cc numbers
 47  cc_number = [
 48      1,  # knob 0, mod wheel
 49      2,  # knob 1, breath control
 50      7,  # knob 2, volume
 51      10,  # knob 3 pan
 52      11,  # knob 4, expression
 53      53,  # knob 5
 54      54,  # knob 6
 55      74,  # knob 7
 56      74,  # knob 8, Filter frequency cutoff
 57      71,  # knob 9, Filter resonance
 58      58,  # knob 10
 59      59,  # knob 11
 60      60,  # knob 12
 61      61,  # knob 13
 62      62,  # knob 14
 63      63,  # knob 15
 64  ]
 65  
 66  # CC range list defines the characteristics of the potentiometers
 67  #  This list contains the input object, minimum value, and maximum value for each knob.
 68  #   example ranges:
 69  #   0 min, 127 max: full range control voltage
 70  #   36 (C2) min, 84 (B5) max: 49-note keyboard
 71  #   21 (A0) min, 108 (C8) max: 88-note grand piano
 72  cc_range = [
 73      (36, 84),  # knob 0: C2 to B5: 49-note keyboard
 74      (36, 84),  # knob 1
 75      (36, 84),  # knob 2
 76      (36, 84),  # knob 3
 77      (36, 84),  # knob 4
 78      (36, 84),  # knob 5
 79      (36, 84),  # knob 6
 80      (36, 84),  # knob 7
 81      (0, 127),  # knob 8: 0 to 127: full range MIDI CC/control voltage for VCV Rack
 82      (0, 127),  # knob 9
 83      (0, 127),  # knob 10
 84      (0, 127),  # knob 11
 85      (0, 127),  # knob 12
 86      (0, 127),  # knob 13
 87      (0, 127),  # knob 14
 88      (0, 127),  # knob 15
 89  ]
 90  
 91  print("---Grand Central MIDI Knobs---")
 92  print("   USB MIDI channel: {}".format(USB_MIDI_channel))
 93  print("   TRS MIDI channel: {}".format(CLASSIC_MIDI_channel))
 94  
 95  # Initialize cc_value list with current value and offset placeholders
 96  cc_value = []
 97  for _ in range(knob_count):
 98      cc_value.append((0, 0))
 99  last_cc_value = []
100  for _ in range(knob_count):
101      last_cc_value.append((0, 0))
102  
103  #  range_index converts an analog value (ctl) to an indexed integer
104  #  Input is masked to 8 bits to reduce noise then a scaled hysteresis offset
105  #  is applied. The helper returns new index value (idx) and input
106  #  hysteresis offset (offset) based on the number of control slices (ctrl_max).
107  def range_index(ctl, ctrl_max, old_idx, offset):
108      if (ctl + offset > 65535) or (ctl + offset < 0):
109          offset = 0
110      idx = int(map_range((ctl + offset) & 0xFF00, 1200, 65500, 0, ctrl_max))
111      if idx != old_idx:  # if index changed, adjust hysteresis offset
112          # offset is 25% of the control slice (65536/ctrl_max)
113          offset = int(
114              0.25 * sign(idx - old_idx) * (65535 / ctrl_max)
115          )  # edit 0.25 to adjust slices
116      return idx, offset
117  
118  
119  def sign(x):  # determine the sign of x
120      if x >= 0:
121          return 1
122      else:
123          return -1
124  
125  
126  while True:
127      # read all the knob values
128      for i in range(knob_count):
129          cc_value[i] = range_index(
130              knob[i].value,
131              (cc_range[i][1] - cc_range[i][0] + 1),
132              cc_value[i][0],
133              cc_value[i][1],
134          )
135          if cc_value[i] != last_cc_value[i]:  # only send if it changed
136              # Form a MIDI CC message and send it:
137              usb_midi.send(ControlChange(cc_number[i], cc_value[i][0] + cc_range[i][0]))
138              classic_midi.send(
139                  ControlChange(cc_number[i], cc_value[i][0] + cc_range[i][0])
140              )
141              last_cc_value[i] = cc_value[i]
142              led.value = True
143  
144      time.sleep(0.01)
145      led.value = False