Gotta Go Faster, Part 6.

 
The fourth logical block - VU meters is not really about the traditional kind of VU meters. It is about me wanting my alternative mode doing something useful. With I2S stereo input a value of 1.0 equals 0 dBFS. I therefor thought it would be useful to let the alternative mode visualise the input headroom, i.e. the amount left before clipping the input (in this case the 1.0 Vrms input sensitivity of the DSP acting as the preamp). 

The sound capturing code below is the same as in previous posts but I have removed the FFT related parts for now. The headroom has to be visualised and I will once again use my Longruner WS2812B 8x32 Matrix. The main problem running this LED matrix with the Arduino Uno was write out speed, i.e. the lack of speed. Another problem with the Arduino Uno was the lack of memory preventing me from using a feature rich but memory hungry library like FastLED. Teensy got loads of memory and the brilliant non blocking library WS2812Serial. Note the // LED and // init leds sections in the code. Also note the CRGB leds variable. 

I have added a couple of functions. Read the comment above for a quick explanation of what they do. The function displayChannel will draw a VU like bar on the LED matrix (change this function into something that suits your output display and your likings). It is called twice from the function displayHeadroom, one output per channel. Note how the dbfs function is used to calculate the headroom.

Why is this useful? I use mainly two sound sources, a turntable and an Airplay 2 receiver. The output level of the turntable changes when I swap cartridge and the output level of the Airplay receiver changes with its builtin software volume control. A lot of CD players also got controls to set output level. Being able to measure and visualise the input level is an easy way to assure I am not clipping the input. This has proven to be much more useful than I initially thought. I am drawing peak values with an additional hold, the hold values are drawn with faded intensity.  The Teensy Audio Library got builtin rms support too and a bunch of other analytic functions.

// Teensy
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
// PIN
const uint8_t   LED_PIN       = 1;
// 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>
// 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 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                    noSound = true;
SoundLevel              leftCh;
SoundLevel              rightCh;
Timer                   silenceTimer;
Trigger                 updateTrigger;
CRGB                    leds[NUM_LEDS];
// Teensy Audio
AudioInputI2Sslave      i2sSlave;
AudioMixer4             mixer;
AudioAnalyzePeak        peakLeft;
AudioAnalyzePeak        peakRight;
AudioConnection         patchCordPeakLeft(i2sSlave, 0, peakLeft, 0);
AudioConnection         patchCordMixerLeft(i2sSlave, 0, mixer, 0);
AudioConnection         patchCordPeakRight(i2sSlave, 1, peakRight, 0);
AudioConnection         patchCordMixerRight(i2sSlave, 1, mixer, 1);

// 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;
}

// convert I2S reading [0,1] to dBFS
float dbfs(float reading) {
  return 20 * log10(abs(reading));
}

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

void displayChannel(SoundLevel channel, byte y) {
  static const uint8_t HI = (XRES / 4) * 3;
  static const uint8_t LO = XRES / 2;
  // get dBFS as a positive number
  uint16_t peak = round(abs(dbfs(channel.peak)));
  uint16_t level = round(abs(dbfs(channel.value)));
  // constrain to max resolution and convert to headroom
  peak = XRES - constrain(peak, 0, XRES);
  level = XRES - constrain(level, 0, XRES);
  CRGB hiColor;
  CRGB miColor;
  CRGB loColor;
  for (byte x = 1; x < XRES; x += 2) {
    hiColor = x < LO ? CRGB::Green : x > HI ? CRGB::Red : CRGB::Yellow;
    miColor = hiColor;
    miColor.fadeLightBy(64);
    loColor = miColor;
    loColor.fadeLightBy(128);
    if (peak >= x) {
      leds[pixel(x, y)] = loColor;
    }
    if (level >= x) {
      leds[pixel(x, y + 1)] = hiColor;
      leds[pixel(x, y + 2)] = hiColor;
      leds[pixel(x, y + 3)] = miColor;
    } else if (peak >= x) {
      leds[pixel(x, y + 1)] = loColor;
      leds[pixel(x, y + 2)] = loColor;
      leds[pixel(x, y + 3)] = loColor;
    }
  }
}

void displayHeadroom() {
  static const uint8_t UPPER = YRES / 2;
  static const uint8_t LOWER = 0;
  FastLED.clear();
  // left channel on upper half
  displayChannel(leftCh, UPPER);
  // right channel on lower half
  displayChannel(rightCh, LOWER);
  FastLED.show();
}

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 updateCallback() {
  processChannels();
  if (altMode) {
    displayHeadroom();
  } 
}

void setup() {
  // init sound system
  AudioMemory(15);
  mixer.gain(0, 0.5);
  mixer.gain(1, 0.5);
  // 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);
}

void loop() {
  updateTrigger.update();
}