Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(966)

Unified Diff: services/media/audio/platform/linux/alsa_output.cc

Issue 1419593007: Add an ALSA output to the motown audio server. (Closed) Base URL: https://github.com/domokit/mojo.git@change5
Patch Set: Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: services/media/audio/platform/linux/alsa_output.cc
diff --git a/services/media/audio/platform/linux/alsa_output.cc b/services/media/audio/platform/linux/alsa_output.cc
new file mode 100644
index 0000000000000000000000000000000000000000..addfc3203b55adb74d59500d4beb075434fb20d7
--- /dev/null
+++ b/services/media/audio/platform/linux/alsa_output.cc
@@ -0,0 +1,340 @@
+// 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 <set>
+
+#include "mojo/services/media/common/cpp/local_time.h"
+#include "services/media/audio/platform/linux/alsa_output.h"
+
+namespace mojo {
+namespace media {
+namespace audio {
+
+static constexpr LocalDuration TGT_LATENCY = local_time::from_msec(35);
jeffbrown 2015/11/04 20:33:42 TGT is pretty opaque as acronyms go, prefer TARGET
johngro 2015/11/06 20:22:07 Done.
+static constexpr LocalDuration LOW_BUF_THRESH = local_time::from_msec(30);
+static constexpr LocalDuration ERROR_RECOVERY_TIME = local_time::from_msec(300);
+static constexpr LocalDuration WAIT_FOR_ALSA_DELAY = local_time::from_usec(500);
+static const std::set<uint8_t> SUPPORTED_CHANNEL_COUNTS({ 1, 2 });
+static const std::set<uint32_t> SUPPORTED_SAMPLE_RATES({
+ 48000, 32000, 24000, 16000, 8000, 4000,
+ 44100, 22050, 11025,
+});
+static const std::set<int> RECOVERABLE_ALSA_ERRORS({
+ -EINTR, -EPIPE, -ESTRPIPE,
jeffbrown 2015/11/04 20:33:42 Kind of odd to put these in a set<> given that it'
johngro 2015/11/06 20:22:07 It just makes the code a bit more readable and mai
+});
+
+AlsaOutput::AlsaOutput(AudioOutputManager* manager)
+ : StandardOutputBase(manager) { }
+
+AlsaOutput::~AlsaOutput() {
+ // We should have been cleaned up already, but in release builds, call cleanup
+ // anyway, just in case something got missed.
jeffbrown 2015/11/04 20:33:42 Why not just CHECK in release mode? Yeah we'll cr
johngro 2015/11/06 20:22:07 A call to cleanup might (someday) do more than jus
+ DCHECK(!alsa_device_);
+ Cleanup();
+}
+
+AudioOutputPtr AlsaOutput::New(AudioOutputManager* manager) {
+ return AudioOutputPtr(new AlsaOutput(manager));
+}
+
+MediaResult AlsaOutput::Configure(LpcmMediaTypeDetailsPtr config) {
+ if (!config) { return MediaResult::INVALID_ARGUMENT; }
+ if (output_format_) { return MediaResult::BAD_STATE; }
+
+ uint32_t bytes_per_sample;
+ switch (config->sample_format) {
+ case LpcmSampleFormat::UNSIGNED_8:
+ alsa_format_ = SND_PCM_FORMAT_U8;
+ silence_byte_ = 0x80;
+ bytes_per_sample = 1;
+ break;
+
+ case LpcmSampleFormat::SIGNED_16:
+ alsa_format_ = SND_PCM_FORMAT_S16;
+ silence_byte_ = 0x00;
+ bytes_per_sample = 2;
+ break;
+
+ case LpcmSampleFormat::SIGNED_24_IN_32:
+ default:
+ return MediaResult::UNSUPPORTED_CONFIG;
+ }
+
+ if (SUPPORTED_SAMPLE_RATES.find(config->frames_per_second) ==
+ SUPPORTED_SAMPLE_RATES.end()) {
+ return MediaResult::UNSUPPORTED_CONFIG;
+ }
+
+ if (SUPPORTED_CHANNEL_COUNTS.find(config->samples_per_frame) ==
+ SUPPORTED_CHANNEL_COUNTS.end()) {
+ return MediaResult::UNSUPPORTED_CONFIG;
+ }
+
+ // Compute the ratio between frames and local time ticks.
+ LinearTransform::Ratio sec_per_tick(LocalDuration::period::num,
+ LocalDuration::period::den);
+ LinearTransform::Ratio frames_per_sec(config->frames_per_second, 1);
+ bool is_precise = LinearTransform::Ratio::Compose(frames_per_sec,
+ sec_per_tick,
+ &frames_per_tick_);
+ DCHECK(is_precise);
+
+ // Figure out how many bytes there are per frame.
+ output_bytes_per_frame_ = bytes_per_sample * config->samples_per_frame;
+
+ // Success
+ output_format_ = config.Pass();
+ return MediaResult::OK;
+}
+
+MediaResult AlsaOutput::Init() {
+ if (!output_format_) { return MediaResult::BAD_STATE; }
+ if (alsa_device_) { return MediaResult::BAD_STATE; }
+
+ snd_pcm_sframes_t res;
+ res = snd_pcm_open(&alsa_device_,
+ "default",
+ SND_PCM_STREAM_PLAYBACK,
+ SND_PCM_NONBLOCK);
+ if (res != 0) {
+ // TODO(johngro): log something here?
jeffbrown 2015/11/04 20:33:42 Yeah. LOG(ERROR) << "You're hosed." ;)
johngro 2015/11/06 20:22:07 Done.
+ DCHECK(!alsa_device_);
jeffbrown 2015/11/04 20:33:42 This DCHECK seems unnecessary given your test at t
johngro 2015/11/06 20:22:06 Assuming that ALSA never has any bugs, but yes...
+ return MediaResult::INTERNAL_ERROR;
+ }
+
+ res = snd_pcm_set_params(alsa_device_,
+ alsa_format_,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ output_format_->samples_per_frame,
+ output_format_->frames_per_second,
+ 1, // allow ALSA resample
jeffbrown 2015/11/04 20:33:42 Hmm. Guess we'll disable this in the future.
johngro 2015/11/06 20:22:07 how about right now? Not sure how this got set TB
+ local_time::to_usec<unsigned int>(TGT_LATENCY));
+ if (res) {
+ // TODO(johngro): log something here?
jeffbrown 2015/11/04 20:33:42 Please do. ALSA is such a pain to debug when brin
johngro 2015/11/06 20:22:07 Done.
+ Cleanup();
+ return MediaResult::INTERNAL_ERROR;
+ }
+
+ // Figure out how big our mixing buffer needs to be, then allocate it.
+ res = snd_pcm_avail_update(alsa_device_);
+ if (res <= 0) {
+ LOG(ERROR) << "[" << this << "] : "
+ << "Fatal error (" << res
+ << ") attempting to determine ALSA buffer size.";
+ Cleanup();
+ return MediaResult::INTERNAL_ERROR;
+ }
+
+ mix_buf_frames_ = res;
+ mix_buf_bytes_ = res * output_bytes_per_frame_;
+ mix_buf_.reset(new uint8_t[mix_buf_bytes_]);
+
+ return MediaResult::OK;
+}
+
+void AlsaOutput::Cleanup() {
+ if (alsa_device_) {
+ snd_pcm_close(alsa_device_);
+ alsa_device_ = nullptr;
+ }
+
+ mix_buf_ = nullptr;
+ mix_buf_bytes_ = 0;
+ mix_buf_frames_ = 0;
+}
+
+bool AlsaOutput::StartMixJob(MixJob* job, const LocalTime& process_start) {
+ DCHECK(job);
+
+ // Are we not primed? If so, fill a mix buffer with silence and send it to
+ // the alsa device. Schedule a callback for a short time in the future so
+ // ALSA has a chance to start the output and we can take our best guess of the
+ // function which maps output frames to local time.
+ if (!primed_) {
+ HandleAsUnderflow();
+ return false;
+ }
+
+ // Figure out how many frames of audio we need to produce in order to top off
+ // the buffer. If we are primed, but do not know the transformation between
+ // audio frames and local time ticks, do our best to figure it out in the
+ // process.
+ snd_pcm_sframes_t avail;
+ if (!local_to_output_known_) {
+ snd_pcm_sframes_t delay;
+
+ int res = snd_pcm_avail_delay(alsa_device_, &avail, &delay);
+ LocalTime now = LocalClock::now();
+
+ if (res < 0) {
+ HandleAlsaError(res);
+ return false;
+ }
+
+ DCHECK_GE(delay, 0);
+ int64_t now_ticks = now.time_since_epoch().count();
jeffbrown 2015/11/04 20:33:42 Looking at this conversion to int64_t, I wonder wh
johngro 2015/11/06 20:22:07 yeah, an adapter might be a good idea. If we end
+ local_to_output_ = LinearTransform(now_ticks, frames_per_tick_, -delay);
+ local_to_output_known_ = true;
+ frames_sent_ = 0;
+ while (++local_to_output_gen_ == MixJob::INVALID_GENERATION) {}
jeffbrown 2015/11/04 20:33:42 A little weird but ok.
johngro 2015/11/06 20:22:07 Acknowledged. I could wrap this in a method if yo
+ } else {
+ avail = snd_pcm_avail_update(alsa_device_);
+ if (avail < 0) {
+ HandleAlsaError(avail);
+ return false;
+ }
+ }
+
+ // Compute the time that we think we will completely underflow, then back off
+ // from that by the low buffer threshold and use that to determine when we
+ // should mix again.
+ int64_t playout_time_ticks;
+ bool trans_ok = local_to_output_.DoReverseTransform(frames_sent_,
jeffbrown 2015/11/04 20:33:42 Am I correct in thinking that the reverse transfor
johngro 2015/11/06 20:22:07 No, outputs cannot be paused. They are either run
+ &playout_time_ticks);
+ DCHECK(trans_ok);
jeffbrown 2015/11/04 20:33:43 Ahh, here's our check for singularity. Odd though
johngro 2015/11/06 20:22:07 This also checks for overflow, not just singularit
+ LocalTime playout_time = LocalTime(LocalDuration(playout_time_ticks));
+ LocalTime low_buf_time = playout_time - LOW_BUF_THRESH;
+
+ if (process_start >= low_buf_time) {
+ // Because of the way that ALSA consumes data and updates its internal
+ // bookkeeping, it is possible that we are past our low buffer threshold,
+ // but ALSA still thinks that there is no room to write new frames. If this
+ // is the case, just try again a short amt of time in the future.
jeffbrown 2015/11/04 20:33:42 amt -> amount
johngro 2015/11/06 20:22:07 Done.
+ DCHECK_GE(avail, 0);
+ if (!avail) {
+ SetNextSchedDelay(WAIT_FOR_ALSA_DELAY);
+ return false;
+ }
+
+ // Limit the amt that we queue to be no more than what ALSA will currently
+ // accept, or what it currently will take to fill us to our target latency.
+ //
+ // The playout target had better be ahead of the playout time, or we are
+ // almost certainly going to underflow. If this happens, for whatever
+ // reason, just try to send a full buffer and deal with the underflow when
+ // ALSA notices it.
+ int64_t fill_amt;
+ LocalTime playout_target = LocalClock::now() + TGT_LATENCY;
+ if (playout_target > playout_time) {
+ fill_amt = (playout_target - playout_time).count();
+ } else {
+ fill_amt = TGT_LATENCY.count();
+ }
+
+ DCHECK_GE(fill_amt, 0);
+ DCHECK_LE(fill_amt, std::numeric_limits<int32_t>::max());
jeffbrown 2015/11/04 20:33:42 Are we guaranteed that numerator <= denominator?
johngro 2015/11/06 20:22:07 We should be. If N > D, it implies that our audio
+ fill_amt *= frames_per_tick_.numerator;
jeffbrown 2015/11/04 20:33:43 I might recommend that we create a for rounded sca
johngro 2015/11/06 20:22:07 good idea. See above; its the type of thing which
+ fill_amt += frames_per_tick_.denominator - 1;
+ fill_amt /= frames_per_tick_.denominator;
+
+ job->buf_frames = (avail < fill_amt) ? avail : fill_amt;
+ if (job->buf_frames > mix_buf_frames_) {
+ job->buf_frames = mix_buf_frames_;
+ }
+
+ job->buf = mix_buf_.get();
+ job->start_pts_of = frames_sent_;
+ job->local_to_output = &local_to_output_;
+ job->local_to_output_gen = local_to_output_gen_;
+
+ // TODO(johngro): optimize this if we can. The first buffer we mix can just
+ // put its samples directly into the output buffer, and does not need to
+ // accumulate and clip. In theory, we only need to put silence in the
+ // places where our outputs are not going to already overwrite.
+ ::memset(job->buf,
+ silence_byte_,
jeffbrown 2015/11/04 20:33:43 Technically silence might not always be a byte. A
johngro 2015/11/06 20:22:07 Acknowledged. This only works because we (current
+ job->buf_frames * output_bytes_per_frame_);
+ return true;
+ }
+
+ // Wait until its time to mix some more data.
+ SetNextSchedTime(low_buf_time);
+ return false;
+}
+
+bool AlsaOutput::FinishMixJob(const MixJob& job) {
+ DCHECK(job.buf == mix_buf_.get());
+ DCHECK(job.buf_frames);
+
+ // We should always be able to write all of the data that we mixed.
+ snd_pcm_sframes_t res;
+ res = snd_pcm_writei(alsa_device_, job.buf, job.buf_frames);
+ if (res != job.buf_frames) {
+ HandleAlsaError(res);
+ return false;
+ }
+
+ frames_sent_ += res;
+ return true;
+}
+
+void AlsaOutput::HandleAsUnderflow() {
+ snd_pcm_sframes_t res;
+
+ // If we were already primed, then this is a legitimate underflow, not the
+ // startup case or recovery from some other error.
+ if (primed_) {
+ // TODO(johngro): come up with a way to properly throttle this. Also, add a
+ // friendly name to the output so the log helps to identify which output
+ // underflowed.
+ LOG(WARNING) << "[" << this << "] : underflow";
+ res = snd_pcm_recover(alsa_device_, -EPIPE, true);
+ if (res < 0) {
+ HandleAsError(res);
+ return;
+ }
+ }
+
+ // TODO(johngro): We don't actually have to fill up the entire lead time with
+ // silence. When we have better control of our thread priorities, prime this
+ // with the minimimum amt we can get away with and still be able to start
+ // mixing without underflowing.
+ DCHECK(mix_buf_);
+ ::memset(mix_buf_.get(), silence_byte_, mix_buf_bytes_);
jeffbrown 2015/11/04 20:33:42 This has shown up twice now. Consider making a fu
johngro 2015/11/06 20:22:07 Sure; this (moving to frames only) means performin
+ res = snd_pcm_writei(alsa_device_, mix_buf_.get(), mix_buf_frames_);
+
+ if (res < 0) {
+ HandleAsError(res);
+ return;
+ }
+
+ primed_ = true;
+ local_to_output_known_ = false;
+ SetNextSchedDelay(local_time::from_msec(1));
+}
+
+void AlsaOutput::HandleAsError(snd_pcm_sframes_t code) {
jeffbrown 2015/11/04 20:33:42 The name of this function is a little too similar
johngro 2015/11/06 20:22:07 Permission to say HandleALSAError instead of Handl
+ // TODO(johngro): Throttle this somehow.
+ LOG(WARNING) << "[" << this << "] : Attempting to recover from ALSA error "
+ << code;
+
+ if (RECOVERABLE_ALSA_ERRORS.find(code) != RECOVERABLE_ALSA_ERRORS.end()) {
jeffbrown 2015/11/04 20:33:42 If we got EINTR then the correct response was to r
johngro 2015/11/06 20:22:07 I'm not sure I understand your request here. The
+ code = snd_pcm_recover(alsa_device_, code, true);
+ if (!code) {
+ primed_ = false;
+ local_to_output_known_ = false;
+ SetNextSchedDelay(ERROR_RECOVERY_TIME);
+ }
+ }
+
+ LOG(ERROR) << "[" << this << "] : Fatal ALSA error "
+ << code << ". Shutting down";
+ ShutdownSelf();
+}
+
+void AlsaOutput::HandleAlsaError(snd_pcm_sframes_t code) {
+ // ALSA signals an underflow by returning -EPIPE from jobs. If the error code
+ // is -EPIPE, treat this as an underflow and attempt to reprime the pipeline.
+ if (code == -EPIPE) {
+ HandleAsUnderflow();
+ } else {
+ HandleAsError(code);
+ }
+}
+
+} // namespace audio
+} // namespace media
+} // namespace mojo
+

Powered by Google App Engine
This is Rietveld 408576698