blob: ccfd379fbdd7c89667e241dd44fa342421303f37 [file] [log] [blame]
// 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/audio_renderer.h"
#include <fuchsia/media/cpp/fidl.h>
#include <lib/fzl/vmar-manager.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/vmo.h>
#include <gtest/gtest.h>
#include "src/media/audio/audio_core/audio_admin.h"
#include "src/media/audio/audio_core/audio_device_manager.h"
#include "src/media/audio/audio_core/audio_driver.h"
#include "src/media/audio/audio_core/stream_volume_manager.h"
#include "src/media/audio/audio_core/testing/fake_audio_device.h"
#include "src/media/audio/audio_core/testing/threading_model_fixture.h"
#include "src/media/audio/audio_core/throttle_output.h"
#include "src/media/audio/lib/clock/testing/clock_test.h"
namespace media::audio {
namespace {
// Used when the ReadLockContext is unused by the test.
static media::audio::ReadableStream::ReadLockContext rlctx;
constexpr uint32_t kAudioRendererUnittestFrameRate = 48000;
constexpr size_t kAudioRendererUnittestVmoSize = 16ull * 1024;
class AudioRendererTest : public testing::ThreadingModelFixture {
public:
AudioRendererTest() {
FX_CHECK(vmo_mapper_.CreateAndMap(kAudioRendererUnittestVmoSize,
/*flags=*/0, nullptr, &vmo_) == ZX_OK);
}
protected:
void SetUp() override {
testing::ThreadingModelFixture::SetUp();
renderer_ = AudioRenderer::Create(fidl_renderer_.NewRequest(), &context());
EXPECT_NE(renderer_.get(), nullptr);
fidl_renderer_.set_error_handler(
[](auto status) { EXPECT_TRUE(status == ZX_OK) << "Renderer disconnected: " << status; });
fake_output_ = testing::FakeAudioOutput::Create(
context().process_config().device_config(), &threading_model(), &context().device_manager(),
&context().link_matrix(), context().clock_factory());
}
fuchsia::media::AudioStreamType PcmStreamType() {
return fuchsia::media::AudioStreamType{
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = 1,
.frames_per_second = kAudioRendererUnittestFrameRate,
};
}
// Creates a new payload buffer of |size| bytes and registers it with the renderer with |id|.
//
// A handle to the new VMO is returned.
zx::vmo AddPayloadBuffer(uint32_t id, size_t size, fuchsia::media::AudioRenderer* renderer) {
zx::vmo vmo;
EXPECT_EQ(ZX_OK, zx::vmo::create(size, 0, &vmo));
zx::vmo duplicate;
EXPECT_EQ(ZX_OK, vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate));
renderer->AddPayloadBuffer(id, std::move(duplicate));
RunLoopUntilIdle();
return vmo;
}
void TearDown() override {
// Dropping the channel queues up a reference to the Renderer through its error handler, which
// will not work since the rest of this class is destructed before the loop and its
// queued functions are. Here, we ensure the error handler runs before this class' destructors
// run.
{ auto r = std::move(fidl_renderer_); }
RunLoopUntilIdle();
// This ensures that the device is properly unwired from RouteGraph etc., before ~AudioDevice.
context().device_manager().RemoveDevice(fake_output_);
testing::ThreadingModelFixture::TearDown();
}
zx::clock GetReferenceClock() {
zx::clock fidl_clock;
fidl_renderer_->GetReferenceClock(
[&fidl_clock](zx::clock ref_clock) { fidl_clock = std::move(ref_clock); });
RunLoopUntilIdle();
return fidl_clock;
}
fuchsia::media::AudioRendererPtr fidl_renderer_;
std::shared_ptr<AudioRenderer> renderer_;
std::shared_ptr<testing::FakeAudioOutput> fake_output_;
fzl::VmoMapper vmo_mapper_;
zx::vmo vmo_;
fuchsia::media::AudioStreamType stream_type_ = {
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = 1,
.frames_per_second = kAudioRendererUnittestFrameRate,
};
};
constexpr zx::duration kMinLeadTime = zx::nsec(123456789);
constexpr int64_t kInvalidLeadTimeNs = -1;
// Validate that MinLeadTime is provided to AudioRenderer clients accurately
TEST_F(AudioRendererTest, MinLeadTimePadding) {
// We must set our output's delay, before linking it, before calling SetPcmStreamType().
fake_output_->SetPresentationDelay(kMinLeadTime);
// Our RouteGraph links one FakeAudioOutput to the Renderer-under-test. Thus we can set
// our output's PresentationDelay, fully expecting this value to be reflected as-is to
// renderer+clients.
context().route_graph().AddRenderer(std::move(renderer_));
context().route_graph().AddDeviceToRoutes(fake_output_.get());
// SetPcmStreamType triggers the routing preparation completion, which connects output(s) to
// renderer. Renderers react to new outputs in `OnLinkAdded` by recalculating minimum lead time.
fidl_renderer_->SetPcmStreamType(PcmStreamType());
RunLoopUntilIdle();
auto lead_time_ns = kInvalidLeadTimeNs;
fidl_renderer_->GetMinLeadTime(
[&lead_time_ns](int64_t received_lead_time_ns) { lead_time_ns = received_lead_time_ns; });
RunLoopUntilIdle();
ASSERT_NE(lead_time_ns, kInvalidLeadTimeNs) << "No response received for GetMinLeadTime";
EXPECT_EQ(lead_time_ns, kMinLeadTime.to_nsecs()) << "Incorrect GetMinLeadTime received";
}
TEST_F(AudioRendererTest, AllocatePacketQueueForLinks) {
context().route_graph().AddRenderer(std::move(renderer_));
context().route_graph().AddDeviceToRoutes(fake_output_.get());
const size_t kFrames = 16;
fidl_renderer_->SetPcmStreamType(PcmStreamType());
AddPayloadBuffer(0, zx_system_get_page_size(), fidl_renderer_.get());
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kFrames * sizeof(float);
fidl_renderer_->SendPacketNoReply(std::move(packet));
RunLoopUntilIdle();
std::vector<LinkMatrix::LinkHandle> links;
context().link_matrix().SourceLinks(*fake_output_, &links);
ASSERT_EQ(1u, links.size());
for (auto& link : links) {
auto stream = link.stream;
ASSERT_TRUE(stream);
{ // Expect a buffer.
auto buffer = stream->ReadLock(rlctx, Fixed(0), kFrames);
ASSERT_TRUE(buffer);
EXPECT_NE(nullptr, buffer->payload());
}
{ // No more buffers.
auto buffer = stream->ReadLock(rlctx, Fixed(kFrames), 10);
ASSERT_FALSE(buffer);
}
}
}
TEST_F(AudioRendererTest, SendPacket_NO_TIMESTAMP) {
context().route_graph().AddRenderer(std::move(renderer_));
context().route_graph().AddDeviceToRoutes(fake_output_.get());
fidl_renderer_->SetPcmStreamType(PcmStreamType());
AddPayloadBuffer(0, zx_system_get_page_size(), fidl_renderer_.get());
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = 128;
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
RunLoopUntilIdle();
std::vector<LinkMatrix::LinkHandle> links;
context().link_matrix().SourceLinks(*fake_output_, &links);
ASSERT_EQ(1u, links.size());
auto stream = links[0].stream;
ASSERT_TRUE(stream);
// Expect 3 buffers. Since these have NO_TIMESTAMP and also no discontinutity flag, they should
// be continuous starting at pts 0.
constexpr int64_t kPacketSizeFrames = 32;
int64_t expected_packet_pts = 0;
for (uint32_t i = 0; i < 3; ++i) {
auto buffer = stream->ReadLock(rlctx, Fixed(expected_packet_pts), kPacketSizeFrames);
ASSERT_TRUE(buffer);
EXPECT_EQ(buffer->start().Floor(), expected_packet_pts);
EXPECT_EQ(buffer->length(), kPacketSizeFrames);
EXPECT_NE(nullptr, buffer->payload());
expected_packet_pts = buffer->end().Floor();
}
// Send another set of packets after lead time + padding to ensure these packets cannot be played
// continuously with the last set of packets. Now we use FLAG_DISCONTINUITY which means they
// will not be continuous with the previous packets.
context().clock_factory()->AdvanceMonoTimeBy(stream->GetPresentationDelay() + zx::msec(30));
packet.flags |= fuchsia::media::STREAM_PACKET_FLAG_DISCONTINUITY;
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
fidl_renderer_->SendPacketNoReply(fidl::Clone(packet));
RunLoopUntilIdle();
{
// Read enough frames to include all three packets in the same buffer.
// The buffer should appear PresentationDelay+30ms after expected_packet_pts.
auto delay_ms = stream->GetPresentationDelay().to_msecs() + 30 + 1 /* round up */;
auto total_packets = 3 * kPacketSizeFrames + delay_ms * kAudioRendererUnittestFrameRate / 1000;
auto buffer = stream->ReadLock(rlctx, Fixed(expected_packet_pts), total_packets);
ASSERT_TRUE(buffer);
// GT here as we are not continuous with the previous packet.
EXPECT_GT(buffer->start().Floor(), expected_packet_pts);
EXPECT_EQ(buffer->length(), kPacketSizeFrames);
EXPECT_NE(nullptr, buffer->payload());
expected_packet_pts = buffer->end().Floor();
}
for (uint32_t i = 0; i < 2; ++i) {
auto buffer = stream->ReadLock(rlctx, Fixed(expected_packet_pts), kPacketSizeFrames);
ASSERT_TRUE(buffer);
EXPECT_EQ(buffer->start().Floor(), expected_packet_pts);
EXPECT_EQ(buffer->length(), kPacketSizeFrames);
EXPECT_NE(nullptr, buffer->payload());
expected_packet_pts = buffer->end().Floor();
}
}
// The renderer should be routed once the format is set.
TEST_F(AudioRendererTest, RegistersWithRouteGraphIfHasUsageStreamTypeAndBuffers) {
EXPECT_EQ(context().link_matrix().DestLinkCount(*renderer_), 0u);
zx::vmo duplicate;
ASSERT_EQ(
vmo_.duplicate(ZX_RIGHT_TRANSFER | ZX_RIGHT_WRITE | ZX_RIGHT_READ | ZX_RIGHT_MAP, &duplicate),
ZX_OK);
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
auto* renderer_raw = renderer_.get();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT);
RunLoopUntilIdle();
EXPECT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 0u);
fidl_renderer_->SetPcmStreamType(stream_type_);
RunLoopUntilIdle();
EXPECT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 1u);
fidl_renderer_->AddPayloadBuffer(0, std::move(duplicate));
RunLoopUntilIdle();
EXPECT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 1u);
}
// AudioRenderer should survive, if it calls Play while already playing.
TEST_F(AudioRendererTest, DoublePlay) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[](int64_t ref_time, int64_t media_time) {
EXPECT_NE(ref_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_NE(media_time, fuchsia::media::NO_TIMESTAMP);
});
RunLoopFor(zx::msec(20)); // wait for any Play-related pended actions to try to complete
fidl_renderer_->Play(fuchsia::media::NO_TIMESTAMP, 0, [](int64_t ref_time, int64_t media_time) {
EXPECT_NE(ref_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_NE(media_time, fuchsia::media::NO_TIMESTAMP);
});
RunLoopFor(zx::msec(20)); // wait for any Play-related pended actions to try to complete
EXPECT_TRUE(fidl_renderer_.is_bound());
}
// AudioRenderer should survive, if it calls Pause for a second time before calling Play.
// Timestamps returned from this second Pause should be the same as those from the first.
TEST_F(AudioRendererTest, DoublePause) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
RunLoopUntilIdle();
int64_t received_ref_time = fuchsia::media::NO_TIMESTAMP;
int64_t received_media_time = fuchsia::media::NO_TIMESTAMP;
fidl_renderer_->Pause(
[&received_ref_time, &received_media_time](int64_t ref_time, int64_t media_time) {
EXPECT_NE(ref_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_NE(media_time, fuchsia::media::NO_TIMESTAMP);
received_ref_time = ref_time;
received_media_time = media_time;
FX_LOGS(INFO) << "Received ref_time " << ref_time << ", media_time " << media_time;
});
RunLoopFor(zx::msec(20)); // wait for any Pause-related pended actions to try to complete
fidl_renderer_->Pause(
[&received_ref_time, &received_media_time](int64_t ref_time, int64_t media_time) {
EXPECT_NE(ref_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_NE(media_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_EQ(received_ref_time, ref_time);
EXPECT_EQ(received_media_time, media_time);
});
RunLoopFor(zx::msec(20)); // wait for any Pause-related pended actions to try to complete
EXPECT_TRUE(fidl_renderer_.is_bound());
}
// AudioRenderer should survive if calling Pause before ever calling Play.
// We return timestamps that try to indicate that no previous timeline transform was established.
TEST_F(AudioRendererTest, PauseBeforePlay) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->Pause([](int64_t ref_time, int64_t media_time) {
EXPECT_EQ(ref_time, fuchsia::media::NO_TIMESTAMP);
EXPECT_EQ(media_time, fuchsia::media::NO_TIMESTAMP);
});
RunLoopFor(zx::msec(20)); // wait for any Pause-related pended actions to try to complete
EXPECT_TRUE(fidl_renderer_.is_bound());
}
TEST_F(AudioRendererTest, ReportsPlayAndPauseToPolicy) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
bool received_play_callback = false;
fidl_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[&received_play_callback](int64_t ref_time, int64_t media_time) {
received_play_callback = true;
});
auto run_loop_count = 0;
while (!received_play_callback && run_loop_count < 100) {
RunLoopFor(zx::msec(5));
++run_loop_count;
}
EXPECT_TRUE(context().audio_admin().IsActive(RenderUsage::SYSTEM_AGENT));
bool received_pause_callback = false;
fidl_renderer_->Pause([&received_pause_callback](int64_t ref_time, int64_t media_time) {
received_pause_callback = true;
});
run_loop_count = 0;
while (!received_pause_callback && run_loop_count < 100) {
RunLoopFor(zx::msec(5));
++run_loop_count;
}
EXPECT_FALSE(context().audio_admin().IsActive(RenderUsage::SYSTEM_AGENT));
}
// AudioCore should survive, if a renderer is unbound between a Play call and its callback.
TEST_F(AudioRendererTest, RemoveRendererDuringPlay) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[](int64_t ref_time, int64_t media_time) {
FX_LOGS(INFO)
<< "Play callback: ref " << ref_time << ", media " << media_time;
});
// Simulate closing the client binding. This will shutdown the renderer.
fidl_renderer_.Unbind();
RunLoopFor(zx::msec(20)); // wait for any Play-related pended actions to try to complete
}
// AudioCore should survive, if a renderer is unbound immediately after PlayNoReply, as AudioCore
// may kick off deferred actions that need to be safely retired.
TEST_F(AudioRendererTest, RemoveRendererDuringPlayNoReply) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
// Simulate closing the client binding. This will shutdown the renderer.
fidl_renderer_.Unbind();
RunLoopFor(zx::msec(20)); // wait for any Play-related pended actions to try to complete
}
// AudioCore should survive, if a renderer is unbound between a Pause call and its callback.
TEST_F(AudioRendererTest, RemoveRendererDuringPause) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::COMMUNICATION);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
fidl_renderer_->Pause([](int64_t ref_time, int64_t media_time) {
FX_LOGS(INFO) << "Pause callback: ref " << ref_time << ", media " << media_time;
});
// Simulate closing the client binding. This will shutdown the renderer.
fidl_renderer_.Unbind();
RunLoopFor(zx::msec(20)); // wait for any Pause-related pended actions to try to complete
}
// AudioCore should survive, if a renderer is unbound immediately after PauseNoReply, as AudioCore
// may kick off deferred actions that need to be safely retired.
TEST_F(AudioRendererTest, RemoveRendererDuringPauseNoReply) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
fidl_renderer_->PauseNoReply();
// Simulate closing the client binding. This will shutdown the renderer.
fidl_renderer_.Unbind();
RunLoopFor(zx::msec(20)); // wait for any Pause-related pended actions to try to complete
}
TEST_F(AudioRendererTest, RemoveRendererWhileBufferLocked) {
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
context().route_graph().AddRenderer(std::move(renderer_));
fidl_renderer_->SetUsage(fuchsia::media::AudioRenderUsage::SYSTEM_AGENT);
fidl_renderer_->SetPcmStreamType(stream_type_);
fidl_renderer_->AddPayloadBuffer(0, std::move(vmo_));
fidl_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
// Enqueue a packet
fuchsia::media::StreamPacket packet;
packet.pts = fuchsia::media::NO_TIMESTAMP;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = 128;
fidl_renderer_->SendPacketNoReply(std::move(packet));
RunLoopUntilIdle();
// This will be the packet queue created when the link between the renderer and output was formed.
auto packet_queue = fake_output_->stream();
ASSERT_TRUE(packet_queue);
// Acquire a buffer.
auto buf = packet_queue->ReadLock(rlctx, Fixed(0), 32);
ASSERT_TRUE(buf);
EXPECT_EQ(buf->start().Floor(), 0);
EXPECT_EQ(buf->length(), 32);
// Simulate closing the client binding. This will shutdown the renderer.
fidl_renderer_.Unbind();
RunLoopUntilIdle();
// Now release the buffer.
buf = std::nullopt;
RunLoopUntilIdle();
}
TEST_F(AudioRendererTest, ReferenceClockIsAdvancing) {
auto fidl_clock = GetReferenceClock();
clock::testing::VerifyAdvances(fidl_clock);
clock::testing::VerifyAdvances(renderer_->reference_clock(), context().clock_factory());
}
TEST_F(AudioRendererTest, GetReferenceClockReturnsReadOnlyClock) {
auto fidl_clock = GetReferenceClock();
clock::testing::VerifyCannotBeRateAdjusted(fidl_clock);
}
TEST_F(AudioRendererTest, DefaultClockIsInitiallyMonotonic) {
auto fidl_clock = GetReferenceClock();
clock::testing::VerifyIsSystemMonotonic(fidl_clock);
clock::testing::VerifyIsSystemMonotonic(renderer_->reference_clock());
}
TEST_F(AudioRendererTest, DefaultClockIsFlexible) {
clock::testing::VerifyCanBeRateAdjusted(renderer_->reference_clock());
EXPECT_TRUE(renderer_->reference_clock().is_adjustable());
EXPECT_TRUE(renderer_->reference_clock().is_client_clock());
}
// The renderer clock is valid, before and after devices are routed.
TEST_F(AudioRendererTest, ReferenceClockIsCorrectAfterDeviceChange) {
auto* renderer_raw = renderer_.get();
context().route_graph().AddRenderer(std::move(renderer_));
RunLoopUntilIdle();
auto fidl_clock = GetReferenceClock();
fidl_renderer_->SetPcmStreamType(stream_type_);
RunLoopUntilIdle();
ASSERT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 1u);
context().route_graph().AddDeviceToRoutes(fake_output_.get());
RunLoopUntilIdle();
ASSERT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 1u);
clock::testing::VerifyAdvances(fidl_clock);
clock::testing::VerifyIsSystemMonotonic(fidl_clock);
clock::testing::VerifyCannotBeRateAdjusted(fidl_clock);
// Remove the device and validate that the reference clock received from the renderer remains
// valid, even after the device is removed (and thus renderer is rerouted to a ThrottleOutput)
context().route_graph().RemoveDeviceFromRoutes(fake_output_.get());
// device_manager will call RemoveDevice again during TearDown, which is benign
RunLoopUntilIdle();
ASSERT_EQ(context().link_matrix().DestLinkCount(*renderer_raw), 1u);
clock::testing::VerifyAdvances(fidl_clock);
clock::testing::VerifyIsSystemMonotonic(fidl_clock);
clock::testing::VerifyCannotBeRateAdjusted(fidl_clock);
}
} // namespace
} // namespace media::audio