From d0918b98fa0cfba21208a4fb5ed153687b8f02c3 Mon Sep 17 00:00:00 2001 From: Ryan Billing Date: Fri, 4 Oct 2013 01:57:00 +1300 Subject: 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 Reviewed-by: Michael Giacomelli --- apps/lang/english.lang | 17 ++ apps/menus/sound_menu.c | 5 +- apps/settings_list.c | 6 +- docs/CREDITS | 1 + lib/rbcodec/dsp/compressor.c | 379 +++++++++++++++++++++------- lib/rbcodec/dsp/compressor.h | 1 + manual/configure_rockbox/sound_settings.tex | 3 + 7 files changed, 322 insertions(+), 90 deletions(-) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index d0dc5c5fc4..dcad532f7a 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -11910,6 +11910,23 @@ swcodec: "Soft Knee" + + id: LANG_COMPRESSOR_ATTACK + desc: in sound settings + user: core + + *: none + swcodec: "Attack Time" + + + *: none + swcodec: "Attack Time" + + + *: none + swcodec: "Attack Time" + + id: LANG_COMPRESSOR_RELEASE desc: in sound settings diff --git a/apps/menus/sound_menu.c b/apps/menus/sound_menu.c index fd192cb661..28cc257193 100644 --- a/apps/menus/sound_menu.c +++ b/apps/menus/sound_menu.c @@ -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) diff --git a/apps/settings_list.c b/apps/settings_list.c index 6ffb2b551b..bd2bfce36f 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -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, diff --git a/docs/CREDITS b/docs/CREDITS index b78c44904b..94e8df2706 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -632,6 +632,7 @@ Vanja Cvelbar Richard Quirk Kirill Stryaponoff Roman Poltoradnev +Ryan Billing The libmad team The wavpack team diff --git a/lib/rbcodec/dsp/compressor.c b/lib/rbcodec/dsp/compressor.c index a222caed7f..685851ec29 100644 --- a/lib/rbcodec/dsp/compressor.c +++ b/lib/rbcodec/dsp/compressor.c @@ -23,44 +23,155 @@ #include "fracmul.h" #include -/* 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 */ + +/* 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; +} -#define UNITY (1L << 24) /* unity gain in S7.24 format */ +/** 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; + + /* 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; + } - bool changed = settings == &curr_set; /* If frequency change */ + + /* 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"); -#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); +#endif - /* 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_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: diff --git a/lib/rbcodec/dsp/compressor.h b/lib/rbcodec/dsp/compressor.h index e41950926e..35aa0eeb65 100644 --- a/lib/rbcodec/dsp/compressor.h +++ b/lib/rbcodec/dsp/compressor.h @@ -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); diff --git a/manual/configure_rockbox/sound_settings.tex b/manual/configure_rockbox/sound_settings.tex index f06bfaf6e5..d2da07b983 100644 --- a/manual/configure_rockbox/sound_settings.tex +++ b/manual/configure_rockbox/sound_settings.tex @@ -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 -- cgit v1.2.3