|
| 1 | +### cpx-expressive-midi-controller v1.2 |
| 2 | +### CircuitPython (on CPX) MIDI controller using the seven touch pads |
| 3 | +### and accelerometer for modulation (cc1) and pitch bend |
| 4 | +### Left button adjusts octave (switch left) or semitone (switch right) |
| 5 | +### Right button adjusts scale, major or chromatic |
| 6 | +### Switch right also disables pitch bend and modulation |
| 7 | + |
| 8 | +### Tested with CPX and CircuitPython and 4.0.0-beta.5 |
| 9 | + |
| 10 | +### Needs recent adafruit_midi module |
| 11 | + |
| 12 | +### copy this file to CPX as code.py |
| 13 | + |
| 14 | +### MIT License |
| 15 | + |
| 16 | +### Copyright (c) 2019 Kevin J. Walters |
| 17 | + |
| 18 | +### Permission is hereby granted, free of charge, to any person obtaining a copy |
| 19 | +### of this software and associated documentation files (the "Software"), to deal |
| 20 | +### in the Software without restriction, including without limitation the rights |
| 21 | +### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 22 | +### copies of the Software, and to permit persons to whom the Software is |
| 23 | +### furnished to do so, subject to the following conditions: |
| 24 | + |
| 25 | +### The above copyright notice and this permission notice shall be included in all |
| 26 | +### copies or substantial portions of the Software. |
| 27 | + |
| 28 | +### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 29 | +### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 30 | +### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 31 | +### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 32 | +### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 33 | +### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 34 | +### SOFTWARE. |
| 35 | + |
| 36 | +import time |
| 37 | + |
| 38 | +import digitalio |
| 39 | +import touchio |
| 40 | +import busio |
| 41 | +import board |
| 42 | +import usb_midi |
| 43 | +import neopixel |
| 44 | + |
| 45 | +import adafruit_lis3dh |
| 46 | +import adafruit_midi |
| 47 | + |
| 48 | +from adafruit_midi.note_on import NoteOn |
| 49 | +from adafruit_midi.control_change import ControlChange |
| 50 | +from adafruit_midi.pitch_bend import PitchBend |
| 51 | + |
| 52 | +# MIDI defines middle C as 60 and modulation wheel is cc 1 by convention |
| 53 | +midi_note_C4 = 60 |
| 54 | +midi_cc_modwheel = const(1) |
| 55 | + |
| 56 | +# 0x19 is the i2c address of the onboard accelerometer |
| 57 | +acc_i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA) |
| 58 | +acc_int1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT) |
| 59 | +acc = adafruit_lis3dh.LIS3DH_I2C(acc_i2c, address=0x19, int1=acc_int1) |
| 60 | +acc.range = adafruit_lis3dh.RANGE_2_G |
| 61 | +acc.data_rate = adafruit_lis3dh.DATARATE_10_HZ |
| 62 | + |
| 63 | +# brightness 1.0 saves memory by removing need for a second buffer |
| 64 | +# 10 is number of NeoPixels on CPX |
| 65 | +numpixels = const(10) |
| 66 | +pixels = neopixel.NeoPixel(board.NEOPIXEL, numpixels, brightness=1.0) |
| 67 | + |
| 68 | +# Turn NeoPixel on to represent a note using RGB x 10 |
| 69 | +# to represent 30 notes - doesn't do anything with pitch bend |
| 70 | +def noteLED(pix, pnote, pvel): |
| 71 | + note30 = (pnote - midi_note_C4) % (3 * numpixels) |
| 72 | + pos = note30 % numpixels |
| 73 | + r, g, b = pix[pos] |
| 74 | + if pvel == 0: |
| 75 | + brightness = 0 |
| 76 | + else: |
| 77 | + # max brightness will be 32 |
| 78 | + brightness = round(pvel / 127 * 30 + 2) |
| 79 | + # Pick R/G/B based on range within the 30 notes |
| 80 | + if note30 < 10: |
| 81 | + r = brightness |
| 82 | + elif note30 < 20: |
| 83 | + g = brightness |
| 84 | + else: |
| 85 | + b = brightness |
| 86 | + pix[pos] = (r, g, b) |
| 87 | + |
| 88 | +# white pulse used to indicate octave changes |
| 89 | +flashbrightness = 20 |
| 90 | +def flashLED(pix, position): |
| 91 | + pos = position % numpixels |
| 92 | + t1 = time.monotonic() |
| 93 | + oldcolour = pix[pos] |
| 94 | + while time.monotonic() - t1 < 0.25: |
| 95 | + for i in range(0, flashbrightness, 2): |
| 96 | + pix[pos] = (i, i, i) |
| 97 | + for i in range(flashbrightness, 0, -2): |
| 98 | + pix[pos] = (i, i, i) |
| 99 | + pix[pos] = oldcolour |
| 100 | + |
| 101 | +midi_channel = 1 |
| 102 | +midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], |
| 103 | + out_channel=midi_channel-1) |
| 104 | + |
| 105 | +# CPX counter-clockwise order of touch capable pads (i.e. not A0) |
| 106 | +pads = [board.A4, |
| 107 | + board.A5, |
| 108 | + board.A6, |
| 109 | + board.A7, |
| 110 | + board.A1, |
| 111 | + board.A2, |
| 112 | + board.A3] |
| 113 | + |
| 114 | +# The touch pads calibrate themselves as they are created, just once here |
| 115 | +touchpads = [touchio.TouchIn(pad) for pad in pads] |
| 116 | +del pads # done with that |
| 117 | + |
| 118 | +pb_midpoint = 8192 |
| 119 | +pitch_bend_value = pb_midpoint # mid point - no bend |
| 120 | +min_pb_change = 250 |
| 121 | + |
| 122 | +mod_wheel = 0 |
| 123 | +min_mod_change = 5 |
| 124 | + |
| 125 | +# button A is on left (usb at top) |
| 126 | +button_left = digitalio.DigitalInOut(board.BUTTON_A) |
| 127 | +button_left.switch_to_input(pull=digitalio.Pull.DOWN) |
| 128 | +button_right = digitalio.DigitalInOut(board.BUTTON_B) |
| 129 | +button_right.switch_to_input(pull=digitalio.Pull.DOWN) |
| 130 | +switch_left = digitalio.DigitalInOut(board.SLIDE_SWITCH) |
| 131 | +switch_left.switch_to_input(pull=digitalio.Pull.UP) |
| 132 | + |
| 133 | +# some example scales in semitones |
| 134 | +scale_st = {"major": [0, 2, 4, 5, 7, 9, 11], |
| 135 | + "chromatic": [0, 1, 2, 3, 4, 5, 6]} |
| 136 | +scales = ["major", "chromatic"] |
| 137 | +scale_idx = 0 |
| 138 | +base_note = midi_note_C4 # C4 middle C |
| 139 | + |
| 140 | +def make_scale(scale_name): |
| 141 | + return [semitone_offset + base_note |
| 142 | + for semitone_offset in scale_st[scale_name]] |
| 143 | + |
| 144 | +midi_notes = make_scale(scales[scale_idx]) |
| 145 | +keydown = [False] * 7 |
| 146 | + |
| 147 | +velocity = 127 |
| 148 | +min_octave = -3 |
| 149 | +max_octave = +3 |
| 150 | +octave = 0 |
| 151 | +min_semitone = -11 |
| 152 | +max_semitone = +11 |
| 153 | +semitone = 0 |
| 154 | + |
| 155 | +# 1/10 = 10 Hz - review data_rate setting if this is changed |
| 156 | +acc_read_t = time.monotonic() |
| 157 | +acc_read_period = 1/10 |
| 158 | +# For accelerometer do nothing between 0 and 1.3 (ms-2) |
| 159 | +acc_nullzone = 1.3 |
| 160 | +acc_range = 4.0 |
| 161 | + |
| 162 | +# Convert an accelerometer reading |
| 163 | +# from min_msm2 to min_msm2+range to an int from 0 to value_range |
| 164 | +# or return 0 or value_range outside those values |
| 165 | +# The conversion is applied "symmetrically" to negative numbers |
| 166 | +def scale_acc(acc_msm2, min_msm2, range_msm2, value_range): |
| 167 | + if acc_msm2 >= 0.0: |
| 168 | + sign_a_m = 1 |
| 169 | + magn_acc_msm2 = acc_msm2 |
| 170 | + else: |
| 171 | + sign_a_m = -1 |
| 172 | + magn_acc_msm2 = abs(acc_msm2) |
| 173 | + |
| 174 | + adj_msm2 = magn_acc_msm2 - min_msm2 |
| 175 | + |
| 176 | + # deal with out of bounds values else scale value |
| 177 | + # pylint: disable=no-else-return |
| 178 | + if adj_msm2 <= 0: |
| 179 | + return 0 |
| 180 | + elif adj_msm2 >= range_msm2: |
| 181 | + return sign_a_m * value_range |
| 182 | + else: |
| 183 | + return sign_a_m * round(adj_msm2 / range_msm2 * value_range) |
| 184 | + |
| 185 | +# Scan each pad and look for changes by comparing |
| 186 | +# with keystate stored in keydown boolean list |
| 187 | +# and send note on/off messages accordingly |
| 188 | +# Send pitch bend and mod wheel cc based on tilt from accelerometer |
| 189 | +# Change octave and semitone based on buttons |
| 190 | +while True: |
| 191 | + for idx, touchpad in enumerate(touchpads): |
| 192 | + if touchpad.value != keydown[idx]: |
| 193 | + keydown[idx] = touchpad.value |
| 194 | + # 12 semitones in an octave |
| 195 | + note = midi_notes[idx] + octave * 12 + semitone |
| 196 | + if keydown[idx]: |
| 197 | + midi.send(NoteOn(note, velocity)) |
| 198 | + noteLED(pixels, note, velocity) |
| 199 | + else: |
| 200 | + midi.send(NoteOn(note, 0)) # Using note on 0 for off |
| 201 | + noteLED(pixels, note, 0) |
| 202 | + |
| 203 | + # Perform rate limited checks on the accelerometer |
| 204 | + # if switch is to left |
| 205 | + now_t = time.monotonic() |
| 206 | + if switch_left.value and now_t - acc_read_t > acc_read_period: |
| 207 | + acc_read_t = time.monotonic() |
| 208 | + ax, ay, az = acc.acceleration |
| 209 | + |
| 210 | + # scale from 0 to 127 (maximum cc 7bit value) |
| 211 | + new_mod_wheel = abs(scale_acc(ay, acc_nullzone, acc_range, 127)) |
| 212 | + if (abs(new_mod_wheel - mod_wheel) > min_mod_change |
| 213 | + or (new_mod_wheel == 0 and mod_wheel != 0)): |
| 214 | + midi.send(ControlChange(midi_cc_modwheel, new_mod_wheel)) |
| 215 | + mod_wheel = new_mod_wheel |
| 216 | + |
| 217 | + # scale from 0 to +/- 8191 (almost maximum signed 14bit values) |
| 218 | + new_pitch_bend_value = (pb_midpoint |
| 219 | + - scale_acc(ax, acc_nullzone, acc_range, |
| 220 | + pb_midpoint - 1)) |
| 221 | + if (abs(new_pitch_bend_value - pitch_bend_value) > min_pb_change |
| 222 | + or (new_pitch_bend_value == pb_midpoint |
| 223 | + and pitch_bend_value != pb_midpoint)): |
| 224 | + midi.send(PitchBend(new_pitch_bend_value)) |
| 225 | + pitch_bend_value = new_pitch_bend_value |
| 226 | + |
| 227 | + # left button increase octave / semitones shift based on switch |
| 228 | + # does not currently clear playing notes (buglet) |
| 229 | + if button_left.value: |
| 230 | + if switch_left.value: |
| 231 | + octave += 1 |
| 232 | + if octave > max_octave: |
| 233 | + octave = min_octave |
| 234 | + flashLED(pixels, octave) |
| 235 | + else: |
| 236 | + semitone += 1 |
| 237 | + if semitone > max_semitone: |
| 238 | + semitone = min_semitone |
| 239 | + # semitone range is more than number of pixels! |
| 240 | + flashLED(pixels, semitone) |
| 241 | + |
| 242 | + while button_left.value: |
| 243 | + pass # wait for button up |
| 244 | + |
| 245 | + # right button cycles through scales |
| 246 | + if button_right.value: |
| 247 | + scale_idx += 1 |
| 248 | + if scale_idx >= len(scales): |
| 249 | + scale_idx = 0 |
| 250 | + flashLED(pixels, scale_idx) |
| 251 | + midi_notes = make_scale(scales[scale_idx]) |
| 252 | + |
| 253 | + while button_right.value: |
| 254 | + pass # wait for button up |
0 commit comments