aboutsummaryrefslogtreecommitdiffstats
path: root/Alc
diff options
context:
space:
mode:
Diffstat (limited to 'Alc')
-rw-r--r--Alc/ALc.c8
-rw-r--r--Alc/ALu.c3
-rw-r--r--Alc/mastering.c477
-rw-r--r--Alc/mastering.h111
4 files changed, 431 insertions, 168 deletions
diff --git a/Alc/ALc.c b/Alc/ALc.c
index 9a1051c9..ecacf8c5 100644
--- a/Alc/ALc.c
+++ b/Alc/ALc.c
@@ -1706,8 +1706,9 @@ static void alcSetError(ALCdevice *device, ALCenum errorCode)
struct Compressor *CreateDeviceLimiter(const ALCdevice *device)
{
- return CompressorInit(0.0f, 0.0f, AL_FALSE, AL_TRUE, 0.0f, 0.0f, 0.5f, 2.0f,
- 0.0f, -3.0f, 3.0f, device->Frequency);
+ return CompressorInit(device->RealOut.NumChannels, device->Frequency,
+ AL_TRUE, AL_TRUE, AL_TRUE, AL_TRUE, AL_TRUE, 0.001f, 0.002f,
+ 0.0f, 0.0f, -0.0003f, INFINITY, 0.0f, 0.020f, 0.200f);
}
/* UpdateClockBase
@@ -2231,7 +2232,8 @@ static ALCenum UpdateDeviceParams(ALCdevice *device, const ALCint *attrList)
*/
if(gainLimiter != ALC_FALSE)
{
- if(!device->Limiter || device->Frequency != GetCompressorSampleRate(device->Limiter))
+ if(!device->Limiter || device->Frequency != GetCompressorSampleRate(device->Limiter) ||
+ device->RealOut.NumChannels != GetCompressorChannelCount(device->Limiter))
{
al_free(device->Limiter);
device->Limiter = CreateDeviceLimiter(device);
diff --git a/Alc/ALu.c b/Alc/ALu.c
index e9d8aa02..03abb116 100644
--- a/Alc/ALu.c
+++ b/Alc/ALu.c
@@ -1838,8 +1838,7 @@ void aluMixData(ALCdevice *device, ALvoid *OutBuffer, ALsizei NumSamples)
SamplesToDo, device->RealOut.NumChannels);
if(device->Limiter)
- ApplyCompression(device->Limiter, device->RealOut.NumChannels, SamplesToDo,
- device->RealOut.Buffer);
+ ApplyCompression(device->Limiter, SamplesToDo, device->RealOut.Buffer);
if(device->DitherDepth > 0.0f)
ApplyDither(device->RealOut.Buffer, &device->DitherSeed, device->DitherDepth,
diff --git a/Alc/mastering.c b/Alc/mastering.c
index 1636c8d9..78f6038d 100644
--- a/Alc/mastering.c
+++ b/Alc/mastering.c
@@ -7,226 +7,439 @@
#include "almalloc.h"
+extern inline ALsizei GetCompressorChannelCount(const Compressor *Comp);
extern inline ALuint GetCompressorSampleRate(const Compressor *Comp);
-#define RMS_WINDOW_SIZE (1<<7)
-#define RMS_WINDOW_MASK (RMS_WINDOW_SIZE-1)
-#define RMS_VALUE_MAX (1<<24)
-static_assert(RMS_VALUE_MAX < (UINT_MAX / RMS_WINDOW_SIZE), "RMS_VALUE_MAX is too big");
-
-
-/* Multichannel compression is linked via one of two modes:
+/* This sliding hold follows the input level with an instant attack and a
+ * fixed duration hold before an instant release to the next highest level.
+ * It is a sliding window maximum (descending maxima) implementation based on
+ * Richard Harter's ascending minima algorithm available at:
*
- * Summed - Absolute sum of all channels.
- * Maxed - Absolute maximum of any channel.
+ * http://www.richardhartersworld.com/cri/2001/slidingmin.html
*/
-static void SumChannels(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo,
- ALfloat (*restrict OutBuffer)[BUFFERSIZE])
+static ALfloat UpdateSlidingHold(SlidingHold *Hold, const ALsizei i, const ALfloat in)
{
- ALsizei c, i;
+ const ALsizei mask = BUFFERSIZE - 1;
+ const ALsizei length = Hold->Length;
+ ALfloat *restrict values = Hold->Values;
+ ALsizei *restrict expiries = Hold->Expiries;
+ ALsizei lowerIndex = Hold->LowerIndex;
+ ALsizei upperIndex = Hold->UpperIndex;
- for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] = 0.0f;
+ if(i >= expiries[upperIndex])
+ upperIndex = (upperIndex + 1) & mask;
- for(c = 0;c < NumChans;c++)
+ if(in >= values[upperIndex])
{
- for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] += OutBuffer[c][i];
+ values[upperIndex] = in;
+ expiries[upperIndex] = i + length;
+ lowerIndex = upperIndex;
}
+ else
+ {
+ while(in >= values[lowerIndex])
+ lowerIndex = (lowerIndex - 1) & mask;
- for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] = fabsf(Comp->Envelope[i]);
+ lowerIndex = (lowerIndex + 1) & mask;
+ values[lowerIndex] = in;
+ expiries[lowerIndex] = i + length;
+ }
+
+ Hold->LowerIndex = lowerIndex;
+ Hold->UpperIndex = upperIndex;
+
+ return values[upperIndex];
}
-static void MaxChannels(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo,
- ALfloat (*restrict OutBuffer)[BUFFERSIZE])
+static void ShiftSlidingHold(SlidingHold *Hold, const ALsizei n)
{
+ const ALsizei mask = BUFFERSIZE - 1;
+ const ALsizei lowerIndex = Hold->LowerIndex;
+ ALsizei *restrict expiries = Hold->Expiries;
+ ALsizei i = Hold->UpperIndex;
+
+ while(i != lowerIndex)
+ {
+ expiries[i] -= n;
+ i = (i + 1) & mask;
+ }
+
+ expiries[i] -= n;
+}
+
+/* Multichannel compression is linked via the absolute maximum of all
+ * channels.
+ */
+static void LinkChannels(Compressor *Comp, const ALsizei SamplesToDo, ALfloat (*restrict OutBuffer)[BUFFERSIZE])
+{
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const ALsizei index = Comp->SideChainIndex + Comp->LookAhead;
+ const ALsizei numChans = Comp->NumChans;
+ ALfloat *restrict sideChain = Comp->SideChain;
ALsizei c, i;
for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] = 0.0f;
+ sideChain[(index + i) & mask] = 0.0f;
- for(c = 0;c < NumChans;c++)
+ for(c = 0;c < numChans;c++)
{
for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] = maxf(Comp->Envelope[i], fabsf(OutBuffer[c][i]));
+ {
+ ALsizei offset = (index + i) & mask;
+
+ sideChain[offset] = maxf(sideChain[offset], fabsf(OutBuffer[c][i]));
+ }
}
}
-/* Envelope detection/sensing can be done via:
- *
- * RMS - Rectangular windowed root mean square of linking stage.
- * Peak - Implicit output from linking stage.
+/* This calculates the squared crest factor of the control signal for the
+ * basic automation of the attack/release times. As suggested by the paper,
+ * it uses an instantaneous squared peak detector and a squared RMS detector
+ * both with 200ms release times.
*/
-static void RmsDetection(Compressor *Comp, const ALsizei SamplesToDo)
+static void CrestDetector(Compressor *Comp, const ALsizei SamplesToDo)
{
- ALuint sum = Comp->RmsSum;
- ALuint *window = Comp->RmsWindow;
- ALsizei index = Comp->RmsIndex;
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const ALfloat a_crest = Comp->CrestCoeff;
+ const ALsizei index = Comp->SideChainIndex + Comp->LookAhead;
+ const ALfloat *restrict sideChain = Comp->SideChain;
+ ALfloat *restrict crestFactor = Comp->CrestFactor;
+ ALfloat y2_peak = Comp->LastPeakSq;
+ ALfloat y2_rms = Comp->LastRmsSq;
ALsizei i;
for(i = 0;i < SamplesToDo;i++)
{
- ALfloat sig = Comp->Envelope[i];
-
- sum -= window[index];
- window[index] = fastf2i(minf(sig * sig * 65536.0f, RMS_VALUE_MAX));
- sum += window[index];
- index = (index + 1) & RMS_WINDOW_MASK;
+ ALfloat x_abs = sideChain[(index + i) & mask];
+ ALfloat x2 = maxf(0.000001f, x_abs * x_abs);
- Comp->Envelope[i] = sqrtf(sum / 65536.0f / RMS_WINDOW_SIZE);
+ y2_peak = maxf(x2, lerp(x2, y2_peak, a_crest));
+ y2_rms = lerp(x2, y2_rms, a_crest);
+ crestFactor[i] = y2_peak / y2_rms;
}
- Comp->RmsSum = sum;
- Comp->RmsIndex = index;
+ Comp->LastPeakSq = y2_peak;
+ Comp->LastRmsSq = y2_rms;
}
-/* This isn't a very sophisticated envelope follower, but it gets the job
- * done. First, it operates at logarithmic scales to keep transitions
- * appropriate for human hearing. Second, it can apply adaptive (automated)
- * attack/release adjustments based on the signal.
+/* The side-chain starts with a simple peak detector (based on the absolute
+ * value of the incoming signal) and performs most of its operations in the
+ * log domain.
*/
-static void FollowEnvelope(Compressor *Comp, const ALsizei SamplesToDo)
+static void PeakDetector(Compressor *Comp, const ALsizei SamplesToDo)
{
- ALfloat attackMin = Comp->AttackMin;
- ALfloat attackMax = Comp->AttackMax;
- ALfloat releaseMin = Comp->ReleaseMin;
- ALfloat releaseMax = Comp->ReleaseMax;
- ALfloat last = Comp->EnvLast;
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const ALsizei index = Comp->SideChainIndex + Comp->LookAhead;
+ ALfloat *restrict sideChain = Comp->SideChain;
ALsizei i;
for(i = 0;i < SamplesToDo;i++)
{
- ALfloat env = log10f(maxf(Comp->Envelope[i], 0.000001f));
- ALfloat slope = minf(1.0f, fabsf(env - last) / 4.5f);
+ ALuint offset = (index + i) & mask;
+ ALfloat x_abs = sideChain[offset];
- if(env > last)
- last = minf(env, last + lerp(attackMin, attackMax, 1.0f - (slope * slope)));
- else
- last = maxf(env, last + lerp(releaseMin, releaseMax, 1.0f - (slope * slope)));
+ sideChain[offset] = logf(maxf(0.000001f, x_abs));
+ }
+}
- Comp->Envelope[i] = last;
+/* An optional hold can be used to extend the peak detector so it can more
+ * solidly detect fast transients. This is best used when operating as a
+ * limiter.
+ */
+static void PeakHoldDetector(Compressor *Comp, const ALsizei SamplesToDo)
+{
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const ALsizei index = Comp->SideChainIndex + Comp->LookAhead;
+ ALfloat *restrict sideChain = Comp->SideChain;
+ SlidingHold *hold = Comp->Hold;
+ ALsizei i;
+
+ for(i = 0;i < SamplesToDo;i++)
+ {
+ ALsizei offset = (index + i) & mask;
+ ALfloat x_abs = sideChain[offset];
+ ALfloat x_G = logf(maxf(0.000001f, x_abs));
+
+ sideChain[offset] = UpdateSlidingHold(hold, i, x_G);
}
- Comp->EnvLast = last;
+ ShiftSlidingHold(hold, SamplesToDo);
}
-/* The envelope is converted to control gain with an optional soft knee. */
-static void EnvelopeGain(Compressor *Comp, const ALsizei SamplesToDo, const ALfloat Slope)
+/* This is the heart of the feed-forward compressor. It operates in the log
+ * domain (to better match human hearing) and can apply some basic automation
+ * to knee width, attack/release times, make-up/post gain, and clipping
+ * reduction.
+ */
+static void GainCompressor(Compressor *Comp, const ALsizei SamplesToDo)
{
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const bool autoKnee = Comp->Auto.Knee;
+ const bool autoAttack = Comp->Auto.Attack;
+ const bool autoRelease = Comp->Auto.Release;
+ const bool autoPostGain = Comp->Auto.PostGain;
+ const bool autoDeclip = Comp->Auto.Declip;
+ const ALsizei lookAhead = Comp->LookAhead;
const ALfloat threshold = Comp->Threshold;
- const ALfloat knee = Comp->Knee;
+ const ALfloat slope = Comp->Slope;
+ const ALfloat attack = Comp->Attack;
+ const ALfloat release = Comp->Release;
+ const ALsizei index = Comp->SideChainIndex;
+ const ALfloat *restrict crestFactor = Comp->CrestFactor;
+ ALfloat *restrict sideChain = Comp->SideChain;
+ ALfloat postGain = Comp->PostGain;
+ ALfloat knee = Comp->Knee;
+ ALfloat c_est = Comp->GainEstimate;
+ ALfloat a_adp = Comp->AdaptCoeff;
+ ALfloat t_att = attack;
+ ALfloat t_rel = release - attack;
+ ALfloat a_att = expf(-1.0f / t_att);
+ ALfloat a_rel = expf(-1.0f / t_rel);
+ ALfloat y_1 = Comp->LastRelease;
+ ALfloat y_L = Comp->LastAttack;
+ ALfloat c_dev = Comp->LastGainDev;
ALsizei i;
- if(!(knee > 0.0f))
+ for(i = 0;i < SamplesToDo;i++)
{
- for(i = 0;i < SamplesToDo;i++)
+ const ALfloat y2_crest = crestFactor[i];
+ ALfloat x_G = sideChain[(index + lookAhead + i) & mask];
+ ALfloat x_over = x_G - threshold;
+ ALfloat knee_h;
+ ALfloat y_G;
+ ALfloat x_L;
+
+ if(autoKnee)
+ knee = maxf(0.0f, 2.5f * (c_dev + c_est));
+ knee_h = 0.5f * knee;
+
+ /* This is the gain computer. It applies a static compression curve
+ * to the control signal.
+ */
+ if(x_over <= -knee_h)
+ y_G = 0.0f;
+ else if(fabsf(x_over) < knee_h)
+ y_G = (x_over + knee_h) * (x_over + knee_h) / (2.0f * knee);
+ else
+ y_G = x_over;
+
+ x_L = -slope * y_G;
+
+ if(autoAttack)
+ {
+ t_att = 2.0f * attack / y2_crest;
+ a_att = expf(-1.0f / t_att);
+ }
+
+ if(autoRelease)
{
- ALfloat gain = Slope * (threshold - Comp->Envelope[i]);
- Comp->Envelope[i] = powf(10.0f, minf(0.0f, gain));
+ t_rel = 2.0f * release / y2_crest - t_att;
+ a_rel = expf(-1.0f / t_rel);
}
+
+ /* Gain smoothing (ballistics) is done via a smooth decoupled peak
+ * detector. The attack time is subtracted from the release time
+ * above to compensate for the chained operating mode.
+ */
+ y_1 = maxf(x_L, lerp(x_L, y_1, a_rel));
+ y_L = lerp(y_1, y_L, a_att);
+
+ /* Knee width and make-up gain automation make use of a smoothed
+ * measurement of deviation between the control signal and estimate.
+ * The estimate is also used to bias the measurement to hot-start its
+ * average.
+ */
+ c_dev = lerp(-y_L - c_est, c_dev, a_adp);
+
+ if(autoPostGain)
+ {
+ /* Clipping reduction is only viable when make-up gain is being
+ * automated. It modifies the deviation to further attenuate the
+ * control signal when clipping is detected. The adaptation
+ * time is sufficiently long enough to suppress further clipping
+ * at the same output level.
+ */
+ if(autoDeclip)
+ {
+ x_G = sideChain[(index + i) & mask];
+ if((x_G - c_dev - c_est - y_L) > threshold)
+ c_dev = x_G - c_est - y_L - threshold;
+ }
+
+ postGain = -(c_dev + c_est);
+ }
+
+ sideChain[(index + i) & mask] = expf(postGain - y_L);
}
- else
- {
- const ALfloat lower = threshold - (0.5f * knee);
- const ALfloat upper = threshold + (0.5f * knee);
- const ALfloat m = 0.5f * Slope / knee;
+ Comp->LastRelease = y_1;
+ Comp->LastAttack = y_L;
+ Comp->LastGainDev = c_dev;
+}
+
+/* Combined with the hold time, a look-ahead delay can improve handling of
+ * fast transients by allowing the envelope time to converge prior to
+ * reaching the offending impulse. This is best used when operating as a
+ * limiter.
+ */
+static void SignalDelay(Compressor *Comp, const ALsizei SamplesToDo, ALfloat (*restrict OutBuffer)[BUFFERSIZE])
+{
+ const ALsizei mask = BUFFERSIZE - 1;
+ const ALsizei numChans = Comp->NumChans;
+ const ALsizei indexIn = Comp->DelayIndex;
+ const ALsizei indexOut = Comp->DelayIndex - Comp->LookAhead;
+ ALfloat (*restrict delay)[BUFFERSIZE] = Comp->Delay;
+ ALsizei c, i;
+
+ for(c = 0;c < numChans;c++)
+ {
for(i = 0;i < SamplesToDo;i++)
{
- ALfloat env = Comp->Envelope[i];
- ALfloat gain;
-
- if(env > lower && env < upper)
- gain = m * (env - lower) * (lower - env);
- else
- gain = Slope * (threshold - env);
+ ALfloat sig = OutBuffer[c][i];
- Comp->Envelope[i] = powf(10.0f, minf(0.0f, gain));
+ OutBuffer[c][i] = delay[c][(indexOut + i) & mask];
+ delay[c][(indexIn + i) & mask] = sig;
}
}
-}
+ Comp->DelayIndex = (indexIn + SamplesToDo) & mask;
+}
-Compressor *CompressorInit(const ALfloat PreGainDb, const ALfloat PostGainDb,
- const ALboolean SummedLink, const ALboolean RmsSensing,
- const ALfloat AttackTimeMin, const ALfloat AttackTimeMax,
- const ALfloat ReleaseTimeMin, const ALfloat ReleaseTimeMax,
- const ALfloat Ratio, const ALfloat ThresholdDb,
- const ALfloat KneeDb, const ALuint SampleRate)
+/* The compressor is initialized with the following settings:
+ *
+ * NumChans - Number of channels to process.
+ * SampleRate - Sample rate to process.
+ * AutoKnee - Whether to automate the knee width parameter.
+ * AutoAttack - Whether to automate the attack time parameter.
+ * AutoRelease - Whether to automate the release time parameter.
+ * AutoPostGain - Whether to automate the make-up (post) gain parameter.
+ * AutoDeclip - Whether to automate clipping reduction. Ignored when
+ * not automating make-up gain.
+ * LookAheadTime - Look-ahead time (in seconds).
+ * HoldTime - Peak hold-time (in seconds).
+ * PreGainDb - Gain applied before detection (in dB).
+ * PostGainDb - Make-up gain applied after compression (in dB).
+ * ThresholdDb - Triggering threshold (in dB).
+ * Ratio - Compression ratio (x:1). Set to INFINITY for true
+ * limiting. Ignored when automating knee width.
+ * KneeDb - Knee width (in dB). Ignored when automating knee
+ * width.
+ * AttackTimeMin - Attack time (in seconds). Acts as a maximum when
+ * automating attack time.
+ * ReleaseTimeMin - Release time (in seconds). Acts as a maximum when
+ * automating release time.
+ */
+Compressor* CompressorInit(const ALuint NumChans, const ALuint SampleRate,
+ const ALboolean AutoKnee, const ALboolean AutoAttack,
+ const ALboolean AutoRelease, const ALboolean AutoPostGain,
+ const ALboolean AutoDeclip, const ALfloat LookAheadTime,
+ const ALfloat HoldTime, const ALfloat PreGainDb,
+ const ALfloat PostGainDb, const ALfloat ThresholdDb,
+ const ALfloat Ratio, const ALfloat KneeDb,
+ const ALfloat AttackTime, const ALfloat ReleaseTime)
{
Compressor *Comp;
+ ALsizei lookAhead;
+ ALsizei hold;
size_t size;
- ALsizei i;
+ lookAhead = (ALsizei)minf(BUFFERSIZE, roundf(maxf(0.0f, LookAheadTime) * SampleRate));
+ hold = (ALsizei)minf(BUFFERSIZE, roundf(maxf(0.0f, HoldTime) * SampleRate));
size = sizeof(*Comp);
- if(RmsSensing)
- size += sizeof(Comp->RmsWindow[0]) * RMS_WINDOW_SIZE;
- Comp = al_calloc(16, size);
- Comp->PreGain = powf(10.0f, PreGainDb / 20.0f);
- Comp->PostGain = powf(10.0f, PostGainDb / 20.0f);
- Comp->SummedLink = SummedLink;
- Comp->AttackMin = 1.0f / maxf(0.000001f, AttackTimeMin * SampleRate * logf(10.0f));
- Comp->AttackMax = 1.0f / maxf(0.000001f, AttackTimeMax * SampleRate * logf(10.0f));
- Comp->ReleaseMin = -1.0f / maxf(0.000001f, ReleaseTimeMin * SampleRate * logf(10.0f));
- Comp->ReleaseMax = -1.0f / maxf(0.000001f, ReleaseTimeMax * SampleRate * logf(10.0f));
- Comp->Ratio = Ratio;
- Comp->Threshold = ThresholdDb / 20.0f;
- Comp->Knee = maxf(0.0f, KneeDb / 20.0f);
- Comp->SampleRate = SampleRate;
+ if(lookAhead > 0)
+ {
+ size += sizeof(*Comp->Delay) * NumChans;
+ if(hold > 0)
+ size += sizeof(*Comp->Hold);
+ }
- Comp->RmsSum = 0;
- if(RmsSensing)
- Comp->RmsWindow = (ALuint*)(Comp+1);
- else
- Comp->RmsWindow = NULL;
- Comp->RmsIndex = 0;
+ Comp = al_calloc(16, size);
+ Comp->NumChans = NumChans;
+ Comp->SampleRate = SampleRate;
+ Comp->Auto.Knee = AutoKnee;
+ Comp->Auto.Attack = AutoAttack;
+ Comp->Auto.Release = AutoRelease;
+ Comp->Auto.PostGain = AutoPostGain;
+ Comp->Auto.Declip = AutoPostGain && AutoDeclip;
+ Comp->LookAhead = lookAhead;
+ Comp->PreGain = powf(10.0f, PreGainDb / 20.0f);
+ Comp->PostGain = PostGainDb * logf(10.0f) / 20.0f;
+ Comp->Threshold = ThresholdDb * logf(10.0f) / 20.0f;
+ Comp->Slope = 1.0f / maxf(1.0f, Ratio) - 1.0f;
+ Comp->Knee = maxf(0.0f, KneeDb * logf(10.0f) / 20.0f);
+ Comp->Attack = maxf(1.0f, AttackTime * SampleRate);
+ Comp->Release = maxf(1.0f, ReleaseTime * SampleRate);
+
+ /* Knee width automation actually treats the compressor as a limiter. By
+ * varying the knee width, it can effectively be seen as applying
+ * compression over a wide range of ratios.
+ */
+ if(AutoKnee)
+ Comp->Slope = -1.0f;
+
+ if(lookAhead > 0)
+ {
+ if(hold > 0)
+ {
+ Comp->Hold = (SlidingHold*)(Comp + 1);
+ Comp->Hold->Values[0] = -INFINITY;
+ Comp->Hold->Expiries[0] = hold;
+ Comp->Hold->Length = hold;
+ Comp->Delay = (ALfloat(*)[])(Comp->Hold + 1);
+ }
+ else
+ {
+ Comp->Delay = (ALfloat(*)[])(Comp + 1);
+ }
+ }
- for(i = 0;i < BUFFERSIZE;i++)
- Comp->Envelope[i] = 0.0f;
- Comp->EnvLast = -6.0f;
+ Comp->CrestCoeff = expf(-1.0f / (0.200f * SampleRate)); // 200ms
+ Comp->GainEstimate = Comp->Threshold * -0.5f * Comp->Slope;
+ Comp->AdaptCoeff = expf(-1.0f / (2.0f * SampleRate)); // 2s
return Comp;
}
-void ApplyCompression(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo,
- ALfloat (*restrict OutBuffer)[BUFFERSIZE])
+void ApplyCompression(Compressor *Comp, const ALsizei SamplesToDo, ALfloat (*restrict OutBuffer)[BUFFERSIZE])
{
+ const ALsizei mask = 2*BUFFERSIZE - 1;
+ const ALsizei numChans = Comp->NumChans;
+ const ALfloat preGain = Comp->PreGain;
+ const ALsizei index = Comp->SideChainIndex;
+ ALfloat *restrict sideChain = Comp->SideChain;
ALsizei c, i;
- if(Comp->PreGain != 1.0f)
+ if(preGain != 1.0f)
{
- for(c = 0;c < NumChans;c++)
+ for(c = 0;c < numChans;c++)
{
for(i = 0;i < SamplesToDo;i++)
- OutBuffer[c][i] *= Comp->PreGain;
+ OutBuffer[c][i] *= preGain;
}
}
- if(Comp->SummedLink)
- SumChannels(Comp, NumChans, SamplesToDo, OutBuffer);
- else
- MaxChannels(Comp, NumChans, SamplesToDo, OutBuffer);
+ LinkChannels(Comp, SamplesToDo, OutBuffer);
- if(Comp->RmsWindow)
- RmsDetection(Comp, SamplesToDo);
- FollowEnvelope(Comp, SamplesToDo);
+ if(Comp->Auto.Attack || Comp->Auto.Release)
+ CrestDetector(Comp, SamplesToDo);
- if(Comp->Ratio > 0.0f)
- EnvelopeGain(Comp, SamplesToDo, 1.0f - (1.0f / Comp->Ratio));
+ if(Comp->Hold)
+ PeakHoldDetector(Comp, SamplesToDo);
else
- EnvelopeGain(Comp, SamplesToDo, 1.0f);
+ PeakDetector(Comp, SamplesToDo);
- if(Comp->PostGain != 1.0f)
- {
- for(i = 0;i < SamplesToDo;i++)
- Comp->Envelope[i] *= Comp->PostGain;
- }
- for(c = 0;c < NumChans;c++)
+ GainCompressor(Comp, SamplesToDo);
+
+ if(Comp->Delay)
+ SignalDelay(Comp, SamplesToDo, OutBuffer);
+
+ for(c = 0;c < numChans;c++)
{
for(i = 0;i < SamplesToDo;i++)
- OutBuffer[c][i] *= Comp->Envelope[i];
+ OutBuffer[c][i] *= sideChain[(index + i) & mask];
}
+
+ Comp->SideChainIndex = (index + SamplesToDo) & mask;
}
diff --git a/Alc/mastering.h b/Alc/mastering.h
index 0a7b4901..206a6c91 100644
--- a/Alc/mastering.h
+++ b/Alc/mastering.h
@@ -6,51 +6,100 @@
/* For BUFFERSIZE. */
#include "alMain.h"
+/* These structures assume BUFFERSIZE is a power of 2.
+ */
+typedef struct SlidingHold
+{
+ ALfloat Values[BUFFERSIZE];
+ ALsizei Expiries[BUFFERSIZE];
+ ALsizei LowerIndex;
+ ALsizei UpperIndex;
+ ALsizei Length;
+} SlidingHold;
+
+/* General topology and basic automation was based on the following paper:
+ *
+ * D. Giannoulis, M. Massberg and J. D. Reiss,
+ * "Parameter Automation in a Dynamic Range Compressor,"
+ * Journal of the Audio Engineering Society, v61 (10), Oct. 2013
+ *
+ * Available (along with supplemental reading) at:
+ *
+ * http://c4dm.eecs.qmul.ac.uk/audioengineering/compressors/
+ */
typedef struct Compressor {
+ ALsizei NumChans;
+ ALuint SampleRate;
+ struct {
+ ALuint Knee:1;
+ ALuint Attack:1;
+ ALuint Release:1;
+ ALuint PostGain:1;
+ ALuint Declip:1;
+ } Auto;
+ ALsizei LookAhead;
ALfloat PreGain;
ALfloat PostGain;
- ALboolean SummedLink;
- ALfloat AttackMin;
- ALfloat AttackMax;
- ALfloat ReleaseMin;
- ALfloat ReleaseMax;
- ALfloat Ratio;
ALfloat Threshold;
+ ALfloat Slope;
ALfloat Knee;
- ALuint SampleRate;
-
- ALuint RmsSum;
- ALuint *RmsWindow;
- ALsizei RmsIndex;
- ALfloat Envelope[BUFFERSIZE];
- ALfloat EnvLast;
+ ALfloat Attack;
+ ALfloat Release;
+ ALfloat SideChain[2*BUFFERSIZE];
+ ALsizei SideChainIndex;
+ ALfloat CrestFactor[BUFFERSIZE];
+ SlidingHold *Hold;
+ ALfloat (*Delay)[BUFFERSIZE];
+ ALsizei DelayIndex;
+ ALfloat CrestCoeff;
+ ALfloat GainEstimate;
+ ALfloat AdaptCoeff;
+ ALfloat LastPeakSq;
+ ALfloat LastRmsSq;
+ ALfloat LastRelease;
+ ALfloat LastAttack;
+ ALfloat LastGainDev;
} Compressor;
-/* The compressor requires the following information for proper
- * initialization:
+/* The compressor is initialized with the following settings:
*
+ * NumChans - Number of channels to process.
+ * SampleRate - Sample rate to process.
+ * AutoKnee - Whether to automate the knee width parameter.
+ * AutoAttack - Whether to automate the attack time parameter.
+ * AutoRelease - Whether to automate the release time parameter.
+ * AutoPostGain - Whether to automate the make-up (post) gain parameter.
+ * AutoDeclip - Whether to automate clipping reduction. Ignored when
+ * not automating make-up gain.
+ * LookAheadTime - Look-ahead time (in seconds).
+ * HoldTime - Peak hold-time (in seconds).
* PreGainDb - Gain applied before detection (in dB).
- * PostGainDb - Gain applied after compression (in dB).
- * SummedLink - Whether to use summed (true) or maxed (false) linking.
- * RmsSensing - Whether to use RMS (true) or Peak (false) sensing.
- * AttackTimeMin - Minimum attack time (in seconds).
- * AttackTimeMax - Maximum attack time. Automates when min != max.
- * ReleaseTimeMin - Minimum release time (in seconds).
- * ReleaseTimeMax - Maximum release time. Automates when min != max.
- * Ratio - Compression ratio (x:1). Set to 0 for true limiter.
+ * PostGainDb - Make-up gain applied after compression (in dB).
* ThresholdDb - Triggering threshold (in dB).
- * KneeDb - Knee width (below threshold; in dB).
- * SampleRate - Sample rate to process.
+ * Ratio - Compression ratio (x:1). Set to INFINIFTY for true
+ * limiting. Ignored when automating knee width.
+ * KneeDb - Knee width (in dB). Ignored when automating knee
+ * width.
+ * AttackTimeMin - Attack time (in seconds). Acts as a maximum when
+ * automating attack time.
+ * ReleaseTimeMin - Release time (in seconds). Acts as a maximum when
+ * automating release time.
*/
-Compressor *CompressorInit(const ALfloat PreGainDb, const ALfloat PostGainDb,
- const ALboolean SummedLink, const ALboolean RmsSensing, const ALfloat AttackTimeMin,
- const ALfloat AttackTimeMax, const ALfloat ReleaseTimeMin, const ALfloat ReleaseTimeMax,
- const ALfloat Ratio, const ALfloat ThresholdDb, const ALfloat KneeDb,
- const ALuint SampleRate);
+Compressor* CompressorInit(const ALuint NumChans, const ALuint SampleRate,
+ const ALboolean AutoKnee, const ALboolean AutoAttack,
+ const ALboolean AutoRelease, const ALboolean AutoPostGain,
+ const ALboolean AutoDeclip, const ALfloat LookAheadTime,
+ const ALfloat HoldTime, const ALfloat PreGainDb,
+ const ALfloat PostGainDb, const ALfloat ThresholdDb,
+ const ALfloat Ratio, const ALfloat KneeDb,
+ const ALfloat AttackTime, const ALfloat ReleaseTime);
-void ApplyCompression(struct Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo,
+void ApplyCompression(struct Compressor *Comp, const ALsizei SamplesToDo,
ALfloat (*restrict OutBuffer)[BUFFERSIZE]);
+inline ALsizei GetCompressorChannelCount(const Compressor *Comp)
+{ return Comp->NumChans; }
+
inline ALuint GetCompressorSampleRate(const Compressor *Comp)
{ return Comp->SampleRate; }