| // Copyright 2012 The Chromium Authors |
| // 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 <algorithm> |
| #include <memory> |
| #include <string> |
| |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/notreached.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "media/audio/audio_features.h" |
| #include "media/audio/null_audio_sink.h" |
| #include "media/base/audio_glitch_info.h" |
| #include "media/base/audio_timestamp_helper.h" |
| #include "media/base/limits.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/output_device_info.h" |
| #include "media/base/silent_sink_suspender.h" |
| #include "media/base/speech_recognition_client.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/platform/audio/web_audio_device_source_type.h" |
| #include "third_party/blink/public/platform/modules/webrtc/webrtc_logging.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/public/web/modules/media/audio/audio_device_factory.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/public/web/web_local_frame_client.h" |
| #include "third_party/blink/public/web/web_view.h" |
| |
| using blink::AudioDeviceFactory; |
| using blink::WebAudioDevice; |
| using blink::WebAudioLatencyHint; |
| using blink::WebAudioSinkDescriptor; |
| using blink::WebLocalFrame; |
| using blink::WebView; |
| |
| namespace content { |
| |
| namespace { |
| |
| using ::media::limits::kMaxWebAudioBufferSize; |
| using ::media::limits::kMinWebAudioBufferSize; |
| |
| blink::WebAudioDeviceSourceType GetLatencyHintSourceType( |
| WebAudioLatencyHint::AudioContextLatencyCategory latency_category) { |
| switch (latency_category) { |
| case WebAudioLatencyHint::kCategoryInteractive: |
| return blink::WebAudioDeviceSourceType::kWebAudioInteractive; |
| case WebAudioLatencyHint::kCategoryBalanced: |
| return blink::WebAudioDeviceSourceType::kWebAudioBalanced; |
| case WebAudioLatencyHint::kCategoryPlayback: |
| return blink::WebAudioDeviceSourceType::kWebAudioPlayback; |
| case WebAudioLatencyHint::kCategoryExact: |
| return blink::WebAudioDeviceSourceType::kWebAudioExact; |
| case WebAudioLatencyHint::kLastValue: |
| NOTREACHED(); |
| } |
| NOTREACHED(); |
| } |
| |
| |
| media::AudioParameters GetOutputDeviceParameters( |
| const blink::LocalFrameToken& frame_token, |
| const std::string& device_id) { |
| TRACE_EVENT0("webaudio", "GetOutputDeviceParameters"); |
| return AudioDeviceFactory::GetInstance() |
| ->GetOutputDeviceInfo(frame_token, device_id) |
| .output_params(); |
| } |
| |
| scoped_refptr<media::AudioRendererSink> GetNullAudioSink( |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner) { |
| return base::MakeRefCounted<media::NullAudioSink>(task_runner); |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<RendererWebAudioDeviceImpl> RendererWebAudioDeviceImpl::Create( |
| const WebAudioSinkDescriptor& sink_descriptor, |
| int number_of_output_channels, |
| const blink::WebAudioLatencyHint& latency_hint, |
| std::optional<float> context_sample_rate, |
| media::AudioRendererSink::RenderCallback* callback) { |
| // The `number_of_output_channels` does not manifest the actual channel |
| // layout of the audio output device. We use the best guess to the channel |
| // layout based on the number of channels. |
| media::ChannelLayout layout = |
| media::GuessChannelLayout(number_of_output_channels); |
| |
| // Use "discrete" channel layout when the best guess was not successful. |
| if (layout == media::CHANNEL_LAYOUT_UNSUPPORTED) { |
| layout = media::CHANNEL_LAYOUT_DISCRETE; |
| } |
| |
| return std::unique_ptr<RendererWebAudioDeviceImpl>( |
| new RendererWebAudioDeviceImpl( |
| sink_descriptor, {layout, number_of_output_channels}, latency_hint, |
| context_sample_rate, callback, |
| base::BindOnce(&GetOutputDeviceParameters), |
| base::BindRepeating(&GetNullAudioSink))); |
| } |
| |
| int RendererWebAudioDeviceImpl::GetOutputBufferSize( |
| const blink::WebAudioLatencyHint& latency_hint, |
| int resolved_context_sample_rate, |
| const media::AudioParameters& hardware_params) { |
| const media::AudioParameters::HardwareCapabilities hardware_capabilities = |
| hardware_params.hardware_capabilities().value_or( |
| media::AudioParameters::HardwareCapabilities()); |
| |
| const float scale_factor = static_cast<float>(resolved_context_sample_rate) / |
| hardware_params.sample_rate(); |
| |
| int min_hardware_buffer_size = hardware_capabilities.min_frames_per_buffer; |
| int max_hardware_buffer_size = hardware_capabilities.max_frames_per_buffer; |
| |
| // The hardware may not provide explicit buffer size limits. In such cases, |
| // we fall back to predefined minimum and maximum buffer sizes. Additionally, |
| // hardware-provided limits are defined at the hardware's default sample rate. |
| // We must scale these limits to the context's sample rate, as subsequent |
| // buffer size calculations rely on the context sample rate. |
| int min_buffer_size = kMinWebAudioBufferSize; |
| if (min_hardware_buffer_size != 0) { |
| min_buffer_size = std::max( |
| kMinWebAudioBufferSize, |
| static_cast<int>(std::ceil(min_hardware_buffer_size * scale_factor))); |
| } |
| |
| int max_buffer_size = kMaxWebAudioBufferSize; |
| if (max_hardware_buffer_size != 0) { |
| max_buffer_size = std::min( |
| kMaxWebAudioBufferSize, |
| static_cast<int>(std::ceil(max_hardware_buffer_size * scale_factor))); |
| } |
| // Ensure that the `min_buffer_size` does not exceed `max_buffer_size`. |
| // This can occur when a small scale_factor leads to inverted limits after |
| // scaling and clamping. |
| max_buffer_size = std::max(min_buffer_size, max_buffer_size); |
| |
| // Scale default buffer size to context rate. buffer size calculations for |
| // each latency hint now use the context rate (instead of hardware rate). |
| // Scaling ensures the calculated buffer size corresponds to the desired |
| // callback interval at the context rate. |
| int scaled_default_buffer_size = static_cast<int>( |
| std::ceil(hardware_params.frames_per_buffer() * scale_factor)); |
| |
| // Clamp the scaled default buffer size to the valid range. |
| scaled_default_buffer_size = |
| std::clamp(scaled_default_buffer_size, min_buffer_size, max_buffer_size); |
| |
| int output_buffer_size = -1; |
| switch (latency_hint.Category()) { |
| case WebAudioLatencyHint::kCategoryInteractive: |
| output_buffer_size = media::AudioLatency::GetInteractiveBufferSize( |
| scaled_default_buffer_size); |
| break; |
| case WebAudioLatencyHint::kCategoryBalanced: |
| output_buffer_size = media::AudioLatency::GetRtcBufferSize( |
| resolved_context_sample_rate, scaled_default_buffer_size); |
| break; |
| case WebAudioLatencyHint::kCategoryPlayback: |
| output_buffer_size = media::AudioLatency::GetHighLatencyBufferSize( |
| resolved_context_sample_rate, scaled_default_buffer_size); |
| break; |
| case WebAudioLatencyHint::kCategoryExact: |
| output_buffer_size = media::AudioLatency::GetExactBufferSize( |
| base::Seconds(latency_hint.Seconds()), resolved_context_sample_rate, |
| scaled_default_buffer_size, min_buffer_size, max_buffer_size, |
| kMaxWebAudioBufferSize); |
| break; |
| case WebAudioLatencyHint::kLastValue: |
| NOTREACHED(); |
| } |
| |
| CHECK(output_buffer_size != -1) |
| << "RendererWebAudioDeviceImpl::GetOutputBufferSize: Output buffer size " |
| "was not updated from initial value (-1). " |
| << "Latency Hint Category: " << static_cast<int>(latency_hint.Category()); |
| |
| TRACE_EVENT_INSTANT( |
| "webaudio", "RendererWebAudioDeviceImpl::GetOutputBufferSize", |
| "latency_hint", blink::WebAudioLatencyHint::AsString(latency_hint), |
| "resolved_context_sample_rate", resolved_context_sample_rate, |
| "hardware_params", hardware_params.AsHumanReadableString(), |
| "scale_factor", scale_factor, "min_buffer_size", min_buffer_size, |
| "max_buffer_size", max_buffer_size, "scaled_default_buffer_size", |
| scaled_default_buffer_size, "output_buffer_size", output_buffer_size); |
| |
| return output_buffer_size; |
| } |
| |
| RendererWebAudioDeviceImpl::RendererWebAudioDeviceImpl( |
| const WebAudioSinkDescriptor& sink_descriptor, |
| media::ChannelLayoutConfig layout_config, |
| const blink::WebAudioLatencyHint& latency_hint, |
| std::optional<float> context_sample_rate, |
| media::AudioRendererSink::RenderCallback* callback, |
| OutputDeviceParamsCallback device_params_cb, |
| CreateSilentSinkCallback create_silent_sink_cb) |
| : sink_descriptor_(sink_descriptor), |
| latency_hint_(latency_hint), |
| webaudio_callback_(callback), |
| frame_token_(sink_descriptor.Token()), |
| main_thread_task_runner_( |
| base::SingleThreadTaskRunner::GetCurrentDefault()), |
| create_silent_sink_cb_(std::move(create_silent_sink_cb)) { |
| TRACE_EVENT0("webaudio", |
| "RendererWebAudioDeviceImpl::RendererWebAudioDeviceImpl"); |
| DCHECK(webaudio_callback_); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| |
| std::string device_id; |
| switch (sink_descriptor_.Type()) { |
| case blink::WebAudioSinkDescriptor::kAudible: |
| device_id = sink_descriptor_.SinkId().Utf8(); |
| break; |
| case blink::WebAudioSinkDescriptor::kSilent: |
| // Use the default audio device's parameters for a silent sink. |
| device_id = std::string(); |
| break; |
| } |
| |
| original_sink_params_ = |
| std::move(device_params_cb).Run(frame_token_, device_id); |
| |
| // On systems without audio hardware the returned parameters may be invalid. |
| // In which case just choose whatever we want for the fake device. |
| if (!original_sink_params_.IsValid()) { |
| SendLogMessage(base::StringPrintf( |
| "%s => (original_sink_params_ is invalid =[original_sink_params_=%s])", |
| __func__, original_sink_params_.AsHumanReadableString().c_str())); |
| original_sink_params_.Reset(media::AudioParameters::AUDIO_FAKE, |
| media::ChannelLayoutConfig::Stereo(), 48000, |
| 480); |
| |
| // Inform the Blink client (e.g. AudioContext) that we have invalid device |
| // parameters. |
| if (base::FeatureList::IsEnabled(blink::features::kAudioContextOnError)) { |
| // Post a task on the same thread, and the posted task will be executed |
| // once the construction sequence is finished. |
| main_thread_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&RendererWebAudioDeviceImpl::NotifyRenderError, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| SendLogMessage(base::StringPrintf( |
| "%s => (hardware_params=[%s])", __func__, |
| original_sink_params_.AsHumanReadableString().c_str())); |
| |
| // If the 'WebAudioRemoveAudioDestinationResampler' feature is enabled and |
| // a context sample rate is provided, use the provided context sample rate. |
| // Otherwise, fall back to the use default hardware sample rate to create |
| // sink. |
| int resolved_context_sample_rate; |
| if (base::FeatureList::IsEnabled( |
| features::kWebAudioRemoveAudioDestinationResampler) && |
| context_sample_rate.has_value()) { |
| resolved_context_sample_rate = *context_sample_rate; |
| } else { |
| resolved_context_sample_rate = original_sink_params_.sample_rate(); |
| } |
| |
| const int output_buffer_size = GetOutputBufferSize( |
| latency_hint_, resolved_context_sample_rate, original_sink_params_); |
| |
| DCHECK_NE(0, output_buffer_size); |
| |
| current_sink_params_.Reset(original_sink_params_.format(), layout_config, |
| resolved_context_sample_rate, output_buffer_size); |
| |
| // Specify the latency info to be passed to the browser side. |
| current_sink_params_.set_latency_tag(AudioDeviceFactory::GetSourceLatencyType( |
| GetLatencyHintSourceType(latency_hint_.Category()))); |
| SendLogMessage( |
| base::StringPrintf("%s => (sink_params=[%s])", __func__, |
| current_sink_params_.AsHumanReadableString().c_str())); |
| |
| if (base::FeatureList::IsEnabled(media::kLiveCaptionWebAudio)) { |
| auto* web_local_frame = WebLocalFrame::FromFrameToken(frame_token_); |
| if (web_local_frame) { |
| speech_recognition_client_ = |
| web_local_frame->Client()->CreateSpeechRecognitionClient(); |
| if (speech_recognition_client_) { |
| speech_recognition_client_->Reconfigure(current_sink_params_); |
| } |
| } |
| } |
| } |
| |
| RendererWebAudioDeviceImpl::~RendererWebAudioDeviceImpl() { |
| // In case device is not stopped, we can stop it here. |
| Stop(); |
| } |
| |
| void RendererWebAudioDeviceImpl::Start() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| |
| // Already started. |
| if (!is_stopped_) { |
| return; |
| } |
| |
| if (!sink_) { |
| CreateAudioRendererSink(); |
| } |
| |
| sink_->Start(); |
| sink_->Play(); |
| is_stopped_ = false; |
| } |
| |
| void RendererWebAudioDeviceImpl::Pause() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| if (sink_) |
| sink_->Pause(); |
| if (silent_sink_suspender_) |
| silent_sink_suspender_->OnPaused(); |
| } |
| |
| void RendererWebAudioDeviceImpl::Resume() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| if (sink_) |
| sink_->Play(); |
| } |
| |
| void RendererWebAudioDeviceImpl::Stop() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| if (sink_) { |
| sink_->Stop(); |
| sink_ = nullptr; |
| } |
| |
| silent_sink_suspender_.reset(); |
| is_stopped_ = true; |
| } |
| |
| double RendererWebAudioDeviceImpl::SampleRate() { |
| return current_sink_params_.sample_rate(); |
| } |
| |
| int RendererWebAudioDeviceImpl::FramesPerBuffer() { |
| return current_sink_params_.frames_per_buffer(); |
| } |
| |
| int RendererWebAudioDeviceImpl::MaxChannelCount() { |
| return original_sink_params_.channels(); |
| } |
| |
| void RendererWebAudioDeviceImpl::SetDetectSilence( |
| bool enable_silence_detection) { |
| SendLogMessage(base::StringPrintf("%s({enable_silence_detection=%s})", |
| __func__, |
| base::ToString(enable_silence_detection))); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| if (silent_sink_suspender_) |
| silent_sink_suspender_->SetDetectSilence(enable_silence_detection); |
| } |
| |
| int RendererWebAudioDeviceImpl::Render( |
| base::TimeDelta delay, |
| base::TimeTicks delay_timestamp, |
| const media::AudioGlitchInfo& glitch_info, |
| media::AudioBus* dest) { |
| if (!is_rendering_) { |
| SendLogMessage(base::StringPrintf("%s => (rendering is alive [frames=%d])", |
| __func__, dest->frames())); |
| is_rendering_ = true; |
| } |
| |
| int frames_filled = |
| webaudio_callback_->Render(delay, delay_timestamp, glitch_info, dest); |
| if (speech_recognition_client_) { |
| speech_recognition_client_->AddAudio(*dest); |
| } |
| |
| return frames_filled; |
| } |
| |
| void RendererWebAudioDeviceImpl::OnRenderError() { |
| if (!base::FeatureList::IsEnabled(blink::features::kAudioContextOnError)) { |
| return; |
| } |
| |
| // This function gets called from the audio infra, non-main thread, so this |
| // posts a cross-thread task to the main thread task runner. |
| main_thread_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&RendererWebAudioDeviceImpl::NotifyRenderError, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void RendererWebAudioDeviceImpl::NotifyRenderError() { |
| if (!base::FeatureList::IsEnabled(blink::features::kAudioContextOnError)) { |
| return; |
| } |
| |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| SendLogMessage(base::StringPrintf("%s", __func__)); |
| |
| webaudio_callback_->OnRenderError(); |
| } |
| |
| void RendererWebAudioDeviceImpl::SetSilentSinkTaskRunnerForTesting( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner) { |
| silent_sink_task_runner_ = std::move(task_runner); |
| } |
| |
| scoped_refptr<base::SingleThreadTaskRunner> |
| RendererWebAudioDeviceImpl::GetSilentSinkTaskRunner() { |
| if (!silent_sink_task_runner_) { |
| silent_sink_task_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner( |
| {base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| } |
| return silent_sink_task_runner_; |
| } |
| |
| void RendererWebAudioDeviceImpl::SendLogMessage(const std::string& message) { |
| blink::WebRtcLogMessage(base::StringPrintf("[WA]RWADI::%s", message.c_str())); |
| } |
| |
| void RendererWebAudioDeviceImpl::CreateAudioRendererSink() { |
| TRACE_EVENT0("webaudio", |
| "RendererWebAudioDeviceImpl::CreateAudioRendererSink"); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| CHECK(!sink_); |
| |
| switch (sink_descriptor_.Type()) { |
| case blink::WebAudioSinkDescriptor::kAudible: |
| sink_ = AudioDeviceFactory::GetInstance()->NewAudioRendererSink( |
| GetLatencyHintSourceType(latency_hint_.Category()), frame_token_, |
| media::AudioSinkParameters(base::UnguessableToken(), |
| sink_descriptor_.SinkId().Utf8())); |
| |
| // Use a task runner 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. |
| silent_sink_suspender_ = std::make_unique<media::SilentSinkSuspender>( |
| this, base::Seconds(30), current_sink_params_, sink_, |
| GetSilentSinkTaskRunner()); |
| sink_->Initialize(current_sink_params_, silent_sink_suspender_.get()); |
| break; |
| case blink::WebAudioSinkDescriptor::kSilent: |
| sink_ = create_silent_sink_cb_.Run(GetSilentSinkTaskRunner()); |
| sink_->Initialize(current_sink_params_, this); |
| break; |
| } |
| } |
| |
| media::OutputDeviceStatus |
| RendererWebAudioDeviceImpl::MaybeCreateSinkAndGetStatus() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (!sink_) { |
| CreateAudioRendererSink(); |
| } |
| |
| // The device status of a silent sink is always OK. |
| bool is_silent_sink = |
| sink_descriptor_.Type() == blink::WebAudioSinkDescriptor::kSilent; |
| media::OutputDeviceStatus status = |
| is_silent_sink ? media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_OK |
| : sink_->GetOutputDeviceInfo().device_status(); |
| |
| // If sink status is not OK, reset `sink_` and `silent_sink_suspender_` |
| // because this instance will be destroyed. |
| if (status != media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_OK) { |
| Stop(); |
| } |
| return status; |
| } |
| |
| } // namespace content |