FastLED 3.9.15
Loading...
Searching...
No Matches
vibe.h
Go to the documentation of this file.
1// Vibe - Self-normalizing audio-reactive analysis for FastLED
2//
3// Produces self-normalizing, FPS-independent bass/mid/treb levels with
4// asymmetric attack/decay smoothing. Algorithm ported from Ryan Geiss's
5// MilkDrop v2.25c visualizer's DoCustomSoundAnalysis() function.
6//
7// CORRECTED ALGORITHM (as of 2026-03-07):
8// Now uses slow symmetric EMA for long-term normalization, matching the
9// MilkDrop v2.25c algorithm: long_avg = long_avg*0.992 + avg*0.008 at 30fps.
10// First frame initializes all averages directly (no smoothing lag), preventing
11// false spikes and startup transients.
12//
13// Key properties:
14// - Self-normalizing: relative levels center around 1.0 using slow EMA
15// - Asymmetric smoothing: fast attack (beats hit hard), slow decay (graceful fade)
16// - Dual timescale: short-term smoothing for beat tracking, slow EMA for normalization
17// - FPS-independent: identical behavior at 30fps, 60fps, or 144fps
18// - Spike detection: bass > bassAtt indicates a beat is happening (energy rising)
19// - First-frame init: no false spikes on startup (matches MilkDrop)
20//
21// Usage:
22// Vibe vibe;
23// vibe.onBassSpike.add([]() { /* beat! */ });
24// // In update loop:
25// vibe.update(context);
26// float zoom = 1.0f + 0.1f * (vibe.getBass() - 1.0f);
27
28#pragma once
29
30// NOTE (#2328): The defensive `#ifndef FL_AUDIO_DETECTOR_VIBE_H` guard that
31// was added in #2325 has been removed. It existed because Meson was emitting
32// -I flags with mixed spellings (backslash vs forward-slash, relative vs
33// absolute) on Windows, which defeated clang's #pragma once file-identity
34// check. All `-I` flags are now absolute + forward-slash + spelled
35// identically (see ci/meson/shared/meson.build path_norm_*), so #pragma once
36// is sufficient on its own.
37
39#include "fl/audio/fft/fft.h"
41#include "fl/stl/function.h"
42#include "fl/stl/shared_ptr.h"
43#include "fl/stl/noexcept.h"
44
45namespace fl {
46namespace audio {
47namespace detector {
48
49struct VibeLevels {
50 // Self-normalizing relative levels (~1.0 = average for current song)
51 float bass = 1.0f;
52 float mid = 1.0f;
53 float treb = 1.0f;
54 float vol = 1.0f; // (bass + mid + treb) / 3
55
56 // Spike detection (true when energy is rising — a beat/transient)
57 bool bassSpike = false;
58 bool midSpike = false;
59 bool trebSpike = false;
60
61 // Absolute values (non-normalized)
62 float bassRaw = 0.0f; // Immediate absolute band energy
63 float midRaw = 0.0f;
64 float trebRaw = 0.0f;
65 float bassAvg = 0.0f; // Short-term smoothed absolute
66 float midAvg = 0.0f;
67 float trebAvg = 0.0f;
68 float bassLongAvg = 0.0f; // Long-term average absolute
69 float midLongAvg = 0.0f;
70 float trebLongAvg = 0.0f;
71};
72
73class Vibe : public Detector {
74public:
76 ~Vibe() FL_NOEXCEPT override;
77
78 // Detector interface
79 void update(shared_ptr<Context> context) FL_NOEXCEPT override;
80 void fireCallbacks() FL_NOEXCEPT override;
81 bool needsFFT() const FL_NOEXCEPT override { return true; }
82 const char* getName() const FL_NOEXCEPT override { return "Vibe"; }
83 void reset() FL_NOEXCEPT override;
84 void setSampleRate(int sampleRate) FL_NOEXCEPT override { mSampleRate = sampleRate; }
85
86 // ---- Self-normalizing relative levels (the primary API) ----
87 // These hover around 1.0 for the current song. >1 = louder than average,
88 // <1 = quieter.
89
90 float getBass() const FL_NOEXCEPT { return mImmRel[0]; }
91 float getMid() const FL_NOEXCEPT { return mImmRel[1]; }
92 float getTreb() const FL_NOEXCEPT { return mImmRel[2]; }
93 float getVol() const FL_NOEXCEPT { return (mImmRel[0] + mImmRel[1] + mImmRel[2]) / 3.0f; }
94
95 // ---- Smoothed relative levels ("attenuated") ----
96 // Short-term smoothed versions. When bass > bassAtt, a beat is happening.
97
98 float getBassAtt() const FL_NOEXCEPT { return mAvgRel[0]; }
99 float getMidAtt() const FL_NOEXCEPT { return mAvgRel[1]; }
100 float getTrebAtt() const FL_NOEXCEPT { return mAvgRel[2]; }
101 float getVolAtt() const FL_NOEXCEPT { return (mAvgRel[0] + mAvgRel[1] + mAvgRel[2]) / 3.0f; }
102
103 // ---- Spike detection ----
104 // True when the immediate relative level exceeds the smoothed level,
105 // meaning energy is rising — a transient/beat is in progress.
106
107 bool isBassSpike() const FL_NOEXCEPT { return mBassSpike; }
108 bool isMidSpike() const FL_NOEXCEPT { return mMidSpike; }
109 bool isTrebSpike() const FL_NOEXCEPT { return mTrebSpike; }
110
111 // ---- Raw absolute values (for advanced use) ----
112
113 float getBassRaw() const FL_NOEXCEPT { return mImm[0]; }
114 float getMidRaw() const FL_NOEXCEPT { return mImm[1]; }
115 float getTrebRaw() const FL_NOEXCEPT { return mImm[2]; }
116
117 float getBassAvg() const FL_NOEXCEPT { return mAvg[0]; }
118 float getMidAvg() const FL_NOEXCEPT { return mAvg[1]; }
119 float getTrebAvg() const FL_NOEXCEPT { return mAvg[2]; }
120
121 float getBassLongAvg() const FL_NOEXCEPT { return mLongAvg[0]; }
122 float getMidLongAvg() const FL_NOEXCEPT { return mLongAvg[1]; }
123 float getTrebLongAvg() const FL_NOEXCEPT { return mLongAvg[2]; }
124
125 // ---- Callbacks ----
126
127 // Fired every frame with comprehensive vibe levels
128 function_list<void(const VibeLevels&)> onVibeLevels;
129 // Fired when a spike is detected in each band (rising edge only)
130 function_list<void()> onBassSpike;
131 function_list<void()> onMidSpike;
132 function_list<void()> onTrebSpike;
133
134 // ---- Configuration ----
135
136 // Target FPS for rate adjustment (default 30.0).
137 // Only needed if the audio update rate doesn't match the actual frame rate.
138 void setTargetFps(float fps) FL_NOEXCEPT { mTargetFps = fps; }
139
140 // Diagnostic counters
141 static int getPrivateFFTCount() FL_NOEXCEPT;
142 static void resetPrivateFFTCount() FL_NOEXCEPT;
143
144private:
145 int mSampleRate = 44100;
146 int mFrameCount = 0;
147 float mTargetFps = 30.0f;
148
149 // Band energy data
150 float mImm[3] = {}; // Immediate band energy (absolute)
151 float mAvg[3] = {}; // Short-term smoothed (absolute)
152 // Long-term slow symmetric EMA (MilkDrop v2.25c algorithm)
153 // Rate = 0.992 at 30fps (tau ≈ 4.2 seconds), tracks average level for normalization
154 float mLongAvg[3] = {}; // Long-term average for self-normalization
155 float mImmRel[3] = {1.0f, 1.0f, 1.0f}; // Immediate relative to song
156 float mAvgRel[3] = {1.0f, 1.0f, 1.0f}; // Smoothed relative to song
157
158 // Beat detection state
159 bool mBassSpike = false;
160 bool mMidSpike = false;
161 bool mTrebSpike = false;
162 bool mPrevBassSpike = false;
163 bool mPrevMidSpike = false;
164 bool mPrevTrebSpike = false;
165
166 // Silence gate envelopes — decay mImmRel[i] and mAvgRel[i] toward zero
167 // when Context::isSilent() is true. Fixes MilkDrop self-normalization's
168 // stuck-at-1.0 behavior when music stops. Pass-through during audio so
169 // beat dynamics are preserved exactly.
172
173 // FPS-independent rate adjustment
174 static float adjustRateToFPS(float rateAtFps1, float fps1, float actualFps) FL_NOEXCEPT;
175};
176
177} // namespace detector
178} // namespace audio
179} // namespace fl
static void resetPrivateFFTCount() FL_NOEXCEPT
Definition vibe.cpp.hpp:21
float getBass() const FL_NOEXCEPT
Definition vibe.h:90
float getMidAvg() const FL_NOEXCEPT
Definition vibe.h:118
void reset() FL_NOEXCEPT override
Definition vibe.cpp.hpp:169
float getTrebRaw() const FL_NOEXCEPT
Definition vibe.h:115
float getVolAtt() const FL_NOEXCEPT
Definition vibe.h:101
function_list< void(const VibeLevels &)> onVibeLevels
Definition vibe.h:128
bool needsFFT() const FL_NOEXCEPT override
Definition vibe.h:81
float getBassAvg() const FL_NOEXCEPT
Definition vibe.h:117
float getBassLongAvg() const FL_NOEXCEPT
Definition vibe.h:121
void setSampleRate(int sampleRate) FL_NOEXCEPT override
Definition vibe.h:84
float getMidLongAvg() const FL_NOEXCEPT
Definition vibe.h:122
void fireCallbacks() FL_NOEXCEPT override
Definition vibe.cpp.hpp:135
bool isMidSpike() const FL_NOEXCEPT
Definition vibe.h:108
float getTrebLongAvg() const FL_NOEXCEPT
Definition vibe.h:123
float getTreb() const FL_NOEXCEPT
Definition vibe.h:92
void update(shared_ptr< Context > context) FL_NOEXCEPT override
Definition vibe.cpp.hpp:45
float getMid() const FL_NOEXCEPT
Definition vibe.h:91
float getVol() const FL_NOEXCEPT
Definition vibe.h:93
SilenceEnvelope mImmRelEnv[3]
Definition vibe.h:170
float getBassAtt() const FL_NOEXCEPT
Definition vibe.h:98
float getTrebAtt() const FL_NOEXCEPT
Definition vibe.h:100
float getMidAtt() const FL_NOEXCEPT
Definition vibe.h:99
function_list< void()> onMidSpike
Definition vibe.h:131
void setTargetFps(float fps) FL_NOEXCEPT
Definition vibe.h:138
function_list< void()> onBassSpike
Definition vibe.h:130
static float adjustRateToFPS(float rateAtFps1, float fps1, float actualFps) FL_NOEXCEPT
Definition vibe.cpp.hpp:196
float getMidRaw() const FL_NOEXCEPT
Definition vibe.h:114
~Vibe() FL_NOEXCEPT override
const char * getName() const FL_NOEXCEPT override
Definition vibe.h:82
SilenceEnvelope mAvgRelEnv[3]
Definition vibe.h:171
static int getPrivateFFTCount() FL_NOEXCEPT
Definition vibe.cpp.hpp:20
function_list< void()> onTrebSpike
Definition vibe.h:132
bool isTrebSpike() const FL_NOEXCEPT
Definition vibe.h:109
float getBassRaw() const FL_NOEXCEPT
Definition vibe.h:113
bool isBassSpike() const FL_NOEXCEPT
Definition vibe.h:107
float getTrebAvg() const FL_NOEXCEPT
Definition vibe.h:119
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT