blob: 996b2e3af562ea7bb2782cc776249d63edb5c24a [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 <zircon/types.h>
#include <array>
#include <cmath>
#include <iomanip>
#include <map>
#include <set>
#include <string>
#include <test/thermal/cpp/fidl.h>
#include "lib/zx/time.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/renderer_shim.h"
using ASF = fuchsia::media::AudioSampleFormat;
namespace media::audio::test {
namespace {
struct ResultsIndex {
HermeticFidelityTest::RenderPath path;
size_t channel;
uint32_t thermal_state;
bool operator<(const ResultsIndex& rhs) const {
return std::tie(path, channel, thermal_state) <
std::tie(rhs.path, rhs.channel, rhs.thermal_state);
}
};
}; // namespace
// For each path|channel|thermal_state, 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 path/channel/thermal_state. Thus, this must be
// refactored if two test cases need to specify the same path|output_channels|thermal_state (an
// example would be Dynamic Range testing -- the same measurements, but at different volumes).
// static
// Retrieve (initially allocating, if necessary) the array of level results for this path|channel.
std::array<double, HermeticFidelityTest::kNumReferenceFreqs>& HermeticFidelityTest::level_results(
RenderPath path, size_t channel, uint32_t thermal_state) {
// 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{
.path = path,
.channel = channel,
.thermal_state = thermal_state,
};
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(
RenderPath path, size_t channel, uint32_t thermal_state) {
// 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{
.path = path,
.channel = channel,
.thermal_state = thermal_state,
};
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 to frequencies that fit perfectly into our signal buffer.
// Internal frequencies must be integers, so we don't need to Window the output before frequency
// analysis. We use buffer size and frame rate. Thus, when measuring real-world frequency 2000 Hz
// with buffer size 65536 at frame rate 96 kHz, we use the internal frequency 1365, rather than
// 1365.333... -- translating to a real-world frequency of 1999.5 Hz (this is not a problem).
//
// We also want these internal frequencies 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 a computed internal frequency is not integral, we use the odd neighbor, rather than round.
void HermeticFidelityTest::TranslateReferenceFrequencies(uint32_t device_frame_rate) {
for (auto freq_idx = 0u; freq_idx < kReferenceFrequencies.size(); ++freq_idx) {
double internal_freq =
static_cast<double>(kReferenceFrequencies[freq_idx] * kFreqTestBufSize) / device_frame_rate;
uint32_t floor_freq = std::floor(internal_freq);
uint32_t ceil_freq = std::ceil(internal_freq);
translated_ref_freqs_[freq_idx] = (floor_freq % 2) ? floor_freq : ceil_freq;
}
}
// Retrieve the number of thermal subscribers, and set them all to the specified thermal state.
// thermal_test_control is synchronous: when SetThermalState returns, a change is committed.
zx_status_t HermeticFidelityTest::ConfigurePipelineForThermal(uint32_t state) {
constexpr size_t kMaxRetries = 100;
constexpr zx::duration kRetryPeriod = zx::msec(10);
std::optional<size_t> audio_subscriber;
std::vector<::test::thermal::SubscriberInfo> subscriber_data;
// We might query thermal::test::Control before AudioCore has subscribed, so wait for it.
for (size_t retries = 0u; retries < kMaxRetries; ++retries) {
auto status = thermal_test_control()->GetSubscriberInfo(&subscriber_data);
if (status != ZX_OK) {
ADD_FAILURE() << "GetSubscriberInfo failed: " << status;
return status;
}
// There is only one thermal subscriber for audio; there might be others of non-audio types.
for (auto subscriber_num = 0u; subscriber_num < subscriber_data.size(); ++subscriber_num) {
if (subscriber_data[subscriber_num].actor_type == fuchsia::thermal::ActorType::AUDIO) {
audio_subscriber = subscriber_num;
break;
}
}
if (audio_subscriber.has_value()) {
break;
}
zx::nanosleep(zx::deadline_after(kRetryPeriod));
}
if (!audio_subscriber.has_value()) {
ADD_FAILURE() << "No audio-related thermal subscribers. "
"Don't set thermal_state if a pipeline has no thermal support";
return ZX_ERR_TIMED_OUT;
}
auto max_state = subscriber_data[audio_subscriber.value()].num_thermal_states - 1;
if (state > max_state) {
ADD_FAILURE() << "Subscriber cannot be put into thermal state " << state
<< " (max: " << max_state << ")";
return ZX_ERR_NOT_SUPPORTED;
}
auto status = this->thermal_test_control()->SetThermalState(audio_subscriber.value(), state);
if (status != ZX_OK) {
ADD_FAILURE() << "SetThermalState failed: " << status;
return status;
}
return ZX_OK;
}
template <ASF InputFormat, ASF OutputFormat>
AudioBuffer<OutputFormat> HermeticFidelityTest::GetRendererOutput(
TypedFormat<InputFormat> input_format, size_t input_buffer_frames, RenderPath path,
AudioBuffer<InputFormat> input, VirtualOutput<OutputFormat>* device) {
FX_CHECK(input_format.frames_per_second() == 96000);
fuchsia::media::AudioRenderUsage usage = fuchsia::media::AudioRenderUsage::MEDIA;
if (path == RenderPath::Communications) {
usage = fuchsia::media::AudioRenderUsage::COMMUNICATION;
}
// Render input such that first input frame will be rendered into first ring buffer frame.
if (path == RenderPath::Ultrasound) {
auto renderer = CreateUltrasoundRenderer(input_format, input_buffer_frames, true);
auto packets = renderer->AppendPackets({&input});
renderer->PlaySynchronized(this, device, 0);
renderer->WaitForPackets(this, packets);
} else {
auto renderer = CreateAudioRenderer(input_format, input_buffer_frames, usage);
auto packets = renderer->AppendPackets({&input});
renderer->PlaySynchronized(this, device, 0);
renderer->WaitForPackets(this, packets);
}
// Extract it from the VAD ring-buffer.
return device->SnapshotRingBuffer();
}
template <ASF InputFormat, ASF OutputFormat>
void HermeticFidelityTest::DisplaySummaryResults(
const TestCase<InputFormat, OutputFormat>& test_case) {
// Loop by channel, displaying summary results, in a separate loop from checking each result.
for (const auto& channel_spec : test_case.channels_to_measure) {
// Show results in tabular forms, for easy copy into hermetic_fidelity_results.cc.
const auto& chan_level_results_db =
level_results(test_case.path, channel_spec.channel, test_case.thermal_state.value_or(0));
printf("\n\tFull-spectrum Frequency Response - %s - output channel %zu",
test_case.test_name.c_str(), channel_spec.channel);
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
printf("%s%8.3f,", (freq_idx % 10 == 0 ? "\n" : ""),
floor(chan_level_results_db[freq_idx] / kFidelityDbTolerance) * kFidelityDbTolerance);
}
printf("\n");
const auto& chan_sinad_results_db =
sinad_results(test_case.path, channel_spec.channel, test_case.thermal_state.value_or(0));
printf("\n\tSignal-to-Noise and Distortion - %s - output channel %zu",
test_case.test_name.c_str(), channel_spec.channel);
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
printf("%s%8.3f,", (freq_idx % 10 == 0 ? "\n" : ""),
floor(chan_sinad_results_db[freq_idx] / kFidelityDbTolerance) * kFidelityDbTolerance);
}
printf("\n\n");
}
}
template <ASF InputFormat, ASF OutputFormat>
void HermeticFidelityTest::VerifyResults(const TestCase<InputFormat, OutputFormat>& test_case) {
// 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.path, channel_spec.channel, test_case.thermal_state.value_or(0));
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
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) << kReferenceFrequencies[freq_idx]
<< " Hz): " << std::setprecision(7)
<< floor(chan_level_results_db[freq_idx] / kFidelityDbTolerance) * kFidelityDbTolerance;
}
const auto& chan_sinad_results_db =
sinad_results(test_case.path, channel_spec.channel, test_case.thermal_state.value_or(0));
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
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) << kReferenceFrequencies[freq_idx]
<< " Hz): " << std::setprecision(7)
<< floor(chan_sinad_results_db[freq_idx] / kFidelityDbTolerance) * kFidelityDbTolerance;
}
}
}
// Additional fidelity assessments, potentially added in the future:
// (1) Cross-talk (FreqResp/Sinad across channels)
// This could identify issues masked by our collapse to mono for final device output.
// (2) 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.
// (3) 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) {
// Compute input signal length: time to ramp in, enough to analyze, time to ramp out.
size_t input_signal_frames =
std::ceil(static_cast<double>(kFreqTestBufSize * tc.input_format.frames_per_second()) /
tc.output_format.frames_per_second());
// Compute the renderer payload buffer size (including pre-signal and post-signal silence).
// TODO(mpuryear): revisit, once pipeline automatically handles filter_width by feeding silence.
auto input_signal_start = tc.pipeline.pos_filter_width;
auto input_buffer_frames = input_signal_start + tc.pipeline.neg_filter_width +
input_signal_frames + tc.pipeline.pos_filter_width +
tc.pipeline.neg_filter_width;
// TODO(mpuryear): support source frequencies other than 96k, when necessary
FX_CHECK(tc.input_format.frames_per_second() == 96000)
<< "For now, non-96k renderer frame rates are disallowed in this test";
// Translate from input frame number to output frame number.
auto input_frame_to_output_frame = [](size_t input_frame) { return input_frame; };
auto input_type_mono =
Format::Create<InputFormat>(1, tc.input_format.frames_per_second()).take_value();
auto bookend_silence = GenerateSilentAudio(input_type_mono, input_signal_start);
auto total_input_frames = input_signal_start + tc.pipeline.neg_filter_width +
input_signal_frames + tc.pipeline.pos_filter_width + input_signal_start;
// 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(channel, tc.input_format.channels()) << "Cannot play out-of-range input channel";
}
//
// Then, calculate the length of the output signal and set up the VAD, with a 1-sec ring-buffer.
auto output_buffer_frames = input_frame_to_output_frame(input_buffer_frames);
FX_CHECK(output_buffer_frames < tc.output_format.frames_per_second());
FX_CHECK(tc.output_format.frames_per_second() == 96000)
<< "For now, non-96k device frame rates are disallowed in this test";
auto device = CreateOutput(AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS, tc.output_format,
tc.output_format.frames_per_second(), std::nullopt,
tc.pipeline.output_device_gain_db);
// Generate the device-rate-specific internal frequency values for our power-of-two-sized buffer.
TranslateReferenceFrequencies(tc.output_format.frames_per_second());
if (tc.thermal_state.has_value()) {
if (ConfigurePipelineForThermal(tc.thermal_state.value()) != ZX_OK) {
return;
}
}
//
// Now iterate through the spectrum, completely processing one frequency at a time.
for (auto freq_idx = 0u; freq_idx < kNumReferenceFreqs; ++freq_idx) {
auto freq = translated_ref_freqs_[freq_idx]; // The frequency within our power-of-two buffer
auto freq_for_display = kReferenceFrequencies[freq_idx];
// Write input signal to input buffer. Start with silence, for pre-ramping; this aligns the
// input and output WAV files (if enabled). Append signal to account for full ramp-in and
// ramp-out. Finally, include trailing silence to flush out any cached values and show decay.
auto signal_section = GenerateCosineAudio(input_type_mono, input_signal_frames, freq);
auto input_mono = bookend_silence;
input_mono.Append(AudioBufferSlice(&signal_section));
input_mono.Append(AudioBufferSlice(
&signal_section, 0, tc.pipeline.neg_filter_width + tc.pipeline.pos_filter_width));
input_mono.Append(AudioBufferSlice(&bookend_silence));
FX_CHECK(input_mono.NumFrames() == total_input_frames)
<< "Miscalculated input_mono length: testcode error";
auto silence_mono = GenerateSilentAudio(input_type_mono, total_input_frames);
std::vector<AudioBufferSlice<InputFormat>> channels;
for (auto play_channel = 0u; 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() == total_input_frames);
if constexpr (kDebugInputBuffer) {
// 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.
std::string tag = "\nInput buffer for " + std::to_string(freq_for_display) + " Hz [" +
std::to_string(freq_idx) + "]";
input.Display(0, 16, tag);
input.Display(input_signal_start - 16, input_signal_start + 16, "Start of input signal");
input.Display(input_signal_start + input_signal_frames - 16,
input_signal_start + input_signal_frames + 16,
"End of input signal; start of additional section for ramp in/out");
input.Display(input_signal_start + tc.pipeline.neg_filter_width + input_signal_frames +
tc.pipeline.pos_filter_width - 16,
input_signal_start + tc.pipeline.neg_filter_width + input_signal_frames +
tc.pipeline.pos_filter_width + 16,
"End of additional ramp section");
input.Display(input.NumFrames() - 16, input.NumFrames(), "End of input buffer");
}
// 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_for_display == kFrequencyForSavedWavFiles) {
std::string test_name = tc.test_name + "_" + std::to_string(freq_for_display) + "hz";
HermeticPipelineTest::WriteWavFile<InputFormat>(test_name, "input",
AudioBufferSlice(&input));
}
}
// Set up the renderer, run it and retrieve the output.
auto ring_buffer =
GetRendererOutput(tc.input_format, input_buffer_frames, tc.path, input, device);
// Loop here on each channel to measure...
for (const auto& channel_spec : tc.channels_to_measure) {
auto ring_buffer_chan = AudioBufferSlice(&ring_buffer).GetChannel(channel_spec.channel);
// Analyze the results
auto output_analysis_start =
input_frame_to_output_frame(input_signal_start + tc.pipeline.neg_filter_width);
auto output = AudioBufferSlice(&ring_buffer_chan, output_analysis_start,
output_analysis_start + kFreqTestBufSize);
if constexpr (kDebugOutputBuffer) {
std::string tag = "\nOutput buffer for " + std::to_string(freq_for_display) + " Hz [" +
std::to_string(freq_idx) + "], channel " +
std::to_string(channel_spec.channel);
// For debugging, show critical locations in the output buffer we retrieved.
ring_buffer_chan.Display(output_analysis_start - 16, output_analysis_start + 16, tag);
ring_buffer_chan.Display(output_analysis_start + kFreqTestBufSize - 16,
output_analysis_start + kFreqTestBufSize + 16,
"End of output analysis section");
}
auto channel_is_out_of_band = (channel_spec.freq_resp_lower_limits_db[0] == -INFINITY);
auto out_of_band = (freq_for_display < tc.low_cut_frequency ||
freq_for_display > tc.low_pass_frequency || channel_is_out_of_band);
double sinad_db, level_db = 0.0;
if (!out_of_band) {
auto result = MeasureAudioFreqs(output, {freq});
level_db = DoubleToDb(result.magnitudes[freq]);
sinad_db = DoubleToDb(result.magnitudes[freq] / result.total_magn_other);
if constexpr (kDisplayInProgressResults) {
FX_LOGS(INFO) << "Channel " << channel_spec.channel << ": " << std::setw(5)
<< freq_for_display << " Hz -- level " << std::fixed
<< std::setprecision(4) << std::setw(9) << level_db << " db, sinad "
<< std::setw(8) << sinad_db << " db";
}
} else {
// 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.
sinad_db = DoubleToDb(1.0 / MeasureAudioFreqs(output, {}).total_magn_other);
if constexpr (kDisplayInProgressResults) {
FX_LOGS(INFO) << "Channel " << channel_spec.channel << ": " << std::setw(5)
<< freq_for_display << " Hz -- out-of-band rejection " << std::fixed
<< std::setprecision(4) << 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_for_display == kFrequencyForSavedWavFiles) {
std::string test_name = tc.test_name + "_chan" + std::to_string(channel_spec.channel) +
"_" + std::to_string(freq_for_display) + "hz";
HermeticPipelineTest::WriteWavFile<OutputFormat>(test_name, "output", output);
}
}
// Retrieve the arrays of measurements for this path and channel
auto& curr_level_db =
level_results(tc.path, channel_spec.channel, tc.thermal_state.value_or(0));
auto& curr_sinad_db =
sinad_results(tc.path, channel_spec.channel, tc.thermal_state.value_or(0));
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_level_db[freq_idx] = level_db;
curr_sinad_db[freq_idx] = sinad_db;
}
}
}
if constexpr (kDisplaySummaryResults) {
DisplaySummaryResults(tc);
}
VerifyResults(tc);
}
// We only run the pipeline fidelity tests with FLOAT inputs/outputs, for full data precision.
template void HermeticFidelityTest::Run<ASF::FLOAT, ASF::FLOAT>(
const TestCase<ASF::FLOAT, ASF::FLOAT>& tc);
} // namespace media::audio::test