arduino-audio-tools
Loading...
Searching...
No Matches
AutomaticGainControlStream.h
Go to the documentation of this file.
1#pragma once
2
3#include <math.h>
4
7
8namespace audio_tools {
9
22 // Desired average RMS level.
23 float target_db = -18.0f;
24
25 // Maximum automatic amplification.
26 float max_gain_db = 6.0f;
27
28 // Maximum automatic attenuation.
29 float min_gain_db = -24.0f;
30
31 // Time to reduce gain when the input gets louder.
32 float attack_seconds = 0.25f;
33
34 // Time to increase gain when the input gets quieter.
35 //
36 // Keep this relatively long to preserve fades.
37 float release_seconds = 3.0f;
38
39 // Below this RMS level the signal is considered silence.
40 //
41 // The normalizer will NOT increase gain below this level.
42 float silence_db = -55.0f;
43
44 // Maximum allowed peak after normalization.
45 //
46 // -1 dBFS leaves a small amount of digital headroom.
47 float peak_headroom_db = -1.0f;
48
49 // Reset gain and detector state when AudioInfo changes.
51};
52
64template <typename T>
66
67template <>
68struct AGCPCMTraits<int16_t> {
69 static constexpr float max_sample = 32768.0f;
70 static constexpr float min_value = -32768.0f;
71 static constexpr float max_value = 32767.0f;
72 static constexpr int fixed_shift = 0;
73};
74
75template <>
77 static constexpr float max_sample = 8388608.0f;
78 static constexpr float min_value = -8388608.0f;
79 static constexpr float max_value = 8388607.0f;
80 static constexpr int fixed_shift = 8;
81};
82
83template <>
84struct AGCPCMTraits<int32_t> {
85 static constexpr float max_sample = 2147483648.0f;
86 static constexpr float min_value = -2147483648.0f;
87 static constexpr float max_value = 2147483647.0f;
88 static constexpr int fixed_shift = 16;
89};
90
108 public:
111
114
118 setStream(out);
119 }
120
124 setOutput(out);
125 }
126
128 void setStream(Stream& in) override {
129 p_in = &in;
130 p_out = p_in;
131 }
132
134 void setOutput(Print& out) override { p_out = &out; }
135
140
142 bool begin() override { return begin(info); }
143
145 bool begin(AudioInfo cfg) {
146 setAudioInfo(cfg);
147 reset();
148 is_started = true;
149 return true;
150 }
151
154 config = cfg;
156 reset();
157 is_started = true;
158 return true;
159 }
160
162 void end() override { is_started = false; }
163
166
169
171 void reset() {
172 current_gain_db = 0.0f;
173 measured_db = -100.0f;
174 has_level = false;
175 }
176
178 void setAudioInfo(AudioInfo cfg) override {
180
182 reset();
183 }
184
186 channels = cfg.channels;
188
189 if (sample_rate == 0) {
190 sample_rate = 44100;
191 }
192 }
193
195 size_t readBytes(uint8_t* data, size_t len) override {
196 if (data == nullptr || p_in == nullptr) {
197 return 0;
198 }
199
200 size_t result = p_in->readBytes(data, len);
201
202 if (result > 0 && is_started) {
203 process(data, result);
204 }
205
206 return result;
207 }
208
210 size_t write(const uint8_t* data, size_t len) override {
211 if (data == nullptr || p_out == nullptr) {
212 return 0;
213 }
214
215 if (is_started && len > 0) {
216 process(data, len);
217 }
218
219 return p_out->write(data, len);
220 }
221
223 int availableForWrite() override {
224 return p_out == nullptr ? 0 : p_out->availableForWrite();
225 }
226
228 int available() override { return p_in == nullptr ? 0 : p_in->available(); }
229
230 void flush() override {
231 if (p_out != nullptr) p_out->flush();
232 }
233
234 protected:
235 Print* p_out = nullptr;
236 Stream* p_in = nullptr;
238 bool is_started = false;
239 uint32_t sample_rate = 44100;
240 uint16_t channels = 2;
241 uint16_t bits_per_sample = 16;
242 float current_gain_db = 0.0f;
243 float measured_db = -100.0f;
244 bool has_level = false;
245
246 static float dbToLinear(float db) { return powf(10.0f, db / 20.0f); }
247
248 static float linearToDb(float value) {
249 if (value <= 0.0000001f) {
250 return -100.0f;
251 }
252
253 return 20.0f * log10f(value);
254 }
255
263 float smoothingCoefficient(float seconds, size_t samples) const {
264 if (seconds <= 0.0f) {
265 return 1.0f;
266 }
267
268 if (sample_rate == 0 || channels == 0 || samples == 0) {
269 return 1.0f;
270 }
271
272 float block_seconds = static_cast<float>(samples) /
273 static_cast<float>(sample_rate * channels);
274
275 return 1.0f - expf(-block_seconds / seconds);
276 }
277
287 template <typename T>
288 void analyze(const T* data, size_t samples, float& rms, float& peak) {
289#if PREFER_FIXEDPOINT
290 constexpr int shift = AGCPCMTraits<T>::fixed_shift;
291 constexpr float max_sample = AGCPCMTraits<T>::max_sample;
292
293 int64_t sum_sq = 0;
294 int32_t peak_raw = 0;
295
296 for (size_t i = 0; i < samples; ++i) {
297 // int24_t's conversion operators are not const-qualified, so a
298 // local (non-const) copy is needed before the cast.
299 T sample = data[i];
300 int32_t value = static_cast<int32_t>(sample) >> shift;
301 int32_t abs_value = value < 0 ? -value : value;
302
303 if (abs_value > peak_raw) {
304 peak_raw = abs_value;
305 }
306
307 sum_sq += static_cast<int64_t>(value) * value;
308 }
309
310 float scaled_max = max_sample / static_cast<float>(1 << shift);
311
312 rms = samples == 0 ? 0.0f
313 : sqrtf(static_cast<float>(sum_sq) /
314 static_cast<float>(samples)) /
315 scaled_max;
316 peak = static_cast<float>(peak_raw) / scaled_max;
317#else
318 float sum = 0.0f;
319 peak = 0.0f;
320
321 constexpr float max_sample = AGCPCMTraits<T>::max_sample;
322
323 for (size_t i = 0; i < samples; ++i) {
324 // int24_t's conversion operators are not const-qualified, so a
325 // local (non-const) copy is needed before the cast.
326 T sample = data[i];
327 float value = static_cast<float>(sample) / max_sample;
328
329 float abs_value = fabsf(value);
330
331 sum += value * value;
332
333 if (abs_value > peak) {
334 peak = abs_value;
335 }
336 }
337
338 rms = samples == 0 ? 0.0f : sqrtf(sum / static_cast<float>(samples));
339#endif
340 }
341
345 bool analyze(const uint8_t* buffer, size_t bytes, float& rms, float& peak) {
346 if (channels == 0) {
347 return false;
348 }
349
350 switch (bits_per_sample) {
351 case 16:
352 analyze(reinterpret_cast<const int16_t*>(buffer),
353 bytes / sizeof(int16_t), rms, peak);
354 return true;
355
356 case 24:
357 analyze(reinterpret_cast<const int24_t*>(buffer),
358 bytes / sizeof(int24_t), rms, peak);
359 return true;
360
361 case 32:
362 analyze(reinterpret_cast<const int32_t*>(buffer),
363 bytes / sizeof(int32_t), rms, peak);
364 return true;
365
366 default:
367 return false;
368 }
369 }
370
374 float desiredGainDb(float rms_db, float peak_db) const {
375 /*
376 * Do not amplify silence.
377 *
378 * This is particularly important for fades and prevents
379 * background noise from being amplified after a song becomes
380 * quiet.
381 */
382 if (rms_db <= config.silence_db) {
383 return current_gain_db;
384 }
385
386 /*
387 * RMS normalization.
388 *
389 * Example:
390 *
391 * target = -18 dB
392 * measured = -24 dB
393 *
394 * desired = +6 dB
395 */
396 float desired = config.target_db - rms_db;
397
398 /*
399 * Automatic gain limits.
400 */
401 if (desired > config.max_gain_db) {
402 desired = config.max_gain_db;
403 }
404
405 if (desired < config.min_gain_db) {
406 desired = config.min_gain_db;
407 }
408
409 /*
410 * Peak protection.
411 *
412 * Never intentionally request enough gain to put the detected
413 * peak above the configured headroom.
414 */
415 float peak_after_gain = peak_db + desired;
416
417 if (peak_after_gain > config.peak_headroom_db) {
418 float peak_limited_gain = config.peak_headroom_db - peak_db;
419
420 if (peak_limited_gain < desired) {
421 desired = peak_limited_gain;
422 }
423 }
424
425 /*
426 * Peak limiting can have pushed the gain below our configured
427 * lower bound, so apply the minimum one final time.
428 */
429 if (desired < config.min_gain_db) {
430 desired = config.min_gain_db;
431 }
432
433 return desired;
434 }
435
436 void updateGain(float desired_gain_db, size_t samples) {
437 float coefficient;
438
439 if (desired_gain_db < current_gain_db) {
440 /*
441 * Input got louder.
442 *
443 * Reduce gain relatively quickly.
444 */
445 coefficient = smoothingCoefficient(config.attack_seconds, samples);
446
447 } else {
448 /*
449 * Input got quieter.
450 *
451 * Increase gain slowly.
452 *
453 * This is what preserves fades.
454 */
455 coefficient = smoothingCoefficient(config.release_seconds, samples);
456 }
457
458 current_gain_db += coefficient * (desired_gain_db - current_gain_db);
459
460 /*
461 * Avoid unnecessary floating point movement when we are
462 * already effectively at the target.
463 */
464 if (fabsf(current_gain_db - desired_gain_db) < 0.001f) {
465 current_gain_db = desired_gain_db;
466 }
467 }
468
483 template <typename T>
484 void apply(T* data, size_t samples, float gain) {
485#if PREFER_FIXEDPOINT
486 q1_14_t fixed_gain(gain);
487
488 for (size_t i = 0; i < samples; ++i) {
489 data[i] = fixed_gain.scale(data[i]);
490 }
491#else
492 constexpr float min_value = AGCPCMTraits<T>::min_value;
493
494 constexpr float max_value = AGCPCMTraits<T>::max_value;
495
496 const float dgain = static_cast<float>(gain);
497
498 for (size_t i = 0; i < samples; ++i) {
499 float value = static_cast<float>(data[i]) * dgain;
500
501 if (value > max_value) {
502 value = max_value;
503 }
504
505 if (value < min_value) {
506 value = min_value;
507 }
508
509 data[i] = static_cast<T>(value);
510 }
511#endif
512 }
513
517 void apply(uint8_t* buffer, size_t bytes, float gain) {
518 switch (bits_per_sample) {
519 case 16:
520 apply(reinterpret_cast<int16_t*>(buffer), bytes / sizeof(int16_t),
521 gain);
522 break;
523
524 case 24:
525 apply(reinterpret_cast<int24_t*>(buffer), bytes / sizeof(int24_t),
526 gain);
527 break;
528
529 case 32:
530 apply(reinterpret_cast<int32_t*>(buffer), bytes / sizeof(int32_t),
531 gain);
532 break;
533
534 default:
535 break;
536 }
537 }
538
539 void process(const uint8_t* input, size_t bytes) {
540 if (input == nullptr || bytes == 0 || channels == 0) {
541 return;
542 }
543
544 /*
545 * VolumeStream also modifies the supplied PCM buffer in-place,
546 * so AutomaticGainControlStream follows the same ModifyingStream
547 * convention.
548 */
549 uint8_t* buffer = const_cast<uint8_t*>(input);
550
551 float rms = 0.0;
552 float peak = 0.0;
553
554 /*
555 * First analyze the ORIGINAL signal.
556 *
557 * It is important that this happens before applying the new
558 * gain.
559 */
560 if (!analyze(buffer, bytes, rms, peak)) {
561 return;
562 }
563
564 /*
565 * Convert RMS and peak to dBFS.
566 */
567 float rms_db = linearToDb(static_cast<float>(rms));
568
569 float peak_db = linearToDb(static_cast<float>(peak));
570
571 measured_db = rms_db;
572 has_level = true;
573
574 size_t sample_size = bits_per_sample / 8;
575
576 if (sample_size == 0) {
577 return;
578 }
579
580 size_t samples = bytes / sample_size;
581
582 /*
583 * Determine desired gain.
584 */
585 float desired_gain_db = desiredGainDb(rms_db, peak_db);
586
587 /*
588 * Move current gain towards desired gain using asymmetric
589 * attack/release smoothing.
590 */
591 updateGain(desired_gain_db, samples);
592
593 /*
594 * Convert dB gain into a linear multiplier.
595 */
596 float gain = dbToLinear(current_gain_db);
597
598 /*
599 * Apply gain to the PCM data.
600 */
601 apply(buffer, bytes, gain);
602 }
603};
604
605} // namespace audio_tools
Definition Arduino.h:56
virtual int availableForWrite()
Definition Arduino.h:128
virtual size_t write(const uint8_t *data, size_t len)
Definition Arduino.h:120
virtual void flush()
Definition Arduino.h:130
Definition Arduino.h:136
virtual size_t readBytes(uint8_t *data, size_t len)
Definition Arduino.h:140
virtual int available()
Definition Arduino.h:139
virtual void setAudioInfo(AudioInfo info)=0
Defines the input AudioInfo.
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
Slow automatic loudness normalization for PCM audio.
Definition AutomaticGainControlStream.h:107
void updateGain(float desired_gain_db, size_t samples)
Definition AutomaticGainControlStream.h:436
void flush() override
Definition AutomaticGainControlStream.h:230
void setOutput(Print &out) override
Defines the output Print target.
Definition AutomaticGainControlStream.h:134
uint16_t channels
Definition AutomaticGainControlStream.h:240
AutomaticGainControlStream(AudioOutput &out)
Definition AutomaticGainControlStream.h:123
Stream * p_in
Definition AutomaticGainControlStream.h:236
uint32_t sample_rate
Definition AutomaticGainControlStream.h:239
float measured_db
Definition AutomaticGainControlStream.h:243
bool begin(AutomaticGainControlStreamConfig cfg)
Starts processing with the given AutomaticGainControlStreamConfig.
Definition AutomaticGainControlStream.h:153
static float dbToLinear(float db)
Definition AutomaticGainControlStream.h:246
size_t readBytes(uint8_t *data, size_t len) override
Reads and gain-adjusts PCM data from the input Stream.
Definition AutomaticGainControlStream.h:195
bool is_started
Definition AutomaticGainControlStream.h:238
uint16_t bits_per_sample
Definition AutomaticGainControlStream.h:241
AutomaticGainControlStreamConfig config
Definition AutomaticGainControlStream.h:237
AutomaticGainControlStreamConfig & getConfig()
Provides access to the current configuration.
Definition AutomaticGainControlStream.h:168
AutomaticGainControlStream(AudioStream &out)
Definition AutomaticGainControlStream.h:117
AutomaticGainControlStreamConfig defaultConfig()
Provides the default configuration.
Definition AutomaticGainControlStream.h:137
void end() override
Stops processing; the underlying Stream/Print is left untouched.
Definition AutomaticGainControlStream.h:162
int available() override
Provides the available data of the input.
Definition AutomaticGainControlStream.h:228
float smoothingCoefficient(float seconds, size_t samples) const
Definition AutomaticGainControlStream.h:263
size_t write(const uint8_t *data, size_t len) override
Gain-adjusts PCM data in place and writes it to the output.
Definition AutomaticGainControlStream.h:210
void analyze(const T *data, size_t samples, float &rms, float &peak)
Definition AutomaticGainControlStream.h:288
void apply(uint8_t *buffer, size_t bytes, float gain)
Definition AutomaticGainControlStream.h:517
AutomaticGainControlStream()=default
Default constructor: call setStream()/setOutput() before begin()
int availableForWrite() override
Provides the available space of the output.
Definition AutomaticGainControlStream.h:223
static float linearToDb(float value)
Definition AutomaticGainControlStream.h:248
void setConfig(const AutomaticGainControlStreamConfig &cfg)
Updates the configuration without resetting the current gain state.
Definition AutomaticGainControlStream.h:165
Print * p_out
Definition AutomaticGainControlStream.h:235
void apply(T *data, size_t samples, float gain)
Definition AutomaticGainControlStream.h:484
bool begin() override
Starts processing using the current AudioInfo.
Definition AutomaticGainControlStream.h:142
void reset()
Resets the current gain and level detector state.
Definition AutomaticGainControlStream.h:171
float desiredGainDb(float rms_db, float peak_db) const
Definition AutomaticGainControlStream.h:374
bool has_level
Definition AutomaticGainControlStream.h:244
void setStream(Stream &in) override
Defines the input Stream; output is set to the same Stream.
Definition AutomaticGainControlStream.h:128
float current_gain_db
Definition AutomaticGainControlStream.h:242
AutomaticGainControlStream(Stream &out)
Constructs the stream with a bidirectional Stream as input and output.
Definition AutomaticGainControlStream.h:113
void setAudioInfo(AudioInfo cfg) override
Updates the AudioInfo; resets state when reset_on_audio_change is set.
Definition AutomaticGainControlStream.h:178
void process(const uint8_t *input, size_t bytes)
Definition AutomaticGainControlStream.h:539
bool begin(AudioInfo cfg)
Starts processing with the given AudioInfo, keeping the current config.
Definition AutomaticGainControlStream.h:145
bool analyze(const uint8_t *buffer, size_t bytes, float &rms, float &peak)
Definition AutomaticGainControlStream.h:345
Abstract class: Objects can be put into a pipleline.
Definition AudioStreams.h:68
24bit integer which is used for I2S sound processing. The values are represented as int32_t,...
Definition int24_4bytes_t.h:22
Fixed-point Q1.14 number: a plain 16-bit signed integer holding 1 integer bit and 14 fractional bits ...
Definition q1_14_t.h:16
int16_t scale(int16_t sample) const
scales a 16-bit PCM sample by this Q1.14 factor
Definition q1_14_t.h:83
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
Definition AutomaticGainControlStream.h:65
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
uint8_t bits_per_sample
Number of bits per sample (int16_t = 16 bits)
Definition AudioTypes.h:57
Configuration for AutomaticGainControlStream.
Definition AutomaticGainControlStream.h:21
float silence_db
Definition AutomaticGainControlStream.h:42
float peak_headroom_db
Definition AutomaticGainControlStream.h:47
float release_seconds
Definition AutomaticGainControlStream.h:37
float target_db
Definition AutomaticGainControlStream.h:23
float attack_seconds
Definition AutomaticGainControlStream.h:32
float max_gain_db
Definition AutomaticGainControlStream.h:26
float min_gain_db
Definition AutomaticGainControlStream.h:29
bool reset_on_audio_change
Definition AutomaticGainControlStream.h:50