FastLED 3.9.15
Loading...
Searching...
No Matches
audio Directory Reference
+ Directory dependency graph for audio:

Directories

 detector
 
 fft
 

Files

 _build.cpp.hpp
 Unity build header for fl/audio/ directory.
 
 audio.cpp.hpp
 
 audio.h
 
 audio_batch.cpp.hpp
 
 audio_batch.h
 
 audio_context.cpp.hpp
 
 audio_context.h
 
 audio_detector.h
 
 audio_frame.h
 
 audio_input.cpp.hpp
 
 audio_input.h
 
 audio_manager.cpp.hpp
 
 audio_manager.h
 
 audio_processor.cpp.hpp
 
 audio_processor.h
 
 audio_reactive.cpp.hpp
 
 audio_reactive.h
 
 auto_gain.cpp.hpp
 
 auto_gain.h
 
 frequency_bin_mapper.cpp.hpp
 Implementation of FrequencyBinMapper for fft::FFT bin to frequency channel mapping.
 
 frequency_bin_mapper.h
 
 input.h
 
 mic_profiles.h
 
 mic_response_data.h
 High-resolution microphone frequency response data and utilities.
 
 noise_floor_tracker.cpp.hpp
 
 noise_floor_tracker.h
 
 signal_conditioner.cpp.hpp
 
 signal_conditioner.h
 
 silence_envelope.cpp.hpp
 
 silence_envelope.h
 
 spectral_equalizer.cpp.hpp
 Implementation of SpectralEqualizer for frequency-dependent gain correction.
 
 spectral_equalizer.h
 
 synth.cpp.hpp
 
 synth.h
 Bandlimited audio synthesizer - waveform oscillator module.
 

Detailed Description

Quick Start Examples

Easiest: FastLED.add(AudioConfig) (Recommended)

The simplest way to get audio-reactive LEDs. FastLED.add() creates the microphone, wires up a scheduler task that auto-reads samples, and returns an AudioProcessor ready for callbacks. No manual update() loop needed — audio is pumped automatically during FastLED.show().

#include "FastLED.h"
#define NUM_LEDS 60
#define LED_PIN 2
// I2S pins for INMP441 microphone (adjust for your board)
#define I2S_WS 7
#define I2S_SD 8
#define I2S_CLK 4
void setup() {
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(128);
// One line: create mic + auto-pump task
// FastLED stores the AudioProcessor internally, so no global needed
auto config = fl::AudioConfig::CreateInmp441(I2S_WS, I2S_SD, I2S_CLK, fl::Right);
auto audio = FastLED.add(config);
audio->setAutoGainEnabled(true);
// Flash white on every beat
audio->onBeat([] {
});
// Map bass level to hue
audio->onBass([](float level) {
uint8_t hue = static_cast<uint8_t>(level * 160);
fill_solid(leds, NUM_LEDS, CHSV(hue, 255, 255));
});
// Dim to black on silence
audio->onSilenceStart([] {
});
// audio goes out of scope here — that's fine, FastLED keeps it alive.
// Use FastLED.remove(audio) if you ever need to tear it down.
}
void loop() {
FastLED.show(); // Audio is auto-pumped here
}
void setup()
void loop()
fl::UIAudio audio("Audio Input")
#define NUM_LEDS
fl::CRGB leds[NUM_LEDS]
#define LED_PIN
#define I2S_CLK
#define I2S_SD
#define I2S_WS
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
uint8_t hue
Definition advanced.h:94
void fadeToBlackBy(CRGB *leds, fl::u16 num_leds, fl::u8 fadeBy)
void fill_solid(CRGB *targetArray, int numToFill, const CRGB &color) FL_NOEXCEPT
Fill a range of LEDs with a solid color.
Definition fill.cpp.hpp:9
constexpr EOrder GRB
Definition eorder.h:19
fl::hsv8 CHSV
Definition chsv.h:11
fl::CRGB CRGB
Definition crgb.h:25
@ White
<div style='background:#FFFFFF;width:4em;height:4em;'></div>
Definition crgb.h:646
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:510

How it works: FastLED.add(config) internally calls IAudioInput::create(), starts the mic, stores the AudioProcessor in an internal list, and creates a fl::task::every_ms(1) that drains all buffered samples and feeds them to the AudioProcessor. The task runs during FastLED.show() via end-frame → async_run()Scheduler::update(). Use FastLED.remove(audio) to tear down a specific processor, or let it live for the lifetime of the program.

Platform behavior:

Test injection: On any platform, pass a custom IAudioInput directly:

auto audio = FastLED.add(fakeInput); // works on host/stub too
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414

High-Level: Beat-Reactive LEDs (Manual Update)

If you need more control over when samples are read (e.g., reading from a buffer at a specific rate), you can create the AudioProcessor yourself and call update() manually.

