Skip to content

Commit 02ecc68

Browse files
authored
Merge pull request #1894 from adafruit/audio_spectrum_lightshow
Adding audio spectrum light show
2 parents 2a5d759 + 68491bc commit 02ecc68

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

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

Feather_Sense_Audio_Visualizer_13x9_RGB_LED_Matrix/code.py renamed to Feather_Sense_Audio_Visualizer_13x9_RGB_LED_Matrix/waterfall_visualizer/code.py

File renamed without changes.

0 commit comments

Comments
 (0)