Skip to content

Commit 366b0da

Browse files
authored
Create cpx-expressive-midi-controller.py
1 parent 7e31f60 commit 366b0da

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)