summaryrefslogtreecommitdiffstats
path: root/media/audio/linux/alsa_output.cc
diff options
context:
space:
mode:
Diffstat (limited to 'media/audio/linux/alsa_output.cc')
-rw-r--r--media/audio/linux/alsa_output.cc521
1 files changed, 521 insertions, 0 deletions
diff --git a/media/audio/linux/alsa_output.cc b/media/audio/linux/alsa_output.cc
new file mode 100644
index 0000000..0dc9ca9
--- /dev/null
+++ b/media/audio/linux/alsa_output.cc
@@ -0,0 +1,521 @@
+// Copyright (c) 2009 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.
+//
+// The audio stream implementation is made difficult because different methods
+// are available for calling depending on what state the stream is. Here is the
+// state transition table for the stream.
+//
+// STATE_CREATED -> Open() -> STATE_OPENED
+// STATE_OPENED -> Start() -> STATE_STARTED
+// STATE_OPENED -> Close() -> STATE_CLOSED
+// STATE_STARTED -> Stop() -> STATE_STOPPED
+// STATE_STARTED -> Close() -> STATE_CLOSING | STATE_CLOSED
+// STATE_STOPPED -> Close() -> STATE_CLOSING | STATE_CLOSED
+// STATE_CLOSING -> [automatic] -> STATE_CLOSED
+//
+// Error states and resource management:
+//
+// Entrance into STATE_STOPPED signals schedules a call to ReleaseResources().
+//
+// Any state may transition to STATE_ERROR. On transitioning into STATE_ERROR,
+// the function doing the transition is reponsible for scheduling a call to
+// ReleaseResources() or otherwise ensuring resources are cleaned (eg., as is
+// done in Open()). This should be done while holding the lock to avoid a
+// destruction race condition where the stream is deleted via ref-count before
+// the ReleaseResources() task is scheduled. In particular, be careful of
+// resource management in a transtiion from STATE_STOPPED -> STATE_ERROR if
+// that becomes necessary in the future.
+//
+// STATE_ERROR may transition to STATE_CLOSED. In this situation, no further
+// resource management is done because it is assumed that the resource
+// reclaimation was executed at the point of the state transition into
+// STATE_ERROR.
+//
+// Entrance into STATE_CLOSED implies a transition through STATE_STOPPED, which
+// triggers the resource management code.
+//
+// The destructor is not responsible for ultimate cleanup of resources.
+// Instead, it only checks that the stream is in a state where all resources
+// have been cleaned up. These states are STATE_CREATED, STATE_CLOSED,
+// STATE_ERROR.
+//
+// TODO(ajwong): This incorrectly handles the period size for filling of the
+// ALSA device buffer. Currently the period size is hardcoded, and not
+// reported to the sound device. Also, there are options that let the device
+// wait until the buffer is minimally filled before playing. Those should be
+// explored. Also consider doing the device interactions either outside of the
+// class lock, or under a different lock to avoid unecessarily blocking other
+// threads.
+
+#include "media/audio/linux/alsa_output.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/stl_util-inl.h"
+#include "base/time.h"
+
+// Require 10ms latency from the audio device. Taken from ALSA documentation
+// example.
+// TODO(ajwong): Figure out what this parameter actually does, and what a good
+// value would be.
+static const unsigned int kTargetLatencyMicroseconds = 10000;
+
+// Minimal amount of time to sleep. If any future event is expected to
+// execute within this timeframe, treat it as if it should execute immediately.
+//
+// TODO(ajwong): Determine if there is a sensible minimum sleep resolution and
+// adjust accordingly.
+static const int64 kMinSleepMilliseconds = 10L;
+
+const char* AlsaPCMOutputStream::kDefaultDevice = "plug:default";
+
+AlsaPCMOutputStream::AlsaPCMOutputStream(const std::string& device_name,
+ int min_buffer_ms,
+ AudioManager::Format format,
+ int channels,
+ int sample_rate,
+ char bits_per_sample)
+ : state_(STATE_CREATED),
+ device_name_(device_name),
+ playback_handle_(NULL),
+ source_callback_(NULL),
+ playback_thread_("PlaybackThread"),
+ channels_(channels),
+ sample_rate_(sample_rate),
+ bits_per_sample_(bits_per_sample),
+ min_buffer_frames_((min_buffer_ms * sample_rate_) /
+ base::Time::kMillisecondsPerSecond),
+ packet_size_(0),
+ device_write_suspended_(true), // Start suspended.
+ resources_released_(false) {
+ CHECK(channels_ == 2) << "Only 2-channel audio is supported right now.";
+ CHECK(AudioManager::AUDIO_PCM_LINEAR == format)
+ << "Only linear PCM supported.";
+ CHECK(bits_per_sample % 8 == 0) << "Only allow byte-aligned samples";
+
+ // Reference self to avoid accidental deletion before the message loop is
+ // done.
+ AddRef();
+
+ switch (bits_per_sample) {
+ case 8:
+ pcm_format_ = SND_PCM_FORMAT_S8;
+ break;
+
+ case 16:
+ pcm_format_ = SND_PCM_FORMAT_S16;
+ break;
+
+ case 24:
+ pcm_format_ = SND_PCM_FORMAT_S24;
+ break;
+
+ case 32:
+ pcm_format_ = SND_PCM_FORMAT_S32;
+ break;
+
+ default:
+ LOG(DFATAL) << "Unsupported bits per sample: " << bits_per_sample_;
+ state_ = STATE_ERROR;
+ break;
+ }
+
+ // Interleaved audio is expected, so each frame has one sample per channel.
+ bytes_per_frame_ = channels_ * (bits_per_sample_ / 8);
+}
+
+AlsaPCMOutputStream::~AlsaPCMOutputStream() {
+ AutoLock l(lock_);
+ // In STATE_CREATED, STATE_CLOSED, and STATE_ERROR, resources are guaranteed
+ // to be released.
+ CHECK(state_ == STATE_CREATED ||
+ state_ == STATE_CLOSED ||
+ state_ == STATE_ERROR);
+}
+
+bool AlsaPCMOutputStream::Open(size_t packet_size) {
+ AutoLock l(lock_);
+
+ // Check that stream is coming from the correct state and early out if not.
+ if (state_ == STATE_ERROR) {
+ return false;
+ }
+ if (state_ != STATE_CREATED) {
+ NOTREACHED() << "Stream must be in STATE_CREATED on Open. Instead in: "
+ << state_;
+ return false;
+ }
+
+ // Open the device and set the parameters.
+ // TODO(ajwong): Can device open block? Probably. If yes, we need to move
+ // the open call into a different thread.
+ int error = snd_pcm_open(&playback_handle_, device_name_.c_str(),
+ SND_PCM_STREAM_PLAYBACK, 0);
+ if (error < 0) {
+ LOG(ERROR) << "Cannot open audio device (" << device_name_ << "): "
+ << snd_strerror(error);
+ EnterStateError_Locked();
+ return false;
+ }
+ if ((error = snd_pcm_set_params(playback_handle_,
+ pcm_format_,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ channels_,
+ sample_rate_,
+ 1, // soft_resample -- let ALSA resample
+ kTargetLatencyMicroseconds)) < 0) {
+ LOG(ERROR) << "Unable to set PCM parameters: " << snd_strerror(error);
+ if (!CloseDevice_Locked()) {
+ LOG(WARNING) << "Unable to close audio device. Leaking handle.";
+ }
+ playback_handle_ = NULL;
+ EnterStateError_Locked();
+ return false;
+ }
+
+ // Configure the buffering.
+ packet_size_ = packet_size;
+ DCHECK_EQ(0U, packet_size_ % bytes_per_frame_)
+ << "Buffers should end on a frame boundary. Frame size: "
+ << bytes_per_frame_;
+
+ // Everything is okay. Stream is officially STATE_OPENED for business.
+ state_ = STATE_OPENED;
+
+ return true;
+}
+
+void AlsaPCMOutputStream::Start(AudioSourceCallback* callback) {
+ AutoLock l(lock_);
+
+ // Check that stream is coming from the correct state and early out if not.
+ if (state_ == STATE_ERROR) {
+ return;
+ }
+ if (state_ != STATE_OPENED) {
+ NOTREACHED() << "Can only be started from STATE_OPEN. Current state: "
+ << state_;
+ return;
+ }
+
+ source_callback_ = callback;
+
+ playback_thread_.Start();
+ playback_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::BufferPackets));
+
+ state_ = STATE_STARTED;
+}
+
+void AlsaPCMOutputStream::Stop() {
+ AutoLock l(lock_);
+ // If the stream is in STATE_ERROR, it is effectively stopped already.
+ if (state_ == STATE_ERROR) {
+ return;
+ }
+ StopInternal_Locked();
+}
+
+void AlsaPCMOutputStream::StopInternal_Locked() {
+ // Check the lock is held in a debug build.
+ DCHECK((lock_.AssertAcquired(), true));
+
+ if (state_ != STATE_STARTED) {
+ NOTREACHED() << "Stream must be in STATE_STARTED to Stop. Instead in: "
+ << state_;
+ return;
+ }
+
+ // Move immediately to STATE_STOPPED to signal that all functions should cease
+ // working at this point. Then post a task to the playback thread to release
+ // resources.
+ state_ = STATE_STOPPED;
+
+ playback_thread_.message_loop()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::ReleaseResources));
+}
+
+void AlsaPCMOutputStream::EnterStateError_Locked() {
+ // Check the lock is held in a debug build.
+ DCHECK((lock_.AssertAcquired(), true));
+
+ state_ = STATE_ERROR;
+ resources_released_ = true;
+
+ // TODO(ajwong): Call OnError() on source_callback_.
+}
+
+void AlsaPCMOutputStream::Close() {
+ AutoLock l(lock_);
+
+ // If in STATE_ERROR, all asynchronous resource reclaimation is finished, so
+ // just change states and release this instance to delete ourself.
+ if (state_ == STATE_ERROR) {
+ Release();
+ state_ = STATE_CLOSED;
+ return;
+ }
+
+ // Otherwise, cleanup as necessary.
+ if (state_ == STATE_CLOSED || state_ == STATE_CLOSING) {
+ NOTREACHED() << "Attempting to close twice.";
+ return;
+ }
+
+ // If the stream is still running, stop it.
+ if (state_ == STATE_STARTED) {
+ StopInternal_Locked();
+ }
+
+ // If it is stopped (we may have just transitioned here in the previous if
+ // block), check if the resources have been released. If they have,
+ // transition immediately to STATE_CLOSED. Otherwise, move to
+ // STATE_CLOSING, and the ReleaseResources() task will move to STATE_CLOSED
+ // for us.
+ //
+ // If the stream has been stopped, close.
+ if (state_ == STATE_STOPPED) {
+ if (resources_released_) {
+ state_ = STATE_CLOSED;
+ } else {
+ state_ = STATE_CLOSING;
+ }
+ } else {
+ // TODO(ajwong): Can we safely handle state_ == STATE_CREATED?
+ NOTREACHED() << "Invalid state on close: " << state_;
+ // In release, just move to STATE_ERROR, and hope for the best.
+ EnterStateError_Locked();
+ }
+}
+
+bool AlsaPCMOutputStream::CloseDevice_Locked() {
+ // Check the lock is held in a debug build.
+ DCHECK((lock_.AssertAcquired(), true));
+
+ int error = snd_pcm_close(playback_handle_);
+ if (error < 0) {
+ LOG(ERROR) << "Cannot close audio device (" << device_name_ << "): "
+ << snd_strerror(error);
+ return false;
+ }
+
+ return true;
+}
+
+void AlsaPCMOutputStream::ReleaseResources() {
+ AutoLock l(lock_);
+
+ // Shutdown the audio device.
+ if (!CloseDevice_Locked()) {
+ LOG(WARNING) << "Unable to close audio device. Leaking handle.";
+ playback_handle_ = NULL;
+ }
+
+ // Delete all the buffers.
+ STLDeleteElements(&buffered_packets_);
+
+ // Release the source callback.
+ source_callback_->OnClose(this);
+
+ // Shutdown the thread.
+ DCHECK_EQ(PlatformThread::CurrentId(), playback_thread_.thread_id());
+ playback_thread_.message_loop()->Quit();
+
+ // TODO(ajwong): Do we need to join the playback thread?
+
+ // If the stream is closing, then this function has just completed the last
+ // bit needed before closing. Transition to STATE_CLOSED.
+ if (state_ == STATE_CLOSING) {
+ state_ = STATE_CLOSED;
+ }
+
+ // TODO(ajwong): Currently, the stream is leaked after the |playback_thread_|
+ // is stopped. Find a way to schedule its deletion on another thread, maybe
+ // using a DestructionObserver.
+}
+
+snd_pcm_sframes_t AlsaPCMOutputStream::GetFramesOfDelay_Locked() {
+ // Check the lock is held in a debug build.
+ DCHECK((lock_.AssertAcquired(), true));
+
+ // Find the number of frames queued in the sound device.
+ snd_pcm_sframes_t delay_frames = 0;
+ int error = snd_pcm_delay(playback_handle_, &delay_frames);
+ if (error < 0) {
+ error = snd_pcm_recover(playback_handle_,
+ error /* Original error. */,
+ 0 /* Silenty recover. */);
+ }
+ if (error < 0) {
+ LOG(ERROR) << "Could not query sound device for delay. Assuming 0: "
+ << snd_strerror(error);
+ }
+
+ for (std::deque<Packet*>::const_iterator it = buffered_packets_.begin();
+ it != buffered_packets_.end();
+ ++it) {
+ delay_frames += ((*it)->size - (*it)->used) / bytes_per_frame_;
+ }
+
+ return delay_frames;
+}
+
+void AlsaPCMOutputStream::BufferPackets() {
+ AutoLock l(lock_);
+
+ // Handle early outs for errored, stopped, or closing streams.
+ if (state_ == STATE_ERROR ||
+ state_ == STATE_STOPPED ||
+ state_ == STATE_CLOSING) {
+ return;
+ }
+ if (state_ != STATE_STARTED) {
+ NOTREACHED() << "Invalid stream state while buffering. "
+ << "Expected STATE_STARTED. Current state: " << state_;
+ return;
+ }
+
+ // Early out if the buffer is already full.
+ snd_pcm_sframes_t delay_frames = GetFramesOfDelay_Locked();
+ if (delay_frames < min_buffer_frames_) {
+ // Grab one packet. Drop the lock for the synchronous call. This will
+ // still stall the playback thread, but at least it will not block any
+ // other threads.
+ //
+ // TODO(ajwong): Move to cpu@'s non-blocking audio source.
+ scoped_ptr<Packet> packet;
+ size_t capacity = packet_size_; // Snag it for non-locked usage.
+ {
+ AutoUnlock synchronous_data_fetch(lock_);
+ packet.reset(new Packet(capacity));
+ size_t used = source_callback_->OnMoreData(this, packet->buffer.get(),
+ packet->capacity);
+ CHECK(used <= capacity) << "Data source overran buffer. Aborting.";
+ packet->size = used;
+ // TODO(ajwong): Do more buffer validation here, like checking that the
+ // packet is correctly aligned to frames, etc.
+ }
+ // After reacquiring the lock, recheck state to make sure it is still
+ // STATE_STARTED.
+ if (state_ != STATE_STARTED) {
+ return;
+ }
+ buffered_packets_.push_back(packet.release());
+
+ // Recalculate delay frames.
+ delay_frames = GetFramesOfDelay_Locked();
+ }
+
+ // Since the current implementation of OnMoreData() blocks, only try to grab
+ // one packet per task. If the buffer is still too low, post another
+ // BufferPackets() task immediately. Otherwise, calculate when the buffer is
+ // likely to need filling and schedule a poll for the future.
+ int next_fill_time_ms = (delay_frames - min_buffer_frames_) / sample_rate_;
+ if (next_fill_time_ms <= kMinSleepMilliseconds) {
+ playback_thread_.message_loop()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::BufferPackets));
+ } else {
+ // TODO(ajwong): Measure the reliability of the delay interval. Use
+ // base/histogram.h.
+ playback_thread_.message_loop()->PostDelayedTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::BufferPackets),
+ next_fill_time_ms);
+ }
+
+ // If the |device_write_suspended_|, the audio device write tasks have
+ // stopped scheduling themselves due to an underrun of the in-memory buffer.
+ // Post a new task to restart it since we now have data.
+ if (device_write_suspended_) {
+ device_write_suspended_ = false;
+ playback_thread_.message_loop()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::FillAlsaDeviceBuffer));
+ }
+}
+
+void AlsaPCMOutputStream::FillAlsaDeviceBuffer() {
+ // TODO(ajwong): Try to move some of this code out from underneath the lock.
+ AutoLock l(lock_);
+
+ // Find the number of frames that the device can accept right now.
+ snd_pcm_sframes_t device_buffer_frames_avail =
+ snd_pcm_avail_update(playback_handle_);
+
+ // Write up to |device_buffer_frames_avail| frames to the ALSA device.
+ while (device_buffer_frames_avail > 0) {
+ if (buffered_packets_.empty()) {
+ device_write_suspended_ = true;
+ break;
+ }
+
+ Packet* current_packet = buffered_packets_.front();
+
+ // Only process non 0-lengthed packets.
+ if (current_packet->used < current_packet->size) {
+ // Calculate the number of frames we have to write.
+ char* buffer_pos = current_packet->buffer.get() + current_packet->used;
+ snd_pcm_sframes_t buffer_frames =
+ (current_packet->size - current_packet->used) /
+ bytes_per_frame_;
+ snd_pcm_sframes_t frames_to_write =
+ std::min(buffer_frames, device_buffer_frames_avail);
+
+ // Check that device_buffer_frames_avail isn't < 0.
+ DCHECK_GT(frames_to_write, 0);
+
+ // Write it to the device.
+ int frames_written =
+ snd_pcm_writei(playback_handle_, buffer_pos, frames_to_write);
+ if (frames_written < 0) {
+ // Recover from EINTR, EPIPE (overrun/underrun), ESTRPIPE (stream
+ // suspended).
+ //
+ // TODO(ajwong): Check that we do not need to loop on recover, here and
+ // anywhere else we use recover.
+ frames_written = snd_pcm_recover(playback_handle_,
+ frames_written /* Original error. */,
+ 0 /* Silenty recover. */);
+ }
+ if (frames_written < 0) {
+ LOG(ERROR) << "Failed to write to pcm device: "
+ << snd_strerror(frames_written);
+ ReleaseResources();
+ EnterStateError_Locked();
+ break;
+ } else {
+ current_packet->used += frames_written * bytes_per_frame_;
+ DCHECK_LE(current_packet->used, current_packet->size);
+ }
+ }
+
+ if (current_packet->used >= current_packet->size) {
+ delete current_packet;
+ buffered_packets_.pop_front();
+ }
+ }
+
+ // If the memory buffer was not underrun, schedule another fill in the future.
+ if (!device_write_suspended_) {
+ playback_thread_.message_loop()->PostDelayedTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPCMOutputStream::FillAlsaDeviceBuffer),
+ kTargetLatencyMicroseconds / base::Time::kMicrosecondsPerMillisecond);
+ }
+}
+
+void AlsaPCMOutputStream::SetVolume(double left_level, double right_level) {
+ NOTIMPLEMENTED();
+}
+
+void AlsaPCMOutputStream::GetVolume(double* left_level, double* right_level) {
+ NOTIMPLEMENTED();
+}
+
+size_t AlsaPCMOutputStream::GetNumBuffers() {
+ return 0;
+}