| // 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 <lib/syslog/cpp/macros.h> |
| |
| #include <gmock/gmock.h> |
| |
| #include "src/media/audio/audio_core/testing/integration/hermetic_audio_test.h" |
| #include "src/media/audio/audio_core/v1/testing/fake_audio_driver.h" |
| |
| using ASF = fuchsia::media::AudioSampleFormat; |
| using AudioDeviceInfo = fuchsia::media::AudioDeviceInfo; |
| using AudioGainInfo = fuchsia::media::AudioGainInfo; |
| using AudioGainInfoFlags = fuchsia::media::AudioGainInfoFlags; |
| using AudioGainValidFlags = fuchsia::media::AudioGainValidFlags; |
| |
| namespace media::audio::test { |
| |
| namespace { |
| constexpr size_t kFrameRate = 48000; |
| static const auto kFormat = Format::Create<ASF::SIGNED_16>(1, kFrameRate).value(); |
| } // namespace |
| |
| // This test directly changes events in audio_dev_enum_. These changes are safe because |
| // we never change events before calling CreateInput or CreateOutput, and we use Unbind |
| // when we remove a device manually. |
| class AudioDeviceEnumeratorTest : public HermeticAudioTest { |
| protected: |
| template <typename DeviceT> |
| void TestSetDeviceGain(DeviceT* device) { |
| device->fidl().events().OnSetGain = nullptr; |
| audio_dev_enum_.events().OnDeviceGainChanged = |
| AddCallback("OnDeviceGainChanged", [device](uint64_t token, AudioGainInfo info) { |
| EXPECT_EQ(token, device->token()); |
| EXPECT_EQ(info.gain_db, -30.0f); |
| EXPECT_EQ(info.flags, AudioGainInfoFlags(0)); |
| }); |
| audio_dev_enum_->SetDeviceGain( |
| device->token(), AudioGainInfo{.gain_db = -30.0}, |
| AudioGainValidFlags::GAIN_VALID | AudioGainValidFlags::MUTE_VALID); |
| ExpectCallbacks(); |
| } |
| |
| template <typename DeviceT> |
| void TestDeviceInitializesToUnityGain(DeviceT* device) { |
| audio_dev_enum_->GetDeviceGain( |
| device->token(), AddCallback("GetDeviceGain", [device](uint64_t token, AudioGainInfo info) { |
| EXPECT_EQ(token, device->token()); |
| EXPECT_FLOAT_EQ(info.gain_db, 0); |
| })); |
| ExpectCallbacks(); |
| } |
| |
| template <typename Interface> |
| struct DeviceHolder { |
| fidl::InterfacePtr<Interface> ptr; |
| uint64_t token; |
| }; |
| |
| template <typename CreateDeviceT> |
| void TestPlugUnplugDurability(CreateDeviceT create_device) { |
| VirtualDevice::PlugProperties plug_properties = { |
| .plugged = true, |
| .hardwired = false, |
| .can_notify = true, |
| }; |
| |
| // Create two unique devices. |
| auto d1 = |
| (*this.*create_device)({0x01, 0x00}, kFormat, kFrameRate, plug_properties, 0, std::nullopt); |
| auto d2 = |
| (*this.*create_device)({0x02, 0x00}, kFormat, kFrameRate, plug_properties, 0, std::nullopt); |
| |
| // Take control of these events. |
| audio_dev_enum_.events().OnDeviceAdded = nullptr; |
| audio_dev_enum_.events().OnDeviceRemoved = nullptr; |
| audio_dev_enum_.events().OnDefaultDeviceChanged = nullptr; |
| |
| // Repeat the plug-unplug cycle many times. |
| for (int k = 0; k < 20; k++) { |
| // Unplug d2. |
| d2->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), false); |
| audio_dev_enum_.events().OnDefaultDeviceChanged = AddCallback( |
| "OnDefaultDeviceChanged after unplug Device2", |
| [d1](uint64_t old_token, uint64_t new_token) { EXPECT_EQ(new_token, d1->token()); }); |
| ExpectCallbacks(); |
| |
| // Unplug d1. |
| d1->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), false); |
| audio_dev_enum_.events().OnDefaultDeviceChanged = |
| AddCallback("OnDefaultDeviceChanged after unplug Device1", |
| [](uint64_t old_token, uint64_t new_token) { EXPECT_EQ(new_token, 0u); }); |
| ExpectCallbacks(); |
| |
| // Plug d1. |
| d1->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), true); |
| audio_dev_enum_.events().OnDefaultDeviceChanged = AddCallback( |
| "OnDefaultDeviceChanged after plug Device1", |
| [d1](uint64_t old_token, uint64_t new_token) { EXPECT_EQ(new_token, d1->token()); }); |
| ExpectCallbacks(); |
| |
| // Plug d2. |
| d2->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), true); |
| audio_dev_enum_.events().OnDefaultDeviceChanged = AddCallback( |
| "OnDefaultDeviceChanged after plug Device2", |
| [d2](uint64_t old_token, uint64_t new_token) { EXPECT_EQ(new_token, d2->token()); }); |
| ExpectCallbacks(); |
| } |
| |
| Unbind(d1); |
| Unbind(d2); |
| } |
| |
| template <typename CreateDeviceT> |
| void TestAddRemoveMany(CreateDeviceT create_device) { |
| std::vector<uint64_t> known_tokens; |
| // Too many iterations has a tendancy to time out on CQ. |
| for (uint8_t k = 0; k < 25; k++) { |
| auto d = |
| (*this.*create_device)({k, 0x00}, kFormat, kFrameRate, std::nullopt, 0, std::nullopt); |
| known_tokens.push_back(d->token()); |
| |
| // Test GetDevices(). |
| std::vector<uint64_t> got_tokens; |
| audio_dev_enum_->GetDevices( |
| AddCallback("GetDevices", [&got_tokens](std::vector<AudioDeviceInfo> devices) { |
| for (auto& d : devices) { |
| got_tokens.push_back(d.token_id); |
| } |
| })); |
| ExpectCallbacks(); |
| EXPECT_THAT(got_tokens, ::testing::UnorderedElementsAreArray(known_tokens)); |
| } |
| |
| // TearDown will test device removal. |
| } |
| }; |
| |
| TEST_F(AudioDeviceEnumeratorTest, OnDeviceGainChangedIgnoresInvalidTokensInSets) { |
| // Neither of these commands should trigger an event. |
| audio_dev_enum_->SetDeviceGain(ZX_KOID_INVALID, AudioGainInfo{.gain_db = -30.0}, |
| AudioGainValidFlags::GAIN_VALID); |
| audio_dev_enum_->SetDeviceGain(33, AudioGainInfo{.gain_db = -30.0}, |
| AudioGainValidFlags::GAIN_VALID); |
| |
| audio_dev_enum_.events().OnDeviceGainChanged = AddUnexpectedCallback("OnDeviceGainChanged"); |
| |
| // Since this call happens after the above calls, any event triggered by |
| // the above calls should have been received by the time this call returns. |
| audio_dev_enum_->GetDevices(AddCallback("GetDevices")); |
| ExpectCallbacks(); |
| } |
| |
| // TODO(https://fxbug.dev/42075702): Reenable after fixing this test flaky behavior. |
| TEST_F(AudioDeviceEnumeratorTest, DISABLED_SetDeviceGain_Input) { |
| TestSetDeviceGain(CreateInput({0xff, 0x00}, kFormat, kFrameRate)); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, SetDeviceGain_Output) { |
| TestSetDeviceGain(CreateOutput({0xff, 0x00}, kFormat, kFrameRate)); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, DeviceInitializesToUnityGain_Input) { |
| TestDeviceInitializesToUnityGain(CreateInput({0xff, 0x00}, kFormat, kFrameRate)); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, DeviceInitializesToUnityGain_Output) { |
| TestDeviceInitializesToUnityGain(CreateOutput({0xff, 0x00}, kFormat, kFrameRate)); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, AddRemoveDevice_Input) { |
| // Internally, this exercises OnDeviceAdded, and TearDown exercises OnDeviceRemoved, |
| // and both exercise OnDefaultDeviceChanged. |
| CreateInput({0xff, 0x00}, kFormat, kFrameRate); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, AddRemoveDevice_Output) { |
| // Internally, this exercises OnDeviceAdded, and TearDown exercises OnDeviceRemoved. |
| // and both exercise OnDefaultDeviceChanged. |
| CreateOutput({0xff, 0x00}, kFormat, kFrameRate); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, RemoveDeviceUnplugged_Input) { |
| auto device = CreateInput({0xff, 0x00}, kFormat, kFrameRate); |
| device->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), false, |
| [](fuchsia::virtualaudio::Device_ChangePlugState_Result result) { |
| ASSERT_FALSE(result.is_err()); |
| }); |
| RunLoopUntilIdle(); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, RemoveDeviceUnplugged_Output) { |
| auto device = CreateOutput({0xff, 0x00}, kFormat, kFrameRate); |
| device->fidl()->ChangePlugState(zx::clock::get_monotonic().get(), false, |
| [](fuchsia::virtualaudio::Device_ChangePlugState_Result result) { |
| ASSERT_FALSE(result.is_err()); |
| }); |
| RunLoopUntilIdle(); |
| } |
| |
| // TODO(https://fxbug.dev/42153575): disabled due to flakes when run in AEMU on CQ |
| // These are disabled by #if instead of DISABLED because they mention types that |
| // do no exist when the tests are DISABLED. |
| #if 0 |
| |
| TEST_F(AudioDeviceEnumeratorTest, PlugUnplugDurability_Input) { |
| // In the following expression, C++ insists that we explicitly name the current class, |
| // i.e. typeof(this). This name is created by the TEST_F macro. |
| TestPlugUnplugDurability( |
| &AudioDeviceEnumeratorTest_PlugUnplugDurability_Input_Test::CreateInput<ASF::SIGNED_16>); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, PlugUnplugDurability_Output) { |
| // In the following expression, C++ insists that we explicitly name the current class, |
| // i.e. typeof(this). This name is created by the TEST_F macro. |
| TestPlugUnplugDurability( |
| &AudioDeviceEnumeratorTest_PlugUnplugDurability_Output_Test::CreateOutput<ASF::SIGNED_16>); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, AddRemoveMany_Input) { |
| // In the following expression, C++ insists that we explicitly name the current class, |
| // i.e. typeof(this). This name is created by the TEST_F macro. |
| TestAddRemoveMany( |
| &AudioDeviceEnumeratorTest_AddRemoveMany_Input_Test::CreateInput<ASF::SIGNED_16>); |
| } |
| |
| TEST_F(AudioDeviceEnumeratorTest, AddRemoveMany_Output) { |
| // In the following expression, C++ insists that we explicitly name the current class, |
| // i.e. typeof(this). This name is created by the TEST_F macro. |
| TestAddRemoveMany( |
| &AudioDeviceEnumeratorTest_AddRemoveMany_Output_Test::CreateOutput<ASF::SIGNED_16>); |
| } |
| |
| #endif |
| |
| // The following tests use AddDeviceByChannel to add devices, rather than using |
| // CreateInput or CreateOutput. |
| namespace { |
| static const std::string kManufacturer = "Test Manufacturer"; |
| static const std::string kProduct = "Test Product"; |
| static const audio_stream_unique_id_t kUniqueId = { |
| .data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, |
| }; |
| static const std::string kUniqueIdString = "000102030405060708090a0b0c0d0e0f"; |
| } // namespace |
| |
| class AudioDeviceEnumeratorAddByChannelTest : public HermeticAudioTest { |
| protected: |
| void SetUp() override; |
| void TearDown() override; |
| |
| const std::vector<fuchsia::media::AudioDeviceInfo>& devices() { return devices_; } |
| fuchsia::media::AudioDeviceEnumerator& audio_device_enumerator() { |
| return *audio_device_enumerator_.get(); |
| } |
| |
| uint64_t device_token() { return device_token_; } |
| void set_device_token(uint64_t token) { device_token_ = token; } |
| |
| private: |
| void EnumeratorAddDevice(zx::channel remote_channel) { |
| fidl::InterfaceHandle<fuchsia::hardware::audio::StreamConfig> stream_config = {}; |
| stream_config.set_channel(std::move(remote_channel)); |
| audio_device_enumerator_->AddDeviceByChannel("test device", false, std::move(stream_config)); |
| } |
| |
| fuchsia::media::AudioDeviceEnumeratorPtr audio_device_enumerator_; |
| std::vector<fuchsia::media::AudioDeviceInfo> devices_; |
| uint64_t device_token_; |
| |
| std::unique_ptr<testing::FakeAudioDriver> driver_; |
| fzl::VmoMapper ring_buffer_; |
| }; |
| |
| void AudioDeviceEnumeratorAddByChannelTest::SetUp() { |
| HermeticAudioTest::SetUp(); |
| audio_device_enumerator_ = TakeOwnershipOfAudioDeviceEnumerator(); |
| |
| zx::channel local_channel; |
| zx::channel remote_channel; |
| zx_status_t status = zx::channel::create(0u, &local_channel, &remote_channel); |
| EXPECT_EQ(ZX_OK, status); |
| |
| driver_ = std::make_unique<testing::FakeAudioDriver>(std::move(local_channel), dispatcher()); |
| ASSERT_NE(driver_, nullptr); |
| driver_->set_device_manufacturer(kManufacturer); |
| driver_->set_device_product(kProduct); |
| driver_->set_stream_unique_id(kUniqueId); |
| // Allocate a ring buffer large enough for 1 second of 48khz 2 channel 16-bit audio. |
| ring_buffer_ = driver_->CreateRingBuffer(48000 * sizeof(int16_t) * 2); |
| driver_->Start(); |
| |
| audio_device_enumerator_.events().OnDeviceAdded = [this](fuchsia::media::AudioDeviceInfo info) { |
| devices_.push_back(std::move(info)); |
| }; |
| |
| EnumeratorAddDevice(std::move(remote_channel)); |
| } |
| |
| void AudioDeviceEnumeratorAddByChannelTest::TearDown() { |
| ASSERT_TRUE(audio_device_enumerator_.is_bound()); |
| audio_device_enumerator_.events().OnDeviceRemoved = [this](uint64_t dev_token) { |
| EXPECT_EQ(dev_token, device_token()); |
| devices_.clear(); |
| }; |
| |
| driver_ = nullptr; |
| RunLoopUntil([this]() { return devices().empty(); }); |
| |
| ASSERT_TRUE(audio_device_enumerator_.is_bound()); |
| audio_device_enumerator_.Unbind(); |
| |
| HermeticAudioTest::TearDown(); |
| } |
| |
| // Test that |AddDeviceByChannel| result in an |OnDeviceAdded| event. |
| TEST_F(AudioDeviceEnumeratorAddByChannelTest, AddDevice) { |
| // Expect that the added device is enumerated via the device enumerator. |
| this->RunLoopUntil([this]() { return !this->devices().empty(); }); |
| |
| ASSERT_EQ(1u, this->devices().size()); |
| auto device = this->devices()[0]; |
| EXPECT_EQ(kManufacturer + " " + kProduct, device.name); |
| EXPECT_EQ(kUniqueIdString, device.unique_id); |
| EXPECT_EQ(false, device.is_input); |
| |
| this->set_device_token(device.token_id); |
| } |
| |
| // Test that the info in |GetDevices| matches the info in the |OnDeviceAdded| event. |
| TEST_F(AudioDeviceEnumeratorAddByChannelTest, GetDevices) { |
| this->RunLoopUntil([this]() { return !this->devices().empty(); }); |
| |
| std::optional<std::vector<fuchsia::media::AudioDeviceInfo>> devices; |
| this->audio_device_enumerator().GetDevices( |
| [&devices](std::vector<fuchsia::media::AudioDeviceInfo> devices_in) { |
| devices = std::move(devices_in); |
| }); |
| this->RunLoopUntil([&devices]() { return devices.has_value(); }); |
| |
| ASSERT_EQ(1u, devices->size()); |
| auto device = (*devices)[0]; |
| EXPECT_EQ(kManufacturer + " " + kProduct, device.name); |
| EXPECT_EQ(kUniqueIdString, device.unique_id); |
| EXPECT_EQ(false, device.is_input); |
| |
| this->set_device_token(device.token_id); |
| } |
| |
| } // namespace media::audio::test |