/*
 * 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 "DynamicsCompressor.h"

#include "AlignmentUtils.h"
#include "AudioBlock.h"
#include "AudioNodeEngine.h"
#include "nsDebug.h"

using mozilla::AudioBlockCopyChannelWithScale;
using mozilla::WEBAUDIO_BLOCK_SIZE;

namespace WebCore {

DynamicsCompressor::DynamicsCompressor(float sampleRate,
                                       unsigned numberOfChannels)
    : m_numberOfChannels(numberOfChannels),
      m_sampleRate(sampleRate),
      m_compressor(sampleRate, numberOfChannels) {
  // Uninitialized state - for parameter recalculation.
  m_lastFilterStageRatio = -1;
  m_lastAnchor = -1;
  m_lastFilterStageGain = -1;

  setNumberOfChannels(numberOfChannels);
  initializeParameters();
}

size_t DynamicsCompressor::sizeOfIncludingThis(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  size_t amount = aMallocSizeOf(this);
  amount += m_preFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf);
  for (size_t i = 0; i < m_preFilterPacks.Length(); i++) {
    if (m_preFilterPacks[i]) {
      amount += m_preFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf);
    }
  }

  amount += m_postFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf);
  for (size_t i = 0; i < m_postFilterPacks.Length(); i++) {
    if (m_postFilterPacks[i]) {
      amount += m_postFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf);
    }
  }

  amount += aMallocSizeOf(m_sourceChannels.get());
  amount += aMallocSizeOf(m_destinationChannels.get());
  amount += m_compressor.sizeOfExcludingThis(aMallocSizeOf);
  return amount;
}

void DynamicsCompressor::setParameterValue(unsigned parameterID, float value) {
  MOZ_ASSERT(parameterID < ParamLast);
  if (parameterID < ParamLast) m_parameters[parameterID] = value;
}

void DynamicsCompressor::initializeParameters() {
  // Initializes compressor to default values.

  m_parameters[ParamThreshold] = -24;    // dB
  m_parameters[ParamKnee] = 30;          // dB
  m_parameters[ParamRatio] = 12;         // unit-less
  m_parameters[ParamAttack] = 0.003f;    // seconds
  m_parameters[ParamRelease] = 0.250f;   // seconds
  m_parameters[ParamPreDelay] = 0.006f;  // seconds

  // Release zone values 0 -> 1.
  m_parameters[ParamReleaseZone1] = 0.09f;
  m_parameters[ParamReleaseZone2] = 0.16f;
  m_parameters[ParamReleaseZone3] = 0.42f;
  m_parameters[ParamReleaseZone4] = 0.98f;

  m_parameters[ParamFilterStageGain] = 4.4f;  // dB
  m_parameters[ParamFilterStageRatio] = 2;
  m_parameters[ParamFilterAnchor] = 15000 / nyquist();

  m_parameters[ParamPostGain] = 0;   // dB
  m_parameters[ParamReduction] = 0;  // dB

  // Linear crossfade (0 -> 1).
  m_parameters[ParamEffectBlend] = 1;
}

float DynamicsCompressor::parameterValue(unsigned parameterID) {
  MOZ_ASSERT(parameterID < ParamLast);
  return m_parameters[parameterID];
}

void DynamicsCompressor::setEmphasisStageParameters(
    unsigned stageIndex, float gain, float normalizedFrequency /* 0 -> 1 */) {
  float gk = 1 - gain / 20;
  float f1 = normalizedFrequency * gk;
  float f2 = normalizedFrequency / gk;
  float r1 = fdlibm_expf(-f1 * M_PI);
  float r2 = fdlibm_expf(-f2 * M_PI);

  MOZ_ASSERT(m_numberOfChannels == m_preFilterPacks.Length());

  for (unsigned i = 0; i < m_numberOfChannels; ++i) {
    // Set pre-filter zero and pole to create an emphasis filter.
    ZeroPole& preFilter = m_preFilterPacks[i]->filters[stageIndex];
    preFilter.setZero(r1);
    preFilter.setPole(r2);

    // Set post-filter with zero and pole reversed to create the de-emphasis
    // filter. If there were no compressor kernel in between, they would cancel
    // each other out (allpass filter).
    ZeroPole& postFilter = m_postFilterPacks[i]->filters[stageIndex];
    postFilter.setZero(r2);
    postFilter.setPole(r1);
  }
}

