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