You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('') and can be up to 35 characters long.
272 lines
12 KiB
272 lines
12 KiB
/*


* AudioEffectCompWDR2_F32: Wide Dynamic Rnage Compressor #2


*


* Bob Larkin W7PUA 11 December 2020 *********** UNDER DEVELOPMENT SUBJECT TO CHANGE!!!!


* This is an attempt to simplify and further comment the Chip Audette WDRC compressor.


* Derived from: Chip Audette (OpenAudio) Feb 2017


* Which was derived From: WDRC_circuit from CHAPRO from BTNRC: https://github.com/BTNRH/chapro


* As of Feb 2017, CHAPRO license is listed as "Creative Commons?"


*


* MIT License. Use at your own risk.


*/


/*


* WDRC2 Wide dynamic range compressor #2. Amplifies input signals by a fixed amoount


* when the input is low. Above a first knee, the gain is reduce progressively more as


* the input gets greater. On a dB out vs. dB in curve, this shows as a chnge in the


* original 1:1 slope to a lesser slope of 1:cr1 where cr1 is the first compression ratio.


* Finally there is a second knee where the gain is reduced at an even greater rate. In the


* extreme this becomes a hard limiter, but it can continue to increase slightly at a dB


* rate of 1:cr2, with cr2 the second compression ratio.




Vout dB








0.0 **********#


 **********


 @********** 1:cr2


 ****


 ***


 ***


 *** 1:cr1


 ***


 @***


 *


 *


 *


 * Vout = Vin + g0 (all in dB)


 * 1:1


 *


 * * Vout vs. Vin in dB *


* Knees (breakpoints) are shown with '@'


* Zero, zero intersection shown with '#'


* Slopes are ratio of: output:input (in dB)


* 


* ________________________________________________________________ Vin dB


k1 k2 0.0




* The graph shows the changes in gain on a log or dB scale. A 1:1 slope represents


* a constant gain with level. When the slope is less, say cr1:1 where cr1 might be 3,


* the voltage gain is decreasing as the input level increases.


*


* The model here is, I believe, the same as the two references above (Audette and CHAPRO).


* The variable names have been changed to avoid confusion with those of audiologists and


* to be easier to follow for nonaudiologists. Here goes:


* gain0DB Gain, in dB of the compressor for low level inputs (g0 on graph) [38 dB]


* knee1dB First knee on the gain curve where the dB gain slope decreases(k1) [50 dB]


* cr1 Compression ratio on dB curve between knee1dB and knee2dB [3.0]


* knee2dB Second knee on the gain curve where the dB gain slope decreases further (k2) [20 dB]


* cr2 Compression ratio on dB curve above knee2dB [10.0]


*


* The presets for the above quantities, shown in square brackest, are qite aggressive,


* with a lot of compression (up to 38 dB). This is for demonstration, and each


* situation will have different settings. For the presets, the following data


* was measured, essentiallly as predicted:


* vIn (rel full scale)=0.001 vInDB=60.05 vOutDBvInDB=38.00


* vIn (rel full scale)=0.003 vInDB=50.47 vOutDBvInDB=38.00


* vIn (rel full scale)=0.01 vInDB=40.00 vOutDBvInDB=31.38


* vIn (rel full scale)=0.03 vInDB=30.45 vOutDBvInDB=24.97


* vIn (rel full scale)=0.1 vInDB=19.98 vOutDBvInDB=19.98


* vIn (rel full scale)=0.3 vInDB=10.45 vOutDBvInDB= 9.40


* vIn (rel full scale)=1.0 vInDB= 0.01 vOutDBvInDB=0.01


*


* vInDB refers to the time averaged envelope voltage.


* Needing a zero reference, this has been chosen as full ADC range output. This is ±1.0


* peak or 0.707 RMS in F32 terminology. If this is fixed, the lowlevel gain will also be


* determined. This is calculated in the constructor.


*


* The curve is for gainOffsetDB = 0.0. This parameter raises and lowers the entire gain


* curve by this many dB. This is equivalent to a postcompressor gain (or loss).


*


* Note: This is all done in conventional 10 based dB. This ends up with scaling in


* several places that could be eliminated by using 2B instead of dB, i.e.,


* use log2() and 2^(). This would seem to be faster, but less "readable."


*


* *********** UNDER DEVELOPMENT SUBJECT TO CHANGE!!!! *********


*/




