Skip to content

Commit 6802359

Browse files
authored
Merge pull request #2563 from adafruit/4x4_midi_project
Adding code for 4x4 encoder midi project
2 parents c3923f6 + 0095402 commit 6802359

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed

4x4_MIDI_Messager/OCRA_small.pcf

135 KB
Binary file not shown.

4x4_MIDI_Messager/code.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
import time
5+
import asyncio
6+
import board
7+
import digitalio
8+
from rainbowio import colorwheel
9+
import keypad
10+
import displayio
11+
import busio
12+
import adafruit_seesaw.seesaw
13+
import adafruit_seesaw.neopixel
14+
import adafruit_seesaw.rotaryio
15+
import adafruit_seesaw.digitalio
16+
from adafruit_bitmap_font import bitmap_font
17+
from adafruit_display_text import label
18+
import adafruit_displayio_ssd1306
19+
import adafruit_midi
20+
from adafruit_midi.control_change import ControlChange
21+
22+
# MIDI CC messages, values and names assigned to each encoder
23+
cc_values = [
24+
{'cc_val': (0, 127), 'cc_message': (14), 'cc_name': "Volume"},
25+
{'cc_val': (0, 127), 'cc_message': (15), 'cc_name': "Repeats"},
26+
{'cc_val': (0, 127), 'cc_message': (16), 'cc_name': "Size"},
27+
{'cc_val': (0, 127), 'cc_message': (17), 'cc_name': "Mod"},
28+
{'cc_val': (0, 127), 'cc_message': (18), 'cc_name': "Spread"},
29+
{'cc_val': (0, 127), 'cc_message': (19), 'cc_name': "Scan"},
30+
{'cc_val': (0, 127), 'cc_message': (20), 'cc_name': "Ramp"},
31+
{'cc_val': (1, 3), 'cc_message': (21), 'cc_name': "Mod Number"},
32+
{'cc_val': (1, 3), 'cc_message': (22), 'cc_name': "Mod Bank"},
33+
{'cc_val': (1, 3), 'cc_message': (23), 'cc_name': "Mode"},
34+
{'cc_val': (0, 1), 'cc_message': (102), 'cc_name': "Bypass/Engage"},
35+
{'cc_val': (60, 200), 'cc_message': (93), 'cc_name': "Tap Tempo"},
36+
{'cc_val': (0, 1), 'cc_message': (24), 'cc_name': "Loop (R Hold)"},
37+
{'cc_val': (0, 1), 'cc_message': (25), 'cc_name': "Scan (L Hold)"},
38+
{'cc_val': (0, 127), 'cc_message': (26), 'cc_name': "Clear (Both Hold)"},
39+
{'cc_val': (0, 1), 'cc_message': (51), 'cc_name': "MIDI Clock Ignore"}
40+
]
41+
42+
displayio.release_displays()
43+
44+
oled_reset = board.D13
45+
46+
i2c = board.STEMMA_I2C()
47+
# STEMMA OLED setup
48+
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset)
49+
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
50+
51+
splash = displayio.Group()
52+
display.show(splash)
53+
font = bitmap_font.load_font('/OCRA_small.pcf')
54+
# main label/MIDI message name text; centered
55+
main_area = label.Label(
56+
font, text="4x4 MIDI Messager", color=0xFFFFFF)
57+
main_area.anchor_point = (0.5, 0.0)
58+
main_area.anchored_position = (display.width / 2, 0)
59+
# MIDI message number text
60+
msg_area = label.Label(
61+
font, text="CC Msg: 10", color=0xFFFFFF)
62+
msg_area.anchor_point = (0.0, 0.5)
63+
msg_area.anchored_position = (0, display.height / 2)
64+
# MIDI message value text
65+
val_area = label.Label(
66+
font, text="CC Val: 50", color=0xFFFFFF)
67+
val_area.anchor_point = (0.0, 1.0)
68+
val_area.anchored_position = (0, display.height)
69+
# MIDI message status text
70+
status_area = label.Label(
71+
font, text="Sent!", color=0xFFFFFF)
72+
status_area.anchor_point = (1.0, 1.0)
73+
status_area.anchored_position = (display.width, display.height)
74+
75+
splash.append(main_area)
76+
splash.append(msg_area)
77+
splash.append(val_area)
78+
splash.append(status_area)
79+
# MIDI over UART setup for MIDI FeatherWing
80+
uart = busio.UART(board.TX, board.RX, baudrate=31250, timeout=0.001)
81+
midi_in_channel = 1
82+
midi_out_channel = 1
83+
midi = adafruit_midi.MIDI(
84+
midi_in=uart,
85+
midi_out=uart,
86+
in_channel=(midi_in_channel - 1),
87+
out_channel=(midi_out_channel - 1),
88+
debug=False,
89+
)
90+
# quad rotary encoder setup
91+
ss0 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x49)
92+
ss1 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4A)
93+
ss2 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4B)
94+
ss3 = adafruit_seesaw.seesaw.Seesaw(i2c, 0x4C)
95+
# button pins for the encoders
96+
pins = [12, 14, 17, 9]
97+
# interrupts for the button pins. pins are passed as a bitmask
98+
ss0.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
99+
ss1.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
100+
ss2.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
101+
ss3.set_GPIO_interrupts(1 << pins[0] | 1 << pins[1] | 1 << pins[2] | 1 << pins[3], True)
102+
# arrays for the encoders and switches
103+
enc0 = []
104+
enc1 = []
105+
enc2 = []
106+
enc3 = []
107+
sw0 = []
108+
sw1 = []
109+
sw2 = []
110+
sw3 = []
111+
# creating encoders and switches, enabling interrupts for encoders
112+
for i in range(4):
113+
enc0.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss0, i))
114+
enc1.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss1, i))
115+
enc2.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss2, i))
116+
enc3.append(adafruit_seesaw.rotaryio.IncrementalEncoder(ss3, i))
117+
sw0.append(adafruit_seesaw.digitalio.DigitalIO(ss0, pins[i]))
118+
sw0[i].switch_to_input(digitalio.Pull.UP)
119+
sw1.append(adafruit_seesaw.digitalio.DigitalIO(ss1, pins[i]))
120+
sw1[i].switch_to_input(digitalio.Pull.UP)
121+
sw2.append(adafruit_seesaw.digitalio.DigitalIO(ss2, pins[i]))
122+
sw2[i].switch_to_input(digitalio.Pull.UP)
123+
sw3.append(adafruit_seesaw.digitalio.DigitalIO(ss3, pins[i]))
124+
sw3[i].switch_to_input(digitalio.Pull.UP)
125+
ss0.enable_encoder_interrupt(encoder=i)
126+
ss1.enable_encoder_interrupt(encoder=i)
127+
ss2.enable_encoder_interrupt(encoder=i)
128+
ss3.enable_encoder_interrupt(encoder=i)
129+
# neopixels on each PCB
130+
pix0 = adafruit_seesaw.neopixel.NeoPixel(ss0, 18, 4, auto_write = True)
131+
pix0.brightness = 0.5
132+
pix1 = adafruit_seesaw.neopixel.NeoPixel(ss1, 18, 4, auto_write = True)
133+
pix1.brightness = 0.5
134+
pix2 = adafruit_seesaw.neopixel.NeoPixel(ss2, 18, 4, auto_write = True)
135+
pix2.brightness = 0.5
136+
pix3 = adafruit_seesaw.neopixel.NeoPixel(ss3, 18, 4, auto_write = True)
137+
pix3.brightness = 0.5
138+
# encoder position arrays
139+
last_pos0 = [60, 60, 60, 60]
140+
last_pos1 = [60, 60, 60, 0]
141+
last_pos2 = [0, 0, 0, 120]
142+
last_pos3 = [0, 0, 0, 0]
143+
pos0 = [60, 60, 60, 60]
144+
pos1 = [60, 60, 60, 0]
145+
pos2 = [0, 0, 0, 120]
146+
pos3 = [0, 0, 0, 0]
147+
# color arrays for the neopixels
148+
c0 = [0, 16, 32, 48]
149+
c1 = [64, 80, 96, 112]
150+
c2 = [128, 144, 160, 176]
151+
c3 = [192, 208, 224, 240]
152+
# setting starting colors for neopixels
153+
for r in range(4):
154+
pix0[r] = colorwheel(c0[r])
155+
pix1[r] = colorwheel(c1[r])
156+
pix2[r] = colorwheel(c2[r])
157+
pix3[r] = colorwheel(c3[r])
158+
# array of all 16 encoder positions
159+
encoder_posititions = [60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 120, 0, 0, 0, 0]
160+
161+
class MIDI_Messages:
162+
# tracks sending a message and index 0-15
163+
def __init__(self):
164+
self.send_msg = False
165+
self.midi_index = 0
166+
167+
class NeoPixel_Attributes:
168+
# tracks color, neopixel index and seesaw
169+
def __init__(self):
170+
self.color = c0
171+
self.index = 0
172+
self.strip = pix0
173+
174+
async def send_midi(midi_msg):
175+
# sends MIDI message if send_msg is True/button pressed
176+
while True:
177+
if midi_msg.send_msg is True:
178+
m = midi_msg.midi_index
179+
main_area.text = f"{cc_values[m]['cc_name']}"
180+
msg_area.text = f"CC Msg: {cc_values[m]['cc_message']}"
181+
val_area.text = f"CC Val: {encoder_posititions[m]}"
182+
midi.send(ControlChange(cc_values[m]['cc_message'], encoder_posititions[m]))
183+
status_area.text = "Sent!"
184+
print(f"sending midi: {m}, {encoder_posititions[m]}, {cc_values[m]['cc_message']}")
185+
time.sleep(1)
186+
midi_msg.send_msg = False
187+
else:
188+
status_area.text = " "
189+
await asyncio.sleep(0)
190+
191+
async def rainbows(the_color):
192+
# Updates colors of the neopixels to scroll through rainbow
193+
while True:
194+
the_color.strip[the_color.index] = colorwheel(the_color.color[the_color.index])
195+
await asyncio.sleep(0)
196+
197+
async def monitor_interrupts(pin0, pin1, pin2, pin3, the_color, midi_msg): #pylint: disable=too-many-statements
198+
# function to keep encoder value pinned between CC value range
199+
def normalize(val, min_v, max_v):
200+
return max(min(max_v, val), min_v)
201+
# read encoder function
202+
def read_encoder(enc_group, pos, last_pos, pix, colors, index_diff):
203+
# check all four encoders if interrupt is detected
204+
for p in range(4):
205+
pos[p] = enc_group[p].position
206+
if pos[p] != last_pos[p]:
207+
main_index = p + index_diff
208+
# update CC value
209+
if pos[p] > last_pos[p]:
210+
colors[p] += 8
211+
encoder_posititions[main_index] = encoder_posititions[main_index] + 1
212+
else:
213+
colors[p] -= 8
214+
encoder_posititions[main_index] = encoder_posititions[main_index] - 1
215+
encoder_posititions[main_index] = normalize(encoder_posititions[main_index],
216+
cc_values[main_index]['cc_val'][0],
217+
cc_values[main_index]['cc_val'][1])
218+
colors[p] = (colors[p] + 256) % 256 # wrap around to 0-256
219+
print(main_index, encoder_posititions[main_index])
220+
main_area.text = f"{cc_values[main_index]['cc_name']}"
221+
msg_area.text = f"CC Msg: {cc_values[main_index]['cc_message']}"
222+
val_area.text = f"CC Val: {encoder_posititions[main_index]}"
223+
last_pos[p] = pos[p]
224+
# update NeoPixel colors
225+
the_color.color = colors
226+
the_color.index = p
227+
the_color.strip = pix
228+
# function to read button press
229+
def press_switches(sw, index):
230+
if not sw[index].value:
231+
# signals that a MIDI message should be sent
232+
midi_msg.send_msg = True
233+
midi_msg.midi_index = index
234+
print(f"button {index} pressed")
235+
# interrupt pins are passed as a keypad
236+
with keypad.Keys(
237+
(pin0, pin1, pin2, pin3,), value_when_pressed=False, pull=True
238+
) as keys:
239+
while True:
240+
key_event = keys.events.get()
241+
if key_event and key_event.pressed:
242+
key_number = key_event.key_number
243+
# seesaw 0
244+
if key_number == 0:
245+
read_encoder(enc0, pos0, last_pos0, pix0, c0, 0)
246+
press_switches(sw0, 0)
247+
press_switches(sw0, 1)
248+
press_switches(sw0, 2)
249+
press_switches(sw0, 3)
250+
# seesaw 1
251+
elif key_number == 1:
252+
read_encoder(enc1, pos1, last_pos1, pix1, c1, 4)
253+
press_switches(sw1, 0)
254+
press_switches(sw1, 1)
255+
press_switches(sw1, 2)
256+
press_switches(sw1, 3)
257+
# update index to 4-7
258+
midi_msg.midi_index = midi_msg.midi_index + 4
259+
# seesaw 2
260+
elif key_number == 2:
261+
read_encoder(enc2, pos2, last_pos2, pix2, c2, 8)
262+
press_switches(sw2, 0)
263+
press_switches(sw2, 1)
264+
press_switches(sw2, 2)
265+
press_switches(sw2, 3)
266+
# update index 8-11
267+
midi_msg.midi_index = midi_msg.midi_index + 8
268+
# seesaw 3
269+
else:
270+
read_encoder(enc3, pos3, last_pos3, pix3, c3, 12)
271+
press_switches(sw3, 0)
272+
press_switches(sw3, 1)
273+
press_switches(sw3, 2)
274+
press_switches(sw3, 3)
275+
# update index 12-15
276+
midi_msg.midi_index = midi_msg.midi_index + 12
277+
# clear interrupt flag to reset interrupt pin
278+
ss0.get_GPIO_interrupt_flag()
279+
ss1.get_GPIO_interrupt_flag()
280+
ss2.get_GPIO_interrupt_flag()
281+
ss3.get_GPIO_interrupt_flag()
282+
await asyncio.sleep(0)
283+
284+
async def main():
285+
the_color = NeoPixel_Attributes()
286+
midi_msg = MIDI_Messages()
287+
# interrupt listener task
288+
interrupt_task = asyncio.create_task(monitor_interrupts(board.D5, board.D6, board.D9,
289+
board.D10, the_color, midi_msg))
290+
# neopixel task
291+
pixels_task = asyncio.create_task(rainbows(the_color))
292+
# midi task
293+
midi_task = asyncio.create_task(send_midi(midi_msg))
294+
295+
await asyncio.gather(interrupt_task, pixels_task, midi_task)
296+
297+
asyncio.run(main())

0 commit comments

Comments
 (0)