496 lines
18 KiB
C++
496 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 <cmath>
|
|
|
|
#include "mozilla/FloatingPoint.h"
|
|
#include "WebAudioUtils.h"
|
|
|
|
using namespace mozilla::dom; // for WebAudioUtils
|
|
using mozilla::MakeUnique;
|
|
using mozilla::PositiveInfinity;
|
|
|
|
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));
|
|
}
|
|
|
|
size_t DynamicsCompressorKernel::sizeOfExcludingThis(
|
|
mozilla::MallocSizeOf aMallocSizeOf) const {
|
|
size_t amount = 0;
|
|
amount += m_preDelayBuffers.ShallowSizeOfExcludingThis(aMallocSizeOf);
|
|
for (size_t i = 0; i < m_preDelayBuffers.Length(); i++) {
|
|
amount += aMallocSizeOf(m_preDelayBuffers[i].get());
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
void DynamicsCompressorKernel::setNumberOfChannels(unsigned numberOfChannels) {
|
|
if (m_preDelayBuffers.Length() == numberOfChannels) return;
|
|
|
|
m_preDelayBuffers.Clear();
|
|
for (unsigned i = 0; i < numberOfChannels; ++i)
|
|
m_preDelayBuffers.AppendElement(MakeUnique<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].get(), 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 - fdlibm_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.1f;
|
|
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 = fdlibm_powf(fullRangeMakeupGain, 0.6f);
|
|
|
|
float masterLinearGain =
|
|
WebAudioUtils::ConvertDecibelsToLinear(dbPostGain) * fullRangeMakeupGain;
|
|
|
|
// Attack parameters.
|
|
attackTime = std::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 (std::isnan(m_detectorAverage)) m_detectorAverage = 1;
|
|
if (std::isinf(m_detectorAverage)) m_detectorAverage = 1;
|
|
|
|
float desiredGain = m_detectorAverage;
|
|
|
|
// Pre-warp so we get desiredGain after sin() warp below.
|
|
float scaledDesiredGain = fdlibm_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;
|
|
if (scaledDesiredGain == 0.0) {
|
|
compressionDiffDb = PositiveInfinity<float>();
|
|
} else {
|
|
compressionDiffDb = WebAudioUtils::ConvertLinearToDecibels(
|
|
m_compressorGain / scaledDesiredGain, -1000.0f);
|
|
}
|
|
|
|
if (isReleasing) {
|
|
// Release mode - compressionDiffDb should be negative dB
|
|
m_maxAttackCompressionDiffDb = -1;
|
|
|
|
// Fix gremlins.
|
|
if (std::isnan(compressionDiffDb)) compressionDiffDb = -1;
|
|
if (std::isinf(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 = std::max(-12.0f, x);
|
|
x = std::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 (std::isnan(compressionDiffDb)) compressionDiffDb = 1;
|
|
if (std::isinf(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 = std::max(0.5f, m_maxAttackCompressionDiffDb);
|
|
|
|
float x = 0.25f / effAttenDiffDb;
|
|
envelopeRate = 1 - fdlibm_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].get();
|
|
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 = std::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 = std::min(1.0f, detectorAverage);
|
|
|
|
// Fix gremlins.
|
|
if (std::isnan(detectorAverage)) detectorAverage = 1;
|
|
if (std::isinf(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 = std::min(1.0f, compressorGain);
|
|
}
|
|
|
|
// Warp pre-compression gain to smooth out sharp exponential transition
|
|
// points.
|
|
float postWarpCompressorGain =
|
|
fdlibm_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 * fdlibm_log10f(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].get();
|
|
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].get(), 0, sizeof(float) * MaxPreDelayFrames);
|
|
|
|
m_preDelayReadIndex = 0;
|
|
m_preDelayWriteIndex = DefaultPreDelayFrames;
|
|
|
|
m_maxAttackCompressionDiffDb = -1; // uninitialized state
|
|
}
|
|
|
|
} // namespace WebCore
|