summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webaudio/blink/DynamicsCompressorKernel.cpp')
-rw-r--r--dom/media/webaudio/blink/DynamicsCompressorKernel.cpp494
1 files changed, 494 insertions, 0 deletions
diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
new file mode 100644
index 0000000000..cffbe49967
--- /dev/null
+++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
@@ -0,0 +1,494 @@
+/*
+ * 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 - 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 = 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 = 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 - 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 = 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