diff options
Diffstat (limited to 'dom/media/webaudio/blink/DynamicsCompressorKernel.cpp')
-rw-r--r-- | dom/media/webaudio/blink/DynamicsCompressorKernel.cpp | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp new file mode 100644 index 000000000..e5b4aba2f --- /dev/null +++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp @@ -0,0 +1,491 @@ +/* + * 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 std; + +using namespace mozilla::dom; // for WebAudioUtils +using mozilla::IsInfinite; +using mozilla::IsNaN; +using mozilla::MakeUnique; + +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 - 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 = 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].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 = 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].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 |