| // Copyright 2018 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 <fuchsia/media/cpp/fidl.h> |
| #include <fuchsia/virtualaudio/cpp/fidl.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/gtest/real_loop_fixture.h> |
| |
| #include "lib/component/cpp/environment_services_helper.h" |
| #include "src/lib/fxl/logging.h" |
| #include "src/media/audio/audio_core/test/audio_tests_shared.h" |
| |
| namespace media::audio::test { |
| |
| // The AudioLoopbackEnvironment class allows us to make configuration changes |
| // before any test case begins, and after all test cases complete. |
| class AudioLoopbackEnvironment : public ::testing::Environment { |
| public: |
| // Before any test cases in this program, synchronously connect to the service |
| // to ensure that the audio and audio_core components are present and loaded, |
| // and that at least one (virtual) audio output device is present. |
| // |
| // On assert-false during this SetUp method, no test cases run, and they may |
| // display as passed. However, the overall binary returns non-zero (fail). |
| void SetUp() override { |
| // This is an unchanging input for the entire component; get it once here. |
| auto environment_services = component::GetEnvironmentServices(); |
| |
| // We need at least one active audio output, for loopback capture to work. |
| // So use this Control to enable virtualaudio and add a virtual audio output |
| // that will exist for the entirety of this binary's test cases. We will |
| // remove it and disable virtual_audio immediately afterward. |
| environment_services->ConnectToService( |
| virtual_audio_control_sync_.NewRequest()); |
| zx_status_t status = virtual_audio_control_sync_->Enable(); |
| ASSERT_EQ(status, ZX_OK) << "Failed to enable virtualaudio"; |
| |
| // Create an output device using default settings, save it while tests run. |
| environment_services->ConnectToService( |
| virtual_audio_output_sync_.NewRequest()); |
| status = virtual_audio_output_sync_->Add(); |
| ASSERT_EQ(status, ZX_OK) << "Failed to add virtual audio output"; |
| |
| // Ensure that the output is active before we proceed to running tests. |
| uint32_t num_inputs, num_outputs = 0, num_tries = 0; |
| do { |
| status = |
| virtual_audio_control_sync_->GetNumDevices(&num_inputs, &num_outputs); |
| ASSERT_EQ(status, ZX_OK) << "GetNumDevices failed"; |
| |
| ++num_tries; |
| } while (num_outputs == 0 && num_tries < 100); |
| ASSERT_GT(num_outputs, 0u) |
| << "Timed out trying to add virtual audio output"; |
| |
| // Synchronously calling a FIDL method with a callback guarantees that the |
| // service is loaded and running before the sync method itself returns. |
| // |
| // This is not the case for sync calls _without_ callback, nor async calls, |
| // because of pipelining inherent in FIDL's design. |
| environment_services->ConnectToService(audio_dev_enum_sync_.NewRequest()); |
| // Ensure that the audio_core binary is resident. |
| uint64_t default_output; |
| bool connected_to_svc = (audio_dev_enum_sync_->GetDefaultOutputDevice( |
| &default_output) == ZX_OK); |
| |
| ASSERT_TRUE(connected_to_svc) << "Failed in GetDefaultOutputDevice"; |
| } |
| |
| // Ensure that our virtual device is gone when our test bin exits. |
| void TearDown() override { |
| // Remove our virtual audio output device |
| zx_status_t status = virtual_audio_output_sync_->Remove(); |
| ASSERT_EQ(status, ZX_OK) << "Failed to add virtual audio output"; |
| |
| // And ensure that virtualaudio is disabled, by the time we leave. |
| status = virtual_audio_control_sync_->Disable(); |
| ASSERT_EQ(status, ZX_OK) << "Failed to disable virtualaudio"; |
| |
| // Wait for GetNumDevices(output) to equal zero before proceeding. |
| uint32_t num_inputs = 1, num_outputs = 1, num_tries = 0; |
| do { |
| status = |
| virtual_audio_control_sync_->GetNumDevices(&num_inputs, &num_outputs); |
| ASSERT_EQ(status, ZX_OK) << "GetNumDevices failed"; |
| |
| ++num_tries; |
| } while ((num_outputs != 0 || num_inputs != 0) && num_tries < 100); |
| ASSERT_EQ(num_outputs, 0u) << "Timed out while disabling virtualaudio"; |
| ASSERT_EQ(num_inputs, 0u) << "Timed out while disabling virtualaudio"; |
| |
| virtual_audio_output_sync_.Unbind(); |
| virtual_audio_control_sync_.Unbind(); |
| } |
| |
| // If needed, this (overriding) function would also need to be public. |
| // ~AudioLoopbackEnvironment() override {} |
| |
| fuchsia::virtualaudio::ControlSyncPtr virtual_audio_control_sync_; |
| fuchsia::virtualaudio::OutputSyncPtr virtual_audio_output_sync_; |
| fuchsia::media::AudioDeviceEnumeratorSyncPtr audio_dev_enum_sync_; |
| }; |
| |
| // |
| // AudioLoopbackTest |
| // |
| // Base Class for testing simple playback and capture with loopback. |
| class AudioLoopbackTest : public gtest::RealLoopFixture { |
| public: |
| static constexpr int32_t kSampleRate = 8000; |
| static constexpr int kChannelCount = 1; |
| static constexpr int kSampleSeconds = 1; |
| static constexpr int16_t kPlaybackData1 = 0x1000; |
| static constexpr int16_t kPlaybackData2 = 0xfff; |
| static constexpr int16_t kCaptureData1 = 0x7fff; |
| |
| protected: |
| void SetUp() override; |
| void TearDown() override; |
| |
| std::shared_ptr<component::Services> environment_services_; |
| fuchsia::media::AudioPtr audio_; |
| |
| void SetUpRenderer(unsigned int index, int16_t data); |
| void CleanUpRenderer(unsigned int index); |
| |
| fuchsia::media::AudioRendererPtr audio_renderer_[2]; |
| fzl::VmoMapper payload_buffer_[2]; |
| size_t playback_size_[2]; |
| size_t playback_sample_size_[2]; |
| |
| void SetUpCapturer(unsigned int index, int16_t data); |
| void CleanUpCapturer(unsigned int index); |
| |
| fuchsia::media::AudioCapturerPtr audio_capturer_[1]; |
| fzl::VmoMapper capture_buffer_[1]; |
| size_t capture_size_[1]; |
| size_t capture_sample_size_[1]; |
| |
| bool error_occurred_ = false; |
| |
| void ErrorHandler(zx_status_t error) { |
| error_occurred_ = true; |
| FXL_LOG(ERROR) << "Unexpected error: " << error; |
| } |
| }; |
| |
| // AudioLoopbackTest implementation |
| // |
| |
| // SetUpRenderer |
| // |
| // For loopback tests, setup the first audio_renderer interface. |
| void AudioLoopbackTest::SetUpRenderer(unsigned int index, int16_t data) { |
| ASSERT_LT(index, countof(audio_renderer_)); |
| |
| int16_t* buffer; |
| zx::vmo payload_vmo; |
| |
| audio_->CreateAudioRenderer(audio_renderer_[index].NewRequest()); |
| ASSERT_TRUE(audio_renderer_[index].is_bound()); |
| |
| audio_renderer_[index].set_error_handler( |
| [this](zx_status_t error) { ErrorHandler(error); }); |
| |
| fuchsia::media::AudioStreamType format; |
| format.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16; |
| format.channels = kChannelCount; |
| format.frames_per_second = kSampleRate; |
| |
| playback_sample_size_[index] = sizeof(int16_t); |
| |
| playback_size_[index] = format.frames_per_second * format.channels * |
| playback_sample_size_[index] * kSampleSeconds; |
| |
| zx_status_t status = payload_buffer_[index].CreateAndMap( |
| playback_size_[index], ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, |
| &payload_vmo, ZX_RIGHT_READ | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER); |
| ASSERT_EQ(status, ZX_OK) << "Renderer VmoMapper:::CreateAndMap(" << index |
| << ") failed - " << status; |
| |
| buffer = reinterpret_cast<int16_t*>(payload_buffer_[index].start()); |
| for (int32_t i = 0; i < kSampleRate * kSampleSeconds; i++) |
| buffer[i] = data; |
| |
| audio_renderer_[index]->SetPcmStreamType(format); |
| audio_renderer_[index]->AddPayloadBuffer(0, std::move(payload_vmo)); |
| } |
| |
| // CleanUpRenderer |
| // |
| // Flush the output and free the vmo that was used by Renderer1. |
| void AudioLoopbackTest::CleanUpRenderer(unsigned int index) { |
| ASSERT_LT(index, countof(audio_renderer_)); |
| bool flushed = false; |
| |
| // Flush the audio |
| audio_renderer_[index]->DiscardAllPackets( |
| [&flushed]() { flushed = true; }); |
| EXPECT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &flushed]() { return error_occurred_ || flushed; }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| |
| EXPECT_TRUE(flushed); |
| payload_buffer_[index].Unmap(); |
| } |
| |
| // SetUpCapturer |
| // |
| // For loopback tests, setup an audio_capturer interface |
| void AudioLoopbackTest::SetUpCapturer(unsigned int index, int16_t data) { |
| ASSERT_LT(index, countof(audio_capturer_)); |
| |
| int16_t* buffer; |
| zx::vmo capture_vmo; |
| |
| audio_->CreateAudioCapturer(audio_capturer_[index].NewRequest(), true); |
| ASSERT_TRUE(audio_capturer_[index].is_bound()); |
| |
| audio_capturer_[index].set_error_handler( |
| [this](zx_status_t error) { ErrorHandler(error); }); |
| |
| fuchsia::media::AudioStreamType format; |
| format.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16; |
| format.channels = kChannelCount; |
| format.frames_per_second = kSampleRate; |
| |
| capture_sample_size_[index] = sizeof(int16_t); |
| |
| capture_size_[index] = format.frames_per_second * format.channels * |
| capture_sample_size_[index] * kSampleSeconds; |
| |
| // ZX_VM_PERM_WRITE taken here as we pre-fill the buffer to catch any |
| // cases where we get back a packet without anything having been done |
| // with it. |
| zx_status_t status = capture_buffer_[index].CreateAndMap( |
| capture_size_[index], ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, |
| &capture_vmo, |
| ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER); |
| ASSERT_EQ(status, ZX_OK) << "Capturer VmoMapper:::CreateAndMap failed - " |
| << status; |
| |
| buffer = reinterpret_cast<int16_t*>(capture_buffer_[index].start()); |
| for (int32_t i = 0; i < kSampleRate * kSampleSeconds; i++) |
| buffer[i] = data; |
| |
| audio_capturer_[index]->SetPcmStreamType(format); |
| audio_capturer_[index]->AddPayloadBuffer(0, std::move(capture_vmo)); |
| } |
| |
| // CleanUpCapturer |
| // |
| void AudioLoopbackTest::CleanUpCapturer(unsigned int index) { |
| ASSERT_LT(index, countof(audio_capturer_)); |
| } |
| |
| // |
| // AudioLoopbackTest implementation |
| // |
| void AudioLoopbackTest::SetUp() { |
| ::gtest::RealLoopFixture::SetUp(); |
| |
| environment_services_ = component::GetEnvironmentServices(); |
| environment_services_->ConnectToService(audio_.NewRequest()); |
| ASSERT_TRUE(audio_.is_bound()); |
| |
| audio_.set_error_handler([this](zx_status_t error) { ErrorHandler(error); }); |
| audio_->SetSystemGain(0.0f); |
| audio_->SetSystemMute(false); |
| } |
| |
| void AudioLoopbackTest::TearDown() { |
| EXPECT_FALSE(error_occurred_); |
| EXPECT_TRUE(audio_.is_bound()); |
| |
| ::gtest::RealLoopFixture::TearDown(); |
| } |
| |
| // SingleStream |
| // |
| // Creates a single output stream and a loopback capture and verifies it gets |
| // back what it puts in. |
| TEST_F(AudioLoopbackTest, SingleStream) { |
| fuchsia::media::StreamPacket packet, captured; |
| |
| // SetUp playback stream |
| SetUpRenderer(0, kPlaybackData1); |
| SetUpCapturer(0, kCaptureData1); |
| |
| auto* capture = reinterpret_cast<int16_t*>(capture_buffer_[0].start()); |
| |
| // Add a callback for when we get our captured packet. |
| bool produced_packet = false; |
| audio_capturer_[0].events().OnPacketProduced = |
| [this, &captured, &produced_packet](fuchsia::media::StreamPacket packet) { |
| // We only care about the first set of captured samples |
| if (captured.payload_size == 0) { |
| captured = packet; |
| audio_capturer_[0]->StopAsyncCaptureNoReply(); |
| produced_packet = true; |
| } |
| }; |
| |
| // Get the minimum duration after submitting a packet to when we can start |
| // capturing what we sent on the loopback interface |
| zx_duration_t sleep_duration = 0; |
| audio_renderer_[0]->GetMinLeadTime([&sleep_duration](zx_duration_t t) { |
| // Give a little wiggle room. |
| sleep_duration = t + ZX_MSEC(5); |
| }); |
| ASSERT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &sleep_duration]() { |
| return error_occurred_ || (sleep_duration > 0); |
| }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| ASSERT_FALSE(error_occurred_); |
| |
| packet.payload_offset = 0; |
| packet.payload_size = playback_size_[0]; |
| |
| audio_renderer_[0]->SendPacketNoReply(packet); |
| |
| int64_t ref_time_received = -1; |
| int64_t media_time_received = -1; |
| |
| // Start playing right now, so that after we've delayed at least 1 leadtime, |
| // we should have mixed audio available for capture. Our playback is sized |
| // to be much much larger than our capture to prevent test flakes. |
| audio_renderer_[0]->Play(zx_clock_get_monotonic(), 0, |
| [&ref_time_received, &media_time_received]( |
| int64_t ref_time, int64_t media_time) { |
| ref_time_received = ref_time; |
| media_time_received = media_time; |
| }); |
| ASSERT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &ref_time_received]() { |
| return error_occurred_ || (ref_time_received > -1); |
| }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| ASSERT_FALSE(error_occurred_); |
| |
| // We expect that media_time 0 played back at some point after the 'zero' |
| // time on the system. |
| EXPECT_EQ(media_time_received, 0); |
| EXPECT_GE(ref_time_received, 0); |
| |
| // Give the playback some time to get mixed. |
| zx_nanosleep(zx_clock_get_monotonic() + sleep_duration); |
| |
| // Capture 10 samples of audio. |
| audio_capturer_[0]->StartAsyncCapture(10); |
| EXPECT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &produced_packet]() { return error_occurred_ || produced_packet; }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| |
| // Check that we got 10 samples as we expected. |
| EXPECT_EQ(captured.payload_size / capture_sample_size_[0], 10U); |
| |
| // Check that all of the samples contain the expected data. |
| for (size_t i = 0; i < (captured.payload_size / capture_sample_size_[0]); |
| i++) { |
| size_t index = (captured.payload_offset + i) % 8000; |
| |
| EXPECT_EQ(capture[index], kPlaybackData1); |
| } |
| |
| CleanUpRenderer(0); |
| CleanUpCapturer(0); |
| } |
| |
| // DualStream |
| // |
| // Creates a pair of output streams and a loopback capture and verifies it gets |
| // back what it puts in. |
| TEST_F(AudioLoopbackTest, DualStream) { |
| fuchsia::media::StreamPacket packet[2], captured; |
| |
| // SetUp playback streams |
| SetUpRenderer(0, kPlaybackData1); |
| SetUpRenderer(1, kPlaybackData2); |
| |
| // SetUp loopback capture |
| SetUpCapturer(0, kCaptureData1); |
| |
| auto* capture = reinterpret_cast<int16_t*>(capture_buffer_[0].start()); |
| |
| // Add a callback for when we get our captured packet. |
| bool produced_packet = false; |
| audio_capturer_[0].events().OnPacketProduced = |
| [this, &captured, &produced_packet](fuchsia::media::StreamPacket packet) { |
| // We only care about the first set of captured samples |
| if (captured.payload_size == 0) { |
| captured = packet; |
| audio_capturer_[0]->StopAsyncCaptureNoReply(); |
| produced_packet = true; |
| } |
| }; |
| |
| // Get the minimum duration after submitting a packet to when we can start |
| // capturing what we sent on the loopback interface. This assumes that the |
| // latency will be the same for both playback streams. This happens to be |
| // true for this test as we create the renderers with the same parameters, but |
| // is not a safe assumption for the general users of this API to make. |
| zx_duration_t sleep_duration = 0; |
| audio_renderer_[0]->GetMinLeadTime([&sleep_duration](zx_duration_t t) { |
| // Give a little wiggle room. |
| sleep_duration = t + ZX_MSEC(5); |
| }); |
| ASSERT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &sleep_duration]() { |
| return error_occurred_ || (sleep_duration > 0); |
| }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| ASSERT_FALSE(error_occurred_); |
| |
| for (int i = 0; i < 2; i++) { |
| packet[i].payload_offset = 0; |
| packet[i].payload_size = playback_size_[i]; |
| audio_renderer_[i]->SendPacketNoReply(packet[i]); |
| } |
| |
| int64_t ref_time_received = -1; |
| int64_t media_time_received = -1; |
| |
| // Start playing right now, so that after we've delayed at least 1 leadtime, |
| // we should have mixed audio available for capture. Our playback is sized |
| // to be much much larger than our capture to prevent test flakes. |
| auto playat = zx_clock_get_monotonic(); |
| audio_renderer_[0]->PlayNoReply(playat, 0); |
| // Only get the callback for the second renderer. |
| audio_renderer_[1]->Play(playat, 0, |
| [&ref_time_received, &media_time_received]( |
| int64_t ref_time, int64_t media_time) { |
| ref_time_received = ref_time; |
| media_time_received = media_time; |
| }); |
| ASSERT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &ref_time_received]() { |
| return error_occurred_ || (ref_time_received > -1); |
| }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| ASSERT_FALSE(error_occurred_); |
| |
| // We expect that media_time 0 played back at some point after the 'zero' |
| // time on the system. |
| EXPECT_EQ(media_time_received, 0); |
| EXPECT_GT(ref_time_received, 0); |
| |
| // Give the playback some time to get mixed. |
| zx_nanosleep(zx_deadline_after(sleep_duration)); |
| |
| // Capture 10 samples of audio. |
| audio_capturer_[0]->StartAsyncCapture(10); |
| EXPECT_TRUE(RunLoopWithTimeoutOrUntil( |
| [this, &produced_packet]() { return error_occurred_ || produced_packet; }, |
| kDurationResponseExpected, kDurationGranularity)) |
| << kTimeoutErr; |
| |
| // Check that we got 10 samples as we expected. |
| EXPECT_EQ(captured.payload_size / capture_sample_size_[0], 10U); |
| |
| // Check that all of the samples contain the expected data. |
| for (size_t i = 0; i < (captured.payload_size / capture_sample_size_[0]); |
| i++) { |
| size_t index = (captured.payload_offset + i) % 8000; |
| EXPECT_EQ(capture[index], kPlaybackData1 + kPlaybackData2); |
| } |
| |
| CleanUpRenderer(1); |
| CleanUpRenderer(0); |
| CleanUpCapturer(0); |
| } |
| |
| } // namespace media::audio::test |
| |
| int main(int argc, char** argv) { |
| ::testing::InitGoogleTest(&argc, argv); |
| |
| // gtest takes ownership of registered environments: *do not delete them* |
| ::testing::AddGlobalTestEnvironment( |
| new ::media::audio::test::AudioLoopbackEnvironment); |
| |
| int result = RUN_ALL_TESTS(); |
| |
| return result; |
| } |