arduino-audio-tools
Loading...
Searching...
No Matches
EqualizerNBands.h
Go to the documentation of this file.
1#pragma once
2#include <math.h>
3#include <string.h>
4
5#include <limits>
6
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:
51
55
63
68 setStream(stream);
69 stream.addNotifyAudioChange(*this);
70 }
71
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
87 return begin();
88 }
89
91 bool begin() override {
93 if (currentSampleRate <= 0) {
94 LOGE("Invalid sample rate: %d", currentSampleRate);
95 return false;
96 }
99
100 // Initialize double-buffering pointers
103
104 // Initialize both kernels with pass-through (identity) filter
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();
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);
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 {
201 return filtered.write(data, len);
202 }
203
204 size_t readBytes(uint8_t* data, size_t len) override {
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:
223
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:
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;
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.
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
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
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>
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)
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)
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
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.
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.
395 memcpy(gains, pendingGains, sizeof(gains));
396 exitCritical();
397
398 memset(tempFloat, 0, sizeof(tempFloat));
399
400 const int M = (NUM_TAPS - 1) / 2;
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
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
490 volatile int16_t* temp = activeKernel;
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
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
#define PI
Definition AudioEffectsSuite.h:27
#define LOGI(...)
Definition AudioLoggerIDF.h:28
#define LOGD(...)
Definition AudioLoggerIDF.h:27
#define LOGE(...)
Definition AudioLoggerIDF.h:30
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:123
AudioInfo info
Definition BaseStream.h:174
virtual void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition BaseStream.h:131
virtual AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition BaseStream.h:154
Definition EqualizerNBands.h:220
volatile int16_t * activeKernel
Definition EqualizerNBands.h:253
SampleT xHistory[NUM_TAPS]
Definition EqualizerNBands.h:251
int idxHist
Definition EqualizerNBands.h:252
EQFIRFilter()
Definition EqualizerNBands.h:222
void setKernel(volatile int16_t *kernel)
Definition EqualizerNBands.h:224
static SampleT fromQ15(AccT acc)
Definition EqualizerNBands.h:255
SampleT process(SampleT sample) override
Definition EqualizerNBands.h:226
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
float sinc(float x)
Definition EqualizerNBands.h:333
~EqualizerNBands()
Definition EqualizerNBands.h:72
bool updateFIRKernel()
Definition EqualizerNBands.h:376
void enterCritical()
Definition EqualizerNBands.h:305
volatile int16_t * updateKernel
Definition EqualizerNBands.h:294
bool setBandGains(float volume)
Set same gain for all frequency bands.
Definition EqualizerNBands.h:160
volatile int16_t * activeKernel
Definition EqualizerNBands.h:293
EqualizerNBands(AudioOutput &out)
Definition EqualizerNBands.h:59
void setupFrequencies(int sampleRate)
Definition EqualizerNBands.h:339
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
size_t readBytes(uint8_t *data, size_t len) override
Definition EqualizerNBands.h:204
bool setBandGain(int band, float volume)
Definition EqualizerNBands.h:140
int16_t kernelA[NUM_TAPS]
Definition EqualizerNBands.h:291
size_t write(const uint8_t *data, size_t len) override
Definition EqualizerNBands.h:199
EqualizerNBands()
Definition EqualizerNBands.h:47
float centerFreqs[NUM_BANDS]
Definition EqualizerNBands.h:280
Stream * p_stream
Input stream for read operations.
Definition EqualizerNBands.h:300
bool autoUpdate
Definition EqualizerNBands.h:217
void setAutoUpdate(bool enabled)
Definition EqualizerNBands.h:194
float map(T x, T in_min, T in_max, T out_min, T out_max)
Definition EqualizerNBands.h:328
EqualizerNBands(Stream &in)
Definition EqualizerNBands.h:54
volatile bool isUpdating
Definition EqualizerNBands.h:212
bool update()
Definition EqualizerNBands.h:197
volatile bool gainsDirty
Definition EqualizerNBands.h:214
void end()
Definition EqualizerNBands.h:131
float windowCoeffs[NUM_TAPS]
Definition EqualizerNBands.h:296
static constexpr float Q15_SCALE
Definition EqualizerNBands.h:279
FilteredStream< SampleT, SampleT > filtered
Definition EqualizerNBands.h:302
void initializeKernel(volatile int16_t *kernel)
Definition EqualizerNBands.h:365
int16_t kernelB[NUM_TAPS]
Definition EqualizerNBands.h:292
int currentSampleRate
Definition EqualizerNBands.h:297
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
void exitCritical()
Definition EqualizerNBands.h:311
bool setBandDB(int band, float gainDb)
Definition EqualizerNBands.h:150
float gains[NUM_BANDS]
Definition EqualizerNBands.h:282
void preCalculateWindow()
Definition EqualizerNBands.h:356
void setStream(Stream &io) override
Definition EqualizerNBands.h:76
float tempFloat[NUM_TAPS]
Definition EqualizerNBands.h:288
int getBandCount() const
Get number of bands.
Definition EqualizerNBands.h:190
bool begin(AudioInfo info)
Definition EqualizerNBands.h:85
float getBandDB(int band) const
Definition EqualizerNBands.h:176
Vector< EQFIRFilter > fir_vector
Vector of FIR filters for each channel.
Definition EqualizerNBands.h:301
float pendingGains[NUM_BANDS]
Definition EqualizerNBands.h:284
void maybeUpdateKernel()
Definition EqualizerNBands.h:318
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:1644
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
size_t writeData(Print *p_out, T *data, int samples, int maxSamples=512)
Definition AudioTypes.h:512
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