arduino-audio-tools
Loading...
Searching...
No Matches
EqualizerNBands.h
1#pragma once
2#include <math.h>
3#include <string.h>
4
5#include <limits>
6
7#include "AudioTools/CoreAudio/AudioBasic/Collections/Vector.h"
8#include "AudioTools/CoreAudio/AudioOutput.h"
9#include "AudioTools/CoreAudio/AudioStreams.h"
10#include "AudioToolsConfig.h"
11
12namespace audio_tools {
13
43template <typename SampleT = int16_t, typename AccT = int64_t,
44 int NUM_TAPS = 128, int NUM_BANDS = 12>
46 public:
47 EqualizerNBands() { setBandGains(0.0f); };
51
55
63
68 setStream(stream);
69 stream.addNotifyAudioChange(*this);
70 }
71
72 ~EqualizerNBands() { end(); }
73
76 void setStream(Stream& io) override {
77 p_print = &io;
78 p_stream = &io;
79 };
80
83 void setOutput(Print& out) override { p_print = &out; }
84
85 bool begin(AudioInfo info) {
86 setAudioInfo(info);
87 return begin();
88 }
89
91 bool begin() override {
92 currentSampleRate = audioInfo().sample_rate;
93 if (currentSampleRate <= 0) {
94 LOGE("Invalid sample rate: %d", currentSampleRate);
95 return false;
96 }
97 setupFrequencies(currentSampleRate);
98 preCalculateWindow();
99
100 // Initialize double-buffering pointers
101 activeKernel = kernelA;
102 updateKernel = kernelB;
103
104 // Initialize both kernels with pass-through (identity) filter
105 initializeKernel(kernelA);
106 initializeKernel(kernelB);
107
108 // assign output or source
109 if (p_stream) filtered.setStream(*p_stream);
110 if (p_print) filtered.setOutput(*p_print);
111 filtered.begin(audioInfo());
112
113 // set filters for all channels
114 fir_vector.resize(audioInfo().channels);
115 for (int ch = 0; ch < audioInfo().channels; ch++) {
116 fir_vector[ch].setKernel(activeKernel);
117 filtered.setFilter(ch, &fir_vector[ch]);
118 }
119
120 // calculate the kernel
121 bool rc = updateFIRKernel();
122
123 // Log the initial band settings for visibility
124 for (int band = 0; band < NUM_BANDS; band++) {
125 LOGI("Band %d: Freq=%.2fHz, Gain=%.2fdB", band, getBandFrequency(band),
126 getBandDB(band));
127 }
128 return rc;
129 }
130
131 void end() {
132 fir_vector.clear();
133 currentSampleRate = 0;
134 }
135
140 bool setBandGain(int band, float volume) {
141 // Map -1.0 to 1.0 to -90DB to +12dB
142 float vol_db = volume < 0 ? map<float>(volume, -1.0f, 0.0f, -90.0f, 0.0f) : map<float>(volume, 0.0f, 1.0f, 0.0f, 12.0f);
143 return setBandDB(band, vol_db);
144 }
145
150 bool setBandDB(int band, float gainDb) {
151 if (band < 0 || band >= NUM_BANDS) return false;
152 float db = min(gainDb, 12.0f);
153 db = max(db, -90.0f);
154 pendingGains[band] = db;
155 gainsDirty = true;
156 return true;
157 }
158
160 bool setBandGains(float volume) {
161 for (int band = 0; band < NUM_BANDS; band++) {
162 setBandGain(band, volume);
163 }
164 return true;
165 }
166
168 float getBandGain(int band) {
169 if (band < 0 || band >= NUM_BANDS) return 0.0f;
170 return map<float>(pendingGains[band], -12.0f, 12.0f, -1.0f, 1.0f);
171 }
172
176 float getBandDB(int band) const {
177 if (band < 0 || band >= NUM_BANDS) return 0.0f;
178 return pendingGains[band];
179 }
180
184 float getBandFrequency(int band) const {
185 if (band < 0 || band >= NUM_BANDS) return 0.0f;
186 return centerFreqs[band];
187 }
188
190 int getBandCount() const { return NUM_BANDS; }
191
194 void setAutoUpdate(bool enabled) { autoUpdate = enabled; }
195
196 // update the FIR kernel after changing gains
197 bool update() { return updateFIRKernel(); }
198
199 size_t write(const uint8_t* data, size_t len) override {
200 maybeUpdateKernel();
201 return filtered.write(data, len);
202 }
203
204 size_t readBytes(uint8_t* data, size_t len) override {
205 maybeUpdateKernel();
206 return filtered.readBytes(data, len);
207 }
208
209 protected:
210 // Simple re-entrancy guard: prevents concurrent kernel updates from
211 // corrupting scratch buffers / updateKernel.
212 volatile bool isUpdating = false;
213 // Indicates new gains are pending and kernel should be refreshed.
214 volatile bool gainsDirty = false;
215 // Auto-update kernel during streaming. Default is false to preserve
216 // explicit update() behavior.
217 bool autoUpdate = false;
218
219 // Custom FIR Filter Class
220 class EQFIRFilter : public Filter<SampleT> {
221 public:
222 EQFIRFilter() : activeKernel(nullptr) {}
223
224 void setKernel(volatile int16_t* kernel) { activeKernel = kernel; }
225
226 SampleT process(SampleT sample) override {
227 if (activeKernel == nullptr) {
228 LOGE("Kernel not set!");
229 return sample; // Pass-through if no kernel set
230 }
231
232 xHistory[idxHist] = sample;
233
234 // Use AccT to prevent overflow and/or allow float accumulation
235 AccT acc = (AccT)0;
236 int idx = idxHist;
237
238 for (int n = 0; n < NUM_TAPS; n++) {
239 // Coefficients are Q15 int16_t
240 acc += (AccT)xHistory[idx] * (AccT)activeKernel[n];
241 if (--idx < 0) idx = NUM_TAPS - 1;
242 }
243
244 if (++idxHist >= NUM_TAPS) idxHist = 0;
245
246 // Convert back from Q15 and saturate
247 return fromQ15(acc);
248 }
249
250 protected:
251 SampleT xHistory[NUM_TAPS] = {(SampleT)0};
252 int idxHist = 0;
253 volatile int16_t* activeKernel = nullptr; // Pointer to active kernel
254
255 static inline SampleT fromQ15(AccT acc) {
256 // Default path: integer SampleT (current behavior)
257 if constexpr (std::numeric_limits<SampleT>::is_integer) {
258 // Shift back from Q15
259 if constexpr (std::numeric_limits<AccT>::is_integer) {
260 acc >>= 15;
261 } else {
262 acc = acc / (AccT)(1 << 15);
263 }
264
265 // Saturation to SampleT range
266 const AccT hi = (AccT)std::numeric_limits<SampleT>::max();
267 const AccT lo = (AccT)std::numeric_limits<SampleT>::min();
268 if (acc > hi) acc = hi;
269 if (acc < lo) acc = lo;
270 return (SampleT)acc;
271 } else {
272 // Floating output sample: scale Q15 back to approximately [-1..1]
273 return (SampleT)(acc / (AccT)(1 << 15));
274 }
275 }
276 };
277
278 // Q15 range is [-32768, 32767]. Use 32767 for +1.0 to avoid overflow/wrap.
279 static constexpr float Q15_SCALE = 32767.0f;
280 float centerFreqs[NUM_BANDS];
281 // Gains in dB used by the kernel design.
282 float gains[NUM_BANDS] = {0};
283 // Gains written by user-facing setters. Copied to gains transactionally.
284 float pendingGains[NUM_BANDS] = {0};
285
286 // Scratch buffer used during kernel design. Kept as member to avoid static
287 // storage (shared across instances and non-reentrant).
288 float tempFloat[NUM_TAPS] = {0};
289
290 // Double-buffering: two kernels for thread-safe updates
291 int16_t kernelA[NUM_TAPS];
292 int16_t kernelB[NUM_TAPS];
293 volatile int16_t* activeKernel; // Pointer to currently used kernel
294 volatile int16_t* updateKernel; // Pointer to kernel being updated
295
296 float windowCoeffs[NUM_TAPS]; // Pre-calculated Blackman window
297 int currentSampleRate = 0;
298
299 Print* p_print = nullptr;
300 Stream* p_stream = nullptr;
303
304 // Centralized interrupt guards to avoid repeated #if defined(ARDUINO) blocks.
305 inline void enterCritical() {
306#if defined(ARDUINO)
307 noInterrupts();
308#endif
309 }
310
311 inline void exitCritical() {
312#if defined(ARDUINO)
313 interrupts();
314#endif
315 }
316
317 // Update kernel if any gain changes are pending.
318 inline void maybeUpdateKernel() {
319 if (!autoUpdate) return;
320 if (gainsDirty) {
321 if (updateFIRKernel()) {
322 gainsDirty = false;
323 }
324 }
325 }
326
327 template <typename T>
328 float map(T x, T in_min, T in_max, T out_min, T out_max) {
329 return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
330 }
331
332 // Helper Sinc
333 float sinc(float x) {
334 if (fabsf(x) < 1e-8f) return 1.0f;
335 return sinf(PI * x) / (PI * x);
336 }
337
338 // Setup Center Frequencies Logarithmically spaced between 20Hz and Nyquist
339 void setupFrequencies(int sampleRate) {
340 if (NUM_BANDS <= 0) return;
341 float fMin = log10f(20.0f);
342 float fMax = log10f(sampleRate / 2.0f); // Nyquist frequency
343 if (NUM_BANDS == 1) {
344 centerFreqs[0] = powf(10.0f, (fMin + fMax) * 0.5f);
345 LOGD("Only one band: center frequency set to %.2f Hz", centerFreqs[0]);
346 return;
347 }
348 float step = (fMax - fMin) / (float)(NUM_BANDS - 1);
349 for (int i = 0; i < NUM_BANDS; i++) {
350 centerFreqs[i] = powf(10.0f, fMin + step * (float)i);
351 LOGD("Band %d: center frequency = %.2f Hz", i, centerFreqs[i]);
352 }
353 }
354
355 // Pre-calculate Blackman window coefficients (performance optimization)
356 void preCalculateWindow() {
357 const float N_minus_1 = (float)(NUM_TAPS - 1);
358 for (int n = 0; n < NUM_TAPS; n++) {
359 windowCoeffs[n] = 0.42f - 0.5f * cosf(2.0f * PI * n / N_minus_1) +
360 0.08f * cosf(4.0f * PI * n / N_minus_1);
361 }
362 }
363
364 // Initialize a kernel to pass-through (identity filter)
365 void initializeKernel(volatile int16_t* kernel) {
366 const int M = (NUM_TAPS - 1) / 2;
367 for (int i = 0; i < NUM_TAPS; i++) {
368 if (i == M) {
369 kernel[i] = (int16_t)Q15_SCALE; // Unity gain at center tap
370 } else {
371 kernel[i] = 0;
372 }
373 }
374 }
375
376 bool updateFIRKernel() {
377 if (currentSampleRate <= 0) {
378 LOGE("Invalid sample rate: %d", currentSampleRate);
379 return false; // Not initialized yet
380 }
381
382 // Prevent concurrent updates (e.g., from multiple tasks/threads).
383 // We keep the critical section tiny: only the check/set of the flag.
384 enterCritical();
385 if (isUpdating) {
386 exitCritical();
387 return false;
388 }
389 isUpdating = true;
390 exitCritical();
391
392 // Transactional gain update: copy user-updated pendingGains into gains.
393 // Keep this critical section short to avoid blocking audio processing.
394 enterCritical();
395 memcpy(gains, pendingGains, sizeof(gains));
396 exitCritical();
397
398 memset(tempFloat, 0, sizeof(tempFloat));
399
400 const int M = (NUM_TAPS - 1) / 2;
401 const float sampleRateFloat = (float)currentSampleRate;
402
403 // Base impulse (Pass-through)
404 tempFloat[M] = 1.0f;
405
406 for (int i = 0; i < NUM_BANDS; i++) {
407 // Skip bands with 0dB gain to save precision
408 if (fabs(gains[i]) < 0.1f) continue;
409
410 // Calculate Linear Gain Delta
411 // If gain is +6dB (2.0x), we add (2.0 - 1.0) = +1.0 to the impulse
412 float linGain = powf(10.0f, gains[i] / 20.0f) - 1.0f;
413
414 // Use actual sample rate
415 float fL_hz = centerFreqs[i] * 0.707f; // Lower Edge (-3dB point)
416 float fH_hz = centerFreqs[i] * 1.414f; // Upper Edge (+3dB point)
417
418 // Enforce minimum bandwidth so the windowed-sinc FIR can resolve
419 // this band. The Blackman window main-lobe width is ~4/N in
420 // normalised frequency, i.e. 4*Fs/N Hz. Bands narrower than that
421 // are effectively invisible to the filter and produce a near-flat
422 // (no-effect) response.
423 float minBwHz = 4.0f * sampleRateFloat / (float)NUM_TAPS;
424 float actualBwHz = fH_hz - fL_hz;
425 if (actualBwHz < minBwHz) {
426 float expand = (minBwHz - actualBwHz) * 0.5f;
427 fL_hz = fL_hz - expand;
428 fH_hz = fH_hz + expand;
429 if (fL_hz < 1.0f) fL_hz = 1.0f;
430 }
431
432 float fL = fL_hz / sampleRateFloat;
433 float fH = fH_hz / sampleRateFloat;
434
435 // Clamp to valid normalized frequency range [0, 0.5] (Nyquist)
436 if (fL < 0.0f) fL = 0.0f;
437 if (fH < 0.0f) fH = 0.0f;
438 if (fL > 0.5f) fL = 0.5f;
439 if (fH > 0.5f) fH = 0.5f;
440 if (fH <= fL) continue;
441
442 // Evaluate the windowed bandpass magnitude at the center frequency
443 // so we can normalise to unity. The Blackman window reduces the
444 // passband peak below 1.0; without compensation the actual
445 // boost/cut is weaker than requested.
446 float wCenter = 2.0f * PI * centerFreqs[i] / sampleRateFloat;
447 float hReal = 0.0f;
448 float hImag = 0.0f;
449 for (int n = 0; n < NUM_TAPS; n++) {
450 float nM = (float)(n - M);
451 float bpW = ((2.0f * fH * sinc(2.0f * fH * nM)) -
452 (2.0f * fL * sinc(2.0f * fL * nM))) *
453 windowCoeffs[n];
454 hReal += bpW * cosf(wCenter * n);
455 hImag -= bpW * sinf(wCenter * n);
456 }
457 float bpMag = sqrtf(hReal * hReal + hImag * hImag);
458 float normFactor = (bpMag > 1e-6f) ? (1.0f / bpMag) : 1.0f;
459
460 for (int n = 0; n < NUM_TAPS; n++) {
461 float nM = (float)(n - M);
462
463 // Use pre-calculated window coefficients
464 float window = windowCoeffs[n];
465
466 // Bandpass filter: highpass - lowpass
467 float bp = (2.0f * fH * sinc(2.0f * fH * nM)) -
468 (2.0f * fL * sinc(2.0f * fL * nM));
469
470 // Add the normalised, weighted bandpass to the master kernel
471 tempFloat[n] += bp * window * normFactor * linGain;
472 }
473 }
474
475 // Update the inactive kernel (no interruption needed for writes)
476 for (int i = 0; i < NUM_TAPS; i++) {
477 // DIRECT CONVERSION (No Auto-Normalization)
478 // This ensures +6dB actually outputs louder voltage
479 int32_t q = (int32_t)(tempFloat[i] * Q15_SCALE);
480
481 // Hard Clip the Kernel Coefficients to prevent wrap-around
482 if (q > 32767) q = 32767;
483 if (q < -32768) q = -32768;
484 updateKernel[i] = (int16_t)q;
485 }
486
487 // Atomically swap the kernel pointers
488 // This is the only operation that needs to be atomic
489 enterCritical();
490 volatile int16_t* temp = activeKernel;
491 activeKernel = updateKernel;
492 updateKernel = temp;
493
494 // Update filter references to new active kernel
495 for (auto& fir : fir_vector) {
496 fir.setKernel(activeKernel);
497 }
498 exitCritical();
499
500 // Release re-entrancy guard
501 enterCritical();
502 isUpdating = false;
503 gainsDirty = false;
504 exitCritical();
505
506 LOGI("FIR kernel updated with new gains for %d bands /%d taps.", NUM_BANDS,
507 NUM_TAPS);
508 return true;
509 }
510};
511
512} // namespace audio_tools
virtual void addNotifyAudioChange(AudioInfoSupport &bi)
Adds target to be notified about audio changes.
Definition AudioTypes.h:153
Abstract Audio Ouptut class.
Definition AudioOutput.h:25
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition BaseStream.h:122
virtual void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition BaseStream.h:130
virtual AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition BaseStream.h:153
Definition EqualizerNBands.h:220
N-Band Equalizer using FIR filters with logarithmically spaced bands.
Definition EqualizerNBands.h:45
EqualizerNBands(Print &out)
Definition EqualizerNBands.h:50
void setOutput(Print &out) override
Definition EqualizerNBands.h:83
bool setBandGains(float volume)
Set same gain for all frequency bands.
Definition EqualizerNBands.h:160
EqualizerNBands(AudioOutput &out)
Definition EqualizerNBands.h:59
float getBandGain(int band)
Get current gain for a specific band as normalized volume (-1.0 to 1.0)
Definition EqualizerNBands.h:168
EqualizerNBands(AudioStream &stream)
Definition EqualizerNBands.h:67
float getBandFrequency(int band) const
Definition EqualizerNBands.h:184
bool setBandGain(int band, float volume)
Definition EqualizerNBands.h:140
Stream * p_stream
Input stream for read operations.
Definition EqualizerNBands.h:300
void setAutoUpdate(bool enabled)
Definition EqualizerNBands.h:194
EqualizerNBands(Stream &in)
Definition EqualizerNBands.h:54
bool begin() override
Initializes the equalizer with the current audio info.
Definition EqualizerNBands.h:91
Print * p_print
Output stream for write operations.
Definition EqualizerNBands.h:299
bool setBandDB(int band, float gainDb)
Definition EqualizerNBands.h:150
void setStream(Stream &io) override
Definition EqualizerNBands.h:76
int getBandCount() const
Get number of bands.
Definition EqualizerNBands.h:190
float getBandDB(int band) const
Definition EqualizerNBands.h:176
Vector< EQFIRFilter > fir_vector
Vector of FIR filters for each channel.
Definition EqualizerNBands.h:301
Abstract filter interface definition;.
Definition Filter.h:28
Stream to which we can apply Filters for each channel. The filter might change the result size!
Definition AudioStreams.h:1641
Abstract class: Objects can be put into a pipleline.
Definition AudioStreams.h:68
Definition NoArduino.h:62
Definition NoArduino.h:142
Vector implementation which provides the most important methods as defined by std::vector....
Definition Vector.h:21
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition AudioCodecsBase.h:10
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:55
sample_rate_t sample_rate
Sample Rate: e.g 44100.
Definition AudioTypes.h:57
uint16_t channels
Number of channels: 2=stereo, 1=mono.
Definition AudioTypes.h:59