FastLED 3.10.8
Loading...
Searching...
No Matches
vibe.cpp.hpp
Go to the documentation of this file.
1// Vibe - Self-normalizing audio analysis
2//
3// Algorithm ported from MilkDrop v2.25c by Ryan Geiss.
4// Three-stage processing: immediate fft::FFT → asymmetric EMA → slow EMA normalization.
5//
6// Uses 3 LINEAR fft::FFT bins that map directly to bass/mid/treb bands.
7// Linear bins at 20-11025 Hz with 3 bins gives ~3668 Hz per bin:
8// bin 0: 20-3688 Hz (bass), bin 1: 3688-7356 Hz (mid), bin 2: 7356-11025 Hz (treb)
9
12#include "fl/math/math.h"
13#include "fl/stl/noexcept.h"
14
15namespace fl {
16namespace audio {
17namespace detector {
18
19// Diagnostic FFT counter. Deliberately a plain constant-initialized static,
20// NOT a fl::Singleton -- see FastLED#3486. Builds pass -fdata-sections
21// (ci/boards.py), so this lands in its own `.bss` section and --gc-sections
22// already drops it in sketches that never link Vibe. Wrapping it in a
23// Singleton would add a second word for the instance pointer, a lazy-init
24// branch at every access, and -- because Singleton<T>::instance() hand-rolls
25// `if (!ptr)` instead of using a magic static -- an init race between the
26// audio task and loop() on multi-core targets.
27// FL_LINT_ALLOW_GLOBAL(constant-initialized POD scalar -- already elidable under -fdata-sections; Singleton wrapping is a net size regression and loses constant-initialization. See FastLED#3486.)
28static int sVibeFFTCount = 0;
31
32
34 // Tau chosen for LED visualizer feel: ~0.3s gives ~1 second of silence
35 // to fully gate, matching user expectation that beats decay quickly when
36 // music stops. The envelope targets 0.0 — full silence on the metric.
38 cfg.decayTauSeconds = 0.3f;
39 cfg.targetValue = 0.0f;
40 for (int i = 0; i < 3; ++i) {
41 mImmRelEnv[i].configure(cfg);
42 mAvgRelEnv[i].configure(cfg);
43 // Seed cached "last audio" value at 1.0 to match the detector's
44 // initial public state (getBass()/getMid()/getTreb() return 1.0
45 // before any update()). Without this, the first silent update
46 // would immediately decay from 0 — no visible change, but semantic.
47 mImmRelEnv[i].reset(1.0f);
48 mAvgRelEnv[i].reset(1.0f);
49 }
50}
51
52Vibe::~Vibe() FL_NO_EXCEPT = default;
53
55 if (!context) {
56 return;
57 }
58
60 mSampleRate = context->getSampleRate();
61
62 // --- Step 1: Get 3-band energy from shared context ---
63 BandEnergy energy = context->getBandEnergy();
65
66 span<const i16> pcm = context->getPCM();
67
68 // --- Step 2: Read bass/mid/treb directly ---
69 mImm[0] = energy.bass;
70 mImm[1] = energy.mid;
71 mImm[2] = energy.treb;
72
73 // --- Step 3: Temporal blending (MilkDrop v2.25c algorithm) ---
74 // Compute effective FPS from audio buffer duration
75 float dt = computeAudioDt(pcm.size(), mSampleRate);
76 float actualFps = (dt > 0.0f) ? (1.0f / dt) : mTargetFps;
77
78 if (mFrameCount == 1) {
79 // MilkDrop first-frame initialization: set averages directly
80 // Prevents false spikes and startup transients
81 for (int i = 0; i < 3; i++) {
82 mAvg[i] = mImm[i];
83 mLongAvg[i] = mImm[i];
84 }
85 } else {
86 // Pre-compute FPS-adjusted rates (avoids redundant powf in loop)
87 float attackRate = adjustRateToFPS(0.2f, 30.0f, actualFps);
88 float decayRate = adjustRateToFPS(0.5f, 30.0f, actualFps);
89 float longRate = adjustRateToFPS(0.992f, 30.0f, actualFps);
90
91 for (int i = 0; i < 3; i++) {
92 // Short-term average: asymmetric attack/decay
93 // Fast attack (rate=0.2 at 30fps → ~80% new signal on beats)
94 // Slow decay (rate=0.5 at 30fps → graceful fadeout)
95 float rate = (mImm[i] > mAvg[i]) ? attackRate : decayRate;
96 mAvg[i] = mAvg[i] * rate + mImm[i] * (1.0f - rate);
97
98 // Long-term average: slow symmetric EMA (MilkDrop v2.25c algorithm)
99 // Rate = 0.992 at 30fps → tau ≈ 4.2 seconds
100 // Tracks the overall energy level for self-normalization.
101 // Unlike a running maximum, this centers relative levels around 1.0,
102 // giving beats proper excursions above 1.0 and quiet sections below 1.0.
103 mLongAvg[i] = mLongAvg[i] * longRate + mAvg[i] * (1.0f - longRate);
104 }
105 }
106
107 // --- Step 4: Self-normalizing relative levels ---
108 // Division by long-term average makes levels independent of volume/genre.
109 // Values hover around 1.0; >1 means louder than average, <1 means quieter.
110 for (int i = 0; i < 3; i++) {
111 if (mLongAvg[i] < 0.001f) {
112 mImmRel[i] = 1.0f;
113 mAvgRel[i] = 1.0f;
114 } else {
115 mImmRel[i] = mImm[i] / mLongAvg[i];
116 mAvgRel[i] = mAvg[i] / mLongAvg[i];
117 }
118 }
119
120 // --- Step 4b: Silence gate ---
121 // MilkDrop's self-normalization drives mImmRel toward 1.0 when input is
122 // silent (noise / noise → 1.0), and the < 0.001f clamp above also pins
123 // to 1.0. Neither is what LED effects want when music stops. Route each
124 // band through its SilenceEnvelope: pass-through during audio, exponential
125 // decay toward 0 during silence. The Context::isSilent() flag is populated
126 // by Processor/Reactive from NoiseFloorTracker. Re-uses dt computed above.
127 const bool silent = context->isSilent();
128 for (int i = 0; i < 3; i++) {
129 mImmRel[i] = mImmRelEnv[i].update(silent, mImmRel[i], dt);
130 mAvgRel[i] = mAvgRelEnv[i].update(silent, mAvgRel[i], dt);
131 }
132
133 // --- Step 5: Spike detection ---
134 // When immediate exceeds smoothed, energy is rising — a beat is in progress.
138
139 mBassSpike = mImmRel[0] > mAvgRel[0];
140 mMidSpike = mImmRel[1] > mAvgRel[1];
141 mTrebSpike = mImmRel[2] > mAvgRel[2];
142}
143
145 if (onVibeLevels) {
146 VibeLevels levels;
147 levels.bass = mImmRel[0];
148 levels.mid = mImmRel[1];
149 levels.treb = mImmRel[2];
150 levels.vol = (mImmRel[0] + mImmRel[1] + mImmRel[2]) / 3.0f;
151 levels.bassSpike = mBassSpike;
152 levels.midSpike = mMidSpike;
153 levels.trebSpike = mTrebSpike;
154 levels.bassRaw = mImm[0];
155 levels.midRaw = mImm[1];
156 levels.trebRaw = mImm[2];
157 levels.bassAvg = mAvg[0];
158 levels.midAvg = mAvg[1];
159 levels.trebAvg = mAvg[2];
160 levels.bassLongAvg = mLongAvg[0];
161 levels.midLongAvg = mLongAvg[1];
162 levels.trebLongAvg = mLongAvg[2];
163 onVibeLevels(levels);
164 }
165
166 // Fire spike callbacks on rising edge (transition from no-spike to spike)
168 onBassSpike();
169 }
171 onMidSpike();
172 }
174 onTrebSpike();
175 }
176}
177
179 mFrameCount = 0;
180 for (int i = 0; i < 3; i++) {
181 mImm[i] = 0.0f;
182 mAvg[i] = 0.0f;
183 mLongAvg[i] = 0.0f;
184 mImmRel[i] = 1.0f;
185 mAvgRel[i] = 1.0f;
186 // Match the mImmRel/mAvgRel defaults of 1.0 so the silence gate
187 // picks up from the public state after reset().
188 mImmRelEnv[i].reset(1.0f);
189 mAvgRelEnv[i].reset(1.0f);
190 }
191 mBassSpike = false;
192 mMidSpike = false;
193 mTrebSpike = false;
194 mPrevBassSpike = false;
195 mPrevMidSpike = false;
196 mPrevTrebSpike = false;
197}
198
199// FPS-independent rate adjustment:
200// Converts a per-frame smoothing rate tuned at fps1 to the equivalent rate
201// at actualFps, preserving the per-second behavior.
202//
203// At 30fps with rate=0.5: per-second retention = 0.5^30 ≈ 9.3e-10
204// At 60fps the per-frame rate adjusts to ~0.707 so 0.707^60 ≈ 9.3e-10 (same)
205float Vibe::adjustRateToFPS(float rateAtFps1, float fps1,
206 float actualFps) FL_NO_EXCEPT {
207 if (actualFps <= 0.0f) {
208 return rateAtFps1;
209 }
210 float perSecond = fl::powf(rateAtFps1, fps1);
211 return fl::powf(perSecond, 1.0f / actualFps);
212}
213
214} // namespace detector
215} // namespace audio
216} // namespace fl
static float adjustRateToFPS(float rateAtFps1, float fps1, float actualFps) FL_NO_EXCEPT
Definition vibe.cpp.hpp:205
static int getPrivateFFTCount() FL_NO_EXCEPT
Definition vibe.cpp.hpp:29
~Vibe() FL_NO_EXCEPT override
function_list< void(const VibeLevels &)> onVibeLevels
Definition vibe.h:128
SilenceEnvelope mImmRelEnv[3]
Definition vibe.h:170
void fireCallbacks() FL_NO_EXCEPT override
Definition vibe.cpp.hpp:144
function_list< void()> onMidSpike
Definition vibe.h:131
function_list< void()> onBassSpike
Definition vibe.h:130
static void resetPrivateFFTCount() FL_NO_EXCEPT
Definition vibe.cpp.hpp:30
void reset() FL_NO_EXCEPT override
Definition vibe.cpp.hpp:178
SilenceEnvelope mAvgRelEnv[3]
Definition vibe.h:171
function_list< void()> onTrebSpike
Definition vibe.h:132
void update(shared_ptr< Context > context) FL_NO_EXCEPT override
Definition vibe.cpp.hpp:54
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
static int sVibeFFTCount
Definition vibe.cpp.hpp:28
float computeAudioDt(fl::size pcmSize, int sampleRate) FL_NO_EXCEPT
Compute the time delta (in seconds) for an audio buffer.
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
float powf(float base, float exponent) FL_NO_EXCEPT
Definition math.h:444
Base definition for an LED controller.
Definition crgb.hpp:179