mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 12:37:37 +00:00
480 lines
18 KiB
C++
480 lines
18 KiB
C++
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "DynamicsCompressorKernel.h"
|
|
|
|
#include "DenormalDisabler.h"
|
|
#include <algorithm>
|
|
|
|
#include "mozilla/FloatingPoint.h"
|
|
#include "mozilla/Constants.h"
|
|
#include "WebAudioUtils.h"
|
|
|
|
using namespace std;
|
|
|
|
using mozilla::dom::WebAudioUtils;
|
|
using mozilla::IsInfinite;
|
|
using mozilla::IsNaN;
|
|
|
|
namespace WebCore {
|
|
|
|
|
|
// Metering hits peaks instantly, but releases this fast (in seconds).
|
|
const float meteringReleaseTimeConstant = 0.325f;
|
|
|
|
const float uninitializedValue = -1;
|
|
|
|
DynamicsCompressorKernel::DynamicsCompressorKernel(float sampleRate, unsigned numberOfChannels)
|
|
: m_sampleRate(sampleRate)
|
|
, m_lastPreDelayFrames(DefaultPreDelayFrames)
|
|
, m_preDelayReadIndex(0)
|
|
, m_preDelayWriteIndex(DefaultPreDelayFrames)
|
|
, m_ratio(uninitializedValue)
|
|
, m_slope(uninitializedValue)
|
|
, m_linearThreshold(uninitializedValue)
|
|
, m_dbThreshold(uninitializedValue)
|
|
, m_dbKnee(uninitializedValue)
|
|
, m_kneeThreshold(uninitializedValue)
|
|
, m_kneeThresholdDb(uninitializedValue)
|
|
, m_ykneeThresholdDb(uninitializedValue)
|
|
, m_K(uninitializedValue)
|
|
{
|
|
setNumberOfChannels(numberOfChannels);
|
|
|
|
// Initializes most member variables
|
|
reset();
|
|
|
|
m_meteringReleaseK =
|
|
static_cast<float>(WebAudioUtils::DiscreteTimeConstantForSampleRate(meteringReleaseTimeConstant, sampleRate));
|
|
}
|
|
|
|
void DynamicsCompressorKernel::setNumberOfChannels(unsigned numberOfChannels)
|
|
{
|
|
if (m_preDelayBuffers.Length() == numberOfChannels)
|
|
return;
|
|
|
|
m_preDelayBuffers.Clear();
|
|
for (unsigned i = 0; i < numberOfChannels; ++i)
|
|
m_preDelayBuffers.AppendElement(new float[MaxPreDelayFrames]);
|
|
}
|
|
|
|
void DynamicsCompressorKernel::setPreDelayTime(float preDelayTime)
|
|
{
|
|
// Re-configure look-ahead section pre-delay if delay time has changed.
|
|
unsigned preDelayFrames = preDelayTime * sampleRate();
|
|
if (preDelayFrames > MaxPreDelayFrames - 1)
|
|
preDelayFrames = MaxPreDelayFrames - 1;
|
|
|
|
if (m_lastPreDelayFrames != preDelayFrames) {
|
|
m_lastPreDelayFrames = preDelayFrames;
|
|
for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i)
|
|
memset(m_preDelayBuffers[i], 0, sizeof(float) * MaxPreDelayFrames);
|
|
|
|
m_preDelayReadIndex = 0;
|
|
m_preDelayWriteIndex = preDelayFrames;
|
|
}
|
|
}
|
|
|
|
// Exponential curve for the knee.
|
|
// It is 1st derivative matched at m_linearThreshold and asymptotically approaches the value m_linearThreshold + 1 / k.
|
|
float DynamicsCompressorKernel::kneeCurve(float x, float k)
|
|
{
|
|
// Linear up to threshold.
|
|
if (x < m_linearThreshold)
|
|
return x;
|
|
|
|
return m_linearThreshold + (1 - expf(-k * (x - m_linearThreshold))) / k;
|
|
}
|
|
|
|
// Full compression curve with constant ratio after knee.
|
|
float DynamicsCompressorKernel::saturate(float x, float k)
|
|
{
|
|
float y;
|
|
|
|
if (x < m_kneeThreshold)
|
|
y = kneeCurve(x, k);
|
|
else {
|
|
// Constant ratio after knee.
|
|
float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f);
|
|
float yDb = m_ykneeThresholdDb + m_slope * (xDb - m_kneeThresholdDb);
|
|
|
|
y = WebAudioUtils::ConvertDecibelsToLinear(yDb);
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
// Approximate 1st derivative with input and output expressed in dB.
|
|
// This slope is equal to the inverse of the compression "ratio".
|
|
// In other words, a compression ratio of 20 would be a slope of 1/20.
|
|
float DynamicsCompressorKernel::slopeAt(float x, float k)
|
|
{
|
|
if (x < m_linearThreshold)
|
|
return 1;
|
|
|
|
float x2 = x * 1.001;
|
|
|
|
float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f);
|
|
float x2Db = WebAudioUtils::ConvertLinearToDecibels(x2, -1000.0f);
|
|
|
|
float yDb = WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x, k), -1000.0f);
|
|
float y2Db = WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x2, k), -1000.0f);
|
|
|
|
float m = (y2Db - yDb) / (x2Db - xDb);
|
|
|
|
return m;
|
|
}
|
|
|
|
float DynamicsCompressorKernel::kAtSlope(float desiredSlope)
|
|
{
|
|
float xDb = m_dbThreshold + m_dbKnee;
|
|
float x = WebAudioUtils::ConvertDecibelsToLinear(xDb);
|
|
|
|
// Approximate k given initial values.
|
|
float minK = 0.1;
|
|
float maxK = 10000;
|
|
float k = 5;
|
|
|
|
for (int i = 0; i < 15; ++i) {
|
|
// A high value for k will more quickly asymptotically approach a slope of 0.
|
|
float slope = slopeAt(x, k);
|
|
|
|
if (slope < desiredSlope) {
|
|
// k is too high.
|
|
maxK = k;
|
|
} else {
|
|
// k is too low.
|
|
minK = k;
|
|
}
|
|
|
|
// Re-calculate based on geometric mean.
|
|
k = sqrtf(minK * maxK);
|
|
}
|
|
|
|
return k;
|
|
}
|
|
|
|
float DynamicsCompressorKernel::updateStaticCurveParameters(float dbThreshold, float dbKnee, float ratio)
|
|
{
|
|
if (dbThreshold != m_dbThreshold || dbKnee != m_dbKnee || ratio != m_ratio) {
|
|
// Threshold and knee.
|
|
m_dbThreshold = dbThreshold;
|
|
m_linearThreshold = WebAudioUtils::ConvertDecibelsToLinear(dbThreshold);
|
|
m_dbKnee = dbKnee;
|
|
|
|
// Compute knee parameters.
|
|
m_ratio = ratio;
|
|
m_slope = 1 / m_ratio;
|
|
|
|
float k = kAtSlope(1 / m_ratio);
|
|
|
|
m_kneeThresholdDb = dbThreshold + dbKnee;
|
|
m_kneeThreshold = WebAudioUtils::ConvertDecibelsToLinear(m_kneeThresholdDb);
|
|
|
|
m_ykneeThresholdDb = WebAudioUtils::ConvertLinearToDecibels(kneeCurve(m_kneeThreshold, k), -1000.0f);
|
|
|
|
m_K = k;
|
|
}
|
|
return m_K;
|
|
}
|
|
|
|
void DynamicsCompressorKernel::process(float* sourceChannels[],
|
|
float* destinationChannels[],
|
|
unsigned numberOfChannels,
|
|
unsigned framesToProcess,
|
|
|
|
float dbThreshold,
|
|
float dbKnee,
|
|
float ratio,
|
|
float attackTime,
|
|
float releaseTime,
|
|
float preDelayTime,
|
|
float dbPostGain,
|
|
float effectBlend, /* equal power crossfade */
|
|
|
|
float releaseZone1,
|
|
float releaseZone2,
|
|
float releaseZone3,
|
|
float releaseZone4
|
|
)
|
|
{
|
|
MOZ_ASSERT(m_preDelayBuffers.Length() == numberOfChannels);
|
|
|
|
float sampleRate = this->sampleRate();
|
|
|
|
float dryMix = 1 - effectBlend;
|
|
float wetMix = effectBlend;
|
|
|
|
float k = updateStaticCurveParameters(dbThreshold, dbKnee, ratio);
|
|
|
|
// Makeup gain.
|
|
float fullRangeGain = saturate(1, k);
|
|
float fullRangeMakeupGain = 1 / fullRangeGain;
|
|
|
|
// Empirical/perceptual tuning.
|
|
fullRangeMakeupGain = powf(fullRangeMakeupGain, 0.6f);
|
|
|
|
float masterLinearGain = WebAudioUtils::ConvertDecibelsToLinear(dbPostGain) * fullRangeMakeupGain;
|
|
|
|
// Attack parameters.
|
|
attackTime = max(0.001f, attackTime);
|
|
float attackFrames = attackTime * sampleRate;
|
|
|
|
// Release parameters.
|
|
float releaseFrames = sampleRate * releaseTime;
|
|
|
|
// Detector release time.
|
|
float satReleaseTime = 0.0025f;
|
|
float satReleaseFrames = satReleaseTime * sampleRate;
|
|
|
|
// Create a smooth function which passes through four points.
|
|
|
|
// Polynomial of the form
|
|
// y = a + b*x + c*x^2 + d*x^3 + e*x^4;
|
|
|
|
float y1 = releaseFrames * releaseZone1;
|
|
float y2 = releaseFrames * releaseZone2;
|
|
float y3 = releaseFrames * releaseZone3;
|
|
float y4 = releaseFrames * releaseZone4;
|
|
|
|
// All of these coefficients were derived for 4th order polynomial curve fitting where the y values
|
|
// match the evenly spaced x values as follows: (y1 : x == 0, y2 : x == 1, y3 : x == 2, y4 : x == 3)
|
|
float kA = 0.9999999999999998f*y1 + 1.8432219684323923e-16f*y2 - 1.9373394351676423e-16f*y3 + 8.824516011816245e-18f*y4;
|
|
float kB = -1.5788320352845888f*y1 + 2.3305837032074286f*y2 - 0.9141194204840429f*y3 + 0.1623677525612032f*y4;
|
|
float kC = 0.5334142869106424f*y1 - 1.272736789213631f*y2 + 0.9258856042207512f*y3 - 0.18656310191776226f*y4;
|
|
float kD = 0.08783463138207234f*y1 - 0.1694162967925622f*y2 + 0.08588057951595272f*y3 - 0.00429891410546283f*y4;
|
|
float kE = -0.042416883008123074f*y1 + 0.1115693827987602f*y2 - 0.09764676325265872f*y3 + 0.028494263462021576f*y4;
|
|
|
|
// x ranges from 0 -> 3 0 1 2 3
|
|
// -15 -10 -5 0db
|
|
|
|
// y calculates adaptive release frames depending on the amount of compression.
|
|
|
|
setPreDelayTime(preDelayTime);
|
|
|
|
const int nDivisionFrames = 32;
|
|
|
|
const int nDivisions = framesToProcess / nDivisionFrames;
|
|
|
|
unsigned frameIndex = 0;
|
|
for (int i = 0; i < nDivisions; ++i) {
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Calculate desired gain
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
// Fix gremlins.
|
|
if (IsNaN(m_detectorAverage))
|
|
m_detectorAverage = 1;
|
|
if (IsInfinite(m_detectorAverage))
|
|
m_detectorAverage = 1;
|
|
|
|
float desiredGain = m_detectorAverage;
|
|
|
|
// Pre-warp so we get desiredGain after sin() warp below.
|
|
float scaledDesiredGain = asinf(desiredGain) / (0.5f * M_PI);
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Deal with envelopes
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
// envelopeRate is the rate we slew from current compressor level to the desired level.
|
|
// The exact rate depends on if we're attacking or releasing and by how much.
|
|
float envelopeRate;
|
|
|
|
bool isReleasing = scaledDesiredGain > m_compressorGain;
|
|
|
|
// compressionDiffDb is the difference between current compression level and the desired level.
|
|
float compressionDiffDb = WebAudioUtils::ConvertLinearToDecibels(m_compressorGain / scaledDesiredGain, -1000.0f);
|
|
|
|
if (isReleasing) {
|
|
// Release mode - compressionDiffDb should be negative dB
|
|
m_maxAttackCompressionDiffDb = -1;
|
|
|
|
// Fix gremlins.
|
|
if (IsNaN(compressionDiffDb))
|
|
compressionDiffDb = -1;
|
|
if (IsInfinite(compressionDiffDb))
|
|
compressionDiffDb = -1;
|
|
|
|
// Adaptive release - higher compression (lower compressionDiffDb) releases faster.
|
|
|
|
// Contain within range: -12 -> 0 then scale to go from 0 -> 3
|
|
float x = compressionDiffDb;
|
|
x = max(-12.0f, x);
|
|
x = min(0.0f, x);
|
|
x = 0.25f * (x + 12);
|
|
|
|
// Compute adaptive release curve using 4th order polynomial.
|
|
// Normal values for the polynomial coefficients would create a monotonically increasing function.
|
|
float x2 = x * x;
|
|
float x3 = x2 * x;
|
|
float x4 = x2 * x2;
|
|
float releaseFrames = kA + kB * x + kC * x2 + kD * x3 + kE * x4;
|
|
|
|
#define kSpacingDb 5
|
|
float dbPerFrame = kSpacingDb / releaseFrames;
|
|
|
|
envelopeRate = WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame);
|
|
} else {
|
|
// Attack mode - compressionDiffDb should be positive dB
|
|
|
|
// Fix gremlins.
|
|
if (IsNaN(compressionDiffDb))
|
|
compressionDiffDb = 1;
|
|
if (IsInfinite(compressionDiffDb))
|
|
compressionDiffDb = 1;
|
|
|
|
// As long as we're still in attack mode, use a rate based off
|
|
// the largest compressionDiffDb we've encountered so far.
|
|
if (m_maxAttackCompressionDiffDb == -1 || m_maxAttackCompressionDiffDb < compressionDiffDb)
|
|
m_maxAttackCompressionDiffDb = compressionDiffDb;
|
|
|
|
float effAttenDiffDb = max(0.5f, m_maxAttackCompressionDiffDb);
|
|
|
|
float x = 0.25f / effAttenDiffDb;
|
|
envelopeRate = 1 - powf(x, 1 / attackFrames);
|
|
}
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Inner loop - calculate shaped power average - apply compression.
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
{
|
|
int preDelayReadIndex = m_preDelayReadIndex;
|
|
int preDelayWriteIndex = m_preDelayWriteIndex;
|
|
float detectorAverage = m_detectorAverage;
|
|
float compressorGain = m_compressorGain;
|
|
|
|
int loopFrames = nDivisionFrames;
|
|
while (loopFrames--) {
|
|
float compressorInput = 0;
|
|
|
|
// Predelay signal, computing compression amount from un-delayed version.
|
|
for (unsigned i = 0; i < numberOfChannels; ++i) {
|
|
float* delayBuffer = m_preDelayBuffers[i];
|
|
float undelayedSource = sourceChannels[i][frameIndex];
|
|
delayBuffer[preDelayWriteIndex] = undelayedSource;
|
|
|
|
float absUndelayedSource = undelayedSource > 0 ? undelayedSource : -undelayedSource;
|
|
if (compressorInput < absUndelayedSource)
|
|
compressorInput = absUndelayedSource;
|
|
}
|
|
|
|
// Calculate shaped power on undelayed input.
|
|
|
|
float scaledInput = compressorInput;
|
|
float absInput = scaledInput > 0 ? scaledInput : -scaledInput;
|
|
|
|
// Put through shaping curve.
|
|
// This is linear up to the threshold, then enters a "knee" portion followed by the "ratio" portion.
|
|
// The transition from the threshold to the knee is smooth (1st derivative matched).
|
|
// The transition from the knee to the ratio portion is smooth (1st derivative matched).
|
|
float shapedInput = saturate(absInput, k);
|
|
|
|
float attenuation = absInput <= 0.0001f ? 1 : shapedInput / absInput;
|
|
|
|
float attenuationDb = -WebAudioUtils::ConvertLinearToDecibels(attenuation, -1000.0f);
|
|
attenuationDb = max(2.0f, attenuationDb);
|
|
|
|
float dbPerFrame = attenuationDb / satReleaseFrames;
|
|
|
|
float satReleaseRate = WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame) - 1;
|
|
|
|
bool isRelease = (attenuation > detectorAverage);
|
|
float rate = isRelease ? satReleaseRate : 1;
|
|
|
|
detectorAverage += (attenuation - detectorAverage) * rate;
|
|
detectorAverage = min(1.0f, detectorAverage);
|
|
|
|
// Fix gremlins.
|
|
if (IsNaN(detectorAverage))
|
|
detectorAverage = 1;
|
|
if (IsInfinite(detectorAverage))
|
|
detectorAverage = 1;
|
|
|
|
// Exponential approach to desired gain.
|
|
if (envelopeRate < 1) {
|
|
// Attack - reduce gain to desired.
|
|
compressorGain += (scaledDesiredGain - compressorGain) * envelopeRate;
|
|
} else {
|
|
// Release - exponentially increase gain to 1.0
|
|
compressorGain *= envelopeRate;
|
|
compressorGain = min(1.0f, compressorGain);
|
|
}
|
|
|
|
// Warp pre-compression gain to smooth out sharp exponential transition points.
|
|
float postWarpCompressorGain = sinf(0.5f * M_PI * compressorGain);
|
|
|
|
// Calculate total gain using master gain and effect blend.
|
|
float totalGain = dryMix + wetMix * masterLinearGain * postWarpCompressorGain;
|
|
|
|
// Calculate metering.
|
|
float dbRealGain = 20 * log10(postWarpCompressorGain);
|
|
if (dbRealGain < m_meteringGain)
|
|
m_meteringGain = dbRealGain;
|
|
else
|
|
m_meteringGain += (dbRealGain - m_meteringGain) * m_meteringReleaseK;
|
|
|
|
// Apply final gain.
|
|
for (unsigned i = 0; i < numberOfChannels; ++i) {
|
|
float* delayBuffer = m_preDelayBuffers[i];
|
|
destinationChannels[i][frameIndex] = delayBuffer[preDelayReadIndex] * totalGain;
|
|
}
|
|
|
|
frameIndex++;
|
|
preDelayReadIndex = (preDelayReadIndex + 1) & MaxPreDelayFramesMask;
|
|
preDelayWriteIndex = (preDelayWriteIndex + 1) & MaxPreDelayFramesMask;
|
|
}
|
|
|
|
// Locals back to member variables.
|
|
m_preDelayReadIndex = preDelayReadIndex;
|
|
m_preDelayWriteIndex = preDelayWriteIndex;
|
|
m_detectorAverage = DenormalDisabler::flushDenormalFloatToZero(detectorAverage);
|
|
m_compressorGain = DenormalDisabler::flushDenormalFloatToZero(compressorGain);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicsCompressorKernel::reset()
|
|
{
|
|
m_detectorAverage = 0;
|
|
m_compressorGain = 1;
|
|
m_meteringGain = 1;
|
|
|
|
// Predelay section.
|
|
for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i)
|
|
memset(m_preDelayBuffers[i], 0, sizeof(float) * MaxPreDelayFrames);
|
|
|
|
m_preDelayReadIndex = 0;
|
|
m_preDelayWriteIndex = DefaultPreDelayFrames;
|
|
|
|
m_maxAttackCompressionDiffDb = -1; // uninitialized state
|
|
}
|
|
|
|
} // namespace WebCore
|