blob: ddd3e41984f286143ca61cb9489218945c44e677 [file] [log] [blame]
// Copyright 2016 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/base_renderer.h"
#include <lib/fit/defer.h>
#include "src/media/audio/audio_core/audio_core_impl.h"
#include "src/media/audio/audio_core/audio_output.h"
#include "src/media/audio/lib/clock/clone_mono.h"
#include "src/media/audio/lib/clock/utils.h"
#include "src/media/audio/lib/logging/logging.h"
namespace media::audio {
namespace {
// If client does not specify a ref_time for Play, pad it by this amount
constexpr zx::duration kPaddingForUnspecifiedRefTime = zx::msec(20);
// 4 slabs will allow each renderer to create >500 packets. Any client creating any more packets
// than this that are outstanding at the same time will be disconnected.
constexpr size_t kMaxPacketAllocatorSlabs = 4;
} // namespace
BaseRenderer::BaseRenderer(
fidl::InterfaceRequest<fuchsia::media::AudioRenderer> audio_renderer_request, Context* context)
: AudioObject(Type::AudioRenderer),
context_(*context),
audio_renderer_binding_(this, std::move(audio_renderer_request)),
pts_ticks_per_second_(1'000'000'000, 1),
reference_clock_to_fractional_frames_(fbl::MakeRefCounted<VersionedTimelineFunction>()),
packet_allocator_(kMaxPacketAllocatorSlabs, true),
reporter_(Reporter::Singleton().CreateRenderer()) {
TRACE_DURATION("audio", "BaseRenderer::BaseRenderer");
FX_DCHECK(context);
AUDIO_LOG_OBJ(DEBUG, this);
// Our default clock starts as an adjustable clone of MONOTONIC, but ultimately it will track the
// clock of the device where the renderer is routed.
SetAdjustableReferenceClock();
audio_renderer_binding_.set_error_handler([this](zx_status_t status) {
TRACE_DURATION("audio", "BaseRenderer::audio_renderer_binding_.error_handler", "zx_status",
status);
AUDIO_LOG(DEBUG) << "Client disconnected";
context_.route_graph().RemoveRenderer(*this);
});
}
BaseRenderer::~BaseRenderer() {
AUDIO_LOG_OBJ(DEBUG, this);
wav_writer_.Close();
payload_buffers_.clear();
}
void BaseRenderer::Shutdown() {
TRACE_DURATION("audio", "BaseRenderer::Shutdown");
AUDIO_LOG_OBJ(DEBUG, this);
ReportStop();
wav_writer_.Close();
payload_buffers_.clear();
}
// Because a PacketQueue might need to outlive its Renderer, and because (in the future) there could
// be multiple destinations for a single renderer, we duplicate the raw clock here and send a new
// AudioClock object to each PacketQueue. If the client uses our clock (which is adjustable), then
// one PacketQueue will receive an AudioClock marked adjustable. All other PacketQueues receive
// AudioClocks that are non-adjustable.
fit::result<std::shared_ptr<ReadableStream>, zx_status_t> BaseRenderer::InitializeDestLink(
const AudioObject& dest) {
TRACE_DURATION("audio", "BaseRenderer::InitializeDestLink");
std::optional<AudioClock> clock_for_packet_queue;
if (client_allows_clock_adjustment_ && !adjustable_clock_is_allocated_) {
// Retain WRITE, mark AudioClock adjustable, and note that an adjustable clock has been
// provided.
zx::clock adjustable_duplicate;
auto status = raw_clock().duplicate(ZX_RIGHT_SAME_RIGHTS, &adjustable_duplicate);
if (status != ZX_OK) {
return fit::error(status);
}
FX_DCHECK(adjustable_duplicate.is_valid());
clock_for_packet_queue = AudioClock::ClientAdjustable(std::move(adjustable_duplicate));
adjustable_clock_is_allocated_ = true;
} else {
// This strips off WRITE rights, which is appropriate for a non-adjustable clock.
auto readable_clock = audio::clock::DuplicateClock(raw_clock()).take_value();
clock_for_packet_queue = AudioClock::ClientFixed(std::move(readable_clock));
}
auto queue = std::make_shared<PacketQueue>(*format(), reference_clock_to_fractional_frames_,
std::move(clock_for_packet_queue.value()));
queue->SetUnderflowReporter([this](zx::time start_time, zx::time stop_time) {
reporter_->Underflow(start_time, stop_time);
});
auto stream_usage = usage();
FX_DCHECK(stream_usage) << "A renderer cannot be linked without a usage";
queue->set_usage(*stream_usage);
packet_queues_.insert({&dest, queue});
return fit::ok(std::move(queue));
}
void BaseRenderer::CleanupDestLink(const AudioObject& dest) {
TRACE_DURATION("audio", "BaseRenderer::CleanupDestLink");
auto it = packet_queues_.find(&dest);
FX_DCHECK(it != packet_queues_.end());
auto queue = std::move(it->second);
packet_queues_.erase(it);
// Flush this queue to:
//
// 1) Ensure we release any packet references in order.
// 2) Hold a reference to self until the flush has completed. This is needed because the packets
// in the queue are allocated using a SlabAllocated owned by us, so we ensure we outlive
// our packets.
//
// It's okay to release the reference to |queue| since either the Flush will have completed
// synchronously, or otherwise the mix job will hold a strong reference to the queue and perform
// the flush at the end of the mix job when the packet queue buffers are unlocked.
queue->Flush(PendingFlushToken::Create(context_.threading_model().FidlDomain().dispatcher(),
[self = shared_from_this()] {}));
// If this was our one adjustable clock, mark that a new dest link can use it.
if (queue->reference_clock().is_adjustable()) {
FX_DCHECK(client_allows_clock_adjustment_);
adjustable_clock_is_allocated_ = false;
}
}
void BaseRenderer::RecomputeMinLeadTime() {
TRACE_DURATION("audio", "BaseRenderer::RecomputeMinLeadTime");
zx::duration cur_lead_time;
for (const auto& [_, packet_queue] : packet_queues_) {
cur_lead_time = std::max(cur_lead_time, packet_queue->GetPresentationDelay());
}
if (min_lead_time_ != cur_lead_time) {
reporter_->SetMinLeadTime(cur_lead_time);
min_lead_time_ = cur_lead_time;
ReportNewMinLeadTime();
}
}
// IsOperating is true any time we have any packets in flight. Configuration functions cannot be
// called any time we are operational.
bool BaseRenderer::IsOperating() {
TRACE_DURATION("audio", "BaseRenderer::IsOperating");
for (const auto& [_, packet_queue] : packet_queues_) {
// If the packet queue is not empty then this link _is_ operating.
if (!packet_queue->empty()) {
return true;
}
}
return false;
}
bool BaseRenderer::ValidateConfig() {
TRACE_DURATION("audio", "BaseRenderer::ValidateConfig");
if (config_validated_) {
return true;
}
if (!format_valid() || payload_buffers_.empty()) {
return false;
}
// Compute the number of fractional frames per PTS tick.
Fixed frac_fps(format()->stream_type().frames_per_second);
frac_frames_per_pts_tick_ =
TimelineRate::Product(pts_ticks_per_second_.Inverse(), TimelineRate(frac_fps.raw_value(), 1));
// Compute the PTS continuity threshold expressed in fractional input frames.
if (!pts_continuity_threshold_set_) {
// The user has not explicitly set a continuity threshold. Default to 1/2
// of a PTS tick expressed in fractional input frames, rounded up.
pts_continuity_threshold_frac_frame_ =
Fixed::FromRaw((frac_frames_per_pts_tick_.Scale(1) + 1) >> 1);
} else {
pts_continuity_threshold_frac_frame_ =
Fixed::FromRaw(static_cast<double>(frac_fps.raw_value()) * pts_continuity_threshold_);
}
AUDIO_LOG_OBJ(DEBUG, this) << " threshold_set_: " << pts_continuity_threshold_set_
<< ", thres_frac_frame_: " << std::hex
<< pts_continuity_threshold_frac_frame_.raw_value();
// Compute the number of fractional frames per reference clock tick.
// Later we reconcile the actual reference clock with CLOCK_MONOTONIC
//
frac_frames_per_ref_tick_ = TimelineRate(frac_fps.raw_value(), 1'000'000'000u);
// TODO(mpuryear): Precompute anything else needed here. Adding links to other
// outputs (and selecting resampling filters) might belong here as well.
// Initialize the WavWriter here.
wav_writer_.Initialize(nullptr, format()->stream_type().sample_format,
format()->stream_type().channels,
format()->stream_type().frames_per_second,
(format()->bytes_per_frame() * 8) / format()->stream_type().channels);
config_validated_ = true;
return true;
}
void BaseRenderer::ComputePtsToFracFrames(int64_t first_pts) {
TRACE_DURATION("audio", "BaseRenderer::ComputePtsToFracFrames");
// We should not be calling this, if transformation is already valid.
FX_DCHECK(!pts_to_frac_frames_valid_);
pts_to_frac_frames_ =
TimelineFunction(next_frac_frame_pts_.raw_value(), first_pts, frac_frames_per_pts_tick_);
pts_to_frac_frames_valid_ = true;
AUDIO_LOG_OBJ(DEBUG, this) << " (" << first_pts
<< ") => stime:" << pts_to_frac_frames_.subject_time()
<< ", rtime:" << pts_to_frac_frames_.reference_time()
<< ", sdelta:" << pts_to_frac_frames_.subject_delta()
<< ", rdelta:" << pts_to_frac_frames_.reference_delta();
}
void BaseRenderer::AddPayloadBuffer(uint32_t id, zx::vmo payload_buffer) {
TRACE_DURATION("audio", "BaseRenderer::AddPayloadBuffer");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
AUDIO_LOG_OBJ(DEBUG, this) << " (id: " << id << ")";
// TODO(fxbug.dev/13655): Lift this restriction.
if (IsOperating()) {
FX_LOGS(ERROR) << "Attempted to set payload buffer while in operational mode.";
return;
}
auto vmo_mapper = fbl::MakeRefCounted<RefCountedVmoMapper>();
// Ideally we would reject this request if we already have a payload buffer with |id|, however
// some clients currently rely on being able to update the payload buffer without first calling
// |RemovePayloadBuffer|.
payload_buffers_[id] = vmo_mapper;
zx_status_t res = vmo_mapper->Map(payload_buffer, 0, 0, ZX_VM_PERM_READ, context_.vmar());
if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "Failed to map payload buffer";
return;
}
reporter_->AddPayloadBuffer(id, vmo_mapper->size());
// Things went well, cancel the cleanup hook. If our config had been validated previously, it will
// have to be revalidated as we move into the operational phase of our life.
InvalidateConfiguration();
cleanup.cancel();
}
void BaseRenderer::RemovePayloadBuffer(uint32_t id) {
TRACE_DURATION("audio", "BaseRenderer::RemovePayloadBuffer");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
AUDIO_LOG_OBJ(DEBUG, this) << " (id: " << id << ")";
// TODO(fxbug.dev/13655): Lift this restriction.
if (IsOperating()) {
FX_LOGS(ERROR) << "Attempted to remove payload buffer while in the operational mode.";
return;
}
if (payload_buffers_.erase(id) != 1) {
FX_LOGS(ERROR) << "Invalid payload buffer id";
return;
}
reporter_->RemovePayloadBuffer(id);
cleanup.cancel();
}
void BaseRenderer::SetPtsUnits(uint32_t tick_per_second_numerator,
uint32_t tick_per_second_denominator) {
TRACE_DURATION("audio", "BaseRenderer::SetPtsUnits");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
AUDIO_LOG_OBJ(DEBUG, this) << " (pts ticks per sec: " << std::dec << tick_per_second_numerator
<< " / " << tick_per_second_denominator << ")";
if (IsOperating()) {
FX_LOGS(ERROR) << "Attempted to set PTS units while in operational mode.";
return;
}
if (!tick_per_second_numerator || !tick_per_second_denominator) {
FX_LOGS(ERROR) << "Bad PTS ticks per second (" << tick_per_second_numerator << "/"
<< tick_per_second_denominator << ")";
return;
}
pts_ticks_per_second_ = TimelineRate(tick_per_second_numerator, tick_per_second_denominator);
// Things went well, cancel the cleanup hook. If our config had been validated previously, it will
// have to be revalidated as we move into the operational phase of our life.
InvalidateConfiguration();
cleanup.cancel();
}
void BaseRenderer::SetPtsContinuityThreshold(float threshold_seconds) {
TRACE_DURATION("audio", "BaseRenderer::SetPtsContinuityThreshold");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
AUDIO_LOG_OBJ(DEBUG, this) << " (" << threshold_seconds << " sec)";
if (IsOperating()) {
FX_LOGS(ERROR) << "Attempted to set PTS cont threshold while in operational mode.";
return;
}
if (threshold_seconds < 0.0) {
FX_LOGS(ERROR) << "Invalid PTS continuity threshold (" << threshold_seconds << ")";
return;
}
reporter_->SetPtsContinuityThreshold(threshold_seconds);
pts_continuity_threshold_ = threshold_seconds;
pts_continuity_threshold_set_ = true;
// Things went well, cancel the cleanup hook. If our config had been validated previously, it will
// have to be revalidated as we move into the operational phase of our life.
InvalidateConfiguration();
cleanup.cancel();
}
void BaseRenderer::SendPacket(fuchsia::media::StreamPacket packet, SendPacketCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::SendPacket");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
// It is an error to attempt to send a packet before we have established at least a minimum valid
// configuration. IOW - the format must have been configured, and we must have an established
// payload buffer.
if (!ValidateConfig()) {
FX_LOGS(ERROR) << "Failed to validate configuration during SendPacket";
return;
}
// Lookup our payload buffer.
auto it = payload_buffers_.find(packet.payload_buffer_id);
if (it == payload_buffers_.end()) {
FX_LOGS(ERROR) << "Invalid payload_buffer_id";
return;
}
auto payload_buffer = it->second;
// Start by making sure that the region we are receiving is made from an integral number of audio
// frames. Count the total number of frames in the process.
uint32_t frame_size = format()->bytes_per_frame();
FX_DCHECK(frame_size != 0);
if (packet.payload_size % frame_size) {
FX_LOGS(ERROR) << "Region length (" << packet.payload_size
<< ") is not divisible by by audio frame size (" << frame_size << ")";
return;
}
// Make sure that we don't exceed the maximum permissible frames-per-packet.
uint32_t frame_count = packet.payload_size / frame_size;
if (frame_count > fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET) {
FX_LOGS(ERROR) << "Audio frame count (" << frame_count << ") exceeds maximum allowed ("
<< fuchsia::media::MAX_FRAMES_PER_RENDERER_PACKET << ")";
return;
}
// Make sure that the packet offset/size exists entirely within the payload buffer.
FX_DCHECK(payload_buffer != nullptr);
uint64_t start = packet.payload_offset;
uint64_t end = start + packet.payload_size;
uint64_t pb_size = payload_buffer->size();
if ((start >= pb_size) || (end > pb_size)) {
FX_LOGS(ERROR) << "Bad packet range [" << start << ", " << end << "). Payload buffer size is "
<< pb_size;
return;
}
reporter_->SendPacket(packet);
// Compute the PTS values for this packet applying our interpolation and continuity thresholds as
// we go. Start by checking to see if this our PTS to frames transformation needs to be computed
// (this should be needed after startup, and after each flush operation).
if (!pts_to_frac_frames_valid_) {
ComputePtsToFracFrames((packet.pts == fuchsia::media::NO_TIMESTAMP) ? 0 : packet.pts);
}
// Now compute the starting PTS expressed in fractional input frames. If no explicit PTS was
// provided, interpolate using the next expected PTS.
Fixed start_pts;
Fixed packet_ffpts{0};
if (packet.pts == fuchsia::media::NO_TIMESTAMP) {
start_pts = next_frac_frame_pts_;
// If the packet has both pts == NO_TIMESTAMP and STREAM_PACKET_FLAG_DISCONTINUITY, then we will
// ensure the calculated PTS is playable (that is, greater than now + min_lead_time).
if (packet.flags & fuchsia::media::STREAM_PACKET_FLAG_DISCONTINUITY) {
zx::time ref_now;
zx_status_t status = raw_clock().read(ref_now.get_address());
FX_CHECK(status == ZX_OK);
zx::time deadline = ref_now + min_lead_time_;
auto first_valid_frame =
Fixed::FromRaw(reference_clock_to_fractional_frames_->Apply(deadline.get()));
if (start_pts < first_valid_frame) {
zx::time start_ref_time = deadline + kPaddingForUnspecifiedRefTime;
start_pts =
Fixed::FromRaw(reference_clock_to_fractional_frames_->Apply(start_ref_time.get()));
}
}
} else {
// Looks like we have an explicit PTS on this packet. Boost it into the fractional input frame
// domain, then apply our continuity threshold rules.
packet_ffpts = Fixed::FromRaw(pts_to_frac_frames_.Apply(packet.pts));
Fixed delta = packet_ffpts - next_frac_frame_pts_;
delta = delta.Absolute();
start_pts =
(delta < pts_continuity_threshold_frac_frame_) ? next_frac_frame_pts_ : packet_ffpts;
}
uint32_t frame_offset = packet.payload_offset / frame_size;
AUDIO_LOG_OBJ(TRACE, this) << " [pkt " << std::hex << std::setw(8) << packet_ffpts.raw_value()
<< ", now " << std::setw(8) << next_frac_frame_pts_.raw_value()
<< "] => " << std::setw(8) << start_pts.raw_value() << " - "
<< std::setw(8)
<< start_pts.raw_value() + pts_to_frac_frames_.Apply(frame_count)
<< ", offset " << std::setw(7)
<< pts_to_frac_frames_.Apply(frame_offset);
// Regardless of timing, capture this data to file.
auto packet_buff = reinterpret_cast<uint8_t*>(payload_buffer->start()) + packet.payload_offset;
wav_writer_.Write(packet_buff, packet.payload_size);
wav_writer_.UpdateHeader();
// Snap the starting pts to an input frame boundary.
//
// TODO(fxbug.dev/13374): Don't do this. If a user wants to write an explicit timestamp on a
// source packet which schedules the packet to start at a fractional position on the source time
// line, we should probably permit this. We need to make sure that the mixer cores are ready to
// handle this case before proceeding, however.
start_pts = Fixed(start_pts.Floor());
// Create the packet.
auto packet_ref = packet_allocator_.New(
payload_buffer, packet.payload_offset, Fixed(frame_count), start_pts,
context_.threading_model().FidlDomain().dispatcher(), std::move(callback));
if (!packet_ref) {
FX_LOGS(ERROR) << "Client created too many concurrent Packets; Allocator has created "
<< packet_allocator_.obj_count() << " / " << packet_allocator_.max_obj_count()
<< " max allocations";
return;
}
// The end pts is the value we will use for the next packet's start PTS, if the user does not
// provide an explicit PTS.
next_frac_frame_pts_ = packet_ref->end();
// Distribute our packet to all our dest links
for (auto& [_, packet_queue] : packet_queues_) {
packet_queue->PushPacket(packet_ref);
}
// Things went well, cancel the cleanup hook.
cleanup.cancel();
}
void BaseRenderer::SendPacketNoReply(fuchsia::media::StreamPacket packet) {
TRACE_DURATION("audio", "BaseRenderer::SendPacketNoReply");
AUDIO_LOG_OBJ(TRACE, this);
SendPacket(packet, nullptr);
}
void BaseRenderer::EndOfStream() {
TRACE_DURATION("audio", "BaseRenderer::EndOfStream");
AUDIO_LOG_OBJ(DEBUG, this);
ReportStop();
// Does nothing.
}
void BaseRenderer::DiscardAllPackets(DiscardAllPacketsCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::DiscardAllPackets");
AUDIO_LOG_OBJ(DEBUG, this);
// If the user has requested a callback, create the flush token we will use to invoke the callback
// at the proper time.
fbl::RefPtr<PendingFlushToken> flush_token;
if (callback != nullptr) {
flush_token = PendingFlushToken::Create(context_.threading_model().FidlDomain().dispatcher(),
std::move(callback));
}
// Tell each link to flush. If link is currently processing pending data, it will take a reference
// to the flush token and ensure a callback is queued at the proper time (after all pending
// packet-complete callbacks are queued).
for (auto& [_, packet_queue] : packet_queues_) {
packet_queue->Flush(flush_token);
}
}
void BaseRenderer::DiscardAllPacketsNoReply() {
TRACE_DURATION("audio", "BaseRenderer::DiscardAllPacketsNoReply");
AUDIO_LOG_OBJ(DEBUG, this);
DiscardAllPackets(nullptr);
}
void BaseRenderer::Play(int64_t _reference_time, int64_t media_time, PlayCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::Play");
AUDIO_LOG_OBJ(DEBUG, this) << "Request (ref: "
<< (_reference_time == fuchsia::media::NO_TIMESTAMP ? -1
: _reference_time)
<< ", media: "
<< (media_time == fuchsia::media::NO_TIMESTAMP ? -1 : media_time)
<< ")";
zx::time reference_time(_reference_time);
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
if (!ValidateConfig()) {
FX_LOGS(ERROR) << "Failed to validate configuration during Play";
return;
}
// TODO(mpuryear): What do we want to do here if we are already playing?
// Did the user supply a reference time? If not, figure out a safe starting time based on the
// outputs we are currently linked to.
if (reference_time.get() == fuchsia::media::NO_TIMESTAMP) {
// TODO(mpuryear): How much more than the minimum clock lead time do we want to pad this by?
// Also, if/when lead time requirements change, do we want to introduce a discontinuity?
//
// We could consider an explicit mode (make it default) where timing across outputs is treated
// as "loose". Specifically, make no effort to account for external latency, nor to synchronize
// streams across multiple parallel outputs. In this mode we must update lead time upon changes
// in internal interconnect requirements, but impact should be small since internal lead time
// factors tend to be small, while external factors can be huge.
zx::time ref_now;
auto status = raw_clock_.read(ref_now.get_address());
FX_CHECK(status == ZX_OK) << "Error while reading clock: " << status;
reference_time = ref_now + min_lead_time_ + kPaddingForUnspecifiedRefTime;
}
// If no media time was specified, use the first pending packet's media time.
//
// Note: users specify the units for media time by calling SetPtsUnits(), or nanoseconds if this
// is never called. Internally we use fractional input frames, on the timeline defined when
// transitioning to operational mode.
Fixed frac_frame_media_time;
if (media_time == fuchsia::media::NO_TIMESTAMP) {
// Are we resuming from pause?
if (pause_time_frac_frames_valid_) {
frac_frame_media_time = pause_time_frac_frames_;
} else {
// TODO(mpuryear): peek the first PTS of the pending queue.
frac_frame_media_time = Fixed(0);
}
// If we do not know the pts_to_frac_frames relationship yet, compute one.
if (!pts_to_frac_frames_valid_) {
next_frac_frame_pts_ = frac_frame_media_time;
ComputePtsToFracFrames(0);
}
media_time = pts_to_frac_frames_.ApplyInverse(frac_frame_media_time.raw_value());
} else {
// If we do not know the pts_to_frac_frames relationship yet, compute one.
if (!pts_to_frac_frames_valid_) {
ComputePtsToFracFrames(media_time);
frac_frame_media_time = next_frac_frame_pts_;
} else {
frac_frame_media_time = Fixed::FromRaw(pts_to_frac_frames_.Apply(media_time));
}
}
// Update our transformation.
//
// TODO(mpuryear): if we need to trigger a remix for our outputs, do it here.
//
reference_clock_to_fractional_frames_->Update(TimelineFunction(
frac_frame_media_time.raw_value(), reference_time.get(), frac_frames_per_ref_tick_));
AUDIO_LOG(DEBUG) << "Actual: (ref: " << reference_time.get() << ", media: " << media_time << ")";
AUDIO_LOG(DEBUG) << "frac_frame_media_time:" << std::hex << frac_frame_media_time.raw_value();
// If the user requested a callback, invoke it now.
if (callback != nullptr) {
callback(reference_time.get(), media_time);
}
ReportStart();
// Things went well, cancel the cleanup hook.
cleanup.cancel();
}
void BaseRenderer::PlayNoReply(int64_t reference_time, int64_t media_time) {
TRACE_DURATION("audio", "BaseRenderer::PlayNoReply");
AUDIO_LOG_OBJ(DEBUG, this)
<< " (ref: " << (reference_time == fuchsia::media::NO_TIMESTAMP ? -1 : reference_time)
<< ", media: " << (media_time == fuchsia::media::NO_TIMESTAMP ? -1 : media_time) << ")";
Play(reference_time, media_time, nullptr);
}
void BaseRenderer::Pause(PauseCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::Pause");
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
if (!ValidateConfig()) {
FX_LOGS(ERROR) << "Failed to validate configuration during Pause";
return;
}
zx_time_t ref_now;
auto status = raw_clock().read(&ref_now);
FX_CHECK(status == ZX_OK) << "Error while reading clock: " << status;
// Update our reference clock to fractional frame transformation, keeping it 1st order continuous.
pause_time_frac_frames_ = Fixed::FromRaw(reference_clock_to_fractional_frames_->Apply(ref_now));
pause_time_frac_frames_valid_ = true;
reference_clock_to_fractional_frames_->Update(
TimelineFunction(pause_time_frac_frames_.raw_value(), ref_now, {0, 1}));
// If we do not know the pts_to_frac_frames relationship yet, compute one.
if (!pts_to_frac_frames_valid_) {
next_frac_frame_pts_ = pause_time_frac_frames_;
ComputePtsToFracFrames(0);
}
// If the user requested a callback, figure out the media time that we paused at and report back.
AUDIO_LOG_OBJ(DEBUG, this) << ". Actual (ref: " << ref_now << ", media: "
<< pts_to_frac_frames_.ApplyInverse(
pause_time_frac_frames_.raw_value())
<< ")";
if (callback != nullptr) {
int64_t paused_media_time =
pts_to_frac_frames_.ApplyInverse(pause_time_frac_frames_.raw_value());
callback(ref_now, paused_media_time);
}
ReportStop();
// Things went well, cancel the cleanup hook.
cleanup.cancel();
}
void BaseRenderer::PauseNoReply() {
TRACE_DURATION("audio", "BaseRenderer::PauseNoReply");
AUDIO_LOG_OBJ(DEBUG, this);
Pause(nullptr);
}
void BaseRenderer::ReportStart() {
if (state_ == State::Paused) {
reporter_->StartSession(zx::clock::get_monotonic());
state_ = State::Playing;
}
}
void BaseRenderer::ReportStop() {
if (state_ == State::Playing) {
reporter_->StopSession(zx::clock::get_monotonic());
state_ = State::Paused;
}
}
void BaseRenderer::OnLinkAdded() { RecomputeMinLeadTime(); }
void BaseRenderer::EnableMinLeadTimeEvents(bool enabled) {
TRACE_DURATION("audio", "BaseRenderer::EnableMinLeadTimeEvents");
AUDIO_LOG_OBJ(DEBUG, this);
min_lead_time_events_enabled_ = enabled;
if (enabled) {
ReportNewMinLeadTime();
}
}
void BaseRenderer::GetMinLeadTime(GetMinLeadTimeCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::GetMinLeadTime");
AUDIO_LOG_OBJ(DEBUG, this);
callback(min_lead_time_.to_nsecs());
}
void BaseRenderer::ReportNewMinLeadTime() {
TRACE_DURATION("audio", "BaseRenderer::ReportNewMinLeadTime");
if (min_lead_time_events_enabled_) {
AUDIO_LOG_OBJ(DEBUG, this);
auto& lead_time_event = audio_renderer_binding_.events();
lead_time_event.OnMinLeadTimeChanged(min_lead_time_.to_nsecs());
}
}
// Use our adjustable clock as the default. This starts as an adjustable clone of MONOTONIC, but
// will track the clock of the device where the renderer is routed.
zx_status_t BaseRenderer::SetAdjustableReferenceClock() {
TRACE_DURATION("audio", "BaseRenderer::SetAdjustableReferenceClock");
raw_clock_ = audio::clock::AdjustableCloneOfMonotonic();
if (!raw_clock_.is_valid()) {
FX_LOGS(ERROR) << "Default reference clock is not valid";
return ZX_ERR_INVALID_ARGS;
}
client_allows_clock_adjustment_ = true;
return ZX_OK;
}
// Ensure that the clock has appropriate rights.
// Should also read it here, to ensure everything works?
zx_status_t BaseRenderer::SetCustomReferenceClock(zx::clock ref_clock) {
constexpr auto kRequiredClockRights = ZX_RIGHT_DUPLICATE | ZX_RIGHT_TRANSFER | ZX_RIGHT_READ;
auto status = ref_clock.replace(kRequiredClockRights, &raw_clock_);
if (status != ZX_OK || !raw_clock_.is_valid()) {
FX_PLOGS(WARNING, status) << "Could not set rights on client-submitted reference clock";
return ZX_ERR_INVALID_ARGS;
}
client_allows_clock_adjustment_ = false;
return ZX_OK;
}
// Regardless of the source of the reference clock, we can duplicate and return it here.
void BaseRenderer::GetReferenceClock(GetReferenceClockCallback callback) {
TRACE_DURATION("audio", "BaseRenderer::GetReferenceClock");
AUDIO_LOG_OBJ(DEBUG, this);
// If something goes wrong, hang up the phone and shutdown.
auto cleanup = fit::defer([this]() { context_.route_graph().RemoveRenderer(*this); });
// Regardless of whether raw_clock_ is writable, this strips off the WRITE right.
auto clock_result = audio::clock::DuplicateClock(raw_clock_);
if (!clock_result.is_ok()) {
FX_LOGS(ERROR) << "Could not duplicate reference clock";
return;
}
callback(clock_result.take_value());
cleanup.cancel();
}
} // namespace media::audio