blob: 8a58996d2f96981cd11871d350a548ef14c9a90c [file] [log] [blame]
// 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/audio_core/effects_stage_v2.h"
#include <fuchsia/audio/effects/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fzl/vmo-mapper.h>
#include <gmock/gmock.h>
#include "src/lib/fxl/strings/string_printf.h"
#include "src/media/audio/audio_core/packet_queue.h"
#include "src/media/audio/audio_core/testing/fake_packet_queue.h"
#include "src/media/audio/audio_core/testing/packet_factory.h"
#include "src/media/audio/audio_core/testing/threading_model_fixture.h"
#include "src/media/audio/lib/clock/audio_clock.h"
#include "src/media/audio/lib/clock/clone_mono.h"
using testing::Each;
using testing::FloatEq;
using ASF = fuchsia_mediastreams::wire::AudioSampleFormat;
// Arena type used by test code. The initial size does not matter since
// this is a test (it's ok to dynamically allocate).
using Arena = fidl::Arena<512>;
namespace media::audio {
namespace {
// Used when the ReadLockContext is unused by the test.
static media::audio::ReadableStream::ReadLockContext rlctx;
const Format k48k1ChanFloatFormat =
Format::Create(fuchsia::media::AudioStreamType{
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = 1,
.frames_per_second = 48000,
})
.take_value();
const Format k48k2ChanFloatFormat =
Format::Create(fuchsia::media::AudioStreamType{
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = 2,
.frames_per_second = 48000,
})
.take_value();
std::vector<float> as_vec(void* payload, size_t sample_start_idx, size_t sample_end_idx) {
float* p = reinterpret_cast<float*>(payload);
return std::vector<float>(p + sample_start_idx, p + sample_end_idx);
}
fuchsia_mediastreams::wire::AudioFormat DefaultFormatWithChannels(uint32_t channels) {
return {
.sample_format = ASF::kFloat,
.channel_count = channels,
.frames_per_second = 48000,
};
}
Format ToOldFormat(const fuchsia_mediastreams::wire::AudioFormat& new_format) {
FX_CHECK(new_format.sample_format == fuchsia_mediastreams::wire::AudioSampleFormat::kFloat);
return Format::Create(fuchsia::media::AudioStreamType{
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = new_format.channel_count,
.frames_per_second = new_format.frames_per_second,
})
.take_value();
}
zx::vmo CreateVmoOrDie(uint64_t size_bytes) {
zx::vmo vmo;
if (auto status = zx::vmo::create(size_bytes, 0, &vmo); status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to create VMO with size " << size_bytes;
}
return vmo;
}
zx::vmo DupVmoOrDie(const zx::vmo& vmo, zx_rights_t rights) {
zx::vmo dup;
if (auto status = vmo.duplicate(rights, &dup); status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to duplicate VMO with rights 0x" << std::hex << rights;
}
return dup;
}
//
// ConfigOptions
// This struct is a shorthand for specifying a ProcessorConfiguration.
//
struct ConfigOptions {
bool in_place = false;
fuchsia_mem::wire::Range input_buffer = {.offset = 0, .size = 0};
fuchsia_mem::wire::Range output_buffer = {.offset = 0, .size = 0};
fuchsia_mediastreams::wire::AudioFormat input_format = {
.sample_format = ASF::kFloat,
.channel_count = 1,
.frames_per_second = 48000,
};
fuchsia_mediastreams::wire::AudioFormat output_format = {
.sample_format = ASF::kFloat,
.channel_count = 1,
.frames_per_second = 48000,
};
uint64_t latency_frames = 0;
uint64_t ring_out_frames = 0;
uint64_t max_frames_per_call = 0;
uint64_t block_size_frames = 0;
};
void CreateSeparateVmos(ConfigOptions& options, uint64_t input_size_bytes,
uint64_t output_size_bytes) {
options.input_buffer.vmo = CreateVmoOrDie(input_size_bytes);
options.input_buffer.size = input_size_bytes;
options.output_buffer.vmo = CreateVmoOrDie(output_size_bytes);
options.output_buffer.size = output_size_bytes;
}
void CreateSharedVmo(ConfigOptions& options,
uint64_t vmo_size_bytes, // must be large enough for input & output
uint64_t input_offset_bytes, uint64_t input_size_bytes,
uint64_t output_offset_bytes, uint64_t output_size_bytes) {
options.input_buffer.vmo = CreateVmoOrDie(vmo_size_bytes);
options.input_buffer.offset = input_offset_bytes;
options.input_buffer.size = input_size_bytes;
options.output_buffer.vmo = DupVmoOrDie(options.input_buffer.vmo, ZX_RIGHT_SAME_RIGHTS);
options.output_buffer.offset = output_offset_bytes;
options.output_buffer.size = output_size_bytes;
if (input_offset_bytes == output_offset_bytes) {
options.in_place = true;
}
}
ConfigOptions DupConfigOptions(const ConfigOptions& options) {
return ConfigOptions{
.in_place = options.in_place,
.input_buffer =
{
.vmo = DupVmoOrDie(options.input_buffer.vmo, ZX_RIGHT_SAME_RIGHTS),
.offset = options.input_buffer.offset,
.size = options.input_buffer.size,
},
.output_buffer =
{
.vmo = DupVmoOrDie(options.output_buffer.vmo, ZX_RIGHT_SAME_RIGHTS),
.offset = options.output_buffer.offset,
.size = options.output_buffer.size,
},
.input_format =
{
.sample_format = options.input_format.sample_format,
.channel_count = options.input_format.channel_count,
.frames_per_second = options.input_format.frames_per_second,
},
.output_format =
{
.sample_format = options.output_format.sample_format,
.channel_count = options.output_format.channel_count,
.frames_per_second = options.output_format.frames_per_second,
},
.latency_frames = options.latency_frames,
.ring_out_frames = options.ring_out_frames,
.max_frames_per_call = options.max_frames_per_call,
.block_size_frames = options.block_size_frames,
};
}
fuchsia_audio_effects::wire::ProcessorConfiguration MakeProcessorConfig(Arena& arena,
ConfigOptions options) {
fuchsia_audio_effects::wire::ProcessorConfiguration config(arena);
if (options.max_frames_per_call) {
config.set_max_frames_per_call(arena, options.max_frames_per_call);
}
if (options.block_size_frames) {
config.set_block_size_frames(arena, options.block_size_frames);
}
if (auto& buffer = options.input_buffer; buffer.vmo.is_valid()) {
buffer.vmo = DupVmoOrDie(buffer.vmo, ZX_RIGHT_MAP | ZX_RIGHT_READ | ZX_RIGHT_WRITE);
}
if (auto& buffer = options.output_buffer; buffer.vmo.is_valid()) {
buffer.vmo = DupVmoOrDie(buffer.vmo, ZX_RIGHT_MAP | ZX_RIGHT_READ | ZX_RIGHT_WRITE);
}
fidl::VectorView<fuchsia_audio_effects::wire::InputConfiguration> inputs(arena, 1);
inputs[0].Allocate(arena);
inputs[0].set_buffer(arena, std::move(options.input_buffer));
inputs[0].set_format(arena, std::move(options.input_format));
fidl::VectorView<fuchsia_audio_effects::wire::OutputConfiguration> outputs(arena, 1);
outputs[0].Allocate(arena);
outputs[0].set_buffer(arena, std::move(options.output_buffer));
outputs[0].set_format(arena, std::move(options.output_format));
if (options.latency_frames) {
outputs[0].set_latency_frames(arena, options.latency_frames);
}
if (options.ring_out_frames) {
outputs[0].set_ring_out_frames(arena, options.ring_out_frames);
}
config.set_inputs(fidl::ObjectView{arena, inputs});
config.set_outputs(fidl::ObjectView{arena, outputs});
return config;
}
fidl::ServerEnd<fuchsia_audio_effects::Processor> AttachProcessorChannel(
fuchsia_audio_effects::wire::ProcessorConfiguration& config) {
auto endpoints = fidl::CreateEndpoints<fuchsia_audio_effects::Processor>();
if (!endpoints.is_ok()) {
FX_PLOGS(FATAL, endpoints.status_value()) << "failed to construct a zx::channel";
}
config.set_processor(std::move(endpoints->client));
return std::move(endpoints->server);
}
fuchsia_audio_effects::wire::ProcessorConfiguration DefaultGoodProcessorConfig(Arena& arena) {
constexpr auto kBytes = 480 * sizeof(float);
ConfigOptions options;
CreateSeparateVmos(options, kBytes, kBytes);
auto config = MakeProcessorConfig(arena, std::move(options));
auto unused_server_end = AttachProcessorChannel(config);
return config;
}
//
// Processors
//
class BaseProcessor : public fidl::WireServer<fuchsia_audio_effects::Processor> {
public:
BaseProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: binding_(fidl::BindServer(dispatcher, std::move(server_end), this,
[](BaseProcessor* impl, fidl::UnbindInfo info,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end) {
if (!info.is_user_initiated() && !info.is_peer_closed()) {
FX_PLOGS(ERROR, info.status())
<< "Client disconnected unexpectedly: " << info;
}
})),
buffers_(EffectsStageV2::FidlBuffers::Create(options.input_buffer, options.output_buffer)) {
}
float* input_data() const { return reinterpret_cast<float*>(buffers_.input); }
float* output_data() const { return reinterpret_cast<float*>(buffers_.output); }
private:
fidl::ServerBindingRef<fuchsia_audio_effects::Processor> binding_;
EffectsStageV2::FidlBuffers buffers_;
};
//
// Test Cases
//
class EffectsStageV2Test : public testing::ThreadingModelFixture {
public:
void SetUp() override {
testing::ThreadingModelFixture::SetUp();
fidl_loop_.StartThread("fidl-processor-thread");
}
async_dispatcher_t* fidl_dispatcher() const { return fidl_loop_.dispatcher(); }
std::shared_ptr<testing::FakePacketQueue> MakePacketQueue(const Format& format) {
auto timeline_function = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(
TimelineRate(Fixed(format.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())));
return std::make_shared<testing::FakePacketQueue>(
std::vector<fbl::RefPtr<Packet>>(), format, timeline_function,
context().clock_factory()->CreateClientFixed(clock::AdjustableCloneOfMonotonic()));
}
// Every test reads from a FakePacketQueue.
std::tuple<std::unique_ptr<testing::PacketFactory>, std::shared_ptr<testing::FakePacketQueue>,
std::shared_ptr<EffectsStageV2>>
MakeEffectsStage(fuchsia_audio_effects::wire::ProcessorConfiguration config) {
const Format source_format = ToOldFormat(config.inputs()[0].format());
auto packet_factory = std::make_unique<testing::PacketFactory>(dispatcher(), source_format,
zx_system_get_page_size());
auto stream = MakePacketQueue(source_format);
auto effects_stage = EffectsStageV2::Create(std::move(config), stream).take_value();
return std::make_tuple(std::move(packet_factory), std::move(stream), std::move(effects_stage));
}
Arena& arena() { return arena_; }
// By default, the MakeProcessorWith*() functions create input and output buffers
// that are large enough to process at most this many frames.
static constexpr auto kProcessingBufferMaxFrames = 1024;
template <class ProcessorT>
struct ProcessorInfo {
ProcessorT processor;
bool in_place;
fuchsia_audio_effects::wire::ProcessorConfiguration config;
};
template <class T>
ProcessorInfo<T> MakeProcessor(ConfigOptions options);
template <class T>
ProcessorInfo<T> MakeProcessorWithDifferentVmos(ConfigOptions options);
template <class T>
ProcessorInfo<T> MakeProcessorWithSameRange(ConfigOptions options);
template <class T>
ProcessorInfo<T> MakeProcessorWithSameVmoDifferentRanges(ConfigOptions options);
// A simple test case where the source is a packet queue with a single
// packet of the given size. The test makes two ReadLock calls:
//
// 1. ReadLock(0, packet_frames), which should return a buffer of size
// read_lock_buffer_frames containing data processed by the AddOne effect.
//
// 2. ReadLock(packet_frames, packet_frames), which should return std::nullopt.
//
template <class T>
void TestAddOneWithSinglePacket(ProcessorInfo<T> info, int64_t packet_frames = 480,
int64_t read_lock_buffer_frames = 480);
private:
Arena arena_;
async::Loop fidl_loop_{&kAsyncLoopConfigNoAttachToCurrentThread};
};
template <class T>
EffectsStageV2Test::ProcessorInfo<T> EffectsStageV2Test::MakeProcessor(ConfigOptions options) {
if (options.max_frames_per_call) {
FX_CHECK(options.max_frames_per_call < kProcessingBufferMaxFrames);
}
if (options.block_size_frames) {
FX_CHECK(options.block_size_frames < kProcessingBufferMaxFrames);
}
auto config = MakeProcessorConfig(arena(), DupConfigOptions(options));
auto server_end = AttachProcessorChannel(config);
return {
.processor = T(options, std::move(server_end), fidl_dispatcher()),
.in_place = options.in_place,
.config = std::move(config),
};
}
// The processor uses different VMOs for the input and output.
template <class T>
EffectsStageV2Test::ProcessorInfo<T> EffectsStageV2Test::MakeProcessorWithDifferentVmos(
ConfigOptions options) {
const auto kInputChannels = options.input_format.channel_count;
const auto kOutputChannels = options.output_format.channel_count;
const auto kInputBufferBytes = kProcessingBufferMaxFrames * kInputChannels * sizeof(float);
const auto kOutputBufferBytes = kProcessingBufferMaxFrames * kOutputChannels * sizeof(float);
CreateSeparateVmos(options, kInputBufferBytes, kOutputBufferBytes);
return MakeProcessor<T>(std::move(options));
}
// The processor uses the same fuchsia.mem.Range for the input and output.
// This is an in-place update.
template <class T>
EffectsStageV2Test::ProcessorInfo<T> EffectsStageV2Test::MakeProcessorWithSameRange(
ConfigOptions options) {
FX_CHECK(options.input_format.channel_count == options.output_format.channel_count)
<< "In-place updates requires matched input and output channels";
const auto kVmoSamples = kProcessingBufferMaxFrames * options.input_format.channel_count;
const auto kVmoBytes = kVmoSamples * sizeof(float);
CreateSharedVmo(options, kVmoBytes, // VMO size
0, kVmoBytes, // input buffer offset & size
0, kVmoBytes); // output buffer offset & size
return MakeProcessor<T>(std::move(options));
}
// The processor uses non-overlapping ranges of the same VMO for the input and output.
template <class T>
EffectsStageV2Test::ProcessorInfo<T> EffectsStageV2Test::MakeProcessorWithSameVmoDifferentRanges(
ConfigOptions options) {
const auto kInputChannels = options.input_format.channel_count;
const auto kOutputChannels = options.output_format.channel_count;
// To map input and output separately, the offset must be page-aligned.
const auto page_size = zx_system_get_page_size();
const auto kInputBufferBytes = kProcessingBufferMaxFrames * kInputChannels * sizeof(float);
const auto kOutputBufferBytes = kProcessingBufferMaxFrames * kOutputChannels * sizeof(float);
auto input_bytes = fbl::round_up(kInputBufferBytes, page_size);
auto output_bytes = fbl::round_up(kOutputBufferBytes, page_size);
CreateSharedVmo(options, input_bytes + output_bytes, // VMO size
0, kInputBufferBytes, // input buffer offset & size
input_bytes, kOutputBufferBytes); // output buffer offset & size
return MakeProcessor<T>(std::move(options));
}
// Generic test for a processor that adds one to each input sample.
template <class T>
void EffectsStageV2Test::TestAddOneWithSinglePacket(ProcessorInfo<T> info, int64_t packet_frames,
int64_t read_lock_buffer_frames) {
const auto input_channels = info.config.inputs()[0].format().channel_count;
const auto output_channels = info.config.outputs()[0].format().channel_count;
const Format source_format = ToOldFormat(info.config.inputs()[0].format());
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
// Enqueue one packet of the requested size.
const auto packet_duration =
zx::duration(source_format.frames_per_ns().Inverse().Scale(packet_frames));
stream->PushPacket(packet_factory->CreatePacket(1.0, packet_duration));
{
// Read the first packet. Since our effect adds 1.0 to each sample, and we populated the
// packet with 1.0 samples, we expect to see only 2.0 samples in the result.
auto buf = effects_stage->ReadLock(rlctx, Fixed(0), packet_frames);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 0);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), read_lock_buffer_frames);
auto vec = as_vec(buf->payload(), 0, read_lock_buffer_frames * output_channels);
EXPECT_THAT(vec, Each(FloatEq(2.0f)));
// If the update was in-place, the input should have been overwritten.
// Otherwise it should be unchanged.
if (info.in_place) {
auto vec = as_vec(info.processor.input_data(), 0, read_lock_buffer_frames * input_channels);
EXPECT_THAT(vec, Each(FloatEq(2.0f)));
} else {
auto vec = as_vec(info.processor.input_data(), 0, read_lock_buffer_frames * input_channels);
EXPECT_THAT(vec, Each(FloatEq(1.0f)));
}
}
{
// Read the next packet. This should be null, because there are no more packets.
auto buf = effects_stage->ReadLock(rlctx, Fixed(packet_frames), packet_frames);
ASSERT_FALSE(buf);
}
}
//
// AddOneProcessor
// Basic tests for an N chan -> N chan effect
//
class AddOneProcessor : public BaseProcessor {
public:
AddOneProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher),
num_channels_(options.input_format.channel_count) {
FX_CHECK(options.input_format.channel_count == options.output_format.channel_count);
}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
float* input = input_data();
float* output = output_data();
auto num_frames = request->num_frames;
for (; num_frames > 0; num_frames--) {
for (uint32_t k = 0; k < num_channels_; k++, input++, output++) {
*output = (*input) + 1;
}
}
completer.ReplySuccess(fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>());
}
private:
const uint32_t num_channels_;
};
TEST_F(EffectsStageV2Test, AddOneWithOneChanDifferentVmos) {
auto processor_info = MakeProcessorWithDifferentVmos<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
TEST_F(EffectsStageV2Test, AddOneWithTwoChanDifferentVmos) {
auto processor_info = MakeProcessorWithDifferentVmos<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(2),
.output_format = DefaultFormatWithChannels(2),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
TEST_F(EffectsStageV2Test, AddOneWithOneChanSameRange) {
auto processor_info = MakeProcessorWithSameRange<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
TEST_F(EffectsStageV2Test, AddOneWithOneChanSameVmoDifferentRanges) {
auto processor_info = MakeProcessorWithSameVmoDifferentRanges<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
TEST_F(EffectsStageV2Test, AddOneWithSourceOffset) {
constexpr auto kPacketFrames = 480;
constexpr auto kPacketDuration = zx::msec(10);
std::vector<Fixed> source_offsets = {
Fixed(kPacketFrames / 2),
Fixed(kPacketFrames / 2) + ffl::FromRatio(1, 2),
};
for (auto source_offset : source_offsets) {
std::ostringstream os;
os << "source_offset=" << ffl::String::DecRational << source_offset;
SCOPED_TRACE(os.str());
auto info = MakeProcessorWithSameRange<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
});
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
packet_factory->SeekToFrame(source_offset);
stream->PushPacket(packet_factory->CreatePacket(1.0, kPacketDuration));
// Source frame 100.5 is sampled at dest frame 101.
const int64_t dest_offset_frames = source_offset.Ceiling();
{
// Read the first packet. Since the first source packet is offset by source_offset,
// we should read silence from the source followed by 1.0s. The effect adds one to these
// values, so we should see 1.0s followed by 2.0s.
auto buf = effects_stage->ReadLock(rlctx, Fixed(0), kPacketFrames);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 0);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), kPacketFrames);
auto vec1 = as_vec(buf->payload(), 0, dest_offset_frames);
auto vec2 = as_vec(buf->payload(), dest_offset_frames, kPacketFrames);
EXPECT_THAT(vec1, Each(FloatEq(1.0f)));
EXPECT_THAT(vec2, Each(FloatEq(2.0f)));
}
{
// Read the second packet. This should contain the remainder of the 2.0s, followed
// by 1.0s.
auto buf = effects_stage->ReadLock(rlctx, Fixed(kPacketFrames), kPacketFrames);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), kPacketFrames);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), kPacketFrames);
auto vec1 = as_vec(buf->payload(), 0, dest_offset_frames);
auto vec2 = as_vec(buf->payload(), dest_offset_frames, kPacketFrames);
EXPECT_THAT(vec1, Each(FloatEq(2.0f)));
EXPECT_THAT(vec2, Each(FloatEq(1.0f)));
}
{
// Read the next packet. This should be null, because there are no more packets.
auto buf = effects_stage->ReadLock(rlctx, Fixed(2 * kPacketFrames), kPacketFrames);
ASSERT_FALSE(buf);
}
}
}
TEST_F(EffectsStageV2Test, AddOneWithReadLockSmallerThanProcessingBuffer) {
auto info = MakeProcessorWithSameRange<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 720,
.block_size_frames = 720,
});
// Queue one 10ms packet (480 frames).
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
stream->PushPacket(packet_factory->CreatePacket(1.0, zx::msec(10)));
{
// Read the first packet.
auto buf = effects_stage->ReadLock(rlctx, Fixed(0), 480);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 0);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), 480);
// Our effect adds 1.0, and the source packet is 1.0, so the payload should contain all 2.0.
auto vec = as_vec(buf->payload(), 0, 480);
EXPECT_THAT(vec, Each(FloatEq(2.0f)));
}
{
// The source stream does not have a second packet, however when we processed the first
// packet, we processed 720 frames total (480 from the first packet + 240 of silence).
// This ReadLock should return those 240 frames.
auto buf = effects_stage->ReadLock(rlctx, Fixed(480), 480);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 480);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), 240);
// Since the source stream was silent, and our effect adds 1.0, the payload is 1.0.
auto vec = as_vec(buf->payload(), 0, 240);
EXPECT_THAT(vec, Each(FloatEq(1.0f)));
}
{
// Read again where we left off. This should be null, because our cache is exhausted
// and the source has no more data.
auto buf = effects_stage->ReadLock(rlctx, Fixed(720), 480);
ASSERT_FALSE(buf);
}
}
TEST_F(EffectsStageV2Test, AddOneWithReadLockSmallerThanProcessingBufferAndSourceOffset) {
auto info = MakeProcessorWithSameRange<AddOneProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 720,
.block_size_frames = 720,
});
// Queue one 10ms packet (480 frames) starting at frame 720.
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
packet_factory->SeekToFrame(Fixed(720));
stream->PushPacket(packet_factory->CreatePacket(1.0, zx::msec(10)));
{
// This ReadLock will attempt read 720 frames from the source, but the source is empty.
auto buf = effects_stage->ReadLock(rlctx, Fixed(0), 480);
ASSERT_FALSE(buf);
}
{
// This ReadLock should not read anything from the source because we know
// from the prior ReadLock that the source is empty until 720.
auto buf = effects_stage->ReadLock(rlctx, Fixed(480), 240);
ASSERT_FALSE(buf);
}
{
// Now we have data.
auto buf = effects_stage->ReadLock(rlctx, Fixed(720), 480);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 720);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), 480);
// Our effect adds 1.0, and the source packet is 1.0, so the payload should contain all 2.0.
auto vec = as_vec(buf->payload(), 0, 480);
EXPECT_THAT(vec, Each(FloatEq(2.0f)));
}
{
// The source stream ends at frame 720+480=1200, however the last ReadLock processed
// 240 additional frames from the source. This ReadLock should return those 240 frames.
auto buf = effects_stage->ReadLock(rlctx, Fixed(1200), 480);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 1200);
EXPECT_EQ(buf->start().Fraction().raw_value(), 0);
EXPECT_EQ(buf->length(), 240);
// Our effect adds 1.0, and the source range is silent, so the payload should contain all 1.0s.
auto vec = as_vec(buf->payload(), 0, 240);
EXPECT_THAT(vec, Each(FloatEq(1.0f)));
}
{
// Read again where we left off. This should be null, because our cache is exhausted
// and the source has no more data.
auto buf = effects_stage->ReadLock(rlctx, Fixed(1440), 480);
ASSERT_FALSE(buf);
}
}
//
// AddOneAndDupChannelProcessor
// Test rechannelization from 1 chan -> 2 chan
//
// Since we're adding a channel, we can't (easily) write an in-place processor,
// so we don't test that configuration.
//
class AddOneAndDupChannelProcessor : public BaseProcessor {
public:
AddOneAndDupChannelProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher) {
FX_CHECK(options.input_format.channel_count == 1);
FX_CHECK(options.output_format.channel_count == 2);
}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
float* input = input_data();
float* output = output_data();
auto num_frames = request->num_frames;
for (; num_frames > 0; num_frames--, input++, output += 2) {
output[0] = input[0] + 1;
output[1] = input[0] + 1;
}
completer.ReplySuccess(fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>());
}
};
TEST_F(EffectsStageV2Test, AddOneAndDupChannelWithDifferentVmos) {
auto processor_info = MakeProcessorWithDifferentVmos<AddOneAndDupChannelProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(2),
});
}
TEST_F(EffectsStageV2Test, AddOneAndDupChannelWithSameVmoDifferentRanges) {
auto processor_info = MakeProcessorWithSameVmoDifferentRanges<AddOneAndDupChannelProcessor>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(2),
});
}
//
// AddOneAndRemoveChannelProcessor
// Test rechannelization from 2 chan -> 1 chan
//
// Since we're adding a channel, we can't (easily) write an in-place processor,
// so we don't test that configuration.
//
class AddOneAndRemoveChannelProcessor : public BaseProcessor {
public:
AddOneAndRemoveChannelProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher) {
FX_CHECK(options.input_format.channel_count == 2);
FX_CHECK(options.output_format.channel_count == 1);
}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
float* input = input_data();
float* output = output_data();
auto num_frames = request->num_frames;
for (; num_frames > 0; num_frames--, input += 2, output++) {
output[0] = input[0] + 1;
}
completer.ReplySuccess(fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>());
}
};
TEST_F(EffectsStageV2Test, AddOneAndRemoveChannelWithDifferentVmos) {
auto processor_info = MakeProcessorWithDifferentVmos<AddOneAndRemoveChannelProcessor>({
.input_format = DefaultFormatWithChannels(2),
.output_format = DefaultFormatWithChannels(1),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
TEST_F(EffectsStageV2Test, AddOneAndRemoveChannelWithSameVmoDifferentRanges) {
auto processor_info = MakeProcessorWithSameVmoDifferentRanges<AddOneAndRemoveChannelProcessor>({
.input_format = DefaultFormatWithChannels(2),
.output_format = DefaultFormatWithChannels(1),
});
TestAddOneWithSinglePacket(std::move(processor_info));
}
//
// AddOneWithSizeLimits
// Test limits on the size of an input buffer
//
template <uint64_t MaxFramesPerCall, uint64_t BlockSizeFrames>
class AddOneWithSizeLimitsProcessor : public BaseProcessor {
public:
AddOneWithSizeLimitsProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher) {
FX_CHECK(options.input_format.channel_count == 1);
FX_CHECK(options.output_format.channel_count == 1);
}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
auto num_frames = request->num_frames;
if (MaxFramesPerCall > 0) {
FX_CHECK(num_frames <= MaxFramesPerCall)
<< "expected at most " << MaxFramesPerCall << " frames, got " << num_frames;
}
if (BlockSizeFrames > 0) {
FX_CHECK(num_frames % BlockSizeFrames == 0)
<< "expected multiple of " << BlockSizeFrames << " frames, got " << num_frames;
}
float* input = input_data();
float* output = output_data();
for (uint64_t k = 0; k < num_frames; k++) {
output[k] = input[k] + 1;
}
completer.ReplySuccess(fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>());
}
};
TEST_F(EffectsStageV2Test, AddOneWithSizeLimitsMaxSizeWithoutBlockSize) {
// First ReadLock returns 31 frames.
auto processor_info = MakeProcessorWithDifferentVmos<AddOneWithSizeLimitsProcessor<31, 0>>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 31,
.block_size_frames = 0,
});
TestAddOneWithSinglePacket(std::move(processor_info), 480 /* = packet_frames */,
31 /* = read_lock_buffer_frames */);
}
TEST_F(EffectsStageV2Test, AddOneWithSizeLimitsBlockSizeWithoutMax) {
// First ReadLock returns floor(kProcessingBufferMaxFrames/7)*7 = 1022 frames.
auto processor_info = MakeProcessorWithDifferentVmos<AddOneWithSizeLimitsProcessor<0, 7>>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 0,
.block_size_frames = 7,
});
TestAddOneWithSinglePacket(std::move(processor_info),
kProcessingBufferMaxFrames /* = packet_frames */,
1022 /* = read_lock_buffer_frames */);
}
TEST_F(EffectsStageV2Test, AddOneWithSizeLimitsBlockSizeEqualsMax) {
// First ReadLock returns 8 frames.
auto processor_info = MakeProcessorWithDifferentVmos<AddOneWithSizeLimitsProcessor<8, 8>>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 8,
.block_size_frames = 8,
});
TestAddOneWithSinglePacket(std::move(processor_info), 480 /* = packet_frames */,
8 /* = read_lock_buffer_frames */);
}
TEST_F(EffectsStageV2Test, AddOneWithSizeLimitsBlockSizeLessThanMaxNotDivisible) {
// First ReadLock returns 8*3 = 24 frames.
auto processor_info = MakeProcessorWithDifferentVmos<AddOneWithSizeLimitsProcessor<31, 8>>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 31,
.block_size_frames = 8,
});
TestAddOneWithSinglePacket(std::move(processor_info), 480 /* = packet_frames */,
24 /* = read_lock_buffer_frames */);
}
TEST_F(EffectsStageV2Test, AddOneWithSizeLimitsBlockSizeLessThanMaxDivisible) {
// First ReadLock returns 32 frames.
auto processor_info = MakeProcessorWithDifferentVmos<AddOneWithSizeLimitsProcessor<32, 8>>({
.input_format = DefaultFormatWithChannels(1),
.output_format = DefaultFormatWithChannels(1),
.max_frames_per_call = 32,
.block_size_frames = 8,
});
TestAddOneWithSinglePacket(std::move(processor_info), 480 /* = packet_frames */,
32 /* = read_lock_buffer_frames */);
}
//
// CheckOptionsProcessor
// Test that ProcessOptions is set correctly.
//
class CheckOptionsProcessor : public BaseProcessor {
public:
static constexpr float kExpectedAppliedGainDb = -25.0f;
static constexpr uint32_t kExpectedUsageMask =
StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA),
StreamUsage::WithRenderUsage(RenderUsage::INTERRUPTION)})
.mask();
CheckOptionsProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher) {}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
ASSERT_EQ(request->options.total_applied_gain_db_per_input().count(), 1u);
EXPECT_EQ(request->options.total_applied_gain_db_per_input()[0], kExpectedAppliedGainDb);
ASSERT_EQ(request->options.usage_mask_per_input().count(), 1u);
ASSERT_EQ(request->options.usage_mask_per_input()[0], kExpectedUsageMask);
completer.ReplySuccess(fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>());
}
};
TEST_F(EffectsStageV2Test, PassOptions) {
constexpr auto kPacketFrames = 480;
constexpr auto kPacketDuration = zx::msec(10);
auto info = MakeProcessorWithDifferentVmos<CheckOptionsProcessor>({});
// Enqueue one packet in the source packet queue.
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
stream->PushPacket(packet_factory->CreatePacket(1.0, kPacketDuration));
// Ensure that ULTRASOUND is removed.
const auto usage_mask =
CheckOptionsProcessor::kExpectedUsageMask | (1 << static_cast<int>(RenderUsage::ULTRASOUND));
// Set options.
stream->set_gain_db(CheckOptionsProcessor::kExpectedAppliedGainDb);
stream->set_usage_mask(StreamUsageMask::FromMask(usage_mask));
// Call ReadLock. Validate it returns a buffer, which ensures we invoked the effects processor.
auto buf = effects_stage->ReadLock(rlctx, Fixed(0), kPacketFrames);
ASSERT_TRUE(buf);
}
//
// ReturnMetricsProcessor
// Test an effect that returns metrics.
//
class ReturnMetricsProcessor : public BaseProcessor {
public:
ReturnMetricsProcessor(const ConfigOptions& options,
fidl::ServerEnd<fuchsia_audio_effects::Processor> server_end,
async_dispatcher_t* dispatcher)
: BaseProcessor(options, std::move(server_end), dispatcher) {}
void Process(ProcessRequestView request, ProcessCompleter::Sync& completer) {
completer.ReplySuccess(
fidl::VectorView<fuchsia_audio_effects::wire::ProcessMetrics>::FromExternal(*metrics_));
}
void set_metrics(std::vector<fuchsia_audio_effects::wire::ProcessMetrics>* m) { metrics_ = m; }
private:
std::vector<fuchsia_audio_effects::wire::ProcessMetrics>* metrics_ = nullptr;
};
TEST_F(EffectsStageV2Test, Metrics) {
std::vector<fuchsia_audio_effects::wire::ProcessMetrics> expected_metrics(3);
expected_metrics[0].Allocate(arena());
expected_metrics[0].set_name(arena(), "EffectsStageV2::Process");
expected_metrics[1].Allocate(arena());
expected_metrics[1].set_name(arena(), "stage1");
expected_metrics[1].set_wall_time(arena(), 100);
expected_metrics[1].set_cpu_time(arena(), 101);
expected_metrics[1].set_queue_time(arena(), 102);
expected_metrics[2].Allocate(arena());
expected_metrics[2].set_name(arena(), "stage2");
expected_metrics[2].set_wall_time(arena(), 200);
expected_metrics[2].set_cpu_time(arena(), 201);
expected_metrics[2].set_queue_time(arena(), 201);
constexpr auto kPacketFrames = 480;
constexpr auto kPacketDuration = zx::msec(10);
auto info = MakeProcessorWithDifferentVmos<ReturnMetricsProcessor>({});
info.processor.set_metrics(&expected_metrics);
// Enqueue one packet in the source packet queue.
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(info.config));
stream->PushPacket(packet_factory->CreatePacket(1.0, kPacketDuration));
// Call ReadLock and validate the metrics.
ReadableStream::ReadLockContext ctx;
auto buf = effects_stage->ReadLock(ctx, Fixed(0), kPacketFrames);
ASSERT_TRUE(buf);
EXPECT_EQ(ctx.per_stage_metrics().size(), expected_metrics.size());
for (size_t k = 0; k < expected_metrics.size(); k++) {
if (k >= ctx.per_stage_metrics().size()) {
break;
}
SCOPED_TRACE(fxl::StringPrintf("metrics[%lu]", k));
auto& metrics = ctx.per_stage_metrics()[k];
EXPECT_EQ(static_cast<std::string_view>(metrics.name), expected_metrics[k].name().get());
if (k == 0) {
continue;
}
EXPECT_EQ(metrics.wall_time.to_nsecs(), expected_metrics[k].wall_time());
EXPECT_EQ(metrics.cpu_time.to_nsecs(), expected_metrics[k].cpu_time());
EXPECT_EQ(metrics.queue_time.to_nsecs(), expected_metrics[k].queue_time());
EXPECT_EQ(metrics.page_fault_time.to_nsecs(), 0);
EXPECT_EQ(metrics.kernel_lock_contention_time.to_nsecs(), 0);
}
}
//
// Test that latency affects the stream timeline
//
TEST_F(EffectsStageV2Test, LatencyAffectStreamTimelineAndLeadTime) {
auto config = DefaultGoodProcessorConfig(arena());
config.outputs()[0].set_latency_frames(arena(), 13);
// Create a source packet queue.
auto [packet_factory, stream, effects_stage] = MakeEffectsStage(std::move(config));
// Setup the timeline function so that time 0 aligns to frame 0 with a rate corresponding to the
// streams format.
stream->timeline_function()->Update(TimelineFunction(TimelineRate(
Fixed(k48k2ChanFloatFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())));
// Since our effect introduces 13 frames of latency, the incoming source frame at time 0 can only
// emerge from the effect in output frame 13.
// Conversely, output frame 0 was produced based on the source frame at time -13.
auto ref_clock_to_output_frac_frame =
effects_stage->ref_time_to_frac_presentation_frame().timeline_function;
EXPECT_EQ(Fixed::FromRaw(ref_clock_to_output_frac_frame.Apply(0)), Fixed(13));
// Similarly, at the time we produce output frame 0, we had to draw upon the source frame from
// time -13. Use a fuzzy compare to allow for slight rounding errors.
int64_t frame_13_time = (zx::sec(-13).to_nsecs()) / k48k2ChanFloatFormat.frames_per_second();
auto frame_13_frac_frames =
Fixed::FromRaw(ref_clock_to_output_frac_frame.Apply(frame_13_time)).Absolute();
EXPECT_LE(frame_13_frac_frames.raw_value(), 1);
// Check our initial lead time is only the effect latency.
auto effect_lead_time =
zx::duration(zx::sec(13).to_nsecs() / k48k2ChanFloatFormat.frames_per_second());
EXPECT_EQ(effect_lead_time, effects_stage->GetPresentationDelay());
// Check that setting an external min lead time includes our internal lead time.
const auto external_lead_time = zx::usec(100);
effects_stage->SetPresentationDelay(external_lead_time);
EXPECT_EQ(effect_lead_time + external_lead_time, effects_stage->GetPresentationDelay());
}
//
// Error cases in EffectsStageV2::Create
//
TEST_F(EffectsStageV2Test, CreateSuccess) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_TRUE(result.is_ok()) << "failed with status: " << result.error();
}
TEST_F(EffectsStageV2Test, CreateFailsMissingProcessorHandle) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.clear_processor();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsNoInputs) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.clear_inputs();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsNoOutputs) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.clear_outputs();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsTooManyInputs) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
std::vector<fuchsia_audio_effects::wire::InputConfiguration> inputs(2);
inputs[0] = config.inputs()[0];
config.inputs() =
fidl::VectorView<fuchsia_audio_effects::wire::InputConfiguration>::FromExternal(inputs);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsTooManyOutputs) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
std::vector<fuchsia_audio_effects::wire::OutputConfiguration> outputs(2);
outputs[0] = config.outputs()[0];
config.outputs() =
fidl::VectorView<fuchsia_audio_effects::wire::OutputConfiguration>::FromExternal(outputs);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputMissingFormat) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].clear_format();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputMissingFormat) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.outputs()[0].clear_format();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputFormatNotFloat) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].format().sample_format = ASF::kUnsigned8;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputFormatNotFloat) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.outputs()[0].format().sample_format = ASF::kUnsigned8;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputOutputFpsMismatch) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].format().frames_per_second = 48000;
config.outputs()[0].format().frames_per_second = 44100;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputMissingBuffer) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].clear_buffer();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputMissingBuffer) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.outputs()[0].clear_buffer();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferEmpty) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].buffer().size = 0;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferEmpty) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.outputs()[0].buffer().size = 0;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferVmoInvalid) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].buffer().vmo = zx::vmo();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferVmoInvalid) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.outputs()[0].buffer().vmo = zx::vmo();
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferVmoMustBeMappable) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.inputs()[0].buffer();
ASSERT_EQ(ZX_OK, buffer.vmo.replace(ZX_RIGHT_WRITE, &buffer.vmo));
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferVmoMustBeMappable) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.outputs()[0].buffer();
ASSERT_EQ(ZX_OK, buffer.vmo.replace(ZX_RIGHT_READ, &buffer.vmo));
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferVmoMustBeWritable) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.inputs()[0].buffer();
ASSERT_EQ(ZX_OK, buffer.vmo.replace(ZX_RIGHT_MAP, &buffer.vmo));
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferVmoMustBeReadable) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.outputs()[0].buffer();
ASSERT_EQ(ZX_OK, buffer.vmo.replace(ZX_RIGHT_MAP, &buffer.vmo));
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferVmoTooSmall) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.inputs()[0].buffer();
uint64_t vmo_size;
ASSERT_EQ(ZX_OK, buffer.vmo.get_size(&vmo_size));
buffer.size = vmo_size + 1;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok()); // too large by 1 byte
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferVmoTooSmall) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.outputs()[0].buffer();
uint64_t vmo_size;
ASSERT_EQ(ZX_OK, buffer.vmo.get_size(&vmo_size));
buffer.size = vmo_size + 1; // too large by 1 byte
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferOffsetTooLarge) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.inputs()[0].buffer();
uint64_t vmo_size;
ASSERT_EQ(ZX_OK, buffer.vmo.get_size(&vmo_size));
buffer.offset = vmo_size - buffer.size + 1; // too large by 1 byte
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferOffsetTooLarge) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& buffer = config.outputs()[0].buffer();
uint64_t vmo_size;
ASSERT_EQ(ZX_OK, buffer.vmo.get_size(&vmo_size));
buffer.offset = vmo_size - buffer.size + 1; // too large by 1 byte
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputBufferTooSmall) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.set_max_frames_per_call(arena(), 10);
config.inputs()[0].buffer().size = 9 * sizeof(float);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferTooSmall) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.set_max_frames_per_call(arena(), 10);
config.outputs()[0].buffer().size = 9 * sizeof(float);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsOutputBufferPartiallyOverlapsInputBuffer) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto& input_buffer = config.inputs()[0].buffer();
auto& output_buffer = config.outputs()[0].buffer();
input_buffer.vmo = CreateVmoOrDie(1024);
input_buffer.offset = 0;
input_buffer.size = 256;
output_buffer.vmo = DupVmoOrDie(input_buffer.vmo, ZX_RIGHT_SAME_RIGHTS);
output_buffer.offset = 255;
output_buffer.size = 256;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsBlockSizeTooBig) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto max_frames = config.inputs()[0].buffer().size / sizeof(float);
config.set_block_size_frames(arena(), max_frames + 1);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsMaxFramesPerCallTooBig) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
auto max_frames = config.inputs()[0].buffer().size / sizeof(float);
config.set_max_frames_per_call(arena(), max_frames + 1);
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputSampleFormatDoesNotMatchSource) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].format().sample_format = ASF::kUnsigned8;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputChannelCountDoesNotMatchSource) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].format().channel_count = 2;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
TEST_F(EffectsStageV2Test, CreateFailsInputFpsDoesNotMatchSource) {
auto config = DefaultGoodProcessorConfig(arena());
auto stream = MakePacketQueue(k48k1ChanFloatFormat);
config.inputs()[0].format().frames_per_second = 44100;
auto result = EffectsStageV2::Create(std::move(config), stream);
EXPECT_FALSE(result.is_ok());
}
//
// FidlBuffers
//
TEST(EffectsStageV2BuffersTest, CreateSeparate) {
ConfigOptions options;
CreateSeparateVmos(options, 128, 256);
auto buffers = EffectsStageV2::FidlBuffers::Create(options.input_buffer, options.output_buffer);
ASSERT_NE(buffers.input, nullptr);
ASSERT_NE(buffers.output, nullptr);
EXPECT_EQ(buffers.input_size, options.input_buffer.size);
EXPECT_EQ(buffers.output_size, options.output_buffer.size);
// Must not overlap.
auto input_start = reinterpret_cast<char*>(buffers.input);
auto output_start = reinterpret_cast<char*>(buffers.output);
EXPECT_TRUE(input_start + buffers.input_size <= output_start ||
output_start + buffers.output_size <= input_start)
<< "input_start=0x" << buffers.input << ", input_size=" << buffers.input_size
<< "output_start=0x" << buffers.output << ", output_size=" << buffers.output_size;
// Must be readable and writable.
// These loops should crash if not readable nor writable.
for (char* p = input_start; p < input_start + buffers.input_size; p++) {
(*p) += 1;
}
for (char* p = output_start; p < output_start + buffers.output_size; p++) {
(*p) += 1;
}
}
TEST(EffectsStageV2BuffersTest, CreateSharedOverlappingZeroOffsets) {
ConfigOptions options;
CreateSharedVmo(options,
10, // vmo_size_bytes
0, 10, // input_offset_bytes, input_size_bytes
0, 10); // output_offset_bytes output_size_bytes
auto buffers = EffectsStageV2::FidlBuffers::Create(options.input_buffer, options.output_buffer);
ASSERT_NE(buffers.input, nullptr);
ASSERT_NE(buffers.output, nullptr);
EXPECT_EQ(buffers.input_size, options.input_buffer.size);
EXPECT_EQ(buffers.output_size, options.output_buffer.size);
// Must be overlapping.
auto input_start = reinterpret_cast<char*>(buffers.input);
auto output_start = reinterpret_cast<char*>(buffers.output);
EXPECT_EQ(input_start, output_start)
<< "input_start=0x" << buffers.input << ", input_size=" << buffers.input_size
<< "output_start=0x" << buffers.output << ", output_size=" << buffers.output_size;
// Must be readable and writable.
// This loop should crash if not readable nor writable.
for (char* p = input_start; p < input_start + buffers.input_size; p++) {
(*p) += 1;
}
}
TEST(EffectsStageV2BuffersTest, CreateSharedOverlappingNonzeroOffsets) {
// Offsets must be a multiple of the page size.
const auto page_size = zx_system_get_page_size();
ConfigOptions options;
CreateSharedVmo(options,
page_size * 2, // vmo_size_bytes
page_size, page_size, // input_offset_bytes, input_size_bytes
page_size, page_size); // output_offset_bytes output_size_bytes
auto buffers = EffectsStageV2::FidlBuffers::Create(options.input_buffer, options.output_buffer);
ASSERT_NE(buffers.input, nullptr);
ASSERT_NE(buffers.output, nullptr);
EXPECT_EQ(buffers.input_size, options.input_buffer.size);
EXPECT_EQ(buffers.output_size, options.output_buffer.size);
// Must be overlapping.
auto input_start = reinterpret_cast<char*>(buffers.input);
auto output_start = reinterpret_cast<char*>(buffers.output);
EXPECT_EQ(input_start, output_start)
<< "input_start=0x" << buffers.input << ", input_size=" << buffers.input_size
<< "output_start=0x" << buffers.output << ", output_size=" << buffers.output_size;
// Must be readable and writable.
// This loop should crash if not readable nor writable.
for (char* p = input_start; p < input_start + buffers.input_size; p++) {
(*p) += 1;
}
}
TEST(EffectsStageV2BuffersTest, CreateSharedNonOverlapping) {
// Offsets must be a multiple of the page size.
const auto page_size = zx_system_get_page_size();
ConfigOptions options;
CreateSharedVmo(options,
page_size * 2, // vmo_size_bytes
0, page_size, // input_offset_bytes, input_size_bytes
page_size, page_size); // output_offset_bytes output_size_bytes
auto buffers = EffectsStageV2::FidlBuffers::Create(options.input_buffer, options.output_buffer);
ASSERT_NE(buffers.input, nullptr);
ASSERT_NE(buffers.output, nullptr);
EXPECT_EQ(buffers.input_size, options.input_buffer.size);
EXPECT_EQ(buffers.output_size, options.output_buffer.size);
// Must be adjacent.
auto input_start = reinterpret_cast<char*>(buffers.input);
auto output_start = reinterpret_cast<char*>(buffers.output);
EXPECT_EQ(input_start + buffers.input_size, output_start)
<< "input_start=0x" << buffers.input << ", input_size=" << buffers.input_size
<< "output_start=0x" << buffers.output << ", output_size=" << buffers.output_size;
// Must be readable and writable.
// These loops should crash if not readable nor writable.
for (char* p = input_start; p < input_start + buffers.input_size; p++) {
(*p) += 1;
}
for (char* p = output_start; p < output_start + buffers.output_size; p++) {
(*p) += 1;
}
}
} // namespace
} // namespace media::audio