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