#include "FastLED.h"
#include "fl/audio/input.h"
using namespace fl;
#define NUM_LEDS 60
#define DATA_PIN 5
AudioProcessor audio;
bool gBeat = false;
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
// Create microphone input (ESP32 + INMP441)
auto config = AudioConfig::CreateInmp441(
15, // WS pin
32, // SD pin
14, // SCK pin
Left
);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) {
// Handle error - check errorMsg
return;
}
mic->start();
// Register event callbacks
audio.onBeat([]() {
gBeat = true;
});
audio.onBass([](float level) {
// level is 0.0 - 1.0
fill_solid(leds, NUM_LEDS / 3, CHSV(0, 255, level * 255));
});
audio.onMid([](float level) {
fill_solid(leds + NUM_LEDS / 3, NUM_LEDS / 3, CHSV(96, 255, level * 255));
});
audio.onTreble([](float level) {
fill_solid(leds + 2 * NUM_LEDS / 3, NUM_LEDS / 3, CHSV(160, 255, level * 255));
});
}
void loop() {
AudioSample sample = mic->read();
if (sample.isValid()) {
audio.update(sample); // Manual update
}
if (gBeat) {
gBeat = false;
// Flash white on beat
}
FastLED.show();
}
#define DATA_PIN
Definition ClientReal.h:82
uint8_t gHue
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode)
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
Definition sample.cpp.hpp:9
unsigned char uint8_t
Definition s16x16x4.h:209
Base definition for an LED controller.
Definition crgb.hpp:179

High-Level: Percussion-Reactive LEDs

Use onKick(), onSnare(), and onHiHat() callbacks to trigger different colors for each drum hit.

#include "FastLED.h"
#include "fl/audio/input.h"
using namespace fl;
#define NUM_LEDS 60
#define DATA_PIN 5
AudioProcessor audio;
CRGB gFlashColor = CRGB::Black;
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
auto config = AudioConfig::CreateInmp441(15, 32, 14, Left);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) return;
mic->start();
// Each drum hit triggers a different color
audio.onKick([]() {
gFlashColor = CRGB::Red;
});
audio.onSnare([]() {
gFlashColor = CRGB::Yellow;
});
audio.onHiHat([]() {
gFlashColor = CRGB::Cyan;
});
audio.onTom([]() {
gFlashColor = CRGB::Purple;
});
}
void loop() {
AudioSample sample = mic->read();
if (sample.isValid()) {
audio.update(sample);
}
if (gFlashColor != CRGB(CRGB::Black)) {
fill_solid(leds, NUM_LEDS, gFlashColor);
gFlashColor = CRGB::Black;
}
FastLED.show();
}
@ Red
<div style='background:#FF0000;width:4em;height:4em;'></div>
Definition crgb.h:622
@ Cyan
<div style='background:#00FFFF;width:4em;height:4em;'></div>
Definition crgb.h:523
@ Yellow
<div style='background:#FFFF00;width:4em;height:4em;'></div>
Definition crgb.h:648
@ Purple
<div style='background:#800080;width:4em;height:4em;'></div>
Definition crgb.h:621

High-Level: Polling API (No Callbacks)

If you prefer polling over callbacks, AudioProcessor also provides getter methods that return uint8_t-scaled values (0-255), perfect for direct LED control.

#include "FastLED.h"
#include "fl/audio/input.h"
using namespace fl;
#define NUM_LEDS 60
#define DATA_PIN 5
AudioProcessor audio;
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
auto config = AudioConfig::CreateInmp441(15, 32, 14, Left);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) return;
mic->start();
}
void loop() {
AudioSample sample = mic->read();
if (sample.isValid()) {
audio.update(sample);
}
// Poll values directly - all return 0-255
uint8_t bass = audio.getBassLevel();
uint8_t mid = audio.getMidLevel();
uint8_t treble = audio.getTrebleLevel();
uint8_t energy = audio.getEnergy();
// Map bass to hue, energy to brightness
fill_solid(leds, NUM_LEDS, CHSV(bass, 255, energy));
// Strobe on beat
if (audio.isBeat()) {
}
// Percussion polling
if (audio.isKick()) {
}
if (audio.isSnare()) {
}
// Tempo-aware effects
float bpm = audio.getBPM();
// Use bpm to sync animation speed
FastLED.show();
}
void bpm()

WLED-Style Equalizer (16-Bin Spectrum)

A dead-simple WLED-compatible equalizer: 16 frequency bins normalized to 0.0-1.0, plus bass/mid/treble/volume/zcf convenience getters. All values are pre-normalized — just multiply by 255 if you want bytes.

#include "FastLED.h"
#define NUM_LEDS 16
void setup() {
FastLED.addLeds<WS2812B, 2, GRB>(leds, NUM_LEDS);
auto config = fl::AudioConfig::CreateInmp441(7, 8, 4, fl::Right);
audio = FastLED.add(config);
// Callback: get everything in one struct
audio->onEqualizer([](const fl::Equalizer& eq) {
// eq.bass, eq.mid, eq.treble, eq.volume, eq.zcf — all 0.0-1.0
// eq.bins — span<const float, 16>, each 0.0-1.0
for (int i = 0; i < 16; ++i) {
uint8_t brightness = static_cast<uint8_t>(eq.bins[i] * 255);
leds[i] = CHSV(i * 16, 255, brightness);
}
});
}
void loop() {
FastLED.show();
}
fl::UISlider brightness("Brightness", BRIGHTNESS, 0, 255)

Or use polling — no callbacks needed:

