// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/webaudio/convolver_handler.h"

#include <memory>

#include "base/metrics/histogram_macros.h"
#include "base/synchronization/lock.h"
#include "media/base/audio_bus.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_convolver_options.h"
#include "third_party/blink/renderer/modules/webaudio/audio_buffer.h"
#include "third_party/blink/renderer/modules/webaudio/audio_graph_tracer.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node_input.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node_output.h"
#include "third_party/blink/renderer/platform/audio/reverb.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"

namespace blink {

namespace {

// Note about empirical tuning:
// The maximum FFT size affects reverb performance and accuracy.
// In single-threaded partitioned convolution, 8192 is a good compromise between
// amortizing FFT computation and minimizing phase errors and real-time load
// spikes with larger FFT sizes.
constexpr unsigned kMaxFftSize = 8192;

constexpr unsigned kDefaultNumberOfInputChannels = 2;
constexpr unsigned kDefaultNumberOfOutputChannels = 1;

}  // namespace

ConvolverHandler::ConvolverHandler(AudioNode& node, float sample_rate)
    : AudioHandler(NodeType::kNodeTypeConvolver, node, sample_rate) {
  AddInput();
  AddOutput(kDefaultNumberOfOutputChannels);

  // Node-specific default mixing rules.
  channel_count_ = kDefaultNumberOfInputChannels;
  SetInternalChannelCountMode(V8ChannelCountMode::Enum::kClampedMax);
  SetInternalChannelInterpretation(AudioBus::kSpeakers);

  Initialize();

  // Until something is connected, we're not actively processing, so disable
  // outputs so that we produce a single channel of silence.  The graph lock is
  // needed to be able to disable outputs.
  DeferredTaskHandler::GraphAutoLocker context_locker(
      Context()->GetDeferredTaskHandler());

  DisableOutputs();
}

scoped_refptr<ConvolverHandler> ConvolverHandler::Create(AudioNode& node,
                                                         float sample_rate) {
  return base::AdoptRef(new ConvolverHandler(node, sample_rate));
}

ConvolverHandler::~ConvolverHandler() {
  Uninitialize();
}

void ConvolverHandler::Process(uint32_t frames_to_process) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("webaudio.audionode"),
               "ConvolverHandler::Process");
  AudioBus* output_bus = Output(0).Bus();
  DCHECK(output_bus);
  DCHECK(Context()->IsAudioThread());

  // Synchronize with possible dynamic changes to the impulse response.
  base::AutoTryLock try_locker(process_lock_);
  if (try_locker.is_acquired()) {
    if (!IsInitialized() || !reverb_) {
      output_bus->Zero();
    } else {
      // Process using the convolution engine.
      // Note that we can handle the case where nothing is connected to the
      // input, in which case we'll just feed silence into the convolver.
      scoped_refptr<AudioBus> input_bus = Input(0).Bus();
      reverb_->Process(input_bus.get(), output_bus, frames_to_process);
    }
  } else {
    // Too bad - the tryLock() failed.  We must be in the middle of setting a
    // new impulse response.
    output_bus->Zero();
  }
}

