|
| 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 | +REQUIRES Adafruit_ZeroFFT LIBRARY, install via Arduino Library manager. |
| 9 | +*/ |
| 10 | + |
| 11 | +#include <Adafruit_IS31FL3741.h> // For LED driver |
| 12 | +#include <PDM.h> // For microphone |
| 13 | +#include <Adafruit_ZeroFFT.h> // For math |
| 14 | + |
| 15 | +// FFT/SPECTRUM CONFIG ---- |
| 16 | + |
| 17 | +#define NUM_SAMPLES 512 // Audio & FFT buffer, MUST be a power of two |
| 18 | +#define SPECTRUM_SIZE (NUM_SAMPLES / 2) // Output spectrum is 1/2 of FFT output |
| 19 | +// Bottom of spectrum tends to be noisy, while top often exceeds musical |
| 20 | +// range and is just harmonics, so clip both ends off: |
| 21 | +#define LOW_BIN 5 // Lowest bin of spectrum that contributes to graph |
| 22 | +#define HIGH_BIN 150 // Highest bin " |
| 23 | + |
| 24 | +// GLOBAL VARIABLES ------- |
| 25 | + |
| 26 | +Adafruit_EyeLights_buffered glasses; // LED matrix is buffered for smooth animation |
| 27 | +extern PDMClass PDM; // Microphone |
| 28 | +short audio_buf[3][NUM_SAMPLES]; // Audio input buffers, 16-bit signed |
| 29 | +uint8_t active_buf = 0; // Buffer # into which audio is currently recording |
| 30 | +volatile int samples_read = 0; // # of samples read into current buffer thus far |
| 31 | +volatile bool mic_on = false; // true when reading from mic, false when full/stopped |
| 32 | +float spectrum[SPECTRUM_SIZE]; // FFT results are stored & further processed here |
| 33 | +float dynamic_level = 10.0; // For adapting to changing audio volume |
| 34 | +int frames; // For frames-per-second calculation |
| 35 | +uint32_t start_time; // Ditto |
| 36 | + |
| 37 | +struct { // Values associated with each column of the matrix |
| 38 | + int first_bin; // First spectrum bin index affecting column |
| 39 | + int num_bins; // Number of spectrum bins affecting column |
| 40 | + float *bin_weights; // List of spectrum bin weightings |
| 41 | + uint32_t color; // GFX-style 'RGB565' color for column |
| 42 | + float top; // Current column top position |
| 43 | + float dot; // Current column 'falling dot' position |
| 44 | + float velocity; // Current velocity of falling dot |
| 45 | +} column_table[18]; |
| 46 | + |
| 47 | +// Crude error handler, prints message to Serial console, flashes LED |
| 48 | +void err(char *str, uint8_t hz) { |
| 49 | + Serial.println(str); |
| 50 | + pinMode(LED_BUILTIN, OUTPUT); |
| 51 | + for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); |
| 52 | +} |
| 53 | + |
| 54 | +void setup() { // Runs once at program start... |
| 55 | + |
| 56 | + Serial.begin(115200); |
| 57 | + //while(!Serial); |
| 58 | + if (! glasses.begin()) err("IS3741 not found", 2); |
| 59 | + |
| 60 | + // FFT/SPECTRUM SETUP ----- |
| 61 | + |
| 62 | + uint8_t spectrum_bits = (int)log2f((float)SPECTRUM_SIZE); // e.g. 8 = 256 bin spectrum |
| 63 | + // Scale LOW_BIN and HIGH_BIN to 0.0 to 1.0 equivalent range in spectrum |
| 64 | + float low_frac = log2f((float)LOW_BIN) / (float)spectrum_bits; |
| 65 | + float frac_range = log2((float)HIGH_BIN) / (float)spectrum_bits - low_frac; |
| 66 | + // Serial.printf("%d %f %f\n", spectrum_bits, low_frac, frac_range); |
| 67 | + |
| 68 | + // To keep the display lively, tables are precomputed where each column of |
| 69 | + // the matrix (of which there are few) is the sum value and weighting of |
| 70 | + // several bins from the FFT spectrum output (of which there are many). |
| 71 | + // The tables also help visually linearize the output so octaves are evenly |
| 72 | + // spaced, as on a piano keyboard, whereas the source spectrum data is |
| 73 | + // spaced by frequency in Hz. |
| 74 | + |
| 75 | + for (int column=0; column<18; column++) { |
| 76 | + // Determine the lower and upper frequency range for this column, as |
| 77 | + // fractions within the scaled 0.0 to 1.0 spectrum range. 0.95 below |
| 78 | + // creates slight frequency overlap between columns, looks nicer. |
| 79 | + float lower = low_frac + frac_range * ((float)column / 18.0 * 0.95); |
| 80 | + float upper = low_frac + frac_range * ((float)(column + 1) / 18.0); |
| 81 | + float mid = (lower + upper) * 0.5; // Center of lower-to-upper range |
| 82 | + float half_width = (upper - lower) * 0.5 + 1e-2; // 1/2 of lower-to-upper range |
| 83 | + // Map fractions back to spectrum bin indices that contribute to column |
| 84 | + int first_bin = int(pow(2, (float)spectrum_bits * lower) + 1e-4); |
| 85 | + int last_bin = int(pow(2, (float)spectrum_bits * upper) + 1e-4); |
| 86 | + //Serial.printf("%d %d %d\n", column, first_bin, last_bin); |
| 87 | + float total_weight = 0.0; // Accumulate weight for this bin |
| 88 | + int num_bins = last_bin - first_bin + 1; |
| 89 | + // Allocate space for bin weights for column, stop everything if out of RAM. |
| 90 | + column_table[column].bin_weights = (float *)malloc(num_bins * sizeof(float)); |
| 91 | + if (column_table[column].bin_weights == NULL) err("Malloc fail", 10); |
| 92 | + for (int bin_index = first_bin; bin_index <= last_bin; bin_index++) { |
| 93 | + // Find distance from column's overall center to individual bin's |
| 94 | + // center, expressed as 0.0 (bin at center) to 1.0 (bin at limit of |
| 95 | + // lower-to-upper range). |
| 96 | + float bin_center = log2f((float)bin_index + 0.5) / (float)spectrum_bits; |
| 97 | + float dist = fabs(bin_center - mid) / half_width; |
| 98 | + if (dist < 1.0) { // Filter out a few math stragglers at either end |
| 99 | + // Bin weights have a cubic falloff curve within range: |
| 100 | + dist = 1.0 - dist; // Invert dist so 1.0 is at center |
| 101 | + float bin_weight = (((3.0 - (dist * 2.0)) * dist) * dist); |
| 102 | + column_table[column].bin_weights[bin_index - first_bin] = bin_weight; |
| 103 | + total_weight += bin_weight; |
| 104 | + } |
| 105 | + } |
| 106 | + //Serial.println(column); |
| 107 | + // Scale bin weights so total is 1.0 for each column, but then mute |
| 108 | + // lower columns slightly and boost higher columns. It graphs better. |
| 109 | + for (int i=0; i<num_bins; i++) { |
| 110 | + column_table[column].bin_weights[i] = column_table[column].bin_weights[i] / |
| 111 | + total_weight * (0.6 + (float)i / 18.0 * 2.0); |
| 112 | + //Serial.printf(" %f\n", column_table[column].bin_weights[i]); |
| 113 | + } |
| 114 | + column_table[column].first_bin = first_bin; |
| 115 | + column_table[column].num_bins = num_bins; |
| 116 | + column_table[column].color = glasses.color565(glasses.ColorHSV( |
| 117 | + 57600UL * column / 18, 255, 255)); // Red (0) to purple (57600) |
| 118 | + column_table[column].top = 6.0; // Start off bottom of graph |
| 119 | + column_table[column].dot = 6.0; |
| 120 | + column_table[column].velocity = 0.0; |
| 121 | + } |
| 122 | + |
| 123 | + for (int i=0; i<SPECTRUM_SIZE; i++) spectrum[i] = 0.0; |
| 124 | + |
| 125 | + // HARDWARE SETUP --------- |
| 126 | + |
| 127 | + // Configure glasses for max brightness, enable output |
| 128 | + glasses.setLEDscaling(0xFF); |
| 129 | + glasses.setGlobalCurrent(0xFF); |
| 130 | + glasses.enable(true); |
| 131 | + |
| 132 | + // Configure PDM mic, mono 16 KHz |
| 133 | + PDM.onReceive(onPDMdata); |
| 134 | + PDM.begin(1, 16000); |
| 135 | + |
| 136 | + start_time = millis(); |
| 137 | +} |
| 138 | + |
| 139 | +void loop() { // Repeat forever... |
| 140 | + |
| 141 | + short *audio_data; // Pointer to newly-received audio |
| 142 | + |
| 143 | + while (mic_on) yield(); // Wait for next buffer to finish recording |
| 144 | + // Full buffer received -- active_buf is index to new data |
| 145 | + audio_data = &audio_buf[active_buf][0]; // New data is here |
| 146 | + active_buf = 1 - active_buf; // Swap buffers to record into other one, |
| 147 | + mic_on = true; // and start recording next batch |
| 148 | + |
| 149 | + // Perform FFT operation on newly-received data, |
| 150 | + // results go back into the same buffer. |
| 151 | + ZeroFFT(audio_data, NUM_SAMPLES); |
| 152 | + |
| 153 | + // Convert FFT output to spectrum. log(y) looks better than raw data. |
| 154 | + // Only LOW_BIN to HIGH_BIN elements are needed. |
| 155 | + for(int i=LOW_BIN; i<=HIGH_BIN; i++) { |
| 156 | + spectrum[i] = (audio_data[i] > 0) ? log((float)audio_data[i]) : 0.0; |
| 157 | + } |
| 158 | + |
| 159 | + // Find min & max range of spectrum bin values, with limits. |
| 160 | + float lower = spectrum[LOW_BIN], upper = spectrum[LOW_BIN]; |
| 161 | + for (int i=LOW_BIN+1; i<=HIGH_BIN; i++) { |
| 162 | + if (spectrum[i] < lower) lower = spectrum[i]; |
| 163 | + if (spectrum[i] > upper) upper = spectrum[i]; |
| 164 | + } |
| 165 | + //Serial.printf("%f %f\n", lower, upper); |
| 166 | + if (upper < 2.5) upper = 2.5; |
| 167 | + |
| 168 | + // Adjust dynamic level to current spectrum output, keeps the graph |
| 169 | + // 'lively' as ambient volume changes. Sparkle but don't saturate. |
| 170 | + if (upper > dynamic_level) { |
| 171 | + // Got louder. Move level up quickly but allow initial "bump." |
| 172 | + dynamic_level = dynamic_level * 0.5 + upper * 0.5; |
| 173 | + } else { |
| 174 | + // Got quieter. Ease level down, else too many bumps. |
| 175 | + dynamic_level = dynamic_level * 0.75 + lower * 0.25; |
| 176 | + } |
| 177 | + |
| 178 | + // Apply vertical scale to spectrum data. Results may exceed |
| 179 | + // matrix height...that's OK, adds impact! |
| 180 | + float scale = 15.0 / (dynamic_level - lower); |
| 181 | + for (int i=LOW_BIN; i<=HIGH_BIN; i++) { |
| 182 | + spectrum[i] = (spectrum[i] - lower) * scale; |
| 183 | + } |
| 184 | + |
| 185 | + // Clear screen, filter and draw each column of the display... |
| 186 | + glasses.fill(0); |
| 187 | + for(int column=0; column<18; column++) { |
| 188 | + int first_bin = column_table[column].first_bin; |
| 189 | + // Start BELOW matrix and accumulate bin weights UP, saves math |
| 190 | + float column_top = 7.0; |
| 191 | + for (int bin_offset=0; bin_offset<column_table[column].num_bins; bin_offset++) { |
| 192 | + column_top -= spectrum[first_bin + bin_offset] * column_table[column].bin_weights[bin_offset]; |
| 193 | + } |
| 194 | + // Column top positions are filtered to appear less 'twitchy' -- |
| 195 | + // last data still has a 30% influence on current positions. |
| 196 | + column_top = (column_top * 0.7) + (column_table[column].top * 0.3); |
| 197 | + column_table[column].top = column_top; |
| 198 | + |
| 199 | + if(column_top < column_table[column].dot) { // Above current falling dot? |
| 200 | + column_table[column].dot = column_top - 0.5; // Move dot up |
| 201 | + column_table[column].velocity = 0.0; // and clear out velocity |
| 202 | + } else { |
| 203 | + column_table[column].dot += column_table[column].velocity; // Move dot down |
| 204 | + column_table[column].velocity += 0.015; // and accelerate |
| 205 | + } |
| 206 | + |
| 207 | + // Draw column and peak dot |
| 208 | + int itop = (int)column_top; // Quantize column top to pixel space |
| 209 | + glasses.drawLine(column, itop, column, itop + 20, column_table[column].color); |
| 210 | + glasses.drawPixel(column, (int)column_table[column].dot, 0xE410); |
| 211 | + } |
| 212 | + |
| 213 | + glasses.show(); // Buffered mode MUST use show() to refresh matrix |
| 214 | + |
| 215 | + frames += 1; |
| 216 | + uint32_t elapsed = millis() - start_time; |
| 217 | + //Serial.println(frames * 1000 / elapsed); |
| 218 | +} |
| 219 | + |
| 220 | +// PDM mic interrupt handler, called when new data is ready |
| 221 | +void onPDMdata() { |
| 222 | + //digitalWrite(LED_BUILTIN, millis() & 1024); // Debug heartbeat |
| 223 | + if (int bytes_to_read = PDM.available()) { |
| 224 | + if (mic_on) { |
| 225 | + int byte_limit = (NUM_SAMPLES - samples_read) * 2; // Space remaining, |
| 226 | + bytes_to_read = min(bytes_to_read, byte_limit); // don't overflow! |
| 227 | + PDM.read(&audio_buf[active_buf][samples_read], bytes_to_read); |
| 228 | + samples_read += bytes_to_read / 2; // Increment counter |
| 229 | + if (samples_read >= NUM_SAMPLES) { // Buffer full? |
| 230 | + mic_on = false; // Stop and |
| 231 | + samples_read = 0; // reset counter for next time |
| 232 | + } |
| 233 | + } else { |
| 234 | + // Mic is off (code is busy) - must read but discard data. |
| 235 | + // audio_buf[2] is a 'bit bucket' for this. |
| 236 | + PDM.read(audio_buf[2], bytes_to_read); |
| 237 | + } |
| 238 | + } |
| 239 | +} |
0 commit comments