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_out = &io;
78 p_io = &io;
79 };
80
83 void setOutput(Print& out) override { p_out = &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_io) filtered.setStream(*p_io);
110 if (p_out) filtered.setOutput(*p_out);
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);
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 {
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 void flush() override { filtered.flush(); }
210
211 protected:
212 // Simple re-entrancy guard: prevents concurrent kernel updates from
213 // corrupting scratch buffers / updateKernel.
214 volatile bool isUpdating = false;
215 // Indicates new gains are pending and kernel should be refreshed.
216 volatile bool gainsDirty = false;
217 // Auto-update kernel during streaming. Default is false to preserve
218 // explicit update() behavior.
219 bool autoUpdate = false;
220
221 // Custom FIR Filter Class
222 class EQFIRFilter : public Filter<SampleT> {
223 public:
224 EQFIRFilter() : activeKernel(nullptr) {}
225
226 void setKernel(volatile int16_t* kernel) { activeKernel = kernel; }
227
228 SampleT process(SampleT sample) override {
229 if (activeKernel == nullptr) {
230 LOGE("Kernel not set!");
231 return sample; // Pass-through if no kernel set
232 }
233
234 xHistory[idxHist] = sample;
235
236 // Use AccT to prevent overflow and/or allow float accumulation
237 AccT acc = (AccT)0;
238 int idx = idxHist;
239
240 for (int n = 0; n < NUM_TAPS; n++) {
241 // Coefficients are Q15 int16_t
242 acc += (AccT)xHistory[idx] * (AccT)activeKernel[n];
243 if (--idx < 0) idx = NUM_TAPS - 1;
244 }
245
246 if (++idxHist >= NUM_TAPS) idxHist = 0;
247
248 // Convert back from Q15 and saturate
249 return fromQ15(acc);
250 }
251
252 protected:
253 SampleT xHistory[NUM_TAPS] = {(SampleT)0};
254 int idxHist = 0;
255 volatile int16_t* activeKernel = nullptr; // Pointer to active kernel
256
257 static inline SampleT fromQ15(AccT acc) {
258 // Default path: integer SampleT (current behavior)
259 if constexpr (std::numeric_limits<SampleT>::is_integer) {
260 // Shift back from Q15
261 if constexpr (std::numeric_limits<AccT>::is_integer) {
262 acc >>= 15;
263 } else {
264 acc = acc / (AccT)(1 << 15);
265 }
266
267 // Saturation to SampleT range
268 const AccT hi = (AccT)std::numeric_limits<SampleT>::max();
269 const AccT lo = (AccT)std::numeric_limits<SampleT>::min();
270 if (acc > hi) acc = hi;
271 if (acc < lo) acc = lo;
272 return (SampleT)acc;
273 } else {
274 // Floating output sample: scale Q15 back to approximately [-1..1]
275 return (SampleT)(acc / (AccT)(1 << 15));
276 }
277 }
278 };
279
280 // Q15 range is [-32768, 32767]. Use 32767 for +1.0 to avoid overflow/wrap.
281 static constexpr float Q15_SCALE = 32767.0f;
282 float centerFreqs[NUM_BANDS];
283 // Gains in dB used by the kernel design.
284 float gains[NUM_BANDS] = {0};
285 // Gains written by user-facing setters. Copied to gains transactionally.
286 float pendingGains[NUM_BANDS] = {0};
287
288 // Scratch buffer used during kernel design. Kept as member to avoid static
289 // storage (shared across instances and non-reentrant).
290 float tempFloat[NUM_TAPS] = {0};
291
292 // Double-buffering: two kernels for thread-safe updates
293 int16_t kernelA[NUM_TAPS];
294 int16_t kernelB[NUM_TAPS];
295 volatile int16_t* activeKernel; // Pointer to currently used kernel
296 volatile int16_t* updateKernel; // Pointer to kernel being updated
297
298 float windowCoeffs[NUM_TAPS]; // Pre-calculated Blackman window
300
301 Print* p_out = nullptr;
302 Stream* p_io = nullptr;
305
306 // Centralized interrupt guards to avoid repeated #if defined(ARDUINO) blocks.
307 inline void enterCritical() {
308#if defined(ARDUINO)
309 noInterrupts();
310#endif
311 }
312
313 inline void exitCritical() {
314#if defined(ARDUINO)
315 interrupts();
316#endif
317 }
318
319 // Update kernel if any gain changes are pending.
320 inline void maybeUpdateKernel() {
321 if (!autoUpdate) return;
322 if (gainsDirty) {
323 if (updateFIRKernel()) {
324 gainsDirty = false;
325 }
326 }
327 }
328
329 template <typename T>
330 float map(T x, T in_min, T in_max, T out_min, T out_max) {
331 return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
332 }
333
334 // Helper Sinc
335 float sinc(float x) {
336 if (fabsf(x) < 1e-8f) return 1.0f;
337 return sinf(PI * x) / (PI * x);
338 }
339
340 // Setup Center Frequencies Logarithmically spaced between 20Hz and Nyquist
341 void setupFrequencies(int sampleRate) {
342 if (NUM_BANDS <= 0) return;
343 float fMin = log10f(20.0f);
344 float fMax = log10f(sampleRate / 2.0f); // Nyquist frequency
345 if (NUM_BANDS == 1) {
346 centerFreqs[0] = powf(10.0f, (fMin + fMax) * 0.5f);
347 LOGD("Only one band: center frequency set to %.2f Hz", centerFreqs[0]);
348 return;
349 }
350 float step = (fMax - fMin) / (float)(NUM_BANDS - 1);
351 for (int i = 0; i < NUM_BANDS; i++) {
352 centerFreqs[i] = powf(10.0f, fMin + step * (float)i);
353 LOGD("Band %d: center frequency = %.2f Hz", i, centerFreqs[i]);
354 }
355 }
356
357 // Pre-calculate Blackman window coefficients (performance optimization)
359 const float N_minus_1 = (float)(NUM_TAPS - 1);
360 for (int n = 0; n < NUM_TAPS; n++) {
361 windowCoeffs[n] = 0.42f - 0.5f * cosf(2.0f * PI * n / N_minus_1) +
362 0.08f * cosf(4.0f * PI * n / N_minus_1);
363 }
364 }
365
366 // Initialize a kernel to pass-through (identity filter)
367 void initializeKernel(volatile int16_t* kernel) {
368 const int M = (NUM_TAPS - 1) / 2;
369 for (int i = 0; i < NUM_TAPS; i++) {
370 if (i == M) {
371 kernel[i] = (int16_t)Q15_SCALE; // Unity gain at center tap
372 } else {
373 kernel[i] = 0;
374 }
375 }
376 }
377
379 if (currentSampleRate <= 0) {
380 LOGE("Invalid sample rate: %d", currentSampleRate);
381 return false; // Not initialized yet
382 }
383
384 // Prevent concurrent updates (e.g., from multiple tasks/threads).
385 // We keep the critical section tiny: only the check/set of the flag.
387 if (isUpdating) {
388 exitCritical();
389 return false;
390 }
391 isUpdating = true;
392 exitCritical();
393
394 // Transactional gain update: copy user-updated pendingGains into gains.
395 // Keep this critical section short to avoid blocking audio processing.
397 memcpy(gains, pendingGains, sizeof(gains));
398 exitCritical();
399
400 memset(tempFloat, 0, sizeof(tempFloat));
401
402 const int M = (NUM_TAPS - 1) / 2;
403 const float sampleRateFloat = (float)currentSampleRate;
404
405 // Base impulse (Pass-through)
406 tempFloat[M] = 1.0f;
407
408 for (int i = 0; i < NUM_BANDS; i++) {
409 // Skip bands with 0dB gain to save precision
410 if (fabs(gains[i]) < 0.1f) continue;
411
412 // Calculate Linear Gain Delta
413 // If gain is +6dB (2.0x), we add (2.0 - 1.0) = +1.0 to the impulse
414 float linGain = powf(10.0f, gains[i] / 20.0f) - 1.0f;
415
416 // Use actual sample rate
417 float fL_hz = centerFreqs[i] * 0.707f; // Lower Edge (-3dB point)
418 float fH_hz = centerFreqs[i] * 1.414f; // Upper Edge (+3dB point)
419
420 // Enforce minimum bandwidth so the windowed-sinc FIR can resolve
421 // this band. The Blackman window main-lobe width is ~4/N in
422 // normalised frequency, i.e. 4*Fs/N Hz. Bands narrower than that
423 // are effectively invisible to the filter and produce a near-flat
424 // (no-effect) response.
425 float minBwHz = 4.0f * sampleRateFloat / (float)NUM_TAPS;
426 float actualBwHz = fH_hz - fL_hz;
427 if (actualBwHz < minBwHz) {
428 float expand = (minBwHz - actualBwHz) * 0.5f;
429 fL_hz = fL_hz - expand;
430 fH_hz = fH_hz + expand;
431 if (fL_hz < 1.0f) fL_hz = 1.0f;
432 }
433
434 float fL = fL_hz / sampleRateFloat;
435 float fH = fH_hz / sampleRateFloat;
436
437 // Clamp to valid normalized frequency range [0, 0.5] (Nyquist)
438 if (fL < 0.0f) fL = 0.0f;
439 if (fH < 0.0f) fH = 0.0f;
440 if (fL > 0.5f) fL = 0.5f;
441 if (fH > 0.5f) fH = 0.5f;
442 if (fH <= fL) continue;
443
444 // Evaluate the windowed bandpass magnitude at the center frequency
445 // so we can normalise to unity. The Blackman window reduces the
446 // passband peak below 1.0; without compensation the actual
447 // boost/cut is weaker than requested.
448 float wCenter = 2.0f * PI * centerFreqs[i] / sampleRateFloat;
449 float hReal = 0.0f;
450 float hImag = 0.0f;
451 for (int n = 0; n < NUM_TAPS; n++) {
452 float nM = (float)(n - M);
453 float bpW = ((2.0f * fH * sinc(2.0f * fH * nM)) -
454 (2.0f * fL * sinc(2.0f * fL * nM))) *
455 windowCoeffs[n];
456 hReal += bpW * cosf(wCenter * n);
457 hImag -= bpW * sinf(wCenter * n);
458 }
459 float bpMag = sqrtf(hReal * hReal + hImag * hImag);
460 float normFactor = (bpMag > 1e-6f) ? (1.0f / bpMag) : 1.0f;
461
462 for (int n = 0; n < NUM_TAPS; n++) {
463 float nM = (float)(n - M);
464
465 // Use pre-calculated window coefficients
466 float window = windowCoeffs[n];
467
468 // Bandpass filter: highpass - lowpass
469 float bp = (2.0f * fH * sinc(2.0f * fH * nM)) -
470 (2.0f * fL * sinc(2.0f * fL * nM));
471
472 // Add the normalised, weighted bandpass to the master kernel
473 tempFloat[n] += bp * window * normFactor * linGain;
474 }
475 }
476
477 // Update the inactive kernel (no interruption needed for writes)
478 for (int i = 0; i < NUM_TAPS; i++) {
479 // DIRECT CONVERSION (No Auto-Normalization)
480 // This ensures +6dB actually outputs louder voltage
481 int32_t q = (int32_t)(tempFloat[i] * Q15_SCALE);
482
483 // Hard Clip the Kernel Coefficients to prevent wrap-around
484 if (q > 32767) q = 32767;
485 if (q < -32768) q = -32768;
486 updateKernel[i] = (int16_t)q;
487 }
488
489 // Atomically swap the kernel pointers
490 // This is the only operation that needs to be atomic
492 volatile int16_t* temp = activeKernel;
494 updateKernel = temp;
495
496 // Update filter references to new active kernel
497 for (auto& fir : fir_vector) {
498 fir.setKernel(activeKernel);
499 }
500 exitCritical();
501
502 // Release re-entrancy guard
504 isUpdating = false;
505 gainsDirty = false;
506 exitCritical();
507
508 LOGI("FIR kernel updated with new gains for %d bands /%d taps.", NUM_BANDS,
509 NUM_TAPS);
510 return true;
511 }
512};
513
514} // namespace audio_tools
#define PI
Definition AudioEffectsSuite.h:28
#define LOGI(...)
Definition AudioLoggerIDF.h:28
#define LOGD(...)
Definition AudioLoggerIDF.h:27
#define LOGE(...)
Definition AudioLoggerIDF.h:30
Definition Arduino.h:56
Definition Arduino.h:136
virtual void addNotifyAudioChange(AudioInfoSupport &bi)
Adds target to be notified about audio changes.
Definition AudioTypes.h:150
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:120
AudioInfo info
Definition BaseStream.h:171
virtual void setAudioInfo(AudioInfo newInfo) override
Defines the input AudioInfo.
Definition BaseStream.h:128
virtual AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition BaseStream.h:151
Definition EqualizerNBands.h:222
volatile int16_t * activeKernel
Definition EqualizerNBands.h:255
SampleT xHistory[NUM_TAPS]
Definition EqualizerNBands.h:253
int idxHist
Definition EqualizerNBands.h:254
EQFIRFilter()
Definition EqualizerNBands.h:224
void setKernel(volatile int16_t *kernel)
Definition EqualizerNBands.h:226
static SampleT fromQ15(AccT acc)
Definition EqualizerNBands.h:257
SampleT process(SampleT sample) override
Processes the input value and returns the filtered output value.
Definition EqualizerNBands.h:228
N-Band Equalizer using FIR filters with logarithmically spaced bands.
Definition EqualizerNBands.h:45
EqualizerNBands(Print &out)
Definition EqualizerNBands.h:50
void flush() override
Definition EqualizerNBands.h:209
void setOutput(Print &out) override
Definition EqualizerNBands.h:83
float sinc(float x)
Definition EqualizerNBands.h:335
~EqualizerNBands()
Definition EqualizerNBands.h:72
bool updateFIRKernel()
Definition EqualizerNBands.h:378
void enterCritical()
Definition EqualizerNBands.h:307
volatile int16_t * updateKernel
Definition EqualizerNBands.h:296
bool setBandGains(float volume)
Set same gain for all frequency bands.
Definition EqualizerNBands.h:160
volatile int16_t * activeKernel
Definition EqualizerNBands.h:295
EqualizerNBands(AudioOutput &out)
Definition EqualizerNBands.h:59
void setupFrequencies(int sampleRate)
Definition EqualizerNBands.h:341
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:293
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:282
bool autoUpdate
Definition EqualizerNBands.h:219
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:330
EqualizerNBands(Stream &in)
Definition EqualizerNBands.h:54
volatile bool isUpdating
Definition EqualizerNBands.h:214
bool update()
Definition EqualizerNBands.h:197
Print * p_out
Output stream for write operations.
Definition EqualizerNBands.h:301
volatile bool gainsDirty
Definition EqualizerNBands.h:216
void end()
Definition EqualizerNBands.h:131
float windowCoeffs[NUM_TAPS]
Definition EqualizerNBands.h:298
static constexpr float Q15_SCALE
Definition EqualizerNBands.h:281
FilteredStream< SampleT, SampleT > filtered
Definition EqualizerNBands.h:304
void initializeKernel(volatile int16_t *kernel)
Definition EqualizerNBands.h:367
int16_t kernelB[NUM_TAPS]
Definition EqualizerNBands.h:294
int currentSampleRate
Definition EqualizerNBands.h:299
bool begin() override
Initializes the equalizer with the current audio info.
Definition EqualizerNBands.h:91
void exitCritical()
Definition EqualizerNBands.h:313
bool setBandDB(int band, float gainDb)
Definition EqualizerNBands.h:150
float gains[NUM_BANDS]
Definition EqualizerNBands.h:284
void preCalculateWindow()
Definition EqualizerNBands.h:358
void setStream(Stream &io) override
Definition EqualizerNBands.h:76
float tempFloat[NUM_TAPS]
Definition EqualizerNBands.h:290
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:303
float pendingGains[NUM_BANDS]
Definition EqualizerNBands.h:286
void maybeUpdateKernel()
Definition EqualizerNBands.h:320
Stream * p_io
Input stream for read operations.
Definition EqualizerNBands.h:302
Abstract filter interface definition. Subclasses implement process() to transform audio samples one a...
Definition Filter.h:28
Stream to which we can apply Filters for each channel. The filter might change the result size!
Definition AudioStreams.h:1557
Abstract class: Objects can be put into a pipleline.
Definition AudioStreams.h:68
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 LMSEchoCancellationStream.h:6
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:51
sample_rate_t sample_rate
Sample Rate: e.g 44100.
Definition AudioTypes.h:53
uint16_t channels
Number of channels: 2=stereo, 1=mono.
Definition AudioTypes.h:55