void ConvolverHandler::SetBuffer(AudioBuffer* buffer,
                                 ExceptionState& exception_state) {
  DCHECK(IsMainThread());

  if (!buffer) {
    DeferredTaskHandler::GraphAutoLocker context_locker(
        Context()->GetDeferredTaskHandler());
    base::AutoLock locker(process_lock_);
    reverb_.reset();
    shared_buffer_ = nullptr;
    return;
  }

  if (buffer->sampleRate() != Context()->sampleRate()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({"The buffer sample rate of ",
                String::Number(buffer->sampleRate()),
                " does not match the context rate of ",
                String::Number(Context()->sampleRate()), " Hz."}));
    return;
  }

  unsigned number_of_channels = buffer->numberOfChannels();
  uint32_t buffer_length = buffer->length();

  // The current implementation supports only 1-, 2-, or 4-channel impulse
  // responses, with the 4-channel response being interpreted as true-stereo
  // (see Reverb class).
  bool is_channel_count_good = number_of_channels == 1 ||
                               number_of_channels == 2 ||
                               number_of_channels == 4;

  if (!is_channel_count_good) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({"The buffer must have 1, 2, or 4 channels, not ",
                String::Number(number_of_channels)}));
    return;
  }

  {
    // Get some statistics on the size of the impulse response.
    UMA_HISTOGRAM_LONG_TIMES("WebAudio.ConvolverNode.ImpulseResponseLength",
                             base::Seconds(buffer->duration()));
  }

  // Wrap the AudioBuffer by an AudioBus. It's an efficient pointer set and not
  // a memcpy().  This memory is simply used in the Reverb constructor and no
  // reference to it is kept for later use in that class.
  scoped_refptr<AudioBus> buffer_bus =
      AudioBus::Create(number_of_channels, buffer_length, false);

  // Check to see if any of the channels have been transferred.  Note that an
  // AudioBuffer cannot be created with a length of 0, so if any channel has a
  // length of 0, it was transferred.
  bool any_buffer_detached = false;
  for (unsigned i = 0; i < number_of_channels; ++i) {
    if (buffer->getChannelData(i)->length() == 0) {
      any_buffer_detached = true;
      break;
    }
  }

  if (any_buffer_detached) {
    // If any channel is detached, we're supposed to treat it as if all were.
    // This means the buffer effectively has length 0, which is the same as if
    // no buffer were given.
    DeferredTaskHandler::GraphAutoLocker context_locker(
        Context()->GetDeferredTaskHandler());
    base::AutoLock locker(process_lock_);
    reverb_.reset();
    shared_buffer_ = nullptr;
    return;
  }

  for (unsigned i = 0; i < number_of_channels; ++i) {
    buffer_bus->SetChannelMemory(
        i, buffer->getChannelData(i)->AsSpan().first(buffer_length));
  }

  buffer_bus->SetSampleRate(buffer->sampleRate());

  // Create the reverb with the given impulse response.
  std::unique_ptr<Reverb> reverb = std::make_unique<Reverb>(
      buffer_bus.get(), GetDeferredTaskHandler().RenderQuantumFrames(),
      kMaxFftSize, normalize_);

  {
    // The context must be locked since changing the buffer can
    // re-configure the number of channels that are output.
    DeferredTaskHandler::GraphAutoLocker context_locker(
        Context()->GetDeferredTaskHandler());

    // Synchronize with process().
    base::AutoLock locker(process_lock_);
    reverb_ = std::move(reverb);
    shared_buffer_ = buffer->CreateSharedAudioBuffer();
    if (buffer) {
      // This will propagate the channel count to any nodes connected further
      // downstream in the graph.
      Output(0).SetNumberOfChannels(ComputeNumberOfOutputChannels(
          Input(0).NumberOfChannels(), shared_buffer_->numberOfChannels()));
    }
  }
}

bool ConvolverHandler::RequiresTailProcessing() const {
  // Always return true even if the tail time and latency might both be zero.
  return true;
}

double ConvolverHandler::TailTime() const {
  DCHECK(Context()->IsAudioThread());
  base::AutoTryLock try_locker(process_lock_);
  if (try_locker.is_acquired()) {
    return reverb_ ? reverb_->ImpulseResponseLength() /
                         static_cast<double>(Context()->sampleRate())
                   : 0;
  }
  // Since we don't want to block the Audio Device thread, we return a large
  // value instead of trying to acquire the lock.
  return std::numeric_limits<double>::infinity();
}

double ConvolverHandler::LatencyTime() const {
  DCHECK(Context()->IsAudioThread());
  base::AutoTryLock try_locker(process_lock_);
  if (try_locker.is_acquired()) {
    return reverb_ ? reverb_->LatencyFrames() /
                         static_cast<double>(Context()->sampleRate())
                   : 0;
  }
  // Since we don't want to block the Audio Device thread, we return a large
  // value instead of trying to acquire the lock.
  return std::numeric_limits<double>::infinity();
}