void loop() {
// All return 0.0-1.0
float bass = audio->getEqBass();
float mid = audio->getEqMid();
float treble = audio->getEqTreble();
float volume = audio->getEqVolume();
float zcf = audio->getEqZcf();
float bin5 = audio->getEqBin(5); // 16 bins (0-15)
fill_solid(leds, NUM_LEDS, CHSV(bass * 160, 255, volume * 255));
FastLED.show();
}

Bin layout (WLED-compatible):

Bins Range Getter
0-3 ~60-320 Hz (bass) getEqBass()
4-10 ~320-2560 Hz (mid) getEqMid()
11-15 ~2560-5120 Hz (treble) getEqTreble()

VibeDetector: MilkDrop-Inspired Self-Normalizing Audio Analysis

VibeDetector provides self-normalizing, FPS-independent bass/mid/treb levels with asymmetric attack/decay smoothing. The algorithm is a direct port of Ryan Geiss's DoCustomSoundAnalysis() from MilkDrop v2.25c — the legendary Winamp visualizer. See MILK_DROP_AUDIO_REACTIVE.md for a detailed technical analysis of the original algorithm.

Key properties:

Understanding the Values

Vibe levels are not 0.0-1.0 like the Equalizer. They are ratios against the long-term average energy of the current song:

Value Meaning
1.0 Average level for this song/environment
> 1.0 Louder than recent average (spike/beat)
< 1.0 Quieter than recent average
~0.7 Quiet passage
~1.3 Loud passage / beat hit

The self-normalization means the same preset code works on quiet acoustic songs and loud electronic music without any gain or threshold calibration.

Two Sets of Levels: Immediate vs Smoothed

Each band has two relative levels:

The relationship between these two is the core of MilkDrop's beat detection:

Beat Detection Pattern

The canonical MilkDrop idiom for beat-reactive effects:

// In your loop:
float bass = audio->getVibeBass(); // immediate relative
float bassAtt = audio->getVibeBassAtt(); // smoothed relative
// Beat intensity: positive when a beat is hitting, zero/negative otherwise
float beatIntensity = bass - bassAtt;
// Binary beat: true/false
bool beat = bass > bassAtt;
// Or use the convenience method:
bool beat = audio->isVibeBassSpike();
// Scale an effect proportionally to the music's dynamics:
float zoom = 1.0f + 0.1f * (bass - 1.0f);
// Decay that responds to bass hits:
uint8_t decay = 240 + static_cast<uint8_t>(bass * 10);

Choosing Between Vibe and Other Band APIs

API Range Use when...
getVibeBass() ~1.0 (unbounded) You want self-normalizing, beat-reactive effects that adapt to any song
getBassLevel() 0.0-1.0 You want simple normalized levels for brightness/color mapping
getEqBass() 0.0-1.0 You want WLED-compatible spectrum analysis
getBassRaw() 0+ (absolute) You need raw FFT energy for custom algorithms

Example: Callback API

#include "FastLED.h"
#define NUM_LEDS 60
#define LED_PIN 2
// I2S pins for INMP441 microphone
#define I2S_WS 7
#define I2S_SD 8
#define I2S_CLK 4
void setup() {
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(128);
auto config = fl::AudioConfig::CreateInmp441(I2S_WS, I2S_SD, I2S_CLK, fl::Right);
audio = FastLED.add(config);
// Flash white on bass spike (rising edge only)
audio->onVibeBassSpike([] {
});
// React to all three bands every frame
audio->onVibeLevels([](const fl::VibeLevels& v) {
// v.bass/mid/treb hover around 1.0; >1 means louder than recent average
uint8_t hue = static_cast<uint8_t>(v.mid * 80);
uint8_t brightness = static_cast<uint8_t>(constrain(v.vol * 200, 0, 255));
});
}
void loop() {
FastLED.show(); // Audio is auto-pumped here
}
Snapshot of self-normalizing MilkDrop-style vibe levels.
Definition audio_batch.h:21

Polling API (Complete)

// ---- Immediate relative levels (~1.0 = average, unbounded) ----
float bass = audio->getVibeBass(); // Immediate relative bass
float mid = audio->getVibeMid(); // Immediate relative mid
float treb = audio->getVibeTreb(); // Immediate relative treble
float vol = audio->getVibeVol(); // Average of bass/mid/treb
// ---- Smoothed relative levels (for beat comparison) ----
float bassAtt = audio->getVibeBassAtt(); // Smoothed relative bass
float midAtt = audio->getVibeMidAtt(); // Smoothed relative mid
float trebAtt = audio->getVibeTrebAtt(); // Smoothed relative treble
float volAtt = audio->getVibeVolAtt(); // Average of smoothed bands
// ---- Spike detection (bass > bassAtt = beat) ----
bool bassBeat = audio->isVibeBassSpike(); // true when bass is rising
bool midBeat = audio->isVibeMidSpike(); // true when mid is rising
bool trebBeat = audio->isVibeTrebSpike(); // true when treb is rising

VibeLevels Callback Struct

The onVibeLevels callback provides everything in one struct per frame:

struct VibeLevels {
// Self-normalizing relative levels (~1.0 = average)
float bass, mid, treb; // Immediate relative
float vol; // (bass + mid + treb) / 3
// Spike detection
bool bassSpike, midSpike, trebSpike; // true when energy is rising
// Absolute values (for advanced use)
float bassRaw, midRaw, trebRaw; // Immediate absolute energy
float bassAvg, midAvg, trebAvg; // Short-term smoothed absolute
float bassLongAvg, midLongAvg, trebLongAvg; // Long-term average absolute
};

Spike Callbacks

Spike callbacks fire on the rising edge only (transition from no-spike to spike), so you get one event per beat rather than continuous firing:

audio->onVibeBassSpike([] { /* bass beat! */ });
audio->onVibeMidSpike([] { /* mid-range transient */ });
audio->onVibeTrebSpike([] { /* high-frequency hit (hi-hat, cymbal) */ });

Mid-Level: Custom Detector

Create your own detector by subclassing AudioDetector. This gives you direct access to AudioContext for FFT data while integrating into the update loop.

#include "FastLED.h"
#include "fl/audio/input.h"
using namespace fl;
// Custom detector that triggers when a specific frequency band spikes
class SparkleDetector : public AudioDetector {
public:
bool triggered = false;
float intensity = 0.0f;
// Tell the system we need FFT data
bool needsFFT() const override { return true; }
const char* getName() const override { return "SparkleDetector"; }
void update(shared_ptr<AudioContext> context) override {
// Access cached FFT (computed once, shared across all detectors)
const FFTBins& bins = context->getFFT(16);
// Check high-frequency bins (bins 12-15) for sparkle trigger
float highEnergy = 0.0f;
for (int i = 12; i < 16; i++) {
highEnergy += bins.bins_raw[i];
}
highEnergy /= 4.0f;
triggered = (highEnergy > 0.6f);
intensity = highEnergy;
}
void fireCallbacks() override {
// Called after all detectors have updated - safe to trigger effects
// (In this simple example we just set flags read in loop())
}
void reset() override {
triggered = false;
intensity = 0.0f;
}
};
bool triggered
Definition simple.h:94

Mid-Level: Multi-Detector with AudioContext

Create an AudioContext manually to share FFT data across multiple detectors. This demonstrates the two-phase update/fireCallbacks loop.

#include "FastLED.h"
#include "fl/audio/input.h"
using namespace fl;
#define NUM_LEDS 60
#define DATA_PIN 5
// Two simple detectors that share the same AudioContext
class BassMonitor : public AudioDetector {
public:
float level = 0.0f;
bool needsFFT() const override { return true; }
const char* getName() const override { return "BassMonitor"; }
void update(shared_ptr<AudioContext> context) override {
const FFTBins& bins = context->getFFT(16);
// Average first 4 bins (low frequencies)
level = 0.0f;
for (int i = 0; i < 4; i++) level += bins.bins_raw[i];
level /= 4.0f;
}
};
class TrebleMonitor : public AudioDetector {
public:
float level = 0.0f;
bool needsFFT() const override { return true; }
const char* getName() const override { return "TrebleMonitor"; }
void update(shared_ptr<AudioContext> context) override {
const FFTBins& bins = context->getFFT(16);
// Average last 4 bins (high frequencies)
level = 0.0f;
for (int i = 12; i < 16; i++) level += bins.bins_raw[i];
level /= 4.0f;
}
};
BassMonitor bassMonitor;
TrebleMonitor trebleMonitor;
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
auto config = AudioConfig::CreateInmp441(15, 32, 14, Left);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) return;
mic->start();
}
void loop() {
AudioSample sample = mic->read();
if (!sample.isValid()) return;
// Create a shared context - FFT is computed once, shared by both detectors
// Phase 1: Update all detectors (reads from context, no side effects)
bassMonitor.update(context);
trebleMonitor.update(context);
// Phase 2: Fire callbacks (safe to trigger effects now)
bassMonitor.fireCallbacks();
trebleMonitor.fireCallbacks();
// Use detector results
uint8_t bassBrightness = bassMonitor.level * 255;
uint8_t trebleBrightness = trebleMonitor.level * 255;
for (int i = 0; i < NUM_LEDS / 2; i++) {
leds[i] = CHSV(0, 255, bassBrightness); // Red for bass
}
for (int i = NUM_LEDS / 2; i < NUM_LEDS; i++) {
leds[i] = CHSV(160, 255, trebleBrightness); // Blue for treble
}
FastLED.show();
}

Low-Level: Direct FFT Access

For full control, use AudioSample and FFT directly. This is useful when you want to build your own visualizer or analysis pipeline.

#include "FastLED.h"
#include "fl/audio/input.h"
#include "fl/audio/audio.h"
using namespace fl;
#define NUM_LEDS 16
#define DATA_PIN 5
FFT fft;
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
auto config = AudioConfig::CreateInmp441(15, 32, 14, Left);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) return;
mic->start();
}
void loop() {
AudioSample sample = mic->read();
if (!sample.isValid()) return;
// Run FFT - 16 bins mapped to 16 LEDs
FFTBins bins(16);
FFT_Args args(512, 16, 174.6f, 4698.3f, 44100);
fft.run(sample.pcm(), &bins, args);
// Map FFT bins directly to LED brightness
for (int i = 0; i < NUM_LEDS; i++) {
uint8_t brightness = constrain(bins.bins_raw[i] * 255, 0, 255);
leds[i] = CHSV(i * 16, 255, brightness);
}
FastLED.show();
}
AudioAnalyzeFFT1024 fft
corkscrew_args args
Definition old.h:149

