Skip to content

Commit cf6bcf7

Browse files
authored
Merge pull request #1842 from PaintYourDragon/main
Add EyeLights audio spectrum
2 parents f203261 + 840389e commit cf6bcf7

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

EyeLights_Audio_Spectrum/code.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
AUDIO SPECTRUM LIGHT SHOW for Adafruit EyeLights (LED Glasses + Driver).
3+
Uses onboard microphone and a lot of math to react to music.
4+
"""
5+
6+
from array import array
7+
from math import log
8+
from time import monotonic
9+
from supervisor import reload
10+
import board
11+
from audiobusio import PDMIn
12+
from busio import I2C
13+
import adafruit_is31fl3741
14+
from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses
15+
from rainbowio import colorwheel
16+
from ulab import numpy as np
17+
from ulab.scipy.signal import spectrogram
18+
19+
20+
# FFT/SPECTRUM CONFIG ----
21+
22+
fft_size = 256 # Sample size for Fourier transform, MUST be power of two
23+
spectrum_size = fft_size // 2 # Output spectrum is 1/2 of FFT result
24+
# Bottom of spectrum tends to be noisy, while top often exceeds musical
25+
# range and is just harmonics, so clip both ends off:
26+
low_bin = 10 # Lowest bin of spectrum that contributes to graph
27+
high_bin = 75 # Highest bin "
28+
29+
30+
# HARDWARE SETUP ---------
31+
32+
# Manually declare I2C (not board.I2C() directly) to access 1 MHz speed...
33+
i2c = I2C(board.SCL, board.SDA, frequency=1000000)
34+
35+
# Initialize the IS31 LED driver, buffered for smoother animation
36+
glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
37+
glasses.show() # Clear any residue on startup
38+
glasses.global_current = 5 # Not too bright please
39+
40+
# Initialize mic and allocate recording buffer (default rate is 16 MHz)
41+
mic = PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, bit_depth=16)
42+
rec_buf = array("H", [0] * fft_size) # 16-bit audio samples
43+
44+
45+
# FFT/SPECTRUM SETUP -----
46+
47+
# To keep the display lively, tables are precomputed where each column of
48+
# the matrix (of which there are few) is the sum value and weighting of
49+
# several bins from the FFT spectrum output (of which there are many).
50+
# The tables also help visually linearize the output so octaves are evenly
51+
# spaced, as on a piano keyboard, whereas the source spectrum data is
52+
# spaced by frequency in Hz.
53+
column_table = []
54+
55+
spectrum_bits = log(spectrum_size, 2) # e.g. 7 for 128-bin spectrum
56+
# Scale low_bin and high_bin to 0.0 to 1.0 equivalent range in spectrum
57+
low_frac = log(low_bin, 2) / spectrum_bits
58+
frac_range = log(high_bin, 2) / spectrum_bits - low_frac
59+
60+
for column in range(glasses.width):
61+
# Determine the lower and upper frequency range for this column, as
62+
# fractions within the scaled 0.0 to 1.0 spectrum range. 0.95 below
63+
# creates slight frequency overlap between columns, looks nicer.
64+
lower = low_frac + frac_range * (column / glasses.width * 0.95)
65+
upper = low_frac + frac_range * ((column + 1) / glasses.width)
66+
mid = (lower + upper) * 0.5 # Center of lower-to-upper range
67+
half_width = (upper - lower) * 0.5 # 1/2 of lower-to-upper range
68+
# Map fractions back to spectrum bin indices that contribute to column
69+
first_bin = int(2 ** (spectrum_bits * lower) + 1e-4)
70+
last_bin = int(2 ** (spectrum_bits * upper) + 1e-4)
71+
bin_weights = [] # Each spectrum bin's weighting will be added here
72+
for bin_index in range(first_bin, last_bin + 1):
73+
# Find distance from column's overall center to individual bin's
74+
# center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of
75+
# lower-to-upper range).
76+
bin_center = log(bin_index + 0.5, 2) / spectrum_bits
77+
dist = abs(bin_center - mid) / half_width
78+
if dist < 1.0: # Filter out a few math stragglers at either end
79+
# Bin weights have a cubic falloff curve within range:
80+
dist = 1.0 - dist # Invert dist so 1.0 is at center
81+
bin_weights.append(((3.0 - (dist * 2.0)) * dist) * dist)
82+
# Scale bin weights so total is 1.0 for each column, but then mute
83+
# lower columns slightly and boost higher columns. It graphs better.
84+
total = sum(bin_weights)
85+
bin_weights = [
86+
(weight / total) * (0.8 + idx / glasses.width * 1.4)
87+
for idx, weight in enumerate(bin_weights)
88+
]
89+
# List w/five elements is stored for each column:
90+
# 0: Index of the first spectrum bin that impacts this column.
91+
# 1: A list of bin weights, starting from index above, length varies.
92+
# 2: Color for drawing this column on the LED matrix. The 225 is on
93+
# purpose, providing hues from red to purple, leaving out magenta.
94+
# 3: Current height of the 'falling dot', updated each frame
95+
# 4: Current velocity of the 'falling dot', updated each frame
96+
column_table.append(
97+
[
98+
first_bin - low_bin,
99+
bin_weights,
100+
colorwheel(225 * column / glasses.width),
101+
glasses.height,
102+
0.0,
103+
]
104+
)
105+
# print(column_table)
106+
107+
108+
# MAIN LOOP -------------
109+
110+
dynamic_level = 10 # For responding to changing volume levels
111+
frames, start_time = 0, monotonic() # For frames-per-second calc
112+
113+
while True:
114+
# The try/except here is because VERY INFREQUENTLY the I2C bus will
115+
# encounter an error when accessing the LED driver, whether from bumping
116+
# around the wires or sometimes an I2C device just gets wedged. To more
117+
# robustly handle the latter, the code will restart if that happens.
118+
try:
119+
mic.record(rec_buf, fft_size) # Record batch of 16-bit samples
120+
samples = np.array(rec_buf) # Convert to ndarray
121+
# Compute spectrogram and trim results. Only the left half is
122+
# normally needed (right half is mirrored), but we trim further as
123+
# only the low_bin to high_bin elements are interesting to graph.
124+
spectrum = spectrogram(samples)[low_bin : high_bin + 1]
125+
# Linearize spectrum output. spectrogram() is always nonnegative,
126+
# but add a tiny value to change any zeros to nonzero numbers
127+
# (avoids rare 'inf' error)
128+
spectrum = np.log(spectrum + 1e-7)
129+
# Determine minimum & maximum across all spectrum bins, with limits
130+
lower = max(np.min(spectrum), 4)
131+
upper = min(max(np.max(spectrum), lower + 6), 20)
132+
133+
# Adjust dynamic level to current spectrum output, keeps the graph
134+
# 'lively' as ambient volume changes. Sparkle but don't saturate.
135+
if upper > dynamic_level:
136+
# Got louder. Move level up quickly but allow initial "bump."
137+
dynamic_level = upper * 0.7 + dynamic_level * 0.3
138+
else:
139+
# Got quieter. Ease level down, else too many bumps.
140+
dynamic_level = dynamic_level * 0.5 + lower * 0.5
141+
142+
# Apply vertical scale to spectrum data. Results may exceed
143+
# matrix height...that's OK, adds impact!
144+
data = (spectrum - lower) * (7 / (dynamic_level - lower))
145+
146+
for column, element in enumerate(column_table):
147+
# Start BELOW matrix and accumulate bin weights UP, saves math
148+
first_bin = element[0]
149+
column_top = glasses.height + 1
150+
for bin_offset, weight in enumerate(element[1]):
151+
column_top -= data[first_bin + bin_offset] * weight
152+
153+
if column_top < element[3]: # Above current falling dot?
154+
element[3] = column_top - 0.5 # Move dot up
155+
element[4] = 0 # and clear out velocity
156+
else:
157+
element[3] += element[4] # Move dot down
158+
element[4] += 0.2 # and accelerate
159+
160+
column_top = int(column_top) # Quantize to pixel space
161+
for row in range(column_top): # Erase area above column
162+
glasses.pixel(column, row, 0)
163+
for row in range(column_top, 5): # Draw column
164+
glasses.pixel(column, row, element[2])
165+
glasses.pixel(column, int(element[3]), 0xE08080) # Draw peak dot
166+
167+
glasses.show() # Buffered mode MUST use show() to refresh matrix
168+
169+
frames += 1
170+
# print(frames / (monotonic() - start_time), "FPS")
171+
172+
except OSError: # See "try" notes above regarding rare I2C errors.
173+
print("Restarting")
174+
reload()

0 commit comments

Comments
 (0)