DSP Compressor: Sidechain, Exponential Atk/Rls

This is an improvement to the current compressor which I have added
to my own Sansa Fuze V2 build.  I am submitting here in case others
find it interesting.

Features added to the existing compressor:
Attack, Look-ahead, Sidechain Filtering.
Exponential attack and release characteristic response.

Benefits from adding missing features:
Attack:
Preserve perceived "brightness" of tone by letting onset transients
come through at a higher level than the rest of the compressed program
material.

Look-ahead:
With Attack comes clipping on the leading several cycles of a transient
onset.  With look-ahead function, this can be pre-emptively mitigated with
a slower gain change (less distortion).  Look-ahead limiting is implemented
to prevent clipping while keeping gain change ramp to an interval near 3ms
instead of instant attack.

The existing compressor implementation distorts the leading edge of a
transient by causing instant gain change, resulting in log() distortion.
This sounds "woofy" to me.

Exponential Attack/Release:
eMore natural sounding.  On attack, this is a true straight line of 10dB per
attack interval.  Release is a little different, however, sounds natural as
an analog compressor.

Sidechain Filtering:
Mild high-pass filter reduces response to low frequency onsets.  For example,
a hard kick drum is less likely to make the whole of the program material
appear to fade in and out.  Combined with a moderate attack time, such a
transient will ride through with minimal audible artifact.

Overall these changes make dynamic music sound more "open", more natural.  The
goal of a compressor is to make dyanamic music sound louder without necessarily
sounding as though it has been compressed.  I believe these changes come closer to this goal.

Enjoy.  If not, I am enjoying it

Change-Id: I664eace546c364b815b4dc9ed4a72849231a0eb2
Reviewed-on: http://gerrit.rockbox.org/626
Tested: Purling Nayuki <cyq.yzfl@gmail.com>
Reviewed-by: Michael Giacomelli <giac2000@hotmail.com>
This commit is contained in:
Ryan Billing 2013-10-04 01:57:00 +13:00 committed by Michael Giacomelli
parent 5b5f0755d6
commit d0918b98fa
7 changed files with 323 additions and 91 deletions

View file

@ -11910,6 +11910,23 @@
swcodec: "Soft Knee"
</voice>
</phrase>
<phrase>
id: LANG_COMPRESSOR_ATTACK
desc: in sound settings
user: core
<source>
*: none
swcodec: "Attack Time"
</source>
<dest>
*: none
swcodec: "Attack Time"
</dest>
<voice>
*: none
swcodec: "Attack Time"
</voice>
</phrase>
<phrase>
id: LANG_COMPRESSOR_RELEASE
desc: in sound settings

View file

@ -140,12 +140,15 @@ static int timestretch_callback(int action,const struct menu_item_ex *this_item)
MENUITEM_SETTING(compressor_knee,
&global_settings.compressor_settings.knee,
lowlatency_callback);
MENUITEM_SETTING(compressor_attack,
&global_settings.compressor_settings.attack_time,
lowlatency_callback);
MENUITEM_SETTING(compressor_release,
&global_settings.compressor_settings.release_time,
lowlatency_callback);
MAKE_MENU(compressor_menu,ID2P(LANG_COMPRESSOR), NULL, Icon_NOICON,
&compressor_threshold, &compressor_gain, &compressor_ratio,
&compressor_knee, &compressor_release);
&compressor_knee, &compressor_attack, &compressor_release);
#endif
#if (CONFIG_CODEC == MAS3587F) || (CONFIG_CODEC == MAS3539F)

View file

