blob: cba62a5d5d182890b3e9bd59798b34b3ed01a6fb [file] [log] [blame]
// Copyright (c) 2010 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 "chrome/browser/chromeos/audio_mixer_pulse.h"
#include <pulse/pulseaudio.h>
#include "base/logging.h"
#include "base/message_loop.h"
#include "base/task.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/browser_thread.h"
namespace chromeos {
// Using asynchronous versions of the threaded PulseAudio API, as well
// as a worker thread so gets, sets, and the init sequence do not block the
// calling thread. GetVolume() and IsMute() can still be called synchronously
// if needed, but take a bit longer (~2ms vs ~0.3ms).
//
// Set calls just return without waiting. If you must guarantee the value has
// been set before continuing, immediately call the blocking Get version to
// synchronously get the value back.
//
// TODO(davej): Serialize volume/mute to preserve settings when restarting?
namespace {
const int kInvalidDeviceId = -1;
const double kMinVolumeDb = -90.0;
// Choosing 6.0dB here instead of 0dB to give user chance to amplify audio some
// in case sounds or their setup is too quiet for them.
const double kMaxVolumeDb = 6.0;
// Used for passing custom data to the PulseAudio callbacks.
struct CallbackWrapper {
AudioMixerPulse* instance;
bool done;
void* userdata;
};
} // namespace
// AudioInfo contains all the values we care about when getting info for a
// Sink (output device) used by GetAudioInfo().
struct AudioMixerPulse::AudioInfo {
pa_cvolume cvolume;
bool muted;
};
AudioMixerPulse::AudioMixerPulse()
: device_id_(kInvalidDeviceId),
last_channels_(0),
mainloop_lock_count_(0),
mixer_state_(UNINITIALIZED),
pa_context_(NULL),
pa_mainloop_(NULL) {
}
AudioMixerPulse::~AudioMixerPulse() {
bool run_all_pending = false;
{
base::AutoLock lock(mixer_state_lock_);
// DoInit() has not even been called yet, but is still in the thread message
// queue. Force it out of the queue and exit DoInit() right away.
if ((!pa_mainloop_) && (mixer_state_ == INITIALIZING)) {
mixer_state_ = UNINITIALIZED;
run_all_pending = true;
}
}
if (run_all_pending)
thread_->message_loop()->RunAllPending();
PulseAudioFree();
if (thread_ != NULL) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// A ScopedAllowIO object is required to join the thread when calling Stop.
// The worker thread should be idle at this time.
// See http://crosbug.com/11110 for discussion.
base::ThreadRestrictions::ScopedAllowIO allow_io_for_thread_join;
thread_->message_loop()->AssertIdle();
thread_->Stop();
thread_.reset();
}
}
void AudioMixerPulse::Init(InitDoneCallback* callback) {
DCHECK(callback);
if (!InitThread()) {
callback->Run(false);
delete callback;
return;
}
// Post the task of starting up, which can block for 200-500ms,
// so best not to do it on the caller's thread.
thread_->message_loop()->PostTask(FROM_HERE,
NewRunnableMethod(this, &AudioMixerPulse::DoInit, callback));
}
bool AudioMixerPulse::InitSync() {
if (!InitThread())
return false;
return PulseAudioInit();
}
double AudioMixerPulse::GetVolumeDb() const {
if (!MainloopLockIfReady())
return AudioMixer::kSilenceDb;
AudioInfo data;
GetAudioInfo(&data);
MainloopUnlock();
return pa_sw_volume_to_dB(data.cvolume.values[0]);
}
bool AudioMixerPulse::GetVolumeLimits(double* vol_min, double* vol_max) {
if (vol_min)
*vol_min = kMinVolumeDb;
if (vol_max)
*vol_max = kMaxVolumeDb;
return true;
}
void AudioMixerPulse::SetVolumeDb(double vol_db) {
if (!MainloopLockIfReady())
return;
// last_channels_ determines the number of channels on the main output device,
// and is used later to set the volume on all channels at once.
if (!last_channels_) {
AudioInfo data;
GetAudioInfo(&data);
last_channels_ = data.cvolume.channels;
}
pa_operation* pa_op;
pa_cvolume cvolume;
pa_cvolume_set(&cvolume, last_channels_, pa_sw_volume_from_dB(vol_db));
pa_op = pa_context_set_sink_volume_by_index(pa_context_, device_id_,
&cvolume, NULL, NULL);
pa_operation_unref(pa_op);
MainloopUnlock();
VLOG(1) << "Set volume to " << vol_db << " dB";
}
bool AudioMixerPulse::IsMute() const {
if (!MainloopLockIfReady())
return false;
AudioInfo data;
GetAudioInfo(&data);
MainloopUnlock();
return data.muted;
}
void AudioMixerPulse::SetMute(bool mute) {
if (!MainloopLockIfReady())
return;
pa_operation* pa_op;
pa_op = pa_context_set_sink_mute_by_index(pa_context_, device_id_,
mute ? 1 : 0, NULL, NULL);
pa_operation_unref(pa_op);
MainloopUnlock();
VLOG(1) << "Set mute to " << mute;
}
AudioMixer::State AudioMixerPulse::GetState() const {
base::AutoLock lock(mixer_state_lock_);
// If we think it's ready, verify it is actually so.
if ((mixer_state_ == READY) &&
(pa_context_get_state(pa_context_) != PA_CONTEXT_READY))
mixer_state_ = IN_ERROR;
return mixer_state_;
}
////////////////////////////////////////////////////////////////////////////////
// Private functions follow
void AudioMixerPulse::DoInit(InitDoneCallback* callback) {
bool success = PulseAudioInit();
callback->Run(success);
delete callback;
}
bool AudioMixerPulse::InitThread() {
base::AutoLock lock(mixer_state_lock_);
if (mixer_state_ != UNINITIALIZED)
return false;
if (thread_ == NULL) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
thread_.reset(new base::Thread("AudioMixerPulse"));
if (!thread_->Start()) {
thread_.reset();
return false;
}
}
mixer_state_ = INITIALIZING;
return true;
}
// static
void AudioMixerPulse::ConnectToPulseCallbackThunk(
pa_context* context, void* userdata) {
CallbackWrapper* data =
static_cast<CallbackWrapper*>(userdata);
data->instance->OnConnectToPulseCallback(context, &data->done);
}
void AudioMixerPulse::OnConnectToPulseCallback(
pa_context* context, bool* connect_done) {
pa_context_state_t state = pa_context_get_state(context);
if (state == PA_CONTEXT_READY ||
state == PA_CONTEXT_FAILED ||
state == PA_CONTEXT_TERMINATED) {
// Connection process has reached a terminal state. Wake PulseAudioInit().
*connect_done = true;
MainloopSignal();
}
}
bool AudioMixerPulse::PulseAudioInit() {
pa_context_state_t state = PA_CONTEXT_FAILED;
{
base::AutoLock lock(mixer_state_lock_);
if (mixer_state_ != INITIALIZING)
return false;
pa_mainloop_ = pa_threaded_mainloop_new();
if (!pa_mainloop_) {
LOG(ERROR) << "Can't create PulseAudio mainloop";
mixer_state_ = UNINITIALIZED;
return false;
}
if (pa_threaded_mainloop_start(pa_mainloop_) != 0) {
LOG(ERROR) << "Can't start PulseAudio mainloop";
pa_threaded_mainloop_free(pa_mainloop_);
mixer_state_ = UNINITIALIZED;
return false;
}
}
while (true) {
// Create connection to default server.
if (!MainloopSafeLock())
return false;
while (true) {
pa_mainloop_api* pa_mlapi = pa_threaded_mainloop_get_api(pa_mainloop_);
if (!pa_mlapi) {
LOG(ERROR) << "Can't get PulseAudio mainloop api";
break;
}
// This one takes the most time if run at app startup.
pa_context_ = pa_context_new(pa_mlapi, "ChromeAudio");
if (!pa_context_) {
LOG(ERROR) << "Can't create new PulseAudio context";
break;
}
MainloopUnlock();
if (!MainloopSafeLock())
return false;
CallbackWrapper data = {this, false, NULL};
pa_context_set_state_callback(pa_context_,
&ConnectToPulseCallbackThunk,
&data);
if (pa_context_connect(pa_context_, NULL,
PA_CONTEXT_NOAUTOSPAWN, NULL) != 0) {
LOG(ERROR) << "Can't start connection to PulseAudio sound server";
} else {
// Wait until we have a completed connection or fail.
do {
MainloopWait();
} while (!data.done);
state = pa_context_get_state(pa_context_);
if (state == PA_CONTEXT_FAILED) {
LOG(ERROR) << "PulseAudio connection failed (daemon not running?)";
} else if (state == PA_CONTEXT_TERMINATED) {
LOG(ERROR) << "PulseAudio connection terminated early";
} else if (state != PA_CONTEXT_READY) {
LOG(ERROR) << "Unknown problem connecting to PulseAudio";
}
}
pa_context_set_state_callback(pa_context_, NULL, NULL);
break;
}
MainloopUnlock();
if (state != PA_CONTEXT_READY)
break;
if (!MainloopSafeLock())
return false;
GetDefaultPlaybackDevice();
MainloopUnlock();
if (device_id_ == kInvalidDeviceId)
break;
{
base::AutoLock lock(mixer_state_lock_);
if (mixer_state_ == SHUTTING_DOWN)
return false;
mixer_state_ = READY;
}
return true;
}
// Failed startup sequence, clean up now.
PulseAudioFree();
return false;
}
void AudioMixerPulse::PulseAudioFree() {
{
base::AutoLock lock(mixer_state_lock_);
if (!pa_mainloop_)
mixer_state_ = UNINITIALIZED;
if ((mixer_state_ == UNINITIALIZED) || (mixer_state_ == SHUTTING_DOWN))
return;
// If still initializing on another thread, this will cause it to exit.
mixer_state_ = SHUTTING_DOWN;
}
DCHECK(pa_mainloop_);
MainloopLock();
if (pa_context_) {
pa_context_disconnect(pa_context_);
pa_context_unref(pa_context_);
pa_context_ = NULL;
}
MainloopUnlock();
pa_threaded_mainloop_stop(pa_mainloop_);
pa_threaded_mainloop_free(pa_mainloop_);
pa_mainloop_ = NULL;
{
base::AutoLock lock(mixer_state_lock_);
mixer_state_ = UNINITIALIZED;
}
}
void AudioMixerPulse::CompleteOperation(pa_operation* pa_op,
bool* done) const {
// After starting any operation, this helper checks if it started OK, then
// waits for it to complete by iterating through the mainloop until the
// operation is not running anymore.
CHECK(pa_op);
while (pa_operation_get_state(pa_op) == PA_OPERATION_RUNNING) {
// If operation still running, but we got what we needed, cancel it now.
if (*done) {
pa_operation_cancel(pa_op);
break;
}
MainloopWait();
}
pa_operation_unref(pa_op);
}
// Must be called with mainloop lock held
void AudioMixerPulse::GetDefaultPlaybackDevice() {
DCHECK_GT(mainloop_lock_count_, 0);
DCHECK(pa_context_);
DCHECK(pa_context_get_state(pa_context_) == PA_CONTEXT_READY);
CallbackWrapper data = {this, false, NULL};
pa_operation* pa_op = pa_context_get_sink_info_list(pa_context_,
EnumerateDevicesCallback,
&data);
CompleteOperation(pa_op, &data.done);
return;
}
void AudioMixerPulse::OnEnumerateDevices(const pa_sink_info* sink_info,
int eol, bool* done) {
if (device_id_ != kInvalidDeviceId)
return;
// TODO(davej): Should we handle cases of more than one output sink device?
// eol is < 0 for error, > 0 for end of list, ==0 while listing.
if (eol == 0) {
device_id_ = sink_info->index;
}
*done = true;
MainloopSignal();
}
// static
void AudioMixerPulse::EnumerateDevicesCallback(pa_context* unused,
const pa_sink_info* sink_info,
int eol,
void* userdata) {
CallbackWrapper* data =
static_cast<CallbackWrapper*>(userdata);
data->instance->OnEnumerateDevices(sink_info, eol, &data->done);
}
// Must be called with lock held
void AudioMixerPulse::GetAudioInfo(AudioInfo* info) const {
DCHECK_GT(mainloop_lock_count_, 0);
CallbackWrapper data = {const_cast<AudioMixerPulse*>(this), false, info};
pa_operation* pa_op = pa_context_get_sink_info_by_index(pa_context_,
device_id_,
GetAudioInfoCallback,
&data);
CompleteOperation(pa_op, &data.done);
}
// static
void AudioMixerPulse::GetAudioInfoCallback(pa_context* unused,
const pa_sink_info* sink_info,
int eol,
void* userdata) {
CallbackWrapper* data = static_cast<CallbackWrapper*>(userdata);
AudioInfo* info = static_cast<AudioInfo*>(data->userdata);
// Copy just the information we care about.
if (eol == 0) {
info->cvolume = sink_info->volume;
info->muted = sink_info->mute ? true : false;
data->done = true;
}
data->instance->MainloopSignal();
}
inline void AudioMixerPulse::MainloopLock() const {
pa_threaded_mainloop_lock(pa_mainloop_);
++mainloop_lock_count_;
}
inline void AudioMixerPulse::MainloopUnlock() const {
--mainloop_lock_count_;
pa_threaded_mainloop_unlock(pa_mainloop_);
}
// Must be called with the lock held.
inline void AudioMixerPulse::MainloopWait() const {
DCHECK_GT(mainloop_lock_count_, 0);
pa_threaded_mainloop_wait(pa_mainloop_);
}
// Must be called with the lock held.
inline void AudioMixerPulse::MainloopSignal() const {
DCHECK_GT(mainloop_lock_count_, 0);
pa_threaded_mainloop_signal(pa_mainloop_, 0);
}
inline bool AudioMixerPulse::MainloopSafeLock() const {
base::AutoLock lock(mixer_state_lock_);
if ((mixer_state_ == SHUTTING_DOWN) || (!pa_mainloop_))
return false;
pa_threaded_mainloop_lock(pa_mainloop_);
++mainloop_lock_count_;
return true;
}
inline bool AudioMixerPulse::MainloopLockIfReady() const {
base::AutoLock lock(mixer_state_lock_);
if (mixer_state_ != READY)
return false;
if (!pa_mainloop_)
return false;
pa_threaded_mainloop_lock(pa_mainloop_);
++mainloop_lock_count_;
return true;
}
} // namespace chromeos