Low-Level: Raw PCM Analysis

For the simplest audio reactivity without FFT, use AudioSample properties directly.

#include "FastLED.h"
#include "fl/audio/input.h"
#include "fl/audio/audio.h"
using namespace fl;
#define NUM_LEDS 30
#define DATA_PIN 5
void setup() {
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
auto config = AudioConfig::CreateInmp441(15, 32, 14, Left);
string errorMsg;
mic = IAudioInput::create(config, &errorMsg);
if (!mic) return;
mic->start();
}
void loop() {
AudioSample sample = mic->read();
if (!sample.isValid()) return;
float rms = sample.rms(); // Volume level (RMS amplitude)
float zcf = sample.zcf(); // Zero-crossing factor (0.0 - 1.0)
// High ZCF (> 0.4) = hissing/noise, low ZCF + high RMS = music
bool isMusic = (zcf < 0.35f) && (rms > 500.0f);
// Map RMS to LED count (simple VU meter)
int litCount = map(constrain(rms, 0, 10000), 0, 10000, 0, NUM_LEDS);
for (int i = 0; i < litCount; i++) {
// Green -> Yellow -> Red gradient
leds[i] = CHSV(96 - (i * 96 / NUM_LEDS), 255, isMusic ? 255 : 64);
}
FastLED.show();
}
float rms(fl::span< const int16_t > data)
Definition simple.h:104
MapRedBlackTree< Key, T, Compare, fl::allocator_slab< char > > map
Definition map.h:283

Synthesizer: Generating Audio Output

The Synth module generates bandlimited waveforms for audio output. Useful for creating tones, alerts, or musical output from your microcontroller.

#include "FastLED.h"
#include "fl/audio/synth.h"
using namespace fl;
void setup() {
Serial.begin(115200);
// Create a shared engine (holds anti-aliasing tables)
auto engine = ISynthEngine::create(32, 16);
// Create oscillators
auto saw = ISynthOscillator::create(engine, SynthShape::Sawtooth);
auto sq = ISynthOscillator::create(engine, SynthShape::Square);
auto tri = ISynthOscillator::create(engine, SynthShape::Triangle);
// Generate 256 samples of a 440 Hz tone at 44.1 kHz
float buffer[256];
float freq = 440.0f / 44100.0f; // Normalized frequency
saw->generateSamples(buffer, 256, freq);
// Custom waveform via SynthParams(reflect, peakTime, halfHeight, zeroWait)
SynthParams custom(1, 0.3f, 0.5f, 0.1f);
auto osc = ISynthOscillator::create(engine, custom);
osc->generateSamples(buffer, 256, freq);
}
void loop() {
// Synth runs in setup for this demo
}
Bandlimited audio synthesizer - waveform oscillator module.
#define Serial
Definition serial.h:304

API Layers

┌──────────────────────────────────────────────┐
│ Your Sketch (.ino) │
├──────────────────────────────────────────────┤
│ FastLED.add(AudioConfig) (integration) │ ← Easiest
│ auto-pump via fl::task::every_ms(1) │
│ returns shared_ptr<AudioProcessor> │
├──────────────────────────────────────────────┤
│ AudioProcessor (high-level facade) │ ← Manual control
│ callbacks: onBeat(), onBass(), ... │
│ polling: isBeat(), getBassLevel(), ... │
├──────────────────────────────────────────────┤
│ AudioContext (shared FFT cache) │ ← Intermediate
│ AudioDetector (base class) │
│ Detectors: Beat, Vocal, Percussion, ... │
├──────────────────────────────────────────────┤
│ AudioSample (PCM + RMS + ZCF) │ ← Low-level
│ FFT / FFTBins (spectrum analysis) │
│ SoundLevelMeter (dBFS / SPL calibration) │
├──────────────────────────────────────────────┤
│ IAudioInput (hardware abstraction) │ ← Platform
│ I2S, PDM, Teensy Audio Library │
└──────────────────────────────────────────────┘

Choosing Your Level

Level Use when... Key classes
Easiest (FastLED.add) You want audio-reactive LEDs with zero boilerplate FastLED.add(AudioConfig)
High (AudioProcessor) You want manual control over when samples are processed AudioProcessor
Mid (AudioContext) You're writing a custom detector or need shared FFT caching AudioContext, AudioDetector
Low (AudioSample/FFT) You want raw spectrum data or PCM-level control AudioSample, FFT, FFTBins
Output (Synth) You need to generate audio waveforms ISynthEngine, ISynthOscillator

How It Works

Update Loop (3-Stage Pipeline)

When you call audio.update(sample), three stages run in sequence:

  1. Signal Conditioning — Raw PCM is cleaned: DC offset removal, spike filtering, noise gate. This stage modifies the sample before any analysis.
  2. Detector Update — Each active detector's update(context) is called with a shared AudioContext. Detectors read FFT/PCM data and compute their internal state, but do not fire callbacks yet. The FFT is computed lazily on first access and cached — if three detectors all call context->getFFT(16), the FFT runs only once.
  3. Callback Firing — After all detectors have finished updating, each detector's fireCallbacks() is called. This two-phase design prevents callback code from interfering with other detectors' analysis within the same frame.

