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();
}