blob: 7c586734cdc35808ddd0777522624d4f382e67b8 [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_fidelity_test.h"
#include <fuchsia/media/cpp/fidl.h>
#include <fuchsia/thermal/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <zircon/types.h>
#include <array>
#include <cmath>
#include <iomanip>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <test/thermal/cpp/fidl.h>
#include "src/lib/fxl/strings/concatenate.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/clock/clone_mono.h"
#include "src/media/audio/lib/format/audio_buffer.h"
#include "src/media/audio/lib/test/renderer_shim.h"
using ASF = fuchsia::media::AudioSampleFormat;
namespace media::audio::test {
// Value related to cmdline flags
//
// If --save-input-and-output is specified, saving input|output files for every test frequency
// consumes too much on-device storage. Just save the files for this specified frequency.
constexpr int32_t kFrequencyForSavedWavFiles = 1000;
//
// Custom build-time flags
//
// For normal CQ operation, the below should be FALSE.
//
// Debug positioning and values of the renderer's input buffer, by showing certain sections.
constexpr bool kDisplayInputBuffer = false;
// Debug positioning and values of the output ring buffer snapshot, by showing certain sections.
constexpr bool kDisplayOutputBuffer = false;
// If debugging input/output ring buffer contents, show sections for ALL frequencies.
constexpr bool kDisplayBuffersAtAllFrequencies = false;
// Retain/display worst-case single-test-case results in a looped run. Used to update limits.
constexpr bool kRetainWorstCaseResults = false;
// Show results at test-end in tabular form, for copy/compare to results vectors.
constexpr bool kDisplaySummaryResults = false;
//
// For normal CQ operation, the below should be TRUE. (They aid in debugging sporadic CQ issues.)
//
// Displaying results on-the-fly helps correlate an UNDERFLOW with the affected frequency.
constexpr bool kDisplayInProgressResults = true;
// On significant FR/SiNAD failure, display relevant output buffer sections or analysis metadata.
constexpr bool kDisplayOutputBufferOnFailure = true;
constexpr bool kDisplayAnalysisDataOnFailure = true;
// Related configuration
//
// How many input frames on either side of "positions of interest" to display
constexpr int64_t kInputDisplayWindow = 16;
// How many output frames on either side of "positions of interest" to display
constexpr int64_t kOutputDisplayWindow = 48;
// Displaying a larger set of "beginning of signal" and "end of signal" frames helps us diagnose
// output delays and incorrect pipeline widths.
constexpr int64_t kOutputAdditionalSignalStartDisplayWindow = 128;
constexpr int64_t kOutputAdditionalSignalEndDisplayWindow = 80;
// If not displaying buffers at all frequencies, only show this one (applies to input and output).
// 1 kHz is a reasonable mid-range input for saved files, debugging, and single-frequency tests.
constexpr int32_t kFrequencyForBufferDebugging = 1000;
// Dumping buffers for every failure may be too verbose. Only dump ones where FR fails by 20 dB...
constexpr double kDisplayOutputBufferOnFailureFreqRespDbTolerance = 20.0;
// ... or SiNAD fails by 20 dB.
constexpr double kDisplayOutputBufferOnFailureSinadDbTolerance = 20.0;
// Only display analysis data (significant frequency bins) if FR or SiNAD fail by 20 dB.
constexpr double kDisplayAnalysisDataOnFailureDbTolerance = 20.0;
//
// Consts related to fidelity testing thresholds
//
// The power-of-two size of our spectrum analysis buffer, and our frequency spectrum set.
constexpr int64_t kFreqTestBufSize = 65536;
// When testing fidelity, we compare actual measured dB to expected dB. These tests are designed
// to pass if 'actual >= expected', OR less but within the following tolerance. This tolerance
// also sets the digits of precision for 'expected' values, when stored or displayed.
constexpr double kFidelityDbTolerance = 0.001;
// If kDisplayAnalysisDataOnFailureDbTolerance, display freq bins with magnitude >= this val.
constexpr double kMinAnalysisMagnitudeToDisplay = 1e-4;
// For each test_name|channel, we maintain two results arrays: Frequency Response and
// Signal-to-Noise-and-Distortion (SiNAD). A map of array results is saved as a function-local
// static variable. If kRetainWorstCaseResults is set, we persist results across repeated test runs.
//
// Note: two test cases must not collide on the same test_name/channel. Thus, test cases must take
// care not to reuse test_name upon copy-and-paste.
struct ResultsIndex {
std::string test_name;
int32_t channel;
bool operator<(const ResultsIndex& rhs) const {
return std::tie(test_name, channel) < std::tie(rhs.test_name, rhs.channel);
}
};
// When displaying in-progress results, storing worst-case results and comparing results to required
// minimum thresholds, we use the actual level_db and sinad_db values as calculated.
// Purely when displaying summary results, we cap level_db and sinad_db to 0 and 160 respectively.
//
// Why? Summary results are usually shown for copying to results vectors (e.g. fidelity_results.cc)
// to be used for future pass/fail comparisons. When making these assessments, frequency response >0
// dB is not preferable to 0 dB. Most all analog hardware cannot realize SiNAD >160 dB, and within
// our current implementation a value >160 dB is clearly a "harmonic spike" since it would require
// >26.5 bits of precision (exceeding the precision of a normalized-float32 pipeline like ours).
// Also, clamping our required SiNAD to 160 dB conceptually pairs with FIDL constant MUTED_GAIN_DB.
constexpr double kMaxFrequencyResponse = 0.0;
constexpr double kMaxSignalToNoiseAndDistortion = 160.0;
// static
const std::array<double, HermeticFidelityTest::kNumReferenceFreqs> HermeticFidelityTest::FillArray(
double val) {
std::array<double, HermeticFidelityTest::kNumReferenceFreqs> arr;
arr.fill(val);
return arr;
}
// static
// Retrieve (initially allocating, if necessary) the array of level results for this path|channel.
std::array<double, HermeticFidelityTest::kNumReferenceFreqs>& HermeticFidelityTest::level_results(
std::string test_name, int32_t channel) {
// Allocated only when first needed, and automatically cleaned up when process exits.
static auto results_level_db =
new std::map<ResultsIndex, std::array<double, HermeticFidelityTest::kNumReferenceFreqs>>();
ResultsIndex index{
.test_name = test_name,
.channel = channel,
};
if (results_level_db->find(index) == results_level_db->end()) {
auto& results = (*results_level_db)[index];
std::fill(results.begin(), results.end(), std::numeric_limits<double>::infinity());
}
return results_level_db->find(index)->second;
}
// static
// Retrieve (initially allocating, if necessary) the array of SiNAD results for this path|channel.
// A map of these array results is saved as a function-local static variable.
std::array<double, HermeticFidelityTest::kNumReferenceFreqs>& HermeticFidelityTest::sinad_results(
std::string test_name, int32_t channel) {
// Allocated only when first needed, and automatically cleaned up when process exits.
static auto results_sinad_db =
new std::map<ResultsIndex, std::array<double, HermeticFidelityTest::kNumReferenceFreqs>>();
ResultsIndex index{
.test_name = test_name,
.channel = channel,
};
if (results_sinad_db->find(index) == results_sinad_db->end()) {
auto& results = (*results_sinad_db)[index];
std::fill(results.begin(), results.end(), std::numeric_limits<double>::infinity());
}
return results_sinad_db->find(index)->second;
}
void HermeticFidelityTest::SetUp() {
HermeticPipelineTest::SetUp();
// We save input|output files if requested. Ensure the requested frequency is one we measure.
save_fidelity_wav_files_ = HermeticPipelineTest::save_input_and_output_files_;
if (save_fidelity_wav_files_) {
bool requested_frequency_found = false;
for (auto freq : kReferenceFrequencies) {
if (freq == kFrequencyForSavedWavFiles) {
requested_frequency_found = true;
break;
}
}
if (!requested_frequency_found) {
FX_LOGS(WARNING) << kFrequencyForSavedWavFiles
<< " is not in the frequency list, a WAV file cannot be saved";
save_fidelity_wav_files_ = false;
}
}
}
// Translate real-world frequencies into 'internal_periods', the number of complete wavelengths that
// fit perfectly into our signal buffer. If this is an integer, we won't need to Window the output
// before frequency analysis. Example: when measuring real-world frequency 2000 Hz at frame rate 96
// kHz, for buffer size 65536 this translates into 1365.333... periods, but we use the integer 1365.
// This translates back to a real-world frequency of 1999.5 Hz, which is not a problem.
//
// We also want internal_periods to have fewer common factors with our buffer size and frame rates,
// as this can mask problems where previous buffer sections are erroneously repeated. So if it is
// not integral, we return the odd neighbor rather than round.
int32_t HermeticFidelityTest::FrequencyToPeriods(int32_t device_frame_rate, int32_t frequency) {
double internal_periods = static_cast<double>(frequency * kFreqTestBufSize) / device_frame_rate;
auto floor_periods = static_cast<int32_t>(std::floor(internal_periods));
auto ceil_periods = static_cast<int32_t>(std::ceil(internal_periods));
return (floor_periods % 2) ? floor_periods : ceil_periods;
}
template <fuchsia::media::AudioSampleFormat InputFormat,
fuchsia::media::AudioSampleFormat OutputFormat>
std::vector<HermeticFidelityTest::Frequency> HermeticFidelityTest::GetTestFrequencies(
const HermeticFidelityTest::TestCase<InputFormat, OutputFormat>& tc) {
if (tc.single_frequency_to_test.has_value()) {
auto freq_display_val = tc.single_frequency_to_test.value();
// If `single_frequency_to_test` is specified, it should be in kReferenceFrequencies.
// If not, round down to a lower frequency that is, and use those results.
size_t freq_idx;
for (freq_idx = 0u; freq_idx < kNumReferenceFreqs - 1; ++freq_idx) {
if (freq_display_val < kReferenceFrequencies[freq_idx + 1]) {
break;
}
}
if (freq_display_val != kReferenceFrequencies[freq_idx]) {
FX_LOGS(WARNING) << "Frequency " << freq_display_val
<< " not found in kReferenceFrequencies. Using "
<< kReferenceFrequencies[freq_idx] << " Hz instead";
freq_display_val = kReferenceFrequencies[freq_idx];
}
return {{
.display_val = freq_display_val,
.periods = FrequencyToPeriods(tc.output_format.frames_per_second(), freq_display_val),
.idx = freq_idx,
}};
}
std::vector<HermeticFidelityTest::Frequency> frequencies;
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
frequencies.push_back({
.display_val = kReferenceFrequencies[freq_idx],
.periods = FrequencyToPeriods(tc.output_format.frames_per_second(),
kReferenceFrequencies[freq_idx]),
.idx = freq_idx,
});
}
return frequencies;
}
// Render source such that first input frame will be rendered into first ring buffer frame.
// Create a renderer, submit packets, play, wait for them to be rendered, shut down the renderer,
// and extract the output from the VAD ring buffer.
template <ASF InputFormat, ASF OutputFormat>
AudioBuffer<OutputFormat> HermeticFidelityTest::GetRendererOutput(
TypedFormat<InputFormat> input_format, int64_t input_buffer_frames, RenderPath path,
AudioBuffer<InputFormat> input, VirtualOutput<OutputFormat>* device, ClockMode clock_mode,
std::optional<float> gain_db) {
RendererShimImpl* renderer;
if (path != RenderPath::Ultrasound) {
std::optional<zx::clock> clock;
zx::clock::update_args args;
zx::clock offset_clock;
zx::time now;
switch (clock_mode) {
case ClockMode::Default:
break;
case ClockMode::Flexible:
clock = zx::clock(ZX_HANDLE_INVALID);
break;
case ClockMode::Monotonic:
clock = audio::clock::CloneOfMonotonic();
break;
case ClockMode::Offset:
// Set a reference clock with an offset of +20usec.
EXPECT_EQ(zx::clock::create(ZX_CLOCK_OPT_MONOTONIC | ZX_CLOCK_OPT_CONTINUOUS, nullptr,
&offset_clock),
ZX_OK)
<< "Offset clock could not be created";
now = zx::clock::get_monotonic();
args.reset().set_both_values(now, now + zx::usec(20));
EXPECT_EQ(offset_clock.update(args), ZX_OK) << "clock.update with set_both_values failed";
clock = std::move(offset_clock);
break;
case ClockMode::RateAdjusted:
clock = audio::clock::AdjustableCloneOfMonotonic();
args.reset().set_rate_adjust(100);
EXPECT_EQ(clock->update(args), ZX_OK) << "Could not rate-adjust a custom clock";
break;
}
fuchsia::media::AudioRenderUsage usage = fuchsia::media::AudioRenderUsage::MEDIA;
if (path == RenderPath::Communications) {
usage = fuchsia::media::AudioRenderUsage::COMMUNICATION;
}
auto audio_renderer =
CreateAudioRenderer(input_format, input_buffer_frames, usage, std::move(clock));
if (gain_db.has_value()) {
audio_renderer->SetGain(*gain_db);
}
renderer = audio_renderer;
} else {
renderer = CreateUltrasoundRenderer(input_format, input_buffer_frames, true);
}
auto packets = renderer->AppendPackets<InputFormat>({&input});
renderer->PlaySynchronized(this, device, 0);
renderer->WaitForPackets(this, packets);
auto buffer = device->SnapshotRingBuffer();
// Free the renderer now, instead of accumulating one for every test frequency.
if (path == RenderPath::Ultrasound) {
Unbind(static_cast<UltrasoundRendererShim<InputFormat>*>(renderer));
} else {
Unbind(static_cast<AudioRendererShim<InputFormat>*>(renderer));
}
return buffer;
}
// Measuring system response requires providing enough input for a full output response.
//
// Our input buffer contains initial silence, (more-than-enough) signal, then final silence.
// The [silence+signal+silence] must include adequate length for OUTPUT ramp-up and stabilization,
// a sufficient section of fully stabilized signal for analysis, and ultimately ramp-down/ring-out.
//
// Output ramping may occur before AND after input transitions, so we refer to 5 output sections:
// ramp-in, initial stabilization, analysis section, final stabilization, ramp-out.
// The input signal contains these directly-corresponding sections:
// initial silence, initial stabilization, analysis section, final stabilization, final silence.
//
// For this source ___________________________ .
// input signal, with _____________| |____________________ .
// Initial and Final .
// frames I and F: I F .
// .
// A system may produce /\_^=~_~_--------------^-_~/\_ .
// this output signal: -------_~_^_/ \/~_=_~-/\~_-_----- .
// ^ ^ .
// "Ramp-in" (pre I): RRRRRR . . .
// "initial Stabilization" (at/post I): SSSSSSSSS. . .
// "final (De)stabilization" (pre/at F): . .DDDDD .
// "ramp-Out"/"ring-Out" (post F): . . OOOOOOOOOOOOOOOO .
// stable "Analysis section": AAAAAAAAAAAAAA .
// .
// Thus, our source signals ___________________________ .
// conceptually include the _____________| |____________________ .
// corresponding sections: .
// 1: initial silence 1111111111111 .
// 2: initial stabilization 222222222 .
// 3: analysis section 33333333333333 .
// 4: final stabilization 44444 .
// 5: final silence 555555555555555555555 .
//
// Test writers use HermeticPipelineTest::PipelineConstants to convey these transition widths.
// static
// Input buffer should contain exact silence for first/last sections and immediate continuous signal
// across the three middle sections, with a full-scale value at start of analysis section (this
// becomes the OUTPUT analysis section's first frame). Depending on input signal frequency, there
// will be an identical full-scale value at either start of final stabilization (periods-per-buffer
// is integral), or earlier by less than a frame (if non-integral). Conceptually, these values must
// be identical so that the resulting (guaranteed-integral) output analysis section can be perfectly
// "infinitely looped" (which is how spectral-analysis FFT essentially treats it).
template <ASF InputFormat>
void HermeticFidelityTest::DisplayInputBufferSections(const AudioBuffer<InputFormat>& buffer,
const std::string& initial_tag,
const SignalSectionIndices& input_indices) {
printf("\n");
buffer.Display(0, kInputDisplayWindow, initial_tag);
buffer.Display(input_indices.stabilization_start - kInputDisplayWindow,
input_indices.stabilization_start,
"End of initial silence (should be entirely silent)");
buffer.Display(input_indices.stabilization_start,
input_indices.stabilization_start + kInputDisplayWindow,
"Start of initial stabilization (should start immediately)");
buffer.Display(input_indices.analysis_start - kInputDisplayWindow, input_indices.analysis_start,
"End of initial stabilization (should lead toward a full-scale value)");
buffer.Display(input_indices.analysis_start, input_indices.analysis_start + kInputDisplayWindow,
"Start of signal-to-be-analyzed (should start at a full-scale value)");
buffer.Display(input_indices.analysis_end - kInputDisplayWindow, input_indices.analysis_end,
"End of signal-to-be-analyzed (should lead toward a full-scale value)");
buffer.Display(input_indices.analysis_end, input_indices.analysis_end + kInputDisplayWindow,
"Start of final stabilization (should start at/after a full-scale value)");
buffer.Display(input_indices.stabilization_end - kInputDisplayWindow,
input_indices.stabilization_end,
"End of final stabilization (should continue without attenuation)");
buffer.Display(input_indices.stabilization_end,
input_indices.stabilization_end + kInputDisplayWindow,
"Start of final_silence (should be immediately silent)");
buffer.Display(buffer.NumFrames() - kInputDisplayWindow, buffer.NumFrames(),
"End of final silence (and end of input buffer)");
}
// static
// If output pipeline has no phase shift, then we expect full-scale values in both first frame of
// analysis section, and first frame after analysis section. If pipeline has phase shift, they
// should still be identical but may not be full-scale (analysis section should still be loopable).
template <ASF OutputFormat>
void HermeticFidelityTest::DisplayOutputBufferSections(const AudioBuffer<OutputFormat>& buffer,
const std::string& initial_tag,
const SignalSectionIndices& output_indices) {
printf("\n");
buffer.Display(0, kOutputDisplayWindow, initial_tag);
buffer.Display(output_indices.stabilization_start - kOutputDisplayWindow,
output_indices.stabilization_start,
"End of ramp-in (may end in destabilization, then sudden rise)");
buffer.Display(output_indices.stabilization_start,
output_indices.stabilization_start + kOutputDisplayWindow,
"Start of initial stabilization (may start with overshoot; should stabilize)");
buffer.Display(output_indices.analysis_start - kOutputDisplayWindow -
kOutputAdditionalSignalStartDisplayWindow,
output_indices.analysis_start,
"End of initial stabilization (should fully stabilize by end of section)");
buffer.Display(output_indices.analysis_start,
output_indices.analysis_start + kOutputDisplayWindow +
kOutputAdditionalSignalStartDisplayWindow,
"Start of analysis section (should start with max value for this channel)");
buffer.Display(
output_indices.analysis_end - kOutputDisplayWindow - kOutputAdditionalSignalEndDisplayWindow,
output_indices.analysis_end,
"End of analysis section (should resemble end of initial stabilization)");
buffer.Display(
output_indices.analysis_end,
output_indices.analysis_end + kOutputDisplayWindow + kOutputAdditionalSignalEndDisplayWindow,
"Start of final stabilization (should resemble start of analysis section)");
buffer.Display(output_indices.stabilization_end - kOutputDisplayWindow,
output_indices.stabilization_end,
"End of final stabilization (may destabilize, then suddenly drop)");
buffer.Display(output_indices.stabilization_end,
output_indices.stabilization_end + kOutputDisplayWindow,
"Start of final ramp-out (may be unstable)");
buffer.Display(buffer.NumFrames() - kOutputDisplayWindow, buffer.NumFrames(),
"End of output buffer (should be silent)");
}
template <ASF InputFormat, ASF OutputFormat>
void HermeticFidelityTest::DisplaySummaryResults(
const TestCase<InputFormat, OutputFormat>& test_case,
const std::vector<HermeticFidelityTest::Frequency>& frequencies_to_display) {
// Loop by channel, displaying summary results, in a separate loop from checking each result.
std::string single_freq_info{""};
if (frequencies_to_display.size() == 1) {
single_freq_info =
fxl::Concatenate({" source ", std::to_string(frequencies_to_display[0].display_val),
" Hz [", std::to_string(frequencies_to_display[0].idx), "] -"});
}
for (const auto& channel_spec : test_case.channels_to_measure) {
// Show results in tabular forms, for easy copy into hermetic_fidelity_results.cc.
// We don't enforce greater-than-unity response if it occurs, so clamp these to a max of 0.0.
const auto& chan_level_results_db = level_results(test_case.test_name, channel_spec.channel);
printf("\n\tFull-spectrum Frequency Response - %s -%s output channel %d",
test_case.test_name.c_str(), single_freq_info.c_str(), channel_spec.channel);
for (const auto& freq : frequencies_to_display) {
printf("%s %8.3f,", (freq.idx % 10 == 0 ? "\n" : ""),
std::min(floor(chan_level_results_db[freq.idx] / kFidelityDbTolerance) *
kFidelityDbTolerance,
kMaxFrequencyResponse));
}
printf("\n");
const auto& chan_sinad_results_db = sinad_results(test_case.test_name, channel_spec.channel);
printf("\n\tSignal-to-Noise and Distortion - %s -%s output channel %d",
test_case.test_name.c_str(), single_freq_info.c_str(), channel_spec.channel);
for (const auto& freq : frequencies_to_display) {
printf("%s %8.3f,", (freq.idx % 10 == 0 ? "\n" : ""),
std::min(floor(chan_sinad_results_db[freq.idx] / kFidelityDbTolerance) *
kFidelityDbTolerance,
kMaxSignalToNoiseAndDistortion));
}
printf("\n\n");
}
}
template <ASF InputFormat, ASF OutputFormat>
void HermeticFidelityTest::VerifyResults(
const TestCase<InputFormat, OutputFormat>& test_case,
const std::vector<HermeticFidelityTest::Frequency>& frequencies_to_verify) {
// Loop by channel_to_measure
for (const auto& channel_spec : test_case.channels_to_measure) {
const auto& chan_level_results_db = level_results(test_case.test_name, channel_spec.channel);
for (const auto& freq : frequencies_to_verify) {
EXPECT_GE(chan_level_results_db[freq.idx],
channel_spec.freq_resp_lower_limits_db[freq.idx] - kFidelityDbTolerance)
<< " Channel " << channel_spec.channel << ", FreqResp [" << std::setw(2) << freq.idx
<< "] (" << std::setw(5) << freq.display_val << " Hz): " << std::setprecision(7)
<< floor(chan_level_results_db[freq.idx] / kFidelityDbTolerance) * kFidelityDbTolerance;
}
const auto& chan_sinad_results_db = sinad_results(test_case.test_name, channel_spec.channel);
for (const auto& freq : frequencies_to_verify) {
EXPECT_GE(chan_sinad_results_db[freq.idx],
channel_spec.sinad_lower_limits_db[freq.idx] - kFidelityDbTolerance)
<< " Channel " << channel_spec.channel << ", SINAD [" << std::setw(2) << freq.idx
<< "] (" << std::setw(5) << freq.display_val << " Hz): " << std::setprecision(7)
<< floor(chan_sinad_results_db[freq.idx] / kFidelityDbTolerance) * kFidelityDbTolerance;
}
}
}
// Additional fidelity assessments, potentially added in the future:
// (1) Dynamic range (1kHz input at -30/60/90 db: measure level, SiNAD. Overall gain sensitivity)
// This should clearly show the impact of dynamic compression in the effects chain.
// (2) Assess the e2e input data path (from device to capturer)
// Included for completeness: we apply no capture effects; should equal audio_fidelity_tests.
template <ASF InputFormat, ASF OutputFormat>
void HermeticFidelityTest::Run(
const HermeticFidelityTest::TestCase<InputFormat, OutputFormat>& tc) {
// Translate from input frame number to output frame number.
// Return a double-precision float; let the caller decide whether/how to reduce it to int.
auto input_frame_to_output_frame = [tc](int64_t input_frame) {
return static_cast<double>(input_frame * tc.output_format.frames_per_second()) /
static_cast<double>(tc.input_format.frames_per_second());
};
// Translate from output frame number to input frame number.
auto output_frame_to_input_frame = [tc](int64_t output_frame) {
return static_cast<double>(output_frame * tc.input_format.frames_per_second()) /
static_cast<double>(tc.output_format.frames_per_second());
};
if (tc.path == RenderPath::Ultrasound) {
ASSERT_EQ(tc.renderer_clock_mode, ClockMode::Default)
<< "Ultrasound path cannot be tested with a non-default clock";
}
// We will analyze a specific number of output frames (our 'analysis section'). Depending on
// rate-conversion, this translates to a different number of input signal frames.
//
// We'll need this potentially-fractional input-signal-length value later.
auto input_signal_frames_to_measure_double = output_frame_to_input_frame(kFreqTestBufSize);
// Our frequency analysis does not window the output it receives, which means we want a specific
// number of (integral) signal wavelengths to fit within the OUTPUT buffer analysis section.
// We want the SAME number of wavelengths in our INPUT signal (regardless of rate-conversion
// ratio), but the LENGTH of that input signal is scaled by rate-conversion ratio and becomes
// input_signal_frames_to_measure.
//
// However, certain rate-conversion ratios WOULD lead to non-integral input buffer lengths!
// Buffer lengths of course must be integral, but frequencies need not be.
// If our ideal input length WOULD be fractional, we (1) "ceiling" the input buffer length to be
// integral, then compensate later by (2) adjusting input frequency correspondingly.
// We insert a slightly-larger number of signal wavelengths in our slightly-larger (integral)
// input buffer, which is equivalent to inserting the intended (integral) number of signal
// wavelengths in the FRACTIONAL input length that (via rate-conversion) will translate perfectly
// to the integral frequency, within an output buffer of the required integral length.
//
// Here's the actual (integral) signal length corresponding to the output section we analyze.
// We use input_signal_frames_to_measure_double later, if we must adjust the source frequency.
auto input_signal_frames_to_measure =
static_cast<int64_t>(std::ceil(input_signal_frames_to_measure_double));
// Compute lengths of the other portions of our full input signal, so that we generate an output
// signal with a fully-stabilized steady-state analysis section.
auto init_silence_len =
static_cast<int64_t>(std::max(tc.pipeline.ramp_in_width, tc.pipeline.pos_filter_width));
auto init_stabilization_len =
static_cast<int64_t>(std::max(tc.pipeline.stabilization_width, tc.pipeline.neg_filter_width));
auto final_stabilization_len = static_cast<int64_t>(
std::max(tc.pipeline.destabilization_width, tc.pipeline.pos_filter_width));
auto final_silence_len =
static_cast<int64_t>(std::max(tc.pipeline.decay_width, tc.pipeline.neg_filter_width));
auto input_type_mono =
Format::Create<InputFormat>(1, tc.input_format.frames_per_second()).take_value();
auto init_silence = GenerateSilentAudio(input_type_mono, init_silence_len);
auto final_silence = GenerateSilentAudio(input_type_mono, final_silence_len);
auto input_stabilization_start = init_silence_len;
auto input_analysis_start = input_stabilization_start + init_stabilization_len;
auto input_analysis_end = input_analysis_start + input_signal_frames_to_measure;
auto input_stabilization_end = input_analysis_end + final_stabilization_len;
auto input_signal_len =
init_stabilization_len + input_signal_frames_to_measure + final_stabilization_len;
auto total_input_buffer_len = init_silence_len + input_signal_len + final_silence_len;
if constexpr (kDisplayInputBuffer) {
FX_LOGS(INFO) << "init_silence_len " << init_silence_len << " + pre-stabilization "
<< init_stabilization_len << " + frames_to_measure "
<< input_signal_frames_to_measure << " + post-stabilization "
<< final_stabilization_len << " + final_silence_len " << final_silence_len
<< " = total buffer " << total_input_buffer_len;
}
// We create the AudioBuffer later. Ensure no out-of-range channels are requested to play.
for (const auto& channel : tc.channels_to_play) {
ASSERT_LT(static_cast<int32_t>(channel), tc.input_format.channels())
<< "Cannot play out-of-range input channel";
}
// Calculate the output buffer length needed for our total input signal (initial silence, full
// ramp-in, the signal to be analyzed, and full ramp-out). Set up a virtual audio device with
// a ring-buffer large enough to receive that output length. Round up any partial frames, to
// guarantee we have adequate output space for the full input signal.
auto output_buffer_frames_needed =
static_cast<int64_t>(std::ceil(input_frame_to_output_frame(total_input_buffer_len)));
audio_stream_unique_id_t device_id = AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS;
if (tc.device_id.has_value()) {
device_id = tc.device_id.value();
}
auto device = CreateOutput(device_id, tc.output_format, output_buffer_frames_needed, std::nullopt,
tc.pipeline.output_device_gain_db);
if (tc.thermal_state.has_value()) {
if (ConfigurePipelineForThermal(tc.thermal_state.value()) != ZX_OK) {
return;
}
}
for (auto effect_config : tc.effect_configs) {
fuchsia::media::audio::EffectsController_UpdateEffect_Result result;
auto status =
effects_controller()->UpdateEffect(effect_config.name, effect_config.config, &result);
ASSERT_EQ(status, ZX_OK);
}
int32_t nyquist_limit, low_pass_frequency;
nyquist_limit =
std::min(tc.input_format.frames_per_second(), tc.output_format.frames_per_second()) / 2;
low_pass_frequency = tc.low_pass_frequency.value_or(nyquist_limit);
if (tc.low_pass_frequency.has_value() && tc.low_pass_frequency.value() > nyquist_limit) {
FX_LOGS(WARNING) << "low_pass_frequency (" << tc.low_pass_frequency.value()
<< ") should not exceed the Nyquist limits for this input/output pair ("
<< tc.input_format.frames_per_second() << ", "
<< tc.output_format.frames_per_second() << "): reducing low_pass_frequency to "
<< nyquist_limit;
low_pass_frequency = nyquist_limit;
}
ASSERT_GE(tc.low_cut_frequency, 0)
<< "low_cut_frequency (" << tc.low_cut_frequency << ") cannot be negative";
ASSERT_LE(tc.low_cut_frequency, low_pass_frequency)
<< "low_cut_frequency (" << tc.low_cut_frequency << ") cannot exceed low_pass_frequency ("
<< low_pass_frequency << ")";
if (tc.single_frequency_to_test.has_value()) {
ASSERT_LE(tc.single_frequency_to_test.value(), nyquist_limit)
<< "Specified frequency (" << tc.single_frequency_to_test.value() << ") exceeds "
<< nyquist_limit << ", the Nyquist limit for this input/output pair ("
<< tc.input_format.frames_per_second() << ", " << tc.output_format.frames_per_second()
<< ")";
if (tc.low_pass_frequency.has_value()) {
ASSERT_LE(tc.single_frequency_to_test.value(), tc.low_pass_frequency.value())
<< "Specified frequency (" << tc.single_frequency_to_test.value() << ") exceeds "
<< tc.low_pass_frequency.value() << ", the specified low-pass limit";
}
ASSERT_GE(tc.single_frequency_to_test.value(), tc.low_cut_frequency)
<< "Specified frequency (" << tc.single_frequency_to_test.value() << ") is less than "
<< tc.low_cut_frequency << ", the specified low-cut limit";
}
// This is the factor mentioned earlier (where we set input_signal_frames_to_measure_double). We
// apply this adjustment to freq, to perfectly fit an integral number of wavelengths into the
// intended FRACTIONAL Input buffer length. (This fractional input length is translated via
// rate-conversion into the exact integral Output buffer length used in our analysis.)
auto source_rate_adjustment_factor =
static_cast<double>(input_signal_len) / input_signal_frames_to_measure_double;
// Generate rate-specific internal frequency values for our power-of-two-sized analysis buffer.
auto frequencies_to_test = GetTestFrequencies(tc);
// Process each frequency completely, one at a time
for (Frequency freq : frequencies_to_test) {
auto adjusted_periods = source_rate_adjustment_factor * static_cast<double>(freq.periods);
if (freq.display_val * 2 > tc.input_format.frames_per_second() ||
adjusted_periods * 2.0 > static_cast<double>(input_signal_len)) {
continue;
}
// To make it easier to debug the generation of the input signal, include a phase offset so that
// the beginning of the signal section is aligned with the exact beginning of the cosine signal.
// But don't apply any phase offset if the frequency is zero.
auto phase = freq.periods ? (-2.0 * M_PI * static_cast<double>(init_stabilization_len) *
adjusted_periods / static_cast<double>(input_signal_len))
: 0.0;
auto amplitude = SampleFormatTraits<InputFormat>::kUnityValue -
SampleFormatTraits<InputFormat>::kSilentValue;
auto signal_section =
GenerateCosineAudio(input_type_mono, input_signal_len, adjusted_periods, amplitude, phase);
// Write input signal to input buffer. This starts with silence for pre-ramp-in (which aligns
// input and output WAV files, if enabled). Before/after signal_section, we include additional
// signal to account for the stabilization periods corresponding to input signal start and end.
auto input_mono = init_silence;
input_mono.Append(AudioBufferSlice(&signal_section));
input_mono.Append(AudioBufferSlice(&final_silence));
FX_CHECK(input_mono.NumFrames() == static_cast<int64_t>(total_input_buffer_len))
<< "Incorrect input_mono length: testcode logic error";
auto silence_mono = GenerateSilentAudio(input_type_mono, total_input_buffer_len);
std::vector<AudioBufferSlice<InputFormat>> channels;
for (auto play_channel = 0; play_channel < tc.input_format.channels(); ++play_channel) {
if (tc.channels_to_play.find(play_channel) != tc.channels_to_play.end()) {
channels.push_back(AudioBufferSlice(&input_mono));
} else {
channels.push_back(AudioBufferSlice(&silence_mono));
}
}
auto input = AudioBuffer<InputFormat>::Interleave(channels);
FX_CHECK(input.NumFrames() == static_cast<int64_t>(total_input_buffer_len))
<< "Incorrect input length: testcode logic error";
if constexpr (kDisplayInputBuffer) {
if (kDisplayBuffersAtAllFrequencies || freq.display_val == kFrequencyForBufferDebugging) {
// We construct the input buffer in pieces. If signals don't align at these seams, it causes
// distortion. For debugging, show these "seam" locations in the input buffer we created.
DisplayInputBufferSections(
input,
fxl::Concatenate({"Input buffer for ", std::to_string(freq.display_val), " Hz [",
std::to_string(freq.idx), "]"}),
{.stabilization_start = input_stabilization_start,
.analysis_start = input_analysis_start,
.analysis_end = input_analysis_end,
.stabilization_end = input_stabilization_end});
}
}
// Save off the input file, if requested.
if (save_fidelity_wav_files_) {
// We shouldn't save files for ALL frequencies -- just save the files for this frequency.
if (freq.display_val == kFrequencyForSavedWavFiles) {
HermeticPipelineTest::WriteWavFile<InputFormat>(
fxl::Concatenate({tc.test_name, "_", std::to_string(freq.display_val)}), "input",
AudioBufferSlice(&input));
}
}
// Set up the renderer, run it and retrieve the output.
auto ring_buffer = GetRendererOutput(tc.input_format, total_input_buffer_len, tc.path, input,
device, tc.renderer_clock_mode, tc.gain_db);
// In case of underflows, exit NOW (don't assess this buffer or run other frequencies).
// TODO(fxbug.dev/80003): Remove workarounds when underflow conditions are fixed.
if (DeviceHasUnderflows(device)) {
FX_LOGS(INFO) << "Test case will exit early: underflows were detected";
break;
}
// For each channel: 1. analyze output, 2) display in-progress results if configured, 3) display
// output buffer sections if applicable, 4) exit if underflows, 5) save results for later.
for (const auto& channel_spec : tc.channels_to_measure) {
auto ring_buffer_chan = AudioBufferSlice(&ring_buffer).GetChannel(channel_spec.channel);
// Analyze the results. Round our output position, so we start as close as possible to the
// input signal start. That said, being off by one in either direction is still OK since the
// analysis section is bookended by full ramps in/out on either side, containing identical
// data (i.e. the analysis section's first value is repeated immediately after the section
// ends; conversely its final value is "pre-repeated" immediately prior to section start).
auto output_stabilization_start =
static_cast<int64_t>(std::round(input_frame_to_output_frame(input_stabilization_start)));
auto output_analysis_start =
static_cast<int64_t>(std::round(input_frame_to_output_frame(input_analysis_start)));
auto output_analysis_end = output_analysis_start + kFreqTestBufSize;
auto output_stabilization_end =
output_analysis_end +
static_cast<int64_t>(std::round(input_frame_to_output_frame(final_stabilization_len)));
auto output = AudioBufferSlice(&ring_buffer_chan, output_analysis_start, output_analysis_end);
auto channel_is_out_of_band = (channel_spec.freq_resp_lower_limits_db[0] == -INFINITY);
auto out_of_band = (freq.display_val < tc.low_cut_frequency ||
freq.display_val > low_pass_frequency || channel_is_out_of_band);
double sinad_db, level_db = 0.0;
AudioFreqResult freq_result;
if (out_of_band) {
// For out-of-band frequencies, we use the sinad array to store Out-of-Band Rejection,
// which is measured as the sinad(all frequencies), assuming a full-scale input.
freq_result = MeasureAudioFreqs(output, {});
sinad_db = DoubleToDb(1.0 / freq_result.total_magn_other);
if constexpr (kDisplayInProgressResults) {
FX_LOGS(INFO) << "Channel " << channel_spec.channel << ": " << std::setw(5)
<< freq.display_val << " Hz [" << std::setw(2) << freq.idx
<< "] -- out-of-band rejection " << std::fixed << std::setprecision(4)
<< std::setw(8) << sinad_db << " db";
}
} else {
freq_result = MeasureAudioFreqs(output, {static_cast<int32_t>(freq.periods)});
level_db = DoubleToDb(freq_result.magnitudes[freq.periods]);
if (isinf(level_db) && level_db < 0) {
// If an expected signal was truly absent (silence), we probably underflowed. This
// [level_db, sinad_db] pair is meaningless, so set sinad_db to -INFINITY as well.
sinad_db = -INFINITY;
} else {
sinad_db =
DoubleToDb(freq_result.magnitudes[freq.periods] / freq_result.total_magn_other);
}
if constexpr (kDisplayInProgressResults) {
FX_LOGS(INFO) << "Channel " << channel_spec.channel << ": " << std::setw(5)
<< freq.display_val << " Hz [" << std::setw(2) << freq.idx << "] -- level "
<< std::fixed << std::setprecision(4) << std::setw(9) << level_db
<< " db, SiNAD " << std::setw(8) << sinad_db << " db";
}
}
if (save_fidelity_wav_files_) {
// We shouldn't save files for the full frequency set -- just save files for this frequency.
if (freq.display_val == kFrequencyForSavedWavFiles) {
HermeticPipelineTest::WriteWavFile<OutputFormat>(
fxl::Concatenate({tc.test_name, "_chan", std::to_string(channel_spec.channel), "_",
std::to_string(freq.display_val)}),
"output", output);
}
}
if constexpr (kDisplayOutputBufferOnFailure || kDisplayAnalysisDataOnFailure ||
kDisplayOutputBuffer) {
double required_level_db =
channel_spec.freq_resp_lower_limits_db[freq.idx] - kFidelityDbTolerance;
double required_sinad_db =
channel_spec.sinad_lower_limits_db[freq.idx] - kFidelityDbTolerance;
// Display output buffer on failure, if all of 1) 'display buffer on failure' flag is set,
// 2) frequency is not out-of-band, 3) buffer is NOT entirely silent (SiNAD > -infinity),
// and 4) failure (Frequency Response or SiNAD) exceeds tolerance.
bool display_output_buffer_for_failure =
kDisplayOutputBufferOnFailure && !out_of_band && !(isinf(sinad_db) && sinad_db < 0) &&
(level_db + kDisplayOutputBufferOnFailureFreqRespDbTolerance < required_level_db ||
sinad_db + kDisplayOutputBufferOnFailureSinadDbTolerance < required_sinad_db);
// Display FFT metadata on failure, if all of 1) 'display metadata on failure' flag is set,
// 2) buffer is NOT entirely silent (SiNAD == -infinity), and 3) failure exceeds tolerance.
bool display_analysis_data_for_failure =
kDisplayAnalysisDataOnFailure && !isinf(sinad_db) &&
(level_db + kDisplayAnalysisDataOnFailureDbTolerance < required_level_db ||
sinad_db + kDisplayAnalysisDataOnFailureDbTolerance < required_sinad_db);
// Display output buffer anyway, if 1) 'display output buffer' config flag is set, and 2) we
// are configured to display either all frequencies or this specific frequency.
bool display_output_buffer_for_success =
kDisplayOutputBuffer &&
(kDisplayBuffersAtAllFrequencies || freq.display_val == kFrequencyForBufferDebugging);
if (display_output_buffer_for_failure || display_analysis_data_for_failure) {
printf(
"\nFAILURE (freq resp %f dB, should have been %f dB; sinad %f dB, should have been "
"%f dB)",
level_db, required_level_db, sinad_db, required_sinad_db);
}
if (display_analysis_data_for_failure) {
freq_result.Display(fxl::Concatenate({"Frequency analysis results for ",
std::to_string(freq.display_val), " Hz:"}),
kMinAnalysisMagnitudeToDisplay);
}
if (display_output_buffer_for_failure || display_output_buffer_for_success) {
std::string tag =
fxl::Concatenate({"Output buffer for ", std::to_string(freq.display_val), " Hz [",
std::to_string(freq.idx), "] (", std::to_string(freq.periods),
"-periods-in-", std::to_string(kFreqTestBufSize),
", adjusted-freq ", std::to_string(adjusted_periods), "; channel ",
std::to_string(channel_spec.channel)});
DisplayOutputBufferSections(ring_buffer_chan, tag,
{.stabilization_start = output_stabilization_start,
.analysis_start = output_analysis_start,
.analysis_end = output_analysis_end,
.stabilization_end = output_stabilization_end});
}
}
// Retrieve the arrays of measurements for this path and channel
auto& curr_level_db = level_results(tc.test_name, channel_spec.channel);
auto& curr_sinad_db = sinad_results(tc.test_name, channel_spec.channel);
if constexpr (kRetainWorstCaseResults) {
curr_level_db[freq.idx] = std::min(curr_level_db[freq.idx], level_db);
curr_sinad_db[freq.idx] = std::min(curr_sinad_db[freq.idx], sinad_db);
} else {
curr_sinad_db[freq.idx] = sinad_db;
curr_level_db[freq.idx] = level_db;
}
}
}
if constexpr (kDisplaySummaryResults) {
DisplaySummaryResults(tc, frequencies_to_test);
}
// Only check results and pass/fail if we made a complete set of measurements without underflows.
// If there were underflows, SKIP (don't fail) so it won't look like a fidelity regression.
// TODO(fxbug.dev/80003): Remove workarounds when underflow conditions are fixed.
if (DeviceHasUnderflows(device)) {
GTEST_SKIP() << "Skipping threshold checks due to underflows";
__builtin_unreachable();
}
VerifyResults(tc, frequencies_to_test);
}
template void HermeticFidelityTest::Run<ASF::UNSIGNED_8, ASF::FLOAT>(
const TestCase<ASF::UNSIGNED_8, ASF::FLOAT>& tc);
template void HermeticFidelityTest::Run<ASF::SIGNED_16, ASF::FLOAT>(
const TestCase<ASF::SIGNED_16, ASF::FLOAT>& tc);
template void HermeticFidelityTest::Run<ASF::SIGNED_24_IN_32, ASF::FLOAT>(
const TestCase<ASF::SIGNED_24_IN_32, ASF::FLOAT>& tc);
template void HermeticFidelityTest::Run<ASF::FLOAT, ASF::FLOAT>(
const TestCase<ASF::FLOAT, ASF::FLOAT>& tc);
} // namespace media::audio::test