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.
226 lines
11 KiB
226 lines
11 KiB
4 years ago
|
/*
|
||
|
* AudioEffectCompressor2_F32.h
|
||
|
*
|
||
|
* Bob Larkin W7PUA 11 December 2020
|
||
|
*
|
||
|
* This is a general purpose audio compressor block (C++ class). It works by determining
|
||
|
* the average input of the input signal, and based on a pre-determined curve,
|
||
|
* changes the gain going through the block.
|
||
|
* A good discussion is the Wikipedia page:
|
||
|
* https://en.wikipedia.org/wiki/Dynamic_range_compression
|
||
|
* This compressor includes up to 5 dB/dB line segments allowing for most of the
|
||
|
* features listed. These include
|
||
|
* Multi segment compression curves, up to 5
|
||
|
* Limiting
|
||
|
* Approximation to "soft knees"
|
||
|
* Expansion for suppressing low-level artifacts
|
||
|
* Anticipation
|
||
|
* Scale offset for use such as hearing-aid audiology
|
||
|
* This is derived from the WDRC compressor. 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.
|
||
|
*/
|
||
|
/* Compressor #2. Amplifies input signals by varying amoounts depending
|
||
|
* on the signal level. This is controlled by up to 5 line segments specified
|
||
|
* by a point at the highest input level for the line along with a compression ratio
|
||
|
* (1/slope) for the line segment. This can provide limiting at the highest inputs
|
||
|
* by setting compressionRatio[0]=1000.0 (i.e., some big value). Expansion at the
|
||
|
* lw levels provides a squelch action, using a very small compression
|
||
|
* ratio like 0.01.
|
||
|
*
|
||
|
* A special case is the [0] segment, that continues at the same slope for inputs up
|
||
|
* to any level. For this the kneeDB[0] can be the expected 0.0 or other values
|
||
|
* on that line. The output level for kneeDB[0] is the input variable, marginDB.
|
||
|
* This allows gain control near clipping but below. marginDB is typically 1 or 2 dB.
|
||
|
*
|
||
|
Vout dB
|
||
|
|
|
||
|
|
|
||
|
0.0 + kneeDB[0]
|
||
|
marg + kneeDB[1] @********************
|
||
|
| @************ 1:compressionRatio[0]
|
||
|
| ****
|
||
|
| ***
|
||
|
| ***
|
||
|
| *** 1:compressionRatio[1]
|
||
|
| kneeDB[2] ***
|
||
|
| @***
|
||
|
| *
|
||
|
| *
|
||
|
| *
|
||
|
| *
|
||
|
| * 1:compressionRatio[2]
|
||
|
| *
|
||
|
| * === Vout in dB vs. Vin in dB ===
|
||
|
|* Three segment example
|
||
|
* Knees (breakpoints) are shown with '@'
|
||
|
*| compressionRatio[] are ratio of: input change (in dB):output change in dB
|
||
|
* |
|
||
|
* |________|___________________|____________________________|________ vIn dB
|
||
|
k1 k2 0.0
|
||
|
|
||
|
* The graph shows the changes in gain on a log or dB scale. A compressionRatio
|
||
|
* of 1 represents a constant gain with level. When the compressionRatio is greater
|
||
|
* than 1, say 2.0, the voltage gain is decreasing as the input level increases.
|
||
|
*
|
||
|
* vInDB refers to the time averaged envelope voltage.
|
||
|
* The zero reference is the full ADC range output. This is ±1.0
|
||
|
* peak or 0.707 RMS in F32 terminology.
|
||
|
*
|
||
|
* The curve is for gainOffsetDB = 0.0. This parameter raises and lowers the
|
||
|
* input scale for the kneeDB[] parameter.
|
||
|
*
|
||
|
* Timing: For 44.1 kHz sample rate and 256 samples per update, the update( ) time
|
||
|
* runs 240 to 270 icroseconds using Teensy 3.6.
|
||
|
*/
|
||
|
|
||
|
#ifndef _AUDIO_EFFECT_COMPRESSOR2_F32_H
|
||
|
#define _AUDIO_EFFECT_COMPRESSOR2_F32_H
|
||
|
|
||
|
#include <Arduino.h>
|
||
|
#include <AudioStream_F32.h>
|
||
|
|
||
|
// The following 3 defines are simplified implementations for common uses.
|
||
|
// They replace the begin() function that is otherwise required.
|
||
|
// See testCompressor2.ino example for how to use these defines.
|
||
|
// None of these support offsetting the input scale as done in Tympan.
|
||
|
|
||
|
/* limiterBegin(pointerObject, float marDB, float linearInDB) has 2 segments.
|
||
|
* It is linear up to an input of linearInDB (typically -15.0f) and
|
||
|
* then virtually limits for higher input levels. The output level at
|
||
|
* this point is marDB, the margin to prevent clipping, like -2 dB.
|
||
|
* This is not a clipper with waveform distortion, but rather decreases
|
||
|
* the gain, dB for dB, as the input increases in the limiter region.
|
||
|
* pobject is a pointer to the INO AudioEffectCompressor2_F32 object.
|
||
|
* This function replaces begin() for the AudioEffectCompressor2_F32 object.
|
||
|
*/
|
||
|
#define limiterBegin(pobject, marDB, linearInDB) struct compressionCurve _curv={marDB,0.0f,{0.0,linearInDB,-1000.0f,-1000.0f,-1000.0f},{100.0,1.0f,1.0f,1.0f,1.0f}}; pobject->setCompressionCurve(&_curv); pobject->begin();
|
||
|
|
||
|
/* basicCompressorBegin has a 3 segments. It is linear up to an input linearInDB
|
||
|
* and then decreases gain according to compressionRatioDB up to an input -10 dB where it
|
||
|
* is almost limited, with an increase of output level of 1 dB for a 10 dB increase
|
||
|
* in input level. The output level at full input is 1 dB below full output.
|
||
|
* This function replaces begin() for the AudioEffectCompressor2_F32 object.
|
||
|
*/
|
||
|
#define basicCompressorBegin(pobject, linearInDB, compressionRatio) struct compressionCurve _curv={-1.0,0.0f,{0.0,-10.0f,linearInDB,-1000.0f,-1000.0f},{10.0f,compressionRatio,1.0f,1.0f,1.0f}}; pobject->setCompressionCurve(&_curv); pobject->begin();
|
||
|
|
||
|
/* squelchCompressorBegin is similar to basicCompression above, except that there is
|
||
|
* an expansion region for low levels. So, the call defines the four regions in
|
||
|
* terms of the input levels. squelchInDB sets the lowest input level
|
||
|
* before the squelching effect starts. */
|
||
|
#define squelchCompressorBegin(pobject, squelchInDB, linearInDB, compressionInDB, compressionRatio) struct compressionCurve _curv={-1.0,0.0f,{0.0,compressionInDB,linearInDB,squelchInDB,-1000.0f},{10.0,compressionRatio,1.0f,0.1f,1.0f}}; pobject->setCompressionCurve(&_curv); pobject->begin();
|
||
|
|
||
|
// Basic definition of compression curve:
|
||
|
struct compressionCurve {
|
||
|
float marginDB;
|
||
|
float offsetDB;
|
||
|
float kneeDB[5];
|
||
|
float compressionRatio[5];
|
||
|
};
|
||
|
|
||
|
// ---------------------------------------------------------------------------
|
||
|
|
||
|
class AudioEffectCompressor2_F32 : public AudioStream_F32
|
||
|
{
|
||
|
//GUI: inputs:1, outputs:1 //this line used for automatic generation of GUI node
|
||
|
//GUI: shortName: Compressor2
|
||
|
public:
|
||
|
AudioEffectCompressor2_F32(void): AudioStream_F32(1, inputQueueArray) {
|
||
|
setAttackReleaseSec(0.005f, 0.100f);
|
||
|
}
|
||
|
|
||
|
AudioEffectCompressor2_F32(const AudioSettings_F32 &settings): AudioStream_F32(1, inputQueueArray) {
|
||
|
//setSampleRate_Hz(settings.sample_rate_Hz);
|
||
|
setAttackReleaseSec(0.005f, 0.100f);
|
||
|
}
|
||
|
|
||
|
virtual void update(void);
|
||
|
void begin(void);
|
||
|
void setCompressionCurve(struct compressionCurve*);
|
||
|
// 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 getCurrentInputDB(void) { return sampleInputDB; }
|
||
|
float getCurrentGainDB(void) { return sampleGainDB; }
|
||
|
float getvInMaxDB(void) {
|
||
|
float vRet = vInMaxDB;
|
||
|
vInMaxDB = -1000.0f; // Reset for next max measure
|
||
|
return vRet;
|
||
|
}
|
||
|
//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));
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
|
||
|
/* Definition of the compression curve.
|
||
|
* Input and output are normally referenced to the full scale range (-1.0 to 1.0 in float).
|
||
|
* The full scale point is called 0 dB.
|
||
|
* There are 5 slopes, specified by the top input level for each line segment (kneeDB[])
|
||
|
* and the Compression Ratio (1/slope) of the segment (compressionRatio[]).
|
||
|
* These are numbered from the highest to the lowest allowing
|
||
|
* the number of segments to be adjusted, i.e., there is always a segment 0
|
||
|
* but there may not be a segment 3 and 4, for instance. This becomes very flexible,
|
||
|
* as there can be, say, compression for top levels and expansion for very low levels.
|
||
|
*
|
||
|
* A special case is segment 0. On a plot of output dB vs input db, an input
|
||
|
* level of kneeDB[0] produces an output of marginDB. A typical marginDB might
|
||
|
* be -1.0 or -2.0. This margin allows controlling the gain without clipping in the DAC.
|
||
|
*
|
||
|
* This structure can have multiple curve structures like cCurve that can be used by
|
||
|
* cmpr1.setCompressionCurve(&cCurve);
|
||
|
* cmpr1.begin();
|
||
|
*
|
||
|
* Unused segments can have knees like kneeDB[4] at, say -1000.0f.
|
||
|
* Values below -500 will slightly speed up the update() function. if the bottom
|
||
|
* two segments are not used, kneeDB[3] would also be set to -1000.0f amd so forth.
|
||
|
*
|
||
|
* Finally, there is a variable offsetDB that allows for a shift in the input scale.
|
||
|
* It simply shifts the definition input levels, in dB.
|
||
|
* This allows converting from DSP scales that have 0dB at full DSP scale
|
||
|
* to auditory scales such as are used in the Tympan library. An offsetDB=119.0
|
||
|
* would allow all inputs to be in "SPL" units with a maximum input value of 119.
|
||
|
*/
|
||
|
struct compressionCurve curve0 = { -2.0f, 0.0f, // margin, offset
|
||
|
{0.0f, -10.0f, -20.0f, -30.0f, -1000.0f}, // kneeDB[]
|
||
|
{ 100.0f, 2.0f, 1.5f, 1.0f, 1.0f} }; // compressionRatio
|
||
|
// VoutDB at each knee, needed to find gain; determined at begin():
|
||
|
float outKneeDB[5]={-2.0f, -3.0f, -8.0f -978.0f, -978.0f};
|
||
|
// slopes are 1/compressionRatio determined at begin() to save update{} time
|
||
|
float slope[5] = {0.01f, 0.5f, 0.666667f, 1.0f, 1.0f};
|
||
|
|
||
|
// Save time in update() by ignoring unused (low level) segments:
|
||
|
uint16_t firstIndex = 3;
|
||
|
float vPeakSave = 0.0f;
|
||
|
float vInMaxDB = -1000.0f; // Only for reporting
|
||
|
bool printIO = false; // Diagnostics Only
|
||
|
float sampleInputDB, sampleGainDB;
|
||
|
};
|
||
|
#endif
|