summaryrefslogtreecommitdiffstats
path: root/media/audio
diff options
context:
space:
mode:
authorajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-08-04 21:49:12 +0000
committerajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-08-04 21:49:12 +0000
commit83337270c01c66378d443f5c3d15926cac164747 (patch)
treea94d2b86aa74a0fa15a5f9404f4abc6c7cc725e4 /media/audio
parentcdf6cc8f2b5036894359123804d28b92b9cfa9ee (diff)
downloadchromium_src-83337270c01c66378d443f5c3d15926cac164747.zip
chromium_src-83337270c01c66378d443f5c3d15926cac164747.tar.gz
chromium_src-83337270c01c66378d443f5c3d15926cac164747.tar.bz2
Reimplement the AlsaPcmOutputStream and fix the threading issues.
Changes the threading model from one thread per stream to one shared thread. Also, redoes the alsa reading/buffering logic to assume an asynchronous data source, and respect the different packet sizes. The Alsa device is set to non-blocking now. State transitions are cleaned up, and the threading semantics are reworked. Now linux audio will no longer crash on shutdown, seek, pause, or tab close. This implementation does still leak though. :( The leak will be fixed in another CL. Review URL: http://codereview.chromium.org/160497 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@22414 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media/audio')
-rw-r--r--media/audio/audio_output.h4
-rw-r--r--media/audio/linux/alsa_output.cc861
-rw-r--r--media/audio/linux/alsa_output.h248
-rw-r--r--media/audio/linux/alsa_output_unittest.cc379
-rw-r--r--media/audio/linux/alsa_wrapper.cc60
-rw-r--r--media/audio/linux/alsa_wrapper.h38
-rw-r--r--media/audio/linux/audio_manager_linux.cc38
-rw-r--r--media/audio/linux/audio_manager_linux.h20
8 files changed, 1136 insertions, 512 deletions
diff --git a/media/audio/audio_output.h b/media/audio/audio_output.h
index a12742e..14c0828 100644
--- a/media/audio/audio_output.h
+++ b/media/audio/audio_output.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Copyright (c) 2008-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.
@@ -43,11 +43,9 @@ class AudioOutputStream {
public:
enum State {
STATE_CREATED = 0, // The output stream is created.
- STATE_OPENED, // The output stream is opened.
STATE_STARTED, // The output stream is started.
STATE_PAUSED, // The output stream is paused.
STATE_STOPPED, // The output stream is stopped.
- STATE_CLOSING, // The output stream is being closed.
STATE_CLOSED, // The output stream is closed.
STATE_ERROR, // The output stream is in error state.
};
diff --git a/media/audio/linux/alsa_output.cc b/media/audio/linux/alsa_output.cc
index 67cb16a..4f02318 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;
}
diff --git a/media/audio/linux/alsa_output.h b/media/audio/linux/alsa_output.h
index 2b619c0..a36c1c2 100644
--- a/media/audio/linux/alsa_output.h
+++ b/media/audio/linux/alsa_output.h
@@ -2,33 +2,34 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
-// Creates an output stream based on the ALSA PCM interface. The current
-// implementation creates one thread per ALSA playback handle that is
-// responsible for synchronously pulling data from the audio data source.
+// Creates an output stream based on the ALSA PCM interface.
//
-// This output stream buffers in two places:
-// (1) In the ALSA device
-// (2) In an in-memory buffer.
+// On device write failure, the stream will move itself to an invalid state.
+// No more data will be pulled from the data source, or written to the device.
+// All calls to public API functions will either no-op themselves, or return an
+// error if possible. Specifically, If the stream is in an error state, Open()
+// will return false, and Start() will call OnError() immediately on the
+// provided callback.
//
-// The ALSA device buffer is kept as full as possible. The in-memory buffer
-// attempts to keep enough extra data so that |min_buffer_ms| worth of data
-// is available between the in-memory buffer and the device buffer. Requests
-// to the audio data source are made if the total amount buffered falls below
-// |min_buffer_ms|.
-//
-// On device write failure, the stream will move into an invalid state. No
-// more data will be pulled from the data source, and the playback thread will
-// be stopped.
+// TODO(ajwong): The OnClose() and OnError() calling needing fixing.
//
// If the stream is successfully opened, Close() must be called before the
-// stream is deleted.
+// stream is deleted as Close() is responsible for ensuring resource cleanup
+// occurs.
+//
+// This object's thread-safety is a little tricky. This object's public API
+// can only be called from the thread that created the object. Calling the
+// public APIs in any method that may cause concurrent execution will result in
+// a race condition. When modifying the code in this class, please read the
+// threading assumptions at the top of the implementation file to avoid
+// introducing race conditions between tasks posted to the internal
+// message_loop, and the thread calling the public APIs.
#ifndef MEDIA_AUDIO_LINUX_ALSA_OUTPUT_H_
#define MEDIA_AUDIO_LINUX_ALSA_OUTPUT_H_
#include <alsa/asoundlib.h>
-#include <deque>
#include <string>
#include "base/lock.h"
@@ -36,27 +37,34 @@
#include "base/scoped_ptr.h"
#include "base/thread.h"
#include "media/audio/audio_output.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
-class Thread;
+class AlsaWrapper;
-class AlsaPCMOutputStream :
+class AlsaPcmOutputStream :
public AudioOutputStream,
- public base::RefCountedThreadSafe<AlsaPCMOutputStream> {
+ public base::RefCountedThreadSafe<AlsaPcmOutputStream> {
public:
// Set to "default" which should avoid locking the sound device and allow
// ALSA to multiplex sound from different processes that want to write PCM
// data.
- static const char* kDefaultDevice;
+ static const char kDefaultDevice[];
// Create a PCM Output stream for the ALSA device identified by
- // |device_name|. If unsure of hte device_name, use kDefaultDevice.
- AlsaPCMOutputStream(const std::string& device_name,
- int min_buffer_ms,
+ // |device_name|. The AlsaPcmOutputStream uses |wrapper| to communicate with
+ // the alsa libraries, allowing for dependency injection during testing. All
+ // requesting of data, and writing to the alsa device will be done on
+ // |message_loop|.
+ //
+ // If unsure of what to use for |device_name|, use |kDefaultDevice|.
+ AlsaPcmOutputStream(const std::string& device_name,
AudioManager::Format format,
int channels,
int sample_rate,
- char bits_per_sample);
- virtual ~AlsaPCMOutputStream();
+ int bits_per_sample,
+ AlsaWrapper* wrapper,
+ MessageLoop* message_loop);
+ virtual ~AlsaPcmOutputStream();
// Implementation of AudioOutputStream.
virtual bool Open(size_t packet_size);
@@ -67,73 +75,24 @@ class AlsaPCMOutputStream :
virtual void GetVolume(double* left_level, double* right_level);
private:
- // Closes the playback handle, reporting errors if any occur. Returns true
- // if the device was successfully closed.
- bool CloseDevice_Locked();
-
- // Stops playback, ignoring state checks.
- void StopInternal_Locked();
-
- // Moves the stream into the error state, setting the correct internal flags.
- // Ensure that all resources are cleaned up before executing this function.
- void EnterStateError_Locked();
-
- // Releases all the resources in the audio stream. This method will also
- // terminate the playback thread itself.
- //
- // This method must be run in the |playback_thead_|.
- void ReleaseResources();
-
- // Retrieve the total number of frames buffered in both memory and in the
- // audio device. Use this to determine if more data should be requested from
- // the audio source.
- snd_pcm_sframes_t GetFramesOfDelay_Locked();
-
- // Buffer more packets from data source if necessary.
- //
- // This function must be run in the |playback_thread_|.
- void BufferPackets();
-
- // Returns true if our buffer is underfull.
- bool ShouldBufferMore_NoLock();
-
- // Write as many buffered packets into the device as there is room in the
- // device buffer.
- //
- // This function must be run in the |playback_thread_|.
- void FillAlsaDeviceBuffer();
-
- // State of the stream.
- State state_;
-
- // The ALSA device name to use.
- std::string device_name_;
-
- // Handle to the actual PCM playback device.
- snd_pcm_t* playback_handle_;
-
- // Period size for ALSA ring-buffer. Basically, how long to wait between
- // writes.
- snd_pcm_sframes_t period_size_;
-
- // Callback used to request more data from the data source.
- AudioSourceCallback* source_callback_;
-
- // Playback thread.
- base::Thread playback_thread_;
-
- // Lock for field access to this object.
- Lock lock_;
-
- // Sample format configuration.
- snd_pcm_format_t pcm_format_;
- const int channels_;
- const int sample_rate_;
- const char bits_per_sample_;
- char bytes_per_frame_;
+ friend class AlsaPcmOutputStreamTest;
+ FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket_StopStream);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, BufferPacket_UnfinishedPacket);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, ConstructedState);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, OpenClose);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, PcmOpenFailed);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, PcmSetParamsFailed);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, ScheduleNextWrite);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, ScheduleNextWrite_StopStream);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, StartStop);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, WritePacket_FinishedPacket);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, WritePacket_NormalPacket);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, WritePacket_StopStream);
+ FRIEND_TEST(AlsaPcmOutputStreamTest, WritePacket_WriteFails);
// In-memory buffer to hold sound samples before pushing to the sound device.
- // TODO(ajwong): There are now about 3 buffer queue implementations. Factor
+ // TODO(ajwong): There are now about 3 buffer/packet implementations. Factor
// them out.
struct Packet {
explicit Packet(int new_capacity)
@@ -147,22 +106,105 @@ class AlsaPCMOutputStream :
size_t used;
scoped_array<char> buffer;
};
- int min_buffer_frames_;
- std::deque<Packet*> buffered_packets_;
- size_t packet_size_;
- // Flag indiciating the device write tasks have stopped scheduling
- // themselves. This should only be modified by the BufferPackets() and
- // FillAlsaDeviceBuffer() methods.
- bool device_write_suspended_;
+ // Flags indicating the state of the stream.
+ enum InternalState {
+ kInError = 0,
+ kCreated,
+ kIsOpened,
+ kIsPlaying,
+ kIsStopped,
+ kIsClosed
+ };
+ friend std::ostream& ::operator<<(std::ostream& os, InternalState);
+
+ // Various tasks that complete actions started in the public API.
+ void FinishOpen(snd_pcm_t* playback_handle, size_t packet_size);
+ void StartTask(AudioSourceCallback* callback);
+ void CloseTask();
+
+ // Functions to get another packet from the data source and write it into the
+ // ALSA device.
+ void BufferPacket(Packet* packet);
+ void WritePacket(Packet* packet);
+ void WriteTask();
+ void ScheduleNextWrite(Packet* current_packet);
+
+ // Functions to safeguard state transitions and ensure that transitions are
+ // only allowed occuring on the thread that created the object. All changes
+ // to the object state should go through these functions.
+ bool CanTransitionTo(InternalState to);
+ bool CanTransitionTo_Locked(InternalState to);
+ InternalState TransitionTo(InternalState to);
+
+ // Utility functions for talking with the ALSA API.
+ static snd_pcm_sframes_t FramesInPacket(const Packet& packet,
+ int bytes_per_frame);
+ static int64 FramesToMicros(int frames, int sample_rate);
+ static int64 FramesToMillis(int frames, int sample_rate);
+ bool CloseDevice(snd_pcm_t* handle);
+ snd_pcm_sframes_t GetAvailableFrames();
+
+ // Struct holding all mutable the data that must be shared by the
+ // message_loop() and the thread that created the object.
+ class SharedData {
+ public:
+ explicit SharedData(MessageLoop* state_transition_loop);
+
+ // Functions to safeguard state transitions and ensure that transitions are
+ // only allowed occuring on the thread that created the object. All
+ // changes to the object state should go through these functions.
+ bool CanTransitionTo(InternalState to);
+ bool CanTransitionTo_Locked(InternalState to);
+ InternalState TransitionTo(InternalState to);
+ InternalState state();
+
+ float volume();
+ void set_volume(float v);
+
+ private:
+ Lock lock_;
+
+ InternalState state_;
+ float volume_; // Volume level from 0.0 to 1.0.
+
+ MessageLoop* const state_transition_loop_;
+ } shared_data_;
+
+ // Configuration constants from the constructor. Referenceable by all threads
+ // since they are constants.
+ const std::string device_name_;
+ const snd_pcm_format_t pcm_format_;
+ const int channels_;
+ const int sample_rate_;
+ const int bytes_per_sample_;
+ const int bytes_per_frame_;
+
+ // Flag indicating the code should stop reading from the data source or
+ // writing to the ALSA device. This is set because the device has entered
+ // an unrecoverable error state, or the ClosedTask() has executed.
+ bool stop_stream_;
+
+ // Wrapper class to invoke all the ALSA functions.
+ AlsaWrapper* wrapper_;
+
+ // Handle to the actual PCM playback device.
+ snd_pcm_t* playback_handle_;
+
+ // Callback used to request more data from the data source.
+ AudioSourceCallback* source_callback_;
+
+ scoped_ptr<Packet> packet_;
+ int frames_per_packet_;
- // Flag indicating that the resources are already cleaned.
- bool resources_released_;
+ // Used to check which message loop is allowed to call the public APIs.
+ MessageLoop* client_thread_loop_;
- // Volume level from 0 to 1.
- float volume_;
+ // The message loop responsible for querying the data source, and writing to
+ // the output device.
+ MessageLoop* message_loop_;
- DISALLOW_COPY_AND_ASSIGN(AlsaPCMOutputStream);
+ DISALLOW_COPY_AND_ASSIGN(AlsaPcmOutputStream);
};
#endif // MEDIA_AUDIO_LINUX_ALSA_OUTPUT_H_
diff --git a/media/audio/linux/alsa_output_unittest.cc b/media/audio/linux/alsa_output_unittest.cc
new file mode 100644
index 0000000..b215b3a
--- /dev/null
+++ b/media/audio/linux/alsa_output_unittest.cc
@@ -0,0 +1,379 @@
+// 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.
+
+#include "base/logging.h"
+#include "media/audio/linux/alsa_output.h"
+#include "media/audio/linux/alsa_wrapper.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::_;
+using testing::DoAll;
+using testing::Eq;
+using testing::Return;
+using testing::SetArgumentPointee;
+using testing::StrictMock;
+using testing::StrEq;
+
+class MockAlsaWrapper : public AlsaWrapper {
+ public:
+ MOCK_METHOD4(PcmOpen, int(snd_pcm_t** handle, const char* name,
+ snd_pcm_stream_t stream, int mode));
+ MOCK_METHOD1(PcmClose, int(snd_pcm_t* handle));
+ MOCK_METHOD1(PcmPrepare, int(snd_pcm_t* handle));
+ MOCK_METHOD1(PcmDrop, int(snd_pcm_t* handle));
+ MOCK_METHOD3(PcmWritei, snd_pcm_sframes_t(snd_pcm_t* handle,
+ const void* buffer,
+ snd_pcm_uframes_t size));
+ MOCK_METHOD3(PcmRecover, int(snd_pcm_t* handle, int err, int silent));
+ MOCK_METHOD7(PcmSetParams, int(snd_pcm_t* handle, snd_pcm_format_t format,
+ snd_pcm_access_t access, unsigned int channels,
+ unsigned int rate, int soft_resample,
+ unsigned int latency));
+ MOCK_METHOD1(PcmName, const char*(snd_pcm_t* handle));
+ MOCK_METHOD1(PcmAvailUpdate, snd_pcm_sframes_t (snd_pcm_t* handle));
+
+ MOCK_METHOD1(StrError, const char*(int errnum));
+};
+
+class MockAudioSourceCallback : public AudioOutputStream::AudioSourceCallback {
+ public:
+ MOCK_METHOD3(OnMoreData, size_t(AudioOutputStream* stream,
+ void* dest, size_t max_size));
+ MOCK_METHOD1(OnClose, void(AudioOutputStream* stream));
+ MOCK_METHOD2(OnError, void(AudioOutputStream* stream, int code));
+};
+
+class AlsaPcmOutputStreamTest : public testing::Test {
+ protected:
+ AlsaPcmOutputStreamTest()
+ : packet_(kTestPacketSize + 1) {
+ test_stream_ = new AlsaPcmOutputStream(kTestDeviceName,
+ kTestFormat,
+ kTestChannels,
+ kTestSampleRate,
+ kTestBitsPerSample,
+ &mock_alsa_wrapper_,
+ &message_loop_);
+
+ packet_.size = kTestPacketSize;
+ }
+
+ virtual ~AlsaPcmOutputStreamTest() {
+ test_stream_ = NULL;
+ }
+
+ static const int kTestChannels;
+ static const int kTestSampleRate;
+ static const int kTestBitsPerSample;
+ static const int kTestBytesPerFrame;
+ static const AudioManager::Format kTestFormat;
+ static const char kTestDeviceName[];
+ static const char kDummyMessage[];
+ static const int kTestFramesPerPacket;
+ static const size_t kTestPacketSize;
+ static const int kTestFailedErrno;
+ static snd_pcm_t* const kFakeHandle;
+
+ StrictMock<MockAlsaWrapper> mock_alsa_wrapper_;
+ MessageLoop message_loop_;
+ scoped_refptr<AlsaPcmOutputStream> test_stream_;
+ AlsaPcmOutputStream::Packet packet_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(AlsaPcmOutputStreamTest);
+};
+
+const int AlsaPcmOutputStreamTest::kTestChannels = 2;
+const int AlsaPcmOutputStreamTest::kTestSampleRate =
+ AudioManager::kAudioCDSampleRate;
+const int AlsaPcmOutputStreamTest::kTestBitsPerSample = 8;
+const int AlsaPcmOutputStreamTest::kTestBytesPerFrame =
+ AlsaPcmOutputStreamTest::kTestBitsPerSample / 8 *
+ AlsaPcmOutputStreamTest::kTestChannels;
+const AudioManager::Format AlsaPcmOutputStreamTest::kTestFormat =
+ AudioManager::AUDIO_PCM_LINEAR;
+const char AlsaPcmOutputStreamTest::kTestDeviceName[] = "TestDevice";
+const char AlsaPcmOutputStreamTest::kDummyMessage[] = "dummy";
+const int AlsaPcmOutputStreamTest::kTestFramesPerPacket = 100;
+const size_t AlsaPcmOutputStreamTest::kTestPacketSize =
+ AlsaPcmOutputStreamTest::kTestFramesPerPacket *
+ AlsaPcmOutputStreamTest::kTestBytesPerFrame;
+const int AlsaPcmOutputStreamTest::kTestFailedErrno = -EACCES;
+snd_pcm_t* const AlsaPcmOutputStreamTest::kFakeHandle =
+ reinterpret_cast<snd_pcm_t*>(1);
+
+TEST_F(AlsaPcmOutputStreamTest, ConstructedState) {
+ EXPECT_EQ(AlsaPcmOutputStream::kCreated,
+ test_stream_->shared_data_.state());
+
+ // Only supports 2 channel.
+ test_stream_ = new AlsaPcmOutputStream(kTestDeviceName,
+ kTestFormat,
+ kTestChannels + 1,
+ kTestSampleRate,
+ kTestBitsPerSample,
+ &mock_alsa_wrapper_,
+ &message_loop_);
+ EXPECT_EQ(AlsaPcmOutputStream::kInError,
+ test_stream_->shared_data_.state());
+
+ // Bad bits per sample.
+ test_stream_ = new AlsaPcmOutputStream(kTestDeviceName,
+ kTestFormat,
+ kTestChannels,
+ kTestSampleRate,
+ kTestBitsPerSample - 1,
+ &mock_alsa_wrapper_,
+ &message_loop_);
+ EXPECT_EQ(AlsaPcmOutputStream::kInError,
+ test_stream_->shared_data_.state());
+
+ // Bad format.
+ test_stream_ = new AlsaPcmOutputStream(kTestDeviceName,
+ AudioManager::AUDIO_PCM_DELTA,
+ kTestChannels,
+ kTestSampleRate,
+ kTestBitsPerSample,
+ &mock_alsa_wrapper_,
+ &message_loop_);
+ EXPECT_EQ(AlsaPcmOutputStream::kInError,
+ test_stream_->shared_data_.state());
+}
+
+TEST_F(AlsaPcmOutputStreamTest, OpenClose) {
+ int64 expected_micros = 2 *
+ AlsaPcmOutputStream::FramesToMicros(kTestPacketSize / kTestBytesPerFrame,
+ kTestSampleRate);
+
+ // Open() call opens the playback device, sets the parameters, posts a task
+ // with the resulting configuration data, and transitions the object state to
+ // kIsOpened.
+ EXPECT_CALL(mock_alsa_wrapper_,
+ PcmOpen(_, StrEq(kTestDeviceName),
+ SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK))
+ .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle),
+ Return(0)));
+ EXPECT_CALL(mock_alsa_wrapper_,
+ PcmSetParams(kFakeHandle,
+ SND_PCM_FORMAT_S8,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ kTestChannels,
+ kTestSampleRate,
+ 1,
+ expected_micros))
+ .WillOnce(Return(0));
+
+ // Open the stream.
+ ASSERT_TRUE(test_stream_->Open(kTestPacketSize));
+ message_loop_.RunAllPending();
+
+ EXPECT_EQ(AlsaPcmOutputStream::kIsOpened,
+ test_stream_->shared_data_.state());
+ EXPECT_EQ(kFakeHandle, test_stream_->playback_handle_);
+ EXPECT_EQ(kTestFramesPerPacket, test_stream_->frames_per_packet_);
+ EXPECT_TRUE(test_stream_->packet_.get());
+ EXPECT_FALSE(test_stream_->stop_stream_);
+
+ // Now close it and test that everything was released.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmClose(kFakeHandle))
+ .WillOnce(Return(0));
+ test_stream_->Close();
+ message_loop_.RunAllPending();
+
+ EXPECT_EQ(NULL, test_stream_->playback_handle_);
+ EXPECT_FALSE(test_stream_->packet_.get());
+ EXPECT_TRUE(test_stream_->stop_stream_);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, PcmOpenFailed) {
+ EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, _, _, _))
+ .WillOnce(Return(kTestFailedErrno));
+ EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno))
+ .WillOnce(Return(kDummyMessage));
+
+ // If open fails, the stream stays in kCreated because it has effectively had
+ // no changes.
+ ASSERT_FALSE(test_stream_->Open(kTestPacketSize));
+ EXPECT_EQ(AlsaPcmOutputStream::kCreated,
+ test_stream_->shared_data_.state());
+}
+
+TEST_F(AlsaPcmOutputStreamTest, PcmSetParamsFailed) {
+ EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, _, _, _))
+ .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle),
+ Return(0)));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmSetParams(_, _, _, _, _, _, _))
+ .WillOnce(Return(kTestFailedErrno));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmClose(kFakeHandle))
+ .WillOnce(Return(0));
+ EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno))
+ .WillOnce(Return(kDummyMessage));
+
+ // If open fails, the stream stays in kCreated because it has effectively had
+ // no changes.
+ ASSERT_FALSE(test_stream_->Open(kTestPacketSize));
+ EXPECT_EQ(AlsaPcmOutputStream::kCreated,
+ test_stream_->shared_data_.state());
+}
+
+TEST_F(AlsaPcmOutputStreamTest, StartStop) {
+ // Open() call opens the playback device, sets the parameters, posts a task
+ // with the resulting configuration data, and transitions the object state to
+ // kIsOpened.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmOpen(_, _, _, _))
+ .WillOnce(DoAll(SetArgumentPointee<0>(kFakeHandle),
+ Return(0)));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmSetParams(_, _, _, _, _, _, _))
+ .WillOnce(Return(0));
+
+ // Open the stream.
+ ASSERT_TRUE(test_stream_->Open(kTestPacketSize));
+ message_loop_.RunAllPending();
+
+ // Expect Device setup.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmDrop(kFakeHandle))
+ .WillOnce(Return(0));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmPrepare(kFakeHandle))
+ .WillOnce(Return(0));
+
+ // Expect the pre-roll.
+ MockAudioSourceCallback mock_callback;
+ EXPECT_CALL(mock_callback,
+ OnMoreData(test_stream_.get(), _, kTestPacketSize))
+ .Times(2)
+ .WillRepeatedly(Return(kTestPacketSize));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmWritei(kFakeHandle, _, _))
+ .Times(2)
+ .WillRepeatedly(Return(kTestPacketSize));
+
+ // Expect scheduling.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmAvailUpdate(kFakeHandle))
+ .WillOnce(Return(1));
+
+ test_stream_->Start(&mock_callback);
+ message_loop_.RunAllPending();
+
+ EXPECT_CALL(mock_alsa_wrapper_, PcmClose(kFakeHandle))
+ .WillOnce(Return(0));
+ test_stream_->Close();
+ message_loop_.RunAllPending();
+}
+
+TEST_F(AlsaPcmOutputStreamTest, WritePacket_FinishedPacket) {
+ // Nothing should happen. Don't set any expectations and Our strict mocks
+ // should verify most of this.
+
+ // Test regular used-up packet.
+ packet_.used = packet_.size;
+ test_stream_->WritePacket(&packet_);
+
+ // Test empty packet.
+ packet_.used = packet_.size = 0;
+ test_stream_->WritePacket(&packet_);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, WritePacket_NormalPacket) {
+ // Write a little less than half the data.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmWritei(_, packet_.buffer.get(), _))
+ .WillOnce(Return(packet_.size / kTestBytesPerFrame / 2 - 1));
+
+ test_stream_->WritePacket(&packet_);
+
+ ASSERT_EQ(packet_.size / 2 - kTestBytesPerFrame, packet_.used);
+
+ // Write the rest.
+ EXPECT_CALL(mock_alsa_wrapper_,
+ PcmWritei(_, packet_.buffer.get() + packet_.used, _))
+ .WillOnce(Return(packet_.size / kTestBytesPerFrame / 2 + 1));
+ test_stream_->WritePacket(&packet_);
+ EXPECT_EQ(packet_.size, packet_.used);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, WritePacket_WriteFails) {
+ // Fail due to a recoverable error and see that PcmRecover code path
+ // continues normally.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmWritei(_, _, _))
+ .WillOnce(Return(-EINTR));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmRecover(_, _, _))
+ .WillOnce(Return(packet_.size / kTestBytesPerFrame / 2 - 1));
+
+ test_stream_->WritePacket(&packet_);
+
+ ASSERT_EQ(packet_.size / 2 - kTestBytesPerFrame, packet_.used);
+
+ // Fail the next write, and see that stop_stream_ is set.
+ EXPECT_CALL(mock_alsa_wrapper_, PcmWritei(_, _, _))
+ .WillOnce(Return(kTestFailedErrno));
+ EXPECT_CALL(mock_alsa_wrapper_, PcmRecover(_, _, _))
+ .WillOnce(Return(kTestFailedErrno));
+ EXPECT_CALL(mock_alsa_wrapper_, StrError(kTestFailedErrno))
+ .WillOnce(Return(kDummyMessage));
+ test_stream_->WritePacket(&packet_);
+ EXPECT_EQ(packet_.size / 2 - kTestBytesPerFrame, packet_.used);
+ EXPECT_TRUE(test_stream_->stop_stream_);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, WritePacket_StopStream) {
+ // No expectations set on the strict mock because nothing should be called.
+ test_stream_->stop_stream_ = true;
+ test_stream_->WritePacket(&packet_);
+ EXPECT_EQ(packet_.size, packet_.used);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, BufferPacket) {
+ packet_.used = packet_.size;
+
+ // Return a partially filled packet.
+ MockAudioSourceCallback mock_callback;
+ EXPECT_CALL(mock_callback,
+ OnMoreData(test_stream_.get(), packet_.buffer.get(),
+ packet_.capacity))
+ .WillOnce(Return(10));
+
+ test_stream_->source_callback_ = &mock_callback;
+ test_stream_->BufferPacket(&packet_);
+
+ EXPECT_EQ(0u, packet_.used);
+ EXPECT_EQ(10u, packet_.size);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, BufferPacket_UnfinishedPacket) {
+ // No expectations set on the strict mock because nothing should be called.
+ test_stream_->BufferPacket(&packet_);
+ EXPECT_EQ(0u, packet_.used);
+ EXPECT_EQ(kTestPacketSize, packet_.size);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, BufferPacket_StopStream) {
+ test_stream_->stop_stream_ = true;
+ test_stream_->BufferPacket(&packet_);
+ EXPECT_EQ(0u, packet_.used);
+ EXPECT_EQ(0u, packet_.size);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, ScheduleNextWrite) {
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsOpened);
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsPlaying);
+
+ EXPECT_CALL(mock_alsa_wrapper_, PcmAvailUpdate(_))
+ .WillOnce(Return(10));
+ test_stream_->ScheduleNextWrite(&packet_);
+
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsClosed);
+}
+
+TEST_F(AlsaPcmOutputStreamTest, ScheduleNextWrite_StopStream) {
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsOpened);
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsPlaying);
+
+ test_stream_->stop_stream_ = true;
+ test_stream_->ScheduleNextWrite(&packet_);
+
+ // TODO(ajwong): Find a way to test whether or not another task has been
+ // posted so we can verify that the Alsa code will indeed break the task
+ // posting loop.
+
+ test_stream_->shared_data_.TransitionTo(AlsaPcmOutputStream::kIsClosed);
+}
diff --git a/media/audio/linux/alsa_wrapper.cc b/media/audio/linux/alsa_wrapper.cc
new file mode 100644
index 0000000..9557cac
--- /dev/null
+++ b/media/audio/linux/alsa_wrapper.cc
@@ -0,0 +1,60 @@
+// 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.
+
+#include "media/audio/linux/alsa_wrapper.h"
+
+#include <alsa/asoundlib.h>
+
+AlsaWrapper::AlsaWrapper() {
+}
+
+AlsaWrapper::~AlsaWrapper() {
+}
+
+int AlsaWrapper::PcmOpen(snd_pcm_t** handle, const char* name,
+ snd_pcm_stream_t stream, int mode) {
+ return snd_pcm_open(handle, name, stream, mode);
+}
+
+int AlsaWrapper::PcmClose(snd_pcm_t* handle) {
+ return snd_pcm_close(handle);
+}
+
+int AlsaWrapper::PcmPrepare(snd_pcm_t* handle) {
+ return snd_pcm_prepare(handle);
+}
+
+int AlsaWrapper::PcmDrop(snd_pcm_t* handle) {
+ return snd_pcm_drop(handle);
+}
+
+snd_pcm_sframes_t AlsaWrapper::PcmWritei(snd_pcm_t* handle,
+ const void* buffer,
+ snd_pcm_uframes_t size) {
+ return snd_pcm_writei(handle, buffer, size);
+}
+
+int AlsaWrapper::PcmRecover(snd_pcm_t* handle, int err, int silent) {
+ return snd_pcm_recover(handle, err, silent);
+}
+
+const char* AlsaWrapper::PcmName(snd_pcm_t* handle) {
+ return snd_pcm_name(handle);
+}
+
+int AlsaWrapper::PcmSetParams(snd_pcm_t* handle, snd_pcm_format_t format,
+ snd_pcm_access_t access, unsigned int channels,
+ unsigned int rate, int soft_resample,
+ unsigned int latency) {
+ return snd_pcm_set_params(handle, format, access, channels, rate,
+ soft_resample, latency);
+}
+
+snd_pcm_sframes_t AlsaWrapper::PcmAvailUpdate(snd_pcm_t* handle) {
+ return snd_pcm_avail_update(handle);
+}
+
+const char* AlsaWrapper::StrError(int errnum) {
+ return snd_strerror(errnum);
+}
diff --git a/media/audio/linux/alsa_wrapper.h b/media/audio/linux/alsa_wrapper.h
new file mode 100644
index 0000000..a3dafbb
--- /dev/null
+++ b/media/audio/linux/alsa_wrapper.h
@@ -0,0 +1,38 @@
+// 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.
+//
+// AlsaWrapper is a simple stateless class that wraps the alsa library commands
+// we want to use. It's purpose is to allow injection of a mock so that the
+// higher level code is testable.
+
+#include <alsa/asoundlib.h>
+
+#include "base/basictypes.h"
+
+class AlsaWrapper {
+ public:
+ AlsaWrapper();
+ virtual ~AlsaWrapper();
+
+ virtual int PcmOpen(snd_pcm_t** handle, const char* name,
+ snd_pcm_stream_t stream, int mode);
+ virtual int PcmClose(snd_pcm_t* handle);
+ virtual int PcmPrepare(snd_pcm_t* handle);
+ virtual int PcmDrop(snd_pcm_t* handle);
+ virtual snd_pcm_sframes_t PcmWritei(snd_pcm_t* handle,
+ const void* buffer,
+ snd_pcm_uframes_t size);
+ virtual int PcmRecover(snd_pcm_t* handle, int err, int silent);
+ virtual int PcmSetParams(snd_pcm_t* handle, snd_pcm_format_t format,
+ snd_pcm_access_t access, unsigned int channels,
+ unsigned int rate, int soft_resample,
+ unsigned int latency);
+ virtual const char* PcmName(snd_pcm_t* handle);
+ virtual snd_pcm_sframes_t PcmAvailUpdate(snd_pcm_t* handle);
+
+ virtual const char* StrError(int errnum);
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(AlsaWrapper);
+};
diff --git a/media/audio/linux/audio_manager_linux.cc b/media/audio/linux/audio_manager_linux.cc
index a9d299f..3d1d98c 100644
--- a/media/audio/linux/audio_manager_linux.cc
+++ b/media/audio/linux/audio_manager_linux.cc
@@ -8,6 +8,7 @@
#include "base/logging.h"
#include "media/audio/fake_audio_output_stream.h"
#include "media/audio/linux/alsa_output.h"
+#include "media/audio/linux/alsa_wrapper.h"
namespace {
AudioManagerLinux* g_audio_manager = NULL;
@@ -23,28 +24,44 @@ AudioOutputStream* AudioManagerLinux::MakeAudioStream(Format format,
int channels,
int sample_rate,
char bits_per_sample) {
+ // Early return for testing hook. Do this before checking for
+ // |initialized_|.
+ if (format == AudioManager::AUDIO_MOCK) {
+ return FakeAudioOutputStream::MakeFakeStream();
+ }
+
+ if (!initialized_) {
+ return NULL;
+ }
+
// TODO(ajwong): Do we want to be able to configure the device? default
// should work correctly for all mono/stereo, but not surround, which needs
// surround40, surround51, etc.
//
// http://0pointer.de/blog/projects/guide-to-sound-apis.html
- if (format == AudioManager::AUDIO_MOCK) {
- return FakeAudioOutputStream::MakeFakeStream();
- } else {
- AlsaPCMOutputStream* stream =
- new AlsaPCMOutputStream(AlsaPCMOutputStream::kDefaultDevice,
- 100 /* 100ms minimal buffer */,
- format, channels, sample_rate, bits_per_sample);
- return stream;
- }
+ AlsaPcmOutputStream* stream =
+ new AlsaPcmOutputStream(AlsaPcmOutputStream::kDefaultDevice,
+ format, channels, sample_rate, bits_per_sample,
+ wrapper_.get(), audio_thread_.message_loop());
+
+ // TODO(ajwong): Set up this to clear itself when the stream closes.
+ active_streams_[stream] = scoped_refptr<AlsaPcmOutputStream>(stream);
+ return stream;
}
-AudioManagerLinux::AudioManagerLinux() {
+AudioManagerLinux::AudioManagerLinux()
+ : audio_thread_("AudioThread"),
+ initialized_(false) {
}
AudioManagerLinux::~AudioManagerLinux() {
}
+void AudioManagerLinux::Init() {
+ initialized_ = audio_thread_.Start();
+ wrapper_.reset(new AlsaWrapper());
+}
+
void AudioManagerLinux::MuteAll() {
// TODO(ajwong): Implement.
NOTIMPLEMENTED();
@@ -64,6 +81,7 @@ void DestroyAudioManagerLinux(void* not_used) {
AudioManager* AudioManager::GetAudioManager() {
if (!g_audio_manager) {
g_audio_manager = new AudioManagerLinux();
+ g_audio_manager->Init();
base::AtExitManager::RegisterCallback(&DestroyAudioManagerLinux, NULL);
}
return g_audio_manager;
diff --git a/media/audio/linux/audio_manager_linux.h b/media/audio/linux/audio_manager_linux.h
index 191e687..d33c1a4 100644
--- a/media/audio/linux/audio_manager_linux.h
+++ b/media/audio/linux/audio_manager_linux.h
@@ -5,13 +5,23 @@
#ifndef MEDIA_AUDIO_LINUX_AUDIO_MANAGER_LINUX_H_
#define MEDIA_AUDIO_LINUX_AUDIO_MANAGER_LINUX_H_
+#include <map>
+
+#include "base/ref_counted.h"
+#include "base/scoped_ptr.h"
#include "base/thread.h"
#include "media/audio/audio_output.h"
+class AlsaPcmOutputStream;
+class AlsaWrapper;
+
class AudioManagerLinux : public AudioManager {
public:
AudioManagerLinux();
+ // Call before using a newly created AudioManagerLinux instance.
+ void Init();
+
// Implementation of AudioManager.
virtual bool HasAudioDevices();
virtual AudioOutputStream* MakeAudioStream(Format format, int channels,
@@ -25,6 +35,16 @@ class AudioManagerLinux : public AudioManager {
friend void DestroyAudioManagerLinux(void*);
virtual ~AudioManagerLinux();
+ // Thread used to interact with AudioOutputStreams created by this
+ // audio manger.
+ base::Thread audio_thread_;
+ scoped_ptr<AlsaWrapper> wrapper_;
+
+ std::map<AlsaPcmOutputStream*, scoped_refptr<AlsaPcmOutputStream> >
+ active_streams_;
+
+ bool initialized_;
+
DISALLOW_COPY_AND_ASSIGN(AudioManagerLinux);
};