Skip to content

Commit 6581475

Browse files
committed
Adding code for midi neopixel visualizer
code for the midi neopixel visualizer
1 parent d2ad31d commit 6581475

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed

MIDI_NeoPixel_Visualizer/code.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# SPDX-FileCopyrightText: 2026 Noe Ruiz for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
import time
5+
import board
6+
import usb_midi
7+
import adafruit_midi
8+
from digitalio import DigitalInOut, Direction, Pull
9+
from adafruit_midi.control_change import ControlChange
10+
from adafruit_midi.note_off import NoteOff
11+
from adafruit_midi.note_on import NoteOn
12+
from adafruit_midi.pitch_bend import PitchBend
13+
import neopixel
14+
15+
# enable external power pin
16+
# provides power to the external components
17+
external_power = DigitalInOut(board.EXTERNAL_POWER)
18+
external_power.direction = Direction.OUTPUT
19+
external_power.value = True
20+
21+
# NeoPixel LED Setup
22+
NUMPIXELS = 72
23+
BRIGHTNESS = 1
24+
PIN = board.EXTERNAL_NEOPIXELS
25+
ORDER = neopixel.BGR
26+
pixels = neopixel.NeoPixel(PIN, NUMPIXELS, brightness=BRIGHTNESS, auto_write=False, pixel_order=ORDER)
27+
28+
# Matrix layout
29+
MATRIX_ROWS = 6
30+
MATRIX_COLS = 12 # One full chromatic octave per row
31+
32+
# MIDI setup - listen on channels 1 and 2 (0-indexed: 0 and 1)
33+
print(usb_midi.ports)
34+
midi = adafruit_midi.MIDI(
35+
midi_in=usb_midi.ports[0], in_channel=(0, 1), midi_out=usb_midi.ports[1], out_channel=0
36+
)
37+
38+
# MIDI note number that maps to Pixel 0 (top-left of matrix).
39+
# 24 = C1, 36 = C2, 48 = C3
40+
NOTE_OFFSET = 24
41+
42+
# Chromatic note names used for print statements
43+
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
44+
45+
# Base color palette - 24 colors spanning two octaves of the chromatic scale.
46+
# The first 12 colors cover one octave, the next 12 cover the second octave
47+
# with a distinct shifted palette. Colors then tile across all 6 rows.
48+
BASE_COLORS = [
49+
# Octave A - Warm to Cool spectrum
50+
(255, 0, 0), # C - Red
51+
(255, 45, 0), # C# - Red-Orange
52+
(255, 90, 0), # D - Orange
53+
(255, 145, 0), # D# - Amber
54+
(255, 200, 0), # E - Yellow-Orange
55+
(255, 255, 0), # F - Yellow
56+
(128, 255, 0), # F# - Yellow-Green
57+
(0, 255, 0), # G - Green
58+
(0, 255, 128), # G# - Spring Green
59+
(0, 255, 255), # A - Cyan
60+
(0, 128, 255), # A# - Sky Blue
61+
(0, 0, 255), # B - Blue
62+
63+
# Octave B - Rich and saturated shifted palette
64+
(64, 0, 255), # C - Indigo
65+
(128, 0, 255), # C# - Violet
66+
(200, 0, 255), # D - Purple
67+
(255, 0, 200), # D# - Magenta
68+
(255, 0, 128), # E - Hot Pink
69+
(255, 0, 64), # F - Deep Rose
70+
(255, 64, 64), # F# - Salmon
71+
(255, 128, 128), # G - Light Coral
72+
(255, 200, 128), # G# - Peach
73+
(255, 255, 128), # A - Pale Yellow
74+
(128, 255, 128), # A# - Mint
75+
(128, 255, 255), # B - Ice Blue
76+
]
77+
78+
# Expand BASE_COLORS to a full 72-entry list by repeating the 24-color pattern.
79+
PIXEL_COLORS = [BASE_COLORS[i % len(BASE_COLORS)] for i in range(NUMPIXELS)]
80+
81+
FADE_DURATION = 0.1 # Total fade duration in seconds
82+
FADE_STEPS = 5 # Number of steps in the fade
83+
84+
# Non-blocking fade state per pixel:
85+
# pixel_index -> {"color": (r,g,b), "step": int, "last_time": float}
86+
fading_pixels = {}
87+
88+
def note_to_matrix(note):
89+
"""
90+
Map a MIDI note to a (row, col) position in the matrix.
91+
Each row is one octave (12 notes). C always starts at column 0.
92+
NOTE_OFFSET determines which note maps to (row=0, col=0).
93+
"""
94+
offset_note = note - NOTE_OFFSET
95+
row = (offset_note // MATRIX_COLS) % MATRIX_ROWS
96+
col = offset_note % MATRIX_COLS
97+
return row, col
98+
99+
def matrix_to_pixel(row, col):
100+
"""
101+
Convert a (row, col) matrix position to a physical pixel index,
102+
accounting for zigzag wiring.
103+
Even rows (0, 2, 4) run left to right.
104+
Odd rows (1, 3, 5) run right to left.
105+
"""
106+
if row % 2 == 0:
107+
return row * MATRIX_COLS + col # Left to right
108+
else:
109+
return row * MATRIX_COLS + (MATRIX_COLS - 1 - col) # Right to left
110+
111+
def note_to_pixel(note):
112+
"""Map a MIDI note directly to a physical pixel index via the matrix."""
113+
row, col = note_to_matrix(note)
114+
return matrix_to_pixel(row, col)
115+
116+
def note_to_name(note):
117+
"""Return a human-readable note name and octave, e.g. 'C2' for note 36."""
118+
name = NOTE_NAMES[note % 12]
119+
octave = (note // 12) - 1
120+
return f"{name}{octave}"
121+
122+
def color_for_note(note):
123+
"""
124+
Look up the color for a note based on its position across two octaves.
125+
The 24-color palette tiles every two octaves across the matrix rows.
126+
"""
127+
offset_note = note - NOTE_OFFSET
128+
return BASE_COLORS[offset_note % len(BASE_COLORS)]
129+
130+
def color_wipe(color, delay=0.01):
131+
"""Wipe a color across the strip one LED at a time."""
132+
for i in range(NUMPIXELS):
133+
pixels[i] = color
134+
pixels.show()
135+
time.sleep(delay)
136+
137+
def boot_sequence():
138+
"""Animated boot sequence using color wipes."""
139+
color_wipe((255, 0, 0), 0.01) # Red wipe
140+
color_wipe((0, 255, 0), 0.01) # Green wipe
141+
color_wipe((0, 0, 255), 0.01) # Blue wipe
142+
color_wipe((0, 0, 0), 0.01) # Wipe off
143+
144+
def update_fades():
145+
"""Call this every loop iteration to advance any active fades."""
146+
now = time.monotonic()
147+
completed = []
148+
for pixel_index, state in fading_pixels.items():
149+
step_delay = FADE_DURATION / FADE_STEPS
150+
if now - state["last_time"] >= step_delay:
151+
state["step"] += 1
152+
state["last_time"] = now
153+
if state["step"] >= FADE_STEPS:
154+
pixels[pixel_index] = (0, 0, 0)
155+
pixels.show()
156+
completed.append(pixel_index)
157+
else:
158+
r, g, b = state["color"]
159+
factor = (FADE_STEPS - state["step"]) / FADE_STEPS
160+
pixels[pixel_index] = (int(r * factor), int(g * factor), int(b * factor))
161+
pixels.show()
162+
for pixel_index in completed:
163+
del fading_pixels[pixel_index]
164+
165+
# Run boot sequence on startup
166+
boot_sequence()
167+
168+
while True:
169+
msg = midi.receive()
170+
171+
if isinstance(msg, NoteOn) and msg.velocity > 0:
172+
pixel_index = note_to_pixel(msg.note)
173+
color = color_for_note(msg.note)
174+
note_name = note_to_name(msg.note)
175+
row, col = note_to_matrix(msg.note)
176+
# Immediately cancel any active fade on this pixel
177+
if pixel_index in fading_pixels:
178+
del fading_pixels[pixel_index]
179+
pixels[pixel_index] = color
180+
pixels.show()
181+
print(f"Note ON: {note_name} ({msg.note}) -> Row {row}, Col {col}, Pixel {pixel_index}, Color {color}, Channel {msg.channel + 1}")
182+
183+
elif isinstance(msg, NoteOff) or (isinstance(msg, NoteOn) and msg.velocity == 0):
184+
pixel_index = note_to_pixel(msg.note)
185+
color = color_for_note(msg.note)
186+
note_name = note_to_name(msg.note)
187+
row, col = note_to_matrix(msg.note)
188+
fading_pixels[pixel_index] = {
189+
"color": color,
190+
"step": 0,
191+
"last_time": time.monotonic()
192+
}
193+
print(f"Note OFF: {note_name} ({msg.note}) -> Row {row}, Col {col}, Pixel {pixel_index} fading, Channel {msg.channel + 1}")
194+
195+
# Advance all active fades each loop iteration
196+
update_fades()

0 commit comments

Comments
 (0)