diff options
Diffstat (limited to 'media/audio')
-rw-r--r-- | media/audio/audio_output.h | 4 | ||||
-rw-r--r-- | media/audio/linux/alsa_output.cc | 861 | ||||
-rw-r--r-- | media/audio/linux/alsa_output.h | 248 | ||||
-rw-r--r-- | media/audio/linux/alsa_output_unittest.cc | 379 | ||||
-rw-r--r-- | media/audio/linux/alsa_wrapper.cc | 60 | ||||
-rw-r--r-- | media/audio/linux/alsa_wrapper.h | 38 | ||||
-rw-r--r-- | media/audio/linux/audio_manager_linux.cc | 38 | ||||
-rw-r--r-- | media/audio/linux/audio_manager_linux.h | 20 |
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); }; |