diff options
author | slock@chromium.org <slock@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-08-19 01:42:47 +0000 |
---|---|---|
committer | slock@chromium.org <slock@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-08-19 01:42:47 +0000 |
commit | ad6e9d1274c2a33ebf9170f7757f462fea36fae1 (patch) | |
tree | 85c726d73ebb496cae1173759678d55af300b7fe /media/audio/linux | |
parent | c4ed802942d46ce11a6d8728bb7b02110e6076a8 (diff) | |
download | chromium_src-ad6e9d1274c2a33ebf9170f7757f462fea36fae1.zip chromium_src-ad6e9d1274c2a33ebf9170f7757f462fea36fae1.tar.gz chromium_src-ad6e9d1274c2a33ebf9170f7757f462fea36fae1.tar.bz2 |
PulseAudio Sound Playback on Linux
This is the preliminary implementation of a PulseAudio sound backend for Chrome on Linux. At first, PulseAudio's mainloop, mainloop_api, and context constructs will be used instead of the message loop system used in alsa_output. This will be stereo only at first. Also, at first, PulseAudio will be dynamically linked in media.gyp as opposed to the final solution which will dynamically link PulseAudio in runtime if it is available.
BUG=32757
TEST=
Review URL: http://codereview.chromium.org/7473021
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@97408 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media/audio/linux')
-rw-r--r-- | media/audio/linux/alsa_output_unittest.cc | 2 | ||||
-rw-r--r-- | media/audio/linux/audio_manager_linux.cc | 29 | ||||
-rw-r--r-- | media/audio/linux/audio_manager_linux.h | 5 | ||||
-rw-r--r-- | media/audio/linux/pulse_output.cc | 349 | ||||
-rw-r--r-- | media/audio/linux/pulse_output.h | 130 |
5 files changed, 502 insertions, 13 deletions
diff --git a/media/audio/linux/alsa_output_unittest.cc b/media/audio/linux/alsa_output_unittest.cc index 7ccecc7..9fe2b92 100644 --- a/media/audio/linux/alsa_output_unittest.cc +++ b/media/audio/linux/alsa_output_unittest.cc @@ -85,7 +85,7 @@ class MockAudioManagerLinux : public AudioManagerLinux { MOCK_METHOD0(MuteAll, void()); MOCK_METHOD0(UnMuteAll, void()); - MOCK_METHOD1(ReleaseOutputStream, void(AlsaPcmOutputStream* stream)); + MOCK_METHOD1(ReleaseOutputStream, void(AudioOutputStream* stream)); }; class AlsaPcmOutputStreamTest : public testing::Test { diff --git a/media/audio/linux/audio_manager_linux.cc b/media/audio/linux/audio_manager_linux.cc index 4d6e2a3..caab797 100644 --- a/media/audio/linux/audio_manager_linux.cc +++ b/media/audio/linux/audio_manager_linux.cc @@ -16,6 +16,9 @@ #include "media/audio/linux/alsa_input.h" #include "media/audio/linux/alsa_output.h" #include "media/audio/linux/alsa_wrapper.h" +#if defined(USE_PULSEAUDIO) +#include "media/audio/linux/pulse_output.h" +#endif #include "media/base/limits.h" #include "media/base/media_switches.h" @@ -99,15 +102,23 @@ AudioOutputStream* AudioManagerLinux::MakeAudioOutputStream( return NULL; } - std::string device_name = AlsaPcmOutputStream::kAutoSelectDevice; - if (CommandLine::ForCurrentProcess()->HasSwitch( - switches::kAlsaOutputDevice)) { - device_name = CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - switches::kAlsaOutputDevice); + AudioOutputStream* stream; +#if defined(USE_PULSEAUDIO) + if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kUsePulseAudio)) { + stream = new PulseAudioOutputStream(params, this, GetMessageLoop()); + } else { +#endif + std::string device_name = AlsaPcmOutputStream::kAutoSelectDevice; + if (CommandLine::ForCurrentProcess()->HasSwitch( + switches::kAlsaOutputDevice)) { + device_name = CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kAlsaOutputDevice); + } + stream = new AlsaPcmOutputStream(device_name, params, wrapper_.get(), this, + GetMessageLoop()); +#if defined(USE_PULSEAUDIO) } - AlsaPcmOutputStream* stream = - new AlsaPcmOutputStream(device_name, params, wrapper_.get(), this, - GetMessageLoop()); +#endif active_streams_.insert(stream); return stream; } @@ -170,7 +181,7 @@ void AudioManagerLinux::UnMuteAll() { NOTIMPLEMENTED(); } -void AudioManagerLinux::ReleaseOutputStream(AlsaPcmOutputStream* stream) { +void AudioManagerLinux::ReleaseOutputStream(AudioOutputStream* stream) { if (stream) { active_streams_.erase(stream); delete stream; diff --git a/media/audio/linux/audio_manager_linux.h b/media/audio/linux/audio_manager_linux.h index 6e06091..c50cabd 100644 --- a/media/audio/linux/audio_manager_linux.h +++ b/media/audio/linux/audio_manager_linux.h @@ -11,7 +11,6 @@ #include "base/threading/thread.h" #include "media/audio/audio_manager_base.h" -class AlsaPcmOutputStream; class AlsaWrapper; class AudioManagerLinux : public AudioManagerBase { @@ -34,7 +33,7 @@ class AudioManagerLinux : public AudioManagerBase { virtual void MuteAll(); virtual void UnMuteAll(); - virtual void ReleaseOutputStream(AlsaPcmOutputStream* stream); + virtual void ReleaseOutputStream(AudioOutputStream* stream); protected: virtual ~AudioManagerLinux(); @@ -45,7 +44,7 @@ class AudioManagerLinux : public AudioManagerBase { scoped_ptr<AlsaWrapper> wrapper_; - std::set<AlsaPcmOutputStream*> active_streams_; + std::set<AudioOutputStream*> active_streams_; DISALLOW_COPY_AND_ASSIGN(AudioManagerLinux); }; diff --git a/media/audio/linux/pulse_output.cc b/media/audio/linux/pulse_output.cc new file mode 100644 index 0000000..a405538 --- /dev/null +++ b/media/audio/linux/pulse_output.cc @@ -0,0 +1,349 @@ +// Copyright (c) 2011 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/pulse_output.h" + +#include "base/message_loop.h" +#include "media/audio/audio_parameters.h" +#include "media/audio/audio_util.h" +#include "media/audio/linux/audio_manager_linux.h" +#include "media/base/data_buffer.h" +#include "media/base/seekable_buffer.h" + +static pa_sample_format_t BitsToFormat(int bits_per_sample) { + switch (bits_per_sample) { + // Unsupported sample formats shown for reference. I am assuming we want + // signed and little endian because that is what we gave to ALSA. + case 8: + return PA_SAMPLE_U8; + // Also 8-bits: PA_SAMPLE_ALAW and PA_SAMPLE_ULAW + case 16: + return PA_SAMPLE_S16LE; + // Also 16-bits: PA_SAMPLE_S16BE (big endian). + case 24: + return PA_SAMPLE_S24LE; + // Also 24-bits: PA_SAMPLE_S24BE (big endian). + // Other cases: PA_SAMPLE_24_32LE (in LSB of 32-bit field, little endian), + // and PA_SAMPLE_24_32BE (in LSB of 32-bit field, big endian), + case 32: + return PA_SAMPLE_S32LE; + // Also 32-bits: PA_SAMPLE_S32BE (big endian), + // PA_SAMPLE_FLOAT32LE (floating point little endian), + // and PA_SAMPLE_FLOAT32BE (floating point big endian). + default: + return PA_SAMPLE_INVALID; + } +} + +static size_t MicrosecondsToBytes( + uint32 microseconds, uint32 sample_rate, size_t bytes_per_frame) { + return microseconds * sample_rate * bytes_per_frame / + base::Time::kMicrosecondsPerSecond; +} + +void PulseAudioOutputStream::ContextStateCallback(pa_context* context, + void* state_addr) { + pa_context_state_t* state = static_cast<pa_context_state_t*>(state_addr); + *state = pa_context_get_state(context); +} + +void PulseAudioOutputStream::WriteRequestCallback( + pa_stream* playback_handle, size_t length, void* stream_addr) { + PulseAudioOutputStream* stream = + static_cast<PulseAudioOutputStream*>(stream_addr); + + DCHECK_EQ(stream->message_loop_, MessageLoop::current()); + + stream->write_callback_handled_ = true; + + // Fulfill write request. + stream->FulfillWriteRequest(length); +} + +PulseAudioOutputStream::PulseAudioOutputStream(const AudioParameters& params, + AudioManagerLinux* manager, + MessageLoop* message_loop) + : channel_layout_(params.channel_layout), + channel_count_(ChannelLayoutToChannelCount(channel_layout_)), + sample_format_(BitsToFormat(params.bits_per_sample)), + sample_rate_(params.sample_rate), + bytes_per_frame_(params.channels * params.bits_per_sample / 8), + manager_(manager), + pa_context_(NULL), + pa_mainloop_(NULL), + playback_handle_(NULL), + packet_size_(params.GetPacketSize()), + frames_per_packet_(packet_size_ / bytes_per_frame_), + client_buffer_(NULL), + volume_(1.0f), + stream_stopped_(true), + write_callback_handled_(false), + message_loop_(message_loop), + ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), + source_callback_(NULL) { + DCHECK_EQ(message_loop_, MessageLoop::current()); + DCHECK(manager_); + + // TODO(slock): Sanity check input values. +} + +PulseAudioOutputStream::~PulseAudioOutputStream() { + // All internal structures should already have been freed in Close(), + // which calls AudioManagerLinux::Release which deletes this object. + DCHECK(!playback_handle_); + DCHECK(!pa_context_); + DCHECK(!pa_mainloop_); +} + +bool PulseAudioOutputStream::Open() { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + // TODO(slock): Possibly move most of this to an OpenPlaybackDevice function + // in a new class 'pulse_util', like alsa_util. + + // Create a mainloop API and connect to the default server. + pa_mainloop_ = pa_mainloop_new(); + pa_mainloop_api* pa_mainloop_api = pa_mainloop_get_api(pa_mainloop_); + pa_context_ = pa_context_new(pa_mainloop_api, "Chromium"); + pa_context_state_t pa_context_state = PA_CONTEXT_UNCONNECTED; + pa_context_connect(pa_context_, NULL, PA_CONTEXT_NOFLAGS, NULL); + + // Wait until PulseAudio is ready. + pa_context_set_state_callback(pa_context_, &ContextStateCallback, + &pa_context_state); + while (pa_context_state != PA_CONTEXT_READY) { + pa_mainloop_iterate(pa_mainloop_, 1, NULL); + if (pa_context_state == PA_CONTEXT_FAILED || + pa_context_state == PA_CONTEXT_TERMINATED) { + Reset(); + return false; + } + } + + // Set sample specifications and open playback stream. + pa_sample_spec pa_sample_specifications; + pa_sample_specifications.format = sample_format_; + pa_sample_specifications.rate = sample_rate_; + pa_sample_specifications.channels = channel_count_; + playback_handle_ = pa_stream_new(pa_context_, "Playback", + &pa_sample_specifications, NULL); + + // Initialize client buffer. + uint32 output_packet_size = frames_per_packet_ * bytes_per_frame_; + client_buffer_.reset(new media::SeekableBuffer(0, output_packet_size)); + + // Set write callback. + pa_stream_set_write_callback(playback_handle_, &WriteRequestCallback, this); + + // Set server-side buffer attributes. + // (uint32_t)-1 is the default and recommended value from PulseAudio's + // documentation, found at: + // http://freedesktop.org/software/pulseaudio/doxygen/structpa__buffer__attr.html. + pa_buffer_attr pa_buffer_attributes; + pa_buffer_attributes.maxlength = static_cast<uint32_t>(-1); + pa_buffer_attributes.tlength = output_packet_size; + pa_buffer_attributes.prebuf = static_cast<uint32_t>(-1); + pa_buffer_attributes.minreq = static_cast<uint32_t>(-1); + pa_buffer_attributes.fragsize = static_cast<uint32_t>(-1); + + // Connect playback stream. + pa_stream_connect_playback(playback_handle_, NULL, + &pa_buffer_attributes, + (pa_stream_flags_t) + (PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_ADJUST_LATENCY | + PA_STREAM_AUTO_TIMING_UPDATE), + NULL, NULL); + + if (!playback_handle_) { + Reset(); + return false; + } + + return true; +} + +void PulseAudioOutputStream::Reset() { + stream_stopped_ = true; + + // Close the stream. + if (playback_handle_) { + pa_stream_flush(playback_handle_, NULL, NULL); + pa_stream_disconnect(playback_handle_); + + // Release PulseAudio structures. + pa_stream_unref(playback_handle_); + playback_handle_ = NULL; + } + if (pa_context_) { + pa_context_unref(pa_context_); + pa_context_ = NULL; + } + if (pa_mainloop_) { + pa_mainloop_free(pa_mainloop_); + pa_mainloop_ = NULL; + } + + // Release internal buffer. + client_buffer_.reset(); +} + +void PulseAudioOutputStream::Close() { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + Reset(); + + // Signal to the manager that we're closed and can be removed. + // This should be the last call in the function as it deletes "this". + manager_->ReleaseOutputStream(this); +} + +void PulseAudioOutputStream::WaitForWriteRequest() { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + if (stream_stopped_) + return; + + // Iterate the PulseAudio mainloop. If PulseAudio doesn't request a write, + // post a task to iterate the mainloop again. + write_callback_handled_ = false; + pa_mainloop_iterate(pa_mainloop_, 1, NULL); + if (!write_callback_handled_) { + message_loop_->PostTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &PulseAudioOutputStream::WaitForWriteRequest)); + } +} + +bool PulseAudioOutputStream::BufferPacketFromSource() { + uint32 buffer_delay = client_buffer_->forward_bytes(); + pa_usec_t pa_latency_micros; + int negative; + pa_stream_get_latency(playback_handle_, &pa_latency_micros, &negative); + uint32 hardware_delay = MicrosecondsToBytes(pa_latency_micros, + sample_rate_, + bytes_per_frame_); + // TODO(slock): Deal with negative latency (negative == 1). This has yet + // to happen in practice though. + scoped_refptr<media::DataBuffer> packet = + new media::DataBuffer(packet_size_); + size_t packet_size = RunDataCallback(packet->GetWritableData(), + packet->GetBufferSize(), + AudioBuffersState(buffer_delay, + hardware_delay)); + + if (packet_size == 0) + return false; + + // TODO(slock): Swizzling and downmixing. + media::AdjustVolume(packet->GetWritableData(), + packet_size, + channel_count_, + bytes_per_frame_ / channel_count_, + volume_); + packet->SetDataSize(packet_size); + // Add the packet to the buffer. + client_buffer_->Append(packet); + return true; +} + +void PulseAudioOutputStream::FulfillWriteRequest(size_t requested_bytes) { + // If we have enough data to fulfill the request, we can finish the write. + if (stream_stopped_) + return; + + // Request more data from the source until we can fulfill the request or + // fail to receive anymore data. + bool buffering_successful = true; + while (client_buffer_->forward_bytes() < requested_bytes && + buffering_successful) { + buffering_successful = BufferPacketFromSource(); + } + + size_t bytes_written = 0; + if (client_buffer_->forward_bytes() > 0) { + // Try to fulfill the request by writing as many of the requested bytes to + // the stream as we can. + WriteToStream(requested_bytes, &bytes_written); + } + + if (bytes_written < requested_bytes) { + // We weren't able to buffer enough data to fulfill the request. Try to + // fulfill the rest of the request later. + message_loop_->PostTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &PulseAudioOutputStream::FulfillWriteRequest, + requested_bytes - bytes_written)); + } else { + // Continue playback. + message_loop_->PostTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &PulseAudioOutputStream::WaitForWriteRequest)); + } +} + +void PulseAudioOutputStream::WriteToStream(size_t bytes_to_write, + size_t* bytes_written) { + *bytes_written = 0; + while (*bytes_written < bytes_to_write) { + const uint8* chunk; + size_t chunk_size; + + // Stop writing if there is no more data available. + if (!client_buffer_->GetCurrentChunk(&chunk, &chunk_size)) + break; + + // Write data to stream. + pa_stream_write(playback_handle_, chunk, chunk_size, + NULL, 0LL, PA_SEEK_RELATIVE); + client_buffer_->Seek(chunk_size); + *bytes_written += chunk_size; + } +} + +void PulseAudioOutputStream::Start(AudioSourceCallback* callback) { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + CHECK(callback); + source_callback_ = callback; + + // Clear buffer, it might still have data in it. + client_buffer_->Clear(); + stream_stopped_ = false; + + // Start playback. + message_loop_->PostTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &PulseAudioOutputStream::WaitForWriteRequest)); +} + +void PulseAudioOutputStream::Stop() { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + stream_stopped_ = true; +} + +void PulseAudioOutputStream::SetVolume(double volume) { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + volume_ = static_cast<float>(volume); +} + +void PulseAudioOutputStream::GetVolume(double* volume) { + DCHECK_EQ(message_loop_, MessageLoop::current()); + + *volume = volume_; +} + +uint32 PulseAudioOutputStream::RunDataCallback( + uint8* dest, uint32 max_size, AudioBuffersState buffers_state) { + if (source_callback_) + return source_callback_->OnMoreData(this, dest, max_size, buffers_state); + + return 0; +} diff --git a/media/audio/linux/pulse_output.h b/media/audio/linux/pulse_output.h new file mode 100644 index 0000000..7da9228 --- /dev/null +++ b/media/audio/linux/pulse_output.h @@ -0,0 +1,130 @@ +// Copyright (c) 2011 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. +// +// Creates an audio output stream based on the PulseAudio asynchronous API. +// +// If the stream is successfully opened, Close() must be called before the +// stream is deleted as Close() is responsible for ensuring resource cleanup +// occurs. +// +// This object is designed so that all AudioOutputStream methods will be called +// on the same thread that created the object. +// +// WARNING: This object blocks on internal PulseAudio calls in Open() while +// waiting for PulseAudio's context structure to be ready. It also blocks in +// inside PulseAudio in Start() and repeated during playback, waiting for +// PulseAudio write callbacks to occur. + +#ifndef MEDIA_AUDIO_LINUX_PULSE_OUTPUT_H_ +#define MEDIA_AUDIO_LINUX_PULSE_OUTPUT_H_ + +#include <pulse/pulseaudio.h> + +#include "base/memory/scoped_ptr.h" +#include "base/task.h" +#include "media/audio/audio_io.h" +#include "media/base/channel_layout.h" + +namespace media { +class SeekableBuffer; +} + +class AudioManagerLinux; +struct AudioParameters; +class MessageLoop; + +class PulseAudioOutputStream : public AudioOutputStream { + public: + PulseAudioOutputStream(const AudioParameters& params, + AudioManagerLinux* manager, + MessageLoop* message_loop); + + virtual ~PulseAudioOutputStream(); + + // Implementation of AudioOutputStream. + virtual bool Open(); + virtual void Close(); + virtual void Start(AudioSourceCallback* callback); + virtual void Stop(); + virtual void SetVolume(double volume); + virtual void GetVolume(double* volume); + + private: + // PulseAudio Callbacks. + static void ContextStateCallback(pa_context* context, void* state_addr); + static void WriteRequestCallback(pa_stream* playback_handle, size_t length, + void* stream_addr); + + // Iterate the PulseAudio mainloop to get write requests. + void WaitForWriteRequest(); + + // Get another packet from the data source and write it to the client buffer. + bool BufferPacketFromSource(); + + // Fulfill a write request from the write request callback. If the write + // can't be finished a first, post a new attempt to the message loop. + void FulfillWriteRequest(size_t requested_bytes); + + // Write data from the client buffer to the PulseAudio stream. + void WriteToStream(size_t bytes_to_write, size_t* bytes_written); + + // API for Proxying calls to the AudioSourceCallback provided during Start(). + uint32 RunDataCallback(uint8* dest, uint32 max_size, + AudioBuffersState buffers_state); + + // Close() helper function to free internal structs. + void Reset(); + + // Configuration constants from the constructor. Referencable by all threads + // since they are constants. + const ChannelLayout channel_layout_; + const uint32 channel_count_; + const pa_sample_format_t sample_format_; + const uint32 sample_rate_; + const uint32 bytes_per_frame_; + + // Audio manager that created us. Used to report that we've closed. + AudioManagerLinux* manager_; + + // PulseAudio API structs. + pa_context* pa_context_; + pa_mainloop* pa_mainloop_; + + // Handle to the actual PulseAudio playback stream. + pa_stream* playback_handle_; + + // Device configuration data. Populated after Open() completes. + uint32 packet_size_; + uint32 frames_per_packet_; + + // Client side audio buffer feeding pulse audio's server side buffer. + scoped_ptr<media::SeekableBuffer> client_buffer_; + + // Float representation of volume from 0.0 to 1.0. + float volume_; + + // Flag indicating the code should stop reading from the data source or + // writing to the PulseAudio server. This is set because the device has + // entered an unrecoverable error state, or the Close() has executed. + bool stream_stopped_; + + // Whether or not PulseAudio has called the WriteCallback for the most recent + // set of pa_mainloop iterations. + bool write_callback_handled_; + + // Message loop used to post WaitForWriteTasks. Used to prevent blocking on + // the audio thread while waiting for PulseAudio write callbacks. + MessageLoop* message_loop_; + + // Allows us to run tasks on the PulseAudioOutputStream instance which are + // bound by its lifetime. + ScopedRunnableMethodFactory<PulseAudioOutputStream> method_factory_; + + // Callback to audio data source. + AudioSourceCallback* source_callback_; + + DISALLOW_COPY_AND_ASSIGN(PulseAudioOutputStream); +}; + +#endif // MEDIA_AUDIO_LINUX_PULSE_OUTPUT_H_ |