From 764f56fba5e540e77dafd33179f8c7e8d497cb24 Mon Sep 17 00:00:00 2001 From: Holger Wirtz Date: Fri, 22 May 2020 08:29:58 +0200 Subject: [PATCH] Added a limiter after the dexed engine to avoid clipping. --- MicroDexed.ino | 8 +- effect_dynamics.cpp | 144 ++++++++++++++++++++++++++++++++ effect_dynamics.h | 194 ++++++++++++++++++++++++++++++++++++++++++++ fast_log.h | 50 ++++++++++++ 4 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 effect_dynamics.cpp create mode 100644 effect_dynamics.h create mode 100644 fast_log.h diff --git a/MicroDexed.ino b/MicroDexed.ino index ffc27da..65fffc6 100644 --- a/MicroDexed.ino +++ b/MicroDexed.ino @@ -36,12 +36,14 @@ #include "effect_modulated_delay.h" #include "effect_stereo_mono.h" #include "effect_mono_stereo.h" +#include "effect_dynamics.h" #include "PluginFx.h" #include "UI.hpp" #include "source_microdexed.h" // Audio engines AudioSourceMicroDexed* MicroDexed[NUM_DEXED]; +AudioEffectDynamics* dexed_dynamic[NUM_DEXED]; AudioAmplifier* dexed_level[NUM_DEXED]; #if defined(USE_FX) AudioSynthWaveform* chorus_modulator[NUM_DEXED]; @@ -145,6 +147,7 @@ AudioConnection * dynamicConnections[NUM_DEXED * 5]; void create_audio_engine_chain(uint8_t instance_id) { MicroDexed[instance_id] = new AudioSourceMicroDexed(SAMPLE_RATE); + dexed_dynamic[instance_id] = new AudioEffectDynamics; dexed_level[instance_id] = new AudioAmplifier(); mono2stereo[instance_id] = new AudioEffectMonoStereo(); #if defined(USE_FX) @@ -160,7 +163,8 @@ void create_audio_engine_chain(uint8_t instance_id) #endif dynamicConnections[nDynamic++] = new AudioConnection(*MicroDexed[instance_id], 0, microdexed_peak_mixer, instance_id); - dynamicConnections[nDynamic++] = new AudioConnection(*MicroDexed[instance_id], 0, *dexed_level[instance_id], 0); + dynamicConnections[nDynamic++] = new AudioConnection(*MicroDexed[instance_id], 0, *dexed_dynamic[instance_id], 0); + dynamicConnections[nDynamic++] = new AudioConnection(*dexed_dynamic[instance_id], 0, *dexed_level[instance_id], 0); #if defined(USE_FX) dynamicConnections[nDynamic++] = new AudioConnection(*dexed_level[instance_id], 0, *chorus_mixer[instance_id], 0); dynamicConnections[nDynamic++] = new AudioConnection(*dexed_level[instance_id], 0, *modchorus[instance_id], 0); @@ -447,6 +451,8 @@ void setup() Serial.println(F("]")); Serial.print(F("Polyphony: ")); Serial.println(configuration.dexed[instance_id].polyphony, DEC); + + dexed_dynamic[instance_id]->limit(); } Serial.print(F("AUDIO_BLOCK_SAMPLES=")); diff --git a/effect_dynamics.cpp b/effect_dynamics.cpp new file mode 100644 index 0000000..d2d9a03 --- /dev/null +++ b/effect_dynamics.cpp @@ -0,0 +1,144 @@ +/* Audio Library for Teensy 3.X + * Dynamics Processor (Gate, Compressor & Limiter) + * Copyright (c) 2017, Marc Paquette (marc@dacsystemes.com) + * Based on analyse_rms & mixer objects by Paul Stoffregen + * + * Development of this audio library was funded by PJRC.COM, LLC by sales of + * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop + * open source software by purchasing Teensy or other PJRC products. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "effect_dynamics.h" +#include "fast_log.h" +#include "utility/dspinst.h" +#include "utility/sqrt_integer.h" + +static float analyse_rms(int16_t *data) { + + uint32_t *p = (uint32_t *)data; + const uint32_t *end = p + AUDIO_BLOCK_SAMPLES / 2; + int64_t sum = 0; + do { + uint32_t n1 = *p++; + uint32_t n2 = *p++; + uint32_t n3 = *p++; + uint32_t n4 = *p++; + sum = multiply_accumulate_16tx16t_add_16bx16b(sum, n1, n1); + sum = multiply_accumulate_16tx16t_add_16bx16b(sum, n2, n2); + sum = multiply_accumulate_16tx16t_add_16bx16b(sum, n3, n3); + sum = multiply_accumulate_16tx16t_add_16bx16b(sum, n4, n4); + + } while (p < end); + + int32_t meansq = sum / AUDIO_BLOCK_SAMPLES; + return sqrt_uint32(meansq) / 32767.0f; +} + +static void applyGain(int16_t *data, int32_t mult1, int32_t mult2) { + + uint32_t *p = (uint32_t *)data; + const uint32_t *end = p + AUDIO_BLOCK_SAMPLES / 2; + int32_t inc = (mult2 - mult1) / (AUDIO_BLOCK_SAMPLES / 2); + + do { + uint32_t tmp32 = *p; // read 2 samples from *data + int32_t val1 = signed_multiply_32x16b(mult1, tmp32); + mult1 += inc; + int32_t val2 = signed_multiply_32x16t(mult1, tmp32); + mult1 += inc; + val1 = signed_saturate_rshift(val1, 16, 0); + val2 = signed_saturate_rshift(val2, 16, 0); + *p++ = pack_16b_16b(val2, val1); + } while (p < end); +} + +void AudioEffectDynamics::update(void) { + + audio_block_t *block; + + block = receiveWritable(0); + + if (!block) return; + + if (!gateEnabled && !compEnabled && !limiterEnabled) { + + //Transmit & release + transmit(block); + release(block); + return; + } + + //Analyze received block + float rms = analyse_rms(block->data); + + //Compute block RMS level in Db + float inputdb = MIN_DB; + if (rms > 0) inputdb = unitToDb(rms); + + //Gate + if (gateEnabled) { + if (inputdb >= gateThresholdOpen) gatedb = (aGateAttack * gatedb) + (aOneMinusGateAttack * MAX_DB); + else if (inputdb < gateThresholdClose) gatedb = (aGateRelease * gatedb) + (aOneMinusGateRelease * MIN_DB); + } + else gatedb = MAX_DB; + + //Compressor + if (compEnabled) { + float attdb = MAX_DB; //Below knee + if (inputdb >= aLowKnee) { + if(inputdb <= aHighKnee) { + //Knee transition + float knee = inputdb - aLowKnee; + attdb = aKneeRatio * knee * knee * aTwoKneeWidth; + } + else { + //Above knee + attdb = compThreshold + ((inputdb - compThreshold) * compRatio) - inputdb; + } + } + if (attdb <= compdb) compdb = (aCompAttack * compdb) + (aOneMinusCompAttack * attdb); + else compdb = (aCompRelease * compdb) + (aOneMinusCompRelease * attdb); + } + else compdb = MAX_DB; + + //Brickwall Limiter + if (limiterEnabled) { + float outdb = inputdb + compdb + makeupdb; + if (outdb >= limitThreshold) limitdb = (aLimitAttack * limitdb) + (aOneMinusLimitAttack * (limitThreshold - outdb)); + else limitdb *= aLimitRelease; + } + else limitdb = MAX_DB; + + //Compute linear gain + float totalGain = gatedb + compdb + makeupdb + limitdb; + int32_t mult = dbToUnit(totalGain) * 65536.0f; + + //Apply gain to block + applyGain(block->data, last_mult, mult); + last_mult = mult; + + //Transmit & release + transmit(block); + release(block); +} + + + diff --git a/effect_dynamics.h b/effect_dynamics.h new file mode 100644 index 0000000..d5a96b1 --- /dev/null +++ b/effect_dynamics.h @@ -0,0 +1,194 @@ +/* Audio Library for Teensy 3.X + * Dynamics Processor (Gate, Compressor & Limiter) + * Copyright (c) 2018, Marc Paquette (marc@dacsystemes.com) + * Based on analyse_rms, effect_envelope & mixer objects by Paul Stoffregen + * + * Development of this audio library was funded by PJRC.COM, LLC by sales of + * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop + * open source software by purchasing Teensy or other PJRC products. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef effect_dynamics_h_ +#define effect_dynamics_h_ + +#include "Arduino.h" +#include "AudioStream.h" + +#define MIN_DB -110.0f +#define MAX_DB 0.0f + +#define MIN_T 0.03f //Roughly 1 block +#define MAX_T 4.00f + +#define RATIO_OFF 1.0f +#define RATIO_INFINITY 60.0f + +class AudioEffectDynamics : public AudioStream +{ +public: + AudioEffectDynamics(void) : AudioStream(1, inputQueueArray) { + + gate(); + compression(); + limit(); + autoMakeupGain(); + + gatedb = MIN_DB; + compdb = MIN_DB; + limitdb = MIN_DB; + } + + //Sets the gate parameters. + //threshold is in dbFS + //attack & release are in seconds + void gate(float threshold = -50.0f, float attack = MIN_T, float release = 0.3f, float hysterisis = 6.0f) { + + gateEnabled = threshold > MIN_DB; + + gateThresholdOpen = constrain(threshold, MIN_DB, MAX_DB); + gateThresholdClose = gateThresholdOpen - constrain(hysterisis, 0.0f, 6.0f); + + float gateAttackTime = constrain(attack, MIN_T, MAX_T); + float gateReleaseTime = constrain(release, MIN_T, MAX_T); + + aGateAttack = timeToAlpha(gateAttackTime); + aOneMinusGateAttack = 1.0f - aGateAttack; + aGateRelease = timeToAlpha(gateReleaseTime); + aOneMinusGateRelease = 1.0f - aGateRelease; + } + + //Sets the compression parameters. + //threshold & kneeWidth are in db(FS) + //attack and release are in seconds + //ratio is expressed as x:1 i.e. 1 for no compression, 60 for brickwall limiting + //Set kneeWidth to 0 for hard knee + void compression(float threshold = -40.0f, float attack = MIN_T, float release = 0.5f, float ratio = 35.0f, float kneeWidth = 6.0f) { + + compEnabled = threshold < MAX_DB; + + compThreshold = constrain(threshold, MIN_DB, MAX_DB); + float compAttackTime = constrain(attack, MIN_T, MAX_T); + float compReleaseTime = constrain(release, MIN_T, MAX_T); + compRatio = 1.0f / constrain(abs(ratio), RATIO_OFF, RATIO_INFINITY); + float compKneeWidth = constrain(abs(kneeWidth), 0.0f, 32.0f); + computeMakeupGain(); + + aCompAttack = timeToAlpha(compAttackTime); + aOneMinusCompAttack = 1.0f - aCompAttack; + aCompRelease = timeToAlpha(compReleaseTime); + aOneMinusCompRelease = 1.0f - aCompRelease; + aHalfKneeWidth = compKneeWidth / 2.0f; + aTwoKneeWidth = 1.0f / (compKneeWidth * 2.0f); + aKneeRatio = compRatio - 1.0f; + aLowKnee = compThreshold - aHalfKneeWidth; + aHighKnee = compThreshold + aHalfKneeWidth; + } + + //Sets the hard limiter parameters + //threshold is in dbFS + //attack & release are in seconds + void limit(float threshold = -3.0f, float attack = MIN_T, float release = MIN_T) { + + limiterEnabled = threshold < MAX_DB; + + limitThreshold = constrain(threshold, MIN_DB, MAX_DB); + float limitAttackTime = constrain(attack, MIN_T, MAX_T); + float limitReleaseTime = constrain(release, MIN_T, MAX_T); + + computeMakeupGain(); + + aLimitAttack = timeToAlpha(limitAttackTime); + aOneMinusLimitAttack = 1.0f - aLimitAttack; + aLimitRelease = timeToAlpha(limitReleaseTime); + } + + //Enables automatic makeup gain setting + //headroom is in dbFS + void autoMakeupGain(float headroom = 6.0f) { + + mgAutoEnabled = true; + mgHeadroom = constrain(headroom, 0.0f, 60.0f); + computeMakeupGain(); + } + + //Sets a fixed makeup gain value. + //gain is in dbFS + void makeupGain(float gain = 0.0f) { + + mgAutoEnabled = false; + makeupdb = constrain(gain, -12.0f, 24.0f); + } + +private: + audio_block_t *inputQueueArray[1]; + + bool gateEnabled = false; + float gateThresholdOpen; + float gateThresholdClose; + float gatedb; + + bool compEnabled = false; + float compThreshold; + float compRatio; + float compdb; + + bool limiterEnabled = false; + float limitThreshold; + float limitdb; + + bool mgAutoEnabled; + float mgHeadroom; + float makeupdb; + + float aGateAttack; + float aOneMinusGateAttack; + float aGateRelease; + float aOneMinusGateRelease; + float aHalfKneeWidth; + float aTwoKneeWidth; + float aKneeRatio; + float aLowKnee; + float aHighKnee; + float aCompAttack; + float aOneMinusCompAttack; + float aCompRelease; + float aOneMinusCompRelease; + float aLimitAttack; + float aOneMinusLimitAttack; + float aLimitRelease; + + int32_t last_mult; + + void computeMakeupGain() { + if (mgAutoEnabled) { + makeupdb = -compThreshold + (compThreshold * compRatio) + limitThreshold - mgHeadroom; + } + } + + //Computes smoothing time constants for a 10% to 90% change + float timeToAlpha(float time) { + return expf(-0.9542f / (((float)AUDIO_SAMPLE_RATE_EXACT / (float)AUDIO_BLOCK_SAMPLES) * time)); + } + + virtual void update(void); +}; + +#endif diff --git a/fast_log.h b/fast_log.h new file mode 100644 index 0000000..3e2c24b --- /dev/null +++ b/fast_log.h @@ -0,0 +1,50 @@ +/* ---------------------------------------------------------------------- +* https://community.arm.com/tools/f/discussions/4292/cmsis-dsp-new-functionality-proposal/22621#22621 +* Fast approximation to the log2() function. It uses a two step +* process. First, it decomposes the floating-point number into +* a fractional component F and an exponent E. The fraction component +* is used in a polynomial approximation and then the exponent added +* to the result. A 3rd order polynomial is used and the result +* when computing db20() is accurate to 7.984884e-003 dB. +** ------------------------------------------------------------------- */ + +float log2f_approx_coeff[4] = {1.23149591368684f, -4.11852516267426f, 6.02197014179219f, -3.13396450166353f}; + +float log2f_approx(float X) +{ + float *C = &log2f_approx_coeff[0]; + float Y; + float F; + int E; + + // This is the approximation to log2() + F = frexpf(fabsf(X), &E); + + // Y = C[0]*F*F*F + C[1]*F*F + C[2]*F + C[3] + E; + Y = *C++; + Y *= F; + Y += (*C++); + Y *= F; + Y += (*C++); + Y *= F; + Y += (*C++); + Y += E; + return(Y); +} + +// https://codingforspeed.com/using-faster-exponential-approximation/ +inline float expf_approx(float x) { + x = 1.0f + x / 1024; + x *= x; x *= x; x *= x; x *= x; + x *= x; x *= x; x *= x; x *= x; + x *= x; x *= x; + return x; +} + +inline float unitToDb(float unit) { + return 6.02f * log2f_approx(unit); +} + +inline float dbToUnit(float db) { + return expf_approx(db * 2.302585092994046f * 0.05f); +} \ No newline at end of file