Skip to content

Commit c4fa415

Browse files
Add EyeLights audio spectrum Arduino code (WIP)
1 parent 75affc8 commit c4fa415

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
*/
3+
4+
#include <Adafruit_IS31FL3741.h> // For LED driver
5+
#include <PDM.h> // For microphone
6+
#include <Adafruit_ZeroFFT.h> // For math
7+
8+
Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation
9+
extern PDMClass PDM;
10+
11+
#define NUM_SAMPLES 512 // FFT size, MUST be a power of two
12+
short sampleBuffer[NUM_SAMPLES]; // buffer to read samples into, each sample is 16-bits
13+
volatile int samplesRead; // number of samples read (set in interrupt)
14+
15+
#define SPECTRUM_SIZE (NUM_SAMPLES / 2) // Output spectrum is 1/2 of FFT result
16+
17+
// Bottom of spectrum tends to be noisy, while top often exceeds musical
18+
// range and is just harmonics, so clip both ends off:
19+
//#define LOW_BIN 10 // Lowest bin of spectrum that contributes to graph
20+
//#define HIGH_BIN 75 // Highest bin "
21+
22+
#define LOW_BIN 3 // Lowest bin of spectrum that contributes to graph
23+
#define HIGH_BIN 180 // Highest bin "
24+
25+
// Crude error handler, prints message to Serial console, flashes LED
26+
void err(char *str, uint8_t hz) {
27+
Serial.println(str);
28+
pinMode(LED_BUILTIN, OUTPUT);
29+
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
30+
}
31+
32+
float data[SPECTRUM_SIZE];
33+
34+
struct {
35+
int first_bin;
36+
int num_bins;
37+
float *bin_weights;
38+
uint32_t color;
39+
float dot;
40+
float velocity;
41+
} column_table[18];
42+
43+
int frames;
44+
uint32_t start_time;
45+
46+
void setup() { // Runs once at program start...
47+
48+
// Initialize hardware
49+
Serial.begin(115200);
50+
//while(!Serial);
51+
if (! glasses.begin()) err("IS3741 not found", 2);
52+
53+
uint8_t spectrum_bits = (int)log2f((float)SPECTRUM_SIZE);
54+
float low_frac = log2f((float)LOW_BIN) / (float)spectrum_bits;
55+
float frac_range = log2((float)HIGH_BIN) / (float)spectrum_bits - low_frac;
56+
Serial.printf("%d %f %f\n", spectrum_bits, low_frac, frac_range);
57+
58+
for (int column=0; column<18; column++) {
59+
float lower = low_frac + frac_range * ((float)column / 18.0 * 0.95);
60+
float upper = low_frac + frac_range * ((float)(column + 1) / 18.0);
61+
float mid = (lower + upper) * 0.5; // Center of lower-to-upper range
62+
float half_width = (upper - lower) * 0.5 + 1e-2; // 1/2 of lower-to-upper range
63+
// Map fractions back to spectrum bin indices that contribute to column
64+
int first_bin = int(pow(2, (float)spectrum_bits * lower) + 1e-4);
65+
int last_bin = int(pow(2, (float)spectrum_bits * upper) + 1e-4);
66+
Serial.printf("%d %d %d\n", column, first_bin, last_bin);
67+
float total_weight = 0.0;
68+
int num_bins = last_bin - first_bin + 1;
69+
column_table[column].bin_weights = (float *)malloc(num_bins * sizeof(float));
70+
for (int bin_index = first_bin; bin_index <= last_bin; bin_index++) {
71+
// Find distance from column's overall center to individual bin's
72+
// center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of
73+
// lower-to-upper range).
74+
float bin_center = log2f((float)bin_index + 0.5) / (float)spectrum_bits;
75+
float dist = fabs(bin_center - mid) / half_width;
76+
if (dist < 1.0) { // Filter out a few math stragglers at either end
77+
// Bin weights have a cubic falloff curve within range:
78+
dist = 1.0 - dist; // Invert dist so 1.0 is at center
79+
float bin_weight = (((3.0 - (dist * 2.0)) * dist) * dist);
80+
column_table[column].bin_weights[bin_index - first_bin] = bin_weight;
81+
total_weight += bin_weight;
82+
}
83+
}
84+
Serial.println();
85+
Serial.println(column);
86+
for (int i=0; i<num_bins; i++) {
87+
column_table[column].bin_weights[i] = column_table[column].bin_weights[i] / total_weight * (0.6 + (float)i / 18.0 * 1.8);
88+
Serial.printf(" %f\n", column_table[column].bin_weights[i]);
89+
}
90+
column_table[column].first_bin = first_bin;
91+
column_table[column].num_bins = num_bins;
92+
column_table[column].color = glasses.color565(glasses.ColorHSV(57600UL * column / 18, 255, 255));
93+
column_table[column].dot = 5.0;
94+
column_table[column].velocity = 0.0;
95+
}
96+
97+
for (int i=0; i<SPECTRUM_SIZE; i++) {
98+
data[i] = 0.0;
99+
}
100+
101+
// Configure glasses for max brightness, enable output
102+
glasses.setLEDscaling(0xFF);
103+
glasses.setGlobalCurrent(0xFF);
104+
glasses.enable(true);
105+
106+
// Configure PDM mic, mono 16 KHz
107+
PDM.onReceive(onPDMdata);
108+
PDM.begin(1, 16000);
109+
110+
start_time = millis();
111+
}
112+
113+
float dynamic_level = 6.0;
114+
115+
volatile bool mic_on = false;
116+
117+
void loop() { // Repeat forever...
118+
int samplesRemaining = NUM_SAMPLES;
119+
samplesRead = 0;
120+
mic_on = true;
121+
while (samplesRemaining) {
122+
if(samplesRead) { // Set in onPDMdata()
123+
samplesRemaining -= samplesRead;
124+
}
125+
yield();
126+
}
127+
mic_on = false;
128+
129+
// To do: could record into alternating buffers
130+
131+
ZeroFFT(sampleBuffer, NUM_SAMPLES);
132+
133+
134+
135+
// Convert FFT output to spectrum
136+
for(int i=0; i<SPECTRUM_SIZE; i++) {
137+
// data[i] = (data[i] * 0.25) + ((float)sampleBuffer[i] * 0.75);
138+
data[i] = (data[i] * 0.2) + ((sampleBuffer[i] ? log((float)sampleBuffer[i]) : 0.0) * 0.8);
139+
// data[i] = (float)sampleBuffer[i];
140+
#if 0
141+
uint32_t mag2 = fr[i] * fr[i] + fi[i] * fi[i];
142+
if (mag2) {
143+
data[i] = log(sqrt((float)mag2));
144+
// data[i] = sqrt((float)mag2);
145+
} else {
146+
data[i] = 0.0;
147+
}
148+
#endif
149+
}
150+
151+
float lower = data[0], upper = data[0];
152+
for (int i=1; i<SPECTRUM_SIZE; i++) {
153+
if (data[i] < lower) lower = data[i];
154+
if (data[i] > upper) upper = data[i];
155+
}
156+
// Serial.printf("%f %f\n", lower, upper);
157+
// if (lower < 4) lower = 4;
158+
// if (upper < 10) upper = 10;
159+
if (upper < 4.5) upper = 4.5; // because log
160+
161+
162+
163+
if (upper > dynamic_level) {
164+
// Got louder. Move level up quickly but allow initial "bump."
165+
dynamic_level = upper * 0.5 + dynamic_level * 0.5;
166+
} else {
167+
// Got quieter. Ease level down, else too many bumps.
168+
dynamic_level = dynamic_level * 0.7 + lower * 0.3;
169+
}
170+
// dynamic_level = 20.0;
171+
//dynamic_level = upper;
172+
173+
// Apply vertical scale to spectrum data. Results may exceed
174+
// matrix height...that's OK, adds impact!
175+
float scale = 10.0 / (dynamic_level - lower);
176+
for (int i=0; i<SPECTRUM_SIZE; i++) {
177+
data[i] = (data[i] - lower) * scale;
178+
}
179+
180+
glasses.fill(0);
181+
for(int column=0; column<18; column++) {
182+
int first_bin = column_table[column].first_bin;
183+
float column_top = 7.0;
184+
for (int bin_offset=0; bin_offset<column_table[column].num_bins; bin_offset++) {
185+
column_top -= data[first_bin + bin_offset] * column_table[column].bin_weights[bin_offset];
186+
}
187+
if(column_top < column_table[column].dot) {
188+
column_table[column].dot = column_top - 0.5;
189+
column_table[column].velocity = 0.0;
190+
} else {
191+
column_table[column].dot += column_table[column].velocity;
192+
column_table[column].velocity += 0.01;
193+
}
194+
195+
int itop = (int)column_top;
196+
glasses.drawLine(column, itop, column, itop + 50, column_table[column].color);
197+
glasses.drawPixel(column, (int)column_table[column].dot, 0xE410);
198+
}
199+
200+
glasses.show();
201+
202+
frames += 1;
203+
uint32_t elapsed = millis() - start_time;
204+
// Serial.println(frames * 1000 / elapsed);
205+
}
206+
207+
208+
void onPDMdata() {
209+
if (mic_on) {
210+
// query the number of bytes available
211+
int bytesAvailable = PDM.available();
212+
213+
// read into the sample buffer
214+
PDM.read(sampleBuffer, bytesAvailable);
215+
216+
// 16-bit, 2 bytes per sample
217+
samplesRead = bytesAvailable / 2;
218+
}
219+
}

0 commit comments

Comments
 (0)