Gotta Go Faster, Part 5.


The third logical block - Fast Fourier transform (FFT) is added to the previous block. FFT can be quite challenging if done from scratch but it is super simple using the Teensy Audio Libraries. The code below is the same as in the previous post with some additions for a whopping 1024 sample FFT (512 bins). This is where the difference between an Arduino and the Teensy starts to sink in. The Arduino is struggling with a 32 bins FHT where the Teensy is managing a 512 bins FFT like a breeze. The difference in resolution is quite staggering too, lets compare the two. The Arduino Uno managed a 38.46 kHz sample rate, FHT N = 64 -> 38460 / 64 = 601 Hz centre frequency in bin 1. The Teensy 4.0 is sampling my DSP at 48 kHz over I2S, FFT N = 1024 -> 48000 / 1024 = 47 Hz centre frequency in bin 1.

The fft1024.read method in the Teensy Audio Libraries can take two argument, binFirst and binLast and it will then return the sum of all bins between first and last. Extremely convenient to sum bins upp for a logarithmic response. I will visualise 16 bands si I had to use MathLab to come up with a good selection of bins for each band. My function readBand is using a lookup table based on the result from MathLab to return a sum of a range of bins for each band. Remember that the center frequency of bin 0 is 0 Hz and you therefor better skip it. I also only read up to bin 383 (around 18 kHz).

// Teensy
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
// FFT
const uint8_t   NUM_BAND      = 16;
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 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                    noSound = true;
SoundLevel              leftCh;
SoundLevel              rightCh;
SoundLevel              band[NUM_BAND];
Timer                   silenceTimer;
Trigger                 updateTrigger;
// 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);

// 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 updateCallback() {
  processChannels();
  processBands();
}

void setup() {
  // init sound system
  AudioMemory(15);
  mixer.gain(0, 0.5);
  mixer.gain(1, 0.5);
  // init timers and triggers
  silenceTimer.set(SILENCE_MS);
  updateTrigger.setup(UPDATE_MS, updateCallback);
}

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