/*
 * 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
