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

#include "third_party/blink/renderer/bindings/modules/v8/v8_stereo_panner_options.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.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/modules/webaudio/base_audio_context.h"
#include "third_party/blink/renderer/platform/audio/stereo_panner.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/wtf/math_extras.h"

namespace blink {

namespace {

// A StereoPannerNode only supports 1 or 2 channels.
constexpr unsigned kMinimumOutputChannels = 1;
constexpr unsigned kMaximumOutputChannels = 2;

}  // namespace

StereoPannerHandler::StereoPannerHandler(AudioNode& node,
                                         float sample_rate,
                                         AudioParamHandler& pan)
    : AudioHandler(NodeType::kNodeTypeStereoPanner, node, sample_rate),
      pan_(&pan),
      sample_accurate_pan_values_(node.context()->renderQuantumSize()) {
  AddInput();
  AddOutput(kMaximumOutputChannels);

  // The node-specific default mixing rules declare that StereoPannerNode
  // can handle mono to stereo and stereo to stereo conversion.
  channel_count_ = kMaximumOutputChannels;
  SetInternalChannelCountMode(V8ChannelCountMode::Enum::kClampedMax);
  SetInternalChannelInterpretation(AudioBus::kSpeakers);

  Initialize();
}

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

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

void StereoPannerHandler::Process(uint32_t frames_to_process) {
  AudioBus* output_bus = Output(0).Bus();

  if (!IsInitialized() || !Input(0).IsConnected() || !stereo_panner_.get()) {
    output_bus->Zero();
    return;
  }

  scoped_refptr<AudioBus> input_bus = Input(0).Bus();
  if (!input_bus) {
    output_bus->Zero();
    return;
  }

  bool is_sample_accurate = pan_->HasSampleAccurateValues();

  if (is_sample_accurate && pan_->IsAudioRate()) {
    // Apply sample-accurate panning specified by AudioParam automation.
    DCHECK_LE(frames_to_process, sample_accurate_pan_values_.size());
    pan_->CalculateSampleAccurateValues(
        sample_accurate_pan_values_.as_span().first(frames_to_process));
    stereo_panner_->PanWithSampleAccurateValues(
        input_bus.get(), output_bus,
        sample_accurate_pan_values_.as_span().first(frames_to_process));
    return;
  }

  // The pan value is not sample-accurate or not a-rate.  In this case, we have
  // a fixed pan value for the render and just need to incorporate any inputs to
  // the value, if any.
  float pan_value = is_sample_accurate ? pan_->FinalValue() : pan_->Value();

  stereo_panner_->PanToTargetValue(input_bus.get(), output_bus, pan_value,
                                   frames_to_process);
}

void StereoPannerHandler::ProcessOnlyAudioParams(uint32_t frames_to_process) {
  DCHECK_LE(frames_to_process, sample_accurate_pan_values_.size());

  pan_->CalculateSampleAccurateValues(
      sample_accurate_pan_values_.as_span().first(frames_to_process));
}

void StereoPannerHandler::Initialize() {
  if (IsInitialized()) {
    return;
  }

  stereo_panner_ = std::make_unique<StereoPanner>(Context()->sampleRate());

  AudioHandler::Initialize();
}

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

  if (channel_count >= kMinimumOutputChannels &&
      channel_count <= kMaximumOutputChannels) {
    if (channel_count_ != channel_count) {
      channel_count_ = channel_count;
      if (InternalChannelCountMode() != V8ChannelCountMode::Enum::kMax) {
        UpdateChannelsForInputs();
      }
    }
  } else {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        ExceptionMessages::IndexOutsideRange<uint32_t>(
            "channelCount", channel_count, kMinimumOutputChannels,
            ExceptionMessages::kInclusiveBound, kMaximumOutputChannels,
            ExceptionMessages::kInclusiveBound));
  }
}

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

  V8ChannelCountMode::Enum old_mode = InternalChannelCountMode();

  if (mode == V8ChannelCountMode::Enum::kClampedMax ||
      mode == V8ChannelCountMode::Enum::kExplicit) {
    new_channel_count_mode_ = mode;
  } else if (mode == V8ChannelCountMode::Enum::kMax) {
    // This is not supported for a StereoPannerNode, which can only handle
    // 1 or 2 channels.
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "StereoPanner: 'max' is not allowed");
    new_channel_count_mode_ = old_mode;
  } else {
    // Do nothing for other invalid values.
    new_channel_count_mode_ = old_mode;
  }

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

}  // namespace blink