#ifndef _AudioEffectCompWDRC2_F32


#define _AudioEffectCompWDRC2_F32




#include <Arduino.h>


#include <AudioStream_F32.h>




class AudioEffectWDRC2_F32 : public AudioStream_F32


{


//GUI: inputs:1, outputs:1 //this line used for automatic generation of GUI node


//GUI: shortName: CompressWDRC2


public:


AudioEffectWDRC2_F32(void): AudioStream_F32(1,inputQueueArray) {


setAttackReleaseSec(0.005f, 0.100f);


setLowLevelGain(); // Not an independent variable, set by knees, cr's and 0,0 intersection


// setSampleRate_Hz(AUDIO_SAMPLE_RATE);


}




//AudioEffectCompWDRC_F32(AudioSettings_F32 settings): AudioStream_F32(1,inputQueueArray) {


// setSampleRate_Hz(settings.sample_rate_Hz);


//}




// Here is the method called automatically by the audio library


void update(void) {


float vAbs, vPeak;


float vInDB, vOutDB;


float targetGain;




// Receive the input audio data


audio_block_f32_t *block = AudioStream_F32::receiveWritable_f32();


if (!block) return;


// Allocate memory for the output


audio_block_f32_t *out_block = AudioStream_F32::allocate_f32();


if (!out_block)


{


release(block);


return;


}




// Find the smoothed envelope, target gain and compressed output


vPeak = vPeakSave;


for (int k=0; k<block>length; k++) {


vAbs = (block>data[k] >= 0.0f) ? block>data[k] : block>data[k];


if (vAbs >= vPeak) { // Attack (rising level)


vPeak = alpha * vPeak + (oneMinusAlpha) * vAbs;


} else { // Release (decay for falling level)


vPeak = beta * vPeak;


}


// Convert to dB


// At all levels and quite frequency flat, this under estimates by 1.05 dB


vInDB = v2DB_Approx(vPeak) + 1.05f;


// Convert to desired Vout_DB, this is the compression curve


if(vInDB<=knee1DB)


vOutDB = vInDB + gain0DB; // No compression


else if(vInDB<knee2DB)


vOutDB = vInDB + gain0DB + (knee1DB  vInDB)*(cr1  1.0f)/cr1; // Middle region


else


vOutDB = vInDB + gain0DB + (knee2DB  vInDB)*(cr2  1.0f)/cr2 +


(knee1DB  knee2DB)*(cr1  1)/cr1; // High level region


// A note: from the latter, algebra says for a 0, 0 intersection of vInDB and vOutDB


// See setLowLevelGain()




// Convert the needed gain back to a voltage ratio 10^(db/20)


targetGain = pow10f(0.05f*(vOutDB  vInDB + gainOffsetDB));


// And apply target gain to signal stream from the delayed data. The


// delay buffer is circular because of delayBufferMask and length 2^m m<=8.


out_block>data[k] = targetGain * delayData[(k + in_index) & delayBufferMask];




if(printIO) {


Serial.print(block>data[k],6);


Serial.print("," );


Serial.print(delayData[(k + in_index) & delayBufferMask],6);


Serial.print("," );


Serial.println(targetGain);


}




// Put the new data into the delay line, delaySize positions ahead.


// If delaySize==256, this will be the same location as we just got data from.


delayData[(k + in_index + delaySize) & delayBufferMask] = block>data[k];


}


vPeakSave = vPeak; // save last vPeak for next time


sampleInputDB = vInDB; // Last values for get...() functions


sampleGainDB = vOutDB  vInDB;


// transmit the block and release memory


AudioStream_F32::release(block);


AudioStream_F32::transmit(out_block); // send the FIR output


AudioStream_F32::release(out_block);


// Update pointer in_index to delay line for next 128 update


in_index = (in_index + block>length) & delayBufferMask;


}




// gain0DB is the gain at low levels, below compression. Not an independent variable,


// so this should becalled after any change is made to knees and compression ratios.


void setLowLevelGain(void)


{


gain0DB = knee2DB*(1.0f  cr2)/cr2 + (knee2DB  knee1DB)*(cr1  1.0f)/cr1; // Lowlevel gain


}




void setOutputGainOffsetDB(float _gOff) { gainOffsetDB = _gOff; }


void setKnee1LowDB(float _k1) { knee1DB = _k1; }


void setCompressionRatioMiddleDB(float _cr1) { cr1 = _cr1; }


void setKnee2HighDB(float _k2) { knee2DB = _k2; }


void setCompressionRatioHighDB(float _cr2) { cr2 = _cr2; }


// A delay of 256 samples is 256/44100 = 0.0058 sec = 5.8 mSec


void setDelayBufferSize(int16_t _delaySize) { // Any power of 2, i.e., 256, 128, 64, etc.


delaySize = _delaySize;


delayBufferMask = _delaySize  1;


in_index = 0;


}


void printOn(bool _printIO) { printIO = _printIO; } // Diagnostics ONLY. Not for general INO




float getLowLevelGainDB(void) { return gain0DB; }


float getCurrentInputDB(void) { return sampleInputDB; }


float getCurrentGainDB(void) { return sampleGainDB; }




//convert time constants from seconds to unitless parameters, from CHAPRO, agc_prepare.c


void setAttackReleaseSec(const float atk_sec, const float rel_sec) {


// convert ANSI attack & release times to filter time constants


float ansi_atk = atk_sec * sample_rate_Hz / 2.425f;


float ansi_rel = rel_sec * sample_rate_Hz / 1.782f;


alpha = (float) (ansi_atk / (1.0f + ansi_atk));


oneMinusAlpha = 1.0f  alpha;


beta = (float) (ansi_rel / (1.0f + ansi_rel));


}




// Accelerate the powf(10.0,x) function (from Chip's single slope compressor)


float pow10f(float x) {


//return powf(10.0f,x) //standard, but slower


return expf(2.30258509f*x); //faster: exp(log(10.0f)*x)


}




/* See https://github.com/Tympan/Tympan_Library/blob/master/src/AudioCalcGainWDRC_F32.h


* Dr Paul Beckmann


* https://community.arm.com/tools/f/discussions/4292/cmsisdspnewfunctionalityproposal/22621#22621


* Fast approximation to the log2() function. It uses a two step


* process. First, it decomposes the floatingpoint 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.984884e003 dB. Y is log2(X)


*/


float v2DB_Approx(float volts) {


float Y, F;


int E;


// This is the approximation to log2()


F = frexpf(volts, &E); // first separate power of 2;


// Y = C[0]*F*F*F + C[1]*F*F + C[2]*F + C[3] + E;


Y = 1.23149591; //C[0]


Y *= F;


Y += 4.11852516f; //C[1]


Y *= F;


Y += 6.02197014f; //C[2]


Y *= F;


Y += 3.13396450f; //C[3]


Y += E;


// Convert to dB = 20 Log10(volts)


return 6.020599f * Y; // (20.0f/log2(10.0))*Y;


}




private:


audio_block_f32_t *inputQueueArray[1];




float delayData[256]; // The circular delay line for the signal


uint16_t in_index = 0; // Pointer to next block update entry


// And a mask to make the circular buffer limit to a power of 2


uint16_t delayBufferMask = 0X00FF;


uint16_t delaySize = 256;




float sample_rate_Hz = 44100;


float attackSec = 0.005f; // Q: Can this be reduced with the delay line added to the signal path??


float releaseSec = 0.100f;


// This alpha, beta for 5 ms attack, 100ms release, about 0.07 dB max ripple at 1000 Hz


float alpha = 0.98912216f;


float oneMinusAlpha = 0.01087784f;


float beta = 0.9995961f;


// Presets here should be studied/experimented with for each application


float gain0DB = 38.0f; // Gain, in dB for low level inputs


float gainOffsetDB = 0.0f; // Raise/lower entire gain curve by this amount (post gain)


float knee1DB = 50.0f; // First knee on the gain curve


float cr1 = 3.0f; // Compression ratio on dB curve between knee1dB and knee2dB


float knee2DB = 20.0f; // Second knee on the gain curve


float cr2 = 10.0f; // Compression ratio on dB curve above knee2dB


float vPeakSave = 0.0f;


bool printIO = false; // Diagnostics Only


float sampleInputDB, sampleGainDB;


};


#endif


