Gotta Go Faster, Part 7.
The fifth logical block - Music visualisation offers endless opportunities but I will stick with something like my previous bar graph representation of 16 frequency bands. Note that I use my 32 wide LCD matrix for 16 bands with two pixels (LED) binned into each 3D printed cell in my raster. It is not obvious if not seen in real life but rendering the peak hold value as the right pixel in each cell offers a kind of 3D effect with the peak hold almost as a shadow. I really like the effect and binning two pixels per cell also decreases the power needed to drive each cell.
The VU meter parts from previous post is removed from the code below but a lot more is added. The STATUS_PIN is referring to the the onboard LED on the Teensy and is used to lit the LED during boot-up and to turn it off when entering the infinite loop routine. It is an easy way to provide some feedback and o good indicator if something goes astray during the setup phase.
Brightly lit visualisation can be quite obnoxious is a dark environment and some LDR code is added to control the LED matrix intensity based on the ambient light. This is how you hook up an LDR to the Arduino and it is done the same way on a Teensy (I am using ADC0, pin 14 on my Teensy).
The function lightAdjPixel is used to set intensity based on the latest LDR reading. The reading is done by calling the processLdr function. It is also setting the noLight variable to true if it is below the minimum ambient light threshold (LDR_MIN). The result is a display that nicely fades with the ambient light level.
The function displaySpectrum is rendering the music visualisation (change this function into something that suits your output display and your likings). It is drawing the current smoothed value with higher intensity as the left pixel and the peak hold value with lower intensity as the right pixel in each raster cell. The whole matrix is drawn in a shading color and the palette is cycled by the gradientCallback function. The change of colors makes it possible to enjoy the full color spectrum. The change is slow and it prevents the display from being too static and too dynamic.
The difference between the powerful Teensy and the Arduino Uno is like night and day. The Arduino works but just barely. The Teensy shines with great bottom end resolution coupled with a true logarithmic display over the whole spectrum. The display is butter smooth and yet responsive. This kind of gimmicks tend to grow old quite quick but I still find myself mesmerised by this music visualisation. This turned out way better than I expected and I am amazed by the raw power of the Teensy 4.0.
// Teensy #include <Audio.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <SerialFlash.h> // PIN const uint8_t LED_PIN = 1; // pin 8 used for I2S IN1 on Teensy 4.0 // pin 13 is onboard led pin on Teensy 4.0 const uint8_t STATUS_PIN = 13; // pin 20 uded for I2S LRCLK on Teenst 4.0 // pin 21 uded for I2S BCLK on Teenst 4.0 // LDR const uint8_t LDR_ADC = 0; // pin 14 const uint16_t LDR_MIN = 100; const uint16_t LDR_MAX = 600; const uint8_t MAX_DIM = 192; const float LDR_ALPHA = 0.01; // LED const uint8_t XRES = 32; const uint8_t YRES = 8; const uint16_t NUM_LEDS = XRES * YRES; const uint16_t MAX_POWER = 4 * 1000; // milliwatts const uint8_t BRIGHTNESS = 128; #include <WS2812Serial.h> #define USE_WS2812SERIAL #define FASTLED_INTERNAL #include <FastLED.h> // FFT const uint8_t NUM_BAND = XRES / 2; const float BND_ALPHA = 0.2; // SND const float SILENCE = 0.02; const uint8_t PEAK_HOLD = 32; const float PEAK_DECAY = 0.02; const float CHN_ALPHA = 0.9; // ETC const uint16_t POWERUP_MS = 2000; const uint16_t GRADIENT_MS = 1000; const uint32_t SILENCE_MS = 60000; const uint8_t UPDATE_FPS = 60; const uint8_t UPDATE_MS = 1000 / UPDATE_FPS; #include "SupportClasses.h" struct SoundLevel { float value = 0; float peak = 0; uint8_t hold = 0; // update with new reading void update(float reading) { value = reading; if (value > peak) { peak = value; hold = PEAK_HOLD; } else if (hold > 0) { hold--; } else if (peak > 0) { peak -= PEAK_DECAY; } } }; bool altMode = false; bool noLight = true; bool noSound = true; uint8_t dimValue = 0; uint8_t gradientHue = 0; SoundLevel leftCh; SoundLevel rightCh; SoundLevel band[NUM_BAND]; Timer silenceTimer; Trigger updateTrigger; Trigger gradientTrigger; CRGB leds[NUM_LEDS]; // Teensy Audio AudioInputI2Sslave i2sSlave; AudioMixer4 mixer; AudioAnalyzePeak peakLeft; AudioAnalyzePeak peakRight; AudioAnalyzeFFT1024 fft1024; AudioConnection patchCordPeakLeft(i2sSlave, 0, peakLeft, 0); AudioConnection patchCordMixerLeft(i2sSlave, 0, mixer, 0); AudioConnection patchCordPeakRight(i2sSlave, 1, peakRight, 0); AudioConnection patchCordMixerRight(i2sSlave, 1, mixer, 1); AudioConnection patchCordFft1024(mixer, fft1024); // map x, y to pixel index uint16_t pixel(uint8_t x, uint8_t y) { const uint16_t index = XRES * (YRES - y); return (y & 0x01) ? index - XRES + x : index - 1 - x; } // update with light adjusted rgb pixel void lightAdjPixel(uint8_t x, uint8_t y, CRGB color) { const uint16_t index = pixel(x, y); leds[index] = color; leds[index].fadeLightBy(dimValue); if (noLight) leds[index].fadeLightBy(128); } // clamp reading between 0.0 and 1.0 float clamp(float reading) { const float temp = reading < 0 ? 0 : reading; return temp > 1 ? 1 : temp; } // scale reading to display resolution uint8_t scale(float reading, uint8_t resolution) { reading = clamp(reading); return reading > SILENCE ? round(reading * resolution) : 0; } // simple exponential smoothing, alpha is the smoothing factor [0,1] // alpha closer to 1, less smoothing, greater weight to recent changes // alpha closer to 0, more smoothing, less responsive to recent changes float ses(float input, float average, float alpha) { return average + alpha * (input - average); } float readBand(byte index) { const int BAND_LUT[NUM_BAND][2] = { { 1, 1}, { 2, 3}, { 4, 5}, { 6, 8}, { 9, 12}, { 13, 18}, { 19, 25}, { 26, 35}, { 36, 48}, { 49, 65}, { 66, 88}, { 89, 119}, {120, 160}, {161, 214}, {215, 287}, {288, 383}, }; return fft1024.read(BAND_LUT[index][0], BAND_LUT[index][1]); } void processBands() { if (fft1024.available()) { float reading[NUM_BAND]; float maxReading = 0; for (byte i = 0; i < NUM_BAND; i++) { reading[i] = readBand(i); maxReading = max(maxReading, reading[i]); } // get scale to max reading factor float f = maxReading > SILENCE ? 1.0 / maxReading : 0; for (byte i = 0; i < NUM_BAND; i++) { // filter and scale reading to max reading band[i].update(ses(reading[i] * f, band[i].value, BND_ALPHA)); } } } void processChannels() { if (peakLeft.available() && peakRight.available()) { leftCh.update(ses(peakLeft.read(), leftCh.value, CHN_ALPHA)); rightCh.update(ses(peakRight.read(), rightCh.value, CHN_ALPHA)); if (leftCh.value > SILENCE && rightCh.value > SILENCE) { silenceTimer.reset(); } } } void processLdr() { static float avg = 0; int value = analogRead(LDR_ADC); avg = ses(value, avg, LDR_ALPHA); // filter ldr reading value = round(avg); value = constrain(value, LDR_MIN, LDR_MAX); // clamp to ldr min-max value = map(value, LDR_MIN, LDR_MAX, 0, MAX_DIM); // map value to min-max dimValue = MAX_DIM - value; // update based on ldr value noLight = (dimValue == MAX_DIM); // is it pitch black? } void displaySpectrum() { static const uint8_t HUE_STEP = 32 / NUM_BAND; uint8_t level; uint8_t peak; uint8_t hue; uint8_t x; uint8_t y; CHSV hiColor; CHSV loColor; FastLED.clear(); for (byte i = 0; i < NUM_BAND; i++) { x = i << 1; peak = scale(band[i].peak, YRES); level = scale(band[i].value, YRES); hue = gradientHue - (i * HUE_STEP); hiColor = CHSV(hue, 255, 255); loColor = CHSV(hue, 255, 127); for (y = 0; y < YRES; y++) { if (level > y) { lightAdjPixel(x, y, hiColor); } if (peak > y) { lightAdjPixel(x + 1, y, loColor); } } } FastLED.show(); } // gradient hue callback void gradientCallback() { gradientHue--; } void updateCallback() { gradientTrigger.update(); processLdr(); processChannels(); processBands(); displaySpectrum(); } void setup() { // turn on onboard led digitalWrite(STATUS_PIN, HIGH); // init sound system AudioMemory(15); mixer.gain(0, 0.5); mixer.gain(1, 0.5); delay(POWERUP_MS); // init leds FastLED.addLeds<WS2812SERIAL, LED_PIN, BRG>(leds, NUM_LEDS); FastLED.setCorrection(TypicalLEDStrip); FastLED.setMaxPowerInMilliWatts(MAX_POWER); FastLED.setBrightness(BRIGHTNESS); FastLED.clear(); FastLED.show(); // init timers and triggers silenceTimer.set(SILENCE_MS); updateTrigger.setup(UPDATE_MS, updateCallback); gradientTrigger.setup(GRADIENT_MS, gradientCallback); // turn off onboard led digitalWrite(STATUS_PIN, LOW); } void loop() { updateTrigger.update(); }