Lazy Creation

Detectors are created only when you register a callback or call a polling getter. If you only use onBeat() and getBassLevel(), only BeatDetector and FrequencyBands are instantiated. The rest consume zero memory.

FFT Caching

AudioContext caches FFT results per frame. Multiple detectors requesting the same FFT parameters share a single computation. This is why the mid-level API passes a shared_ptr<AudioContext> — it's the shared cache.


AudioProcessor Event Reference

Callbacks

Category Callbacks
Beat onBeat(void()), onBeatPhase(float), onOnset(float), onTempoChange(float, float)
Tempo onTempo(float), onTempoWithConfidence(float, float), onTempoStable(), onTempoUnstable()
Frequency onBass(float), onMid(float), onTreble(float), onFrequencyBands(float, float, float)
Energy onEnergy(float), onNormalizedEnergy(float), onPeak(float), onAverageEnergy(float)
Transient onTransient(), onTransientWithStrength(float), onAttack(float)
Silence onSilence(u8), onSilenceStart(), onSilenceEnd(), onSilenceDuration(u32)
Pitch onPitch(float), onPitchWithConfidence(float, float), onPitchChange(float), onVoiced(u8)
Note onNoteOn(u8, u8), onNoteOff(u8), onNoteChange(u8, u8)
Percussion onPercussion(PercussionType), onKick(), onSnare(), onHiHat(), onTom()
Vocal onVocal(u8), onVocalStart(), onVocalEnd(), onVocalConfidence(float)
Dynamics onCrescendo(), onDiminuendo(), onDynamicTrend(float), onCompressionRatio(float)
Downbeat onDownbeat(), onMeasureBeat(u8), onMeterChange(u8), onMeasurePhase(float)
Backbeat onBackbeat(u8, float, float)
Chord onChord(Chord), onChordChange(Chord), onChordEnd()
Key onKey(Key), onKeyChange(Key), onKeyEnd()
Mood onMood(Mood), onMoodChange(Mood), onValenceArousal(float, float)
Buildup onBuildupStart(), onBuildupProgress(float), onBuildupPeak(), onBuildupEnd(), onBuildup(Buildup)
Drop onDrop(), onDropEvent(Drop), onDropImpact(float)
Equalizer onEqualizer(const Equalizer&)

Polling Getters

// Beat Detection
audio.isBeat(); // u8 - Beat detected this frame?
audio.getBeatConfidence(); // u8 - Beat confidence 0-255
audio.getBPM(); // float - Estimated BPM
// Frequency Bands (0-255)
audio.getBassLevel();
audio.getMidLevel();
audio.getTrebleLevel();
// Energy
audio.getEnergy(); // u8 - Overall energy 0-255
audio.getPeakLevel(); // u8 - Peak level 0-255
// Percussion
audio.isKick(); // u8 - Kick drum detected?
audio.isSnare(); // u8 - Snare detected?
audio.isHiHat(); // u8 - Hi-hat detected?
audio.isTom(); // u8 - Tom detected?
// Vocal
audio.isVocalActive(); // u8 - Vocals present?
audio.getVocalConfidence(); // u8 - Vocal confidence 0-255
// Silence
audio.isSilent(); // u8 - Silence detected?
audio.getSilenceDuration(); // u32 - Silence duration in ms
// Transient
audio.isTransient(); // u8 - Transient detected?
audio.getTransientStrength();// u8 - Transient strength 0-255
// Dynamics
audio.isCrescendo(); // u8 - Volume increasing?
audio.isDiminuendo(); // u8 - Volume decreasing?
audio.getDynamicTrend(); // u8 - Trend direction 0-255
// Pitch
audio.getPitch(); // float - Pitch in Hz
audio.getPitchConfidence(); // u8 - Confidence 0-255
audio.isVoiced(); // u8 - Pitched sound detected?
// Tempo
audio.getTempoBPM(); // float - Tempo in BPM
audio.getTempoConfidence(); // u8 - Tempo confidence 0-255
audio.isTempoStable(); // u8 - Tempo locked?
// Note
audio.getCurrentNote(); // u8 - MIDI note number
audio.getNoteVelocity(); // u8 - Note velocity 0-255
audio.isNoteActive(); // u8 - Note currently playing?
// Downbeat / Structure
audio.isDownbeat(); // u8 - Downbeat this frame?
audio.getCurrentBeatNumber();// u8 - Beat within measure
audio.getMeasurePhase(); // u8 - Phase through measure 0-255
// Backbeat
audio.getBackbeatConfidence(); // u8 - Backbeat confidence 0-255
audio.getBackbeatStrength(); // u8 - Backbeat strength 0-255
// Buildup / Drop (EDM)
audio.isBuilding(); // u8 - Buildup in progress?
audio.getBuildupProgress(); // u8 - Buildup progress 0-255
audio.getBuildupIntensity(); // u8 - Buildup intensity 0-255
audio.getDropImpact(); // u8 - Drop impact 0-255
// Chord / Key / Mood
audio.hasChord(); // u8 - Chord detected?
audio.getChordConfidence(); // u8 - Chord confidence 0-255
audio.hasKey(); // u8 - Key detected?
audio.getKeyConfidence(); // u8 - Key confidence 0-255
audio.getMoodValence(); // u8 - Happy/sad 0-255
audio.getMoodArousal(); // u8 - Calm/energetic 0-255
// Equalizer (WLED-style, all 0.0-1.0)
audio.getEqBass(); // float - Bass level 0.0-1.0
audio.getEqMid(); // float - Mid level 0.0-1.0
audio.getEqTreble(); // float - Treble level 0.0-1.0
audio.getEqVolume(); // float - Volume 0.0-1.0 (AGC-normalized)
audio.getEqZcf(); // float - Zero-crossing factor 0.0-1.0
audio.getEqBin(0); // float - Bin 0 level 0.0-1.0 (16 bins total)

