| Index: services/media/audio/platform/generic/standard_output_base.cc
|
| diff --git a/services/media/audio/platform/generic/standard_output_base.cc b/services/media/audio/platform/generic/standard_output_base.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..eb7c0f3447e09add40d54a0432b8ec908a6fa3be
|
| --- /dev/null
|
| +++ b/services/media/audio/platform/generic/standard_output_base.cc
|
| @@ -0,0 +1,491 @@
|
| +// Copyright 2015 The Chromium 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 <limits>
|
| +
|
| +#include "base/logging.h"
|
| +#include "services/media/audio/audio_track_impl.h"
|
| +#include "services/media/audio/audio_track_to_output_link.h"
|
| +#include "services/media/audio/platform/generic/mixer.h"
|
| +#include "services/media/audio/platform/generic/standard_output_base.h"
|
| +
|
| +namespace mojo {
|
| +namespace media {
|
| +namespace audio {
|
| +
|
| +static constexpr LocalDuration MAX_TRIM_PERIOD = local_time::from_msec(10);
|
| +constexpr uint32_t StandardOutputBase::MixJob::INVALID_GENERATION;
|
| +
|
| +StandardOutputBase::TrackBookkeeping::TrackBookkeeping() {}
|
| +StandardOutputBase::TrackBookkeeping::~TrackBookkeeping() {}
|
| +
|
| +StandardOutputBase::StandardOutputBase(AudioOutputManager* manager)
|
| + : AudioOutput(manager) {
|
| + setup_mix_ =
|
| + [this] (const AudioTrackImplPtr& track, TrackBookkeeping* info) -> bool {
|
| + return SetupMix(track, info);
|
| + };
|
| +
|
| + process_mix_ =
|
| + [this] (const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info,
|
| + const AudioPipe::AudioPacketRefPtr& pkt_ref) -> bool {
|
| + return ProcessMix(track, info, pkt_ref);
|
| + };
|
| +
|
| + setup_trim_ =
|
| + [this] (const AudioTrackImplPtr& track, TrackBookkeeping* info) -> bool {
|
| + return SetupTrim(track, info);
|
| + };
|
| +
|
| + process_trim_ =
|
| + [this] (const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info,
|
| + const AudioPipe::AudioPacketRefPtr& pkt_ref) -> bool {
|
| + return ProcessTrim(track, info, pkt_ref);
|
| + };
|
| +
|
| + next_sched_time_ = LocalClock::now();
|
| + next_sched_time_known_ = true;
|
| +}
|
| +
|
| +StandardOutputBase::~StandardOutputBase() {}
|
| +
|
| +void StandardOutputBase::Process() {
|
| + bool mixed = false;
|
| + LocalTime now = LocalClock::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.
|
| + DCHECK(next_sched_time_known_);
|
| + if (now >= next_sched_time_) {
|
| + // Clear the flag, if the implementation does not set this flag by calling
|
| + // SetNextSchedTime during the cycle, we consider it to be 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;
|
| + }
|
| +
|
| + ForeachTrack(setup_mix_, process_mix_);
|
| + mixed = true;
|
| + } while (FinishMixJob(cur_mix_job_));
|
| + }
|
| +
|
| + if (!next_sched_time_known_) {
|
| + // TODO(johngro): log this as an error.
|
| + ShutdownSelf();
|
| + return;
|
| + }
|
| +
|
| + // If we mixed nothing this time, make sure that we trim all of our track
|
| + // 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) {
|
| + ForeachTrack(setup_trim_, process_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.
|
| + LocalTime max_sched_time = now + MAX_TRIM_PERIOD;
|
| + ScheduleCallback((next_sched_time_ > max_sched_time)
|
| + ? max_sched_time
|
| + : next_sched_time_);
|
| +}
|
| +
|
| +MediaResult StandardOutputBase::InitializeLink(
|
| + const AudioTrackToOutputLinkPtr& link) {
|
| + TrackBookkeeping* bk = AllocBookkeeping();
|
| + AudioTrackToOutputLink::BookkeepingPtr ref(bk);
|
| +
|
| + // We should never fail to allocate our bookkeeping. The only way this can
|
| + // happen is if we have a badly behaved implementation.
|
| + if (!bk) { return MediaResult::INTERNAL_ERROR; }
|
| +
|
| + // We cannot proceed if our track has somehow managed to go away already.
|
| + AudioTrackImplPtr track = link->GetTrack();
|
| + if (!track) { return MediaResult::INVALID_ARGUMENT; }
|
| +
|
| + // Pick a mixer based on the input and output formats.
|
| + bk->mixer = Mixer::Select(track->Format(), output_format_);
|
| + if (bk->mixer == nullptr) { return MediaResult::UNSUPPORTED_CONFIG; }
|
| +
|
| + // Looks like things went well. Stash a reference to our bookkeeping and get
|
| + // out.
|
| + link->output_bookkeeping() = std::move(ref);
|
| + return MediaResult::OK;
|
| +}
|
| +
|
| +StandardOutputBase::TrackBookkeeping* StandardOutputBase::AllocBookkeeping() {
|
| + return new TrackBookkeeping();
|
| +}
|
| +
|
| +void StandardOutputBase::ForeachTrack(const TrackSetupTask& setup,
|
| + const TrackProcessTask& process) {
|
| + for (auto iter = links_.begin(); iter != links_.end(); ) {
|
| + if (shutting_down()) { return; }
|
| +
|
| + // Is the track still around? If so, process it. Otherwise, remove the
|
| + // track entry and move on.
|
| + const AudioTrackToOutputLinkPtr& link = *iter;
|
| + AudioTrackImplPtr track(link->GetTrack());
|
| +
|
| + auto tmp_iter = iter++;
|
| + if (!track) {
|
| + links_.erase(tmp_iter);
|
| + continue;
|
| + }
|
| +
|
| + // It would be nice to be able to use a dynamic cast for this, but currently
|
| + // we are building with no-rtti
|
| + TrackBookkeeping* info =
|
| + static_cast<TrackBookkeeping*>(link->output_bookkeeping().get());
|
| + DCHECK(info);
|
| +
|
| + // Make sure that the mapping between the track's frame time domain and
|
| + // local time is up to date.
|
| + info->UpdateTrackTrans(track);
|
| +
|
| + bool setup_done = false;
|
| + AudioPipe::AudioPacketRefPtr pkt_ref;
|
| + while (true) {
|
| + // Try to grab the front of the packet queue. If it has been flushed
|
| + // since the last time we grabbed it, be sure to reset our mixer's
|
| + // internal filter state.
|
| + bool was_flushed;
|
| + pkt_ref = 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 track yet, do so. If the setup fails
|
| + // for any reason, stop processing packets for this track.
|
| + if (!setup_done) {
|
| + setup_done = setup(track, info);
|
| + if (!setup_done) { break; }
|
| + }
|
| +
|
| + // Now process the packet which is at the front of the track's queue. If
|
| + // the packet has been entirely consumed, pop it off the front and proceed
|
| + // to the next one. Otherwise, we are finished.
|
| + if (!process(track, info, pkt_ref)) { break; }
|
| + link->UnlockPendingQueueFront(&pkt_ref, true);
|
| + }
|
| +
|
| + // Unlock the queue and proceed to the next track.
|
| + link->UnlockPendingQueueFront(&pkt_ref, false);
|
| +
|
| + // Note: there is no point in doing this for the trim task, but it dosn't
|
| + // hurt anything, and its easier then introducing another function to the
|
| + // ForeachTrack arguments to run after each track is processed just for the
|
| + // purpose of setting this flag.
|
| + cur_mix_job_.accumulate = true;
|
| + }
|
| +}
|
| +
|
| +bool StandardOutputBase::SetupMix(const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info) {
|
| + // If we need to recompose our transformation from output frame space to input
|
| + // fractional frames, do so now.
|
| + DCHECK(info);
|
| + info->UpdateOutputTrans(cur_mix_job_);
|
| + cur_mix_job_.frames_produced = 0;
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool StandardOutputBase::ProcessMix(
|
| + const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info,
|
| + const AudioPipe::AudioPacketRefPtr& pkt_ref) {
|
| + // Sanity check our parameters.
|
| + DCHECK(info);
|
| + DCHECK(pkt_ref);
|
| +
|
| + // We had better have a valid job, or why are we here?
|
| + DCHECK(cur_mix_job_.buf);
|
| + DCHECK(cur_mix_job_.buf_frames);
|
| + DCHECK(cur_mix_job_.frames_produced <= cur_mix_job_.buf_frames);
|
| +
|
| + // Have we produced all that we are supposed to? If so, hold the current
|
| + // packet and move on to the next track.
|
| + 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;
|
| + void* buf = static_cast<uint8_t*>(cur_mix_job_.buf)
|
| + + (cur_mix_job_.frames_produced * output_bytes_per_frame_);
|
| +
|
| + // Figure out where this job starts, expressed in fractional input frames.
|
| + int64_t start_pts_ftf;
|
| + bool good = info->out_frames_to_track_frames.DoForwardTransform(
|
| + cur_mix_job_.start_pts_of + cur_mix_job_.frames_produced,
|
| + &start_pts_ftf);
|
| + DCHECK(good);
|
| +
|
| + // If the start of this mix job is past the end of this packet presentation,
|
| + // do no mixing. Let the ForeachTrack loop know that we are done with the
|
| + // packet and it can be released.
|
| + if (start_pts_ftf >= pkt_ref->end_pts()) {
|
| + return true;
|
| + }
|
| +
|
| + // If this track is currently paused (or being sampled extremely slowly), our
|
| + // step size will be zero. We know that this packet will be relevant at some
|
| + // point in the future, but right now it contributes nothing. Tell the
|
| + // ForeachTrack loop that we are done and to hold onto this packet for now.
|
| + if (!info->step_size) {
|
| + return false;
|
| + }
|
| +
|
| + // Figure out how many output samples into the current job this packet starts.
|
| + int64_t delta;
|
| + int64_t output_offset_64;
|
| + if (pkt_ref->start_pts() > start_pts_ftf) {
|
| + delta = pkt_ref->start_pts() - start_pts_ftf;
|
| + output_offset_64 = delta + info->step_size - 1;
|
| + output_offset_64 /= info->step_size;
|
| + } else {
|
| + output_offset_64 = 0;
|
| + }
|
| + DCHECK_GE(output_offset_64, 0);
|
| +
|
| + // If this packet starts after the end of this job (entirely in the future),
|
| + // then we are done for now.
|
| + if (output_offset_64 >= frames_left) {
|
| + return false;
|
| + }
|
| +
|
| + // Figure out the offset (in fractional frames) into this packet where we want
|
| + // to start sampling.
|
| + int64_t input_offset_64;
|
| + if (output_offset_64) {
|
| + input_offset_64 = output_offset_64 * info->step_size;
|
| + input_offset_64 -= delta;
|
| + DCHECK_LT(input_offset_64, info->step_size);
|
| + } else {
|
| + input_offset_64 = start_pts_ftf - pkt_ref->start_pts();
|
| + }
|
| + DCHECK_GE(input_offset_64, 0);
|
| + DCHECK_LE(input_offset_64, std::numeric_limits<int32_t>::max());
|
| + DCHECK_LT(input_offset_64, pkt_ref->end_pts() - pkt_ref->start_pts());
|
| +
|
| + uint32_t input_offset = static_cast<uint32_t>(input_offset_64);
|
| + uint32_t output_offset = static_cast<uint32_t>(output_offset_64);
|
| + const auto& regions = pkt_ref->regions();
|
| + DCHECK(info->mixer != nullptr);
|
| +
|
| + for (size_t i = 0;
|
| + (i < regions.size()) && (output_offset < frames_left);
|
| + ++i) {
|
| + const auto& region = regions[i];
|
| +
|
| + if (input_offset >= region.frac_frame_len) {
|
| + input_offset -= region.frac_frame_len;
|
| + continue;
|
| + }
|
| +
|
| + bool consumed_source = info->mixer->Mix(buf,
|
| + frames_left,
|
| + &output_offset,
|
| + region.base,
|
| + region.frac_frame_len,
|
| + &input_offset,
|
| + info->step_size,
|
| + cur_mix_job_.accumulate);
|
| + DCHECK_LE(output_offset, frames_left);
|
| +
|
| + if (!consumed_source) {
|
| + // Looks like we didn't consume all of this region. Assert that we have
|
| + // produced all of our frames and we are done.
|
| + DCHECK(output_offset == frames_left);
|
| + return false;
|
| + }
|
| +
|
| + input_offset -= region.frac_frame_len;
|
| + }
|
| +
|
| + cur_mix_job_.frames_produced += output_offset;
|
| + DCHECK(cur_mix_job_.frames_produced <= cur_mix_job_.buf_frames);
|
| + return true;
|
| +}
|
| +
|
| +bool StandardOutputBase::SetupTrim(const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info) {
|
| + // Compute the cutoff time we will use to decide wether or not to trim
|
| + // packets. ForeachTracks has already updated our transformation, no need
|
| + // for us to do so here.
|
| + DCHECK(info);
|
| +
|
| + int64_t local_now_ticks = LocalClock::now().time_since_epoch().count();
|
| +
|
| + // The behavior of the RateControlBase implementation guarantees that the
|
| + // transformation into the media timeline is never singular. If the
|
| + // forward transformation fails it can only be because of an overflow,
|
| + // which should be impossible unless the user has defined a playback rate
|
| + // where the ratio between media time ticks and local time ticks is
|
| + // greater than one.
|
| + //
|
| + // IOW - this should never happen. If it does, we just stop processing
|
| + // payloads.
|
| + //
|
| + // TODO(johngro): Log an error? Communicate this to the user somehow?
|
| + if (!info->lt_to_track_frames.DoForwardTransform(local_now_ticks,
|
| + &trim_threshold_)) {
|
| + return false;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool StandardOutputBase::ProcessTrim(
|
| + const AudioTrackImplPtr& track,
|
| + TrackBookkeeping* info,
|
| + const AudioPipe::AudioPacketRefPtr& pkt_ref) {
|
| + 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::TrackBookkeeping::UpdateTrackTrans(
|
| + const AudioTrackImplPtr& track) {
|
| + LinearTransform tmp;
|
| + uint32_t gen;
|
| +
|
| + DCHECK(track);
|
| + track->SnapshotRateTrans(&tmp, &gen);
|
| +
|
| + // If the local time -> media time transformation has not changed since the
|
| + // last time we examines it, just get out now.
|
| + if (lt_to_track_frames_gen == gen) { return; }
|
| +
|
| + // The transformation has changed, re-compute the local time -> track frame
|
| + // transformation.
|
| + LinearTransform scale(0, track->FractionalFrameToMediaTimeRatio(), 0);
|
| + bool good;
|
| +
|
| + lt_to_track_frames.a_zero = tmp.a_zero;
|
| + good = scale.DoReverseTransform(tmp.b_zero, <_to_track_frames.b_zero);
|
| + DCHECK(good);
|
| + good = LinearTransform::Ratio::Compose(scale.scale,
|
| + tmp.scale,
|
| + <_to_track_frames.scale);
|
| + DCHECK(good);
|
| +
|
| + // Update the generation, and invalidate the output to track generation.
|
| + lt_to_track_frames_gen = gen;
|
| + out_frames_to_track_frames_gen = MixJob::INVALID_GENERATION;
|
| +}
|
| +
|
| +void StandardOutputBase::TrackBookkeeping::UpdateOutputTrans(
|
| + const MixJob& job) {
|
| + // We should not be here unless we have a valid mix job. From our point of
|
| + // view, this means that we have a job which supplies a valid transformation
|
| + // from local time to output frames.
|
| + DCHECK(job.local_to_output);
|
| + DCHECK(job.local_to_output_gen != MixJob::INVALID_GENERATION);
|
| +
|
| + // If our generations match, we don't need to re-compute anything. Just use
|
| + // what we have already.
|
| + if (out_frames_to_track_frames_gen == job.local_to_output_gen) { return; }
|
| +
|
| + // Assert that we have a good mapping from local time to fractional track
|
| + // frames.
|
| + //
|
| + // TODO(johngro): Don't assume that 0 means invalid. Make it a proper
|
| + // constant defined somewhere.
|
| + DCHECK(lt_to_track_frames_gen);
|
| +
|
| + // Compose the job supplied transformation from local to output with the
|
| + // track supplied mapping from local to fraction input frames to produce a
|
| + // transformation which maps from output frames to fractional input frames.
|
| + //
|
| + // TODO(johngro): Make this composition operation part of the LinearTransform
|
| + // class instead of doing it by hand here. Its a more complicated task that
|
| + // one might initially think, because of the need to deal with the
|
| + // intermediate offset term, and distributing it to either side of the end of
|
| + // the transformation with a minimum amt of loss, while avoiding overflow.
|
| + //
|
| + // For now, we punt, do it by hand and just assume that everything went well.
|
| + LinearTransform& dst = out_frames_to_track_frames;
|
| +
|
| + // Distribute the intermediate offset entirely to the fractional frame domain
|
| + // for now. We can do better by extracting portions of the intermedate
|
| + // offset that can be scaled by the ratios on either side of with without
|
| + // loss, but for now this should be close enough.
|
| + int64_t intermediate = job.local_to_output->a_zero
|
| + - lt_to_track_frames.a_zero;
|
| + int64_t track_frame_offset;
|
| +
|
| + // TODO(johngro): add routines to LinearTransform::Ratio which allow us to
|
| + // scale using just a ratio without needing to create a linear transform with
|
| + // empty offsets.
|
| + LinearTransform tmp(0, lt_to_track_frames.scale, 0);
|
| + bool good = tmp.DoForwardTransform(intermediate, &track_frame_offset);
|
| + DCHECK(good);
|
| +
|
| + dst.a_zero = job.local_to_output->b_zero;
|
| + dst.b_zero = lt_to_track_frames.b_zero + track_frame_offset;
|
| +
|
| + // TODO(johngro): Add options to allow us to invert one or both of the ratios
|
| + // during composition instead of needing to make a temporary ratio to
|
| + // acomplish the task.
|
| + LinearTransform::Ratio tmp_ratio(job.local_to_output->scale.denominator,
|
| + job.local_to_output->scale.numerator);
|
| + good = LinearTransform::Ratio::Compose(tmp_ratio,
|
| + lt_to_track_frames.scale,
|
| + &dst.scale);;
|
| + DCHECK(good);
|
| +
|
| + // Finally, compute the step size in fractional frames. IOW, every time we
|
| + // move forward one output frame, how many fractional frames of input do we
|
| + // consume. Don't bother doing the multiplication if we already know that the
|
| + // numerator is zero.
|
| + //
|
| + // TODO(johngro): same complaint as before... Do this without a temp. The
|
| + // special casing should be handled in the routine added to
|
| + // LinearTransform::Ratio.
|
| + DCHECK(dst.scale.denominator);
|
| + if (!dst.scale.numerator) {
|
| + step_size = 0;
|
| + } else {
|
| + LinearTransform tmp(0, dst.scale, 0);
|
| + int64_t tmp_step_size;
|
| +
|
| + good = tmp.DoForwardTransform(1, &tmp_step_size);
|
| +
|
| + DCHECK(good);
|
| + DCHECK_GE(tmp_step_size, 0);
|
| + DCHECK_LE(tmp_step_size, std::numeric_limits<uint32_t>::max());
|
| +
|
| + step_size = static_cast<uint32_t>(tmp_step_size);
|
| + }
|
| +
|
| + // Done, update our generation.
|
| + out_frames_to_track_frames_gen = job.local_to_output_gen;
|
| +}
|
| +
|
| +} // namespace audio
|
| +} // namespace media
|
| +} // namespace mojo
|
|
|