unsigned ConvolverHandler::ComputeNumberOfOutputChannels(
    unsigned input_channels,
    unsigned response_channels) const {
  // The number of output channels for a Convolver must be one or two.
  // And can only be one if there's a mono source and a mono response
  // buffer.
  return ClampTo(std::max(input_channels, response_channels), 1, 2);
}

void ConvolverHandler::SetChannelCount(unsigned channel_count,
                                       ExceptionState& exception_state) {
  DCHECK(IsMainThread());
  DeferredTaskHandler::GraphAutoLocker locker(
      Context()->GetDeferredTaskHandler());

  // channelCount must be 1 or 2
  if (channel_count == 1 || channel_count == 2) {
    if (channel_count_ != channel_count) {
      channel_count_ = channel_count;
      UpdateChannelsForInputs();
    }
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        ExceptionMessages::IndexOutsideRange<uint32_t>(
            "channelCount", channel_count, 1,
            ExceptionMessages::kInclusiveBound, 2,
            ExceptionMessages::kInclusiveBound));
  }
}

void ConvolverHandler::SetChannelCountMode(V8ChannelCountMode::Enum mode,
                                           ExceptionState& exception_state) {
  DCHECK(IsMainThread());
  DeferredTaskHandler::GraphAutoLocker locker(
      Context()->GetDeferredTaskHandler());

  V8ChannelCountMode::Enum old_mode = InternalChannelCountMode();

  // The channelCountMode cannot be "max".  For a convolver node, the
  // number of input channels must be 1 or 2 (see
  // https://webaudio.github.io/web-audio-api/#audionode-channelcount-constraints)
  // and "max" would be incompatible with that.
  if (mode == V8ChannelCountMode::Enum::kMax) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "ConvolverNode: channelCountMode cannot be changed to 'max'");
    new_channel_count_mode_ = old_mode;
  } else if (mode == V8ChannelCountMode::Enum::kExplicit ||
             mode == V8ChannelCountMode::Enum::kClampedMax) {
    new_channel_count_mode_ = mode;
  } else {
    NOTREACHED();
  }

  if (new_channel_count_mode_ != old_mode) {
    Context()->GetDeferredTaskHandler().AddChangedChannelCountMode(this);
  }
}

void ConvolverHandler::CheckNumberOfChannelsForInput(AudioNodeInput* input) {
  DCHECK(Context()->IsAudioThread());
  Context()->AssertGraphOwner();

  DCHECK(input);
  DCHECK_EQ(input, &Input(0));

  {
    base::AutoTryLock try_locker(process_lock_);

    // If we couldn't get the lock, it means the main thread is in SetBuffer().
    //
    // Note: Other methods like Process(), TailTime(), and LatencyTime() also
    // acquire this lock using AutoTryLock, but they are called sequentially
    // on the audio thread and cannot run concurrently with this method (which
    // is also on the audio thread). Therefore, the only source of contention
    // in production is the main thread inside SetBuffer(), which uses a
    // blocking AutoLock.
    //
    // We can skip the update here because SetBuffer() will update the output
    // channel count itself.
    if (try_locker.is_acquired()) {
      unsigned number_of_channels = 1;
      if (shared_buffer_) {
        number_of_channels = shared_buffer_->numberOfChannels();
      }

      unsigned number_of_output_channels =
          ComputeNumberOfOutputChannels(input->NumberOfChannels(),
                                        number_of_channels);

      if (IsInitialized() &&
          number_of_output_channels != Output(0).NumberOfChannels()) {
        // We're already initialized but the channel count has changed.
        Uninitialize();
      }

      if (!IsInitialized()) {
        // This will propagate the channel count to any nodes connected further
        // downstream in the graph.
        Output(0).SetNumberOfChannels(number_of_output_channels);
        Initialize();
      }
    }
  }

  // Update the input's internal bus if needed.
  AudioHandler::CheckNumberOfChannelsForInput(input);
}

}  // namespace blink