Writing a Custom Detector

To create your own detector that integrates with AudioProcessor:

Step 1: Subclass AudioDetector

class MyDetector : public fl::AudioDetector {
public:
// Declare FFT needs (return true if your detector uses frequency data)
bool needsFFT() const override { return true; }
// Return true if you need multiple frames of FFT history
bool needsFFTHistory() const override { return false; }
// Unique name for debugging
const char* getName() const override { return "MyDetector"; }
// Phase 1: Read from context, compute internal state
// Do NOT fire callbacks or cause side effects here
void update(fl::shared_ptr<fl::AudioContext> context) override {
const fl::FFTBins& bins = context->getFFT(16);
// ... analyze bins, update internal state ...
}
// Phase 2: Fire callbacks, set flags, trigger effects
// Called after ALL detectors have updated
void fireCallbacks() override {
// ... notify listeners ...
}
// Optional: handle sample rate changes
void setSampleRate(int rate) override { mSampleRate = rate; }
// Optional: reset state
void reset() override { /* ... */ }
private:
int mSampleRate = 44100;
};

Step 2: Use with AudioContext

Wire your detector into a manual update loop (see the Multi-Detector example above), or use it standalone:

myDetector.update(context);
myDetector.fireCallbacks();

Signal Conditioning

AudioProcessor includes a three-stage signal conditioning pipeline. Each stage can be enabled/disabled independently.

Enable/Disable

audio.setSignalConditioningEnabled(true); // DC removal, spike filter, noise gate
audio.setAutoGainEnabled(true); // Automatic gain control
audio.setNoiseFloorTrackingEnabled(true); // Adaptive noise floor

Configuration Structs

SignalConditionerConfig — Cleans raw I2S/PCM data:

Field Default Description
enableDCRemoval true Remove DC offset via running-average high-pass filter
enableSpikeFilter true Reject I2S glitch samples beyond threshold
enableNoiseGate true Hysteresis-based noise gate
spikeThreshold 10000 Absolute sample value beyond which samples are rejected
noiseGateOpenThreshold 500 Signal must exceed this to open the gate
noiseGateCloseThreshold 300 Signal must fall below this to close the gate
dcRemovalAlpha 0.99f Time constant (higher = slower DC adaptation)

AutoGainConfig — Adaptive gain using PI controller with peak envelope tracking (WLED-style):

Field Default Description
preset AGCPreset_Normal Behavior preset: Normal, Vivid, Lazy, or Custom
minGain 1/64 Minimum gain multiplier
maxGain 32.0f Maximum gain multiplier
targetRMSLevel 8000.0f Target RMS level after gain (0-32767)
peakDecayTau 3.3f Peak envelope decay (seconds, Custom only)
kp 0.6f PI proportional gain (Custom only)
ki 1.7f PI integral gain (Custom only)
gainFollowSlowTau 12.3f Slow gain-follow tau (seconds, Custom only)
gainFollowFastTau 0.38f Fast gain-follow tau (seconds, Custom only)

AGC Presets:

Parameter Normal Vivid Lazy
peakDecayTau 3.3s 1.3s 6.7s
kp 0.6 1.5 0.65
ki 1.7 1.85 1.2
gainFollowSlowTau 12.3s 8.2s 16.4s
gainFollowFastTau 0.38s 0.26s 0.51s

NoiseFloorTrackerConfig — Adaptive noise floor with hysteresis:

Field Default Description
decayRate 0.99f How slowly the floor decays (higher = more stable)
attackRate 0.001f How quickly the floor rises when signal is low
hysteresisMargin 100.0f Floor must drop by this before it can rise again
minFloor 10.0f Prevents floor from reaching zero
maxFloor 5000.0f Prevents floor from growing unbounded
crossDomainWeight 0.3f Blend of time-domain (0.0) vs frequency-domain (1.0)

Tuning Example

