summaryrefslogtreecommitdiffstats
path: root/media/audio
diff options
context:
space:
mode:
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);
};