diff options
-rw-r--r-- | Alc/ALc.c | 21 | ||||
-rw-r--r-- | Alc/ALu.c | 95 | ||||
-rw-r--r-- | Alc/mastering.c | 255 | ||||
-rw-r--r-- | CMakeLists.txt | 1 | ||||
-rw-r--r-- | OpenAL32/Include/alMain.h | 32 | ||||
-rw-r--r-- | OpenAL32/Include/alu.h | 22 |
6 files changed, 305 insertions, 121 deletions
@@ -1738,6 +1738,12 @@ 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, -0.5f, 3.0f, device->Frequency); +} + /* UpdateClockBase * * Updates the device's base clock time with however many samples have been @@ -2224,8 +2230,11 @@ static ALCenum UpdateDeviceParams(ALCdevice *device, const ALCint *attrList) */ if(gainLimiter != ALC_FALSE) { - if(!device->Limiter) - device->Limiter = alloc_limiter(); + if(!device->Limiter || device->Frequency != GetCompressorSampleRate(device->Limiter)) + { + al_free(device->Limiter); + device->Limiter = CreateDeviceLimiter(device); + } } else { @@ -3845,7 +3854,7 @@ ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *deviceName) device->FOAOut.NumChannels = 0; device->RealOut.Buffer = NULL; device->RealOut.NumChannels = 0; - device->Limiter = alloc_limiter(); + device->Limiter = NULL; device->AvgSpeakerDist = 0.0f; ATOMIC_INIT(&device->ContextList, NULL); @@ -4021,6 +4030,8 @@ ALC_API ALCdevice* ALC_APIENTRY alcOpenDevice(const ALCchar *deviceName) alstr_get_cstr(device->DeviceName), NULL, "dither", 1 ); + device->Limiter = CreateDeviceLimiter(device); + if(DefaultEffect.type != AL_EFFECT_NULL) { device->DefaultSlot = (ALeffectslot*)device->_slot_mem; @@ -4378,7 +4389,7 @@ ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceN device->FOAOut.NumChannels = 0; device->RealOut.Buffer = NULL; device->RealOut.NumChannels = 0; - device->Limiter = alloc_limiter(); + device->Limiter = NULL; device->AvgSpeakerDist = 0.0f; ATOMIC_INIT(&device->ContextList, NULL); @@ -4441,6 +4452,8 @@ ALC_API ALCdevice* ALC_APIENTRY alcLoopbackOpenDeviceSOFT(const ALCchar *deviceN device->DitherEnabled = GetConfigValueBool(NULL, NULL, "dither", 1); + device->Limiter = CreateDeviceLimiter(device); + { ALCdevice *head = ATOMIC_LOAD_SEQ(&DeviceList); do { @@ -100,17 +100,6 @@ const aluMatrixf IdentityMatrixf = {{ }}; -struct OutputLimiter *alloc_limiter(void) -{ - struct OutputLimiter *limiter = al_calloc(16, sizeof(*limiter)); - /* Limiter attack drops -80dB in 50ms. */ - limiter->AttackRate = 0.05f; - /* Limiter release raises +80dB in 1s. */ - limiter->ReleaseRate = 1.0f; - limiter->Gain = 1.0f; - return limiter; -} - void DeinitVoice(ALvoice *voice) { struct ALvoiceProps *props; @@ -1538,78 +1527,6 @@ static void ApplyDistanceComp(ALfloatBUFFERSIZE *restrict Samples, DistanceComp } -static_assert(LIMITER_VALUE_MAX < (UINT_MAX/LIMITER_WINDOW_SIZE), "LIMITER_VALUE_MAX is too big"); - -static void ApplyLimiter(struct OutputLimiter *Limiter, - ALfloat (*restrict OutBuffer)[BUFFERSIZE], const ALsizei NumChans, - const ALfloat AttackRate, const ALfloat ReleaseRate, - ALfloat *restrict Values, const ALsizei SamplesToDo) -{ - bool do_limit = false; - ALsizei c, i; - - OutBuffer = ASSUME_ALIGNED(OutBuffer, 16); - Values = ASSUME_ALIGNED(Values, 16); - - for(i = 0;i < SamplesToDo;i++) - Values[i] = 0.0f; - - /* First, find the maximum amplitude (squared) for each sample position in each channel. */ - for(c = 0;c < NumChans;c++) - { - for(i = 0;i < SamplesToDo;i++) - { - ALfloat amp = OutBuffer[c][i]; - Values[i] = maxf(Values[i], amp*amp); - } - } - - /* Next, calculate the gains needed to limit the output. */ - { - ALfloat lastgain = Limiter->Gain; - ALsizei wpos = Limiter->Pos; - ALuint sum = Limiter->SquaredSum; - ALfloat gain, rms; - - for(i = 0;i < SamplesToDo;i++) - { - sum -= Limiter->Window[wpos]; - Limiter->Window[wpos] = fastf2u(minf(Values[i]*65536.0f, LIMITER_VALUE_MAX)); - sum += Limiter->Window[wpos]; - - rms = sqrtf((ALfloat)sum / ((ALfloat)LIMITER_WINDOW_SIZE*65536.0f)); - - /* Clamp the minimum RMS to 0dB. The uint used for the squared sum - * inherently limits the maximum RMS to about 21dB, thus the gain - * ranges from 0dB to -21dB. - */ - gain = 1.0f / maxf(rms, 1.0f); - if(lastgain >= gain) - lastgain = maxf(lastgain*AttackRate, gain); - else - lastgain = minf(lastgain/ReleaseRate, gain); - do_limit |= (lastgain < 1.0f); - Values[i] = lastgain; - - wpos = (wpos+1)&LIMITER_WINDOW_MASK; - } - - Limiter->Gain = lastgain; - Limiter->Pos = wpos; - Limiter->SquaredSum = sum; - } - if(do_limit) - { - /* Finally, apply the gains to each channel. */ - for(c = 0;c < NumChans;c++) - { - for(i = 0;i < SamplesToDo;i++) - OutBuffer[c][i] *= Values[i]; - } - } -} - - /* NOTE: Non-dithered conversions have unused extra parameters. */ static inline ALfloat aluF2F(ALfloat val, ...) { return val; } @@ -1857,7 +1774,7 @@ void aluMixData(ALCdevice *device, ALvoid *buffer, ALsizei size) { ALfloat (*OutBuffer)[BUFFERSIZE] = device->RealOut.Buffer; ALsizei OutChannels = device->RealOut.NumChannels; - struct OutputLimiter *Limiter = device->Limiter; + struct Compressor *Limiter = device->Limiter; ALfloat *DitherValues; /* Use NFCtrlData for temp value storage. */ @@ -1865,15 +1782,7 @@ void aluMixData(ALCdevice *device, ALvoid *buffer, ALsizei size) SamplesToDo, OutChannels); if(Limiter) - { - const ALfloat AttackRate = powf(0.0001f, 1.0f/(device->Frequency*Limiter->AttackRate)); - const ALfloat ReleaseRate = powf(0.0001f, 1.0f/(device->Frequency*Limiter->ReleaseRate)); - - /* Use NFCtrlData for temp value storage. */ - ApplyLimiter(Limiter, OutBuffer, OutChannels, - AttackRate, ReleaseRate, device->NFCtrlData, SamplesToDo - ); - } + ApplyCompression(Limiter, OutChannels, SamplesToDo, OutBuffer); /* Dithering. Step 1, generate whitenoise (uniform distribution of * random values between -1 and +1). Use NFCtrlData for random diff --git a/Alc/mastering.c b/Alc/mastering.c new file mode 100644 index 00000000..c9f5bebd --- /dev/null +++ b/Alc/mastering.c @@ -0,0 +1,255 @@ +#include "config.h" + +#include <math.h> + +#include "alu.h" +#include "almalloc.h" + +#define RMS_WINDOW_SIZE (1<<7) +#define RMS_WINDOW_MASK (RMS_WINDOW_SIZE-1) +#define RMS_VALUE_MAX (1<<24) + +#define LOOKAHEAD_SIZE (1<<13) +#define LOOKAHEAD_MASK (LOOKAHEAD_SIZE-1) + +static_assert(RMS_VALUE_MAX < (UINT_MAX / RMS_WINDOW_SIZE), "RMS_VALUE_MAX is too big"); + +typedef struct Compressor { + ALfloat PreGain; + ALfloat PostGain; + ALboolean SummedLink; + ALfloat AttackMin; + ALfloat AttackMax; + ALfloat ReleaseMin; + ALfloat ReleaseMax; + ALfloat Ratio; + ALfloat Threshold; + ALfloat Knee; + ALuint SampleRate; + + ALuint RmsSum; + ALuint *RmsWindow; + ALsizei RmsIndex; + ALfloat Envelope[BUFFERSIZE]; + ALfloat EnvLast; +} Compressor; + +/* Multichannel compression is linked via one of two modes: + * + * Summed - Absolute sum of all channels. + * Maxed - Absolute maximum of any channel. + */ +static void SumChannels(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo, + ALfloat (*restrict OutBuffer)[BUFFERSIZE]) +{ + ALsizei c, i; + + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] = 0.0f; + + for(c = 0;c < NumChans;c++) + { + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] += OutBuffer[c][i]; + } + + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] = fabsf(Comp->Envelope[i]); +} + +static void MaxChannels(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo, + ALfloat (*restrict OutBuffer)[BUFFERSIZE]) +{ + ALsizei c, i; + + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] = 0.0f; + + for(c = 0;c < NumChans;c++) + { + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] = maxf(Comp->Envelope[i], 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. + */ +static void RmsDetection(Compressor *Comp, const ALsizei SamplesToDo) +{ + ALuint sum = Comp->RmsSum; + ALuint *window = Comp->RmsWindow; + ALsizei index = Comp->RmsIndex; + ALsizei i; + + for(i = 0;i < SamplesToDo;i++) + { + ALfloat sig = Comp->Envelope[i]; + + sum -= window[index]; + window[index] = fastf2u(minf(sig * sig * 65536.0f, RMS_VALUE_MAX)); + sum += window[index]; + index = (index + 1) & RMS_WINDOW_MASK; + + Comp->Envelope[i] = sqrtf(sum / 65536.0f / RMS_WINDOW_SIZE); + } + + Comp->RmsSum = sum; + Comp->RmsIndex = index; +} + +/* 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. + */ +static void FollowEnvelope(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; + ALsizei i; + + for(i = 0;i < SamplesToDo;i++) + { + ALfloat env = maxf(-6.0f, log10f(Comp->Envelope[i])); + ALfloat slope = minf(1.0f, fabsf(env - last) / 4.5f); + + 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))); + + Comp->Envelope[i] = last; + } + + Comp->EnvLast = last; +} + +/* The envelope is converted to control gain with an optional soft knee. */ +static void EnvelopeGain(Compressor *Comp, const ALsizei SamplesToDo, const ALfloat Slope) +{ + const ALfloat threshold = Comp->Threshold; + const ALfloat knee = Comp->Knee; + ALsizei i; + + if(!(knee > 0.0f)) + { + for(i = 0;i < SamplesToDo;i++) + { + ALfloat gain = Slope * (threshold - Comp->Envelope[i]); + Comp->Envelope[i] = powf(10.0f, minf(0.0f, gain)); + } + } + else + { + const ALfloat lower = threshold - (0.5f * knee); + const ALfloat upper = threshold + (0.5f * knee); + const ALfloat m = 0.5f * Slope / knee; + + 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); + + Comp->Envelope[i] = powf(10.0f, minf(0.0f, gain)); + } + } +} + + +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 *Comp; + size_t size; + ALsizei i; + + 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; + + Comp->RmsSum = 0; + if(RmsSensing) + Comp->RmsWindow = (ALuint*)(Comp+1); + else + Comp->RmsWindow = NULL; + Comp->RmsIndex = 0; + + for(i = 0;i < BUFFERSIZE;i++) + Comp->Envelope[i] = 0.0f; + Comp->EnvLast = -6.0f; + + return Comp; +} + +ALuint GetCompressorSampleRate(const Compressor *Comp) +{ + return Comp->SampleRate; +} + +void ApplyCompression(Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo, + ALfloat (*restrict OutBuffer)[BUFFERSIZE]) +{ + ALsizei c, i; + + if(Comp->PreGain != 1.0f) + { + for(c = 0;c < NumChans;c++) + { + for(i = 0;i < SamplesToDo;i++) + OutBuffer[c][i] *= Comp->PreGain; + } + } + + if(Comp->SummedLink) + SumChannels(Comp, NumChans, SamplesToDo, OutBuffer); + else + MaxChannels(Comp, NumChans, SamplesToDo, OutBuffer); + + if(Comp->RmsWindow) + RmsDetection(Comp, SamplesToDo); + FollowEnvelope(Comp, SamplesToDo); + + if(Comp->Ratio > 0.0f) + EnvelopeGain(Comp, SamplesToDo, 1.0f - (1.0f / Comp->Ratio)); + else + EnvelopeGain(Comp, SamplesToDo, 1.0f); + + if(Comp->PostGain != 1.0f) + { + for(i = 0;i < SamplesToDo;i++) + Comp->Envelope[i] *= Comp->PostGain; + } + for(c = 0;c < NumChans;c++) + { + for(i = 0;i < SamplesToDo;i++) + OutBuffer[c][i] *= Comp->Envelope[i]; + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 66402624..15311bc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -694,6 +694,7 @@ SET(ALC_OBJS Alc/ALc.c Alc/alcRing.c Alc/bs2b.c Alc/converter.c + Alc/mastering.c Alc/effects/chorus.c Alc/effects/compressor.c Alc/effects/dedicated.c diff --git a/OpenAL32/Include/alMain.h b/OpenAL32/Include/alMain.h index 2ce98745..6e17651f 100644 --- a/OpenAL32/Include/alMain.h +++ b/OpenAL32/Include/alMain.h @@ -380,7 +380,7 @@ extern "C" { struct Hrtf; struct HrtfEntry; -struct OutputLimiter; +struct Compressor; #define DEFAULT_OUTPUT_RATE (44100) @@ -783,7 +783,7 @@ struct ALCdevice_struct ALsizei NumChannels; } RealOut; - struct OutputLimiter *Limiter; + struct Compressor *Limiter; /* The average speaker distance as determined by the ambdec configuration * (or alternatively, by the NFC-HOA reference delay). Only used for NFC. @@ -1047,6 +1047,34 @@ vector_al_string SearchDataFiles(const char *match, const char *subdir); typedef ALfloat ALfloatBUFFERSIZE[BUFFERSIZE]; typedef ALfloat ALfloat2[2]; + +/* The compressor requires the following information for proper + * initialization: + * + * 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. + * ThresholdDb - Triggering threshold (in dB). + * KneeDb - Knee width (below threshold; in dB). + * SampleRate - Sample rate to process. + */ +struct 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); + +ALuint GetCompressorSampleRate(const struct Compressor *Comp); + +void ApplyCompression(struct Compressor *Comp, const ALsizei NumChans, const ALsizei SamplesToDo, + ALfloat (*restrict OutBuffer)[BUFFERSIZE]); + #ifdef __cplusplus } #endif diff --git a/OpenAL32/Include/alu.h b/OpenAL32/Include/alu.h index 8a56ddb2..0b3799c0 100644 --- a/OpenAL32/Include/alu.h +++ b/OpenAL32/Include/alu.h @@ -300,28 +300,6 @@ typedef struct ALvoice { void DeinitVoice(ALvoice *voice); -#define LIMITER_WINDOW_SIZE (1<<7) /* 128 */ -#define LIMITER_WINDOW_MASK (LIMITER_WINDOW_SIZE-1) -#define LIMITER_VALUE_MAX (1<<24) /* 16777216 */ -struct OutputLimiter { - /* RMS detection window, sum of values in the window, and the next write - * pos. Values are 16.16 fixed-point. - */ - ALuint Window[LIMITER_WINDOW_SIZE]; - ALuint SquaredSum; - ALsizei Pos; - - /* In milliseconds. */ - ALfloat AttackRate; - ALfloat ReleaseRate; - - /* The gain last used for limiting. */ - ALfloat Gain; -}; - -struct OutputLimiter *alloc_limiter(void); - - typedef void (*MixerFunc)(const ALfloat *data, ALsizei OutChans, ALfloat (*restrict OutBuffer)[BUFFERSIZE], ALfloat *CurrentGains, const ALfloat *TargetGains, ALsizei Counter, ALsizei OutPos, |