| // 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 <lib/syslog/cpp/macros.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/clock.h> |
| #include <zircon/types.h> |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdint> |
| #include <iomanip> |
| #include <limits> |
| #include <memory> |
| #include <set> |
| #include <sstream> |
| #include <vector> |
| |
| #include <fbl/algorithm.h> |
| #include <gmock/gmock.h> |
| |
| #include "src/media/audio/audio_core/mixer/gain.h" |
| #include "src/media/audio/audio_core/mixer/sinc_sampler.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/clock/utils.h" |
| #include "src/media/audio/lib/logging/logging.h" |
| #include "src/media/audio/lib/test/comparators.h" |
| #include "src/media/audio/lib/test/hermetic_audio_test.h" |
| |
| using ASF = fuchsia::media::AudioSampleFormat; |
| |
| namespace media::audio::test { |
| |
| class ClockSyncPipelineTest : public HermeticAudioTest { |
| struct Peak { |
| size_t index; |
| float value; |
| }; |
| |
| protected: |
| static constexpr size_t kFrameRate = 96000; |
| static constexpr size_t kPayloadFrames = 2 * kFrameRate; // 2sec ring buffer |
| static constexpr size_t kPacketFrames = kFrameRate * RendererShimImpl::kPacketMs / 1000; |
| |
| ClockSyncPipelineTest() : format_(Format::Create<ASF::FLOAT>(1, kFrameRate).value()) {} |
| |
| void TearDown() { |
| ExpectNoOverflowsOrUnderflows(); |
| HermeticAudioTest::TearDown(); |
| } |
| |
| virtual void Init(int32_t clock_slew_ppm, size_t num_frames_input) = 0; |
| virtual size_t ConvergenceFrames() const = 0; |
| virtual double NumFramesOutput(int32_t clock_slew_ppm, size_t num_frames_input) = 0; |
| |
| AudioBuffer<ASF::FLOAT> Impulse(float value = 0.5, size_t pre_silence_frames = 0, |
| size_t post_silence_frames = 0) { |
| AudioBuffer<ASF::FLOAT> out(format_, pre_silence_frames + 1 + post_silence_frames); |
| out.samples()[pre_silence_frames] = value; |
| return out; |
| } |
| |
| AudioBuffer<ASF::FLOAT> FillBuffer(size_t frames, float value = 0.5) { |
| AudioBuffer<ASF::FLOAT> out(format_, frames); |
| for (size_t s = 0; s < out.NumSamples(); s++) { |
| out.samples()[s] = value; |
| } |
| return out; |
| } |
| |
| // For a signal change occurring at frame T, how far BEFORE that frame will the effects of that |
| // change be reflected in the output. We use no effects; this comes from SincSampler only. |
| size_t PreRampFrames() { |
| auto mixer = mixer::SincSampler::Select(format_.stream_type(), format_.stream_type()); |
| return mixer->pos_filter_width().Ceiling(); |
| } |
| |
| // For a signal change occurring at frame T, how far AFTER that frame will the output reflect some |
| // effect of the previous signal. We use no effects; this comes from SincSampler only. |
| size_t PostRampFrames() { |
| auto mixer = mixer::SincSampler::Select(format_.stream_type(), format_.stream_type()); |
| return mixer->neg_filter_width().Ceiling(); |
| } |
| |
| // Maximum number of frames needed for a transition between two adjacent signals. At the beginning |
| // of this interval, the output begins to reflect the new signal; only at the end of this interval |
| // is the full effect shown. During this interval, the output is a cross-fading mixture of the |
| // preceding signal and the new signal. We use no effects; this comes from SincSampler only. |
| // These are SOURCE frames, but rates are so near unity that we safely use them interchangeably. |
| size_t TotalRampFrames() { return PreRampFrames() + PostRampFrames(); } |
| |
| // Offset of the first audio sample. This should be greater than TotalRampFrames() so that there |
| // is silence and then transitional frames at the start of the output, following by the signal. |
| // These are SOURCE frames, but rates are so near unity that we safely use them interchangeably. |
| size_t OffsetFrames() { |
| constexpr size_t kFramesOfSilence = 40; |
| EXPECT_TRUE(kFramesOfSilence > TotalRampFrames()) |
| << "For effective testing, OffsetFrames must exceed TotalRampFrames()"; |
| |
| return kFramesOfSilence; |
| } |
| |
| // Capture the ring buffer and rotate it leftward by the given offset, so the output starts at [0] |
| AudioBuffer<ASF::FLOAT> SnapshotRingBuffer(size_t offset_before_output_start) { |
| auto ring_buffer = output_->SnapshotRingBuffer(); |
| |
| offset_before_output_start %= ring_buffer.NumFrames(); |
| |
| auto shifted = |
| AudioBufferSlice(&ring_buffer, offset_before_output_start, ring_buffer.NumFrames()).Clone(); |
| shifted.Append(AudioBufferSlice(&ring_buffer, 0, offset_before_output_start)); |
| |
| return shifted; |
| } |
| |
| // Return the index of the peak sample, relative to the first frame in the slice. |
| Peak FindPeak(AudioBufferSlice<ASF::FLOAT> slice) { |
| EXPECT_TRUE(slice.format().channels() == 1) << "Channels must match"; |
| EXPECT_TRUE(slice.NumFrames() >= 1) << "Slice must contain data"; |
| size_t peak_idx = 0; |
| float peak_val = slice.SampleAt(0, 0); |
| for (size_t frame = 1; frame < slice.NumFrames(); ++frame) { |
| if (auto s = slice.SampleAt(frame, 0); std::abs(s) > std::abs(peak_val)) { |
| peak_idx = frame; |
| peak_val = s; |
| } |
| } |
| return {.index = peak_idx, .value = peak_val}; |
| } |
| |
| // Verify that the clock for this renderer is running at the expected rate |
| static void CheckClockRate(const zx::clock& clock, int32_t clock_slew_ppm) { |
| auto ref_clock_result = clock::GetClockDetails(clock); |
| ASSERT_TRUE(ref_clock_result.is_ok()); |
| |
| auto numerator = |
| static_cast<double>(ref_clock_result.value().mono_to_synthetic.rate.synthetic_ticks); |
| auto denominator = |
| static_cast<double>(ref_clock_result.value().mono_to_synthetic.rate.reference_ticks); |
| double measured_slew_ppm = (numerator * (1e6 / denominator)) - 1'000'000.0; |
| |
| // Don't wait for a driver clock to fully settle (a minute or more); accept a tolerance |
| constexpr double kSlewTolerance = 0.12; |
| EXPECT_NEAR(measured_slew_ppm, static_cast<double>(clock_slew_ppm), |
| fabs(clock_slew_ppm * kSlewTolerance)); |
| } |
| |
| // Send two impulses separated by frames_between_impulses, using a reference clock with the given |
| // slew. The output should contain two impulses separated by NumFramesOutput. |
| // |
| // This test validates that time is correctly translated between the two clocks. |
| // This test validates the following, with two 1-frame impulses during clock synchronization: |
| // A, The impulses are peak-detected in the output, with expected magnitudes; |
| // B. The impulse-to-impulse interval is the expected number of frames; |
| // C. The renderer clock is running at the expected rate. |
| // All measurements use tolerance ranges except where explicitly stated as exact. |
| void RunImpulseTest(int32_t clock_slew_ppm, size_t frames_between_impulses) { |
| constexpr double kInputImpulseMagnitude = 1.0; |
| constexpr double kOutputImpulseMagnitude = kInputImpulseMagnitude * 0.65; |
| constexpr bool kDebugOutputImpulseValues = false; |
| |
| // These should be zero, once lookahead/decay times are properly accounted-for. |
| const size_t kPreSilenceFrames = PreRampFrames(); |
| const size_t kPostSilenceFrames = PostRampFrames() * 2; |
| |
| Init(clock_slew_ppm, frames_between_impulses); |
| |
| // This is a precise timing test, so clocks must converge before we start. This can take |
| // multiple trips around our ring buffer, so below when calculating the expected start of the |
| // output signal, we must modulo it with the ring-buffer size. |
| auto offset_before_input_start = std::max(OffsetFrames(), ConvergenceFrames()); |
| |
| // We use single-frame impulses in the input signal |
| auto impulse = Impulse(kInputImpulseMagnitude, kPreSilenceFrames, kPostSilenceFrames); |
| |
| // Play two impulses frames_between_impulses apart. |
| auto first_input = renderer_->AppendPackets({&impulse}, offset_before_input_start); |
| auto second_input = |
| renderer_->AppendPackets({&impulse}, offset_before_input_start + frames_between_impulses); |
| |
| if constexpr (kDebugOutputImpulseValues) { |
| auto snapshot = renderer_->payload().Snapshot<ASF::FLOAT>(); |
| snapshot.Display(0, 2 * impulse.NumFrames()); |
| } |
| |
| renderer_->PlaySynchronized(this, output_, 0); |
| renderer_->WaitForPackets(this, first_input); |
| renderer_->WaitForPackets(this, second_input); |
| |
| auto offset_before_output_start = |
| static_cast<size_t>(NumFramesOutput(clock_slew_ppm, offset_before_input_start)); |
| // Shift the output so that neither "peak detection" range crosses the ring buffer boundary. |
| auto ring_buffer = SnapshotRingBuffer(offset_before_output_start); |
| |
| // A. two impulses are detected in the bisected output ring buffer |
| auto num_frames_output = NumFramesOutput(clock_slew_ppm, frames_between_impulses); |
| auto midpoint = num_frames_output / 2; |
| auto first_peak = FindPeak(AudioBufferSlice(&ring_buffer, 0, midpoint)); |
| auto second_peak = FindPeak(AudioBufferSlice(&ring_buffer, midpoint, ring_buffer.NumFrames())); |
| |
| if constexpr (kDebugOutputImpulseValues) { |
| FX_LOGS(INFO) << "Found impulse peaks of [" << first_peak.index << "] " << first_peak.value |
| << " and [" << midpoint + second_peak.index << "] " << second_peak.value; |
| auto first_start = first_peak.index - std::min(first_peak.index, PreRampFrames()); |
| auto second_start = midpoint + second_peak.index - PreRampFrames(); |
| ring_buffer.Display(first_start, first_start + TotalRampFrames()); |
| ring_buffer.Display(second_start, second_start + TotalRampFrames()); |
| } |
| |
| EXPECT_GE(first_peak.value, kOutputImpulseMagnitude); |
| EXPECT_GE(second_peak.value, kOutputImpulseMagnitude); |
| |
| // B. The distance between the two impulses should be num_frames_output. |
| auto peak_to_peak_frames = (midpoint + second_peak.index) - first_peak.index; |
| EXPECT_NEAR(static_cast<double>(peak_to_peak_frames), num_frames_output, 1.0); |
| |
| // C. clock rate check |
| CheckClockRate(renderer_->reference_clock(), clock_slew_ppm); |
| } |
| |
| // Send a flat signal (step function) of size num_frames_input, using a reference clock with the |
| // given slew. The output should contain an equivalent step function of size NumFramesOutput. |
| // |
| // Note, the exact values are not important. The primary goal of this test is to ensure the output |
| // does not have any dropped frames. A buggy mixer might drop frames if there is a gap between mix |
| // calls, specifically when the destination clock is running faster than the source clock. |
| // |
| // This test validates the following, rendering a step function during clock synchronization: |
| // A. The output step signal starts at the expected frame; |
| // B. The output step signal has the expected magnitude for its entirety (no dropouts); |
| // C. The output step signal ends at the expected frame; |
| // D. Subsequent output signal (after PostRampFrames) is precisely zero; |
| // E. The renderer clock is running at the expected rate. |
| // All measurements use tolerance ranges except where explicitly stated as exact. |
| void RunStepTest(int32_t clock_slew_ppm, size_t num_frames_input) { |
| constexpr double kInputStepMagnitude = 0.95; |
| constexpr double kOutputRelativeError = 0.025; |
| |
| Init(clock_slew_ppm, num_frames_input); |
| |
| // This is a precise timing test, so clocks must converge before we start. This can take |
| // multiple trips around our ring buffer, so below when calculating the expected start of the |
| // output signal, we must modulo it with the ring-buffer size. |
| auto offset_before_input_start = std::max(OffsetFrames(), ConvergenceFrames()); |
| auto input = FillBuffer(num_frames_input, kInputStepMagnitude); |
| |
| auto packets = renderer_->AppendPackets({&input}, offset_before_input_start); |
| renderer_->PlaySynchronized(this, output_, 0); |
| renderer_->WaitForPackets(this, packets); |
| |
| // NumFramesOutput returns a double. It's OK to truncate this: we insert transition ranges for |
| // filter TotalRampFrames, between the "must be silence" and "must be non-silence" ranges. |
| auto offset_before_output_start = static_cast<size_t>( |
| NumFramesOutput(clock_slew_ppm, offset_before_input_start - PreRampFrames())); |
| // We shift the output so that neither signal range nor silence range cross the ring's edge. |
| auto ring_buffer = SnapshotRingBuffer(offset_before_output_start); |
| |
| // The output should contain silence, followed by TotalRampFrames of transition, followed by |
| // data, followed by TotalRampFrames of transition, followed again by silence. Ultimately we're |
| // testing that we emit the correct number of output frames. Our test is necessarily imprecise, |
| // despite our using an input signal that is crisp and maximally detectable, because we ignore |
| // the sampler's ramp intervals when doing our "signal or silence" checks. To illustrate: |
| // |
| // max PreRampFrames max PostRampFrames |
| // | | | | |
| // | V num_frames_input V | |
| // \ +-----+-------------------------+-----+ | |
| // \ . | |
| // \ . | |
| // | . | |
| // V num_frames_output (longer) . V |
| // +-----+-----------------------------+-----+ |
| // |
| // |
| // In this case, we expect more output frames than input frames. However, since the delta |
| // is smaller than the maximum PostRampFrames, we cannot be sure if the extra frames are output |
| // or PostRampFrames. This means we cannot check if the system operated correctly. |
| // |
| // To address this problem, the diff between input and output frames must be greater than the |
| // TotalRampFrames. This is checked in Init(). |
| // |
| // We do not enforce a precise output duration or an exact step magnitude. We draw conservative |
| // boundaries around the output and verify that no dropped frames occur within the boundaries. |
| // |
| // We do not check data values during the TotalRampFrames transition, because sinc |
| // filter coefficients have zero-crossings, thus zero data values might be correct during |
| // transition (if the SRC ratio is 1:1, for example). In our shifted ring-buffer, this ramp |
| // begins at frame 0 (we include PreRampFrames() of frames of output before the signal begins). |
| auto num_frames_output = static_cast<size_t>(NumFramesOutput(clock_slew_ppm, num_frames_input)); |
| auto data_start = TotalRampFrames(); // signal reaches full strength |
| auto data_end = num_frames_output; // subsequent silence starts to ramp in |
| auto silence_start = data_start + num_frames_output; // our signal ramp out is complete |
| |
| // A. output step starts at expected frame. |
| // B. magnitude is within tolerance across the entire step range: no dropouts |
| auto data = AudioBufferSlice(&ring_buffer, data_start, data_end); |
| CompareAudioBufferOptions compare_opts; |
| compare_opts.test_label = fxl::StringPrintf("check data (starting at %lu)", data_start); |
| compare_opts.max_relative_error = kOutputRelativeError; |
| auto expect = AudioBufferSlice(&input, 0, data_end - data_start); |
| CompareAudioBuffers(data, expect, compare_opts); |
| |
| // C. output step ends at expected frame. |
| // D. subsequent range is entirely silent |
| auto silence = AudioBufferSlice(&ring_buffer, silence_start, ring_buffer.NumFrames()); |
| ExpectAudioBufferOptions expect_opts; |
| expect_opts.test_label = fxl::StringPrintf("check silence (starting at %lu)", silence_start); |
| ExpectSilentAudioBuffer(silence, expect_opts); |
| |
| // E. clock rate check |
| CheckClockRate(renderer_->reference_clock(), clock_slew_ppm); |
| } |
| |
| // Send a sine wave using a clock with given slew. The output should be a sine wave at slewed |
| // frequency. Each sinusoidal period contains (num_frames_to_analyze / input_freq) frames. |
| // |
| // This test validates the following, rendering a sinusoid during clock synchronization: |
| // A. The output signal's magnitude is essentially unattenuated (within tolerance); |
| // B. The output signal's center frequency is shifted by exactly the expected amount; |
| // C. No other frequencies exceed the noise floor threshold (with a few exceptions); |
| // D. The above-noise-floor frequencies are clustered around the primary output frequency; |
| // E. The width of that cluster (from leftmost to rightmost) is below a certain "peak width"; |
| // F. The renderer clock is running at the expected rate (within a certain tolerance). |
| void RunSineTest(int32_t clock_slew_ppm, size_t num_frames_to_analyze, size_t input_freq) { |
| constexpr double kInputSineMagnitude = 1.0; |
| constexpr double kExpectedOutputSineMagnitude = 0.99; |
| constexpr double kExpectedNoiseFloorDb = -72.0; |
| constexpr size_t kMaxPeakWidth = 2; |
| constexpr bool kDebugOutputSineValues = false; |
| |
| ASSERT_TRUE(fbl::is_pow2(num_frames_to_analyze)) |
| << "num_frames_to_analyze must be a power of 2"; |
| ASSERT_TRUE(num_frames_to_analyze < kPayloadFrames) |
| << "num_frames_to_analyze must fit into the ring-buffer"; |
| Init(clock_slew_ppm, num_frames_to_analyze); |
| |
| // This is a precise frequency detection test, so clocks must converge before we start. This can |
| // take multiple trips around our ring buffer, so below when calculating the start of the output |
| // signal, we must modulo it with the ring-buffer size. |
| auto offset_before_input_start = ConvergenceFrames(); |
| |
| // For fast input clocks, "output frames written" is less than "input frames consumed". |
| // To ensure we produce enough output frames for analysis, we repeat the first part of the input |
| // (specifically, half of the remaining space in the ring buffer). |
| // We can append this without a discontinuity, because the input signal's frequency guarantees |
| // that it fits exactly into num_frames_to_analyze frames (thus it can be perfectly looped). |
| auto actual_num_frames_input = |
| num_frames_to_analyze + (kPayloadFrames - num_frames_to_analyze) / 2; |
| auto input = |
| GenerateCosineAudio(format_, num_frames_to_analyze, input_freq, kInputSineMagnitude); |
| auto input_prefix = |
| AudioBufferSlice(&input, 0, actual_num_frames_input - num_frames_to_analyze); |
| |
| // Verify that this is enough output for our analysis (even after subtracting |
| // TotalRampFrames)... |
| ASSERT_TRUE(NumFramesOutput(clock_slew_ppm, actual_num_frames_input - TotalRampFrames()) > |
| num_frames_to_analyze); |
| // ... and that this additional output doesn't cause us to overrun the ring buffer. |
| ASSERT_TRUE(NumFramesOutput(clock_slew_ppm, actual_num_frames_input) < kPayloadFrames); |
| |
| auto packets = renderer_->AppendPackets({&input, input_prefix}, offset_before_input_start); |
| renderer_->PlaySynchronized(this, output_, 0); |
| renderer_->WaitForPackets(this, packets); |
| |
| // offset_before_input_start is input frame where signal starts. Add PostRampFrames to get the |
| // frame where any effect of preceding silence is completely gone. Translate to output frame. |
| auto offset_before_output_start = static_cast<size_t>( |
| NumFramesOutput(clock_slew_ppm, offset_before_input_start + PostRampFrames())); |
| |
| // Shift the entire buffer (with wraparound) to produce a full-length signal starting at [0]. |
| auto ring_buffer = SnapshotRingBuffer(offset_before_output_start); |
| |
| // Compute the slewed frequency in the output. |
| size_t output_freq = static_cast<double>(input_freq) * |
| static_cast<double>(num_frames_to_analyze) / |
| NumFramesOutput(clock_slew_ppm, num_frames_to_analyze); |
| |
| // As the mixer tracks the input clock's position, it may be a little ahead or behind, resulting |
| // in a cluster of detected frequencies, not just the single expected frequency. Measure this. |
| auto result = |
| MeasureAudioFreq(AudioBufferSlice(&ring_buffer, 0, num_frames_to_analyze), output_freq); |
| |
| // Ensure the FFT has a peak centered on freq. |
| double peak_magnitude = 0; |
| size_t peak_freq = 0; |
| for (size_t freq = 0; freq < result.all_square_magnitudes.size(); ++freq) { |
| if (auto magn = sqrt(result.all_square_magnitudes[freq]); magn > peak_magnitude) { |
| peak_magnitude = magn; |
| peak_freq = freq; |
| } |
| } |
| |
| if constexpr (kDebugOutputSineValues) { |
| double left_max_magn = 0, right_max_magn = 0; |
| for (ssize_t freq = 0; freq < output_freq; ++freq) { |
| auto magn = sqrt(result.all_square_magnitudes[freq]); |
| left_max_magn = std::max(left_max_magn, magn); |
| } |
| for (size_t freq = output_freq + 1; freq < result.all_square_magnitudes.size(); ++freq) { |
| auto magn = sqrt(result.all_square_magnitudes[freq]); |
| right_max_magn = std::max(right_max_magn, magn); |
| } |
| |
| printf("\nPeak frequency bin %zu, magnitude %9.6f. left-max %12.9f; right-max %12.9f\n", |
| peak_freq, peak_magnitude, left_max_magn, right_max_magn); |
| for (size_t freq = (peak_freq & ~0x07) - 64; freq < (peak_freq & ~0x07) + 64; ++freq) { |
| if (freq % 8 == 0) { |
| printf("\n[%zu] ", freq); |
| } |
| printf("%9.6f ", sqrt(result.all_square_magnitudes[freq])); |
| } |
| } |
| |
| // A. Input peak magnitude is 1.0. This will leak out to side freqs, but should remain high. |
| EXPECT_GE(peak_magnitude, kExpectedOutputSineMagnitude); |
| // B. Output frequency is shifted by the expected amount. |
| EXPECT_EQ(peak_freq, output_freq) << "magnitude at peak_freq = " << peak_magnitude; |
| |
| // C. We determine the minimal [peak_start, peak_end] range -- including our center output |
| // frequency -- such that no frequencies outside it exceed our noise floor. |
| // D. Our -75 dB noise floor is chosen somewhat arbitrary (12.5 bits of accurate signal). |
| const double kNoiseFloor = Gain::DbToScale(kExpectedNoiseFloorDb); |
| size_t peak_start = output_freq; |
| double left_max_magn = 0; |
| for (ssize_t freq = output_freq - 1; freq >= 0; --freq) { |
| auto magn = sqrt(result.all_square_magnitudes[freq]); |
| left_max_magn = std::max(left_max_magn, magn); |
| if (magn > kNoiseFloor) { |
| peak_start = freq; |
| } |
| } |
| size_t peak_end = output_freq; |
| double right_max_magn = 0; |
| for (size_t freq = output_freq + 1; freq < result.all_square_magnitudes.size(); ++freq) { |
| auto magn = sqrt(result.all_square_magnitudes[freq]); |
| right_max_magn = std::max(right_max_magn, magn); |
| if (magn > kNoiseFloor) { |
| peak_end = freq; |
| } |
| } |
| |
| // E. The peak should be sharply identified, if synchronization is stable & accurate. We |
| // expressly use a frequency matched to our power-of-2 length (thus require no windowing). |
| // Our peak width should span a single bin; we round out to 2. |
| bool peak_meets_requirements = (peak_end - peak_start <= kMaxPeakWidth); |
| EXPECT_TRUE(peak_meets_requirements) |
| << "At this noise floor, peak width is " << peak_end - peak_start |
| << ". At this width, noise floor is " << std::setprecision(4) |
| << std::log10(left_max_magn) * 20.0 << " dB / " << std::log10(right_max_magn) * 20 |
| << " dB (L/R)"; |
| |
| // F. clock rate check |
| CheckClockRate(renderer_->reference_clock(), clock_slew_ppm); |
| } |
| |
| const TypedFormat<ASF::FLOAT> format_; |
| VirtualOutput<ASF::FLOAT>* output_ = nullptr; |
| AudioRendererShim<ASF::FLOAT>* renderer_ = nullptr; |
| }; |
| |
| class MicroSrcPipelineTest : public ClockSyncPipelineTest { |
| public: |
| // Expected MicroSRC convergence time, in frames: about 15 mix periods at 10ms per period. |
| size_t ConvergenceFrames() const override { return 15 * kPacketFrames; } |
| |
| protected: |
| void Init(int32_t clock_slew_ppm, size_t num_frames_input) override { |
| zx::clock ref_clock = ::media::audio::clock::AdjustableCloneOfMonotonic(); |
| |
| zx::clock::update_args args; |
| args.reset().set_rate_adjust(clock_slew_ppm); |
| ASSERT_TRUE(ref_clock.update(args) == ZX_OK) << "Clock rate_adjust failed"; |
| |
| // Now that the clock is adjusted, remove ZX_RIGHT_WRITE before sending it (AudioCore never |
| // adjusts client-submitted clocks anyway, but this makes it truly impossible). |
| ref_clock = audio::clock::DuplicateClock(ref_clock).take_value(); |
| |
| // Buffer up to 2s of data. |
| output_ = CreateOutput({{0xff, 0x00}}, format_, kPayloadFrames); |
| renderer_ = CreateAudioRenderer(format_, kPayloadFrames, |
| fuchsia::media::AudioRenderUsage::MEDIA, std::move(ref_clock)); |
| |
| // Any initial offset, plus the signal, should fit entirely into the ring buffer |
| auto offset_before_input_start = std::max(OffsetFrames(), ConvergenceFrames()); |
| ASSERT_TRUE(num_frames_input + offset_before_input_start < kPayloadFrames) |
| << "input signal is too big for the ring buffer"; |
| |
| if (clock_slew_ppm) { |
| // In Impulse/Step testing, the length change must exceed transition time, to be detectable. |
| size_t num_frames_output = NumFramesOutput(clock_slew_ppm, num_frames_input); |
| ASSERT_TRUE(std::abs(static_cast<ssize_t>(num_frames_input - num_frames_output)) > |
| static_cast<ssize_t>(TotalRampFrames())) |
| << "Change in signal length is too small to be detectable"; |
| } |
| } |
| |
| double NumFramesOutput(int32_t clock_slew_ppm, size_t num_frames_input) override { |
| return static_cast<double>(num_frames_input) * |
| (1e6 / (1e6 + static_cast<double>(clock_slew_ppm))); |
| } |
| }; |
| |
| class AdjustableClockPipelineTest : public ClockSyncPipelineTest { |
| public: |
| // Expected device clock convergence time in frames. |
| size_t ConvergenceFrames() const override { return 13 * kFrameRate; } |
| |
| protected: |
| void Init(int32_t clock_slew_ppm, size_t num_frames_input) override { |
| // Specify the clock rate for the output device. |
| constexpr int32_t kMonotonicDomain = 0; |
| constexpr int32_t kNonMonotonicDomain = 1; |
| DeviceClockProperties clock_properties = { |
| .domain = (clock_slew_ppm ? kNonMonotonicDomain : kMonotonicDomain), |
| .initial_rate_adjustment_ppm = clock_slew_ppm, |
| }; |
| |
| // Buffer up to 2s of data. |
| output_ = |
| CreateOutput({{0xff, 0x00}}, format_, kPayloadFrames, std::nullopt, 0.0, clock_properties); |
| |
| // With this uninitialized clock, instruct AudioRenderer to use AudioCore's clock. |
| renderer_ = CreateAudioRenderer(format_, kPayloadFrames, |
| fuchsia::media::AudioRenderUsage::MEDIA, zx::clock()); |
| } |
| |
| double NumFramesOutput(int32_t clock_slew_ppm, size_t num_frames_input) override { |
| return num_frames_input; |
| } |
| }; |
| |
| // Use these to debug the tests in the absence of rate-adjustment |
| // |
| // TEST_F(MicroSrcPipelineTest, ImpulseBaseline) { RunImpulseTest(0, kFrameRate); } |
| // TEST_F(MicroSrcPipelineTest, StepBaseline) { RunStepTest(0, kFrameRate); } |
| // TEST_F(MicroSrcPipelineTest, SineBaseline) { RunSineTest(0, 131072, 20000); } |
| // TEST_F(AdjustableClockPipelineTest, ImpulseBaseline) { RunImpulseTest(0, kFrameRate); } |
| // TEST_F(AdjustableClockPipelineTest, StepBaseline) { RunStepTest(0, kFrameRate); } |
| // TEST_F(AdjustableClockPipelineTest, SineBaseline) { RunSineTest(0, 131072, 20000); } |
| |
| // The maximum clock skew is +/-1000 PPM. These tests use a skew less than the maximum, so the two |
| // sides have a chance to converge (at the maximum, the slow side can never fully catch up). |
| // To be discernable from the TotalRampFrames interval, the skew must also be > 291 PPM. |
| // At 96k rate, to make the offset an exact integer, clock skew should be a multiple of 125. |
| TEST_F(MicroSrcPipelineTest, ImpulseUp500) { RunImpulseTest(500, kFrameRate); } |
| TEST_F(MicroSrcPipelineTest, ImpulseUp875) { RunImpulseTest(875, kFrameRate); } |
| TEST_F(MicroSrcPipelineTest, ImpulseDown500) { RunImpulseTest(-500, kFrameRate); } |
| |
| TEST_F(AdjustableClockPipelineTest, ImpulseUp500) { RunImpulseTest(500, kFrameRate); } |
| TEST_F(AdjustableClockPipelineTest, ImpulseDown500) { RunImpulseTest(-500, kFrameRate); } |
| |
| TEST_F(MicroSrcPipelineTest, StepUp500) { RunStepTest(500, kFrameRate); } |
| TEST_F(MicroSrcPipelineTest, StepDown500) { RunStepTest(-500, kFrameRate); } |
| TEST_F(MicroSrcPipelineTest, StepDown625) { RunStepTest(-625, kFrameRate); } |
| |
| TEST_F(AdjustableClockPipelineTest, StepUp500) { RunStepTest(500, kFrameRate); } |
| TEST_F(AdjustableClockPipelineTest, StepDown500) { RunStepTest(-500, kFrameRate); } |
| |
| // For best precision in measuring resultant signal frequency, input signal frequency should be |
| // high, but with room for upward slew without approaching the Nyquist limit(num_input_frames/2). |
| // Input frequency is a multiple of slew, to make expected resultant frequency a round number. |
| // |
| // Sine test input buffer length: the largest power-of-2 (in frames) that fits into 2 secs @ 96kHz. |
| // The numbers below work out to a frequency of 20k / (131072/96kHz) = 14.648 kHz. |
| TEST_F(MicroSrcPipelineTest, SineUp500) { RunSineTest(500, 131072, 20000); } |
| TEST_F(MicroSrcPipelineTest, SineDown500) { RunSineTest(-500, 131072, 20000); } |
| TEST_F(MicroSrcPipelineTest, SineDown750) { RunSineTest(-750, 131072, 20000); } |
| |
| TEST_F(AdjustableClockPipelineTest, SineUp500) { RunSineTest(500, 131072, 20000); } |
| TEST_F(AdjustableClockPipelineTest, SineDown500) { RunSineTest(-500, 131072, 20000); } |
| |
| } // namespace media::audio::test |