| // Copyright 2023 The Fuchsia 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 "src/media/audio/audio_core/v2/device.h" |
| |
| #include <fidl/fuchsia.audio.device/cpp/type_conversions.h> |
| #include <fidl/fuchsia.audio/cpp/natural_types.h> |
| #include <fidl/fuchsia.audio/cpp/type_conversions.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| namespace media_audio { |
| |
| namespace { |
| |
| template <typename ResultT> |
| bool LogResultError(const ResultT& result, const char* debug_context) { |
| if (!result.ok()) { |
| if (!result.is_peer_closed() && !result.is_canceled()) { |
| FX_LOGS(WARNING) << debug_context << ": failed with transport error: " << result; |
| } |
| return true; |
| } |
| if (!result->is_ok()) { |
| FX_LOGS(ERROR) << debug_context |
| << ": failed with code: " << fidl::ToUnderlying(result->error_value()); |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| // Initialization sequence: |
| // |
| // +-------------------------------------------------------------------------------------+ |
| // | Device::Create | |
| // | Control.CreateRingBuffer Observer.WatchPlugState | |
| // +--------------------|-----------|------------------------------|---------------------+ |
| // | | | |
| // +--------------------V----+ +---V-----------------------+ | plugged |
| // | Device::StartRingBuffer | | Device::FetchDelayInfo | | |
| // | RingBuffer.Start | | RingBuffer.WatchDelayInfo | | |
| // +-------------------------+ +---------------------------+ | |
| // | | | |
| // | +---V-----------------------+ | |
| // | | Device::CreatePipeline | | |
| // | +---------------------------+ | |
| // | | | |
| // +-------V-----------V--------+ | |
| // | Device::MaybeStartPipeline | | |
| // | Graph.Start | | |
| // +----------------------------+ | |
| // | | |
| // +-------V------------------+ | |
| // | Device::UpdateRouteGraph |<----------------------+ |
| // +-------|------------------+ |
| // V |
| // routed! |
| |
| // static |
| std::shared_ptr<Device> Device::Create(Args args) { |
| const auto format = args.format; |
| auto device = std::make_shared<Device>(std::move(args)); |
| |
| fidl::Arena<> arena; |
| |
| // Set device gain if requested. |
| if (device->IsOutputPipeline()) { |
| device->MaybeSetGain(std::get<OutputDeviceProfile>(device->config_).driver_gain_db()); |
| } else { |
| device->MaybeSetGain(std::get<InputDeviceProfile>(device->config_).driver_gain_db()); |
| } |
| |
| // Create the ring buffer. |
| auto ring_buffer_endpoints = fidl::CreateEndpoints<fuchsia_audio_device::RingBuffer>(); |
| if (!ring_buffer_endpoints.is_ok()) { |
| FX_PLOGS(FATAL, ring_buffer_endpoints.status_value()) << "fidl::CreateEndpoints failed"; |
| } |
| |
| device->control_client_ |
| ->CreateRingBuffer( |
| fuchsia_audio_device::wire::ControlCreateRingBufferRequest::Builder(arena) |
| .options(fuchsia_audio_device::wire::RingBufferOptions::Builder(arena) |
| .format(format.ToWireFidl(arena)) |
| .ring_buffer_min_bytes(static_cast<uint32_t>(args.min_ring_buffer_bytes)) |
| .Build()) |
| .ring_buffer_server(std::move(ring_buffer_endpoints->server)) |
| .Build()) |
| .Then([device](auto& result) { |
| if (!LogResultError(result, "CreateRingBuffer")) { |
| return; |
| } |
| if (!result->value()->has_properties()) { |
| FX_LOGS(ERROR) << "CreateRingBuffer bug: response missing `properties`"; |
| return; |
| } |
| if (!result->value()->has_ring_buffer()) { |
| FX_LOGS(ERROR) << "CreateRingBuffer bug: response missing `ring_buffer`"; |
| return; |
| } |
| device->ring_buffer_ = fidl::ToNatural(result->value()->ring_buffer()); |
| device->StartRingBuffer(); |
| device->FetchDelayInfo(); |
| }); |
| |
| device->ring_buffer_client_ = |
| fidl::WireSharedClient(std::move(ring_buffer_endpoints->client), args.dispatcher); |
| |
| // Start watching for plug/unplug events. |
| device->WatchPlugState(); |
| |
| return device; |
| } |
| |
| Device::Device(Args args) |
| : graph_client_(std::move(args.graph_client)), |
| info_(fidl::ToNatural(args.info)), |
| thread_(args.thread), |
| config_(std::move(args.config)), |
| route_graph_(std::move(args.route_graph)), |
| effects_loader_(std::move(args.effects_loader)), |
| dispatcher_(args.dispatcher), |
| control_client_(fidl::WireSharedClient(std::move(args.control_client), args.dispatcher)), |
| observer_client_(fidl::WireSharedClient(std::move(args.observer_client), args.dispatcher)) {} |
| |
| void Device::Destroy() { |
| destroyed_ = true; |
| |
| // Close all client channels. |
| observer_client_ = fidl::WireSharedClient<fuchsia_audio_device::Observer>(); |
| control_client_ = fidl::WireSharedClient<fuchsia_audio_device::Control>(); |
| ring_buffer_client_ = fidl::WireSharedClient<fuchsia_audio_device::RingBuffer>(); |
| |
| // Unroute. |
| plug_time_ = std::nullopt; |
| UpdateRouteGraph(); |
| |
| if (output_pipeline_) { |
| output_pipeline_->Destroy(); |
| output_pipeline_ = nullptr; |
| } |
| if (input_pipeline_) { |
| input_pipeline_->Destroy(); |
| input_pipeline_ = nullptr; |
| } |
| } |
| |
| bool Device::IsOutputPipeline() const { |
| return std::holds_alternative<OutputDeviceProfile>(config_); |
| } |
| |
| void Device::WatchPlugState() { |
| observer_client_->WatchPlugState().Then([this, self = shared_from_this()](auto& result) { |
| if (destroyed_ || !LogResultError(result, "WatchPlugState")) { |
| return; |
| } |
| if (!result->value()->has_state() || !result->value()->has_plug_time()) { |
| FX_LOGS(ERROR) << "WatchPlugState bug: response missing required field"; |
| return; |
| } |
| |
| switch (result->value()->state()) { |
| case fuchsia_audio_device::PlugState::kPlugged: |
| plug_time_ = zx::time(result->value()->plug_time()); |
| break; |
| case fuchsia_audio_device::PlugState::kUnplugged: |
| plug_time_ = std::nullopt; |
| break; |
| default: |
| FX_LOGS(ERROR) << "WatchPlugState unknown state '" |
| << fidl::ToUnderlying(result->value()->state()) << "'"; |
| break; |
| } |
| |
| UpdateRouteGraph(); |
| WatchPlugState(); |
| }); |
| } |
| |
| void Device::UpdateRouteGraph() { |
| if (plug_time_.has_value() && !routed_) { |
| FX_CHECK(graph_node_started_); |
| FX_CHECK(!destroyed_); |
| // Need to route if a pipeline is available. |
| if (output_pipeline_) { |
| route_graph_->AddOutputDevice(output_pipeline_, *plug_time_); |
| routed_ = true; |
| } else if (input_pipeline_) { |
| route_graph_->AddInputDevice(input_pipeline_, *plug_time_); |
| routed_ = true; |
| } |
| } else if (!plug_time_.has_value() && routed_) { |
| // Need to unroute. |
| if (output_pipeline_) { |
| route_graph_->RemoveOutputDevice(output_pipeline_); |
| routed_ = false; |
| } else if (input_pipeline_) { |
| route_graph_->RemoveInputDevice(input_pipeline_); |
| routed_ = false; |
| } else { |
| FX_LOGS(FATAL) << "invalid 'routed_' value"; |
| } |
| } |
| } |
| |
| void Device::StartRingBuffer() { |
| FX_CHECK(ring_buffer_client_); |
| |
| fidl::Arena<> arena; |
| ring_buffer_client_ |
| ->Start(fuchsia_audio_device::wire::RingBufferStartRequest::Builder(arena).Build()) |
| .Then([this, self = shared_from_this()](auto& result) { |
| if (destroyed_ || !LogResultError(result, "RingBuffer.Start")) { |
| return; |
| } |
| if (!result->value()->has_start_time()) { |
| FX_LOGS(ERROR) << "RingBuffer.Start bug: response missing `start_time`"; |
| return; |
| } |
| ring_buffer_start_time_ = zx::time(result->value()->start_time()); |
| MaybeStartPipeline(); |
| }); |
| } |
| |
| void Device::FetchDelayInfo() { |
| FX_CHECK(ring_buffer_client_); |
| |
| ring_buffer_client_->WatchDelayInfo().Then([this, self = shared_from_this()](auto& result) { |
| if (destroyed_ || !LogResultError(result, "RingBuffer.WatchDelayInfo")) { |
| return; |
| } |
| if (!result->value()->has_delay_info() || !result->value()->delay_info().has_internal_delay()) { |
| FX_LOGS(ERROR) << "RingBuffer.WatchDelayInfo bug: response missing required field"; |
| return; |
| } |
| delay_info_ = fidl::ToNatural(result->value()->delay_info()); |
| CreatePipeline(); |
| }); |
| } |
| |
| void Device::CreatePipeline() { |
| FX_CHECK(delay_info_); |
| FX_CHECK(ring_buffer_); |
| FX_CHECK(!output_pipeline_); |
| FX_CHECK(!input_pipeline_); |
| |
| fidl::Arena<> arena; |
| auto external_delay_watcher = |
| fuchsia_audio_mixer::wire::ExternalDelayWatcher::Builder(arena) |
| .initial_delay(*delay_info_->internal_delay() + *delay_info_->external_delay()) |
| .Build(); |
| |
| if (IsOutputPipeline()) { |
| OutputDevicePipeline::Create({ |
| .graph_client = graph_client_, |
| .dispatcher = dispatcher_, |
| .consumer = |
| { |
| .name = "OutputDevice" + std::to_string(*info_.token_id()), |
| .thread = thread_, |
| .ring_buffer = fidl::ToWire(arena, std::move(*ring_buffer_)), |
| .external_delay_watcher = external_delay_watcher, |
| }, |
| .config = std::get<OutputDeviceProfile>(config_), |
| .effects_loader = effects_loader_, |
| .callback = |
| [this, self = shared_from_this()](auto pipeline) { |
| if (destroyed_) { |
| return; |
| } |
| if (!pipeline) { |
| FX_LOGS(ERROR) << "Failed to create output pipeline for device " |
| << *info_.token_id(); |
| return; |
| } |
| output_pipeline_ = std::move(pipeline); |
| MaybeStartPipeline(); |
| }, |
| }); |
| } else { |
| InputDevicePipeline::CreateForDevice({ |
| .graph_client = graph_client_, |
| .dispatcher = dispatcher_, |
| .producer = |
| { |
| .name = "InputDevice" + std::to_string(*info_.token_id()), |
| .ring_buffer = fidl::ToWire(arena, std::move(*ring_buffer_)), |
| .external_delay_watcher = external_delay_watcher, |
| }, |
| .config = std::get<InputDeviceProfile>(config_), |
| .thread = thread_, |
| .callback = |
| [this, self = shared_from_this()](auto pipeline) { |
| if (destroyed_) { |
| return; |
| } |
| if (!pipeline) { |
| FX_LOGS(ERROR) << "Failed to create input pipeline for device " |
| << *info_.token_id(); |
| return; |
| } |
| input_pipeline_ = std::move(pipeline); |
| MaybeStartPipeline(); |
| }, |
| }); |
| } |
| |
| // This has been consumed by fidl::ToWire (see above). |
| ring_buffer_ = std::nullopt; |
| } |
| |
| void Device::MaybeStartPipeline() { |
| FX_CHECK(!graph_node_started_); |
| FX_CHECK(!destroyed_); |
| |
| if (!ring_buffer_start_time_ || !delay_info_ || (!output_pipeline_ && !input_pipeline_)) { |
| return; |
| } |
| |
| NodeId node; |
| zx::duration stream_time_at_start; |
| if (IsOutputPipeline()) { |
| // Abstractly, we can think of the hardware buffer as an infinitely long sequence of frames, |
| // where the hardware maintains three pointers into this sequence: |
| // |
| // |<--- external delay --->|<--- internal delay --->| |
| // +-+----------------------+-+----------------------+-+ |
| // ... |P| |F| |W| ... |
| // +-+----------------------+-+----------------------+-+ |
| // |
| // At any moment: |
| // W refers to the frame that is about to be consumed by the device. |
| // F refers to the frame that just exited the device's internal pipeline. |
| // P refers to the frame being presented to the speaker. |
| // |
| // In the above diagram, frames move right-to-left over time: |
| // - "Internal delay" is the time needed for a frame to move from position W to position F; |
| // - "External delay" is the time needed for a frame to move from position F to position P. |
| // |
| // The very first frame of this sequence is frame 0, a.k.a. `stream_time = 0`. This frame is |
| // written to offset 0 of the ring buffer. When the ring buffer starts, F points at frame 0. |
| // Hence, at the moment the ring buffer starts, it conceptually plays the frame at `stream_time |
| // = -external_delay`. (In practice, nothing is played immediately when the device starts; |
| // instead the device is silent for the first "external delay", after which frame 0 becomes the |
| // first frame presented at the speaker.) |
| FX_CHECK(output_pipeline_); |
| node = output_pipeline_->consumer_node(); |
| stream_time_at_start = -zx::nsec(*delay_info_->external_delay()); |
| } else { |
| // The capture buffer works in a similar way, with three analogous pointers: |
| // |
| // |<--- internal delay --->|<--- external delay --->| |
| // +-+----------------------+-+----------------------+-+ |
| // ... |R| |F| |C| ... |
| // +-+----------------------+-+----------------------+-+ |
| // |
| // At any moment: |
| // R refers to the frame just written to the ring buffer, newly available to capture clients. |
| // F refers to the frame just emitted by the interconnect, entering any internal pipeline. |
| // C refers to the frame currently being captured by the microphone. |
| // |
| // As with playback, in our diagram any specific frame will move right-to-left over time: |
| // - "External delay" is the time needed for a frame to move from position C to position F; |
| // - "Internal delay" is the time needed for a frame to move from position F to position R. |
| // |
| // Just as with playback, F points at frame 0 at the moment the ring buffer starts. Hence, at |
| // the moment the ring buffer starts, offset 0 of the ring buffer contains the frame that was |
| // (conceptually) captured "external delay" ago. |
| FX_CHECK(input_pipeline_); |
| FX_CHECK(input_pipeline_->producer_node()); |
| node = *input_pipeline_->producer_node(); |
| stream_time_at_start = -zx::nsec(*delay_info_->external_delay()); |
| } |
| |
| fidl::Arena<> arena; |
| (*graph_client_) |
| ->Start(fuchsia_audio_mixer::wire::GraphStartRequest::Builder(arena) |
| .node_id(node) |
| .when(fuchsia_media2::wire::RealTime::WithReferenceTime( |
| arena, ring_buffer_start_time_->get())) |
| .stream_time(fuchsia_media2::wire::StreamTime::WithStreamTime( |
| arena, stream_time_at_start.get())) |
| .Build()) |
| .Then([this, self = shared_from_this()](auto& result) { |
| if (destroyed_ || !LogResultError(result, "Graph.Start")) { |
| return; |
| } |
| graph_node_started_ = true; |
| UpdateRouteGraph(); |
| }); |
| } |
| |
| void Device::MaybeSetGain(std::optional<float> gain_db) { |
| if (!gain_db) { |
| return; |
| } |
| |
| fidl::Arena<> arena; |
| control_client_ |
| ->SetGain( |
| fuchsia_audio_device::wire::ControlSetGainRequest::Builder(arena) |
| .target_state( |
| fuchsia_audio_device::wire::GainState::Builder(arena).gain_db(*gain_db).Build()) |
| .Build()) |
| .Then([this, self = shared_from_this()](auto& result) { |
| if (!destroyed_) { |
| LogResultError(result, "SetGain"); |
| } |
| }); |
| } |
| |
| } // namespace media_audio |