| // Copyright 2016 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/audio_device_manager.h" |
| |
| #include <lib/fit/promise.h> |
| #include <lib/fit/single_threaded_executor.h> |
| #include <lib/trace/event.h> |
| |
| #include <string> |
| |
| #include "src/media/audio/audio_core/audio_core_impl.h" |
| #include "src/media/audio/audio_core/base_capturer.h" |
| #include "src/media/audio/audio_core/base_renderer.h" |
| #include "src/media/audio/audio_core/driver_output.h" |
| #include "src/media/audio/audio_core/plug_detector.h" |
| #include "src/media/audio/audio_core/reporter.h" |
| #include "src/media/audio/lib/logging/logging.h" |
| |
| namespace media::audio { |
| |
| AudioDeviceManager::AudioDeviceManager(ThreadingModel& threading_model, |
| std::unique_ptr<PlugDetector> plug_detector, |
| RouteGraph& route_graph, LinkMatrix& link_matrix, |
| ProcessConfig& process_config) |
| : threading_model_(threading_model), |
| route_graph_(route_graph), |
| plug_detector_(std::move(plug_detector)), |
| link_matrix_(link_matrix), |
| process_config_(process_config) {} |
| |
| AudioDeviceManager::~AudioDeviceManager() { |
| Shutdown(); |
| FX_DCHECK(devices_.empty()); |
| } |
| |
| // Configure this admin singleton object to manage audio device instances. |
| zx_status_t AudioDeviceManager::Init() { |
| TRACE_DURATION("audio", "AudioDeviceManager::Init"); |
| |
| // Start monitoring for plug/unplug events of pluggable audio output devices. |
| zx_status_t res = |
| plug_detector_->Start(fit::bind_member(this, &AudioDeviceManager::AddDeviceByVersion)); |
| if (res != ZX_OK) { |
| FX_PLOGS(ERROR, res) << "AudioDeviceManager failed to start plug detector"; |
| return res; |
| } |
| |
| return ZX_OK; |
| } |
| |
| // We are no longer managing audio devices, unwind everything. |
| void AudioDeviceManager::Shutdown() { |
| TRACE_DURATION("audio", "AudioDeviceManager::Shutdown"); |
| plug_detector_->Stop(); |
| |
| std::vector<fit::promise<void>> device_promises; |
| for (auto& [_, device] : devices_pending_init_) { |
| device_promises.push_back(device->Shutdown()); |
| } |
| devices_pending_init_.clear(); |
| |
| for (auto& [_, device] : devices_) { |
| device_promises.push_back(device->Shutdown()); |
| } |
| devices_.clear(); |
| |
| fit::run_single_threaded(fit::join_promise_vector(std::move(device_promises))); |
| } |
| |
| fit::promise<void, fuchsia::media::audio::UpdateEffectError> AudioDeviceManager::UpdateEffect( |
| const std::string& instance_name, const std::string& config, bool persist) { |
| if (persist) { |
| persisted_effects_updates_[instance_name] = config; |
| } |
| std::vector<fit::promise<void, fuchsia::media::audio::UpdateEffectError>> promises; |
| for (auto& [_, device] : devices_) { |
| promises.push_back(device->UpdateEffect(instance_name, config)); |
| } |
| return fit::join_promise_vector(std::move(promises)) |
| .then( |
| [](fit::result<std::vector<fit::result<void, fuchsia::media::audio::UpdateEffectError>>>& |
| results) -> fit::result<void, fuchsia::media::audio::UpdateEffectError> { |
| FX_DCHECK(results.is_ok()) << "fit::join_promise_vector returns an error"; |
| bool found = false; |
| for (const auto& result : results.value()) { |
| if (result.is_error() && |
| result.error() == fuchsia::media::audio::UpdateEffectError::INVALID_CONFIG) { |
| return result; |
| } |
| if (result.is_ok()) { |
| found = true; |
| } |
| } |
| if (found) { |
| return fit::ok(); |
| } else { |
| return fit::error(fuchsia::media::audio::UpdateEffectError::NOT_FOUND); |
| } |
| }); |
| } |
| |
| fit::promise<void, fuchsia::media::audio::UpdateEffectError> AudioDeviceManager::UpdateDeviceEffect( |
| const std::string device_id, const std::string& instance_name, const std::string& message) { |
| auto devices = GetDeviceInfos(); |
| const auto dev = std::find_if(devices.begin(), devices.end(), [&device_id](auto candidate) { |
| return candidate.unique_id == device_id; |
| }); |
| if (dev == devices.end()) { |
| return fit::make_error_promise(fuchsia::media::audio::UpdateEffectError::NOT_FOUND); |
| } |
| auto device = devices_[dev->token_id]; |
| FX_DCHECK(device); |
| |
| return device->UpdateEffect(instance_name, message) |
| .then([](fit::result<void, fuchsia::media::audio::UpdateEffectError>& result) |
| -> fit::result<void, fuchsia::media::audio::UpdateEffectError> { |
| if (result.is_ok()) { |
| return fit::ok(); |
| } |
| if (result.error() == fuchsia::media::audio::UpdateEffectError::INVALID_CONFIG) { |
| return result; |
| } else { |
| return fit::error(fuchsia::media::audio::UpdateEffectError::NOT_FOUND); |
| } |
| }); |
| } |
| |
| fit::promise<void, zx_status_t> AudioDeviceManager::UpdatePipelineConfig( |
| const std::string device_id, const PipelineConfig& pipeline_config, |
| const VolumeCurve& volume_curve) { |
| auto devices = GetDeviceInfos(); |
| const auto dev = std::find_if(devices.begin(), devices.end(), [&device_id](auto candidate) { |
| return candidate.unique_id == device_id; |
| }); |
| if (dev == devices.end()) { |
| return fit::make_error_promise(ZX_ERR_NOT_FOUND); |
| } |
| auto device = devices_[dev->token_id]; |
| FX_DCHECK(device); |
| |
| // UpdatePipelineConfig is only valid on a device that is currently routable; the routable state |
| // protects from devices being plugged or unplugged during update of the PipelineConfig, |
| // as well as ensures only one update to the PipelineConfig will be processed at a time. |
| if (!device->routable()) { |
| return fit::make_error_promise(ZX_ERR_BAD_STATE); |
| } |
| |
| // UpdatePipelineConfig is only valid on a device without links (for the purpose of effects |
| // tuning). As such, the device is removed from route_graph to ensure all links are removed. |
| if (device->plugged()) { |
| route_graph_.RemoveDevice(device.get()); |
| } |
| FX_DCHECK(link_matrix_.DestLinkCount(*device) == 0); |
| FX_DCHECK(link_matrix_.SourceLinkCount(*device) == 0); |
| |
| device->UpdateRoutableState(false); |
| auto profile_params = DeviceConfig::OutputDeviceProfile::Parameters{ |
| .pipeline_config = pipeline_config, .volume_curve = volume_curve}; |
| return device->UpdateDeviceProfile(profile_params).and_then([this, device]() { |
| device->UpdateRoutableState(true); |
| if (device->plugged()) { |
| route_graph_.AddDevice(device.get()); |
| } |
| }); |
| } |
| |
| void AudioDeviceManager::AddDevice(const std::shared_ptr<AudioDevice>& device) { |
| TRACE_DURATION("audio", "AudioDeviceManager::AddDevice"); |
| FX_DCHECK(device != nullptr); |
| |
| threading_model_.FidlDomain().executor()->schedule_task( |
| device->Startup() |
| .and_then([this, device]() mutable { |
| devices_pending_init_.insert({device->token(), std::move(device)}); |
| }) |
| .or_else([device](zx_status_t& error) { |
| FX_PLOGS(ERROR, error) << "AddDevice failed"; |
| Reporter::Singleton().FailedToStartDevice(device->name()); |
| device->Shutdown(); |
| })); |
| } |
| |
| void AudioDeviceManager::ActivateDevice(const std::shared_ptr<AudioDevice>& device) { |
| TRACE_DURATION("audio", "AudioDeviceManager::ActivateDevice"); |
| FX_DCHECK(device != nullptr); |
| |
| // Have we already been removed from the pending list? If so, the device is |
| // already shutting down and there is nothing to be done. |
| if (devices_pending_init_.find(device->token()) == devices_pending_init_.end()) { |
| return; |
| } |
| |
| // If this device is still waiting for initialization, move it over to the set of active devices. |
| // Otherwise (if not waiting for initialization), we've been removed. |
| auto dev = devices_pending_init_.extract(device->token()); |
| if (!dev) { |
| return; |
| } |
| |
| devices_.insert(std::move(dev)); |
| device->SetActivated(); |
| |
| // Apply persisted effects updates. |
| std::vector<fit::promise<void, void>> promises; |
| for (auto it : persisted_effects_updates_) { |
| std::string instance_name = it.first; |
| std::string config = it.second; |
| promises.push_back( |
| device->UpdateEffect(instance_name, config) |
| .then([instance_name, |
| config](fit::result<void, fuchsia::media::audio::UpdateEffectError>& result) { |
| if (result.is_error()) { |
| FX_LOGS_FIRST_N(ERROR, 10) << "Unable to update effect " << instance_name |
| << ", error code " << static_cast<int>(result.error()); |
| } |
| })); |
| } |
| threading_model_.FidlDomain().executor()->schedule_task( |
| fit::join_promise_vector(std::move(promises))); |
| |
| // Notify interested users of the new device. |
| auto info = device->GetDeviceInfo(); |
| |
| // We always report is_default as false in the OnDeviceAdded event. There will be a following |
| // DefaultDeviceChange event that will signal if this device is now the default. |
| info.is_default = false; |
| |
| for (auto& client : bindings_.bindings()) { |
| client->events().OnDeviceAdded(info); |
| } |
| |
| if (device->plugged()) { |
| OnDevicePlugged(device, device->plug_time()); |
| } |
| } |
| |
| void AudioDeviceManager::RemoveDevice(const std::shared_ptr<AudioDevice>& device) { |
| TRACE_DURATION("audio", "AudioDeviceManager::RemoveDevice"); |
| FX_DCHECK(device != nullptr); |
| |
| FX_LOGS(INFO) << "Removing " << (device->is_input() ? "input" : "output") << " '" |
| << device->name() << "'"; |
| |
| // If device was active: reset the default (based on most-recently-plugged). |
| OnPlugStateChanged(device, false, device->plug_time()); |
| device->Shutdown(); |
| |
| auto& device_set = device->activated() ? devices_ : devices_pending_init_; |
| device_set.erase(device->token()); |
| |
| // If device was active: notify clients of the removal. |
| if (device->activated()) { |
| for (auto& client : bindings_.bindings()) { |
| client->events().OnDeviceRemoved(device->token()); |
| } |
| } |
| } |
| |
| void AudioDeviceManager::OnPlugStateChanged(const std::shared_ptr<AudioDevice>& device, |
| bool plugged, zx::time plug_time) { |
| TRACE_DURATION("audio", "AudioDeviceManager::OnPlugStateChanged"); |
| FX_DCHECK(device != nullptr); |
| |
| // Update our bookkeeping for device's plug state. If no change, we're done. |
| if (!device->UpdatePlugState(plugged, plug_time)) { |
| return; |
| } |
| |
| // If the device is not yet activated, we should not be changing routes. |
| bool activated = devices_.find(device->token()) != devices_.end(); |
| if (!activated) { |
| return; |
| } |
| |
| if (plugged) { |
| OnDevicePlugged(device, plug_time); |
| } else { |
| OnDeviceUnplugged(device, plug_time); |
| } |
| } |
| |
| std::vector<fuchsia::media::AudioDeviceInfo> AudioDeviceManager::GetDeviceInfos() { |
| TRACE_DURATION("audio", "AudioDeviceManager::GetDevices"); |
| std::vector<fuchsia::media::AudioDeviceInfo> ret; |
| |
| for (const auto& [_, dev] : devices_) { |
| if (dev->token() != ZX_KOID_INVALID) { |
| auto info = dev->GetDeviceInfo(); |
| info.is_default = |
| (dev->token() == (dev->is_input() ? default_input_token_ : default_output_token_)); |
| ret.push_back(std::move(info)); |
| } |
| } |
| |
| return ret; |
| } |
| |
| void AudioDeviceManager::GetDevices(GetDevicesCallback cbk) { cbk(GetDeviceInfos()); } |
| |
| void AudioDeviceManager::GetDeviceGain(uint64_t device_token, GetDeviceGainCallback cbk) { |
| TRACE_DURATION("audio", "AudioDeviceManager::GetDeviceGain"); |
| |
| auto it = devices_.find(device_token); |
| if (it == devices_.end()) { |
| cbk(ZX_KOID_INVALID, {}); |
| return; |
| } |
| |
| auto [_, dev] = *it; |
| FX_DCHECK(dev->device_settings() != nullptr); |
| auto info = dev->device_settings()->GetGainInfo(); |
| cbk(device_token, info); |
| } |
| |
| void AudioDeviceManager::SetDeviceGain(uint64_t device_token, |
| fuchsia::media::AudioGainInfo gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) { |
| TRACE_DURATION("audio", "AudioDeviceManager::SetDeviceGain"); |
| auto it = devices_.find(device_token); |
| if (it == devices_.end()) { |
| return; |
| } |
| auto [_, dev] = *it; |
| |
| // SetGainInfo clamps out-of-range values (e.g. +infinity) into the device- |
| // allowed gain range. NAN is undefined (signless); handle it here and exit. |
| if (((set_flags & fuchsia::media::AudioGainValidFlags::GAIN_VALID) == |
| fuchsia::media::AudioGainValidFlags::GAIN_VALID) && |
| isnan(gain_info.gain_db)) { |
| FX_LOGS(WARNING) << "Invalid device gain " << gain_info.gain_db << " dB -- making no change"; |
| return; |
| } |
| |
| dev->system_gain_dirty = true; |
| |
| // Change the gain and then report the new settings to our clients. |
| dev->SetGainInfo(gain_info, set_flags); |
| NotifyDeviceGainChanged(*dev); |
| } |
| |
| void AudioDeviceManager::GetDefaultInputDevice(GetDefaultInputDeviceCallback cbk) { |
| cbk(default_input_token_); |
| } |
| |
| void AudioDeviceManager::GetDefaultOutputDevice(GetDefaultOutputDeviceCallback cbk) { |
| cbk(default_output_token_); |
| } |
| |
| std::shared_ptr<AudioDevice> AudioDeviceManager::FindLastPlugged(AudioObject::Type type, |
| bool allow_unplugged) { |
| TRACE_DURATION("audio", "AudioDeviceManager::FindLastPlugged"); |
| FX_DCHECK((type == AudioObject::Type::Output) || (type == AudioObject::Type::Input)); |
| std::shared_ptr<AudioDevice> best = nullptr; |
| |
| // TODO(johngro): Consider tracking last-plugged times in a fbl::WAVLTree, so |
| // this operation becomes O(1). N is pretty low right now, so the benefits do |
| // not currently outweigh the complexity of maintaining this index. |
| for (auto& [_, device] : devices_) { |
| if (device->type() != type) { |
| continue; |
| } |
| |
| if ((best == nullptr) || (!best->plugged() && device->plugged()) || |
| ((best->plugged() == device->plugged()) && (best->plug_time() < device->plug_time()))) { |
| best = device; |
| } |
| } |
| |
| FX_DCHECK((best == nullptr) || (best->type() == type)); |
| if (!allow_unplugged && best && !best->plugged()) { |
| return nullptr; |
| } |
| |
| return best; |
| } |
| |
| void AudioDeviceManager::OnDeviceUnplugged(const std::shared_ptr<AudioDevice>& device, |
| zx::time plug_time) { |
| TRACE_DURATION("audio", "AudioDeviceManager::OnDeviceUnplugged"); |
| FX_DCHECK(device); |
| FX_LOGS(INFO) << "Unplugged " << (device->is_input() ? "input" : "output") << " '" |
| << device->name() << "'"; |
| |
| device->UpdatePlugState(/*plugged=*/false, plug_time); |
| |
| if (device->routable()) { |
| route_graph_.RemoveDevice(device.get()); |
| } |
| UpdateDefaultDevice(device->is_input()); |
| } |
| |
| void AudioDeviceManager::OnDevicePlugged(const std::shared_ptr<AudioDevice>& device, |
| zx::time plug_time) { |
| TRACE_DURATION("audio", "AudioDeviceManager::OnDevicePlugged"); |
| FX_DCHECK(device); |
| FX_LOGS(INFO) << "Plugged " << (device->is_input() ? "input" : "output") << " '" << device->name() |
| << "'"; |
| |
| device->UpdatePlugState(/*plugged=*/true, plug_time); |
| |
| if (device->routable()) { |
| route_graph_.AddDevice(device.get()); |
| } |
| UpdateDefaultDevice(device->is_input()); |
| } |
| |
| void AudioDeviceManager::NotifyDeviceGainChanged(const AudioDevice& device) { |
| TRACE_DURATION("audio", "AudioDeviceManager::NotifyDeviceGainChanged"); |
| FX_DCHECK(device.device_settings() != nullptr); |
| auto info = device.device_settings()->GetGainInfo(); |
| |
| for (auto& client : bindings_.bindings()) { |
| client->events().OnDeviceGainChanged(device.token(), info); |
| } |
| } |
| |
| void AudioDeviceManager::UpdateDefaultDevice(bool input) { |
| TRACE_DURATION("audio", "AudioDeviceManager::UpdateDefaultDevice"); |
| const auto new_dev = |
| FindLastPlugged(input ? AudioObject::Type::Input : AudioObject::Type::Output); |
| uint64_t new_id = new_dev ? new_dev->token() : ZX_KOID_INVALID; |
| uint64_t& old_id = input ? default_input_token_ : default_output_token_; |
| |
| if (old_id != new_id) { |
| FX_LOGS(INFO) << "Default " << (input ? "input" : "output") << " '" |
| << (new_dev ? new_dev->name() : "none") << "'"; |
| |
| for (auto& client : bindings_.bindings()) { |
| client->events().OnDefaultDeviceChanged(old_id, new_id); |
| } |
| old_id = new_id; |
| } |
| } |
| |
| void AudioDeviceManager::AddDeviceByVersion(zx::channel device_channel, std::string device_name, |
| bool is_input, AudioDriverVersion version) { |
| FX_LOGS(DEBUG) << "AddingByVersion (" << (version == AudioDriverVersion::V1 ? "V1" : "V2") << ")" |
| << (is_input ? "input" : "output") << " '" << device_name << "'"; |
| switch (version) { |
| case AudioDriverVersion::V1: |
| AddDeviceByChannel(std::move(device_channel), std::move(device_name), is_input); |
| break; |
| case AudioDriverVersion::V2: |
| fidl::InterfaceHandle<fuchsia::hardware::audio::StreamConfig> stream_config = {}; |
| stream_config.set_channel(std::move(device_channel)); |
| AddDeviceByChannel2(std::move(device_name), is_input, std::move(stream_config)); |
| break; |
| } |
| } |
| |
| void AudioDeviceManager::AddDeviceByChannel(zx::channel device_channel, std::string device_name, |
| bool is_input) { |
| TRACE_DURATION("audio", "AudioDeviceManager::AddDeviceByChannel"); |
| FX_LOGS(INFO) << "Adding " << (is_input ? "input" : "output") << " '" << device_name << "'"; |
| |
| // Hand the stream off to the proper type of class to manage. |
| std::shared_ptr<AudioDevice> new_device; |
| if (is_input) { |
| new_device = AudioInput::Create(device_name, std::move(device_channel), &threading_model(), |
| this, &link_matrix_); |
| } else { |
| new_device = std::make_shared<DriverOutput>(device_name, &threading_model(), this, |
| std::move(device_channel), &link_matrix_, |
| process_config_.default_volume_curve()); |
| } |
| |
| if (new_device == nullptr) { |
| FX_LOGS(ERROR) << "Failed to instantiate audio " << (is_input ? "input" : "output") << " for '" |
| << device_name << "'"; |
| } |
| |
| AddDevice(std::move(new_device)); |
| } |
| |
| void AudioDeviceManager::AddDeviceByChannel2( |
| std::string device_name, bool is_input, |
| fidl::InterfaceHandle<fuchsia::hardware::audio::StreamConfig> stream_config) { |
| TRACE_DURATION("audio", "AudioDeviceManager::AddDeviceByChannel2"); |
| FX_LOGS(INFO) << "Adding " << (is_input ? "input" : "output") << " '" << device_name << "'"; |
| |
| // Hand the stream off to the proper type of class to manage. |
| std::shared_ptr<AudioDevice> new_device; |
| if (is_input) { |
| new_device = AudioInput::Create(device_name, std::move(stream_config), &threading_model(), this, |
| &link_matrix_); |
| } else { |
| new_device = std::make_shared<DriverOutput>(device_name, &threading_model(), this, |
| std::move(stream_config), &link_matrix_, |
| process_config_.default_volume_curve()); |
| } |
| |
| if (new_device == nullptr) { |
| FX_LOGS(ERROR) << "Failed to instantiate audio " << (is_input ? "input" : "output") << " for '" |
| << device_name << "'"; |
| } |
| |
| AddDevice(std::move(new_device)); |
| } |
| |
| } // namespace media::audio |