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

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

Issue 160497: Reimplement the AlsaPcmOutputStream and fix the threading issues. (Closed)
Patch Set: Address Andrew's comments. Diff aginst Patch 8 please. Created 11 years, 4 months 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
« no previous file with comments | « media/audio/linux/alsa_output.h ('k') | media/audio/linux/alsa_output_unittest.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: media/audio/linux/alsa_output.cc
diff --git a/media/audio/linux/alsa_output.cc b/media/audio/linux/alsa_output.cc
index 67cb16a7ad54b471f3ac1984713f1fb131f336d9..4f02318fdcec87e7b78773d1eca30f117d2276be 100644
--- a/media/audio/linux/alsa_output.cc
+++ b/media/audio/linux/alsa_output.cc
@@ -2,51 +2,75 @@
// 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.
+// THREAD SAFETY
//
-// 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
+// The AlsaPcmOutputStream object's internal state is accessed by two threads:
//
-// Error states and resource management:
+// client thread - creates the object and calls the public APIs.
+// message loop thread - executes all the internal tasks including querying
+// the data source for more data, writing to the alsa device, and closing
+// the alsa device. It does *not* handle opening the device though.
//
-// Entrance into STATE_STOPPED signals schedules a call to ReleaseResources().
+// The class is designed so that most operations that read/modify the object's
+// internal state are done on the message loop thread. The exception is data
+// conatined in the |shared_data_| structure. Data in this structure needs to
+// be accessed by both threads, and should only be accessed when the
+// |shared_data_.lock_| is held.
//
-// 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.
+// All member variables that are not in |shared_data_| are created/destroyed on
+// the |message_loop_|. This allows safe access to them from any task posted to
+// |message_loop_|. The values in |shared_data_| are considered to be read-only
+// signals by tasks posted to |message_loop_| (see the SEMANTICS of
+// |shared_data_| section below). Because of these two constraints, tasks can,
+// and must, be coded to be safe in the face of a changing |shared_data_|.
//
-// 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.
+// SEMANTICS OF |shared_data_|
//
-// 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.
+// Though |shared_data_| is accessable by both threads, the code has been
+// structured so that all mutations to |shared_data_| are only done in the
+// client thread. The message loop thread only ever reads the shared data.
//
-// 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.
+// This reduces the need for critical sections because the public API code can
+// assume that no mutations occur to the |shared_data_| between queries.
+//
+// On the message loop side, most tasks have been coded such that they can
+// operate safely regardless of when state changes happen to |shared_data_|.
+// Code that is sensitive to the timing holds the |shared_data_.lock_|
+// explicitly for the duration of the critical section.
+//
+//
+// SEMANTICS OF CloseTask()
+//
+// The CloseTask() is responsible for cleaning up any resources that were
+// acquired after a successful Open(). After a CloseTask() has executed,
+// scheduling of reads should stop. Currently scheduled tasks may run, but
+// they should not attempt to access any of the internal structures beyond
+// querying the |stop_stream_| flag and no-oping themselves. This will
+// guarantee that eventually no more tasks will be posted into the message
+// loop, and the AlsaPcmOutputStream will be able to delete itself.
+//
+//
+// SEMANTICS OF ERROR STATES
+//
+// The object has two distinct error states: |shared_data_.state_| == kInError
+// and |stop_stream_|. The |shared_data_.state_| state is only settable
+// by the client thread, and thus cannot be used to signal when the ALSA device
+// fails due to a hardware (or other low-level) event. The |stop_stream_|
+// variable is only accessed by the message loop thread; it is used to indicate
+// that the playback_handle should no longer be used either because of a
+// hardware/low-level event, or because the CloseTask() has been run.
+//
+// When |shared_data_.state_| == kInError, all public API functions will fail
+// with an error (Start() will call the OnError() function on the callback
+// immediately), or no-op themselves with the exception of Close(). Even if an
+// error state has been entered, if Open() has previously returned successfully,
+// Close() must be called to cleanup the ALSA devices and release resources.
+//
+// When |stop_stream_| is set, no more commands will be made against the
+// ALSA device, and playback will effectively stop. From the client's point of
+// view, it will seem that the device has just clogged and stopped requesting
+// data.
#include "media/audio/linux/alsa_output.h"
@@ -56,482 +80,527 @@
#include "base/stl_util-inl.h"
#include "base/time.h"
#include "media/audio/audio_util.h"
+#include "media/audio/linux/alsa_wrapper.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;
+// Amount of time to wait if we've exhausted the data source. This is to avoid
+// busy looping.
+static const int kNoDataSleepMilliseconds = 10;
-// 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;
+// Set to 0 during debugging if you want error messages due to underrun
+// events or other recoverable errors.
+#if defined(NDEBUG)
+static const int kPcmRecoverIsSilent = 1;
+#else
+static const int kPcmRecoverIsSilent = 0;
+#endif
+
+const char AlsaPcmOutputStream::kDefaultDevice[] = "default";
+
+namespace {
+
+snd_pcm_format_t BitsToFormat(char bits_per_sample) {
+ switch (bits_per_sample) {
+ case 8:
+ return SND_PCM_FORMAT_S8;
+
+ case 16:
+ return SND_PCM_FORMAT_S16;
+
+ case 24:
+ return SND_PCM_FORMAT_S24;
+
+ case 32:
+ return SND_PCM_FORMAT_S32;
-const char* AlsaPCMOutputStream::kDefaultDevice = "default";
+ default:
+ return SND_PCM_FORMAT_UNKNOWN;
+ }
+}
-AlsaPCMOutputStream::AlsaPCMOutputStream(const std::string& device_name,
- int min_buffer_ms,
+} // namespace
+
+std::ostream& operator<<(std::ostream& os,
+ AlsaPcmOutputStream::InternalState state) {
+ switch (state) {
+ case AlsaPcmOutputStream::kInError:
+ os << "kInError";
+ break;
+ case AlsaPcmOutputStream::kCreated:
+ os << "kCreated";
+ break;
+ case AlsaPcmOutputStream::kIsOpened:
+ os << "kIsOpened";
+ break;
+ case AlsaPcmOutputStream::kIsPlaying:
+ os << "kIsPlaying";
+ break;
+ case AlsaPcmOutputStream::kIsStopped:
+ os << "kIsStopped";
+ break;
+ case AlsaPcmOutputStream::kIsClosed:
+ os << "kIsClosed";
+ break;
+ };
+ return os;
+}
+
+AlsaPcmOutputStream::AlsaPcmOutputStream(const std::string& device_name,
AudioManager::Format format,
int channels,
int sample_rate,
- char bits_per_sample)
- : state_(STATE_CREATED),
+ int bits_per_sample,
+ AlsaWrapper* wrapper,
+ MessageLoop* message_loop)
+ : shared_data_(MessageLoop::current()),
device_name_(device_name),
- playback_handle_(NULL),
- source_callback_(NULL),
- playback_thread_("PlaybackThread"),
+ pcm_format_(BitsToFormat(bits_per_sample)),
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),
- volume_(1.0) {
- // Reference self to avoid accidental deletion before the message loop is
- // done.
- AddRef();
+ bytes_per_sample_(bits_per_sample / 8),
+ bytes_per_frame_(channels_ * bits_per_sample / 8),
+ stop_stream_(false),
+ wrapper_(wrapper),
+ playback_handle_(NULL),
+ source_callback_(NULL),
+ frames_per_packet_(0),
+ client_thread_loop_(MessageLoop::current()),
+ message_loop_(message_loop) {
// Sanity check input values.
+ // TODO(ajwong): Just try what happens if we allow non 2-channel audio.
if (channels_ != 2) {
LOG(WARNING) << "Only 2-channel audio is supported right now.";
- state_ = STATE_ERROR;
+ shared_data_.TransitionTo(kInError);
}
if (AudioManager::AUDIO_PCM_LINEAR != format) {
LOG(WARNING) << "Only linear PCM supported.";
- state_ = STATE_ERROR;
+ shared_data_.TransitionTo(kInError);
}
- if (bits_per_sample % 8 != 0) {
- // We do this explicitly just incase someone messes up the switch below.
- LOG(WARNING) << "Only allow byte-aligned samples";
- state_ = STATE_ERROR;
+ if (pcm_format_ == SND_PCM_FORMAT_UNKNOWN) {
+ LOG(WARNING) << "Unsupported bits per sample: " << bits_per_sample;
+ shared_data_.TransitionTo(kInError);
}
+}
- 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;
+AlsaPcmOutputStream::~AlsaPcmOutputStream() {
+ InternalState state = shared_data_.state();
+ DCHECK(state == kCreated || state == kIsClosed || state == kInError);
- 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);
+ // TODO(ajwong): Ensure that CloseTask has been called and the
+ // playback handle released by DCHECKing that playback_handle_ is NULL.
+ // Currently, because of Bug 18217, there is a race condition on destruction
+ // where the stream is not always stopped and closed, causing this to fail.
}
-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) {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
-bool AlsaPCMOutputStream::Open(size_t packet_size) {
- AutoLock l(lock_);
+ DCHECK_EQ(0U, packet_size % bytes_per_frame_)
+ << "Buffers should end on a frame boundary. Frame size: "
+ << bytes_per_frame_;
- // 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_;
+ if (!shared_data_.CanTransitionTo(kIsOpened)) {
+ NOTREACHED() << "Invalid state: " << shared_data_.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);
+ // Try to open the device.
+ snd_pcm_t* handle = NULL;
+ int error = wrapper_->PcmOpen(&handle, device_name_.c_str(),
+ SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
if (error < 0) {
LOG(ERROR) << "Cannot open audio device (" << device_name_ << "): "
- << snd_strerror(error);
- EnterStateError_Locked();
+ << wrapper_->StrError(error);
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()) {
+
+ // Configure the device for software resampling, and add enough buffer for
+ // two audio packets.
+ int micros_per_packet =
+ FramesToMicros(packet_size / bytes_per_frame_, sample_rate_);
+ if ((error = wrapper_->PcmSetParams(handle,
+ pcm_format_,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ channels_,
+ sample_rate_,
+ 1, // soft_resample -- let ALSA resample
+ micros_per_packet * 2)) < 0) {
+ LOG(ERROR) << "Unable to set PCM parameters for (" << device_name_
+ << "): " << wrapper_->StrError(error);
+ if (!CloseDevice(handle)) {
+ // TODO(ajwong): Retry on certain errors?
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;
+ // We do not need to check if the transition was successful because
+ // CanTransitionTo() was checked above, and it is assumed that this
+ // object's public API is only called on one thread so the state cannot
+ // transition out from under us.
+ shared_data_.TransitionTo(kIsOpened);
+ message_loop_->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPcmOutputStream::FinishOpen,
+ handle, packet_size));
return true;
}
-void AlsaPCMOutputStream::Start(AudioSourceCallback* callback) {
- AutoLock l(lock_);
+void AlsaPcmOutputStream::Close() {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
- // 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;
+ if (shared_data_.TransitionTo(kIsClosed) == kIsClosed) {
+ message_loop_->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPcmOutputStream::CloseTask));
}
+}
- source_callback_ = callback;
+void AlsaPcmOutputStream::Start(AudioSourceCallback* callback) {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
- playback_thread_.Start();
- playback_thread_.message_loop()->PostTask(FROM_HERE,
- NewRunnableMethod(this, &AlsaPCMOutputStream::BufferPackets));
+ CHECK(callback);
- 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;
+ // Only post the task if we can enter the playing state.
+ if (shared_data_.TransitionTo(kIsPlaying) == kIsPlaying) {
+ message_loop_->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPcmOutputStream::StartTask, callback));
}
- StopInternal_Locked();
}
-void AlsaPCMOutputStream::StopInternal_Locked() {
- // Check the lock is held in a debug build.
- DCHECK((lock_.AssertAcquired(), true));
+void AlsaPcmOutputStream::Stop() {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
- if (state_ != STATE_STARTED) {
- NOTREACHED() << "Stream must be in STATE_STARTED to Stop. Instead in: "
- << state_;
- return;
- }
+ shared_data_.TransitionTo(kIsStopped);
+}
- // 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;
+void AlsaPcmOutputStream::SetVolume(double left_level, double right_level) {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
- playback_thread_.message_loop()->PostTask(
- FROM_HERE,
- NewRunnableMethod(this, &AlsaPCMOutputStream::ReleaseResources));
+ shared_data_.set_volume(static_cast<float>(left_level));
}
-void AlsaPCMOutputStream::EnterStateError_Locked() {
- // Check the lock is held in a debug build.
- DCHECK((lock_.AssertAcquired(), true));
+void AlsaPcmOutputStream::GetVolume(double* left_level, double* right_level) {
+ DCHECK_EQ(MessageLoop::current(), client_thread_loop_);
+
+ *left_level = *right_level = shared_data_.volume();
+}
- state_ = STATE_ERROR;
- resources_released_ = true;
+void AlsaPcmOutputStream::FinishOpen(snd_pcm_t* playback_handle,
+ size_t packet_size) {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // TODO(ajwong): Call OnError() on source_callback_.
+ playback_handle_ = playback_handle;
+ packet_.reset(new Packet(packet_size));
+ frames_per_packet_ = packet_size / bytes_per_frame_;
}
-void AlsaPCMOutputStream::Close() {
- AutoLock l(lock_);
+void AlsaPcmOutputStream::StartTask(AudioSourceCallback* callback) {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // 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;
- }
+ source_callback_ = callback;
- // Otherwise, cleanup as necessary.
- if (state_ == STATE_CLOSED || state_ == STATE_CLOSING) {
- NOTREACHED() << "Attempting to close twice.";
+ // When starting again, drop all packets in the device and prepare it again
+ // incase we are restarting from a pause state and need to flush old data.
+ int error = wrapper_->PcmDrop(playback_handle_);
+ if (error < 0 && error != -EAGAIN) {
+ LOG(ERROR) << "Failure clearing playback device ("
+ << wrapper_->PcmName(playback_handle_) << "): "
+ << wrapper_->StrError(error);
+ stop_stream_ = true;
return;
}
- // If the stream is still running, stop it.
- if (state_ == STATE_STARTED) {
- StopInternal_Locked();
+ error = wrapper_->PcmPrepare(playback_handle_);
+ if (error < 0 && error != -EAGAIN) {
+ LOG(ERROR) << "Failure preparing stream ("
+ << wrapper_->PcmName(playback_handle_) << "): "
+ << wrapper_->StrError(error);
+ stop_stream_ = true;
+ return;
}
- // 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.
+ // Do a best-effort write of 2 packets to pre-roll.
//
- // 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();
- }
+ // TODO(ajwong): Make this track with the us_latency set in Open().
+ // Also handle EAGAIN.
+ BufferPacket(packet_.get());
+ WritePacket(packet_.get());
+ BufferPacket(packet_.get());
+ WritePacket(packet_.get());
+
+ ScheduleNextWrite(packet_.get());
}
-bool AlsaPCMOutputStream::CloseDevice_Locked() {
- // Check the lock is held in a debug build.
- DCHECK((lock_.AssertAcquired(), true));
+void AlsaPcmOutputStream::CloseTask() {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- int error = snd_pcm_close(playback_handle_);
- if (error < 0) {
- LOG(ERROR) << "Cannot close audio device (" << device_name_ << "): "
- << snd_strerror(error);
- return false;
+ // Shutdown the audio device.
+ if (playback_handle_ && !CloseDevice(playback_handle_)) {
+ LOG(WARNING) << "Unable to close audio device. Leaking handle.";
+ }
+ playback_handle_ = NULL;
+
+ // Release the buffer.
+ packet_.reset();
+
+ // The |source_callback_| may be NULL if the stream is being closed before it
+ // was ever started.
+ if (source_callback_) {
+ // TODO(ajwong): We need to call source_callback_->OnClose(), but the
+ // ownerships of the callback is broken right now, so we'd crash. Instead,
+ // just leak. Bug 18217.
+ source_callback_ = NULL;
}
- return true;
+ // Signal anything that might already be scheduled to stop.
+ stop_stream_ = true;
}
-void AlsaPCMOutputStream::ReleaseResources() {
- AutoLock l(lock_);
+void AlsaPcmOutputStream::BufferPacket(Packet* packet) {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // Shutdown the audio device.
- if (!CloseDevice_Locked()) {
- LOG(WARNING) << "Unable to close audio device. Leaking handle.";
- playback_handle_ = NULL;
+ // If stopped, simulate a 0-lengthed packet.
+ if (stop_stream_) {
+ packet->used = packet->size = 0;
+ return;
}
- // Delete all the buffers.
- STLDeleteElements(&buffered_packets_);
-
- // Release the source callback.
- source_callback_->OnClose(this);
+ // Request more data if we don't have any cached.
+ if (packet->used >= packet->size) {
+ packet->used = 0;
+ packet->size = source_callback_->OnMoreData(this, packet->buffer.get(),
+ packet->capacity);
+ CHECK(packet->size <= packet->capacity) << "Data source overran buffer.";
+
+ // This should not happen, but incase it does, drop any trailing bytes
+ // that aren't large enough to make a frame. Without this, packet writing
+ // may stall because the last few bytes in the packet may never get used by
+ // WritePacket.
+ DCHECK(packet->size % bytes_per_frame_ == 0);
+ packet->size = (packet->size / bytes_per_frame_) * bytes_per_frame_;
+
+ media::AdjustVolume(packet->buffer.get(),
+ packet->size,
+ channels_,
+ bytes_per_sample_,
+ shared_data_.volume());
+ }
+}
- // Shutdown the thread.
- DCHECK_EQ(PlatformThread::CurrentId(), playback_thread_.thread_id());
- playback_thread_.message_loop()->Quit();
+void AlsaPcmOutputStream::WritePacket(Packet* packet) {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // TODO(ajwong): Do we need to join the playback thread?
+ CHECK(packet->size % bytes_per_frame_ == 0);
- // 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;
+ // If the device is in error, just eat the bytes.
+ if (stop_stream_) {
+ packet->used = packet->size;
+ return;
}
- // 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.
+ if (packet->used < packet->size) {
+ char* buffer_pos = packet->buffer.get() + packet->used;
+ snd_pcm_sframes_t frames = FramesInPacket(*packet, bytes_per_frame_);
+
+ DCHECK_GT(frames, 0);
+
+ snd_pcm_sframes_t frames_written =
+ wrapper_->PcmWritei(playback_handle_, buffer_pos, frames);
+ if (frames_written < 0) {
+ // Attempt once to immediately recover from EINTR,
+ // EPIPE (overrun/underrun), ESTRPIPE (stream suspended). WritePacket
+ // will eventually be called again, so eventual recovery will happen if
+ // muliple retries are required.
+ frames_written = wrapper_->PcmRecover(playback_handle_,
+ frames_written,
+ kPcmRecoverIsSilent);
+ }
+
+ if (frames_written < 0) {
+ // TODO(ajwong): Is EAGAIN the only error we want to except from stopping
+ // the pcm playback?
+ if (frames_written != -EAGAIN) {
+ LOG(ERROR) << "Failed to write to pcm device: "
+ << wrapper_->StrError(frames_written);
+ // TODO(ajwong): We need to call source_callback_->OnError(), but the
+ // ownerships of the callback is broken right now, so we'd crash.
+ // Instead, just leak. Bug 18217.
+ stop_stream_ = true;
+ }
+ } else {
+ packet->used += frames_written * bytes_per_frame_;
+ }
+ }
}
-snd_pcm_sframes_t AlsaPCMOutputStream::GetFramesOfDelay_Locked() {
- // Check the lock is held in a debug build.
- DCHECK((lock_.AssertAcquired(), true));
+void AlsaPcmOutputStream::WriteTask() {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // 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);
+ if (stop_stream_) {
+ return;
}
- for (std::deque<Packet*>::const_iterator it = buffered_packets_.begin();
- it != buffered_packets_.end();
- ++it) {
- delay_frames += ((*it)->size - (*it)->used) / bytes_per_frame_;
- }
+ BufferPacket(packet_.get());
+ WritePacket(packet_.get());
- return delay_frames;
+ ScheduleNextWrite(packet_.get());
}
-void AlsaPCMOutputStream::BufferPackets() {
- AutoLock l(lock_);
+void AlsaPcmOutputStream::ScheduleNextWrite(Packet* current_packet) {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
- // Handle early outs for errored, stopped, or closing streams.
- if (state_ == STATE_ERROR ||
- state_ == STATE_STOPPED ||
- state_ == STATE_CLOSING) {
+ if (stop_stream_) {
return;
}
- if (state_ != STATE_STARTED) {
- NOTREACHED() << "Invalid stream state while buffering. "
- << "Expected STATE_STARTED. Current state: " << state_;
- return;
+
+ // Calculate when we should have enough buffer for another packet of data.
+ int frames_leftover = FramesInPacket(*current_packet, bytes_per_frame_);
+ int frames_needed =
+ frames_leftover > 0 ? frames_leftover : frames_per_packet_;
+ int frames_until_empty_enough = frames_needed - GetAvailableFrames();
+ int next_fill_time_ms =
+ FramesToMillis(frames_until_empty_enough, sample_rate_);
+
+ // Avoid busy looping if the data source is exhausted.
+ if (current_packet->size == 0) {
+ next_fill_time_ms = std::max(next_fill_time_ms, kNoDataSleepMilliseconds);
}
- // 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;
- media::AdjustVolume(packet->buffer.get(), packet->size,
- channels_, bits_per_sample_ >> 3,
- volume_);
- // 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;
+ // Only schedule more reads/writes if we are still in the playing state.
+ if (shared_data_.state() == kIsPlaying) {
+ if (next_fill_time_ms <= 0) {
+ message_loop_->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPcmOutputStream::WriteTask));
+ } else {
+ // TODO(ajwong): Measure the reliability of the delay interval. Use
+ // base/histogram.h.
+ message_loop_->PostDelayedTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &AlsaPcmOutputStream::WriteTask),
+ next_fill_time_ms);
}
- buffered_packets_.push_back(packet.release());
+ }
+}
- // Recalculate delay frames.
- delay_frames = GetFramesOfDelay_Locked();
+snd_pcm_sframes_t AlsaPcmOutputStream::FramesInPacket(const Packet& packet,
+ int bytes_per_frame) {
+ return (packet.size - packet.used) / bytes_per_frame;
+}
+
+int64 AlsaPcmOutputStream::FramesToMicros(int frames, int sample_rate) {
+ return frames * base::Time::kMicrosecondsPerSecond / sample_rate;
+}
+
+int64 AlsaPcmOutputStream::FramesToMillis(int frames, int sample_rate) {
+ return frames * base::Time::kMillisecondsPerSecond / sample_rate;
+}
+
+bool AlsaPcmOutputStream::CloseDevice(snd_pcm_t* handle) {
+ int error = wrapper_->PcmClose(handle);
+ if (error < 0) {
+ LOG(ERROR) << "Cannot close audio device (" << wrapper_->PcmName(handle)
+ << "): " << wrapper_->StrError(error);
+ return false;
}
- // 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);
+ return true;
+}
+
+snd_pcm_sframes_t AlsaPcmOutputStream::GetAvailableFrames() {
+ DCHECK_EQ(MessageLoop::current(), message_loop_);
+
+ if (stop_stream_) {
+ return 0;
}
- // 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));
+ // Find the number of frames queued in the sound device.
+ snd_pcm_sframes_t available_frames =
+ wrapper_->PcmAvailUpdate(playback_handle_);
+ if (available_frames < 0) {
+ available_frames = wrapper_->PcmRecover(playback_handle_,
+ available_frames,
+ kPcmRecoverIsSilent);
+ }
+ if (available_frames < 0) {
+ LOG(ERROR) << "Failed querying available frames. Assuming 0: "
+ << wrapper_->StrError(available_frames);
+ return 0;
}
+
+ return available_frames;
+}
+
+AlsaPcmOutputStream::SharedData::SharedData(
+ MessageLoop* state_transition_loop)
+ : state_(kCreated),
+ volume_(1.0f),
+ state_transition_loop_(state_transition_loop) {
}
-void AlsaPCMOutputStream::FillAlsaDeviceBuffer() {
- // TODO(ajwong): Try to move some of this code out from underneath the lock.
+bool AlsaPcmOutputStream::SharedData::CanTransitionTo(InternalState to) {
AutoLock l(lock_);
+ return CanTransitionTo_Locked(to);
+}
- // 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_);
+bool AlsaPcmOutputStream::SharedData::CanTransitionTo_Locked(
+ InternalState to) {
+ lock_.AssertAcquired();
- // 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;
- }
+ switch (state_) {
+ case kCreated:
+ return to == kIsOpened || to == kIsClosed || to == kInError;
- 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);
- }
- }
+ case kIsOpened:
+ return to == kIsPlaying || to == kIsStopped ||
+ to == kIsClosed || to == kInError;
- if (current_packet->used >= current_packet->size) {
- delete current_packet;
- buffered_packets_.pop_front();
- }
+ case kIsPlaying:
+ return to == kIsStopped || to == kIsClosed || to == kInError;
+
+ case kIsStopped:
+ return to == kIsPlaying || to == kIsStopped ||
+ to == kIsClosed || to == kInError;
+
+ case kInError:
+ return to == kIsClosed || to == kInError;
+
+ case kIsClosed:
+ default:
+ return false;
}
+}
- // 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);
+AlsaPcmOutputStream::InternalState
+AlsaPcmOutputStream::SharedData::TransitionTo(InternalState to) {
+ DCHECK_EQ(MessageLoop::current(), state_transition_loop_);
+
+ AutoLock l(lock_);
+ if (!CanTransitionTo_Locked(to)) {
+ NOTREACHED() << "Cannot transition from: " << state_ << " to: " << to;
+ state_ = kInError;
+ } else {
+ state_ = to;
}
+ return state_;
+}
+
+AlsaPcmOutputStream::InternalState AlsaPcmOutputStream::SharedData::state() {
+ AutoLock l(lock_);
+ return state_;
}
-void AlsaPCMOutputStream::SetVolume(double left_level, double right_level) {
+float AlsaPcmOutputStream::SharedData::volume() {
AutoLock l(lock_);
- volume_ = static_cast<float>(left_level);
+ return volume_;
}
-void AlsaPCMOutputStream::GetVolume(double* left_level, double* right_level) {
+void AlsaPcmOutputStream::SharedData::set_volume(float v) {
AutoLock l(lock_);
- *left_level = volume_;
- *right_level = volume_;
+ volume_ = v;
}
« no previous file with comments | « media/audio/linux/alsa_output.h ('k') | media/audio/linux/alsa_output_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698