FastLED 3.9.15
Loading...
Searching...
No Matches
audio_reactive.cpp
Go to the documentation of this file.
1#include "fl/audio_reactive.h"
2#include "fl/math.h"
3#include "fl/span.h"
4#include "fl/int.h"
5#include <math.h>
6
7namespace fl {
8
10 : mFFTBins(16) // Initialize with 16 frequency bins
11{
12 // No internal buffers needed
13}
14
16
17void AudioReactive::begin(const AudioConfig& config) {
18 setConfig(config);
19
20 // Reset state
23 mLastBeatTime = 0;
24 mPreviousVolume = 0.0f;
25 mAGCMultiplier = 1.0f;
26 mMaxSample = 0.0f;
27 mAverageLevel = 0.0f;
28}
29
31 mConfig = config;
32}
33
35 if (!sample.isValid()) {
36 return; // Invalid sample, ignore
37 }
38
39 // Extract timestamp from the AudioSample
40 fl::u32 currentTimeMs = sample.timestamp();
41
42 // Process the AudioSample immediately - timing is gated by sample availability
43 processFFT(sample);
44 updateVolumeAndPeak(sample);
45 detectBeat(currentTimeMs);
46 applyGain();
49
50 mCurrentData.timestamp = currentTimeMs;
51}
52
53void AudioReactive::update(fl::u32 currentTimeMs) {
54 // This method handles updates without new sample data
55 // Just apply smoothing and update timestamp
57 mCurrentData.timestamp = currentTimeMs;
58}
59
61 // Get PCM data from AudioSample
62 const auto& pcmData = sample.pcm();
63 if (pcmData.empty()) return;
64
65 // Use AudioSample's built-in FFT capability
66 sample.fft(&mFFTBins);
67
68 // Map FFT bins to frequency channels using WLED-compatible mapping
70}
71
73 // Copy FFT results to frequency bins array
74 for (int i = 0; i < 16; ++i) {
75 if (i < static_cast<int>(mFFTBins.bins_raw.size())) {
76 mCurrentData.frequencyBins[i] = mFFTBins.bins_raw[i];
77 } else {
78 mCurrentData.frequencyBins[i] = 0.0f;
79 }
80 }
81
82 // Apply pink noise compensation (from WLED)
83 for (int i = 0; i < 16; ++i) {
84 mCurrentData.frequencyBins[i] *= PINK_NOISE_COMPENSATION[i];
85 }
86
87 // Find dominant frequency
88 float maxMagnitude = 0.0f;
89 int maxBin = 0;
90 for (int i = 0; i < 16; ++i) {
91 if (mCurrentData.frequencyBins[i] > maxMagnitude) {
92 maxMagnitude = mCurrentData.frequencyBins[i];
93 maxBin = i;
94 }
95 }
96
97 // Convert bin index to approximate frequency
98 // Rough approximation based on WLED frequency mapping
99 const float binCenterFrequencies[16] = {
100 64.5f, // Bin 0: 43-86 Hz
101 107.5f, // Bin 1: 86-129 Hz
102 172.5f, // Bin 2: 129-216 Hz
103 258.5f, // Bin 3: 216-301 Hz
104 365.5f, // Bin 4: 301-430 Hz
105 495.0f, // Bin 5: 430-560 Hz
106 689.0f, // Bin 6: 560-818 Hz
107 969.0f, // Bin 7: 818-1120 Hz
108 1270.5f, // Bin 8: 1120-1421 Hz
109 1658.0f, // Bin 9: 1421-1895 Hz
110 2153.5f, // Bin 10: 1895-2412 Hz
111 2713.5f, // Bin 11: 2412-3015 Hz
112 3359.5f, // Bin 12: 3015-3704 Hz
113 4091.5f, // Bin 13: 3704-4479 Hz
114 5792.5f, // Bin 14: 4479-7106 Hz
115 8182.5f // Bin 15: 7106-9259 Hz
116 };
117
118 mCurrentData.dominantFrequency = binCenterFrequencies[maxBin];
119 mCurrentData.magnitude = maxMagnitude;
120}
121
123 // Get PCM data from AudioSample
124 const auto& pcmData = sample.pcm();
125 if (pcmData.empty()) {
126 mCurrentData.volume = 0.0f;
127 mCurrentData.volumeRaw = 0.0f;
128 mCurrentData.peak = 0.0f;
129 return;
130 }
131
132 // Use AudioSample's built-in RMS calculation
133 float rms = sample.rms();
134
135 // Calculate peak from PCM data
136 float maxSample = 0.0f;
137 for (fl::i16 pcmSample : pcmData) {
138 float absSample = (pcmSample < 0) ? -pcmSample : pcmSample;
139 maxSample = (maxSample > absSample) ? maxSample : absSample;
140 }
141
142 // Scale to 0-255 range (approximately)
143 mCurrentData.volumeRaw = rms / 128.0f; // Rough scaling
144 mCurrentData.volume = mCurrentData.volumeRaw;
145
146 // Peak detection
147 mCurrentData.peak = maxSample / 32768.0f * 255.0f;
148
149 // Update AGC tracking
150 if (mConfig.agcEnabled) {
151 // AGC with attack/decay behavior
152 float agcAttackRate = mConfig.attack / 255.0f * 0.2f + 0.01f; // 0.01 to 0.21
153 float agcDecayRate = mConfig.decay / 255.0f * 0.05f + 0.001f; // 0.001 to 0.051
154
155 // Track maximum level with attack/decay
156 if (maxSample > mMaxSample) {
157 // Rising - use attack rate (faster response)
158 mMaxSample = mMaxSample * (1.0f - agcAttackRate) + maxSample * agcAttackRate;
159 } else {
160 // Falling - use decay rate (slower response)
161 mMaxSample = mMaxSample * (1.0f - agcDecayRate) + maxSample * agcDecayRate;
162 }
163
164 // Update AGC multiplier with proper bounds
165 if (mMaxSample > 1000.0f) {
166 float targetLevel = 16384.0f; // Half of full scale
167 float newMultiplier = targetLevel / mMaxSample;
168
169 // Smooth AGC multiplier changes using attack/decay
170 if (newMultiplier > mAGCMultiplier) {
171 // Increasing gain - use attack rate
172 mAGCMultiplier = mAGCMultiplier * (1.0f - agcAttackRate) + newMultiplier * agcAttackRate;
173 } else {
174 // Decreasing gain - use decay rate
175 mAGCMultiplier = mAGCMultiplier * (1.0f - agcDecayRate) + newMultiplier * agcDecayRate;
176 }
177
178 // Clamp multiplier to reasonable bounds
179 mAGCMultiplier = (mAGCMultiplier < 0.1f) ? 0.1f : ((mAGCMultiplier > 10.0f) ? 10.0f : mAGCMultiplier);
180 }
181 }
182}
183
184void AudioReactive::detectBeat(fl::u32 currentTimeMs) {
185 // Need minimum time since last beat
186 if (currentTimeMs - mLastBeatTime < BEAT_COOLDOWN) {
187 mCurrentData.beatDetected = false;
188 return;
189 }
190
191 // Simple beat detection based on volume increase
192 float currentVolume = mCurrentData.volume;
193
194 // Beat detected if volume significantly increased
195 if (currentVolume > mPreviousVolume + mVolumeThreshold &&
196 currentVolume > 5.0f) { // Minimum volume threshold
197 mCurrentData.beatDetected = true;
198 mLastBeatTime = currentTimeMs;
199 } else {
200 mCurrentData.beatDetected = false;
201 }
202
203 // Update previous volume for next comparison using attack/decay
204 float beatAttackRate = mConfig.attack / 255.0f * 0.5f + 0.1f; // 0.1 to 0.6
205 float beatDecayRate = mConfig.decay / 255.0f * 0.3f + 0.05f; // 0.05 to 0.35
206
207 if (currentVolume > mPreviousVolume) {
208 // Rising volume - use attack rate (faster tracking)
209 mPreviousVolume = mPreviousVolume * (1.0f - beatAttackRate) + currentVolume * beatAttackRate;
210 } else {
211 // Falling volume - use decay rate (slower tracking)
212 mPreviousVolume = mPreviousVolume * (1.0f - beatDecayRate) + currentVolume * beatDecayRate;
213 }
214}
215
217 // Apply gain setting (0-255 maps to 0.0-2.0 multiplier)
218 float gainMultiplier = static_cast<float>(mConfig.gain) / 128.0f;
219
220 mCurrentData.volume *= gainMultiplier;
221 mCurrentData.volumeRaw *= gainMultiplier;
222 mCurrentData.peak *= gainMultiplier;
223
224 for (int i = 0; i < 16; ++i) {
225 mCurrentData.frequencyBins[i] *= gainMultiplier;
226 }
227
228 // Apply AGC if enabled
229 if (mConfig.agcEnabled) {
231 mCurrentData.volumeRaw *= mAGCMultiplier;
233
234 for (int i = 0; i < 16; ++i) {
235 mCurrentData.frequencyBins[i] *= mAGCMultiplier;
236 }
237 }
238}
239
241 // Apply scaling mode to frequency bins
242 for (int i = 0; i < 16; ++i) {
243 float value = mCurrentData.frequencyBins[i];
244
245 switch (mConfig.scalingMode) {
246 case 1: // Logarithmic scaling
247 if (value > 1.0f) {
248 value = logf(value) * 20.0f; // Scale factor
249 } else {
250 value = 0.0f;
251 }
252 break;
253
254 case 2: // Linear scaling (no change)
255 // value remains as-is
256 break;
257
258 case 3: // Square root scaling
259 if (value > 0.0f) {
260 value = sqrtf(value) * 8.0f; // Scale factor
261 } else {
262 value = 0.0f;
263 }
264 break;
265
266 case 0: // No scaling
267 default:
268 // value remains as-is
269 break;
270 }
271
272 mCurrentData.frequencyBins[i] = value;
273 }
274}
275
277 // Attack/decay smoothing - different rates for rising vs falling values
278 // Convert attack/decay times to smoothing factors
279 // Shorter times = less smoothing (faster response)
280 float attackFactor = 1.0f - (mConfig.attack / 255.0f * 0.9f); // Range: 0.1 to 1.0
281 float decayFactor = 1.0f - (mConfig.decay / 255.0f * 0.95f); // Range: 0.05 to 1.0
282
283 // Apply attack/decay smoothing to volume
284 if (mCurrentData.volume > mSmoothedData.volume) {
285 // Rising - use attack time (faster response)
286 mSmoothedData.volume = mSmoothedData.volume * (1.0f - attackFactor) +
287 mCurrentData.volume * attackFactor;
288 } else {
289 // Falling - use decay time (slower response)
290 mSmoothedData.volume = mSmoothedData.volume * (1.0f - decayFactor) +
291 mCurrentData.volume * decayFactor;
292 }
293
294 // Apply attack/decay smoothing to volumeRaw
295 if (mCurrentData.volumeRaw > mSmoothedData.volumeRaw) {
296 mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - attackFactor) +
297 mCurrentData.volumeRaw * attackFactor;
298 } else {
299 mSmoothedData.volumeRaw = mSmoothedData.volumeRaw * (1.0f - decayFactor) +
300 mCurrentData.volumeRaw * decayFactor;
301 }
302
303 // Apply attack/decay smoothing to peak
304 if (mCurrentData.peak > mSmoothedData.peak) {
305 mSmoothedData.peak = mSmoothedData.peak * (1.0f - attackFactor) +
306 mCurrentData.peak * attackFactor;
307 } else {
308 mSmoothedData.peak = mSmoothedData.peak * (1.0f - decayFactor) +
309 mCurrentData.peak * decayFactor;
310 }
311
312 // Apply attack/decay smoothing to frequency bins
313 for (int i = 0; i < 16; ++i) {
314 if (mCurrentData.frequencyBins[i] > mSmoothedData.frequencyBins[i]) {
315 // Rising - use attack time
316 mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - attackFactor) +
317 mCurrentData.frequencyBins[i] * attackFactor;
318 } else {
319 // Falling - use decay time
320 mSmoothedData.frequencyBins[i] = mSmoothedData.frequencyBins[i] * (1.0f - decayFactor) +
321 mCurrentData.frequencyBins[i] * decayFactor;
322 }
323 }
324
325 // Copy non-smoothed values
326 mSmoothedData.beatDetected = mCurrentData.beatDetected;
327 mSmoothedData.dominantFrequency = mCurrentData.dominantFrequency;
328 mSmoothedData.magnitude = mCurrentData.magnitude;
329 mSmoothedData.timestamp = mCurrentData.timestamp;
330}
331
333 return mCurrentData;
334}
335
339
341 return mCurrentData.volume;
342}
343
345 // Average of bins 0-1 (sub-bass and bass)
346 return (mCurrentData.frequencyBins[0] + mCurrentData.frequencyBins[1]) / 2.0f;
347}
348
350 // Average of bins 6-7 (midrange around 1kHz)
351 return (mCurrentData.frequencyBins[6] + mCurrentData.frequencyBins[7]) / 2.0f;
352}
353
355 // Average of bins 14-15 (high frequencies)
356 return (mCurrentData.frequencyBins[14] + mCurrentData.frequencyBins[15]) / 2.0f;
357}
358
360 return mCurrentData.beatDetected;
361}
362
364 float vol = (mCurrentData.volume < 0.0f) ? 0.0f : ((mCurrentData.volume > 255.0f) ? 255.0f : mCurrentData.volume);
365 return static_cast<fl::u8>(vol);
366}
367
368CRGB AudioReactive::volumeToColor(const CRGBPalette16& /* palette */) const {
369 fl::u8 index = volumeToScale255();
370 // Simplified color palette lookup
371 return CRGB(index, index, index); // For now, return grayscale
372}
373
375 if (binIndex >= 16) return 0;
376
377 float value = (mCurrentData.frequencyBins[binIndex] < 0.0f) ? 0.0f :
378 ((mCurrentData.frequencyBins[binIndex] > 255.0f) ? 255.0f : mCurrentData.frequencyBins[binIndex]);
379 return static_cast<fl::u8>(value);
380}
381
382// Helper methods
383float AudioReactive::mapFrequencyBin(int fromBin, int toBin) {
384 if (fromBin < 0 || toBin >= static_cast<int>(mFFTBins.size()) || fromBin > toBin) {
385 return 0.0f;
386 }
387
388 float sum = 0.0f;
389 for (int i = fromBin; i <= toBin; ++i) {
390 if (i < static_cast<int>(mFFTBins.bins_raw.size())) {
391 sum += mFFTBins.bins_raw[i];
392 }
393 }
394
395 return sum / static_cast<float>(toBin - fromBin + 1);
396}
397
399 if (samples.empty()) return 0.0f;
400
401 float sumSquares = 0.0f;
402 for (const auto& sample : samples) {
403 float f = static_cast<float>(sample);
404 sumSquares += f * f;
405 }
406
407 return sqrtf(sumSquares / samples.size());
408}
409
410} // namespace fl
float rms(Slice< const int16_t > data)
Definition simple.h:98
CRGB volumeToColor(const CRGBPalette16 &palette) const
const AudioData & getData() const
void update(fl::u32 currentTimeMs)
void processFFT(const AudioSample &sample)
fl::u8 volumeToScale255() const
void detectBeat(fl::u32 currentTimeMs)
void mapFFTBinsToFrequencyChannels()
float getBass() const
float computeRMS(const fl::vector< fl::i16 > &samples)
static constexpr float PINK_NOISE_COMPENSATION[16]
void updateVolumeAndPeak(const AudioSample &sample)
void setConfig(const AudioConfig &config)
static constexpr fl::u32 BEAT_COOLDOWN
AudioData mSmoothedData
void begin(const AudioConfig &config=AudioConfig{})
float getVolume() const
float getTreble() const
float mapFrequencyBin(int fromBin, int toBin)
const AudioData & getSmoothedData() const
fl::u8 frequencyToScale255(fl::u8 binIndex) const
void processSample(const AudioSample &sample)
AudioConfig mConfig
const VectorPCM & pcm() const
Definition audio.cpp:17
bool isValid() const
Definition audio.h:29
void fft(FFTBins *out) const
Definition audio.cpp:128
float rms() const
Definition audio.cpp:83
fl::u32 timestamp() const
Definition audio.cpp:76
bool empty() const
Definition vector.h:547
fl::size size() const
Definition vector.h:545
unsigned char u8
Definition int.h:17
HeapVector< T, Allocator > vector
Definition vector.h:1214
IMPORTANT!
Definition crgb.h:20
Representation of an RGB pixel (Red, Green, Blue)
Definition crgb.h:86