// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/renderer/media/renderer_webaudiodevice_impl.h"

#include <stddef.h>

#include <memory>
#include <string>

#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "content/renderer/media/audio/audio_device_factory.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_thread_impl.h"
#include "media/base/audio_timestamp_helper.h"
#include "media/base/silent_sink_suspender.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_view.h"

using blink::WebAudioDevice;
using blink::WebAudioLatencyHint;
using blink::WebLocalFrame;
using blink::WebVector;
using blink::WebView;

namespace content {

namespace {

AudioDeviceFactory::SourceType GetLatencyHintSourceType(
    WebAudioLatencyHint::AudioContextLatencyCategory latency_category) {
  switch (latency_category) {
    case WebAudioLatencyHint::kCategoryInteractive:
      return AudioDeviceFactory::kSourceWebAudioInteractive;
    case WebAudioLatencyHint::kCategoryBalanced:
      return AudioDeviceFactory::kSourceWebAudioBalanced;
    case WebAudioLatencyHint::kCategoryPlayback:
      return AudioDeviceFactory::kSourceWebAudioPlayback;
    case WebAudioLatencyHint::kCategoryExact:
      return AudioDeviceFactory::kSourceWebAudioExact;
  }
  NOTREACHED();
  return AudioDeviceFactory::kSourceWebAudioInteractive;
}

int GetOutputBufferSize(const blink::WebAudioLatencyHint& latency_hint,
                        media::AudioLatency::LatencyType latency,
                        const media::AudioParameters& hardware_params) {
  // Adjust output buffer size according to the latency requirement.
  switch (latency) {
    case media::AudioLatency::LATENCY_INTERACTIVE:
      return media::AudioLatency::GetInteractiveBufferSize(
          hardware_params.frames_per_buffer());
      break;
    case media::AudioLatency::LATENCY_RTC:
      return media::AudioLatency::GetRtcBufferSize(
          hardware_params.sample_rate(), hardware_params.frames_per_buffer());
      break;
    case media::AudioLatency::LATENCY_PLAYBACK:
      return media::AudioLatency::GetHighLatencyBufferSize(
          hardware_params.sample_rate(), hardware_params.frames_per_buffer());
      break;
    case media::AudioLatency::LATENCY_EXACT_MS:
      return media::AudioLatency::GetExactBufferSize(
          base::TimeDelta::FromSecondsD(latency_hint.Seconds()),
          hardware_params.sample_rate(), hardware_params.frames_per_buffer());
      break;
    default:
      NOTREACHED();
  }
  return 0;
}

int FrameIdFromCurrentContext() {
  // Assumption: This method is being invoked within a V8 call stack.  CHECKs
  // will fail in the call to frameForCurrentContext() otherwise.
  //
  // Therefore, we can perform look-ups to determine which RenderView is
  // starting the audio device.  The reason for all this is because the creator
  // of the WebAudio objects might not be the actual source of the audio (e.g.,
  // an extension creates a object that is passed and used within a page).
  blink::WebLocalFrame* const web_frame =
      blink::WebLocalFrame::FrameForCurrentContext();
  RenderFrame* const render_frame = RenderFrame::FromWebFrame(web_frame);
  return render_frame ? render_frame->GetRoutingID() : MSG_ROUTING_NONE;
}

media::AudioParameters GetOutputDeviceParameters(int frame_id,
                                                 int session_id,
                                                 const std::string& device_id) {
  return AudioDeviceFactory::GetOutputDeviceInfo(frame_id, session_id,
                                                 device_id)
      .output_params();
}

}  // namespace

std::unique_ptr<RendererWebAudioDeviceImpl> RendererWebAudioDeviceImpl::Create(
    media::ChannelLayout layout,
    int channels,
    const blink::WebAudioLatencyHint& latency_hint,
    WebAudioDevice::RenderCallback* callback,
    int session_id) {
  return std::unique_ptr<RendererWebAudioDeviceImpl>(
      new RendererWebAudioDeviceImpl(layout, channels, latency_hint, callback,
                                     session_id,
                                     base::Bind(&GetOutputDeviceParameters),
                                     base::Bind(&FrameIdFromCurrentContext)));
}

RendererWebAudioDeviceImpl::RendererWebAudioDeviceImpl(
    media::ChannelLayout layout,
    int channels,
    const blink::WebAudioLatencyHint& latency_hint,
    WebAudioDevice::RenderCallback* callback,
    int session_id,
    const OutputDeviceParamsCallback& device_params_cb,
    const RenderFrameIdCallback& render_frame_id_cb)
    : latency_hint_(latency_hint),
      client_callback_(callback),
      session_id_(session_id),
      frame_id_(render_frame_id_cb.Run()) {
  DCHECK(client_callback_);
  DCHECK_NE(frame_id_, MSG_ROUTING_NONE);

  media::AudioParameters hardware_params(
      device_params_cb.Run(frame_id_, session_id_, std::string()));

  // On systems without audio hardware the returned parameters may be invalid.
  // In which case just choose whatever we want for the fake device.
  if (!hardware_params.IsValid()) {
    hardware_params.Reset(media::AudioParameters::AUDIO_FAKE,
                          media::CHANNEL_LAYOUT_STEREO, 48000, 480);
  }

  const media::AudioLatency::LatencyType latency =
      AudioDeviceFactory::GetSourceLatencyType(
          GetLatencyHintSourceType(latency_hint_.Category()));

  const int output_buffer_size =
      GetOutputBufferSize(latency_hint_, latency, hardware_params);
  DCHECK_NE(0, output_buffer_size);

  sink_params_.Reset(hardware_params.format(), layout,
                     hardware_params.sample_rate(), output_buffer_size);

  // Always set channels, this should be a no-op in all but the discrete case;
  // this call will fail if channels doesn't match the layout in other cases.
  sink_params_.set_channels_for_discrete(channels);

  // Specify the latency info to be passed to the browser side.
  sink_params_.set_latency_tag(latency);
}

RendererWebAudioDeviceImpl::~RendererWebAudioDeviceImpl() {
  DCHECK(!sink_);
}

void RendererWebAudioDeviceImpl::Start() {
  DCHECK(thread_checker_.CalledOnValidThread());

  if (sink_)
    return;  // Already started.

  sink_ = AudioDeviceFactory::NewAudioRendererSink(
      GetLatencyHintSourceType(latency_hint_.Category()), frame_id_,
      session_id_, std::string());

  // Use the media thread instead of the render thread for fake Render() calls
  // since it has special connotations for Blink and garbage collection. Timeout
  // value chosen to be highly unlikely in the normal case.
  webaudio_suspender_.reset(new media::SilentSinkSuspender(
      this, base::TimeDelta::FromSeconds(30), sink_params_, sink_,
      GetMediaTaskRunner()));
  sink_->Initialize(sink_params_, webaudio_suspender_.get());

  sink_->Start();
  sink_->Play();
}

void RendererWebAudioDeviceImpl::Stop() {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (sink_) {
    sink_->Stop();
    sink_ = nullptr;
  }

  webaudio_suspender_.reset();
}

double RendererWebAudioDeviceImpl::SampleRate() {
  return sink_params_.sample_rate();
}

int RendererWebAudioDeviceImpl::FramesPerBuffer() {
  return sink_params_.frames_per_buffer();
}

int RendererWebAudioDeviceImpl::Render(base::TimeDelta delay,
                                       base::TimeTicks delay_timestamp,
                                       int prior_frames_skipped,
                                       media::AudioBus* dest) {
  // Wrap the output pointers using WebVector.
  WebVector<float*> web_audio_dest_data(static_cast<size_t>(dest->channels()));
  for (int i = 0; i < dest->channels(); ++i)
    web_audio_dest_data[i] = dest->channel(i);

  if (!delay.is_zero()) {  // Zero values are send at the first call.
    // Substruct the bus duration to get hardware delay.
    delay -=
        media::AudioTimestampHelper::FramesToTime(dest->frames(), SampleRate());
  }
  DCHECK_GE(delay, base::TimeDelta());

  client_callback_->Render(
      web_audio_dest_data, dest->frames(), delay.InSecondsF(),
      (delay_timestamp - base::TimeTicks()).InSecondsF(), prior_frames_skipped);

  return dest->frames();
}

void RendererWebAudioDeviceImpl::OnRenderError() {
  // TODO(crogers): implement error handling.
}

void RendererWebAudioDeviceImpl::SetMediaTaskRunnerForTesting(
    const scoped_refptr<base::SingleThreadTaskRunner>& media_task_runner) {
  media_task_runner_ = media_task_runner;
}

const scoped_refptr<base::SingleThreadTaskRunner>&
RendererWebAudioDeviceImpl::GetMediaTaskRunner() {
  if (!media_task_runner_) {
    media_task_runner_ =
        RenderThreadImpl::current()->GetMediaThreadTaskRunner();
  }
  return media_task_runner_;
}

}  // namespace content
