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