blob: 1358fba14b3eab0e2a4d1011b2d367b4280d9603 [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 "src/media/audio/lib/test/hermetic_golden_test.h"
#include <fuchsia/media/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/media/audio/lib/analysis/analysis.h"
#include "src/media/audio/lib/analysis/generators.h"
#include "src/media/audio/lib/format/audio_buffer.h"
#include "src/media/audio/lib/test/comparators.h"
#include "src/media/audio/lib/test/hermetic_pipeline_test.h"
#include "src/media/audio/lib/test/renderer_shim.h"
#include "src/media/audio/lib/wav/wav_writer.h"
using ASF = fuchsia::media::AudioSampleFormat;
namespace media::audio::test {
namespace {
// Using a macro here to preserve line numbers in the test error messages.
#define EXPECT_WITHIN_RELATIVE_ERROR(actual, expected, threshold) \
do { \
auto label = \
fxl::StringPrintf("\n %s = %f\n %s = %f", #actual, actual, #expected, expected); \
EXPECT_NE(expected, 0) << label; \
if (expected != 0) { \
auto err = abs(actual - expected) / expected; \
EXPECT_LE(err, threshold) << label; \
} \
} while (0)
// WaveformTestRunner wraps some methods used by RunTestCase.
template <ASF InputFormat, ASF OutputFormat>
class WaveformTestRunner {
public:
WaveformTestRunner(const HermeticGoldenTest::TestCase<InputFormat, OutputFormat>& tc) : tc_(tc) {}
void CompareRMSE(AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected);
void CompareRMS(AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected);
void CompareFreqs(AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected,
std::vector<size_t> hz_signals);
void ExpectSilence(AudioBufferSlice<OutputFormat> actual);
private:
const HermeticGoldenTest::TestCase<InputFormat, OutputFormat>& tc_;
};
template <ASF InputFormat, ASF OutputFormat>
void WaveformTestRunner<InputFormat, OutputFormat>::CompareRMSE(
AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected) {
CompareAudioBuffers(actual, expected,
{
.max_relative_error = tc_.max_relative_rms_error,
.test_label = "check data",
.num_frames_per_packet = expected.format().frames_per_second() / 1000 *
RendererShimImpl::kPacketMs,
});
}
template <ASF InputFormat, ASF OutputFormat>
void WaveformTestRunner<InputFormat, OutputFormat>::CompareRMS(
AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected) {
auto expected_rms = MeasureAudioRMS(expected);
auto actual_rms = MeasureAudioRMS(actual);
EXPECT_WITHIN_RELATIVE_ERROR(actual_rms, expected_rms, tc_.max_relative_rms);
}
template <ASF InputFormat, ASF OutputFormat>
void WaveformTestRunner<InputFormat, OutputFormat>::CompareFreqs(
AudioBufferSlice<OutputFormat> actual, AudioBufferSlice<OutputFormat> expected,
std::vector<size_t> hz_signals) {
ASSERT_EQ(expected.NumFrames(), actual.NumFrames());
// FFT requires a power-of-2 number of samples.
auto expected_buf = PadToNearestPower2(expected);
auto actual_buf = PadToNearestPower2(actual);
expected = AudioBufferSlice(&expected_buf);
actual = AudioBufferSlice(&actual_buf);
// Translate hz to periods.
std::unordered_set<size_t> freqs_in_unit_periods;
for (auto hz : hz_signals) {
ASSERT_GT(hz, 0u);
// Frames per period at frequency hz.
size_t fpp = expected.format().frames_per_second() / hz;
// If there are an integer number of periods, we can precisely measure the magnitude at hz.
// Otherwise, the magnitude will be smeared between the two adjacent integers.
size_t periods = expected.NumFrames() / fpp;
freqs_in_unit_periods.insert(periods);
if (expected.NumFrames() % fpp > 0) {
freqs_in_unit_periods.insert(periods + 1);
}
}
auto expected_result = MeasureAudioFreqs(expected, freqs_in_unit_periods);
auto actual_result = MeasureAudioFreqs(actual, freqs_in_unit_periods);
EXPECT_WITHIN_RELATIVE_ERROR(actual_result.total_magn_signal, expected_result.total_magn_signal,
tc_.max_relative_signal_error);
EXPECT_WITHIN_RELATIVE_ERROR(actual_result.total_magn_other, expected_result.total_magn_other,
tc_.max_relative_other_error);
for (auto periods : freqs_in_unit_periods) {
auto hz = static_cast<double>(periods) *
static_cast<double>(expected.format().frames_per_second()) /
static_cast<double>(expected.NumFrames());
SCOPED_TRACE(testing::Message() << "Frequency " << periods << " periods, " << hz << " hz");
EXPECT_WITHIN_RELATIVE_ERROR(actual_result.magnitudes[periods],
expected_result.magnitudes[periods],
tc_.max_relative_signal_error);
EXPECT_WITHIN_RELATIVE_ERROR(actual_result.phases[periods], expected_result.phases[periods],
tc_.max_relative_signal_phase_error);
}
}
template <ASF InputFormat, ASF OutputFormat>
void WaveformTestRunner<InputFormat, OutputFormat>::ExpectSilence(
AudioBufferSlice<OutputFormat> actual) {
CompareAudioBuffers(actual, AudioBufferSlice<OutputFormat>(),
{
.test_label = "check silence",
.num_frames_per_packet = actual.format().frames_per_second() / 1000 *
RendererShimImpl::kPacketMs,
});
}
} // namespace
template <ASF InputFormat, ASF OutputFormat>
void HermeticGoldenTest::Run(const HermeticGoldenTest::TestCase<InputFormat, OutputFormat>& tc) {
WaveformTestRunner<InputFormat, OutputFormat> runner(tc);
const auto& input = tc.input;
const auto& expected_output = tc.expected_output;
auto device = CreateOutput(AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS, expected_output.format(),
AddSlackToOutputFrames(expected_output.NumFrames()), std::nullopt,
tc.pipeline.output_device_gain_db);
auto renderer = CreateAudioRenderer(input.format(), input.NumFrames());
// Render the input at a time such that the first frame of audio will be rendered into
// the first frame of the ring buffer. We need to synchronize with the ring buffer, then
// leave some silence to account for ring in.
auto packets = renderer->AppendPackets({&input});
auto min_start_time = zx::clock::get_monotonic() + renderer->min_lead_time() + zx::msec(20);
auto start_time =
device->NextSynchronizedTimestamp(min_start_time) +
zx::nsec(renderer->format().frames_per_ns().Inverse().Scale(tc.pipeline.neg_filter_width));
renderer->Play(this, start_time, 0);
renderer->WaitForPackets(this, packets);
// The ring buffer should contain the expected output followed by silence.
auto ring_buffer = device->SnapshotRingBuffer();
auto num_data_frames = expected_output.NumFrames();
auto output_data = AudioBufferSlice(&ring_buffer, 0, num_data_frames);
auto output_silence = AudioBufferSlice(&ring_buffer, num_data_frames, device->frame_count());
if (save_input_and_output_files_) {
WriteWavFile<InputFormat>(tc.test_name, "input", &input);
WriteWavFile<OutputFormat>(tc.test_name, "ring_buffer", &ring_buffer);
WriteWavFile<OutputFormat>(tc.test_name, "output", output_data);
WriteWavFile<OutputFormat>(tc.test_name, "expected_output", &expected_output);
}
runner.CompareRMSE(&expected_output, output_data);
runner.ExpectSilence(output_silence);
for (size_t chan = 0; chan < expected_output.format().channels(); chan++) {
auto expected_chan = AudioBufferSlice<OutputFormat>(&expected_output).GetChannel(chan);
auto output_chan = output_data.GetChannel(chan);
SCOPED_TRACE(testing::Message() << "Channel " << chan);
runner.CompareRMS(&output_chan, &expected_chan);
runner.CompareFreqs(&output_chan, &expected_chan, tc.frequencies_hz_to_analyze);
}
}
// Explicitly instantiate (almost) all possible implementations.
// We intentionally don't instantiate implementations with OutputFormat = UNSIGNED_8
// because such hardware is no longer in use, therefore it's not worth testing.
#define INSTANTIATE(InputFormat, OutputFormat) \
template void HermeticGoldenTest::Run<InputFormat, OutputFormat>( \
const TestCase<InputFormat, OutputFormat>& tc);
INSTANTIATE(ASF::UNSIGNED_8, ASF::SIGNED_16)
INSTANTIATE(ASF::UNSIGNED_8, ASF::SIGNED_24_IN_32)
INSTANTIATE(ASF::UNSIGNED_8, ASF::FLOAT)
INSTANTIATE(ASF::SIGNED_16, ASF::SIGNED_16)
INSTANTIATE(ASF::SIGNED_16, ASF::SIGNED_24_IN_32)
INSTANTIATE(ASF::SIGNED_16, ASF::FLOAT)
INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::SIGNED_16)
INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::SIGNED_24_IN_32)
INSTANTIATE(ASF::SIGNED_24_IN_32, ASF::FLOAT)
INSTANTIATE(ASF::FLOAT, ASF::SIGNED_16)
INSTANTIATE(ASF::FLOAT, ASF::SIGNED_24_IN_32)
INSTANTIATE(ASF::FLOAT, ASF::FLOAT)
} // namespace media::audio::test