void DynamicsCompressor::setEmphasisParameters(float gain, float anchorFreq,
                                               float filterStageRatio) {
  setEmphasisStageParameters(0, gain, anchorFreq);
  setEmphasisStageParameters(1, gain, anchorFreq / filterStageRatio);
  setEmphasisStageParameters(
      2, gain, anchorFreq / (filterStageRatio * filterStageRatio));
  setEmphasisStageParameters(
      3, gain,
      anchorFreq / (filterStageRatio * filterStageRatio * filterStageRatio));
}

void DynamicsCompressor::process(const AudioBlock* sourceChunk,
                                 AudioBlock* destinationChunk,
                                 unsigned framesToProcess) {
  // Though numberOfChannels is retrived from destinationBus, we still name it
  // numberOfChannels instead of numberOfDestinationChannels. It's because we
  // internally match sourceChannels's size to destinationBus by channel up/down
  // mix. Thus we need numberOfChannels to do the loop work for both
  // m_sourceChannels and m_destinationChannels.

  unsigned numberOfChannels = destinationChunk->ChannelCount();
  unsigned numberOfSourceChannels = sourceChunk->ChannelCount();

  MOZ_ASSERT(numberOfChannels == m_numberOfChannels && numberOfSourceChannels);

  if (numberOfChannels != m_numberOfChannels || !numberOfSourceChannels) {
    destinationChunk->SetNull(WEBAUDIO_BLOCK_SIZE);
    return;
  }

  switch (numberOfChannels) {
    case 2:  // stereo
      m_sourceChannels[0] =
          static_cast<const float*>(sourceChunk->mChannelData[0]);

      if (numberOfSourceChannels > 1)
        m_sourceChannels[1] =
            static_cast<const float*>(sourceChunk->mChannelData[1]);
      else
        // Simply duplicate mono channel input data to right channel for stereo
        // processing.
        m_sourceChannels[1] = m_sourceChannels[0];

      break;
    case 1:
      m_sourceChannels[0] =
          static_cast<const float*>(sourceChunk->mChannelData[0]);
      break;
    default:
      MOZ_CRASH("not supported.");
  }

  for (unsigned i = 0; i < numberOfChannels; ++i)
    m_destinationChannels[i] = const_cast<float*>(
        static_cast<const float*>(destinationChunk->mChannelData[i]));

  float filterStageGain = parameterValue(ParamFilterStageGain);
  float filterStageRatio = parameterValue(ParamFilterStageRatio);
  float anchor = parameterValue(ParamFilterAnchor);

  if (filterStageGain != m_lastFilterStageGain ||
      filterStageRatio != m_lastFilterStageRatio || anchor != m_lastAnchor) {
    m_lastFilterStageGain = filterStageGain;
    m_lastFilterStageRatio = filterStageRatio;
    m_lastAnchor = anchor;

    setEmphasisParameters(filterStageGain, anchor, filterStageRatio);
  }

  float sourceWithVolume[WEBAUDIO_BLOCK_SIZE + 4];
  float* alignedSourceWithVolume = ALIGNED16(sourceWithVolume);
  ASSERT_ALIGNED16(alignedSourceWithVolume);

  // Apply pre-emphasis filter.
  // Note that the final three stages are computed in-place in the destination
  // buffer.
  for (unsigned i = 0; i < numberOfChannels; ++i) {
    const float* sourceData;
    if (sourceChunk->mVolume == 1.0f) {
      // Fast path, the volume scale doesn't need to get taken into account
      sourceData = m_sourceChannels[i];
    } else {
      AudioBlockCopyChannelWithScale(m_sourceChannels[i], sourceChunk->mVolume,
                                     alignedSourceWithVolume);
      sourceData = alignedSourceWithVolume;
    }

    float* destinationData = m_destinationChannels[i];
    ZeroPole* preFilters = m_preFilterPacks[i]->filters;

    preFilters[0].process(sourceData, destinationData, framesToProcess);
    preFilters[1].process(destinationData, destinationData, framesToProcess);
    preFilters[2].process(destinationData, destinationData, framesToProcess);
    preFilters[3].process(destinationData, destinationData, framesToProcess);
  }

  float dbThreshold = parameterValue(ParamThreshold);
  float dbKnee = parameterValue(ParamKnee);
  float ratio = parameterValue(ParamRatio);
  float attackTime = parameterValue(ParamAttack);
  float releaseTime = parameterValue(ParamRelease);
  float preDelayTime = parameterValue(ParamPreDelay);

  // This is effectively a master volume on the compressed signal
  // (pre-blending).
  float dbPostGain = parameterValue(ParamPostGain);

  // Linear blending value from dry to completely processed (0 -> 1)
  // 0 means the signal is completely unprocessed.
  // 1 mixes in only the compressed signal.
  float effectBlend = parameterValue(ParamEffectBlend);

  float releaseZone1 = parameterValue(ParamReleaseZone1);
  float releaseZone2 = parameterValue(ParamReleaseZone2);
  float releaseZone3 = parameterValue(ParamReleaseZone3);
  float releaseZone4 = parameterValue(ParamReleaseZone4);

  // Apply compression to the pre-filtered signal.
  // The processing is performed in place.
  m_compressor.process(m_destinationChannels.get(), m_destinationChannels.get(),
                       numberOfChannels, framesToProcess,

                       dbThreshold, dbKnee, ratio, attackTime, releaseTime,
                       preDelayTime, dbPostGain, effectBlend,

                       releaseZone1, releaseZone2, releaseZone3, releaseZone4);

  // Update the compression amount.
  setParameterValue(ParamReduction, m_compressor.meteringGain());

  // Apply de-emphasis filter.
  for (unsigned i = 0; i < numberOfChannels; ++i) {
    float* destinationData = m_destinationChannels[i];
    ZeroPole* postFilters = m_postFilterPacks[i]->filters;

    postFilters[0].process(destinationData, destinationData, framesToProcess);
    postFilters[1].process(destinationData, destinationData, framesToProcess);
    postFilters[2].process(destinationData, destinationData, framesToProcess);
    postFilters[3].process(destinationData, destinationData, framesToProcess);
  }
}

