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