| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/media/audio/audio_core/mix_stage.h" |
| |
| #include <zircon/syscalls.h> |
| |
| #include <fbl/string_printf.h> |
| #include <ffl/string.h> |
| #include <gmock/gmock.h> |
| |
| #include "src/media/audio/audio_core/mixer/constants.h" |
| #include "src/media/audio/audio_core/mixer/gain.h" |
| #include "src/media/audio/audio_core/packet_queue.h" |
| #include "src/media/audio/audio_core/ring_buffer.h" |
| #include "src/media/audio/audio_core/testing/fake_stream.h" |
| #include "src/media/audio/audio_core/testing/packet_factory.h" |
| #include "src/media/audio/audio_core/testing/threading_model_fixture.h" |
| #include "src/media/audio/lib/clock/clone_mono.h" |
| #include "src/media/audio/lib/clock/testing/clock_test.h" |
| #include "src/media/audio/lib/clock/utils.h" |
| #include "src/media/audio/lib/format/constants.h" |
| #include "src/media/audio/lib/processing/gain.h" |
| |
| using testing::Each; |
| using testing::FloatEq; |
| |
| namespace media::audio { |
| |
| namespace { |
| |
| // Used when the ReadLockContext is unused by the test. |
| static media::audio::ReadableStream::ReadLockContext rlctx; |
| |
| enum class ClockMode { SAME, WITH_OFFSET, RATE_ADJUST }; |
| |
| constexpr uint32_t kDefaultNumChannels = 2; |
| constexpr uint32_t kDefaultFrameRate = 48000; |
| const Format kDefaultFormat = |
| Format::Create(fuchsia::media::AudioStreamType{ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = kDefaultNumChannels, |
| .frames_per_second = kDefaultFrameRate, |
| }) |
| .take_value(); |
| } // namespace |
| |
| class MixStageTest : public testing::ThreadingModelFixture { |
| protected: |
| static constexpr uint32_t kBlockSizeFrames = 240; |
| |
| void SetUp() { |
| zx::clock zx_device_clock = clock::CloneOfMonotonic(); |
| auto clock_result = audio::clock::DuplicateClock(zx_device_clock); |
| ASSERT_TRUE(clock_result.is_ok()); |
| zx::clock zx_clone_device_clock = clock_result.take_value(); |
| |
| device_clock_ = context().clock_factory()->CreateDeviceFixed(std::move(zx_device_clock), |
| AudioClock::kMonotonicDomain); |
| clone_of_device_clock_ = context().clock_factory()->CreateDeviceFixed( |
| std::move(zx_clone_device_clock), AudioClock::kMonotonicDomain); |
| |
| mix_stage_ = std::make_shared<MixStage>(kDefaultFormat, kBlockSizeFrames, timeline_function_, |
| *device_clock_); |
| } |
| |
| int64_t duration_to_frames(zx::duration delta) { |
| return kDefaultFormat.frames_per_ns().Scale(delta.to_nsecs()); |
| } |
| |
| fbl::RefPtr<VersionedTimelineFunction> timeline_function_ = |
| fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(TimelineRate( |
| Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| |
| // Views the memory at |ptr| as a std::array of |N| elements of |T|. If |offset| is provided, it |
| // is the number of |T| sized elements to skip at the beginning of |ptr|. |
| // |
| // It is entirely up to the caller to ensure that values of |T|, |N|, and |offset| are chosen to |
| // not overflow |ptr|. |
| template <typename T, size_t N> |
| std::array<T, N>& as_array(void* ptr, size_t offset = 0) { |
| return reinterpret_cast<std::array<T, N>&>(static_cast<T*>(ptr)[offset]); |
| } |
| |
| std::unique_ptr<AudioClock> SetPacketFactoryWithOffsetAudioClock(zx::duration clock_offset, |
| testing::PacketFactory& factory); |
| void TestMixStageTrim(ClockMode clock_mode); |
| void TestMixStageUniformFormats(ClockMode clock_mode); |
| void TestMixStageSingleInput(ClockMode clock_mode); |
| |
| void ValidateIsPointSampler(std::shared_ptr<Mixer> should_be_point) { |
| EXPECT_LT(should_be_point->pos_filter_width(), Fixed(1)) |
| << "Mixer pos_filter_width " << should_be_point->pos_filter_width().raw_value() |
| << " too large, should be less than " << Fixed(1).raw_value(); |
| } |
| |
| void ValidateIsSincSampler(std::shared_ptr<Mixer> should_be_sinc) { |
| EXPECT_GT(should_be_sinc->pos_filter_width(), Fixed(1)) |
| << "Mixer pos_filter_width " << should_be_sinc->pos_filter_width().raw_value() |
| << " too small, should be greater than " << Fixed(1).raw_value(); |
| } |
| |
| std::shared_ptr<MixStage> mix_stage_; |
| |
| std::unique_ptr<AudioClock> device_clock_; |
| std::unique_ptr<AudioClock> clone_of_device_clock_; |
| }; |
| |
| TEST_F(MixStageTest, AddInput_MixerSelection) { |
| const Format kSameFrameRate = |
| Format::Create(fuchsia::media::AudioStreamType{ |
| .sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16, |
| .channels = 1, |
| .frames_per_second = kDefaultFrameRate, |
| }) |
| .take_value(); |
| |
| const Format kDiffFrameRate = |
| Format::Create(fuchsia::media::AudioStreamType{ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = kDefaultNumChannels, |
| .frames_per_second = kDefaultFrameRate / 2, |
| }) |
| .take_value(); |
| |
| auto timeline = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| auto tl_same = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kSameFrameRate.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| auto tl_different = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kDiffFrameRate.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| |
| auto adjustable_device_clock = context().clock_factory()->CreateDeviceAdjustable( |
| clock::AdjustableCloneOfMonotonic(), AudioClock::kMonotonicDomain + 1); |
| auto adjustable_device_mix_stage = std::make_shared<MixStage>(kDefaultFormat, kBlockSizeFrames, |
| timeline, *adjustable_device_clock); |
| auto fixed_device_clock = context().clock_factory()->CreateDeviceFixed( |
| clock::CloneOfMonotonic(), AudioClock::kMonotonicDomain); |
| auto fixed_device_mix_stage = |
| std::make_shared<MixStage>(kDefaultFormat, kBlockSizeFrames, timeline, *fixed_device_clock); |
| |
| auto adjustable_client_same_rate = std::make_shared<PacketQueue>( |
| kSameFrameRate, tl_same, |
| context().clock_factory()->CreateClientAdjustable(clock::AdjustableCloneOfMonotonic())); |
| auto adjustable_client_diff_rate = std::make_shared<PacketQueue>( |
| kDiffFrameRate, tl_different, |
| context().clock_factory()->CreateClientAdjustable(clock::AdjustableCloneOfMonotonic())); |
| auto custom_same_rate = std::make_shared<PacketQueue>( |
| kSameFrameRate, tl_same, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| |
| // client adjustable should lead to Point, if same rate |
| ValidateIsPointSampler(adjustable_device_mix_stage->AddInput(adjustable_client_same_rate)); |
| ValidateIsPointSampler(fixed_device_mix_stage->AddInput(adjustable_client_same_rate)); |
| |
| // client adjustable should lead to Sinc, if not same rate |
| ValidateIsSincSampler(adjustable_device_mix_stage->AddInput(adjustable_client_diff_rate)); |
| ValidateIsSincSampler(fixed_device_mix_stage->AddInput(adjustable_client_diff_rate)); |
| |
| // custom clock should lead to Sinc, even if same rate, regardless of hardware-control |
| ValidateIsSincSampler(adjustable_device_mix_stage->AddInput(custom_same_rate)); |
| ValidateIsSincSampler(fixed_device_mix_stage->AddInput(custom_same_rate)); |
| |
| // The default heuristic can still be explicitly indicated, and behaves as above. |
| ValidateIsPointSampler(adjustable_device_mix_stage->AddInput( |
| adjustable_client_same_rate, std::nullopt, Mixer::Resampler::Default)); |
| ValidateIsPointSampler(fixed_device_mix_stage->AddInput(adjustable_client_same_rate, std::nullopt, |
| Mixer::Resampler::Default)); |
| ValidateIsSincSampler(adjustable_device_mix_stage->AddInput( |
| adjustable_client_diff_rate, std::nullopt, Mixer::Resampler::Default)); |
| ValidateIsSincSampler(fixed_device_mix_stage->AddInput(adjustable_client_diff_rate, std::nullopt, |
| Mixer::Resampler::Default)); |
| ValidateIsSincSampler(adjustable_device_mix_stage->AddInput(custom_same_rate, std::nullopt, |
| Mixer::Resampler::Default)); |
| ValidateIsSincSampler( |
| fixed_device_mix_stage->AddInput(custom_same_rate, std::nullopt, Mixer::Resampler::Default)); |
| |
| // |
| // For all, explicit mixer selection can still countermand our default heuristic |
| // |
| // WindowedSinc can still be explicitly specified in same-rate no-microSRC situations |
| ValidateIsSincSampler(adjustable_device_mix_stage->AddInput( |
| adjustable_client_same_rate, std::nullopt, Mixer::Resampler::WindowedSinc)); |
| ValidateIsSincSampler(fixed_device_mix_stage->AddInput(adjustable_client_same_rate, std::nullopt, |
| Mixer::Resampler::WindowedSinc)); |
| |
| // SampleAndHold can still be explicitly specified, even in different-rate situations |
| ValidateIsPointSampler(adjustable_device_mix_stage->AddInput( |
| adjustable_client_diff_rate, std::nullopt, Mixer::Resampler::SampleAndHold)); |
| ValidateIsPointSampler(fixed_device_mix_stage->AddInput(adjustable_client_diff_rate, std::nullopt, |
| Mixer::Resampler::SampleAndHold)); |
| |
| // SampleAndHold can still be explicitly specified, even in microSRC situations |
| ValidateIsPointSampler(adjustable_device_mix_stage->AddInput(custom_same_rate, std::nullopt, |
| Mixer::Resampler::SampleAndHold)); |
| ValidateIsPointSampler(fixed_device_mix_stage->AddInput(custom_same_rate, std::nullopt, |
| Mixer::Resampler::SampleAndHold)); |
| } |
| |
| // TODO(fxbug.dev/50004): Add tests to verify we can read from mix stages with unaligned frames. |
| |
| std::unique_ptr<AudioClock> MixStageTest::SetPacketFactoryWithOffsetAudioClock( |
| zx::duration clock_offset, testing::PacketFactory& factory) { |
| auto custom_clock = |
| clock::testing::CreateCustomClock({.start_val = zx::clock::get_monotonic() + clock_offset}) |
| .take_value(); |
| |
| auto actual_offset = clock::testing::GetOffsetFromMonotonic(custom_clock).take_value(); |
| |
| int64_t seek_frame = round( |
| static_cast<double>(kDefaultFormat.frames_per_second() * actual_offset.get()) / ZX_SEC(1)); |
| factory.SeekToFrame(Fixed(seek_frame)); |
| |
| return context().clock_factory()->CreateClientFixed(std::move(custom_clock)); |
| } |
| |
| void MixStageTest::TestMixStageTrim(ClockMode clock_mode) { |
| // Set timeline rate to match our format. |
| auto timeline_function = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| |
| std::shared_ptr<PacketQueue> packet_queue; |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| |
| if (clock_mode == ClockMode::SAME) { |
| packet_queue = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| } else if (clock_mode == ClockMode::WITH_OFFSET) { |
| auto custom_audio_clock = SetPacketFactoryWithOffsetAudioClock(zx::sec(-2), packet_factory); |
| |
| packet_queue = std::make_shared<PacketQueue>(kDefaultFormat, timeline_function, |
| std::move(custom_audio_clock)); |
| } else { |
| ASSERT_TRUE(clock_mode == ClockMode::RATE_ADJUST) << "Unknown clock mode"; |
| ASSERT_TRUE(false) << "Multi-rate testing not yet implemented"; |
| } |
| |
| mix_stage_->AddInput(packet_queue); |
| |
| bool packet1_released = false; |
| bool packet2_released = false; |
| |
| packet_queue->PushPacket(packet_factory.CreatePacket( |
| 1.0, zx::msec(5), [&packet1_released] { packet1_released = true; })); |
| packet_queue->PushPacket(packet_factory.CreatePacket( |
| 0.5, zx::msec(5), [&packet2_released] { packet2_released = true; })); |
| |
| // Because of how we set up custom clocks, we can't reliably Trim to a specific frame number (we |
| // might be off by half a frame), so we allow ourselves one frame of tolerance either direction. |
| constexpr int64_t kToleranceFrames = 1; |
| |
| // Before 5ms: packet1 is not yet entirely consumed; we should still retain both packets. |
| mix_stage_->Trim(Fixed(duration_to_frames(zx::msec(5)) - kToleranceFrames)); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(packet1_released); |
| |
| // After 5ms: packet1 is consumed and should have been released. We should still retain packet2. |
| mix_stage_->Trim(Fixed(duration_to_frames(zx::msec(5)) + kToleranceFrames)); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(packet1_released); |
| EXPECT_FALSE(packet2_released); |
| |
| // Before 10ms: packet2 is not yet entirely consumed; we should still retain it. |
| mix_stage_->Trim(Fixed(duration_to_frames(zx::msec(10)) - kToleranceFrames)); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(packet2_released); |
| |
| // After 10ms: packet2 is consumed and should have been released. |
| mix_stage_->Trim(Fixed(duration_to_frames(zx::msec(10)) + kToleranceFrames)); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(packet2_released); |
| |
| // Upon any fail, slab_allocator asserts at exit. Clear all allocations, so testing can continue. |
| mix_stage_->Trim(Fixed::Max()); |
| } |
| |
| TEST_F(MixStageTest, Trim) { TestMixStageTrim(ClockMode::SAME); } |
| TEST_F(MixStageTest, Trim_ClockOffset) { TestMixStageTrim(ClockMode::WITH_OFFSET); } |
| |
| void MixStageTest::TestMixStageUniformFormats(ClockMode clock_mode) { |
| // Set timeline rate to match our format. |
| auto timeline_function = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| |
| // Create 2 PacketQueues that we mix together. One may have a clock with an offset, so create a |
| // seperate PacketFactory for it, that can set timestamps appropriately. |
| testing::PacketFactory packet_factory1(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| testing::PacketFactory packet_factory2(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| |
| auto packet_queue1 = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| std::shared_ptr<PacketQueue> packet_queue2; |
| |
| if (clock_mode == ClockMode::SAME) { |
| packet_queue2 = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| } else if (clock_mode == ClockMode::WITH_OFFSET) { |
| auto custom_audio_clock = SetPacketFactoryWithOffsetAudioClock(zx::sec(10), packet_factory2); |
| |
| packet_queue2 = std::make_shared<PacketQueue>(kDefaultFormat, timeline_function, |
| std::move(custom_audio_clock)); |
| } else { |
| ASSERT_TRUE(clock_mode == ClockMode::RATE_ADJUST) << "Unknown clock mode"; |
| ASSERT_TRUE(false) << "Multi-rate testing not yet implemented"; |
| } |
| |
| mix_stage_->AddInput(packet_queue1, std::nullopt, Mixer::Resampler::SampleAndHold); |
| mix_stage_->AddInput(packet_queue2, std::nullopt, Mixer::Resampler::SampleAndHold); |
| |
| // Mix 2 packet queues with the following samples and expected outputs. We'll feed this data |
| // through the mix stage in 3 passes of 2ms windows: |
| // |
| // ----------------------------------- |
| // q1 | 0.1 | 0.2 | 0.2 | 0.3 | 0.3 | 0.3 | |
| // ----------------------------------- |
| // q2 | 0.7 | 0.7 | 0.7 | 0.5 | 0.5 | 0.3 | |
| // ----------------------------------- |
| // mix | 0.8 | 0.9 | 0.9 | 0.8 | 0.8 | 0.6 | |
| // ----------------------------------- |
| // pass | 1 | 2 | 3 | |
| // ----------------------------------- |
| |
| { |
| packet_queue1->PushPacket(packet_factory1.CreatePacket(0.1, zx::msec(1))); |
| packet_queue1->PushPacket(packet_factory1.CreatePacket(0.2, zx::msec(2))); |
| packet_queue1->PushPacket(packet_factory1.CreatePacket(0.3, zx::msec(3))); |
| } |
| { |
| packet_queue2->PushPacket(packet_factory2.CreatePacket(0.7, zx::msec(3))); |
| packet_queue2->PushPacket(packet_factory2.CreatePacket(0.5, zx::msec(2))); |
| packet_queue2->PushPacket(packet_factory2.CreatePacket(0.3, zx::msec(1))); |
| } |
| |
| int64_t output_frame_start = 0; |
| uint32_t output_frame_count = 96; |
| { // Mix frames 0-2ms. Expect 1 ms of 0.8 values, then 1 ms of 0.9 values. |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(output_frame_start), output_frame_count); |
| // 1ms @ 48000hz == 48 frames. 2ms == 96 (frames). |
| ASSERT_TRUE(buf); |
| ASSERT_EQ(buf->length(), 96); |
| // Each frame is 2 channels, so 1ms will be 96 samples. |
| auto& arr1 = as_array<float, 96>(buf->payload(), 0); |
| EXPECT_THAT(arr1, Each(FloatEq(0.8f))) |
| << std::setprecision(5) << "[0] " << arr1[0] << ", [1] " << arr1[1] << ", [94] " << arr1[94] |
| << ", [95] " << arr1[95]; |
| auto& arr2 = as_array<float, 96>(buf->payload(), 96); |
| EXPECT_THAT(arr2, Each(FloatEq(0.9f))) |
| << std::setprecision(5) << "[0] " << arr2[0] << ", [1] " << arr2[1] << ", [94] " << arr2[94] |
| << ", [95] " << arr2[95]; |
| } |
| |
| output_frame_start += output_frame_count; |
| { // Mix frames 2-4ms. Expect 1 ms of 0.9 samples, then 1 ms of 0.8 values. |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(output_frame_start), output_frame_count); |
| ASSERT_TRUE(buf); |
| ASSERT_EQ(buf->length(), 96); |
| |
| auto& arr1 = as_array<float, 96>(buf->payload(), 0); |
| EXPECT_THAT(arr1, Each(FloatEq(0.9f))) |
| << std::setprecision(5) << "[0] " << arr1[0] << ", [1] " << arr1[1] << ", [94] " << arr1[94] |
| << ", [95] " << arr1[95]; |
| auto& arr2 = as_array<float, 96>(buf->payload(), 96); |
| EXPECT_THAT(arr2, Each(FloatEq(0.8f))) |
| << std::setprecision(5) << "[0] " << arr2[0] << ", [1] " << arr2[1] << ", [94] " << arr2[94] |
| << ", [95] " << arr2[95]; |
| } |
| |
| output_frame_start += output_frame_count; |
| { // Mix frames 4-6ms. Expect 1 ms of 0.8 values, then 1 ms of 0.6 values. |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(output_frame_start), output_frame_count); |
| ASSERT_TRUE(buf); |
| ASSERT_EQ(buf->length(), 96); |
| |
| auto& arr1 = as_array<float, 96>(buf->payload(), 0); |
| EXPECT_THAT(arr1, Each(FloatEq(0.8f))) |
| << std::setprecision(5) << "[0] " << arr1[0] << ", [1] " << arr1[1] << ", [94] " << arr1[94] |
| << ", [95] " << arr1[95]; |
| auto& arr2 = as_array<float, 96>(buf->payload(), 96); |
| EXPECT_THAT(arr2, Each(FloatEq(0.6f))) |
| << std::setprecision(5) << "[0] " << arr2[0] << ", [1] " << arr2[1] << ", [94] " << arr2[94] |
| << ", [95] " << arr2[95]; |
| } |
| |
| // Upon any fail, slab_allocator asserts at exit. Clear all allocations, so testing can continue. |
| mix_stage_->Trim(Fixed::Max()); |
| } |
| |
| TEST_F(MixStageTest, MixUniformFormats) { TestMixStageUniformFormats(ClockMode::SAME); } |
| TEST_F(MixStageTest, MixUniformFormats_ClockOffset) { |
| TestMixStageUniformFormats(ClockMode::WITH_OFFSET); |
| } |
| |
| // Validate that a mixer with significant filter width can pull from a source buffer in pieces |
| // (assuming there is sufficient additional read-ahead data to satisfy the filter width!). |
| TEST_F(MixStageTest, MixFromRingBuffersSinc) { |
| // Note: there are non-obvious constraints on the size of this ring because of how we test below. |
| // In ReadLock we specify both a number of frames AND a source reference time to not read beyond. |
| // We specify to read at most 1 msec of source, while specifying a number-of-frames well less than |
| // that. However, filter width is included in these calculations, which means that: |
| // *** Half of the ring duration, PLUS the mixer filter width, must not exceed 1 msec of source. |
| // Currently SincSampler's positive_width is 13 frames so (at 48k) our ring must be <= 70 frames. |
| // This test should be adjusted if SincSampler's filter width increases. |
| constexpr uint32_t kRingSizeFrames = 64; |
| constexpr uint32_t kRingSizeSamples = kRingSizeFrames * kDefaultNumChannels; |
| constexpr uint32_t kFramesPerMs = 48; |
| |
| // Create a new RingBuffer and add it to our mix stage. |
| int64_t safe_write_frame = 0; |
| auto ring_buffer_endpoints = BaseRingBuffer::AllocateSoftwareBuffer( |
| kDefaultFormat, timeline_function_, *device_clock_, kRingSizeFrames, |
| [&safe_write_frame] { return safe_write_frame; }); |
| |
| // We explictly request a SincSampler here to get a non-trivial filter width. |
| mix_stage_->AddInput(ring_buffer_endpoints.reader, std::nullopt, Mixer::Resampler::WindowedSinc); |
| |
| // Fill up the ring buffer with non-empty samples so we can observe them in the mix output. |
| // The first half of the ring is one value, the second half is another. |
| constexpr float kRingBufferSampleValue1 = 0.5; |
| constexpr float kRingBufferSampleValue2 = 0.7; |
| float* ring_buffer_samples = reinterpret_cast<float*>(ring_buffer_endpoints.writer->virt()); |
| for (size_t sample = 0; sample < kRingSizeSamples / 2; ++sample) { |
| ring_buffer_samples[sample] = kRingBufferSampleValue1; |
| ring_buffer_samples[kRingSizeSamples / 2 + sample] = kRingBufferSampleValue2; |
| } |
| |
| // Read the ring in two halves, each is assigned a different source value in the ring above. |
| constexpr uint32_t kRequestedFrames = kRingSizeFrames / 2; |
| { |
| safe_write_frame = 1 * kFramesPerMs; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| ASSERT_EQ(buf->start().Floor(), 0u); |
| ASSERT_EQ(buf->length(), kRequestedFrames); |
| |
| auto& arr = as_array<float, kRequestedFrames * kDefaultNumChannels>(buf->payload(), 0); |
| EXPECT_THAT(arr, Each(FloatEq(kRingBufferSampleValue1))) |
| << std::setprecision(5) << "[0] " << arr[0] << ", [" << (arr.size() - 1) << "] " |
| << arr[arr.size() - 1]; |
| } |
| |
| { |
| safe_write_frame = 2 * kFramesPerMs; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(kRequestedFrames), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| ASSERT_EQ(buf->start().Floor(), kRequestedFrames); |
| ASSERT_EQ(buf->length(), kRequestedFrames); |
| |
| auto& arr = as_array<float, kRequestedFrames * kDefaultNumChannels>(buf->payload(), 0); |
| EXPECT_THAT(arr, Each(FloatEq(kRingBufferSampleValue2))) |
| << std::setprecision(5) << "[0] " << arr[0] << ", [" << (arr.size() - 1) << "] " |
| << arr[arr.size() - 1]; |
| } |
| } |
| |
| TEST_F(MixStageTest, MixNoInputs) { |
| constexpr uint32_t kRequestedFrames = 48; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| |
| // With no inputs, we should return nullopt. |
| ASSERT_FALSE(buf); |
| } |
| |
| TEST_F(MixStageTest, MixSilentInput) { |
| // Add a silent input. |
| auto stream = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory()); |
| stream->set_usage_mask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)}); |
| stream->set_gain_db(fuchsia::media::audio::MUTED_GAIN_DB); |
| // Set timeline rate to match our format. |
| stream->timeline_function()->Update(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| mix_stage_->AddInput(stream); |
| |
| constexpr uint32_t kRequestedFrames = 48; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| |
| // If an input is silent, we can return silence. |
| ASSERT_FALSE(buf); |
| } |
| |
| TEST_F(MixStageTest, MixSilentInputWithNonSilentInput) { |
| // Add a silent input. |
| auto silent_stream = |
| std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory()); |
| silent_stream->set_usage_mask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)}); |
| silent_stream->set_gain_db(fuchsia::media::audio::MUTED_GAIN_DB); |
| // Set timeline rate to match our format. |
| silent_stream->timeline_function()->Update(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| mix_stage_->AddInput(silent_stream); |
| |
| // Add a non-silent input. |
| auto non_silent_stream = |
| std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory()); |
| non_silent_stream->set_usage_mask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)}); |
| non_silent_stream->set_gain_db(0.0); |
| // Set timeline rate to match our format. |
| non_silent_stream->timeline_function()->Update(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| mix_stage_->AddInput(non_silent_stream); |
| |
| constexpr uint32_t kRequestedFrames = 48; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| |
| // If an input is silent, we can return silence. |
| ASSERT_TRUE(buf); |
| } |
| |
| static constexpr auto kInputStreamUsage = StreamUsage::WithRenderUsage(RenderUsage::INTERRUPTION); |
| void MixStageTest::TestMixStageSingleInput(ClockMode clock_mode) { |
| // Set timeline rate to match our format. |
| auto timeline_function = fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| std::shared_ptr<PacketQueue> packet_queue; |
| |
| if (clock_mode == ClockMode::SAME) { |
| packet_queue = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| } else if (clock_mode == ClockMode::WITH_OFFSET) { |
| auto custom_audio_clock = SetPacketFactoryWithOffsetAudioClock(zx::sec(5), packet_factory); |
| |
| packet_queue = std::make_shared<PacketQueue>(kDefaultFormat, timeline_function, |
| std::move(custom_audio_clock)); |
| } else { |
| ASSERT_TRUE(clock_mode == ClockMode::RATE_ADJUST) << "Unknown clock mode"; |
| ASSERT_TRUE(false) << "Multi-rate testing not yet implemented"; |
| } |
| |
| packet_queue->set_usage(kInputStreamUsage); |
| mix_stage_->AddInput(packet_queue); |
| |
| packet_queue->PushPacket(packet_factory.CreatePacket(1.0, zx::msec(5))); |
| |
| { |
| constexpr uint32_t kRequestedFrames = 48; |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_TRUE(buf->usage_mask().contains(kInputStreamUsage)); |
| EXPECT_FLOAT_EQ(buf->total_applied_gain_db(), media_audio::kUnityGainDb); |
| } |
| |
| // Upon any fail, slab_allocator asserts at exit. Clear all allocations, so testing can continue. |
| mix_stage_->Trim(Fixed::Max()); |
| mix_stage_->RemoveInput(*packet_queue); |
| } |
| |
| TEST_F(MixStageTest, MixSingleInput) { TestMixStageSingleInput(ClockMode::SAME); } |
| TEST_F(MixStageTest, MixSingleInput_ClockOffset) { |
| TestMixStageSingleInput(ClockMode::WITH_OFFSET); |
| } |
| |
| TEST_F(MixStageTest, MixMultipleInputs) { |
| // Set timeline rate to match our format. |
| auto timeline_function = TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())); |
| |
| auto input1 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input1->timeline_function()->Update(timeline_function); |
| auto input2 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input2->timeline_function()->Update(timeline_function); |
| mix_stage_->AddInput(input1); |
| mix_stage_->AddInput(input2); |
| |
| constexpr uint32_t kRequestedFrames = 48; |
| |
| // The buffer should return the union of the usage mask, and the largest of the input gains. |
| input1->set_usage_mask(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)})); |
| input1->set_gain_db(-160); |
| input2->set_usage_mask( |
| StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION)})); |
| input2->set_gain_db(-15); |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(buf->usage_mask(), StreamUsageMask({ |
| StreamUsage::WithRenderUsage(RenderUsage::MEDIA), |
| StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION), |
| })); |
| EXPECT_FLOAT_EQ(buf->total_applied_gain_db(), -15); |
| } |
| } |
| |
| // When mixing streams, a buffer's total_applied_gain_db is set, based on the largest of its |
| // inputs. Each input's total_applied_gain_db is determined by ITS input's total_applied_gain_db, |
| // plus its dest_gain. |
| // |
| // Validate that source_gain is appropriately incorporated and the correct (max) value is returned. |
| TEST_F(MixStageTest, BufferGainDbIncludesSourceGain) { |
| // Set timeline rate to match our format. |
| auto timeline_function = TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())); |
| |
| auto input1 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input1->timeline_function()->Update(timeline_function); |
| auto input2 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input2->timeline_function()->Update(timeline_function); |
| auto mixer1 = mix_stage_->AddInput(input1); |
| auto mixer2 = mix_stage_->AddInput(input2); |
| |
| constexpr uint32_t kRequestedFrames = 48; |
| |
| // The buffer should return the union of the usage mask, and the largest of the input gains. |
| input1->set_usage_mask(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)})); |
| input1->set_gain_db(1.0); |
| mixer1->bookkeeping().gain.SetSourceGain(-160); |
| input2->set_usage_mask( |
| StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION)})); |
| input2->set_gain_db(0.0); |
| mixer2->bookkeeping().gain.SetSourceGain(-15); |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(buf->usage_mask(), StreamUsageMask({ |
| StreamUsage::WithRenderUsage(RenderUsage::MEDIA), |
| StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION), |
| })); |
| // If the source gain is included in the calculation, then input2 should be the larger value. |
| EXPECT_FLOAT_EQ(buf->total_applied_gain_db(), -15.0); |
| } |
| } |
| |
| // Validate that dest_gain is appropriately incorporated and the correct (max) value is returned. |
| TEST_F(MixStageTest, BufferMaxAmplitudeIncludesDestGain) { |
| // Set timeline rate to match our format. |
| auto timeline_function = TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())); |
| |
| auto input1 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input1->timeline_function()->Update(timeline_function); |
| auto input2 = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| input2->timeline_function()->Update(timeline_function); |
| auto mixer1 = mix_stage_->AddInput(input1); |
| auto mixer2 = mix_stage_->AddInput(input2); |
| |
| constexpr uint32_t kRequestedFrames = 48; |
| |
| // The buffer should return the union of the usage mask, and the largest of the input gains. |
| input1->set_usage_mask(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)})); |
| input1->set_gain_db(1.0); |
| mixer1->bookkeeping().gain.SetDestGain(-160); |
| input2->set_usage_mask( |
| StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION)})); |
| input2->set_gain_db(0.0); |
| mixer2->bookkeeping().gain.SetDestGain(-15); |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(buf->usage_mask(), StreamUsageMask({ |
| StreamUsage::WithRenderUsage(RenderUsage::MEDIA), |
| StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION), |
| })); |
| // If destination gain is included in the calculation, then input2 should be the larger value. |
| EXPECT_FLOAT_EQ(buf->total_applied_gain_db(), -15); |
| } |
| } |
| |
| TEST_F(MixStageTest, MixWithRingOut) { |
| // Set timeline rate to match our format. |
| auto timeline_function = TimelineFunction( |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs())); |
| |
| auto input = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory()); |
| input->timeline_function()->Update(timeline_function); |
| auto mixer = mix_stage_->AddInput(input, 0, Mixer::Resampler::WindowedSinc); |
| |
| const auto ring_out = Fixed(mixer->neg_filter_width() + mixer->pos_filter_width()).Ceiling(); |
| ASSERT_GT(ring_out, Fixed(0)); |
| |
| constexpr uint32_t kRequestedFrames = 20; |
| input->set_max_frame(kRequestedFrames); |
| |
| // First mix should return a buffer with 20 frames. |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), kRequestedFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(kRequestedFrames, buf->length()); |
| } |
| |
| // Next mix should return a buffer with `ring_out` frames. |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(kRequestedFrames), ring_out); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(ring_out, buf->length()); |
| } |
| |
| // Beyond the ring-out frames, no mix output should be produced. |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(kRequestedFrames + ring_out), 200); |
| ASSERT_FALSE(buf); |
| } |
| } |
| |
| TEST_F(MixStageTest, CachedUntilFullyConsumed) { |
| // Create a packet queue to use as our source stream. |
| auto stream = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function_, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| |
| // Enqueue 10ms of frames in the packet queue. All samples will be initialized to 1.0. |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| bool packet_released = false; |
| stream->PushPacket(packet_factory.CreatePacket(1.0, zx::msec(10), |
| [&packet_released] { packet_released = true; })); |
| auto mix_stage = |
| std::make_shared<MixStage>(kDefaultFormat, 480, timeline_function_, *device_clock_); |
| auto mixer = mix_stage->AddInput(stream); |
| |
| // After mixing half the packet, the packet should not be released. |
| { |
| auto buf = mix_stage->ReadLock(rlctx, Fixed(0), 240); |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(0u, buf->start().Floor()); |
| EXPECT_EQ(240u, buf->length()); |
| EXPECT_EQ(1.0, static_cast<float*>(buf->payload())[0]); |
| EXPECT_FALSE(packet_released); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(packet_released); |
| |
| // After mixing all of the packet, the packet should be released. |
| // However, we set fully consumed = false so the mix buffer will be cached. |
| { |
| auto buf = mix_stage->ReadLock(rlctx, Fixed(240), 240); |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(240, buf->start().Floor()); |
| EXPECT_EQ(240, buf->length()); |
| EXPECT_EQ(1.0, static_cast<float*>(buf->payload())[0]); |
| EXPECT_TRUE(packet_released); |
| buf->set_frames_consumed(0); |
| } |
| |
| // Mixing again should return the same buffer. |
| // This time we set fully consumed = true to discard the cached mix result. |
| { |
| auto buf = mix_stage->ReadLock(rlctx, Fixed(240), 240); |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(240, buf->start().Floor()); |
| EXPECT_EQ(240, buf->length()); |
| EXPECT_EQ(1.0, static_cast<float*>(buf->payload())[0]); |
| buf->set_frames_consumed(240); |
| } |
| |
| // The mix buffer is not cached and the packet is gone. |
| // Skipping past the "ring out" region, we must produce silence. |
| { |
| const auto ring_out = Fixed(mixer->neg_filter_width() + mixer->pos_filter_width()).Ceiling(); |
| auto buf = mix_stage->ReadLock(rlctx, Fixed(480) + ring_out, 480); |
| RunLoopUntilIdle(); |
| ASSERT_FALSE(buf); |
| } |
| } |
| |
| TEST_F(MixStageTest, FirstPacketOffsetLargerThanBlockSize) { |
| // Create a packet queue to use as our source stream. |
| auto stream = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function_, |
| context().clock_factory()->CreateClientFixed(clock::CloneOfMonotonic())); |
| |
| // Enqueue 10ms of frames in the packet queue starting just after the first block. |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| packet_factory.SeekToFrame(Fixed(kBlockSizeFrames + 1)); |
| stream->PushPacket(packet_factory.CreatePacket(1.0, zx::msec(10), [] {})); |
| |
| // packet_factory must outlive mix_stage_. |
| mix_stage_ = std::make_shared<MixStage>(kDefaultFormat, kBlockSizeFrames, timeline_function_, |
| *device_clock_); |
| auto mixer = mix_stage_->AddInput(stream, std::nullopt, Mixer::Resampler::SampleAndHold); |
| |
| // Request the first four blocks. What should happen: |
| // |
| // 1. MixStage requests first block from the packet queue, plus 1 extra frame for the point |
| // sampler's filter width. No packet covers this range, so the packet queue returns nullopt. |
| // Since the MixStage has no source data to mix, it also returns nullopt. |
| // |
| // 2. MixStage requests the second block from the packet queue, plus 1 extra frame. The packet |
| // starts 1 frame into the second block, so this should return 1 frame of silence followed |
| // by the first kBlockSizeFrames-1 frames of the packet. |
| // |
| { |
| auto buf = mix_stage_->ReadLock(rlctx, Fixed(0), 4 * kBlockSizeFrames); |
| ASSERT_TRUE(buf); |
| EXPECT_EQ(buf->start().Floor(), kBlockSizeFrames); |
| EXPECT_EQ(buf->length(), kBlockSizeFrames); |
| EXPECT_EQ(static_cast<float*>(buf->payload())[0], 0.0); |
| EXPECT_EQ(static_cast<float*>(buf->payload())[kDefaultNumChannels], 1.0); |
| EXPECT_EQ(static_cast<float*>(buf->payload())[kBlockSizeFrames - 1], 1.0); |
| } |
| |
| // Trim away the packet so its callback runs before we tear down the PacketFactory. |
| mix_stage_->Trim(Fixed(4 * kBlockSizeFrames)); |
| RunLoopUntilIdle(); |
| } |
| |
| // Double-check the reset of rate-adjustment coefficients upon first ReadLock call, and validate |
| // that source_pos_modulo is not being double-incremented. |
| TEST_F(MixStageTest, PositionResetAndAdvance) { |
| constexpr int32_t dest_frames_per_mix = 96; |
| |
| // We set our timeline slow by 1 source_pos_modulo unit per frame. |
| auto nsec_to_frac_source = |
| fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(TimelineRate( |
| Fixed(kDefaultFormat.frames_per_second()).raw_value() - 1, zx::sec(1).to_nsecs()))); |
| // Set PacketQueue with a clone of the device clock, so micro-SRC doesn't engage. |
| std::shared_ptr<PacketQueue> packet_queue = std::make_shared<PacketQueue>( |
| kDefaultFormat, nsec_to_frac_source, std::move(clone_of_device_clock_)); |
| |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| bool packet_released = false; |
| packet_queue->PushPacket(packet_factory.CreatePacket(1.0, zx::msec(2))); |
| packet_queue->PushPacket(packet_factory.CreatePacket(2.0, zx::msec(2))); |
| packet_queue->PushPacket(packet_factory.CreatePacket( |
| 3.0, zx::msec(2), [&packet_released] { packet_released = true; })); |
| |
| auto mixer = mix_stage_->AddInput(packet_queue, 0.0f, Mixer::Resampler::WindowedSinc); |
| auto& info = mixer->source_info(); |
| auto& bookkeeping = mixer->bookkeeping(); |
| |
| bookkeeping.SetRateModuloAndDenominator(76543, 98765); |
| bookkeeping.source_pos_modulo = 23456; |
| |
| auto source_pos_for_read_lock = Fixed(0); |
| // The first mix resets position, so the above will be overwritten and we'll advance from zero. |
| { |
| auto buffer = mix_stage_->ReadLock(rlctx, source_pos_for_read_lock, dest_frames_per_mix); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(source_pos_for_read_lock.Floor(), buffer->start().Floor()); |
| EXPECT_EQ(dest_frames_per_mix, buffer->length()); |
| source_pos_for_read_lock += Fixed(dest_frames_per_mix); |
| |
| // At a 48k nominal rate, we expect rate_modulo to be 47999 and denom to be 48000. |
| EXPECT_EQ(bookkeeping.step_size, Fixed(kOneFrame - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << bookkeeping.step_size; |
| EXPECT_EQ(bookkeeping.rate_modulo(), |
| static_cast<uint64_t>(kDefaultFormat.frames_per_second()) - 1); |
| EXPECT_EQ(bookkeeping.denominator(), static_cast<uint64_t>(kDefaultFormat.frames_per_second())); |
| |
| // source_pos_modulo should show that we lose 1 source_pos_modulo per dest frame. |
| EXPECT_EQ(bookkeeping.source_pos_modulo, |
| bookkeeping.denominator() - source_pos_for_read_lock.Floor()); |
| // ... which also means we'll be one frac-frame behind. |
| EXPECT_EQ(info.next_source_frame, Fixed(Fixed(info.next_dest_frame) - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << info.next_source_frame; |
| } |
| |
| { |
| auto buffer = mix_stage_->ReadLock(rlctx, source_pos_for_read_lock, dest_frames_per_mix); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(source_pos_for_read_lock.Floor(), buffer->start().Floor()); |
| EXPECT_EQ(dest_frames_per_mix, buffer->length()); |
| source_pos_for_read_lock += Fixed(dest_frames_per_mix); |
| |
| EXPECT_EQ(bookkeeping.step_size, Fixed(kOneFrame - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << bookkeeping.step_size; |
| EXPECT_EQ(bookkeeping.rate_modulo(), |
| static_cast<uint64_t>(kDefaultFormat.frames_per_second()) - 1); |
| EXPECT_EQ(bookkeeping.denominator(), static_cast<uint64_t>(kDefaultFormat.frames_per_second())); |
| |
| EXPECT_EQ(bookkeeping.source_pos_modulo, |
| bookkeeping.denominator() - source_pos_for_read_lock.Floor()); |
| EXPECT_EQ(info.next_source_frame, Fixed(Fixed(info.next_dest_frame) - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << info.next_source_frame; |
| } |
| |
| // Subsequent mixes should not reset position, so this change should persist. |
| bookkeeping.source_pos_modulo += 17; |
| { |
| auto buffer = mix_stage_->ReadLock(rlctx, Fixed(source_pos_for_read_lock), dest_frames_per_mix); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(source_pos_for_read_lock.Floor(), buffer->start().Floor()); |
| EXPECT_EQ(dest_frames_per_mix, buffer->length()); |
| source_pos_for_read_lock += Fixed(dest_frames_per_mix); |
| |
| EXPECT_EQ(bookkeeping.step_size, Fixed(kOneFrame - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << bookkeeping.step_size; |
| EXPECT_EQ(bookkeeping.rate_modulo(), |
| static_cast<uint64_t>(kDefaultFormat.frames_per_second()) - 1); |
| EXPECT_EQ(bookkeeping.denominator(), static_cast<uint64_t>(kDefaultFormat.frames_per_second())); |
| |
| // source_pos_modulo shows the offset, and still losing 1 source_pos_modulo per dest frame |
| EXPECT_EQ(bookkeeping.source_pos_modulo, |
| bookkeeping.denominator() - source_pos_for_read_lock.Floor() + 17); |
| EXPECT_EQ(info.next_source_frame, Fixed(Fixed(info.next_dest_frame) - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << info.next_source_frame; |
| } |
| |
| packet_queue->Flush(); |
| while (!packet_released) { |
| RunLoopUntilIdle(); |
| } |
| } |
| |
| // This is a regression test for fxbug.dev/67996. |
| TEST_F(MixStageTest, DontCrashOnDestOffsetRoundingError) { |
| // Unused, but MixStage::ProcessMix needs this argument. |
| auto input = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory(), |
| zx_system_get_page_size()); |
| |
| // As summarized in the calculations at the link below, the following hard-coded source_info |
| // values result in dest_offset = 301. In order for this offset to not overflow the dest buffer, |
| // we need at least 302 frames in the MixStage output buffer. |
| // https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=67996#c22 |
| // |
| // We use 480, which is 10ms at 48kHz. |
| mix_stage_ = std::make_shared<MixStage>(kDefaultFormat, 480 /* block size in frames */, |
| timeline_function_, *device_clock_); |
| |
| // First step of ReadLock. |
| memset(&mix_stage_->cur_mix_job_, 0, sizeof(mix_stage_->cur_mix_job_)); |
| |
| // The following values are derived from an actual crash. We set only the values needed by |
| // MixStage::ProcessMix. The crux of the bug is that the dest clock's adjusted rate of -1 PPM |
| // caused a rounding error. See discussion at fxbug.dev/67996#c22. |
| mix_stage_->cur_mix_job_.buf = &mix_stage_->output_buffer_[0]; |
| mix_stage_->cur_mix_job_.buf_frames = mix_stage_->output_buffer_frames_; |
| mix_stage_->cur_mix_job_.dest_ref_clock_to_frac_dest_frame = TimelineFunction(); |
| mix_stage_->cur_mix_job_.read_lock_ctx = &rlctx; |
| |
| auto stream = std::make_shared<testing::FakeStream>(kDefaultFormat, context().clock_factory()); |
| auto mixer = mix_stage_->AddInput(input, std::nullopt, Mixer::Resampler::SampleAndHold); |
| mixer->source_info().dest_frames_to_frac_source_frames = |
| TimelineFunction(3582737759, 0, 8192000, 999); |
| mixer->source_info().next_source_frame = Fixed::FromRaw(2414202275419); |
| mixer->bookkeeping().step_size = Fixed(1); |
| mixer->bookkeeping().SetRateModuloAndDenominator(0, 1); |
| |
| // So the next ReadLock call returns a buffer with: |
| // start = Fixed::FromRaw(2414204747776) |
| // length = Fixed(10) |
| stream->Trim(Fixed::FromRaw(2414204747776)); |
| stream->set_max_frame(10); |
| mix_stage_->MixStream(*mixer, *input); |
| } |
| |
| // When a packet starts after the mix starts, position should be advanced per step_size|rate_mod, |
| // including updating source_pos_modulo (not simply scaled with a TimelineRate). |
| TEST_F(MixStageTest, PositionSkip) { |
| constexpr int32_t dest_frames_per_mix = 48; // 1ms |
| |
| // We set our timeline slow by 1 frac-frame per msec, to create source_pos_modulo activity. |
| auto nsec_to_frac_source = |
| fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(TimelineRate( |
| Fixed(kDefaultFormat.frames_per_second()).raw_value() - 1, zx::sec(1).to_nsecs()))); |
| std::shared_ptr<PacketQueue> packet_queue = std::make_shared<PacketQueue>( |
| kDefaultFormat, nsec_to_frac_source, std::move(clone_of_device_clock_)); |
| |
| testing::PacketFactory packet_factory(dispatcher(), kDefaultFormat, zx_system_get_page_size()); |
| bool packet_released = false; |
| packet_queue->PushPacket(packet_factory.CreatePacket( |
| 1.0, zx::msec(1), [&packet_released] { packet_released = true; })); |
| |
| auto mixer = mix_stage_->AddInput(packet_queue, 0.0f, Mixer::Resampler::WindowedSinc); |
| |
| auto source_pos_for_read_lock = Fixed(-mixer->pos_filter_width() + Fixed::FromRaw(4000)); |
| // The first mix resets position, so the above will be overwritten and we'll advance from zero. |
| { |
| auto buffer = mix_stage_->ReadLock(rlctx, source_pos_for_read_lock, dest_frames_per_mix); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(source_pos_for_read_lock.Floor(), buffer->start().Floor()); |
| EXPECT_EQ(dest_frames_per_mix, buffer->length()); |
| source_pos_for_read_lock += Fixed(dest_frames_per_mix); |
| |
| // At a 48k nominal rate, we expect rate_modulo to be 47999 and denom to be 48000. |
| // source_pos_modulo should show that we lose 1 source_pos_modulo per dest frame. |
| // ... which also means our running source position will be 1 frac-frame behind. |
| auto& bookkeeping = mixer->bookkeeping(); |
| EXPECT_EQ(bookkeeping.step_size, Fixed(kOneFrame - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << bookkeeping.step_size; |
| EXPECT_EQ(bookkeeping.rate_modulo(), |
| static_cast<uint64_t>(kDefaultFormat.frames_per_second()) - 1); |
| EXPECT_EQ(bookkeeping.denominator(), static_cast<uint64_t>(kDefaultFormat.frames_per_second())); |
| |
| auto& info = mixer->source_info(); |
| EXPECT_EQ(info.next_dest_frame, source_pos_for_read_lock.Floor()); |
| EXPECT_EQ(info.next_source_frame, Fixed(Fixed(info.next_dest_frame) - Fixed::FromRaw(1))) |
| << ffl::String::DecRational << info.next_source_frame; |
| |
| EXPECT_EQ(bookkeeping.source_pos_modulo, bookkeeping.denominator() - dest_frames_per_mix); |
| } |
| |
| packet_queue->Flush(); |
| while (!packet_released) { |
| RunLoopUntilIdle(); |
| } |
| } |
| |
| class MixStagePositionTest : public MixStageTest { |
| protected: |
| static constexpr int32_t kDestFramesPerMix = 96; |
| |
| void SetUpWithClock(std::unique_ptr<AudioClock> clock) { |
| packet_queue_ = |
| std::make_shared<PacketQueue>(kDefaultFormat, timeline_function_, std::move(clock)); |
| info_ = |
| &(mix_stage_->AddInput(packet_queue_, 0.0f, Mixer::Resampler::WindowedSinc)->source_info()); |
| |
| // Before the first mix: position relationship should not be set |
| EXPECT_EQ(info()->source_ref_clock_to_frac_source_frames_generation, kInvalidGenerationId); |
| // Request the initial mix: position relationship should be set |
| mix_stage_->ReadLock(rlctx, Fixed(0), kDestFramesPerMix); |
| } |
| |
| zx::duration GetDurationErrorForFracFrameError(Fixed frac_source_error, |
| uint64_t source_pos_modulo = 0, |
| uint64_t denominator = 1) { |
| auto clock = clock::testing::CreateCustomClock({.synthetic_offset_from_mono = zx::duration(0)}) |
| .take_value(); |
| auto packet_queue = std::make_shared<PacketQueue>( |
| kDefaultFormat, timeline_function_, |
| context().clock_factory()->CreateClientFixed(std::move(clock))); |
| auto mixer = mix_stage_->AddInput(packet_queue, 0.0f, Mixer::Resampler::WindowedSinc); |
| auto& info = mixer->source_info(); |
| |
| // This method is called multiple times from the same test. |
| // To avoid source-goes-backwards errors, reset the timeline function before calling ReadLock. |
| auto timeline_snapshot = timeline_function_->get(); |
| timeline_function_->Update(TimelineFunction()); |
| timeline_function_->Update(timeline_snapshot.first); |
| |
| // Initial mix |
| mix_stage_->ReadLock(rlctx, Fixed(0), kDestFramesPerMix); |
| RunLoopUntilIdle(); |
| |
| EXPECT_NE(info.source_ref_clock_to_frac_source_frames_generation, kInvalidGenerationId); |
| EXPECT_EQ(info.next_dest_frame, kDestFramesPerMix); |
| EXPECT_EQ(info.source_pos_error, zx::duration(0)); |
| |
| // Inject error, mix |
| info.next_source_frame += frac_source_error; |
| |
| auto& bookkeeping = mixer->bookkeeping(); |
| FX_CHECK(source_pos_modulo < denominator); |
| if (denominator > 1) { |
| bookkeeping.SetRateModuloAndDenominator(1, denominator); |
| bookkeeping.source_pos_modulo = std::min(source_pos_modulo, bookkeeping.denominator() - 1); |
| } |
| mix_stage_->ReadLock(rlctx, Fixed(kDestFramesPerMix), kDestFramesPerMix); |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(info.next_dest_frame, 2 * kDestFramesPerMix); |
| return info.source_pos_error; |
| } |
| |
| void ExpectPositionOffsetsAfterMix(int64_t pre_mix_dest_offset, Fixed pre_mix_source_offset, |
| int64_t post_mix_dest_offset, Fixed post_mix_source_offset) { |
| Fixed expect_long_running_source_pos = |
| info()->next_source_frame + Fixed(kDestFramesPerMix) + post_mix_source_offset; |
| info()->next_source_frame = info()->next_source_frame + pre_mix_source_offset; |
| |
| auto expect_long_running_dest_pos = |
| info()->next_dest_frame + kDestFramesPerMix + post_mix_dest_offset; |
| info()->next_dest_frame = info()->next_dest_frame + pre_mix_dest_offset; |
| |
| mix_stage_->ReadLock(rlctx, Fixed(kDestFramesPerMix), kDestFramesPerMix); |
| EXPECT_EQ(info()->next_source_frame, expect_long_running_source_pos); |
| EXPECT_EQ(info()->next_dest_frame, expect_long_running_dest_pos); |
| } |
| |
| media::audio::Mixer::SourceInfo* info() { return info_; } |
| |
| private: |
| media::audio::Mixer::SourceInfo* info_; |
| std::shared_ptr<PacketQueue> packet_queue_; |
| }; |
| |
| // Verify that SourceInfo.source_pos_error is set to zero if less than one fractional frame. |
| TEST_F(MixStagePositionTest, PosError_IgnoreOneFracFrame) { |
| { |
| SCOPED_TRACE("position_error 0 frac frames"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed(0)).to_nsecs(), 0); |
| } |
| { |
| // Source position error 1 frac frame should be ignored. |
| SCOPED_TRACE("position_error 1 frac frame"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(1)).to_nsecs(), 0); |
| } |
| { |
| // Source position error -1 frac frame should be ignored. |
| SCOPED_TRACE("position_error -1 frac frame"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(-1)).to_nsecs(), 0); |
| } |
| { |
| // Source position error 2 frac frames is not ignored. |
| SCOPED_TRACE("position_error 2 frac frames"); |
| EXPECT_GT(GetDurationErrorForFracFrameError(Fixed::FromRaw(2)).to_nsecs(), 0); |
| } |
| { |
| // Source position error -2 frac frames is not ignored. |
| SCOPED_TRACE("position_error -2 frac frames"); |
| EXPECT_LT(GetDurationErrorForFracFrameError(Fixed::FromRaw(-2)).to_nsecs(), 0); |
| } |
| } |
| |
| // Verify that SourceInfo.source_pos_error correctly rounds to a ns-based equivalent. |
| TEST_F(MixStagePositionTest, PosError_RoundToNs) { |
| // Validate floor behavior without pos_modulo/denominator present |
| { |
| // Source position error 3 frac frames is 7.6 ns, rounds out to 8ns. |
| SCOPED_TRACE("position_error 3 frac frames"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(3)).to_nsecs(), 8); |
| } |
| { |
| // Source position error -3 frac frames is -7.6 ns, rounds out to -8ns. |
| SCOPED_TRACE("position_error -3 frac frames"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(-3)).to_nsecs(), -8); |
| } |
| { |
| // Source position error 8 frac frames is 20.4 ns, rounds in to 20ns. |
| SCOPED_TRACE("position_error 8 frac frames"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(8)).to_nsecs(), 20); |
| } |
| { |
| // Source position error -8 frac frames is -20.4 ns, rounds in to -20ns. |
| SCOPED_TRACE("position_error -8 frac frames"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(-8)).to_nsecs(), -20); |
| } |
| } |
| |
| // Verify that SourceInfo.source_pos_error correctly incorporates source_pos_modulo. |
| TEST_F(MixStagePositionTest, PosError_IncludePosModulo) { |
| // Validate floor behavior plus pos_modulo / denominator contribution |
| { |
| // Source position error 2 +56/100 frac frames is 6.51ns, rounds out to 7ns. |
| SCOPED_TRACE("position_error 2 frac frames plus 56/100"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(2), 56, 100).to_nsecs(), 7); |
| } |
| { |
| // Source position error -2 +23/100 (1.77) frac frames is -4.5ns, rounds out to -5ns. |
| SCOPED_TRACE("position_error -2 frac frames plus 23/100"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(-2), 23, 100).to_nsecs(), -5); |
| } |
| { |
| // Source position error 1 +37/100 frac frames is 3.48ns, rounds in to 3ns, which is ignored. |
| SCOPED_TRACE("position_error 1 frac frame plus 37/100"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(1), 37, 100).to_nsecs(), 0); |
| } |
| { |
| // Source position error -2 +24/100 (1.76) frac frames is -4.48ns, rounds in to -4ns. |
| SCOPED_TRACE("position_error -2 frac frames plus 24/100"); |
| EXPECT_EQ(GetDurationErrorForFracFrameError(Fixed::FromRaw(-2), 24, 100).to_nsecs(), -4); |
| } |
| } |
| |
| // SourceInfo.source_ref_clock_to_frac_source_frames_generation tracks changes in the position |
| // relationship between source and its reference clock. When the stream first starts, the |
| // TimelineFunction is set and the generation is updated. It is updated on both Pause and Play, so |
| // that when Playback resumes the new position relationship is reestablished. |
| // |
| // Verify that SourceInfo.source_ref_clock_to_frac_source_frames_generation updates appropriately on |
| // first mix, Pause and Play. |
| TEST_F(MixStagePositionTest, SourceDestPositionRelationship) { |
| SetUpWithClock(std::move(clone_of_device_clock_)); |
| EXPECT_EQ(info()->source_ref_clock_to_frac_source_frames_generation, 1u); |
| |
| auto long_running_source_pos = info()->next_source_frame; |
| // Pause the timeline and request another mix: position relationship should be cleared |
| timeline_function_->Update(TimelineFunction(Fixed(kDestFramesPerMix).raw_value(), |
| zx::clock::get_monotonic().get(), {0, 1})); |
| mix_stage_->ReadLock(rlctx, Fixed(kDestFramesPerMix), kDestFramesPerMix); |
| EXPECT_EQ(info()->source_ref_clock_to_frac_source_frames_generation, 2u); |
| |
| // Restart the timeline and request another mix: position relationship should be set |
| timeline_function_->Update(TimelineFunction( |
| Fixed(2 * kDestFramesPerMix).raw_value(), zx::clock::get_monotonic().get(), |
| TimelineRate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| mix_stage_->ReadLock(rlctx, Fixed(2 * kDestFramesPerMix), kDestFramesPerMix); |
| EXPECT_EQ(info()->source_ref_clock_to_frac_source_frames_generation, 3u); |
| EXPECT_EQ(info()->next_source_frame, long_running_source_pos + Fixed(2 * kDestFramesPerMix)) |
| << ffl::String::DecRational << info()->next_source_frame; |
| |
| EXPECT_EQ(info()->next_dest_frame, Fixed(3 * kDestFramesPerMix).Floor()); |
| } |
| |
| // On forward dest discontinuity beyond the acceptable 2ms threshold, long-running pos for both dest |
| // and source are reset. |
| TEST_F(MixStagePositionTest, DestDiscontinuityBeyondThreshold) { |
| SetUpWithClock(std::move(clone_of_device_clock_)); |
| // MixStage should reset both dest and source, then advance normally |
| ExpectPositionOffsetsAfterMix(-100, Fixed(0), 0, Fixed(0)); |
| } |
| |
| // On forward dest discontinuity within the acceptable 2ms threshold, long-running pos for both dest |
| // and source are correctly advanced. |
| TEST_F(MixStagePositionTest, DestDiscontinuityWithinThreshold) { |
| SetUpWithClock(std::move(clone_of_device_clock_)); |
| // MixStage should advance both dest and source by that same 96, then advance normally |
| ExpectPositionOffsetsAfterMix(-96, Fixed(0), 0, Fixed(96)); |
| } |
| |
| // On source discontinuity with clocks that indicate "no-sync needed", long-running pos for both |
| // dest and source are not adjusted. |
| TEST_F(MixStagePositionTest, SourceDiscontinuityNoSync) { |
| SetUpWithClock(std::move(clone_of_device_clock_)); |
| // MixStage should not adjust these but merely advance normally |
| ExpectPositionOffsetsAfterMix(0, Fixed(300), 0, Fixed(300)); |
| } |
| |
| // On source discontinuity beyond the recoverability threshold, long-running source pos is reset. |
| TEST_F(MixStagePositionTest, SourceDiscontinuityBeyondThreshold) { |
| auto non_clone = context().clock_factory()->CreateClientFixed( |
| clock::testing::CreateCustomClock({.synthetic_offset_from_mono = zx::duration(0)}) |
| .take_value()); |
| SetUpWithClock(std::move(non_clone)); |
| // MixStage should reset source, then advance normally |
| ExpectPositionOffsetsAfterMix(0, Fixed(300), 0, Fixed(0)); |
| } |
| |
| // On a one-subframe source discontinuity, long-running source pos is untouched, no rate-adjustment |
| TEST_F(MixStagePositionTest, SourceDiscontinuitySingleSubframe) { |
| auto non_clone = context().clock_factory()->CreateClientFixed( |
| clock::testing::CreateCustomClock({.synthetic_offset_from_mono = zx::duration(0)}) |
| .take_value()); |
| SetUpWithClock(std::move(non_clone)); |
| // MixStage should ignore the error and advance normally |
| ExpectPositionOffsetsAfterMix(0, Fixed::FromRaw(-1), 0, Fixed::FromRaw(-1)); |
| } |
| |
| // On source discontinuity within the recoverability threshold, the discontinuity should not be |
| // erased but rate-adjustment should occur. |
| TEST_F(MixStagePositionTest, SourceDiscontinuityWithinThreshold) { |
| auto non_clone = context().clock_factory()->CreateClientFixed( |
| clock::testing::CreateCustomClock({.synthetic_offset_from_mono = zx::duration(0)}) |
| .take_value()); |
| SetUpWithClock(std::move(non_clone)); |
| |
| // Artificially decrement long-running source position by less than 2ms but more than 1 subframe |
| // MixStage should accept the source error, rate-adjust, then advance |
| auto expect_long_running_dest_pos = info()->next_dest_frame + kDestFramesPerMix; |
| Fixed expect_long_running_source_pos = info()->next_source_frame + Fixed(kDestFramesPerMix); |
| info()->next_source_frame = info()->next_source_frame - Fixed::FromRaw(512); |
| |
| mix_stage_->ReadLock(rlctx, Fixed(kDestFramesPerMix), kDestFramesPerMix); |
| EXPECT_LT(info()->next_source_frame, expect_long_running_source_pos); |
| EXPECT_EQ(info()->next_dest_frame, expect_long_running_dest_pos); |
| } |
| |
| } // namespace media::audio |