void DynamicsCompressor::reset() {
  m_lastFilterStageRatio = -1;  // for recalc
  m_lastAnchor = -1;
  m_lastFilterStageGain = -1;

  for (unsigned channel = 0; channel < m_numberOfChannels; ++channel) {
    for (unsigned stageIndex = 0; stageIndex < 4; ++stageIndex) {
      m_preFilterPacks[channel]->filters[stageIndex].reset();
      m_postFilterPacks[channel]->filters[stageIndex].reset();
    }
  }

  m_compressor.reset();
}

void DynamicsCompressor::setNumberOfChannels(unsigned numberOfChannels) {
  if (m_preFilterPacks.Length() == numberOfChannels) return;

  m_preFilterPacks.Clear();
  m_postFilterPacks.Clear();
  for (unsigned i = 0; i < numberOfChannels; ++i) {
    m_preFilterPacks.AppendElement(new ZeroPoleFilterPack4());
    m_postFilterPacks.AppendElement(new ZeroPoleFilterPack4());
  }

  m_sourceChannels = mozilla::MakeUnique<const float*[]>(numberOfChannels);
  m_destinationChannels = mozilla::MakeUnique<float*[]>(numberOfChannels);

  m_compressor.setNumberOfChannels(numberOfChannels);
  m_numberOfChannels = numberOfChannels;
}

}  // namespace WebCore
