| // 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/audio_core/tap_stage.h" |
| |
| #include <gmock/gmock.h> |
| |
| #include "src/media/audio/audio_core/audio_clock.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/packet_factory.h" |
| #include "src/media/audio/audio_core/testing/threading_model_fixture.h" |
| #include "src/media/audio/lib/clock/clone_mono.h" |
| |
| using testing::Each; |
| using testing::FloatEq; |
| |
| namespace media::audio { |
| namespace { |
| |
| constexpr uint32_t kChannels = 2; |
| const Format kDefaultFormat = |
| Format::Create(fuchsia::media::AudioStreamType{ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = kChannels, |
| .frames_per_second = 48000, |
| }) |
| .take_value(); |
| |
| constexpr uint32_t kRingBufferFrameCount = 1024; |
| constexpr uint32_t kPacketFrames = 480; |
| constexpr zx::duration kPacketDuration = zx::msec(10); |
| |
| class TapStageTest : public testing::ThreadingModelFixture { |
| protected: |
| TapStageTest() : TapStageTest(0) {} |
| |
| // tap_frame = source_frame + tap_frame_offset. |
| // |
| // This is used to test that TapStage can correctly convert between arbitrary timelines. |
| TapStageTest(uint32_t tap_frame_offset) : tap_frame_offset_(tap_frame_offset) {} |
| |
| void SetUp() { |
| testing::ThreadingModelFixture::SetUp(); |
| TimelineRate rate(Fixed(kDefaultFormat.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()); |
| auto source_timeline_function = |
| fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(rate)); |
| |
| packet_queue_ = |
| std::make_shared<PacketQueue>(kDefaultFormat, source_timeline_function, |
| AudioClock::ClientFixed(clock::AdjustableCloneOfMonotonic())); |
| ASSERT_TRUE(packet_queue_); |
| |
| auto tap_timeline_function = |
| fbl::MakeRefCounted<VersionedTimelineFunction>(TimelineFunction(0, 0, rate)); |
| |
| auto endpoints = BaseRingBuffer::AllocateSoftwareBuffer( |
| kDefaultFormat, tap_timeline_function, packet_queue_->reference_clock(), |
| kRingBufferFrameCount, [this] { return safe_write_frame_; }); |
| ring_buffer_ = std::move(endpoints.reader); |
| |
| ASSERT_TRUE(ring_buffer_); |
| |
| tap_ = std::make_shared<TapStage>(packet_queue_, std::move(endpoints.writer)); |
| ClearRingBuffer(); |
| } |
| |
| 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]); |
| } |
| |
| void ClearRingBuffer(float value = 0.0) { |
| for (size_t i = 0; i < ring_buffer().size() / sizeof(float); ++i) { |
| reinterpret_cast<float*>(ring_buffer().virt())[i] = value; |
| } |
| } |
| |
| void AdvanceTo(zx::duration d) { |
| auto ref_time = zx::time(0) + d; |
| auto pts_to_frac_frame = ring_buffer_->ref_time_to_frac_presentation_frame().timeline_function; |
| safe_write_frame_ = Fixed::FromRaw(pts_to_frac_frame.Apply(ref_time.get())).Floor(); |
| } |
| |
| TapStage& tap() { return *tap_; } |
| PacketQueue& packet_queue() { return *packet_queue_; } |
| testing::PacketFactory& packet_factory() { return packet_factory_; } |
| ReadableRingBuffer& ring_buffer() { return *ring_buffer_; } |
| |
| template <size_t frame_count> |
| void CheckBuffer(const ReadableStream::Buffer& buffer, int64_t frame, float expected_sample) { |
| EXPECT_EQ(buffer.start(), Fixed(frame)); |
| EXPECT_EQ(buffer.length(), Fixed(frame_count)); |
| auto& arr = as_array<float, frame_count * kChannels>(buffer.payload()); |
| EXPECT_THAT(arr, Each(FloatEq(expected_sample))); |
| } |
| |
| // Assert that |stream| contains a buffer that is exactly |frame_count| frames starting at |frame| |
| // with data that matches only |expected_sample| (that is all the samples in the buffer match |
| // |expected_sample|). |
| template <size_t frame_count> |
| void CheckStream(ReadableStream* stream, int64_t frame, float expected_sample, |
| bool release = true) { |
| auto buffer = stream->ReadLock(Fixed(frame), frame_count); |
| ASSERT_TRUE(buffer); |
| CheckBuffer<frame_count>(*buffer, frame, expected_sample); |
| buffer->set_is_fully_consumed(release); |
| } |
| |
| uint32_t tap_frame_offset_; |
| testing::PacketFactory packet_factory_{dispatcher(), kDefaultFormat, 4 * PAGE_SIZE}; |
| std::shared_ptr<PacketQueue> packet_queue_; |
| std::shared_ptr<ReadableRingBuffer> ring_buffer_; |
| std::shared_ptr<TapStage> tap_; |
| int64_t safe_write_frame_ = 0; |
| }; |
| |
| TEST_F(TapStageTest, CopySinglePacket) { |
| packet_queue().PushPacket(packet_factory().CreatePacket(1.0, kPacketDuration)); |
| |
| // We expect the tap and ring buffer to both be in sync for the first 480. |
| constexpr size_t frame_count = kPacketFrames; |
| CheckStream<frame_count>(&tap(), 0, 1.0, true); |
| AdvanceTo(kPacketDuration); |
| CheckStream<frame_count>(&ring_buffer(), 0, 1.0, true); |
| } |
| |
| // Test that ReadLock returns a buffer correctly sized for whatever buffer was returned by |
| // the source streams |ReadLock|. |
| TEST_F(TapStageTest, TruncateToInputBuffer) { |
| packet_queue().PushPacket(packet_factory().CreatePacket(1.0, kPacketDuration)); |
| |
| constexpr uint32_t frame_count = kPacketFrames; |
| { // Read from the tap, expect to get the same bytes from the packet. |
| auto buffer = tap().ReadLock(Fixed(0), frame_count * 2); |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(buffer->start(), Fixed(0)); |
| EXPECT_EQ(buffer->length(), Fixed(frame_count)); |
| auto& arr = as_array<float, frame_count>(buffer->payload()); |
| EXPECT_THAT(arr, Each(FloatEq(1.0f))); |
| } |
| } |
| |
| // Test the case where a single input buffer will require 2 writes to the ring buffer as the buffer |
| // will cross the end of the ring. |
| TEST_F(TapStageTest, WrapAroundRingBuffer) { |
| // The ring is 1024 frames. So we write: |
| // 0 - 479 = 1.0 samples |
| // 480 - 959 = 2.0 samples |
| // 960 - 1023 = 3.0 samples |
| // 0 - 415 = 3.0 samples (3rd packet wrapped around. |
| packet_queue().PushPacket(packet_factory().CreatePacket(1.0, kPacketDuration)); |
| packet_queue().PushPacket(packet_factory().CreatePacket(2.0, kPacketDuration)); |
| packet_queue().PushPacket(packet_factory().CreatePacket(3.0, kPacketDuration)); |
| |
| { // With the first packet, we'll be fully in sync between the tap and the ring buffer. |
| constexpr size_t frame_count = kPacketFrames; |
| SCOPED_TRACE("first packet in tap"); |
| CheckStream<frame_count>(&tap(), 0, 1.0, true); |
| SCOPED_TRACE("first packet in ring buffer"); |
| AdvanceTo(kPacketDuration); |
| CheckStream<frame_count>(&ring_buffer(), 0, 1.0, true); |
| } |
| |
| { // The second packet is still fully in sync between the tap and the ring buffer. |
| constexpr size_t frame_count = kPacketFrames; |
| SCOPED_TRACE("second packet in tap"); |
| CheckStream<frame_count>(&tap(), frame_count, 2.0, true); |
| SCOPED_TRACE("second packet in ring buffer"); |
| AdvanceTo(zx::msec(20)); |
| CheckStream<frame_count>(&ring_buffer(), frame_count, 2.0, true); |
| } |
| |
| { // For the final packet, we expect the Tap to return one buffer with the entire contents (this |
| // is the packet buffer. |
| constexpr size_t frame_count = kPacketFrames; |
| SCOPED_TRACE("final packet in tap"); |
| CheckStream<frame_count>(&tap(), 2 * frame_count, 3.0, true); |
| } |
| |
| AdvanceTo(zx::msec(30)); |
| |
| // The ring buffer needs to be read in 2 portions, since this packet will wrap around the end of |
| // the ring. |
| // |
| // The ring buffer should return the first 64 frames for the first ReadLock (the only |
| // remaining space before the buffer wraps around). A subsequent ReadLock should return the |
| // remaining frames. |
| constexpr uint32_t expected_frames_region_1 = kRingBufferFrameCount - (2 * kPacketFrames); |
| constexpr uint32_t expected_frames_region_2 = kPacketFrames - expected_frames_region_1; |
| { |
| uint32_t requested_frames = kPacketFrames; |
| constexpr uint32_t expected_frames = expected_frames_region_1; |
| int64_t frame = 2 * kPacketFrames; |
| auto buffer = ring_buffer().ReadLock(Fixed(frame), requested_frames); |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(buffer->start(), Fixed(frame)); |
| EXPECT_EQ(buffer->length(), Fixed(expected_frames)); |
| auto& arr = as_array<float, expected_frames>(buffer->payload()); |
| EXPECT_THAT(arr, Each(FloatEq(3.0f))); |
| } |
| |
| { |
| constexpr uint32_t requested_frames = kPacketFrames; |
| constexpr uint32_t expected_frames = expected_frames_region_2; |
| int64_t frame = kRingBufferFrameCount; |
| auto buffer = ring_buffer().ReadLock(Fixed(frame), requested_frames); |
| ASSERT_TRUE(buffer); |
| EXPECT_EQ(buffer->start(), Fixed(frame)); |
| EXPECT_EQ(buffer->length(), Fixed(expected_frames)); |
| { |
| auto& arr = as_array<float, expected_frames>(buffer->payload()); |
| EXPECT_THAT(arr, Each(FloatEq(3.0f))); |
| } |
| { |
| constexpr uint32_t len = requested_frames - expected_frames; |
| auto& arr = as_array<float, len>(buffer->payload(), |
| expected_frames * ring_buffer().format().channels()); |
| EXPECT_THAT(arr, Each(FloatEq(1.0f))); |
| } |
| } |
| } |
| |
| TEST_F(TapStageTest, PartialTapBuffer) { |
| constexpr float kInitialRingBufferColor = 5.0; |
| ClearRingBuffer(kInitialRingBufferColor); |
| |
| // Test the case where part of a tap buffer is unavailable because of the safe write pointer has |
| // moved beyond that frame. |
| // |
| // Seek the tap buffer so that the first half packet is not available in the ring buffer. We |
| // expect these frames to be untouched in the underlying buffer. |
| AdvanceTo(kPacketDuration / 2); |
| |
| // Pull a full packet through the the tap stage. |
| packet_queue().PushPacket(packet_factory().CreatePacket(1.0, kPacketDuration)); |
| |
| // Expect the full packet to be read out of the tap stage. |
| SCOPED_TRACE("first packet in tap"); |
| CheckStream<kPacketFrames>(&tap(), 0, 1.0, true); |
| |
| // Expect the first half of the packet to be untouched in the ring buffer. |
| SCOPED_TRACE("unmodified ring buffer region"); |
| CheckStream<kPacketFrames / 2>(&ring_buffer(), 0, kInitialRingBufferColor, true); |
| |
| // But the second half should match the input. |
| SCOPED_TRACE("modified ring buffer region"); |
| AdvanceTo(kPacketDuration); |
| CheckStream<kPacketFrames / 2>(&ring_buffer(), kPacketFrames / 2, 1.0, true); |
| |
| // And anything after that should also be unmodified. |
| SCOPED_TRACE("silence after tap"); |
| AdvanceTo(kPacketDuration * 2); |
| CheckStream<kPacketFrames>(&ring_buffer(), kPacketFrames, kInitialRingBufferColor, true); |
| } |
| |
| TEST_F(TapStageTest, ShortSourceBuffer) { |
| constexpr float kSourceBufferColor = 3.0; |
| constexpr float kInitialRingBufferColor = 5.0; |
| |
| // Clear the buffer with a defined bit-pattern. This is so that we may detect if any frames have |
| // or have not been modified by the TapStage. |
| ClearRingBuffer(kInitialRingBufferColor); |
| |
| // Enqueue a single buffer that is half as long as the packet we will read out of the TapStage, |
| // and also offset by a quarter packet of implicit silence: |
| // |
| // --------------------------- |
| // | 1 | |
| // |---------------------------| |
| // | 2 |~~~~~~~~~~~~~~~~~~~~| |
| // |---------------------------| |
| // |~~~~~~| 3 |~~~~~~| |
| // |---------------------------| |
| // |~~~~~~~~~~~~~~~~~~~~| 4 | |
| // --------------------------- |
| // ^ ^ |
| // 0 kPacketFrames |
| // |
| // Where |
| // Region 1 -- The requested frames from the TapStage, which spans frames 0 to kPacketFrames. |
| // Region 2 -- The region before the first frame in source. This should be replicated into the |
| // tap stage as silence. |
| // Region 3 -- Frames available in source that need to be copied into the tap stream. |
| // Region 4 -- Frames that are beyond the end of the source stream packet. We expect the buffer |
| // returned from TapStage to end before this region, and we expect the corresponding |
| // frames in the ring buffer to be unmodified. |
| |
| // Create region '2' in the source stream by seeking past these frames. The next 'CreatePacket' |
| // will occur after this. |
| packet_factory().SeekToFrame(kPacketFrames / 4); |
| // Create region '3' in the source stream. |
| packet_queue().PushPacket(packet_factory().CreatePacket(kSourceBufferColor, kPacketDuration / 2)); |
| |
| // Request 'region 1' |
| auto buffer = tap().ReadLock(Fixed(0), kPacketFrames); |
| // But expect 'region 3', which corresponds to what is available in source. |
| ASSERT_TRUE(buffer); |
| CheckBuffer<kPacketFrames / 2>(*buffer, kPacketFrames / 4, 3.0); |
| |
| AdvanceTo(kPacketDuration); |
| |
| // Verify the silence from 'region 2' is available in the tap stream. |
| SCOPED_TRACE("silence in ring buffer"); |
| CheckStream<kPacketFrames / 4>(&ring_buffer(), 0, 0.0, true); |
| |
| // Followed by 'region 3' |
| SCOPED_TRACE("frames from source in ring buffer"); |
| CheckStream<kPacketFrames / 2>(&ring_buffer(), kPacketFrames / 4, kSourceBufferColor, true); |
| |
| // Ensure that frames in 'region 4' were not modified in the ring buffer. |
| SCOPED_TRACE("unmodified frames"); |
| CheckStream<kPacketFrames / 4>(&ring_buffer(), 3 * kPacketFrames / 4, kInitialRingBufferColor, |
| true); |
| } |
| |
| TEST_F(TapStageTest, SilentSourceStream) { |
| // Initialize the buffer to something that is not silence. |
| ClearRingBuffer(5.0); |
| |
| // Read from the ring buffer. |
| auto buffer = tap().ReadLock(Fixed(0), kPacketFrames); |
| |
| // Our packet queue source is empty so we expect no data here. |
| EXPECT_FALSE(buffer); |
| |
| // But we do expect the frames in our tap to have been written silence. |
| AdvanceTo(kPacketDuration); |
| CheckStream<kPacketFrames>(&ring_buffer(), 0, 0.0, true); |
| } |
| |
| TEST_F(TapStageTest, ShortTapBuffer) { |
| constexpr float kSourceBufferColor = 3.0; |
| constexpr float kInitialRingBufferColor = 5.0; |
| |
| // Clear the buffer with a defined bit-pattern. This is so that we may detect if any frames have |
| // or have not been modified by the TapStage. |
| ClearRingBuffer(kInitialRingBufferColor); |
| |
| // Create region '2' in the source stream by seeking past these frames. The next 'CreatePacket' |
| // will occur after this. |
| packet_factory().SeekToFrame(kPacketFrames / 4); |
| // Create region '3' in the source stream. |
| packet_queue().PushPacket(packet_factory().CreatePacket(kSourceBufferColor, kPacketDuration / 2)); |
| |
| // Request 'region 1' |
| auto buffer = tap().ReadLock(Fixed(0), kPacketFrames); |
| // But expect 'region 3', which corresponds to what is available in source. |
| ASSERT_TRUE(buffer); |
| CheckBuffer<kPacketFrames / 2>(*buffer, kPacketFrames / 4, 3.0); |
| |
| AdvanceTo(kPacketDuration); |
| |
| // Verify the silence from 'region 2' is available in the tap stream. |
| SCOPED_TRACE("silence in ring buffer"); |
| CheckStream<kPacketFrames / 4>(&ring_buffer(), 0, 0.0, true); |
| |
| // Followed by 'region 3' |
| SCOPED_TRACE("frames from source in ring buffer"); |
| CheckStream<kPacketFrames / 2>(&ring_buffer(), kPacketFrames / 4, kSourceBufferColor, true); |
| |
| // Ensure that frames in 'region 4' were not modified in the ring buffer. |
| SCOPED_TRACE("unmodified frames"); |
| CheckStream<kPacketFrames / 4>(&ring_buffer(), 3 * kPacketFrames / 4, kInitialRingBufferColor, |
| true); |
| } |
| |
| class TapStageFrameConversionTest : public TapStageTest { |
| protected: |
| TapStageFrameConversionTest() : TapStageTest(12345) {} |
| }; |
| |
| // Test that we can properly copy a packet when the source and tap streams are using different |
| // TimelineFunctions. |
| TEST_F(TapStageFrameConversionTest, CopySinglePacket) { |
| packet_queue().PushPacket(packet_factory().CreatePacket(1.0, kPacketDuration)); |
| |
| // We expect the tap and ring buffer to both be in sync for the first 480. |
| constexpr size_t frame_count = kPacketFrames; |
| CheckStream<frame_count>(&tap(), 0, 1.0, true); |
| AdvanceTo(kPacketDuration); |
| CheckStream<frame_count>(&ring_buffer(), 0, 1.0, true); |
| } |
| |
| } // namespace |
| } // namespace media::audio |