// Noisy environment: raise thresholds, slower adaptation
SignalConditionerConfig scConfig;
scConfig.spikeThreshold = 15000;
scConfig.noiseGateOpenThreshold = 1000;
scConfig.noiseGateCloseThreshold = 700;
audio.configureSignalConditioner(scConfig);
// Quiet venue: vivid preset for faster adaptation
AutoGainConfig agcConfig;
agcConfig.preset = AGCPreset_Vivid;
agcConfig.targetRMSLevel = 12000.0f;
audio.configureAutoGain(agcConfig);
// Outdoor use: wide hysteresis to handle wind noise
NoiseFloorTrackerConfig nfConfig;
nfConfig.hysteresisMargin = 300.0f;
nfConfig.decayRate = 0.995f;
audio.configureNoiseFloorTracker(nfConfig);

Detector Quick Reference

Detector FFT? History? Key Callbacks Key Polling
BeatDetector Yes No onBeat(), onOnset(float) isBeat(), getBeatConfidence()
TempoAnalyzer No No onTempo(float), onTempoStable() getTempoBPM(), isTempoStable()
EqualizerDetector Yes No onEqualizer(const Equalizer&) getEqBass(), getEqMid(), getEqTreble(), getEqVolume(), getEqZcf(), getEqBin(int)
FrequencyBands Yes No onBass(float), onMid(float), onTreble(float) getBassLevel(), getMidLevel(), getTrebleLevel()
EnergyAnalyzer No No onEnergy(float), onPeak(float) getEnergy(), getPeakLevel()
TransientDetector Yes No onTransient(), onAttack(float) isTransient(), getTransientStrength()
SilenceDetector No No onSilenceStart(), onSilenceEnd() isSilent(), getSilenceDuration()
DynamicsAnalyzer No No onCrescendo(), onDiminuendo() isCrescendo(), isDiminuendo()
PitchDetector Yes No onPitch(float), onVoiced(u8) getPitch(), isVoiced()
NoteDetector Yes No onNoteOn(u8, u8), onNoteOff(u8) getCurrentNote(), isNoteActive()
DownbeatDetector No No onDownbeat(), onMeasureBeat(u8) isDownbeat(), getCurrentBeatNumber()
BackbeatDetector No No onBackbeat(u8, float, float) getBackbeatConfidence()
VocalDetector Yes No onVocalStart(), onVocalEnd() isVocalActive(), getVocalConfidence()
PercussionDetector Yes No onKick(), onSnare(), onHiHat(), onTom() isKick(), isSnare(), isHiHat(), isTom()
ChordDetector Yes Yes onChord(Chord), onChordChange(Chord) hasChord(), getChordConfidence()
KeyDetector Yes Yes onKey(Key), onKeyChange(Key) hasKey(), getKeyConfidence()
MoodAnalyzer Yes Yes onMood(Mood), onValenceArousal(float, float) getMoodValence(), getMoodArousal()
BuildupDetector Yes No onBuildupStart(), onBuildupPeak() isBuilding(), getBuildupProgress()
DropDetector Yes No onDrop(), onDropImpact(float) getDropImpact()
VibeDetector Yes No onVibeLevels(const VibeLevels&), onVibeBassSpike(), onVibeMidSpike(), onVibeTrebSpike() getVibeBass(), getVibeMid(), getVibeTreb(), getVibeVol(), isVibeBassSpike()

Platform Support

Platform Microphone Configuration
ESP32 INMP441 (I2S) AudioConfig::CreateInmp441(ws, sd, clk, channel)
ESP32 PDM mic AudioConfig(AudioConfigPdm(din, clk, i2s_num))
Teensy I2S mic AudioConfig::CreateTeensyI2S(port, channel)

File Structure

fl/audio/
├── README.md # This file
├── input.h # AudioConfig, IAudioInput (hardware abstraction)
├── audio_context.h/.cpp.hpp # Shared FFT cache (lazy evaluation)
├── audio_detector.h # Base class for all detectors
├── audio_processor.h/.cpp.hpp # High-level facade (callbacks + polling + auto-pump)
├── synth.h/.cpp.hpp # Bandlimited waveform synthesizer
├── auto_gain.h/.cpp.hpp # Automatic gain control
├── signal_conditioner.h/.cpp.hpp # Signal conditioning pipeline
├── noise_floor_tracker.h/.cpp.hpp # Adaptive noise floor
├── frequency_bin_mapper.h/.cpp.hpp # FFT bin frequency mapping
├── spectral_equalizer.h/.cpp.hpp # Spectral equalization
└── detectors/ # All detector implementations
├── beat.h # Beat detection
├── vocal.h # Vocal presence detection
├── percussion.h # Kick/snare/hihat/tom detection
├── pitch.h # Pitch estimation
├── note.h # MIDI-style note detection
├── chord.h # Chord recognition
├── key.h # Musical key detection
├── mood_analyzer.h # Valence/arousal analysis
├── tempo_analyzer.h # BPM estimation
├── downbeat.h # Downbeat / measure tracking
├── backbeat.h # Backbeat detection
├── buildup.h # Buildup detection (EDM)
├── drop.h # Drop detection (EDM)
├── vibe.h/.cpp.hpp # MilkDrop-inspired self-normalizing audio analysis
├── transient.h # Transient / attack detection
├── silence.h # Silence detection
├── dynamics_analyzer.h # Crescendo / diminuendo
├── energy_analyzer.h # RMS energy tracking
├── equalizer.h # WLED-style 16-bin equalizer (0.0-1.0)
└── frequency_bands.h # Bass/mid/treble splitting