Skip to content

Commit 6d5adce

Browse files
authored
Merge pull request #3218 from adafruit/MIDI_NeoPixel_Visualizer
Adding code for midi neopixel visualizer
2 parents d2ad31d + 8fd57f4 commit 6d5adce

File tree

1 file changed

+195
-0
lines changed

1 file changed

+195
-0
lines changed

MIDI_NeoPixel_Visualizer/code.py

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

0 commit comments

Comments
 (0)