blob: 3289563913b3342c42c7f97a43916bf521345344 [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/packet_queue.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <iomanip>
#include <ffl/string.h>
#include "src/media/audio/audio_core/audio_clock.h"
#include "src/media/audio/audio_core/audio_object.h"
#include "src/media/audio/audio_core/mixer/gain.h"
#include "src/media/audio/lib/format/format.h"
#include "src/media/audio/lib/logging/logging.h"
namespace media::audio {
namespace {
// To what extent should client-side underflows be logged? (A "client-side underflow" refers to when
// all or part of a packet's data is discarded because its start timestamp has already passed.)
// For each packet queue, we will log the first underflow. For subsequent occurrences, depending on
// audio_core's logging level, we throttle how frequently these are displayed. If log_level is set
// to TRACE or SPEW, all client-side underflows are logged -- at log_level -1: VLOG TRACE -- as
// specified by kUnderflowTraceInterval. If set to INFO, we log less often, at log_level 1: INFO,
// throttling by the factor kUnderflowInfoInterval. If set to WARNING or higher, we throttle these
// even more, specified by kUnderflowWarningInterval. Note: by default we set NDEBUG builds to
// WARNING and DEBUG builds to INFO. To disable all logging of client-side underflows, set
// kLogUnderflow to false.
static constexpr bool kLogUnderflow = true;
static constexpr uint16_t kUnderflowTraceInterval = 1;
static constexpr uint16_t kUnderflowInfoInterval = 10;
static constexpr uint16_t kUnderflowWarningInterval = 100;
} // namespace
PacketQueue::PacketQueue(Format format, AudioClock audio_clock)
: PacketQueue(format, nullptr, std::move(audio_clock)) {}
PacketQueue::PacketQueue(Format format, fbl::RefPtr<VersionedTimelineFunction> timeline_function,
AudioClock audio_clock)
: ReadableStream(std::move(format)),
timeline_function_(std::move(timeline_function)),
audio_clock_(std::move(audio_clock)) {}
PacketQueue::~PacketQueue() {
pending_flush_packet_queue_.clear();
pending_packet_queue_.clear();
pending_flush_token_queue_.clear();
}
void PacketQueue::PushPacket(const fbl::RefPtr<Packet>& packet) {
TRACE_DURATION("audio", "PacketQueue::PushPacket");
std::lock_guard<std::mutex> locker(pending_mutex_);
pending_packet_queue_.emplace_back(std::move(packet));
}
void PacketQueue::Flush(const fbl::RefPtr<PendingFlushToken>& flush_token) {
TRACE_DURATION("audio", "PacketQueue::Flush");
std::deque<fbl::RefPtr<Packet>> flushed_packets;
{
std::lock_guard<std::mutex> locker(pending_mutex_);
flushed_ = true;
if (processing_in_progress_) {
// Is the sink currently mixing? If so, the flush cannot complete until the mix operation has
// finished. Move the 'waiting to be rendered' packets to the back of the 'waiting to be
// flushed queue', and append our flush token (if any) to the pending flush token queue. The
// sink's thread will take care of releasing these objects back to the service thread for
// cleanup when it has finished its current job.
for (auto& packet : pending_packet_queue_) {
pending_flush_packet_queue_.emplace_back(std::move(packet));
}
pending_packet_queue_.clear();
if (flush_token != nullptr) {
pending_flush_token_queue_.emplace_back(std::move(flush_token));
}
return;
} else {
// If the sink is not currently mixing, then we just swap the contents the pending packet
// queues with out local queue and release the packets in the proper order once we have left
// the pending mutex lock.
FX_DCHECK(pending_flush_packet_queue_.empty());
FX_DCHECK(pending_flush_token_queue_.empty());
flushed_packets.swap(pending_packet_queue_);
}
}
// Release the packets, front to back.
for (auto& ptr : flushed_packets) {
ptr = nullptr;
}
}
std::optional<ReadableStream::Buffer> PacketQueue::ReadLock(Fixed frame, size_t frame_count) {
TRACE_DURATION("audio", "PacketQueue::ReadLock");
std::lock_guard<std::mutex> locker(pending_mutex_);
FX_DCHECK(!processing_in_progress_);
if (!pending_packet_queue_.size()) {
return std::nullopt;
}
// TODO(fxbug.dev/50669): Obey ReadLock API.
processing_in_progress_ = true;
auto& packet = pending_packet_queue_.front();
bool is_continuous = !flushed_;
flushed_ = false;
return std::make_optional<ReadableStream::Buffer>(
packet->start(), packet->length(), packet->payload(), is_continuous, usage_mask_,
Gain::kUnityGainDb, [this](bool fully_consumed) { this->ReadUnlock(fully_consumed); });
}
void PacketQueue::ReadUnlock(bool fully_consumed) {
TRACE_DURATION("audio", "PacketQueue::ReadUnlock");
std::lock_guard<std::mutex> locker(pending_mutex_);
FX_DCHECK(processing_in_progress_);
processing_in_progress_ = false;
// Did a flush take place while we were working? If so release each of the packets waiting to
// be flushed back to the service thread, then release each of the flush tokens.
if (!pending_flush_packet_queue_.empty() || !pending_flush_token_queue_.empty()) {
for (auto& ptr : pending_flush_packet_queue_) {
ptr = nullptr;
}
for (auto& ptr : pending_flush_token_queue_) {
ptr = nullptr;
}
pending_flush_packet_queue_.clear();
pending_flush_token_queue_.clear();
return;
}
// If the buffer was fully consumed, release the first packet. The queue must not be empty,
// unless the queue was flushed between ReadLock and ReadUnlock, but that case is handled above.
if (fully_consumed) {
FX_DCHECK(!pending_packet_queue_.empty());
pending_packet_queue_.pop_front();
}
}
void PacketQueue::Trim(Fixed frame) {
TRACE_DURATION("audio", "PacketQueue::Trim");
std::lock_guard<std::mutex> locker(pending_mutex_);
while (!pending_packet_queue_.empty()) {
auto packet = pending_packet_queue_.front();
if (packet->end() > frame) {
return;
}
pending_packet_queue_.pop_front();
}
}
BaseStream::TimelineFunctionSnapshot PacketQueue::ref_time_to_frac_presentation_frame() const {
if (!timeline_function_) {
return {
.timeline_function = TimelineFunction(),
.generation = kInvalidGenerationId,
};
}
auto [timeline_function, generation] = timeline_function_->get();
return {
.timeline_function = timeline_function,
.generation = generation,
};
}
void PacketQueue::ReportUnderflow(Fixed frac_source_start, Fixed frac_source_mix_point,
zx::duration underflow_duration) {
TRACE_INSTANT("audio", "PacketQueue::ReportUnderflow", TRACE_SCOPE_PROCESS);
TRACE_ALERT("audio", "audiounderflow");
uint16_t underflow_count = std::atomic_fetch_add<uint16_t>(&underflow_count_, 1u);
if (underflow_reporter_) {
zx::time start_mono_time;
// If the renderer is paused concurrently with a mix, the timeline_function may be zeroed.
// If that happens, estimate start_mono_time using now - underflow_duration.
auto ref_time_to_fixed = timeline_function_->get().first;
if (ref_time_to_fixed.invertible()) {
auto fixed_to_ref_time = ref_time_to_fixed.Inverse();
auto start_ref_time = zx::time(fixed_to_ref_time.Apply(frac_source_start.raw_value()));
start_mono_time = reference_clock().MonotonicTimeFromReferenceTime(start_ref_time);
} else {
start_mono_time = zx::clock::get_monotonic() - underflow_duration;
}
underflow_reporter_(start_mono_time, start_mono_time + underflow_duration);
}
if constexpr (kLogUnderflow) {
auto underflow_msec = static_cast<double>(underflow_duration.to_nsecs()) / ZX_MSEC(1);
if ((kUnderflowWarningInterval > 0) && (underflow_count % kUnderflowWarningInterval == 0)) {
FX_LOGS(WARNING) << "PACKET QUEUE UNDERFLOW #" << underflow_count + 1 << " (1/"
<< kUnderflowWarningInterval << "): source-start "
<< ffl::Format(frac_source_start).c_str() << " missed mix-point "
<< ffl::Format(frac_source_mix_point).c_str() << " by "
<< std::setprecision(4) << underflow_msec << " ms";
} else if ((kUnderflowInfoInterval > 0) && (underflow_count % kUnderflowInfoInterval == 0)) {
FX_LOGS(INFO) << "PACKET QUEUE UNDERFLOW #" << underflow_count + 1 << " (1/"
<< kUnderflowInfoInterval << "): source-start "
<< ffl::Format(frac_source_start).c_str() << " missed mix-point "
<< ffl::Format(frac_source_mix_point).c_str() << " by " << std::setprecision(4)
<< underflow_msec << " ms";
} else if ((kUnderflowTraceInterval > 0) && (underflow_count % kUnderflowTraceInterval == 0)) {
FX_LOGS(TRACE) << "PACKET QUEUE UNDERFLOW #" << underflow_count + 1 << " (1/"
<< kUnderflowTraceInterval << "): source-start "
<< ffl::Format(frac_source_start).c_str() << " missed mix-point "
<< ffl::Format(frac_source_mix_point).c_str() << " by " << std::setprecision(4)
<< underflow_msec << " ms";
}
}
}
void PacketQueue::ReportPartialUnderflow(Fixed frac_source_offset, int64_t dest_mix_offset) {
TRACE_INSTANT("audio", "PacketQueue::ReportPartialUnderflow", TRACE_SCOPE_PROCESS);
// Shifts by less than four source frames do not necessarily indicate underflow. A shift of this
// duration can be caused by the round-to-nearest-dest-frame step, when our rate-conversion ratio
// is sufficiently large (it can be as large as 4:1).
if (frac_source_offset >= 4) {
auto partial_underflow_count = std::atomic_fetch_add<uint16_t>(&partial_underflow_count_, 1u);
if constexpr (kLogUnderflow) {
if ((kUnderflowWarningInterval > 0) &&
(partial_underflow_count % kUnderflowWarningInterval == 0)) {
FX_LOGS(WARNING) << "PACKET QUEUE SHIFT #" << partial_underflow_count + 1 << " (1/"
<< kUnderflowWarningInterval << "): shifted by "
<< ffl::Format(frac_source_offset).c_str() << " source frames and "
<< dest_mix_offset << " mix (output) frames";
} else if ((kUnderflowInfoInterval > 0) &&
(partial_underflow_count % kUnderflowInfoInterval == 0)) {
FX_LOGS(INFO) << "PACKET QUEUE SHIFT #" << partial_underflow_count + 1 << " (1/"
<< kUnderflowInfoInterval << "): shifted by "
<< ffl::Format(frac_source_offset).c_str() << " source frames and "
<< dest_mix_offset << " mix (output) frames";
} else if ((kUnderflowTraceInterval > 0) &&
(partial_underflow_count % kUnderflowTraceInterval == 0)) {
FX_LOGS(TRACE) << "PACKET QUEUE SHIFT #" << partial_underflow_count + 1 << " (1/"
<< kUnderflowTraceInterval << "): shifted by "
<< ffl::Format(frac_source_offset).c_str() << " source frames and "
<< dest_mix_offset << " mix (output) frames";
}
}
} else {
if constexpr (kLogUnderflow) {
FX_LOGS(TRACE) << "shifted " << dest_mix_offset
<< " mix (output) frames to align with source packet";
}
}
}
} // namespace media::audio