blob: d935fbebfa7f5ca9254d93ee6780119f7dca7924 [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 "garnet/bin/media/audio_core/standard_output_base.h"
#include <fbl/auto_lock.h>
#include <lib/fit/defer.h>
#include <limits>
#include "garnet/bin/media/audio_core/audio_link.h"
#include "garnet/bin/media/audio_core/audio_renderer_format_info.h"
#include "garnet/bin/media/audio_core/audio_renderer_impl.h"
#include "garnet/bin/media/audio_core/mixer/mixer.h"
#include "garnet/bin/media/audio_core/mixer/no_op.h"
#include "lib/fxl/logging.h"
#include "lib/fxl/time/time_delta.h"
namespace media::audio {
static constexpr fxl::TimeDelta kMaxTrimPeriod =
fxl::TimeDelta::FromMilliseconds(10);
StandardOutputBase::StandardOutputBase(AudioDeviceManager* manager)
: AudioOutput(manager) {
next_sched_time_ = fxl::TimePoint::Now();
next_sched_time_known_ = true;
source_link_refs_.reserve(16u);
}
StandardOutputBase::~StandardOutputBase() = default;
zx_status_t StandardOutputBase::Init() {
zx_status_t res = AudioOutput::Init();
if (res != ZX_OK) {
return res;
}
mix_timer_ = ::dispatcher::Timer::Create();
if (mix_timer_ == nullptr) {
return ZX_ERR_NO_MEMORY;
}
::dispatcher::Timer::ProcessHandler process_handler(
[output =
fbl::WrapRefPtr(this)](::dispatcher::Timer* timer) -> zx_status_t {
OBTAIN_EXECUTION_DOMAIN_TOKEN(token, output->mix_domain_);
output->Process();
return ZX_OK;
});
res = mix_timer_->Activate(mix_domain_, std::move(process_handler));
if (res != ZX_OK) {
FXL_LOG(ERROR) << "Failed to activate mix_timer_ (res " << res << ")";
}
return res;
}
void StandardOutputBase::Process() {
bool mixed = false;
fxl::TimePoint now = fxl::TimePoint::Now();
// At this point, we should always know when our implementation would like to
// be called to do some mixing work next. If we do not know, then we should
// have already shut down.
//
// If the next sched time has not arrived yet, don't attempt to mix anything.
// Just trim the queues and move on.
FXL_DCHECK(next_sched_time_known_);
if (now >= next_sched_time_) {
// Clear the flag. If the implementation does not set it during the cycle by
// calling SetNextSchedTime, we consider it an error and shut down.
next_sched_time_known_ = false;
// As long as our implementation wants to mix more and has not run into a
// problem trying to finish the mix job, mix some more.
do {
::memset(&cur_mix_job_, 0, sizeof(cur_mix_job_));
if (!StartMixJob(&cur_mix_job_, now)) {
break;
}
// If we have a mix job, then we must have an output producer, and an
// intermediate buffer allocated, and it must be large enough for the mix
// job we were given.
FXL_DCHECK(mix_buf_);
FXL_DCHECK(output_producer_);
FXL_DCHECK(cur_mix_job_.buf_frames <= mix_buf_frames_);
// If we are not muted, actually do the mix. Otherwise, just fill the
// final buffer with silence. Do not set the 'mixed' flag if we are
// muted. This is our signal that we still need to trim our sources
// (something that happens automatically if we mix).
if (!cur_mix_job_.sw_output_muted) {
// Fill the intermediate buffer with silence.
size_t bytes_to_zero = sizeof(mix_buf_[0]) * cur_mix_job_.buf_frames *
output_producer_->channels();
::memset(mix_buf_.get(), 0, bytes_to_zero);
// Mix each renderer into the intermediate accumulator buffer, then
// reformat (and clip) into the final output buffer.
ForeachLink(TaskType::Mix);
output_producer_->ProduceOutput(mix_buf_.get(), cur_mix_job_.buf,
cur_mix_job_.buf_frames);
mixed = true;
} else {
output_producer_->FillWithSilence(cur_mix_job_.buf,
cur_mix_job_.buf_frames);
}
} while (FinishMixJob(cur_mix_job_));
}
if (!next_sched_time_known_) {
FXL_LOG(ERROR) << "Output failed to schedule next service time. "
<< "Shutting down!";
ShutdownSelf();
return;
}
// If we mixed nothing this time, make sure that we trim all of our renderer
// queues. No matter what is going on with the output hardware, we are not
// allowed to hold onto the queued data past its presentation time.
if (!mixed) {
ForeachLink(TaskType::Trim);
}
// Figure out when we should wake up to do more work again. No matter how
// long our implementation wants to wait, we need to make sure to wake up and
// periodically trim our input queues.
fxl::TimePoint max_sched_time = now + kMaxTrimPeriod;
if (next_sched_time_ > max_sched_time) {
next_sched_time_ = max_sched_time;
}
zx_time_t next_time =
static_cast<zx_time_t>(next_sched_time_.ToEpochDelta().ToNanoseconds());
if (mix_timer_->Arm(next_time)) {
ShutdownSelf();
}
}
zx_status_t StandardOutputBase::InitializeSourceLink(const AudioLinkPtr& link) {
auto mix_bookkeeping = std::make_unique<Bookkeeping>();
// For now, refuse to link to anything but a packet source. This code does
// not currently know how to properly handle a ring-buffer source.
if (link->source_type() != AudioLink::SourceType::Packet) {
return ZX_ERR_INTERNAL;
}
// If we have an output, pick a mixer based on the input and output formats.
// Otherwise, we only need a NoOp mixer (for the time being).
auto& packet_link = *(static_cast<AudioLinkPacketSource*>(link.get()));
if (output_producer_) {
mix_bookkeeping->mixer = Mixer::Select(packet_link.format_info().format(),
*(output_producer_->format()));
} else {
mix_bookkeeping->mixer = MixerPtr(new audio::mixer::NoOp());
}
if (mix_bookkeeping->mixer == nullptr) {
FXL_LOG(ERROR) << "*** Audio system mixer cannot convert between formats "
"*** (could not select mixer while linking to output). "
"Usually, this indicates a 'num_channels' mismatch.";
return ZX_ERR_NOT_SUPPORTED;
}
// The Gain object contains multiple stages. In render, stream gain is
// "source" gain and device (or system) gain is "dest" gain.
//
// The renderer will set this link's source gain once this call returns.
//
// Set the dest gain -- device gain retrieved from device settings.
if (device_settings_ != nullptr) {
AudioDeviceSettings::GainState cur_gain_state;
device_settings_->SnapshotGainState(&cur_gain_state);
mix_bookkeeping->gain.SetDestMute(cur_gain_state.muted);
mix_bookkeeping->gain.SetDestGain(cur_gain_state.gain_db);
}
// Settings should exist but if they don't, we use default DestGain (Unity).
// Things went well. Stash a reference to our bookkeeping and get out.
link->set_bookkeeping(std::move(mix_bookkeeping));
return ZX_OK;
}
void StandardOutputBase::SetupMixBuffer(uint32_t max_mix_frames) {
FXL_DCHECK(output_producer_->channels() > 0u);
FXL_DCHECK(max_mix_frames > 0u);
FXL_DCHECK(static_cast<uint64_t>(max_mix_frames) *
output_producer_->channels() <=
std::numeric_limits<uint32_t>::max());
mix_buf_frames_ = max_mix_frames;
mix_buf_.reset(new float[mix_buf_frames_ * output_producer_->channels()]);
}
void StandardOutputBase::ForeachLink(TaskType task_type) {
// Make a copy of our currently active set of links so that we don't have to
// hold onto mutex_ for the entire mix operation.
{
fbl::AutoLock links_lock(&links_lock_);
ZX_DEBUG_ASSERT(source_link_refs_.empty());
for (const auto& link_ptr : source_links_) {
// For now, skip ring-buffer source links. This code cannot mix them yet.
if (link_ptr->source_type() != AudioLink::SourceType::Packet) {
continue;
}
source_link_refs_.push_back(link_ptr);
}
}
// In all cases, release our temporary references upon leaving this method.
auto cleanup = fit::defer(
[this]() FXL_NO_THREAD_SAFETY_ANALYSIS { source_link_refs_.clear(); });
for (const auto& link : source_link_refs_) {
// Quit early if we should be shutting down.
if (is_shutting_down()) {
return;
}
// Is the link still valid? If so, process it.
if (!link->valid()) {
continue;
}
FXL_DCHECK(link->source_type() == AudioLink::SourceType::Packet);
FXL_DCHECK(link->GetSource()->type() == AudioObject::Type::AudioRenderer);
auto packet_link = static_cast<AudioLinkPacketSource*>(link.get());
auto audio_renderer =
fbl::RefPtr<AudioRendererImpl>::Downcast(link->GetSource());
// It would be nice to be able to use a dynamic cast for this, but currently
// we are building with no-rtti
auto* info = static_cast<Bookkeeping*>(packet_link->bookkeeping().get());
FXL_DCHECK(info);
// Ensure the mapping from source-frame to local-time is up-to-date.
UpdateSourceTrans(audio_renderer, info);
bool setup_done = false;
fbl::RefPtr<AudioPacketRef> pkt_ref;
bool release_audio_renderer_packet;
while (true) {
release_audio_renderer_packet = false;
// Try to grab the packet queue's front. If it has been flushed since the
// last time we grabbed it, reset our mixer's internal filter state.
bool was_flushed;
pkt_ref = packet_link->LockPendingQueueFront(&was_flushed);
if (was_flushed) {
info->mixer->Reset();
}
// If the queue is empty, then we are done.
if (!pkt_ref) {
break;
}
// If we have not set up for this renderer yet, do so. If the setup
// fails for any reason, stop processing packets for this renderer.
if (!setup_done) {
setup_done = (task_type == TaskType::Mix)
? SetupMix(audio_renderer, info)
: SetupTrim(audio_renderer, info);
if (!setup_done) {
// Clear our ramps, if we exit with error?
break;
}
}
// Now process the packet which is at the front of the renderer's queue.
// If the packet has been entirely consumed, pop it off the front and
// proceed to the next one. Otherwise, we are finished.
release_audio_renderer_packet =
(task_type == TaskType::Mix)
? ProcessMix(audio_renderer, info, pkt_ref)
: ProcessTrim(audio_renderer, info, pkt_ref);
// If we have mixed enough output frames, we are done with this mix,
// regardless of what we should now do with the renderer packet.
if ((task_type == TaskType::Mix) &&
(cur_mix_job_.frames_produced == cur_mix_job_.buf_frames)) {
break;
}
// If we still need more output, but could not complete this renderer
// packet (we're paused, or packet is in the future), then we are done.
if (!release_audio_renderer_packet) {
break;
}
// We did consume this entire renderer packet, and we should keep mixing.
pkt_ref.reset();
packet_link->UnlockPendingQueueFront(release_audio_renderer_packet);
}
// Unlock queue (completing packet if needed) and proceed to next renderer.
pkt_ref.reset();
packet_link->UnlockPendingQueueFront(release_audio_renderer_packet);
// Note: there is no point in doing this for Trim tasks, but it doesn't hurt
// anything, and its easier than adding another function to ForeachLink to
// run after each renderer is processed, just to set this flag.
cur_mix_job_.accumulate = true;
}
}
bool StandardOutputBase::SetupMix(
const fbl::RefPtr<AudioRendererImpl>& audio_renderer, Bookkeeping* info) {
// If we need to recompose our transformation from output frame space to input
// fractional frames, do so now.
FXL_DCHECK(info);
UpdateDestTrans(cur_mix_job_, info);
cur_mix_job_.frames_produced = 0;
return true;
}
bool StandardOutputBase::ProcessMix(
const fbl::RefPtr<AudioRendererImpl>& audio_renderer, Bookkeeping* info,
const fbl::RefPtr<AudioPacketRef>& packet) {
// Bookkeeping should contain: the rechannel matrix (eventually).
// Sanity check our parameters.
FXL_DCHECK(info);
FXL_DCHECK(packet);
// We had better have a valid job, or why are we here?
FXL_DCHECK(cur_mix_job_.buf_frames);
FXL_DCHECK(cur_mix_job_.frames_produced <= cur_mix_job_.buf_frames);
// We also must have selected a mixer, or we are in trouble.
FXL_DCHECK(info->mixer);
Mixer& mixer = *(info->mixer);
// If the renderer is currently paused, subject_delta (not just step_size) is
// zero. This packet may be relevant eventually, but currently it contributes
// nothing. Tell ForeachLink we are done, but hold the packet for now.
if (!info->dest_frames_to_frac_source_frames.subject_delta()) {
return false;
}
// Have we produced enough? If so, hold this packet and move to next renderer.
if (cur_mix_job_.frames_produced >= cur_mix_job_.buf_frames) {
return false;
}
uint32_t frames_left = cur_mix_job_.buf_frames - cur_mix_job_.frames_produced;
float* buf = mix_buf_.get() +
(cur_mix_job_.frames_produced * output_producer_->channels());
// Calculate this job's first and last sampling points, in source sub-frames.
int64_t first_sample_ftf = info->dest_frames_to_frac_source_frames(
cur_mix_job_.start_pts_of + cur_mix_job_.frames_produced);
// Without the "-1", this would be the first output frame of the NEXT job.
int64_t final_sample_ftf =
first_sample_ftf +
info->dest_frames_to_frac_source_frames.rate().Scale(frames_left - 1);
// If packet has no frames, there's no need to mix it; it may be skipped.
if (packet->end_pts() == packet->start_pts()) {
return true;
}
// Figure out the PTS of the final frame of audio in our input packet.
FXL_DCHECK((packet->end_pts() - packet->start_pts()) >= Mixer::FRAC_ONE);
int64_t final_pts = packet->end_pts() - Mixer::FRAC_ONE;
// If the PTS of the final frame of audio in our input is before the negative
// window edge of our filter centered at our first sampling point, then this
// packet is entirely in the past and may be skipped.
if (final_pts < (first_sample_ftf - mixer.neg_filter_width())) {
return true;
}
// If the PTS of the first frame of audio in our input is after the positive
// window edge of our filter centered at our final sampling point, then this
// packet is entirely in the future and should be held.
if (packet->start_pts() > (final_sample_ftf + mixer.pos_filter_width())) {
return false;
}
// Evidently this input packet intersects our mixer's filter. Compute where
// (in the output buffer) our first output sample will land, and where (in the
// input packet) we should start sampling the input.
int64_t input_offset_64 = first_sample_ftf - packet->start_pts();
int64_t output_offset_64 = 0;
int64_t first_sample_pos_window_edge =
first_sample_ftf + mixer.pos_filter_width();
// If the packet's first frame comes after the filter window's positive edge,
// then we should skip some output frames before starting to produce data.
if (packet->start_pts() > first_sample_pos_window_edge) {
const TimelineRate& dest_to_src =
info->dest_frames_to_frac_source_frames.rate();
output_offset_64 = dest_to_src.Inverse().Scale(
packet->start_pts() - first_sample_pos_window_edge + Mixer::FRAC_ONE -
1);
input_offset_64 += dest_to_src.Scale(output_offset_64);
}
FXL_DCHECK(output_offset_64 >= 0);
FXL_DCHECK(output_offset_64 < static_cast<int64_t>(frames_left));
FXL_DCHECK(input_offset_64 <= std::numeric_limits<int32_t>::max());
FXL_DCHECK(input_offset_64 >= std::numeric_limits<int32_t>::min());
auto output_offset = static_cast<uint32_t>(output_offset_64);
auto frac_input_offset = static_cast<int32_t>(input_offset_64);
// Looks like we are ready to go. Mix.
FXL_DCHECK(packet->frac_frame_len() <=
static_cast<uint32_t>(std::numeric_limits<int32_t>::max()));
bool consumed_source = false;
if (frac_input_offset < static_cast<int32_t>(packet->frac_frame_len())) {
// When calling Mix(), we communicate the resampling rate with three
// parameters. We augment step_size with rate_modulo and denominator
// arguments that capture the remaining rate component that cannot be
// expressed by a 19.13 fixed-point step_size. Note: step_size and
// frac_input_offset use the same format -- they have the same limitations
// in what they can and cannot communicate.
//
// For perfect position accuracy, just as we track incoming/outgoing
// fractional source offset, we also need to track the ongoing
// subframe_position_modulo. This is now added to Mix() and maintained
// across calls, but not initially set to any value other than zero.
// For now, we are deferring that work, tracking it with MTWN-128.
//
// Q: Why did we solve this issue for Rate but not for initial Position?
// A: We solved this issue for *rate* because its effect accumulates over
// time, causing clearly measurable distortion that becomes crippling with
// larger jobs. For *position*, there is no accumulated magnification over
// time -- in analyzing the distortion that this should cause, mix job
// size affects the distortion's frequency but not its amplitude. We expect
// the effects to be below audible thresholds. Until the effects are
// measurable and attributable to this jitter, we will defer this work.
//
// TODO(mpuryear): integrate bookkeeping into the Mixer itself (MTWN-129).
uint32_t prev_output_offset = output_offset;
// Check whether we are still ramping
bool ramping = info->gain.IsRamping();
if (ramping) {
info->gain.GetScaleArray(
info->scale_arr.get(),
std::min(frames_left - output_offset, Bookkeeping::kScaleArrLen),
cur_mix_job_.local_to_output->rate());
}
consumed_source =
info->mixer->Mix(buf, frames_left, &output_offset, packet->payload(),
packet->frac_frame_len(), &frac_input_offset,
cur_mix_job_.accumulate, info);
FXL_DCHECK(output_offset <= frames_left);
// If src is ramping, advance by delta of output_offset
if (ramping) {
info->gain.Advance(output_offset - prev_output_offset,
cur_mix_job_.local_to_output->rate());
}
}
if (consumed_source) {
FXL_DCHECK(frac_input_offset + info->mixer->pos_filter_width() >=
packet->frac_frame_len());
}
cur_mix_job_.frames_produced += output_offset;
FXL_DCHECK(cur_mix_job_.frames_produced <= cur_mix_job_.buf_frames);
return consumed_source;
}
bool StandardOutputBase::SetupTrim(
const fbl::RefPtr<AudioRendererImpl>& audio_renderer, Bookkeeping* info) {
// Compute the cutoff time used to decide whether to trim packets. ForeachLink
// has already updated our transformation, no need for us to do so here.
FXL_DCHECK(info);
int64_t local_now_ticks =
(fxl::TimePoint::Now() - fxl::TimePoint()).ToNanoseconds();
// RateControlBase guarantees that the transformation into the media timeline
// is never singular. If a forward transformation fails it must be because of
// overflow, which should be impossible unless user defined a playback rate
// where the ratio of media-ticks-to-local-ticks is greater than one.
trim_threshold_ = info->clock_mono_to_frac_source_frames(local_now_ticks);
return true;
}
bool StandardOutputBase::ProcessTrim(
const fbl::RefPtr<AudioRendererImpl>& audio_renderer, Bookkeeping* info,
const fbl::RefPtr<AudioPacketRef>& pkt_ref) {
FXL_DCHECK(pkt_ref);
// If the presentation end of this packet is in the future, stop trimming.
if (pkt_ref->end_pts() > trim_threshold_) {
return false;
}
return true;
}
void StandardOutputBase::UpdateSourceTrans(
const fbl::RefPtr<AudioRendererImpl>& audio_renderer, Bookkeeping* bk) {
FXL_DCHECK(audio_renderer != nullptr);
uint32_t gen = bk->source_trans_gen_id;
audio_renderer->SnapshotCurrentTimelineFunction(
Timeline::local_now(), &bk->clock_mono_to_frac_source_frames, &gen);
// If local->media transformation hasn't changed since last time, we're done.
if (bk->source_trans_gen_id == gen) {
return;
}
// Transformation has changed. Update gen; invalidate dest-to-src generation.
bk->source_trans_gen_id = gen;
bk->dest_trans_gen_id = kInvalidGenerationId;
}
void StandardOutputBase::UpdateDestTrans(const MixJob& job, Bookkeeping* bk) {
// We should only be here if we have a valid mix job. This means a job which
// supplies a valid transformation from local time to output frames.
FXL_DCHECK(job.local_to_output);
FXL_DCHECK(job.local_to_output_gen != kInvalidGenerationId);
// If generations match, don't re-compute -- just use what we have already.
if (bk->dest_trans_gen_id == job.local_to_output_gen) {
return;
}
// Assert we can map from local time to fractional renderer frames.
FXL_DCHECK(bk->source_trans_gen_id != kInvalidGenerationId);
// Combine the job-supplied local-to-output transformation, with the
// renderer-supplied mapping of local-to-input-subframe, to produce a
// transformation which maps from output frames to fractional input frames.
TimelineFunction& dest = bk->dest_frames_to_frac_source_frames;
dest = bk->clock_mono_to_frac_source_frames * job.local_to_output->Inverse();
// Finally, compute the step size in subframes. IOW, every time we move
// forward one output frame, how many input subframes should we consume. Don't
// bother doing the multiplications if already we know the numerator is zero.
FXL_DCHECK(dest.rate().reference_delta());
if (!dest.rate().subject_delta()) {
bk->step_size = 0;
bk->denominator = 0; // shouldn't also need to clear rate_mod and pos_mod
} else {
int64_t tmp_step_size = dest.rate().Scale(1);
FXL_DCHECK(tmp_step_size >= 0);
FXL_DCHECK(tmp_step_size <= std::numeric_limits<uint32_t>::max());
bk->step_size = static_cast<uint32_t>(tmp_step_size);
bk->denominator = bk->SnapshotDenominatorFromDestTrans();
bk->rate_modulo =
dest.rate().subject_delta() - (bk->denominator * bk->step_size);
}
// Done, update our dest_trans generation.
bk->dest_trans_gen_id = job.local_to_output_gen;
}
} // namespace media::audio