blob: c1dfa9613c67208b8db3783ad3975746bf9eb1e0 [file] [log] [blame]
// Copyright 2022 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 <zircon/device/audio.h>
#include <cmath>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/lib/files/path.h"
#include "src/media/audio/audio_core/testing/integration/hermetic_audio_realm.h"
#include "src/media/audio/audio_core/testing/integration/hermetic_audio_test.h"
#include "src/media/audio/audio_core/testing/integration/virtual_device.h"
#include "src/media/audio/lib/format/audio_buffer.h"
#include "src/media/audio/lib/format/format.h"
#include "src/media/audio/lib/format/traits.h"
#include "src/virtualization/tests/enclosed_guest.h"
namespace {
using ::fuchsia::media::AudioSampleFormat;
using ::media::audio::AudioBuffer;
using ::media::audio::AudioBufferSlice;
using ::media::audio::Format;
using ::media::audio::SampleFormatTraits;
using ::media::audio::test::HermeticAudioRealm;
using ::media::audio::test::HermeticAudioTest;
using ::media::audio::test::VirtualOutput;
constexpr int32_t kOutputFrameRate = 48000;
constexpr int32_t kStereoChannelCount = 2;
constexpr AudioSampleFormat kSampleFormat = AudioSampleFormat::FLOAT;
constexpr audio_stream_unique_id_t kOutputId = AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS;
// TODO(fxbug.dev/87646): Consider creating a `virtio_audio_test_util` that directly communicates
// with ALSA instead to have better control over the output buffer.
constexpr char kAplayBinPath[] = "/tmp/vm_extras/aplay";
// TODO(fxbug.dev/87646): Consider creating a `virtio_audio_test_util` that directly generates this
// audio files on-the-fly.
constexpr char kTestFilePath[] = "/tmp/extras/stereo_ramp_48khz_16bit.wav";
constexpr int32_t kRampFrameCount = 65536;
constexpr int32_t kZeroPaddingFrameCount = 1024;
template <class GuestType>
class VirtioSoundGuestTest : public HermeticAudioTest {
protected:
void SetUp() override {
GuestLaunchInfo guest_launch_info;
enclosed_guest_.emplace(loop());
HermeticAudioTest::SetTestSuiteRealmOptions([this, &guest_launch_info] {
return HermeticAudioRealm::Options{
.customize_realm = [this, &guest_launch_info](
::component_testing::RealmBuilder& realm_builder) -> zx_status_t {
auto status = enclosed_guest_->InstallInRealm(realm_builder, guest_launch_info);
if (status != ZX_OK) {
return status;
}
using component_testing::ChildRef;
using component_testing::Protocol;
using component_testing::Route;
realm_builder.AddRoute(Route{
.capabilities = {Protocol{"fuchsia.media.Audio"}},
.source = ChildRef{HermeticAudioRealm::kAudioCore},
.targets = {ChildRef{"guest_manager"}},
});
return ZX_OK;
},
};
});
// Create the realm and start audio services.
HermeticAudioTest::SetUp();
// Now start the guest.
ASSERT_EQ(enclosed_guest_->LaunchInRealm(realm().realm_root(), guest_launch_info,
zx::time::infinite()),
ZX_OK)
<< "Failed to launch guest";
const auto format =
Format::Create<kSampleFormat>(kStereoChannelCount, kOutputFrameRate).take_value();
// Add some padding to ensure that there is enough headroom in the ring buffer.
output_ = CreateOutput(kOutputId, format,
kRampFrameCount + kZeroPaddingFrameCount + 10 * kOutputFrameRate);
}
void TearDown() override {
EXPECT_EQ(enclosed_guest_->Stop(zx::time::infinite()), ZX_OK);
enclosed_guest_.reset();
if constexpr (kEnableAllOverflowAndUnderflowChecksInRealtimeTests) {
ExpectNoOverflowsOrUnderflows();
}
HermeticAudioTest::TearDown();
}
zx_status_t Execute(const std::vector<std::string>& argv) {
return enclosed_guest_->Execute(argv, {}, zx::time::infinite(), nullptr, nullptr);
}
std::optional<int64_t> GetFirstNonSilentFrame(const AudioBuffer<kSampleFormat>& buffer) const {
for (int64_t frame = 0; frame < buffer.NumFrames(); ++frame) {
if (buffer.SampleAt(frame, 0) != SampleFormatTraits<kSampleFormat>::kSilentValue) {
return frame;
}
}
return std::nullopt;
}
AudioBuffer<kSampleFormat> GetOutputRingBuffer() { return output_->SnapshotRingBuffer(); }
bool OutputHasUnderflows() { return DeviceHasUnderflows(output_); }
private:
std::optional<GuestType> enclosed_guest_;
VirtualOutput<kSampleFormat>* output_ = nullptr;
};
// We only support `TerminaEnclosedGuest` since the tests require `virtio-sound` and `alsa-lib`.
TYPED_TEST_SUITE(VirtioSoundGuestTest, ::testing::Types<TerminaEnclosedGuest>,
GuestTestNameGenerator);
TYPED_TEST(VirtioSoundGuestTest, OutputFidelity) {
// The input audio file consists of a stereo linear ramp that covers the entire 16-bit range of
// values with opposing direction in each channel. It is calculated for each frame as follows:
// `buffer[frame][0] = 0x7FFF - frame`
// `buffer[frame][1] = -0x8000 + frame`
// Note that the file consists of `kZeroPaddingFrameCount` frames of zeros at the beginning in
// order to compensate for the initial gain ramp, which is then followed by the
// `kRampFrameCount` ramp frames as described above.
ASSERT_EQ(this->Execute({kAplayBinPath, kTestFilePath}), ZX_OK);
const auto ring_buffer = this->GetOutputRingBuffer();
ASSERT_EQ(ring_buffer.format().channels(), kStereoChannelCount);
// TODO(fxbug.dev/80003): Remove workarounds when underflow conditions are fixed.
if (this->OutputHasUnderflows()) {
GTEST_SKIP() << "Skipping fidelity checks due to underflows";
__builtin_unreachable();
}
const std::optional<int64_t> start_frame = this->GetFirstNonSilentFrame(ring_buffer);
ASSERT_TRUE(start_frame.has_value()) << "Could not find non-silent frame";
FX_LOGS(INFO) << "First non-silent frame: " << *start_frame;
const int64_t end_frame = *start_frame + kRampFrameCount;
ASSERT_LE(end_frame, ring_buffer.NumFrames()) << "Not enough frames in ring buffer";
const auto buffer_slice = AudioBufferSlice(&ring_buffer, *start_frame, end_frame);
// TODO(fxbug.dev/95106): Temporarily limit the end frame to `24000 - kZeroPaddingFrameCount`
// until the buffer repetition issue is resolved (to be replaced by `kRampFrameCount`).
for (int32_t frame = 0; frame < 24000 - kZeroPaddingFrameCount; ++frame) {
EXPECT_FLOAT_EQ(buffer_slice.SampleAt(frame, 0),
static_cast<float>(0x7FFF - frame) / static_cast<float>(kRampFrameCount / 2))
<< "at (" << frame << ", 0)";
EXPECT_FLOAT_EQ(buffer_slice.SampleAt(frame, 1),
static_cast<float>(-0x8000 + frame) / static_cast<float>(kRampFrameCount / 2))
<< "at (" << frame << ", 1)";
}
}
} // namespace