@ -1653,7 +1653,11 @@ const struct settings_list settings[] = {
CHOICE_SETTING(F_SOUNDSETTING|F_NO_WRAP, compressor_settings.knee,
LANG_COMPRESSOR_KNEE, 1, "compressor knee",
"hard knee,soft knee", compressor_set, 2,
ID2P(LANG_COMPRESSOR_HARD_KNEE), ID2P(LANG_COMPRESSOR_SOFT_KNEE)),
ID2P(LANG_COMPRESSOR_HARD_KNEE), ID2P(LANG_COMPRESSOR_SOFT_KNEE)),
INT_SETTING_NOWRAP(F_SOUNDSETTING, compressor_settings.attack_time,
LANG_COMPRESSOR_ATTACK, 5,
"compressor attack time", UNIT_MS, 0, 30,
5, NULL, NULL, compressor_set),
INT_SETTING_NOWRAP(F_SOUNDSETTING, compressor_settings.release_time,
LANG_COMPRESSOR_RELEASE, 500,
"compressor release time", UNIT_MS, 100, 1000,

View file

@ -632,6 +632,7 @@ Vanja Cvelbar
Richard Quirk
Kirill Stryaponoff
Roman Poltoradnev
Ryan Billing
The libmad team
The wavpack team

View file

@ -23,44 +23,155 @@
#include "fracmul.h"
#include <string.h>
/* Define LOGF_ENABLE to enable logf output in this file */
/*#define LOGF_ENABLE*/
/* Define LOGF_ENABLE to enable logf output in this file
* #define LOGF_ENABLE
*/
#include "logf.h"
#include "dsp_proc_entry.h"
#include "compressor.h"
#include "dsp_misc.h"
#define UNITY (1L << 24) /* unity gain in S7.24 format */
#define MAX_DLY 960 /* Max number of samples to delay
output (960 = 5ms @ 192 kHz)
*/
#define MAX_CH 4 /* Is there a good malloc() or equal
for rockbox?
*/
#define DLY_TIME 3 /* milliseconds */
static struct compressor_settings curr_set; /* Cached settings */
static int32_t comp_rel_slope IBSS_ATTR; /* S7.24 format */
static int32_t comp_makeup_gain IBSS_ATTR; /* S7.24 format */
static int32_t comp_curve[66] IBSS_ATTR; /* S7.24 format */
static int32_t release_gain IBSS_ATTR; /* S7.24 format */
static int32_t comp_makeup_gain IBSS_ATTR; /* S7.24 format */
static int32_t comp_curve[66] IBSS_ATTR; /* S7.24 format */
static int32_t release_gain IBSS_ATTR; /* S7.24 format */
static int32_t release_holdoff IBSS_ATTR; /* S7.24 format */
#define UNITY (1L << 24) /* unity gain in S7.24 format */
/* 1-pole filter coefficients for exponential attack/release times */
static int32_t rlsca IBSS_ATTR; /* Release 'alpha' */
static int32_t rlscb IBSS_ATTR; /* Release 'beta' */
static int32_t attca IBSS_ATTR; /* Attack 'alpha' */
static int32_t attcb IBSS_ATTR; /* Attack 'beta' */
static int32_t limitca IBSS_ATTR; /* Limiter Attack 'alpha' */
/* 1-pole filter coefficients for sidechain pre-emphasis filters */
static int32_t hp1ca IBSS_ATTR; /* hpf1 'alpha' */
static int32_t hp2ca IBSS_ATTR; /* hpf2 'beta' */
/* 1-pole hp filter state variables for pre-emphasis filters */
static int32_t hpfx1 IBSS_ATTR; /* hpf1 and hpf2 x[n-1] */
static int32_t hp1y1 IBSS_ATTR; /* hpf2 y[n-1] */
static int32_t hp2y1 IBSS_ATTR; /* hpf2 y[n-1] */
/* Delay Line for look-ahead compression */
static int32_t labuf[MAX_CH][MAX_DLY]; /* look-ahead buffer */
static int32_t delay_time;
static int32_t delay_write;
static int32_t delay_read;
/** 1-Pole LP Filter first coefficient computation
* Returns S7.24 format integer used for "a" coefficient
* rc: "RC Time Constant", or time to decay to 1/e
* fs: Sampling Rate
* Interpret attack and release time as an RC time constant
* (time to decay to 1/e)
* 1-pole filters use approximation
* a0 = 1/(fs*rc + 1)
* b1 = 1.0 - a0
* fs = Sampling Rate
* rc = Time to decay to 1/e
* y[n] = a0*x[n] + b1*y[n-1]
*
* According to simulation on Intel hardware
* this algorithm produces < 2% error for rc < ~100ms
* For rc 100ms - 1000ms, error approaches 0%
* For compressor attack/release times, this is more than adequate.
*
* Error was measured against the more rigorous computation:
* a0 = 1.0 - e^(-1.0/(fs*rc))
*/
int32_t get_lpf_coeff(int32_t rc, int32_t fs, int32_t rc_units)
{
int32_t c = fs*rc;
c /= rc_units;
c += 1;
c = UNITY/c;
return c;
}
/** Coefficients to get 10dB change per time period "rc"
* from 1-pole LP filter topology
* This function is better used to match behavior of
* linear release which was implemented prior to implementation
* of exponential attack/release function
*/
int32_t get_att_rls_coeff(int32_t rc, int32_t fs)
{
int32_t c = UNITY/fs;
c *= 1152; /* 1000 * 10/( 20*log10( 1/e ) ) */
c /= rc;
return c;
}
/** COMPRESSOR UPDATE
* Called via the menu system to configure the compressor process */
* Called via the menu system to configure the compressor process
*/
static bool compressor_update(struct dsp_config *dsp,
const struct compressor_settings *settings)
{
/* make settings values useful */
int threshold = settings->threshold;
bool auto_gain = settings->makeup_gain == 1;
int threshold = settings->threshold;
bool auto_gain = settings->makeup_gain == 1;
static const int comp_ratios[] = { 2, 4, 6, 10, 0 };
int ratio = comp_ratios[settings->ratio];
bool soft_knee = settings->knee == 1;
int release = settings->release_time *
dsp_get_output_frequency(dsp) / 1000;
int ratio = comp_ratios[settings->ratio];
bool soft_knee = settings->knee == 1;
int32_t release = settings->release_time;
int32_t attack = settings->attack_time;
bool changed = settings == &curr_set; /* If frequency change */
/* Compute Attack and Release Coefficients */
int32_t fs = dsp_get_output_frequency(dsp);
/* Release */
rlsca = get_att_rls_coeff(release, fs);
rlscb = UNITY - rlsca ;
/* Attack */
if(attack > 0)
{
attca = get_att_rls_coeff(attack, fs);
attcb = UNITY - attca ;
}
else {
attca = UNITY;
attcb = 0;
}
/* Sidechain pre-emphasis filter coefficients */
hp1ca = fs + 0x003C1; /** The "magic" constant is 1/RC. This filter
* cut-off is approximately 237 Hz
*/
hp1ca = UNITY/hp1ca;
hp1ca *= fs;
hp2ca = fs + 0x02065; /* The "magic" constant is 1/RC. This filter
* cut-off is approximately 2.18 kHz
*/
hp2ca = UNITY/hp2ca;
hp2ca *= fs;
bool changed = settings == &curr_set; /* If frequency changes */
bool active = threshold < 0;
if (memcmp(settings, &curr_set, sizeof (curr_set)))
{
/* Compressor settings have changed since last call */
changed = true;
#if defined(ROCKBOX_HAS_LOGF) && defined(LOGF_ENABLE)
if (settings->threshold != curr_set.threshold)
{
@ -91,6 +202,10 @@ static bool compressor_update(struct dsp_config *dsp,
{
logf(" Compressor Release: %d", release);
}
if (settings->attack_time != cur_set.attack_time)
{
logf(" Compressor Attack: %d", attack);
}
#endif
curr_set = *settings;
@ -125,18 +240,18 @@ static bool compressor_update(struct dsp_config *dsp,
int32_t offset; /* S15.16 format */
} db_curve[5];
/** Set up the shape of the compression curve first as decibel
values */
/* db_curve[0] = bottom of knee
[1] = threshold
[2] = top of knee
[3] = 0 db input
[4] = ~+12db input (2 bits clipping overhead) */
/** Set up the shape of the compression curve first as decibel values
* db_curve[0] = bottom of knee
* [1] = threshold
* [2] = top of knee
* [3] = 0 db input
* [4] = ~+12db input (2 bits clipping overhead)
*/
db_curve[1].db = threshold << 16;
if (soft_knee)
{
/* bottom of knee is 3dB below the threshold for soft knee*/
/* bottom of knee is 3dB below the threshold for soft knee */
db_curve[0].db = db_curve[1].db - (3 << 16);
/* top of knee is 3dB above the threshold for soft knee */
db_curve[2].db = db_curve[1].db + (3 << 16);
@ -175,24 +290,28 @@ static bool compressor_update(struct dsp_config *dsp,
}
/** Now set up the comp_curve table with compression offsets in the
form of gain factors in S7.24 format */
/* comp_curve[0] is 0 (-infinity db) input */
* form of gain factors in S7.24 format
* comp_curve[0] is 0 (-infinity db) input
*/
comp_curve[0] = UNITY;
/* comp_curve[1 to 63] are intermediate compression values
corresponding to the 6 MSB of the input values of a non-clipped
signal */
/** comp_curve[1 to 63] are intermediate compression values
* corresponding to the 6 MSB of the input values of a non-clipped
* signal
*/
for (int i = 1; i < 64; i++)
{
/* db constants are stored as positive numbers;
make them negative here */
/** db constants are stored as positive numbers;
* make them negative here
*/
int32_t this_db = -db[i];
/* no compression below the knee */
if (this_db <= db_curve[0].db)
comp_curve[i] = UNITY;
/* if soft knee and below top of knee,
interpolate along soft knee slope */
/** if soft knee and below top of knee,
* interpolate along soft knee slope
*/
else if (soft_knee && (this_db <= db_curve[2].db))
comp_curve[i] = fp_factor(fp_mul(
((this_db - db_curve[0].db) / 6),
@ -204,14 +323,22 @@ static bool compressor_update(struct dsp_config *dsp,
fp_div((db_curve[1].db - this_db), db_curve[1].db, 16),
db_curve[3].offset, 16), 16) << 8;
}
/* comp_curve[64] is the compression level of a maximum level,
non-clipped signal */
/** comp_curve[64] is the compression level of a maximum level,
* non-clipped signal
*/
comp_curve[64] = fp_factor(db_curve[3].offset, 16) << 8;
/* comp_curve[65] is the compression level of a maximum level,
clipped signal */
/** comp_curve[65] is the compression level of a maximum level,
* clipped signal
*/
comp_curve[65] = fp_factor(db_curve[4].offset, 16) << 8;
/** if using auto peak, then makeup gain is max offset -
* 3dB headroom
*/
comp_makeup_gain = auto_gain ?
fp_factor(-(db_curve[3].offset) - 0x4AC4, 16) << 8 : UNITY;
#if defined(ROCKBOX_HAS_LOGF) && defined(LOGF_ENABLE)
logf("\n *** Compression Offsets ***");
/* some settings for display only, not used in calculations */
@ -233,20 +360,10 @@ static bool compressor_update(struct dsp_config *dsp,
if (i % 4 == 0) DEBUGF("\n");
}
DEBUGF("\n");
logf("Makeup gain:\t%.6f", (float)comp_makeup_gain / UNITY);
#endif
/* if using auto peak, then makeup gain is max offset -
.1dB headroom */
comp_makeup_gain = auto_gain ?
fp_factor(-(db_curve[3].offset) - 0x199A, 16) << 8 : UNITY;
logf("Makeup gain:\t%.6f", (float)comp_makeup_gain / UNITY);
/* calculate per-sample gain change a rate of 10db over release time
*/
comp_rel_slope = 0xAF0BB2 / release;
logf("Release slope:\t%.6f", (float)comp_rel_slope / UNITY);
release_gain = UNITY;
return active;
}
@ -258,39 +375,41 @@ static inline int32_t get_compression_gain(struct sample_format *format,
int32_t sample)
{
const int frac_bits_offset = format->frac_bits - 15;
/* sample must be positive */
if (sample < 0)
sample = -(sample + 1);
/* shift sample into 15 frac bit range */
if (frac_bits_offset > 0)
sample >>= frac_bits_offset;
if (frac_bits_offset < 0)
sample <<= -frac_bits_offset;
/* normal case: sample isn't clipped */
if (sample < (1 << 15))
{
/* index is 6 MSB, rem is 9 LSB */
int index = sample >> 9;
int32_t rem = (sample & 0x1FF) << 22;
/* interpolate from the compression curve:
higher gain - ((rem / (1 << 31)) * (higher gain - lower gain)) */
/** interpolate from the compression curve:
* higher gain - ((rem / (1 << 31)) * (higher gain - lower gain))
*/
return comp_curve[index] - (FRACMUL(rem,
(comp_curve[index] - comp_curve[index + 1])));
}
/* sample is somewhat clipped, up to 2 bits of overhead */
if (sample < (1 << 17))
{
/* straight interpolation:
higher gain - ((clipped portion of sample * 4/3
/ (1 << 31)) * (higher gain - lower gain)) */
/** straight interpolation:
* higher gain - ((clipped portion of sample * 4/3
* / (1 << 31)) * (higher gain - lower gain))
*/
return comp_curve[64] - (FRACMUL(((sample - (1 << 15)) / 3) << 16,
(comp_curve[64] - comp_curve[65])));
}
/* sample is too clipped, return invalid value */
return -1;
}
@ -322,55 +441,115 @@ static void compressor_process(struct dsp_proc_entry *this,
while (count-- > 0)
{
/* use lowest (most compressed) gain factor of the output buffer
sample pair for both samples (mono is also handled correctly here)
*/
/* Use the average of the channels */
int32_t sample_gain = UNITY;
int32_t x = 0;
int32_t tmpx = 0;
int32_t in_buf_max_level = 0;
for (int ch = 0; ch < num_chan; ch++)
{
int32_t this_gain = get_compression_gain(&buf->format, *in_buf[ch]);
if (this_gain < sample_gain)
sample_gain = this_gain;
tmpx = *in_buf[ch];
x += tmpx;
labuf[ch][delay_write] = tmpx;
/* Limiter detection */
if(tmpx < 0) tmpx = -(tmpx + 1);
if(tmpx > in_buf_max_level) in_buf_max_level = tmpx;
}
/* perform release slope; skip if no compression and no release slope
/** Divide it by the number of channels, roughly
* It will be exact if the number of channels a power of 2
* it will be imperfect otherwise. Real division costs too
* much here, and most of the time it will be 2 channels (stereo)
*/
if ((sample_gain != UNITY) || (release_gain != UNITY))
{
/* if larger offset than previous slope, start new release slope
*/
if ((sample_gain <= release_gain) && (sample_gain > 0))
{
x >>= (num_chan >> 1);
/** 1p HP Filters: y[n] = a*(y[n-1] + x - x[n-1])
* Zero and Pole in the same place to reduce computation
* Run the first pre-emphasis filter
*/
int32_t tmp1 = x - hpfx1 + hp1y1;
hp1y1 = FRACMUL_SHL(hp1ca, tmp1, 7);
/* Run the second pre-emphasis filter */
tmp1 = x - hpfx1 + hp2y1;
hp2y1 = FRACMUL_SHL(hp2ca, tmp1, 7);
hpfx1 = x;
/* Apply weighted sum to the pre-emphasis network */
sample_gain = (x>>1) + hp1y1 + (hp2y1<<1); /* x/2 + hp1 + 2*hp2 */
sample_gain >>= 1;
sample_gain += sample_gain >> 1;
sample_gain = get_compression_gain(&buf->format, sample_gain);
/* Exponential Attack and Release */
if ((sample_gain <= release_gain) && (sample_gain > 0))
{
/* Attack */
if(attca != UNITY)
{
int32_t this_gain = FRACMUL_SHL(release_gain, attcb, 7);
this_gain += FRACMUL_SHL(sample_gain, attca, 7);
release_gain = this_gain;
}
else
{
release_gain = sample_gain;
}
/** reset it to delay time so it cannot release before the
* delayed signal releases
*/
release_holdoff = delay_time;
}
else
/* Reverse exponential decay to current gain value */
{
/* Don't start release while output is still above thresh */
if(release_holdoff > 0)
{
release_holdoff--;
}
else
/* keep sloping towards unity gain (and ignore invalid value) */
{
release_gain += comp_rel_slope;
if (release_gain > UNITY)
{
release_gain = UNITY;
}
/* Release */
int32_t this_gain = FRACMUL_SHL(release_gain, rlscb, 7);
this_gain += FRACMUL_SHL(sample_gain,rlsca,7);
release_gain = this_gain;
}
}
/** total gain factor is the product of release gain and makeup gain,
* but avoid computation if possible
*/
int32_t total_gain = FRACMUL_SHL(release_gain, comp_makeup_gain, 7);
/* Look-ahead limiter */
int32_t test_gain = FRACMUL_SHL(total_gain, in_buf_max_level, 3);
if( test_gain > UNITY)
{
release_gain -= limitca;
}
/* total gain factor is the product of release gain and makeup gain,
but avoid computation if possible */
int32_t total_gain = ((release_gain == UNITY) ? comp_makeup_gain :
(comp_makeup_gain == UNITY) ? release_gain :
FRACMUL_SHL(release_gain, comp_makeup_gain, 7));
/* Implement the compressor: apply total gain factor (if any) to the
output buffer sample pair/mono sample */
/** Implement the compressor: apply total gain factor (if any) to the
* output buffer sample pair/mono sample
*/
if (total_gain != UNITY)
{
for (int ch = 0; ch < num_chan; ch++)
{
*in_buf[ch] = FRACMUL_SHL(total_gain, *in_buf[ch], 7);
*in_buf[ch] = FRACMUL_SHL(total_gain, labuf[ch][delay_read], 7);
}
}
in_buf[0]++;
in_buf[1]++;
delay_write++;
delay_read++;
if(delay_write >= MAX_DLY) delay_write = 0;
if(delay_read >= MAX_DLY) delay_read = 0;
}
(void)this;
@ -382,6 +561,8 @@ static intptr_t compressor_configure(struct dsp_proc_entry *this,
unsigned int setting,
intptr_t value)
{
int i,j;
switch (setting)
{
case DSP_PROC_INIT:
@ -394,7 +575,29 @@ static intptr_t compressor_configure(struct dsp_proc_entry *this,
/* Fall-through */
case DSP_RESET:
case DSP_FLUSH:
release_gain = UNITY;
for(i=0; i<MAX_CH; i++)
{
for(j=0; j<MAX_DLY; j++)
{
labuf[i][j] = 0; /* All Silence */
}
}
/* Delay Line Read/Write Pointers */
int32_t fs = dsp_get_output_frequency(dsp);
delay_read = 0;
delay_write = (DLY_TIME*fs/1000);
if(delay_write >= MAX_DLY) {
delay_write = MAX_DLY - 1; /* Limit to the max allocated buffer */
}
delay_time = delay_write;
release_holdoff = delay_write;
limitca = get_att_rls_coeff(DLY_TIME, fs); /** Attack time for
* look-ahead limiter
*/
break;
case DSP_SET_OUT_FREQUENCY:

View file

@ -28,6 +28,7 @@ struct compressor_settings
int ratio;
int knee;
int release_time;
int attack_time;
};
void dsp_set_compressor(const struct compressor_settings *settings);

View file

@ -603,6 +603,9 @@ non-compressed signal to a compressed signal. Hard Knee means that the
transition occurs precisely at the threshold. The Soft Knee setting smoothes
the transition from plus or minus three decibels around the threshold.
The \setting{Attack Time} setting sets the delay in milliseconds between the
input signal exceeding the activation threshold and acting upon it.
The \setting{Release Time} setting sets the recovery time after the signal is
compressed. Once the compressor determines that compression is necessary,
the input signal is reduced appropriately, but the gain isn't allowed to