|
|
|
@ -1,7 +1,7 @@ |
|
|
|
|
/*
|
|
|
|
|
AudioEffectCompressor |
|
|
|
|
|
|
|
|
|
Created: Chip Audette, December 2016 |
|
|
|
|
Created: Chip Audette, Dec 2016 - Jan 2017 |
|
|
|
|
Purpose; Apply dynamic range compression to the audio stream. |
|
|
|
|
Assumes floating-point data. |
|
|
|
|
|
|
|
|
@ -10,8 +10,8 @@ |
|
|
|
|
MIT License. use at your own risk. |
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
#ifndef _AudioEffectCompressor |
|
|
|
|
#define _AudioEffectCompressor |
|
|
|
|
#ifndef _AudioEffectCompressor_F32 |
|
|
|
|
#define _AudioEffectCompressor_F32 |
|
|
|
|
|
|
|
|
|
#include <arm_math.h> //ARM DSP extensions. https://www.keil.com/pack/doc/CMSIS/DSP/html/index.html |
|
|
|
|
#include <AudioStream_F32.h> |
|
|
|
@ -21,20 +21,18 @@ class AudioEffectCompressor_F32 : public AudioStream_F32 |
|
|
|
|
public: |
|
|
|
|
//constructor
|
|
|
|
|
AudioEffectCompressor_F32(void) : AudioStream_F32(1, inputQueueArray_f32) { |
|
|
|
|
setThresh_dBFS(-20.0f); //default to this threshold
|
|
|
|
|
setThresh_dBFS(-20.0f); //set the default value for the threshold for compression
|
|
|
|
|
setCompressionRatio(5.0f); //set the default copression ratio
|
|
|
|
|
setAttack_sec(0.005f, AUDIO_SAMPLE_RATE); //default to this value
|
|
|
|
|
setRelease_sec(0.200f, AUDIO_SAMPLE_RATE); //default to this value
|
|
|
|
|
setCompressionRatio(5.0f); //default to this value
|
|
|
|
|
setThresh_dBFS(-20.0f); //default to this value
|
|
|
|
|
setHPFilterCoeff(); |
|
|
|
|
setHPFilterCoeff(); enableHPFilter(true); //enable the HP filter to remove any DC offset from the audio
|
|
|
|
|
resetStates(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
//here's the method that does all the work
|
|
|
|
|
void update(void) { |
|
|
|
|
//Serial.println("AudioEffectGain_F32: updating."); //for debugging.
|
|
|
|
|
audio_block_f32_t *audio_block; |
|
|
|
|
audio_block = AudioStream_F32::receiveWritable_f32(); |
|
|
|
|
audio_block_f32_t *audio_block = AudioStream_F32::receiveWritable_f32(); |
|
|
|
|
if (!audio_block) return; |
|
|
|
|
|
|
|
|
|
//apply a high-pass filter to get rid of the DC offset
|
|
|
|
@ -43,61 +41,134 @@ class AudioEffectCompressor_F32 : public AudioStream_F32 |
|
|
|
|
//apply the pre-gain...a negative gain value will disable
|
|
|
|
|
if (pre_gain > 0.0f) arm_scale_f32(audio_block->data, pre_gain, audio_block->data, audio_block->length); //use ARM DSP for speed!
|
|
|
|
|
|
|
|
|
|
//compute the desired gain
|
|
|
|
|
//calculate the level of the audio (ie, calculate a smoothed version of the signal power)
|
|
|
|
|
audio_block_f32_t *audio_level_dB_block = AudioStream_F32::allocate_f32(); |
|
|
|
|
calcAudioLevel_dB(audio_block, audio_level_dB_block); //returns through audio_level_dB_block
|
|
|
|
|
|
|
|
|
|
//compute the desired gain based on the observed audio level
|
|
|
|
|
audio_block_f32_t *gain_block = AudioStream_F32::allocate_f32(); |
|
|
|
|
calcGain(audio_block, gain_block); //returns through gain_block
|
|
|
|
|
calcGain(audio_level_dB_block, gain_block); //returns through gain_block
|
|
|
|
|
|
|
|
|
|
//apply the gain...store it back into audio_block
|
|
|
|
|
//apply the desired gain...store the processed audio back into audio_block
|
|
|
|
|
arm_mult_f32(audio_block->data, gain_block->data, audio_block->data, audio_block->length); |
|
|
|
|
|
|
|
|
|
///transmit the block and release memory
|
|
|
|
|
AudioStream_F32::transmit(audio_block); |
|
|
|
|
AudioStream_F32::release(audio_block); |
|
|
|
|
AudioStream_F32::release(gain_block); |
|
|
|
|
AudioStream_F32::release(audio_level_dB_block); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void calcGain(audio_block_f32_t *wav_block, audio_block_f32_t *gain_block) {
|
|
|
|
|
|
|
|
|
|
//calculate the signal power...ie, square the signal: wav_pow = wav.^2
|
|
|
|
|
void calcAudioLevel_dB(audio_block_f32_t *wav_block, audio_block_f32_t *level_dB_block) {
|
|
|
|
|
//calculate the instantaneous signal power (square the signal)
|
|
|
|
|
audio_block_f32_t *wav_pow_block = AudioStream_F32::allocate_f32(); |
|
|
|
|
arm_mult_f32(wav_block->data, wav_block->data, wav_pow_block->data, wav_block->length); |
|
|
|
|
|
|
|
|
|
//loop over each sample
|
|
|
|
|
float32_t gain_pow; |
|
|
|
|
//apply a smoothing filter to wav_pow_block and convert to dB
|
|
|
|
|
float c1 = level_lp_const; |
|
|
|
|
float c2 = 1.0f - c1; |
|
|
|
|
for (int i = 0; i < wav_pow_block->length; i++) { |
|
|
|
|
|
|
|
|
|
//compute target gain (well, we're actualy calculating gain^2) assuming we want to copress
|
|
|
|
|
gain_pow = thresh_pow_FS_wCR / powf(wav_pow_block->data[i], comp_ratio_const); |
|
|
|
|
wav_pow_block->data[i] = c1*prev_level_lp_pow + c2*wav_pow_block->data[i]; |
|
|
|
|
|
|
|
|
|
//if our signal level is below the threshold, don't compress (set target gain to 0dB, which is 1.0)
|
|
|
|
|
if (wav_pow_block->data[i] < thresh_pow_FS) gain_pow = 1.0f; |
|
|
|
|
//save state for next time (and for next data block)
|
|
|
|
|
prev_level_lp_pow = wav_pow_block->data[i]; |
|
|
|
|
|
|
|
|
|
//are we in the attack mode or release mode?
|
|
|
|
|
float32_t c = attack_const; //at first, assume that we're in the attack phase
|
|
|
|
|
if (gain_pow > prev_gain_pow) c = release_const; //here, we decide if we're really in the release phase
|
|
|
|
|
//convert to dB (but not yet multiplied by 10.0
|
|
|
|
|
level_dB_block->data[i] = log10f(wav_pow_block->data[i]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//smooth the gain using the attack or release constants
|
|
|
|
|
gain_pow = c*prev_gain_pow + (1.0f-c)*gain_pow; |
|
|
|
|
//limit the amount that the state of the smoothing filter can go toward negative infinity
|
|
|
|
|
if (prev_level_lp_pow < (1.0E-13)) prev_level_lp_pow = 1.0E-13; //never go less than -130 dBFS
|
|
|
|
|
|
|
|
|
|
//take he sqrt of gain^2 so that we simply get the gain
|
|
|
|
|
//arm_sqrt_f32(gain_pow, &(gain_block->data[i])); //should use the DSP acceleration, if the right CMSIS library is used
|
|
|
|
|
//gain_block->data[i] = __builtin_sqrtf(gain_pow); //seems to give the same speed as the arm_sqrt_f32
|
|
|
|
|
gain_block->data[i] = sqrtf(gain_pow); //also give the same speed and is more portable
|
|
|
|
|
//scale the wav_pow_block by 10.0 to complete the conversion to dB
|
|
|
|
|
arm_scale_f32(level_dB_block->data, 10.0f, level_dB_block->data, level_dB_block->length); //use ARM DSP for speed!
|
|
|
|
|
|
|
|
|
|
//release memory and return
|
|
|
|
|
AudioStream_F32::release(wav_pow_block); |
|
|
|
|
return; //output is passed through level_dB_block
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void calcGain(audio_block_f32_t *audio_level_dB_block, audio_block_f32_t *gain_block) {
|
|
|
|
|
|
|
|
|
|
//first, calculate the instantaneous target gain
|
|
|
|
|
audio_block_f32_t *inst_targ_gain_dB_block = AudioStream_F32::allocate_f32();
|
|
|
|
|
calcInstantaneousTargetGain(audio_level_dB_block, inst_targ_gain_dB_block); |
|
|
|
|
|
|
|
|
|
//second, smooth in time (attack and release) by stepping through each sample
|
|
|
|
|
audio_block_f32_t *gain_dB_block = AudioStream_F32::allocate_f32();
|
|
|
|
|
calcSmoothedGain_dB(inst_targ_gain_dB_block,gain_dB_block); |
|
|
|
|
|
|
|
|
|
//finally, convert from dB to linear gain: gain = 10^(gain_dB/20)
|
|
|
|
|
arm_scale_f32(gain_dB_block->data, 1.0f/20.0f, gain_dB_block->data, gain_dB_block->length); //divide by 20
|
|
|
|
|
for (int i = 0; i < gain_dB_block->length; i++) gain_block->data[i] = powf(10.0f,gain_dB_block->data[i]); //10^(x)
|
|
|
|
|
|
|
|
|
|
//release memory and return
|
|
|
|
|
AudioStream_F32::release(gain_dB_block); |
|
|
|
|
AudioStream_F32::release(inst_targ_gain_dB_block); |
|
|
|
|
return; //output is passed through gain_block
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void calcInstantaneousTargetGain(audio_block_f32_t *audio_level_dB_block, audio_block_f32_t *inst_targ_gain_dB_block) { |
|
|
|
|
|
|
|
|
|
// how much are we above the compression threshold?
|
|
|
|
|
audio_block_f32_t *above_thresh_dB_block = AudioStream_F32::allocate_f32();
|
|
|
|
|
arm_offset_f32(audio_level_dB_block->data, //CMSIS DSP for "add a constant value to all elements"
|
|
|
|
|
-thresh_dBFS, //this is the value to be added
|
|
|
|
|
above_thresh_dB_block->data, //this is the output
|
|
|
|
|
audio_level_dB_block->length);
|
|
|
|
|
|
|
|
|
|
// scale by the compression ratio...this is what the output level should be (this is our target level)
|
|
|
|
|
arm_scale_f32(above_thresh_dB_block->data, //CMSIS DSP for "multiply all elements by a constant value"
|
|
|
|
|
1.0f / comp_ratio, //this is the value to be multiplied
|
|
|
|
|
inst_targ_gain_dB_block->data, //this is the output
|
|
|
|
|
above_thresh_dB_block->length);
|
|
|
|
|
|
|
|
|
|
// compute the instantaneous gai...which is the difference between the target level and the original level
|
|
|
|
|
arm_sub_f32(inst_targ_gain_dB_block->data, //CMSIS DSP for "subtract two vectors element-by-element"
|
|
|
|
|
above_thresh_dB_block->data, //this is the vector to be subtracted
|
|
|
|
|
inst_targ_gain_dB_block->data, //this is the output
|
|
|
|
|
inst_targ_gain_dB_block->length); |
|
|
|
|
|
|
|
|
|
// limit the target gain to attenuation only (this part of the compressor should not make things louder!)
|
|
|
|
|
for (int i=0; i < inst_targ_gain_dB_block->length; i++) { |
|
|
|
|
if (inst_targ_gain_dB_block->data[i] > 0.0f) inst_targ_gain_dB_block->data[i] = 0.0f; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// release memory before returning
|
|
|
|
|
AudioStream_F32::release(above_thresh_dB_block); |
|
|
|
|
return; //output is passed through inst_targ_gain_dB_block
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void calcSmoothedGain_dB(audio_block_f32_t *inst_targ_gain_dB_block, audio_block_f32_t *gain_dB_block) { |
|
|
|
|
float32_t gain_dB; |
|
|
|
|
float32_t one_minus_attack_const = 1.0f - attack_const; |
|
|
|
|
float32_t one_minus_release_const = 1.0f - release_const; |
|
|
|
|
for (int i = 0; i < inst_targ_gain_dB_block->length; i++) { |
|
|
|
|
gain_dB = inst_targ_gain_dB_block->data[i]; |
|
|
|
|
|
|
|
|
|
//smooth the gain using the attack or release constants
|
|
|
|
|
if (gain_dB < prev_gain_dB) { //are we in the attack phase?
|
|
|
|
|
gain_dB_block->data[i] = attack_const*prev_gain_dB + one_minus_attack_const*gain_dB; |
|
|
|
|
} else { //or, we're in the release phase
|
|
|
|
|
gain_dB_block->data[i] = release_const*prev_gain_dB + one_minus_release_const*gain_dB; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//save value for the next time through this loop
|
|
|
|
|
prev_gain_pow = gain_pow; |
|
|
|
|
prev_gain_dB = gain_dB_block->data[i]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//free up the memory and return
|
|
|
|
|
release(wav_pow_block); |
|
|
|
|
//return
|
|
|
|
|
return; //the output here is gain_block
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//methods to set parameters of this module
|
|
|
|
|
void resetStates(void) { |
|
|
|
|
prev_gain_pow = 1.0f; |
|
|
|
|
|
|
|
|
|
//initialize the HP filter (it also resets the filter states)
|
|
|
|
|
prev_level_lp_pow = 1.0f; |
|
|
|
|
prev_gain_dB = 0.0f; |
|
|
|
|
|
|
|
|
|
//initialize the HP filter. (This also resets the filter states,)
|
|
|
|
|
arm_biquad_cascade_df1_init_f32(&hp_filt_struct, hp_nstages, hp_coeff, hp_state); |
|
|
|
|
} |
|
|
|
|
void setPreGain(float g) { pre_gain = g; } |
|
|
|
@ -109,30 +180,43 @@ class AudioEffectCompressor_F32 : public AudioStream_F32 |
|
|
|
|
void setAttack_sec(float a, float fs_Hz) { |
|
|
|
|
attack_sec = a; |
|
|
|
|
attack_const = expf(-1.0f / (attack_sec * fs_Hz)); //expf() is much faster than exp()
|
|
|
|
|
|
|
|
|
|
//also update the time constant for the envelope extraction
|
|
|
|
|
setLevelTimeConst_sec(min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants
|
|
|
|
|
}
|
|
|
|
|
void setRelease_sec(float r, float fs_Hz) { |
|
|
|
|
release_sec = r; |
|
|
|
|
release_const = expf(-1.0f / (release_sec * fs_Hz)); //expf() is much faster than exp()
|
|
|
|
|
}
|
|
|
|
|
void setThresh_dBFS(float thresh_dBFS) { setThreshPow(pow(10.0, thresh_dBFS / 10.0)); } |
|
|
|
|
void setThreshPow(float t_pow) {
|
|
|
|
|
thresh_pow_FS = t_pow; |
|
|
|
|
updateThresholdAndCompRatioConstants(); |
|
|
|
|
|
|
|
|
|
//also update the time constant for the envelope extraction
|
|
|
|
|
setLevelTimeConst_sec(min(attack_sec,release_sec) / 5.0, fs_Hz); //make the level time-constant one-fifth the gain time constants
|
|
|
|
|
} |
|
|
|
|
void setLevelTimeConst_sec(float t_sec, float fs_Hz) { |
|
|
|
|
const float min_t_sec = 0.002f; //this is the minimum allowed value
|
|
|
|
|
level_lp_sec = max(min_t_sec,t_sec); |
|
|
|
|
level_lp_const = expf(-1.0f / (level_lp_sec * fs_Hz)); //expf() is much faster than exp()
|
|
|
|
|
} |
|
|
|
|
void setThresh_dBFS(float val) {
|
|
|
|
|
thresh_dBFS = val; |
|
|
|
|
setThreshPow(pow(10.0, thresh_dBFS / 10.0)); |
|
|
|
|
} |
|
|
|
|
void enableHPFilter(boolean flag) { use_HP_prefilter = flag; }; |
|
|
|
|
|
|
|
|
|
//methods to return information about this module
|
|
|
|
|
float32_t getPreGain_dB(void) { return 20.0 * log10(pre_gain); } |
|
|
|
|
float32_t getPreGain_dB(void) { return 20.0 * log10f(pre_gain); } |
|
|
|
|
float32_t getAttack_sec(void) { return attack_sec; } |
|
|
|
|
float32_t getRelease_sec(void) { return release_sec; } |
|
|
|
|
float32_t getThresh_dBFS(void) { return 10.0 * log10(thresh_pow_FS); } |
|
|
|
|
float32_t getLevelTimeConst_sec(void) { return level_lp_sec; } |
|
|
|
|
float32_t getThresh_dBFS(void) { return thresh_dBFS; } |
|
|
|
|
float32_t getCompressionRatio(void) { return comp_ratio; } |
|
|
|
|
float32_t getCurrentGain_dB(void) { return 10.0 * log10(prev_gain_pow); } |
|
|
|
|
float32_t getCurrentLevel_dBFS(void) { return 10.0* log10f(prev_level_lp_pow); } |
|
|
|
|
float32_t getCurrentGain_dB(void) { return prev_gain_dB; } |
|
|
|
|
|
|
|
|
|
private: |
|
|
|
|
//state-related variables
|
|
|
|
|
audio_block_f32_t *inputQueueArray_f32[1]; //memory pointer for the input to this module
|
|
|
|
|
float32_t prev_gain_pow = 1.0; //last gain^2 used
|
|
|
|
|
float32_t prev_level_lp_pow = 1.0; |
|
|
|
|
float32_t prev_gain_dB = 0.0; //last gain^2 used
|
|
|
|
|
|
|
|
|
|
//HP filter state-related variables
|
|
|
|
|
arm_biquad_casd_df1_inst_f32 hp_filt_struct; |
|
|
|
@ -149,7 +233,7 @@ class AudioEffectCompressor_F32 : public AudioStream_F32 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//private parameters related to gain calculation
|
|
|
|
|
float32_t attack_const, release_const; //used in calcGain(). set by setAttack_sec() and setRelease_sec();
|
|
|
|
|
float32_t attack_const, release_const, level_lp_const; //used in calcGain(). set by setAttack_sec() and setRelease_sec();
|
|
|
|
|
float32_t comp_ratio_const, thresh_pow_FS_wCR; //used in calcGain(); set in updateThresholdAndCompRatioConstants()
|
|
|
|
|
void updateThresholdAndCompRatioConstants(void) { |
|
|
|
|
comp_ratio_const = 1.0f-(1.0f / comp_ratio); |
|
|
|
@ -157,12 +241,18 @@ class AudioEffectCompressor_F32 : public AudioStream_F32 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//settings
|
|
|
|
|
float32_t attack_sec, release_sec;
|
|
|
|
|
float32_t thresh_pow_FS = 1.0f; //threshold for compression, relative to digital full scale
|
|
|
|
|
float32_t attack_sec, release_sec, level_lp_sec;
|
|
|
|
|
float32_t thresh_dBFS = 0.0; //threshold for compression, relative to digital full scale
|
|
|
|
|
float32_t thresh_pow_FS = 1.0f; //same as above, but not in dB
|
|
|
|
|
void setThreshPow(float t_pow) {
|
|
|
|
|
thresh_pow_FS = t_pow; |
|
|
|
|
updateThresholdAndCompRatioConstants(); |
|
|
|
|
} |
|
|
|
|
float32_t comp_ratio = 1.0; //compression ratio
|
|
|
|
|
float32_t pre_gain = -1.0; //gain to apply before the compression. negative value disables
|
|
|
|
|
boolean use_HP_prefilter = false; |
|
|
|
|
boolean use_HP_prefilter; |
|
|
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
#endif |
|
|
|
|
|
|
|
|
|