blob: 7d09129e5df128870ecd27fd1f328686eb2da347 [file] [log] [blame]
// Copyright 2020 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 <fcntl.h>
#include "src/lib/fsl/io/fd.h"
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
#include "src/media/sounds/soundplayer/sound_player_impl.h"
#include "src/media/sounds/soundplayer/test/fake_audio_renderer.h"
namespace soundplayer {
namespace test {
constexpr uint64_t kPayloadSize = 1024;
constexpr uint32_t kFrameSize = 2;
constexpr uint32_t kFramesPerSecond = 44100;
constexpr uint64_t kWavFilePayloadSize = 25438;
constexpr int64_t kWavFileDuration = 288412698;
constexpr uint32_t kWavFileChannels = 1;
constexpr uint32_t kWavFramesPerSecond = 44100;
constexpr uint64_t kOggOpusFilePayloadSize = 530592;
constexpr int64_t kOggOpusFileDuration = 2763500000;
constexpr uint32_t kOggOpusFileChannels = 2;
constexpr uint32_t kOggOpusFramesPerSecond = 48000;
constexpr fuchsia::media::AudioRenderUsage kUsage = fuchsia::media::AudioRenderUsage::MEDIA;
zx_koid_t GetKoid(const zx::vmo& vmo) {
zx_info_handle_basic_t info;
zx_status_t status =
zx_object_get_info(vmo.get(), ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
FX_CHECK(status == ZX_OK);
return info.koid;
}
class FakeAudio : public fuchsia::media::Audio {
public:
FakeAudio() : binding_(this) {}
fuchsia::media::AudioPtr NewPtr() { return binding_.NewBinding().Bind(); }
// fuchsia::media::Audio implementation.
void CreateAudioRenderer(fidl::InterfaceRequest<fuchsia::media::AudioRenderer> request) override {
EXPECT_FALSE(expectations_.empty());
EXPECT_FALSE(expecations_iter_ == expectations_.end());
auto renderer = std::make_unique<FakeAudioRenderer>();
if (warmup_renderer_created_) {
renderer->SetExpectations(*expecations_iter_);
++expecations_iter_;
} else {
renderer->ExpectWarmup(block_warmup_);
warmup_renderer_created_ = true;
}
auto raw_renderer = renderer.get();
renderer->Bind(std::move(request), [this, raw_renderer](zx_status_t status) {
EXPECT_EQ(ZX_ERR_PEER_CLOSED, status);
EXPECT_TRUE(raw_renderer->completed());
renderers_.erase(raw_renderer);
});
renderers_.emplace(raw_renderer, std::move(renderer));
}
void CreateAudioCapturer(fidl::InterfaceRequest<fuchsia::media::AudioCapturer> request,
bool loopback) override {
FX_NOTIMPLEMENTED();
}
// Prevents warmup from completing until |ChangeMinLeadTime| is called with a non-zero
// duration.
void SetBlockWarmup() { block_warmup_ = true; }
// Sets expectations for renderers.
void SetRendererExpectations(const std::vector<FakeAudioRenderer::Expectations>& expectations) {
expectations_ = expectations;
expecations_iter_ = expectations_.begin();
}
void ChangeMinLeadTime(zx::duration min_lead_time) {
for (auto& pair : renderers_) {
pair.second->ChangeMinLeadTime(min_lead_time);
}
}
bool renderers_completed() const { return renderers_.empty(); }
private:
fidl::Binding<fuchsia::media::Audio> binding_;
std::unordered_map<FakeAudioRenderer*, std::unique_ptr<FakeAudioRenderer>> renderers_;
std::vector<FakeAudioRenderer::Expectations> expectations_;
std::vector<FakeAudioRenderer::Expectations>::iterator expecations_iter_;
bool warmup_renderer_created_ = false;
bool block_warmup_ = false;
};
class SoundPlayerTests : public gtest::RealLoopFixture {
public:
SoundPlayerTests()
: ptr_to_under_test_(), under_test_(fake_audio_.NewPtr(), ptr_to_under_test_.NewRequest()) {}
protected:
// Prevents warmup from completing until |ChangeMinLeadTime| is called with a non-zero
// duration.
void SetBlockWarmup() { fake_audio_.SetBlockWarmup(); }
// Sets expectations for renderers.
void SetRendererExpectations(const std::vector<FakeAudioRenderer::Expectations>& expectations) {
fake_audio_.SetRendererExpectations(expectations);
}
void ChangeMinLeadTime(zx::duration min_lead_time) {
fake_audio_.ChangeMinLeadTime(min_lead_time);
}
SoundPlayerImpl& under_test() { return under_test_; }
fuchsia::media::sounds::PlayerPtr& under_test_ptr() { return ptr_to_under_test_; }
bool renderers_completed() const { return fake_audio_.renderers_completed(); }
std::tuple<fuchsia::mem::Buffer, zx_koid_t, fuchsia::media::AudioStreamType> CreateTestSound(
uint64_t size) {
FX_CHECK(size % sizeof(int16_t) == 0);
zx::vmo vmo;
zx_status_t status = zx::vmo::create(size, 0, &vmo);
FX_CHECK(status == ZX_OK);
zx_koid_t koid = GetKoid(vmo);
return {fuchsia::mem::Buffer{
.vmo = std::move(vmo),
.size = size,
},
koid,
fuchsia::media::AudioStreamType{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kFrameSize / sizeof(int16_t),
.frames_per_second = kFramesPerSecond,
}};
}
fidl::InterfaceHandle<fuchsia::io::File> ResourceFile(const std::string& file_name) {
auto fd = fbl::unique_fd(open(("/pkg/data/" + file_name).c_str(), O_RDONLY));
EXPECT_TRUE(fd.is_valid());
return fidl::InterfaceHandle<fuchsia::io::File>(
fsl::TransferChannelFromFileDescriptor(std::move(fd)));
}
private:
FakeAudio fake_audio_;
fuchsia::media::sounds::PlayerPtr ptr_to_under_test_;
SoundPlayerImpl under_test_;
};
// Tests nominal playback of a sound added as a buffer.
TEST_F(SoundPlayerTests, Buffer) {
auto [buffer, koid, stream_type] = CreateTestSound(kPayloadSize);
SetRendererExpectations({{
.payload_buffer_ = koid,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kPayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ = fidl::Clone(stream_type),
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundBuffer(0, std::move(buffer), std::move(stream_type));
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound of the maximum size the renderer will play as a single packet.
TEST_F(SoundPlayerTests, MaxSinglePacketBuffer) {
auto [buffer, koid, stream_type] =
CreateTestSound(fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET * kFrameSize);
SetRendererExpectations({{
.payload_buffer_ = koid,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET * kFrameSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ = fidl::Clone(stream_type),
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundBuffer(0, std::move(buffer), std::move(stream_type));
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound large enough to require two renderer packets.
TEST_F(SoundPlayerTests, TwoPacketBuffer) {
auto [buffer, koid, stream_type] =
CreateTestSound((fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET + 1) * kFrameSize);
SetRendererExpectations({{
.payload_buffer_ = koid,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET * kFrameSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0},
{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET * kFrameSize,
.payload_size = kFrameSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ = fidl::Clone(stream_type),
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundBuffer(0, std::move(buffer), std::move(stream_type));
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound from a wav file.
TEST_F(SoundPlayerTests, WavFile) {
SetRendererExpectations({{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound from a wav file twice.
TEST_F(SoundPlayerTests, WavFileTwice) {
SetRendererExpectations({
{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = false,
},
{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = 1,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = false,
},
});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays and stops a sound from a wav file.
TEST_F(SoundPlayerTests, WavFileStop) {
SetRendererExpectations({{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = true,
}});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_err());
EXPECT_EQ(fuchsia::media::sounds::PlaySoundError::STOPPED, result.err());
play_sound_completed = true;
});
RunLoopUntilIdle();
under_test().StopPlayingSound(0);
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound from a wav file twice.
TEST_F(SoundPlayerTests, WavFileTwiceStopSecond) {
SetRendererExpectations({
{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = true,
},
{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = true,
},
});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
under_test().PlaySound(0, kUsage, [](fuchsia::media::sounds::Player_PlaySound_Result result) {
// Never completes.
EXPECT_TRUE(false);
});
RunLoopUntilIdle();
bool second_play_sound_completed = false;
under_test().PlaySound(
0, kUsage,
[&second_play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_err());
EXPECT_EQ(fuchsia::media::sounds::PlaySoundError::STOPPED, result.err());
second_play_sound_completed = true;
});
RunLoopUntilIdle();
EXPECT_FALSE(second_play_sound_completed);
// Calling |StopPlayingSound| should only stop the second sound from playing. The first should
// continue to block.
under_test().StopPlayingSound(0);
RunLoopUntil([&second_play_sound_completed]() { return second_play_sound_completed; });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Tests that bogus stop requests work (or don't) as expected.
TEST_F(SoundPlayerTests, WavFileBogusStops) {
SetRendererExpectations({{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
// Stop a sound that hasn't been played.
under_test().StopPlayingSound(0);
// Play the sound.
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
// Stop a sound that has already completed.
under_test().StopPlayingSound(0);
// Stop a sound that doesn't exist.
under_test().StopPlayingSound(1);
under_test().RemoveSound(0);
RunLoopUntilIdle();
// Stop a sound that no longer exists.
under_test().StopPlayingSound(0);
RunLoopUntilIdle();
}
// Plays a sound from an ogg/opus file.
TEST_F(SoundPlayerTests, FileOggOpus) {
SetRendererExpectations({{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kOggOpusFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kOggOpusFileChannels,
.frames_per_second = kOggOpusFramesPerSecond,
},
.usage_ = kUsage,
.block_completion_ = false,
}});
under_test().AddSoundFromFile(0, ResourceFile("testfile.ogg"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kOggOpusFileDuration, result.response().duration);
});
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
// Tests that play is deferred until the audio service is ready.
TEST_F(SoundPlayerTests, WhenReady) {
auto [buffer, koid, stream_type] = CreateTestSound(kPayloadSize);
SetBlockWarmup();
SetRendererExpectations({{
.payload_buffer_ = koid,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kPayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ = fidl::Clone(stream_type),
.usage_ = kUsage,
.block_completion_ = false,
}});
// We use |under_test_ptr()| here, because invocation of methods is deferred by deferring
// the channel bind. If we call the implementation directly, we bypass the warmup.
under_test_ptr()->AddSoundBuffer(0, std::move(buffer), std::move(stream_type));
bool play_sound_completed = false;
under_test_ptr()->PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_response());
play_sound_completed = true;
});
RunLoopUntilIdle();
EXPECT_FALSE(play_sound_completed);
ChangeMinLeadTime(zx::msec(10));
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
RunLoopUntil([this]() { return renderers_completed(); });
under_test_ptr()->RemoveSound(0);
RunLoopUntilIdle();
}
// Plays a sound from a wav file. The audio renderer closes the connection on |AddPayloadBuffer|.
TEST_F(SoundPlayerTests, WavFileCloseConnection) {
SetRendererExpectations({{
.payload_buffer_ = ZX_KOID_INVALID,
.packets_ = {{.pts = fuchsia::media::NO_TIMESTAMP,
.payload_buffer_id = 0,
.payload_offset = 0,
.payload_size = kWavFilePayloadSize,
.flags = 0,
.buffer_config = 0,
.stream_segment_id = 0}},
.stream_type_ =
{
.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16,
.channels = kWavFileChannels,
.frames_per_second = kWavFramesPerSecond,
},
.usage_ = kUsage,
.close_on_add_payload_buffer_ = true,
}});
under_test().AddSoundFromFile(0, ResourceFile("sfx.wav"),
[](fuchsia::media::sounds::Player_AddSoundFromFile_Result result) {
EXPECT_TRUE(result.is_response());
EXPECT_EQ(kWavFileDuration, result.response().duration);
});
bool play_sound_completed = false;
under_test().PlaySound(
0, kUsage, [&play_sound_completed](fuchsia::media::sounds::Player_PlaySound_Result result) {
EXPECT_TRUE(result.is_err());
EXPECT_EQ(fuchsia::media::sounds::PlaySoundError::RENDERER_FAILED, result.err());
play_sound_completed = true;
});
RunLoopUntil([&play_sound_completed]() { return play_sound_completed; });
under_test().RemoveSound(0);
RunLoopUntilIdle();
}
} // namespace test
} // namespace soundplayer