| // 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/lib/test/hermetic_audio_test.h" |
| |
| #include <lib/inspect/cpp/hierarchy.h> |
| |
| #include <sstream> |
| #include <vector> |
| |
| #include "src/lib/fxl/strings/join_strings.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/media/audio/audio_core/audio_device.h" |
| #include "src/media/audio/lib/format/format.h" |
| #include "src/media/audio/lib/logging/logging.h" |
| #include "src/media/audio/lib/test/capturer_shim.h" |
| #include "src/media/audio/lib/test/hermetic_audio_environment.h" |
| #include "src/media/audio/lib/test/inspect.h" |
| #include "src/media/audio/lib/test/renderer_shim.h" |
| #include "src/media/audio/lib/test/test_fixture.h" |
| #include "src/media/audio/lib/test/virtual_device.h" |
| |
| namespace media::audio::test { |
| |
| std::optional<HermeticAudioEnvironment::Options> HermeticAudioTest::test_suite_options_; |
| |
| void HermeticAudioTest::SetTestSuiteEnvironmentOptions(HermeticAudioEnvironment::Options options) { |
| test_suite_options_ = options; |
| } |
| |
| void HermeticAudioTest::SetUpEnvironment() { |
| auto options = test_suite_options_.value_or(HermeticAudioEnvironment::Options()); |
| environment_ = std::make_unique<HermeticAudioEnvironment>(options); |
| |
| environment_->ConnectToService(virtual_audio_control_sync_.NewRequest()); |
| virtual_audio_control_sync_->Enable(); |
| |
| environment_->ConnectToService(thermal_controller_.NewRequest()); |
| environment_->ConnectToService(thermal_test_control_sync_.NewRequest()); |
| } |
| |
| void HermeticAudioTest::TearDownEnvironment() { |
| if (virtual_audio_control_sync_.is_bound()) { |
| virtual_audio_control_sync_->Disable(); |
| } |
| environment_ = nullptr; |
| } |
| |
| void HermeticAudioTest::SetUp() { |
| SetUpEnvironment(); |
| TestFixture::SetUp(); |
| |
| environment_->ConnectToService(audio_core_.NewRequest()); |
| AddErrorHandler(audio_core_, "AudioCore"); |
| |
| environment_->ConnectToService(ultrasound_factory_.NewRequest()); |
| AddErrorHandler(ultrasound_factory_, "UltrasoundFactory"); |
| |
| environment_->ConnectToService(audio_dev_enum_.NewRequest()); |
| AddErrorHandler(audio_dev_enum_, "AudioDeviceEnumerator"); |
| WatchForDeviceArrivals(); |
| |
| // A race can occur in which a device is added before the OnDeviceAdded callback is registered, |
| // which causes the OnDefaultDeviceChanged callback to fail to recognize the default device. |
| // |
| // Here, any devices missed by OnDeviceAdded are accounted for; OnDefaultDeviceChanged processes |
| // the most recent pending_default_device_token_ once initial_devices_received_. |
| audio_dev_enum_->GetDevices([this](std::vector<fuchsia::media::AudioDeviceInfo> devices) { |
| for (const auto& info : devices) { |
| if (token_to_unique_id_.count(info.token_id) == 0) { |
| OnDeviceAdded(info); |
| } |
| } |
| initial_devices_received_ = true; |
| while (!pending_default_device_tokens_.empty()) { |
| OnDefaultDeviceChanged(0, pending_default_device_tokens_.front()); |
| pending_default_device_tokens_.pop(); |
| } |
| }); |
| } |
| |
| void HermeticAudioTest::TearDown() { |
| // Remove all components. |
| for (auto& [_, device] : devices_) { |
| device.output = nullptr; |
| device.input = nullptr; |
| } |
| capturers_.clear(); |
| renderers_.clear(); |
| |
| if (audio_dev_enum_.is_bound()) { |
| WaitForDeviceDepartures(); |
| } |
| |
| TestFixture::TearDown(); |
| TearDownEnvironment(); |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| VirtualOutput<SampleFormat>* HermeticAudioTest::CreateOutput( |
| const audio_stream_unique_id_t& device_id, TypedFormat<SampleFormat> format, size_t frame_count, |
| std::optional<DevicePlugProperties> plug_properties, float device_gain_db, |
| std::optional<DeviceClockProperties> device_clock_properties) { |
| FX_CHECK(SampleFormat != fuchsia::media::AudioSampleFormat::UNSIGNED_8) |
| << "hardware is not expected to support UNSIGNED_8"; |
| FX_CHECK(audio_dev_enum_.is_bound()); |
| |
| auto ptr = std::make_unique<VirtualOutput<SampleFormat>>( |
| static_cast<TestFixture*>(this), environment_.get(), device_id, format, frame_count, |
| virtual_output_next_inspect_id_++, plug_properties, device_gain_db, device_clock_properties); |
| auto out = ptr.get(); |
| auto id = AudioDevice::UniqueIdToString(device_id); |
| devices_[id].output = std::move(ptr); |
| |
| // Wait until the device is connected. |
| RunLoopUntil([this, id, out]() { return out->Ready() && devices_[id].info != std::nullopt; }); |
| |
| // Wait for device to become the default. |
| RunLoopUntil([this, id]() { return devices_[id].is_default; }); |
| ExpectNoUnexpectedErrors("during CreateOutput"); |
| return out; |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| VirtualInput<SampleFormat>* HermeticAudioTest::CreateInput( |
| const audio_stream_unique_id_t& device_id, TypedFormat<SampleFormat> format, size_t frame_count, |
| std::optional<DevicePlugProperties> plug_properties, float device_gain_db, |
| std::optional<DeviceClockProperties> device_clock_properties) { |
| FX_CHECK(SampleFormat != fuchsia::media::AudioSampleFormat::UNSIGNED_8) |
| << "hardware is not expected to support UNSIGNED_8"; |
| FX_CHECK(audio_dev_enum_.is_bound()); |
| |
| auto ptr = std::make_unique<VirtualInput<SampleFormat>>( |
| static_cast<TestFixture*>(this), environment_.get(), device_id, format, frame_count, |
| virtual_input_next_inspect_id_++, plug_properties, device_gain_db, device_clock_properties); |
| auto out = ptr.get(); |
| auto id = AudioDevice::UniqueIdToString(device_id); |
| devices_[id].input = std::move(ptr); |
| |
| // Wait until the device is connected. |
| RunLoopUntil([this, out, id]() { return out->Ready() && devices_[id].info != std::nullopt; }); |
| return out; |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| AudioRendererShim<SampleFormat>* HermeticAudioTest::CreateAudioRenderer( |
| TypedFormat<SampleFormat> format, size_t frame_count, fuchsia::media::AudioRenderUsage usage, |
| std::optional<zx::clock> reference_clock) { |
| auto ptr = std::make_unique<AudioRendererShim<SampleFormat>>( |
| static_cast<TestFixture*>(this), audio_core_, format, frame_count, usage, |
| renderer_shim_next_inspect_id_++, std::move(reference_clock)); |
| auto out = ptr.get(); |
| renderers_.push_back(std::move(ptr)); |
| |
| // Wait until the renderer is connected. |
| RunLoopUntil([this, out]() { return ErrorOccurred() || out->created(); }); |
| return out; |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| AudioCapturerShim<SampleFormat>* HermeticAudioTest::CreateAudioCapturer( |
| TypedFormat<SampleFormat> format, size_t frame_count, |
| fuchsia::media::AudioCapturerConfiguration config) { |
| auto ptr = std::make_unique<AudioCapturerShim<SampleFormat>>( |
| static_cast<TestFixture*>(this), audio_core_, format, frame_count, std::move(config), |
| capturer_shim_next_inspect_id_++); |
| auto out = ptr.get(); |
| capturers_.push_back(std::move(ptr)); |
| return out; |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| UltrasoundRendererShim<SampleFormat>* HermeticAudioTest::CreateUltrasoundRenderer( |
| TypedFormat<SampleFormat> format, size_t frame_count, bool wait_for_creation) { |
| auto ptr = std::make_unique<UltrasoundRendererShim<SampleFormat>>( |
| static_cast<TestFixture*>(this), ultrasound_factory_, format, frame_count, |
| renderer_shim_next_inspect_id_++); |
| auto out = ptr.get(); |
| renderers_.push_back(std::move(ptr)); |
| |
| if (wait_for_creation) { |
| out->WaitForDevice(); |
| } |
| return out; |
| } |
| |
| template <fuchsia::media::AudioSampleFormat SampleFormat> |
| UltrasoundCapturerShim<SampleFormat>* HermeticAudioTest::CreateUltrasoundCapturer( |
| TypedFormat<SampleFormat> format, size_t frame_count, bool wait_for_creation) { |
| auto ptr = std::make_unique<UltrasoundCapturerShim<SampleFormat>>( |
| static_cast<TestFixture*>(this), ultrasound_factory_, format, frame_count, |
| capturer_shim_next_inspect_id_++); |
| auto out = ptr.get(); |
| capturers_.push_back(std::move(ptr)); |
| if (wait_for_creation) { |
| out->WaitForDevice(); |
| } |
| return out; |
| } |
| |
| void HermeticAudioTest::Unbind(VirtualInputImpl* device) { |
| auto it = std::find_if(devices_.begin(), devices_.end(), |
| [device](const auto& it) { return it.second.input.get() == device; }); |
| FX_CHECK(it != devices_.end()); |
| |
| device->fidl().Unbind(); |
| devices_.erase(it); |
| } |
| |
| void HermeticAudioTest::Unbind(VirtualOutputImpl* device) { |
| auto it = std::find_if(devices_.begin(), devices_.end(), |
| [device](const auto& it) { return it.second.output.get() == device; }); |
| FX_CHECK(it != devices_.end()); |
| |
| device->fidl().Unbind(); |
| devices_.erase(it); |
| } |
| |
| void HermeticAudioTest::Unbind(RendererShimImpl* renderer) { |
| auto it = std::find_if( |
| renderers_.begin(), renderers_.end(), |
| [renderer](const std::unique_ptr<RendererShimImpl>& p) { return p.get() == renderer; }); |
| FX_CHECK(it != renderers_.end()); |
| |
| renderer->fidl().Unbind(); |
| renderers_.erase(it); |
| } |
| |
| void HermeticAudioTest::Unbind(CapturerShimImpl* capturer) { |
| auto it = std::find_if( |
| capturers_.begin(), capturers_.end(), |
| [capturer](const std::unique_ptr<CapturerShimImpl>& p) { return p.get() == capturer; }); |
| FX_CHECK(it != capturers_.end()); |
| |
| capturer->fidl().Unbind(); |
| capturers_.erase(it); |
| } |
| |
| void HermeticAudioTest::WatchForDeviceArrivals() { |
| audio_dev_enum_.events().OnDeviceAdded = [this](fuchsia::media::AudioDeviceInfo info) { |
| if (token_to_unique_id_.count(info.token_id) > 0) { |
| FAIL() << "Device with token " << info.token_id << " already exists"; |
| } |
| OnDeviceAdded(info); |
| }; |
| |
| audio_dev_enum_.events().OnDeviceRemoved = [this](uint64_t token) { |
| if (token_to_unique_id_.count(token) == 0) { |
| FAIL() << "Unknown device with token " << token; |
| } |
| auto id = token_to_unique_id_[token]; |
| ADD_FAILURE() << "Unexpected removal of device " << id; |
| }; |
| |
| audio_dev_enum_.events().OnDeviceGainChanged = [this](uint64_t token, |
| fuchsia::media::AudioGainInfo gain_info) { |
| if (token_to_unique_id_.count(token) == 0) { |
| FAIL() << "Unknown device with token " << token; |
| } |
| auto id = token_to_unique_id_[token]; |
| if (devices_[id].info == std::nullopt) { |
| FAIL() << "Device has not been added " << id; |
| } |
| devices_[id].info->gain_info = gain_info; |
| AUDIO_LOG(DEBUG) << "Our output device (" << id << ") changed gain: " << gain_info.gain_db |
| << " dB, " |
| << (((gain_info.flags & fuchsia::media::AudioGainInfoFlags::MUTE) == |
| fuchsia::media::AudioGainInfoFlags::MUTE) |
| ? "MUTE" |
| : "UNMUTE"); |
| }; |
| |
| audio_dev_enum_.events().OnDefaultDeviceChanged = [this](uint64_t old_default_token, |
| uint64_t new_default_token) { |
| OnDefaultDeviceChanged(old_default_token, new_default_token); |
| AUDIO_LOG(DEBUG) << "Default device changed (old_token = " << old_default_token |
| << ", new_token = " << new_default_token << ")"; |
| }; |
| } |
| |
| void HermeticAudioTest::WaitForDeviceDepartures() { |
| audio_dev_enum_.events().OnDeviceAdded = [](fuchsia::media::AudioDeviceInfo device) { |
| ADD_FAILURE() << "Unexpected device " << device.unique_id << " added during shutdown"; |
| }; |
| |
| audio_dev_enum_.events().OnDeviceRemoved = [this](uint64_t token) { |
| if (token_to_unique_id_.count(token) == 0) { |
| FAIL() << "Unknown device with token " << token; |
| } |
| auto id = token_to_unique_id_[token]; |
| EXPECT_FALSE(devices_[id].is_removed) << "Duplicate removal of device " << id << " in shutdown"; |
| EXPECT_FALSE(devices_[id].is_default) << "Device was removed while it was still the default!"; |
| devices_[id].is_removed = true; |
| }; |
| |
| audio_dev_enum_.events().OnDeviceGainChanged = [](uint64_t device_token, |
| fuchsia::media::AudioGainInfo) { |
| ADD_FAILURE() << "Unexpected device gain changed (" << device_token << ") during shutdown"; |
| }; |
| |
| audio_dev_enum_.events().OnDefaultDeviceChanged = [this](uint64_t old_default_token, |
| uint64_t new_default_token) { |
| OnDefaultDeviceChanged(old_default_token, new_default_token); |
| }; |
| |
| RunLoopUntil([this]() { |
| for (auto& it : devices_) { |
| if (!it.second.is_removed) { |
| return false; |
| } |
| } |
| return true; |
| }); |
| |
| // Mute events, to avoid flakes from "unbind triggers an event elsewhere". |
| audio_dev_enum_.events().OnDeviceAdded = nullptr; |
| audio_dev_enum_.events().OnDeviceRemoved = nullptr; |
| audio_dev_enum_.events().OnDeviceGainChanged = nullptr; |
| audio_dev_enum_.events().OnDefaultDeviceChanged = nullptr; |
| } |
| |
| void HermeticAudioTest::OnDeviceAdded(fuchsia::media::AudioDeviceInfo info) { |
| auto id = info.unique_id; |
| token_to_unique_id_[info.token_id] = id; |
| if (info.is_input) { |
| if (!devices_[id].input) { |
| ADD_FAILURE() << "Unexpected arrival of input device " << id << ", no such device exists"; |
| } |
| if (devices_[id].info != std::nullopt) { |
| ADD_FAILURE() << "Duplicate arrival of input device " << id; |
| } |
| devices_[id].input->set_token(info.token_id); |
| } else { |
| if (!devices_[id].output) { |
| ADD_FAILURE() << "Unexpected arrival of output device " << id << ", no such device exists"; |
| } |
| if (devices_[id].info != std::nullopt) { |
| ADD_FAILURE() << "Duplicate arrival of output device " << id; |
| } |
| devices_[id].output->set_token(info.token_id); |
| } |
| token_to_unique_id_[info.token_id] = id; |
| devices_[id].info = info; |
| AUDIO_LOG(DEBUG) << "Output device (token = " << info.token_id << ", id = " << id |
| << ") has been added"; |
| } |
| |
| void HermeticAudioTest::OnDefaultDeviceChanged(uint64_t old_default_token, |
| uint64_t new_default_token) { |
| // In case of multiple pending calls during SetUp, process most recent new_default_token. |
| if (!initial_devices_received_) { |
| pending_default_device_tokens_.push(new_default_token); |
| return; |
| } |
| EXPECT_TRUE(old_default_token == 0 || token_to_unique_id_.count(old_default_token) > 0) |
| << "Default device changed from unknown device " << old_default_token << " to " |
| << new_default_token; |
| |
| EXPECT_TRUE(new_default_token == 0 || token_to_unique_id_.count(new_default_token) > 0) |
| << "Default device changed from " << old_default_token << " to unknown device " |
| << new_default_token; |
| |
| AUDIO_LOG(DEBUG) << "Default output device changed from " << old_default_token << " to " |
| << new_default_token; |
| |
| if (old_default_token != 0) { |
| auto id = token_to_unique_id_[old_default_token]; |
| devices_[id].is_default = false; |
| } |
| if (new_default_token != 0) { |
| auto id = token_to_unique_id_[new_default_token]; |
| devices_[id].is_default = true; |
| } |
| } |
| |
| fuchsia::media::AudioDeviceEnumeratorPtr HermeticAudioTest::TakeOwnershipOfAudioDeviceEnumerator() { |
| FX_CHECK(devices_.empty()); |
| FX_CHECK(capturers_.empty()); |
| FX_CHECK(renderers_.empty()); |
| |
| audio_dev_enum_.events().OnDeviceAdded = nullptr; |
| audio_dev_enum_.events().OnDeviceRemoved = nullptr; |
| audio_dev_enum_.events().OnDeviceGainChanged = nullptr; |
| audio_dev_enum_.events().OnDefaultDeviceChanged = nullptr; |
| |
| return std::move(audio_dev_enum_); |
| } |
| |
| void HermeticAudioTest::ExpectNoOverflowsOrUnderflows() { |
| for (auto& [_, device] : devices_) { |
| if (device.output) { |
| ExpectInspectMetrics(device.output.get(), |
| { |
| .children = |
| { |
| {"device underflows", {.uints = {{"count", 0}}}}, |
| {"pipeline underflows", {.uints = {{"count", 0}}}}, |
| }, |
| }); |
| } |
| } |
| for (auto& r : renderers_) { |
| ExpectInspectMetrics(r.get(), { |
| .children = |
| { |
| {"underflows", {.uints = {{"count", 0}}}}, |
| }, |
| }); |
| } |
| for (auto& c : capturers_) { |
| ExpectInspectMetrics(c.get(), { |
| .children = |
| { |
| {"overflows", {.uints = {{"count", 0}}}}, |
| }, |
| }); |
| } |
| } |
| |
| void HermeticAudioTest::ExpectInspectMetrics(VirtualOutputImpl* output, |
| const ExpectedInspectProperties& props) { |
| ExpectInspectMetrics({"output devices", fxl::StringPrintf("%03lu", output->inspect_id())}, props); |
| } |
| |
| void HermeticAudioTest::ExpectInspectMetrics(VirtualInputImpl* input, |
| const ExpectedInspectProperties& props) { |
| ExpectInspectMetrics({"input devices", fxl::StringPrintf("%03lu", input->inspect_id())}, props); |
| } |
| |
| void HermeticAudioTest::ExpectInspectMetrics(RendererShimImpl* renderer, |
| const ExpectedInspectProperties& props) { |
| ExpectInspectMetrics({"renderers", fxl::StringPrintf("%lu", renderer->inspect_id())}, props); |
| } |
| |
| void HermeticAudioTest::ExpectInspectMetrics(CapturerShimImpl* capturer, |
| const ExpectedInspectProperties& props) { |
| ExpectInspectMetrics({"capturers", fxl::StringPrintf("%lu", capturer->inspect_id())}, props); |
| } |
| |
| void HermeticAudioTest::ExpectInspectMetrics(const std::vector<std::string>& path, |
| const ExpectedInspectProperties& props) { |
| auto root = environment_->ReadInspect(HermeticAudioEnvironment::kAudioCoreComponent); |
| auto path_string = fxl::JoinStrings(path, "/"); |
| auto h = root.GetByPath(path); |
| if (!h) { |
| ADD_FAILURE() << "Missing inspect hierarchy for " << path_string; |
| return; |
| } |
| ExpectedInspectProperties::Check(props, path_string, *h); |
| } |
| |
| // Explicitly instantiate all possible implementations. |
| #define INSTANTIATE(T) \ |
| template VirtualOutput<T>* HermeticAudioTest::CreateOutput<T>( \ |
| const audio_stream_unique_id_t&, TypedFormat<T>, size_t, \ |
| std::optional<DevicePlugProperties>, float, std::optional<DeviceClockProperties>); \ |
| template VirtualInput<T>* HermeticAudioTest::CreateInput<T>( \ |
| const audio_stream_unique_id_t&, TypedFormat<T>, size_t, \ |
| std::optional<DevicePlugProperties>, float, std::optional<DeviceClockProperties>); \ |
| template AudioRendererShim<T>* HermeticAudioTest::CreateAudioRenderer<T>( \ |
| TypedFormat<T>, size_t, fuchsia::media::AudioRenderUsage, std::optional<zx::clock>); \ |
| template AudioCapturerShim<T>* HermeticAudioTest::CreateAudioCapturer<T>( \ |
| TypedFormat<T>, size_t, fuchsia::media::AudioCapturerConfiguration); \ |
| template UltrasoundRendererShim<T>* HermeticAudioTest::CreateUltrasoundRenderer<T>( \ |
| TypedFormat<T>, size_t, bool); \ |
| template UltrasoundCapturerShim<T>* HermeticAudioTest::CreateUltrasoundCapturer<T>( \ |
| TypedFormat<T>, size_t, bool); |
| |
| INSTANTIATE_FOR_ALL_FORMATS(INSTANTIATE) |
| |
| } // namespace media::audio::test |