Skip to content

Commit 5576e51

Browse files
committed
first commit neotrellis 8x8 MIDI feedback controller code
1 parent bc70338 commit 5576e51

File tree

1 file changed

+154
-0
lines changed
  • NeoTrellis_MIDI_Feedback_Controller

1 file changed

+154
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# SPDX-FileCopyrightText: 2022 John Park & @todbot Tod Kurt for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
# NeoTrellis 8x8 MIDI Trigger Sequencer
4+
# Does bi-directional MIDI with VCV Rack + Stoermelder MIDI-CAT module
5+
# (can also work with with other apps, such as DAWs that send MIDI for LED controll)
6+
# This is a simplified version of https://github.com/PatchworkBoy/TrowasoftControl
7+
8+
# Requirements:
9+
# -VCV Rack 2, w/ MIDI-CAT
10+
# -sequencer such as Trowa TrigSeq or Count Modula
11+
# -MIDI mappings on pads on "momentary"
12+
13+
import time
14+
import board
15+
import busio
16+
from adafruit_neotrellis.neotrellis import NeoTrellis
17+
from adafruit_neotrellis.multitrellis import MultiTrellis
18+
import usb_midi
19+
import adafruit_midi
20+
from adafruit_midi.note_on import NoteOn
21+
from adafruit_midi.note_off import NoteOff
22+
23+
note_base = 36
24+
note_vel = 127
25+
pad_midi_channel = 0 # add one for "real world" MIDI channel number, e.g. 0=1
26+
led_midi_channel = 2 # see ^
27+
pad_ager_secs = 0.1 # how many seconds to wait for MIDI-CAT to fail
28+
pad_ager_secs_max = 1.0 # seconds max to wait for MIDI-CAT before giving up
29+
30+
# holds whether pad is currently lit or not, when it was pressed before being ack'd
31+
# tuple of (pad lit?, pad_press_time_or_zero_if_ackd)
32+
pad_states = [(False,0)] * 64
33+
34+
midi_usb = adafruit_midi.MIDI( midi_in=usb_midi.ports[0],
35+
midi_out=usb_midi.ports[1] )
36+
37+
i2c = busio.I2C(board.SCL, board.SDA)
38+
trelli = [ # adjust these to match your jumper settings if needed
39+
[NeoTrellis(i2c, False, addr=0x2E), NeoTrellis(i2c, False, addr=0x30)],
40+
[NeoTrellis(i2c, False, addr=0x32), NeoTrellis(i2c, False, addr=0x36)]
41+
]
42+
trellis = MultiTrellis(trelli)
43+
44+
OFF = 0x000000
45+
RED = 0x100000
46+
YELLOW = 0x100c00
47+
GREEN = 0x000c00
48+
CYAN = 0x000303
49+
BLUE = 0x000010
50+
PURPLE = 0x130010
51+
52+
colors = [OFF, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE]
53+
54+
color_table = [ # you can make custom color sections for clarity
55+
1, 1, 1, 1, 5, 5, 5, 5,
56+
1, 1, 1, 1, 5, 5, 5, 5,
57+
1, 1, 1, 1, 5, 5, 5, 5,
58+
1, 1, 1, 1, 5, 5, 5, 5,
59+
4, 4, 4, 4, 6, 6, 6, 6,
60+
4, 4, 4, 4, 6, 6, 6, 6,
61+
4, 4, 4, 4, 6, 6, 6, 6,
62+
4, 4, 4, 4, 6, 6, 6, 6
63+
]
64+
65+
# convert an x,y (0-7,0-7) to 0-63
66+
def xy_to_pos(x,y):
67+
return x+(y*8)
68+
69+
# convert 0-63 to x,y
70+
def pos_to_xy(pos):
71+
return (pos%8, pos//8)
72+
73+
# callback when pads are pressed
74+
def handle_pad(x, y, edge):
75+
pos = xy_to_pos(x,y)
76+
note_val = pos + note_base
77+
if edge == NeoTrellis.EDGE_RISING:
78+
(pad_on, pad_time) = pad_states[pos] # get pad state & press time
79+
print(pad_time)
80+
pad_on = not pad_on # toggle state
81+
pad_states[pos] = (pad_on, time.monotonic()) # and save it w/ new press time
82+
print("handle_pad: x,y,pos",x,y, pos, note_val, pad_on)
83+
if pad_on:
84+
noteon = NoteOn(note_val, note_vel, channel=pad_midi_channel)
85+
midi_usb.send(noteon)
86+
else:
87+
noteoff = NoteOff(note_val, note_vel, channel=pad_midi_channel)
88+
midi_usb.send(noteoff)
89+
90+
# called periodically in main loop to receive MIDI msgs
91+
# saves to pad_state with LED on/off and 0 to indicate pad press acknowledgement
92+
def midi_receive():
93+
msg_in = midi_usb.receive()
94+
if msg_in is None:
95+
return
96+
#print("msg_in:", msg_in.channel, msg_in.note, msg_in.velocity)
97+
if msg_in.channel == led_midi_channel:
98+
pos = msg_in.note - note_base
99+
x,y = pos_to_xy(pos)
100+
if isinstance(msg_in, NoteOn):
101+
print("midi_receive: LED ON ",pos, x,y)
102+
pad_states[pos] = (True, 0) # save pad state with press time 0 = acknowledgdd
103+
trellis.color(x,y, colors[color_table[pos]])
104+
if isinstance(msg_in, NoteOff):
105+
print("midi_receive: LED OFF ",pos, x,y)
106+
pad_states[pos] = (False, 0) # save pad state with press time 0 = acknowledged
107+
trellis.color(x,y, 0x000000)
108+
109+
# attempts to fix the MIDI-CAT bug by retoggling a pad if no LED msg sent
110+
def send_pad_toggle(pad_on,pos):
111+
print("send_pad_toggle",pad_on,pos)
112+
note_val = pos + note_base
113+
noteon = NoteOn(note_val, note_vel, channel=pad_midi_channel)
114+
noteoff = NoteOff(note_val, note_vel, channel=pad_midi_channel)
115+
if pad_on:
116+
midi_usb.send(noteoff)
117+
midi_usb.send(noteon)
118+
else:
119+
midi_usb.send(noteon)
120+
midi_usb.send(noteoff)
121+
122+
# scan list of pads, if any haven't been acknowledged with LED msgs, toggle them
123+
def pad_ager():
124+
for i in range(len(pad_states)):
125+
(pad_on, pad_time) = pad_states[i]
126+
if (pad_time > 0 and
127+
time.monotonic() - pad_time > pad_ager_secs and
128+
time.monotonic() - pad_time < pad_ager_secs_max):
129+
send_pad_toggle(pad_on,i)
130+
131+
# set up actions for each pad and do a startup light show
132+
for y_pad in range(8):
133+
for x_pad in range(8):
134+
trellis.activate_key(x_pad, y_pad, NeoTrellis.EDGE_RISING)
135+
trellis.activate_key(x_pad, y_pad, NeoTrellis.EDGE_FALLING)
136+
trellis.set_callback(x_pad, y_pad, handle_pad)
137+
trellis.color( x_pad, y_pad, PURPLE)
138+
time.sleep(0.005)
139+
140+
for y_pad in range(8): # finish light show
141+
for x_pad in range(8):
142+
trellis.color(x_pad, y_pad, OFF)
143+
time.sleep(0.005)
144+
145+
print("Ready. Be sure VCV Rack2 w/ MIDI-CAT is running and configured")
146+
last_debug_time = 0
147+
while True:
148+
trellis.sync()
149+
midi_receive()
150+
pad_ager()
151+
152+
if time.monotonic() - last_debug_time > 5.0:
153+
last_debug_time = time.monotonic()
154+
# print("pads:", ''.join(['1' if i else '0' for (i,t) in pad_states]))

0 commit comments

Comments
 (0)