| // Copyright 2019 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/route_graph.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <algorithm> |
| #include <sstream> |
| |
| #include "src/media/audio/audio_core/audio_driver.h" |
| #include "src/media/audio/audio_core/logging_flags.h" |
| |
| namespace media::audio { |
| namespace { |
| |
| // TODO(fxbug.dev/55132): Remove this workaround. Just 64000 would still support the range needed. |
| static constexpr int32_t kMinUltrasoundRate = 96000; |
| |
| bool DeviceConfigurationSupportsUsage(AudioDevice* device, StreamUsage usage) { |
| if (usage != StreamUsage::WithRenderUsage(RenderUsage::ULTRASOUND) && |
| usage != StreamUsage::WithCaptureUsage(CaptureUsage::ULTRASOUND)) { |
| return true; |
| } |
| |
| FX_DCHECK(device->format()); |
| auto device_rate = device->format()->frames_per_second(); |
| return device_rate >= kMinUltrasoundRate; |
| } |
| |
| } // namespace |
| |
| RouteGraph::RouteGraph(LinkMatrix* link_matrix) : link_matrix_(*link_matrix) { |
| FX_DCHECK(link_matrix); |
| } |
| |
| RouteGraph::~RouteGraph() { |
| if (throttle_release_fence_) { |
| throttle_release_fence_->complete_ok(); |
| } |
| } |
| |
| void RouteGraph::SetThrottleOutput(ThreadingModel* threading_model, |
| std::shared_ptr<AudioOutput> throttle_output) { |
| fpromise::bridge<void, void> bridge; |
| threading_model->FidlDomain().ScheduleTask(bridge.consumer.promise().then( |
| [throttle_output](fpromise::result<void, void>& _) { return throttle_output->Shutdown(); })); |
| |
| threading_model->FidlDomain().executor()->schedule_task( |
| throttle_output->Startup().or_else([throttle_output](zx_status_t& error) { |
| FX_PLOGS(ERROR, error) << "Failed to initialize the throttle output"; |
| return throttle_output->Shutdown(); |
| })); |
| |
| throttle_release_fence_ = {std::move(bridge.completer)}; |
| throttle_output_ = throttle_output; |
| AddDeviceToRoutes(throttle_output_.get()); |
| } |
| |
| void RouteGraph::AddDeviceToRoutes(AudioDevice* device) { |
| TRACE_DURATION("audio", "RouteGraph::AddDeviceToRoutes"); |
| |
| // Add device, sorted with most-recently-plugged devices first. Use a stable sort so that ties are |
| // broken by most-recently-added device, which helps make unit tests deterministic. |
| devices_.push_front(device); |
| std::stable_sort(devices_.begin(), devices_.end(), [](AudioDevice* a, AudioDevice* b) { |
| if (a->plugged() != b->plugged()) { |
| return a->plugged(); |
| } |
| return a->plug_time() > b->plug_time(); |
| }); |
| |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Added device " << device << " (" << (device->is_input() ? "input" : "output") |
| << ") to route graph"; |
| DisplayDevices(); |
| } |
| UpdateGraphForDeviceChange(); |
| } |
| |
| void RouteGraph::RemoveDeviceFromRoutes(AudioDevice* device) { |
| TRACE_DURATION("audio", "RouteGraph::RemoveDeviceFromRoutes"); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Removing device " << device << " (" |
| << (device->is_input() ? "input" : "output") << ") from route graph"; |
| } |
| |
| auto it = std::find(devices_.begin(), devices_.end(), device); |
| if (it == devices_.end()) { |
| FX_LOGS(WARNING) << "Attempted to remove unregistered device from the route graph."; |
| return; |
| } |
| |
| // Unlink the device (don't just tell its sources/dests to Unlink) so LinkMatrix fully removes it. |
| link_matrix_.Unlink(*device); |
| |
| devices_.erase(it); |
| if constexpr (kLogRoutingChanges) { |
| DisplayDevices(); |
| } |
| UpdateGraphForDeviceChange(); |
| } |
| |
| bool RouteGraph::ContainsDevice(const AudioDevice* device) { |
| return (std::find(devices_.begin(), devices_.end(), device) != devices_.end()); |
| } |
| |
| void RouteGraph::AddRenderer(std::shared_ptr<AudioObject> renderer) { |
| TRACE_DURATION("audio", "RouteGraph::AddRenderer"); |
| FX_DCHECK(throttle_output_); |
| FX_DCHECK(renderer->is_audio_renderer()); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Adding renderer " << renderer.get() << " (" << renderer->usage()->ToString() |
| << ") to route graph"; |
| } |
| renderers_.insert({renderer.get(), RoutableOwnedObject{std::move(renderer), {}}}); |
| |
| if constexpr (kLogRoutingChanges) { |
| DisplayRenderers(); |
| } |
| } |
| |
| void RouteGraph::SetRendererRoutingProfile(const AudioObject& renderer, RoutingProfile profile) { |
| TRACE_DURATION("audio", "RouteGraph::SetRendererRoutingProfile"); |
| FX_DCHECK(renderer.is_audio_renderer()); |
| FX_LOGS(DEBUG) << "Setting renderer route profile: " << &renderer; |
| |
| auto it = renderers_.find(&renderer); |
| if (it == renderers_.end()) { |
| FX_LOGS(WARNING) << "Tried to set routing policy for an unregistered renderer."; |
| return; |
| } |
| |
| it->second.profile = std::move(profile); |
| if (!it->second.profile.routable || !it->second.profile.usage.is_render_usage()) { |
| link_matrix_.Unlink(*it->second.ref); |
| return; |
| } |
| |
| auto output = TargetForUsage(it->second.profile.usage); |
| if (!output.is_linkable()) { |
| FX_LOGS(WARNING) << "Tried to route AudioRenderer, but no device available for usage " |
| << it->second.profile.usage.ToString(); |
| link_matrix_.Unlink(*it->second.ref); |
| return; |
| } |
| |
| if (link_matrix_.AreLinked(*it->second.ref, *output.device)) { |
| return; |
| } |
| |
| link_matrix_.Unlink(*it->second.ref); |
| |
| link_matrix_.LinkObjects(it->second.ref, output.device->shared_from_this(), output.transform); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Setting renderer route profile: " << &renderer; |
| DisplayRenderers(); |
| link_matrix_.DisplayCurrentRouting(); |
| } |
| } |
| |
| void RouteGraph::RemoveRenderer(const AudioObject& renderer) { |
| TRACE_DURATION("audio", "RouteGraph::RemoveRenderer"); |
| FX_DCHECK(renderer.is_audio_renderer()); |
| |
| auto it = renderers_.find(&renderer); |
| if (it == renderers_.end()) { |
| FX_LOGS(INFO) << "Renderer " << &renderer << " was not present in graph."; |
| return; |
| } |
| |
| link_matrix_.Unlink(*it->second.ref); |
| |
| renderers_.erase(it); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Removed renderer from route graph: " << &renderer << " (" |
| << renderer.usage()->ToString() << ")"; |
| DisplayRenderers(); |
| link_matrix_.DisplayCurrentRouting(); |
| } |
| } |
| |
| void RouteGraph::AddCapturer(std::shared_ptr<AudioObject> capturer) { |
| TRACE_DURATION("audio", "RouteGraph::AddCapturer"); |
| FX_DCHECK(capturer->is_audio_capturer()); |
| |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Adding capturer " << capturer.get() << " (" << capturer->usage()->ToString() |
| << ") to route graph"; |
| } |
| |
| capturers_.insert({capturer.get(), RoutableOwnedObject{std::move(capturer), {}}}); |
| |
| if constexpr (kLogRoutingChanges) { |
| DisplayCapturers(); |
| } |
| } |
| |
| void RouteGraph::SetCapturerRoutingProfile(const AudioObject& capturer, RoutingProfile profile) { |
| TRACE_DURATION("audio", "RouteGraph::SetCapturerRoutingProfile"); |
| FX_DCHECK(capturer.is_audio_capturer()); |
| FX_LOGS(DEBUG) << "Setting capturer route profile: " << &capturer; |
| |
| auto it = capturers_.find(&capturer); |
| if (it == capturers_.end()) { |
| FX_LOGS(WARNING) << "Tried to set routing policy for an unregistered capturer."; |
| return; |
| } |
| |
| it->second.profile = std::move(profile); |
| if (!it->second.profile.routable || !it->second.profile.usage.is_capture_usage()) { |
| link_matrix_.Unlink(*it->second.ref); |
| return; |
| } |
| |
| auto target = TargetForUsage(it->second.profile.usage); |
| if (!target.is_linkable()) { |
| FX_LOGS(WARNING) << "Tried to route AudioCapturer, but no device available for usage " |
| << it->second.profile.usage.ToString(); |
| link_matrix_.Unlink(*it->second.ref); |
| return; |
| } |
| |
| if (link_matrix_.AreLinked(*target.device, *it->second.ref)) { |
| return; |
| } |
| |
| link_matrix_.Unlink(*it->second.ref); |
| |
| link_matrix_.LinkObjects(target.device->shared_from_this(), it->second.ref, target.transform); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Setting capturer route profile: " << &capturer; |
| DisplayCapturers(); |
| link_matrix_.DisplayCurrentRouting(); |
| } |
| } |
| |
| void RouteGraph::RemoveCapturer(const AudioObject& capturer) { |
| TRACE_DURATION("audio", "RouteGraph::RemoveCapturer"); |
| FX_DCHECK(capturer.is_audio_capturer()); |
| |
| auto it = capturers_.find(&capturer); |
| if (it == capturers_.end()) { |
| FX_LOGS(WARNING) << "Capturer " << &capturer << " was not present in graph."; |
| return; |
| } |
| |
| link_matrix_.Unlink(*it->second.ref); |
| |
| capturers_.erase(it); |
| if constexpr (kLogRoutingChanges) { |
| FX_LOGS(INFO) << "Removed capturer " << &capturer << " (" << capturer.usage()->ToString() |
| << ") from route graph"; |
| DisplayCapturers(); |
| link_matrix_.DisplayCurrentRouting(); |
| } |
| } |
| |
| void RouteGraph::UpdateGraphForDeviceChange() { |
| TRACE_DURATION("audio", "RouteGraph::UpdateGraphForDeviceChange"); |
| auto [targets, unlink_command] = CalculateTargets(); |
| targets_ = targets; |
| Unlink(unlink_command); |
| |
| { |
| TRACE_DURATION("audio", "RouteGraph::UpdateGraphForDeviceChange.renderers"); |
| std::for_each(renderers_.begin(), renderers_.end(), [this](auto& renderer) { |
| Target target; |
| if (!renderer.second.profile.routable || |
| !((target = TargetForUsage(renderer.second.profile.usage)).is_linkable()) || |
| link_matrix_.DestLinkCount(*renderer.second.ref) > 0u) { |
| return; |
| } |
| |
| link_matrix_.LinkObjects(renderer.second.ref, target.device->shared_from_this(), |
| target.transform); |
| }); |
| } |
| |
| { |
| TRACE_DURATION("audio", "RouteGraph::UpdateGraphForDeviceChange.capturers"); |
| std::for_each(capturers_.begin(), capturers_.end(), [this](auto& capturer) { |
| Target target; |
| if (!capturer.second.profile.routable || |
| !((target = TargetForUsage(capturer.second.profile.usage)).is_linkable()) || |
| link_matrix_.SourceLinkCount(*capturer.second.ref) > 0u) { |
| return; |
| } |
| |
| link_matrix_.LinkObjects(target.device->shared_from_this(), capturer.second.ref, |
| target.transform); |
| }); |
| } |
| if constexpr (kLogRoutingChanges) { |
| DisplayRenderers(); |
| DisplayCapturers(); |
| DisplayDevices(); |
| link_matrix_.DisplayCurrentRouting(); |
| } |
| } |
| |
| std::pair<RouteGraph::Targets, RouteGraph::UnlinkCommand> RouteGraph::CalculateTargets() const { |
| TRACE_DURATION("audio", "RouteGraph::CalculateTargets"); |
| // We generate a new set of targets. |
| // We generate an unlink command to unlink anything linked to a target which has changed. |
| |
| Targets new_targets = {}; |
| UnlinkCommand unlink; |
| for (const auto& usage : kStreamUsages) { |
| const auto idx = HashStreamUsage(usage); |
| new_targets[idx] = [this, usage]() { |
| for (auto device : devices_) { |
| if (device == throttle_output_.get()) { |
| continue; |
| } |
| |
| if (device->profile().supports_usage(usage) && |
| DeviceConfigurationSupportsUsage(device, usage)) { |
| return Target(device, device->profile().loudness_transform()); |
| } |
| } |
| |
| if (usage.is_render_usage()) { |
| return Target(throttle_output_.get(), throttle_output_->profile().loudness_transform()); |
| } else { |
| return Target(); |
| } |
| }(); |
| |
| unlink[idx] = targets_[idx].device != new_targets[idx].device; |
| } |
| |
| return {new_targets, unlink}; |
| } |
| |
| void RouteGraph::Unlink(UnlinkCommand unlink_command) { |
| TRACE_DURATION("audio", "RouteGraph::Unlink"); |
| std::for_each(renderers_.begin(), renderers_.end(), [this, &unlink_command](auto& renderer) { |
| auto usage = renderer.second.profile.usage; |
| if (!usage.is_empty() && unlink_command[HashStreamUsage(usage)]) { |
| link_matrix_.Unlink(*renderer.second.ref); |
| } |
| }); |
| std::for_each(capturers_.begin(), capturers_.end(), [this, &unlink_command](auto& capturer) { |
| auto usage = capturer.second.profile.usage; |
| if (!usage.is_empty() && unlink_command[HashStreamUsage(usage)]) { |
| link_matrix_.Unlink(*capturer.second.ref); |
| } |
| }); |
| } |
| |
| RouteGraph::Target RouteGraph::TargetForUsage(const StreamUsage& usage) const { |
| if (usage.is_empty()) { |
| return Target(); |
| } |
| return targets_[HashStreamUsage(usage)]; |
| } |
| |
| // The API is formed to return more than one output as the target for a RenderUsage, but the current |
| // audio_core implementation only routes to one output per usage. |
| std::unordered_set<AudioDevice*> RouteGraph::TargetsForRenderUsage(const RenderUsage& usage) { |
| auto target = targets_[HashStreamUsage(StreamUsage::WithRenderUsage(usage))]; |
| if (!target.is_linkable()) { |
| FX_LOGS(ERROR) << __FUNCTION__ << " (" << RenderUsageToString(usage) |
| << ") target is not linkable"; |
| return {}; |
| } |
| |
| if constexpr (kLogIdlePolicyCounts) { |
| FX_LOGS(INFO) << __FUNCTION__ << " (" << RenderUsageToString(usage) << ") returning " |
| << target.device; |
| } |
| return {target.device}; |
| } |
| |
| std::shared_ptr<LoudnessTransform> RouteGraph::LoudnessTransformForUsage(const StreamUsage& usage) { |
| return TargetForUsage(usage).transform; |
| } |
| |
| void RouteGraph::DisplayRenderers() { |
| std::stringstream stream; |
| stream << "Renderers:"; |
| if (renderers_.empty()) { |
| stream << " <empty>"; |
| } else { |
| for (const auto& renderer : renderers_) { |
| stream << " " << renderer.first; |
| } |
| } |
| FX_LOGS(INFO) << stream.str(); |
| } |
| |
| void RouteGraph::DisplayCapturers() { |
| std::stringstream stream; |
| stream << "Capturers:"; |
| if (capturers_.empty()) { |
| stream << " <empty>"; |
| } else { |
| for (const auto& capturer : capturers_) { |
| stream << " " << capturer.first; |
| } |
| } |
| FX_LOGS(INFO) << stream.str(); |
| |
| std::stringstream().swap(stream); |
| stream << "Loopbacks:"; |
| if (loopback_capturers_.empty()) { |
| stream << " <empty>"; |
| } else { |
| for (const auto& capturer : loopback_capturers_) { |
| stream << " " << capturer.first; |
| } |
| } |
| FX_LOGS(INFO) << stream.str(); |
| } |
| |
| void RouteGraph::DisplayDevices() { |
| std::stringstream stream; |
| stream << "Devices:"; |
| for (const auto& device : devices_) { |
| stream << " " << device; |
| } |
| FX_LOGS(INFO) << stream.str(); |
| } |
| |
| } // namespace media::audio |