Gotta Go Faster, Part 8.
The last logical block - Fire matrix might require some explanation. I want the amplifier to visualise something when its been idle for a while. I had spotted some examples of fire and flame using FastLED and I really wanted to come up with something along those lines. A lot of these projects use a single or multiple stands to make up the fire but I wanted more of a matrix approach. I found MatrixFireFast by Patrick Rigney after searching the net. I really liked the effect but wanted to refactor it into a reusable object with a more generic way of rendering. The result is the FireMatrix class below.
First thing to note is the use of templating to define the width and height of the matrix. No memory is allocated at runtime and it uses no contractor or destructor. You use the update method to update the whole matrix and you use the color method to get the color of each cell in the matrix. Render the whole matrix or just parts of it between updates, its up to you.
There are three properties you set to change the rendering of the fire matrix; palette, cooling and sparking. The class provides sane defaults but don't hesitate to play around with them if you want to tweak the apperance. You can try it out yourself at WOKWI (it is also an example of how the class can be used).
template <int16_t W, int16_t H> class FireMatrix { private: struct Flare { int16_t x; int16_t y; uint8_t heat; }; // width of the fire matrix static const int16_t WIDTH = W > 8 ? W : 8; // height of the fire matrix static const int16_t HEIGHT = H > 8 ? H : 8; // max number of flares static const uint8_t FLARES = W / 4; // array of flares Flare flare[FLARES]; // fire matrix 4 bit heat points uint8_t heatPoint[WIDTH / 2][HEIGHT]; // number of flares uint8_t flares = 0; // store a 4 bit heat point void putHeat(int16_t x, int16_t y, uint8_t heat) { uint8_t h; if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return; x >>= 1; // divide x by 2 h = heatPoint[x][y]; // if y is odd put heat in lower 4 bits else in upper 4 bits h = y & 1 ? (h & 0xf0) | (heat & 0xf) : (h & 0xf) | (heat << 4); heatPoint[x][y] = h; } // retrieve a 4 bit heat point uint8_t getHeat(int16_t x, int16_t y) { uint8_t h; if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return 0; x >>= 1; // divide x by 2 h = heatPoint[x][y]; // if y is odd get heat from lower 4 bits else from upper 4 bits h = y & 1 ? h & 0xf : (h >> 4) & 0xf; return h; } protected: // heat-up flare and update heat points void heatFlare(uint8_t index) { Flare f = flare[index]; int16_t b = f.heat * 10 / cooling + 1; for (int16_t x = (f.x - b); x < (f.x + b); x++) { for (int16_t y = (f.y - b); y < (f.y + b); y++) { if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) { int16_t d = (cooling * sqrt16((f.x - x) * (f.x - x) + (f.y - y) * (f.y - y)) + 5) / 10; uint8_t n = f.heat > d ? f.heat - d : 0; if (n > getHeat(x, y)) { // can only get brighter putHeat(x, y, n); } } } } } // cool down flare and delete it from flare array if out void coolFlare(uint8_t index) { Flare f = flare[index]; if (f.heat > 0) { f.heat--; flare[index] = f; } else { // flare is out for (int16_t i = index + 1; i < flares; i++) { flare[i - 1] = flare[i]; } flares--; } } // try to ignite new flare if there is room in flare array void sparkFlare() { if (flares < FLARES && random8(100) < sparking) { Flare f; f.x = random16(0, WIDTH); f.y = random16(0, (HEIGHT / 8) + 1); f.heat = 10; flare[flares] = f; flares++; } } public: // default palette based on FastLED HeatColors CRGBPalette16 palette = CRGBPalette16( 0x000000, 0x330000, 0x660000, 0x990000, 0xCC0000, 0xFF0000, 0xFF3300, 0xFF6600, 0xFF9900, 0xFFCC00, 0xFFFF00, 0xFFFF33, 0xFFFF66, 0xFFFF99, 0xFFFFCC, 0xFFFFFF ); // flame cool-off rate, default 14 uint8_t cooling = 14; // chance of sparking new flame in pecent, default 40 uint8_t sparking = 30; // call periodicly to update fire matrix void update() { int16_t x, y; // move all existing heat points up the display and cool off for (y = HEIGHT - 1; y > 0; y--) { for (x = 0; x < WIDTH; x++) { uint8_t h = getHeat(x, y - 1); putHeat(x, y, h > 0 ? h - 1 : 0); } } // heat-up the bottom row for (x = 0; x < WIDTH; x++) { putHeat(x, 0, random8(5, 9)); } // glow and cool off flares for (x = 0; x < flares; x++) { // heat-up flare heatFlare(x); // cool-down flare coolFlare(x); } // try spark a new flare sparkFlare(); } // get color from fire matrix CRGB color(int16_t x, int16_t y) { if (x >= 0 && x < WIDTH && y >= 0 && y < WIDTH) { return ColorFromPalette(palette, 24 * getHeat(x, y)); } else { return CRGB::Black; } } };
The FireMatrix class lives in the SupportClasses.h library. The code below is the same as the one in the previous post but with the fire matrix added. It will be activated after a while if no sound is detected.
// 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; const uint8_t FIRE_FPS = 15; const uint8_t FIRE_MS = 1000 / FIRE_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 fireTimer; Timer silenceTimer; Trigger updateTrigger; Trigger gradientTrigger; FireMatrix<XRES, YRES> fire; 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 displayFire() { if (fireTimer.expired()) { fireTimer.reset(); fire.update(); for (byte x = 0; x < XRES; x++) { for (byte y = 0; y < YRES; y++) { lightAdjPixel(x, y, fire.color(x, y)); } } FastLED.show(); } } 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(); if (silenceTimer.expired()) { displayFire(); } else { 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(); }