diff options
-rw-r--r-- | chrome/browser/chromeos/audio_handler.cc | 123 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_handler.h | 32 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_mixer.h | 67 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_mixer_alsa.cc | 366 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_mixer_alsa.h | 100 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_mixer_pulse.cc (renamed from chrome/browser/chromeos/pulse_audio_mixer.cc) | 116 | ||||
-rw-r--r-- | chrome/browser/chromeos/audio_mixer_pulse.h (renamed from chrome/browser/chromeos/pulse_audio_mixer.h) | 66 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 7 |
8 files changed, 734 insertions, 143 deletions
diff --git a/chrome/browser/chromeos/audio_handler.cc b/chrome/browser/chromeos/audio_handler.cc index f8ad726..7e5d4b9 100644 --- a/chrome/browser/chromeos/audio_handler.cc +++ b/chrome/browser/chromeos/audio_handler.cc @@ -8,13 +8,14 @@ #include "base/logging.h" #include "base/singleton.h" -#include "chrome/browser/chromeos/pulse_audio_mixer.h" +#include "chrome/browser/browser_thread.h" +#include "chrome/browser/chromeos/audio_mixer_alsa.h" +#include "chrome/browser/chromeos/audio_mixer_pulse.h" namespace chromeos { namespace { -const double kSilenceDb = -200.0; 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. @@ -27,10 +28,8 @@ const int kMaxReconnectTries = 4; } // namespace -// This class will set volume using PulseAudio to adjust volume and mute, and -// handles the volume level logic. - -// TODO(davej): Serialize volume/mute for next startup? +// This class will set volume using PulseAudio or ALSA to adjust volume and +// mute, and handles the volume level logic. double AudioHandler::GetVolumePercent() { if (!VerifyMixerConnection()) @@ -40,7 +39,7 @@ double AudioHandler::GetVolumePercent() { } // Set volume using our internal 0-100% range. Notice 0% is a special case of -// silence, so we set the mixer volume to kSilenceDb instead of kMinVolumeDb. +// silence, so we set the mixer volume to kSilenceDb instead of min_volume_db_. void AudioHandler::SetVolumePercent(double volume_percent) { if (!VerifyMixerConnection()) return; @@ -48,7 +47,7 @@ void AudioHandler::SetVolumePercent(double volume_percent) { double vol_db; if (volume_percent <= 0) - vol_db = kSilenceDb; + vol_db = AudioMixer::kSilenceDb; else vol_db = PercentToVolumeDb(volume_percent); @@ -72,7 +71,7 @@ void AudioHandler::AdjustVolumeByPercent(double adjust_by_percent) { double new_volume; if (pct <= 0.1) - new_volume = kSilenceDb; + new_volume = AudioMixer::kSilenceDb; else new_volume = PercentToVolumeDb(pct); @@ -90,24 +89,78 @@ bool AudioHandler::IsMute() { void AudioHandler::SetMute(bool do_mute) { if (!VerifyMixerConnection()) return; - DVLOG(1) << "Setting Mute to " << do_mute; - mixer_->SetMute(do_mute); } +bool AudioHandler::TryToConnect(bool async) { + if (mixer_type_ == MIXER_TYPE_PULSEAUDIO) { + VLOG(1) << "Trying to connect to PulseAudio"; + mixer_.reset(new AudioMixerPulse()); + } else if (mixer_type_ == MIXER_TYPE_ALSA) { + VLOG(1) << "Trying to connect to ALSA"; + mixer_.reset(new AudioMixerAlsa()); + } else { + VLOG(1) << "Cannot find valid volume mixer"; + mixer_.reset(); + return false; + } + + if (async) { + mixer_->Init(NewCallback(this, &AudioHandler::OnMixerInitialized)); + } else { + if (!mixer_->InitSync()) { + VLOG(1) << "Unable to reconnect to Mixer"; + return false; + } + } + return true; +} + +void AudioHandler::UseNextMixer() { + if (mixer_type_ == MIXER_TYPE_PULSEAUDIO) + mixer_type_ = MIXER_TYPE_ALSA; + else + mixer_type_ = MIXER_TYPE_NONE; +} + +static void ClipVolume(double* min_volume, double* max_volume) { + if (*min_volume < kMinVolumeDb) + *min_volume = kMinVolumeDb; + if (*max_volume > kMaxVolumeDb) + *max_volume = kMaxVolumeDb; +} + void AudioHandler::OnMixerInitialized(bool success) { connected_ = success; DVLOG(1) << "OnMixerInitialized, success = " << success; + + if (connected_) { + if (mixer_->GetVolumeLimits(&min_volume_db_, &max_volume_db_)) { + ClipVolume(&min_volume_db_, &max_volume_db_); + } + return; + } + + VLOG(1) << "Unable to connect to mixer, trying next"; + UseNextMixer(); + + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + NewRunnableMethod(this, &AudioHandler::TryToConnect, true)); } AudioHandler::AudioHandler() : connected_(false), - reconnect_tries_(0) { - mixer_.reset(new PulseAudioMixer()); - if (!mixer_->Init(NewCallback(this, &AudioHandler::OnMixerInitialized))) { - LOG(ERROR) << "Unable to connect to PulseAudio"; - } + reconnect_tries_(0), + max_volume_db_(kMaxVolumeDb), + min_volume_db_(kMinVolumeDb), + mixer_type_(MIXER_TYPE_PULSEAUDIO) { + + // Start trying to connect to mixers asychronously, starting with the current + // mixer_type_. If the connection fails, another TryToConnect() for the next + // mixer will be posted at that time. + TryToConnect(true); } AudioHandler::~AudioHandler() { @@ -115,58 +168,62 @@ AudioHandler::~AudioHandler() { }; bool AudioHandler::VerifyMixerConnection() { - PulseAudioMixer::State mixer_state = mixer_->CheckState(); - if (mixer_state == PulseAudioMixer::READY) + if (mixer_ == NULL) + return false; + + AudioMixer::State mixer_state = mixer_->GetState(); + if (mixer_state == AudioMixer::READY) return true; if (connected_) { // Something happened and the mixer is no longer valid after having been // initialized earlier. connected_ = false; - LOG(ERROR) << "Lost connection to PulseAudio"; + LOG(ERROR) << "Lost connection to mixer"; } else { LOG(ERROR) << "Mixer not valid"; } - if ((mixer_state == PulseAudioMixer::INITIALIZING) || - (mixer_state == PulseAudioMixer::SHUTTING_DOWN)) + if ((mixer_state == AudioMixer::INITIALIZING) || + (mixer_state == AudioMixer::SHUTTING_DOWN)) return false; if (reconnect_tries_ < kMaxReconnectTries) { reconnect_tries_++; - VLOG(1) << "Re-connecting to PulseAudio attempt " << reconnect_tries_ << "/" + VLOG(1) << "Re-connecting to mixer attempt " << reconnect_tries_ << "/" << kMaxReconnectTries; - mixer_.reset(new PulseAudioMixer()); - connected_ = mixer_->InitSync(); + + connected_ = TryToConnect(false); + if (connected_) { reconnect_tries_ = 0; return true; } - LOG(ERROR) << "Unable to re-connect to PulseAudio"; + LOG(ERROR) << "Unable to re-connect to mixer"; } return false; } // VolumeDbToPercent() and PercentToVolumeDb() conversion functions allow us // complete control over how the 0 to 100% range is mapped to actual loudness. -// Volume range is from kMinVolumeDb at just above 0% to kMaxVolumeDb at 100% -// with a special case at 0% which maps to kSilenceDb. +// Volume range is from min_volume_db_ at just above 0% to max_volume_db_ +// at 100% with a special case at 0% which maps to kSilenceDb. // // The mapping is confined to these two functions to make it easy to adjust and // have everything else just work. The range is biased to give finer resolution // in the higher volumes if kVolumeBias is less than 1.0. // static -double AudioHandler::VolumeDbToPercent(double volume_db) { - if (volume_db < kMinVolumeDb) +double AudioHandler::VolumeDbToPercent(double volume_db) const { + if (volume_db < min_volume_db_) return 0; - return 100.0 * pow((volume_db - kMinVolumeDb) / - (kMaxVolumeDb - kMinVolumeDb), 1/kVolumeBias); + return 100.0 * pow((volume_db - min_volume_db_) / + (max_volume_db_ - min_volume_db_), 1/kVolumeBias); } // static -double AudioHandler::PercentToVolumeDb(double volume_percent) { +double AudioHandler::PercentToVolumeDb(double volume_percent) const { return pow(volume_percent / 100.0, kVolumeBias) * - (kMaxVolumeDb - kMinVolumeDb) + kMinVolumeDb; + (max_volume_db_ - min_volume_db_) + min_volume_db_; } // static diff --git a/chrome/browser/chromeos/audio_handler.h b/chrome/browser/chromeos/audio_handler.h index 59e9d70..b835f5a 100644 --- a/chrome/browser/chromeos/audio_handler.h +++ b/chrome/browser/chromeos/audio_handler.h @@ -8,12 +8,13 @@ #include "base/basictypes.h" #include "base/scoped_ptr.h" +#include "base/threading/thread.h" template <typename T> struct DefaultSingletonTraits; namespace chromeos { -class PulseAudioMixer; +class AudioMixer; class AudioHandler { public: @@ -30,7 +31,7 @@ class AudioHandler { // as the percentage gets lower, and then switches to silence at 0%. void SetVolumePercent(double volume_percent); - // Adust volume up (positive percentage) or down (negative percentage), + // Adjust volume up (positive percentage) or down (negative percentage), // capping at 100%. GetVolumePercent() will be accurate after this // blocking call. void AdjustVolumeByPercent(double adjust_by_percent); @@ -43,10 +44,20 @@ class AudioHandler { void SetMute(bool do_mute); private: + enum MixerType { + MIXER_TYPE_PULSEAUDIO = 0, + MIXER_TYPE_ALSA, + MIXER_TYPE_NONE, + }; + // Defines the delete on exit Singleton traits we like. Best to have this // and constructor/destructor private as recommended for Singletons. friend struct DefaultSingletonTraits<AudioHandler>; + // Connect to the current mixer_type_. + bool TryToConnect(bool async); + void UseNextMixer(); + void OnMixerInitialized(bool success); AudioHandler(); @@ -54,17 +65,28 @@ class AudioHandler { bool VerifyMixerConnection(); // Conversion between our internal scaling (0-100%) and decibels. - static double VolumeDbToPercent(double volume_db); - static double PercentToVolumeDb(double volume_percent); + double VolumeDbToPercent(double volume_db) const; + double PercentToVolumeDb(double volume_percent) const; + + scoped_ptr<AudioMixer> mixer_; - scoped_ptr<PulseAudioMixer> mixer_; bool connected_; int reconnect_tries_; + // The min and max volume in decibels, limited to the maximum range of the + // audio system being used. + double max_volume_db_; + double min_volume_db_; + + // Which mixer is being used, PulseAudio, ALSA or none. + MixerType mixer_type_; + DISALLOW_COPY_AND_ASSIGN(AudioHandler); }; } // namespace chromeos +DISABLE_RUNNABLE_METHOD_REFCOUNT(chromeos::AudioHandler); + #endif // CHROME_BROWSER_CHROMEOS_AUDIO_HANDLER_H_ diff --git a/chrome/browser/chromeos/audio_mixer.h b/chrome/browser/chromeos/audio_mixer.h new file mode 100644 index 0000000..b77e827 --- /dev/null +++ b/chrome/browser/chromeos/audio_mixer.h @@ -0,0 +1,67 @@ +// 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. + +#ifndef CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_H_ +#define CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/callback.h" + +namespace chromeos { + +class AudioMixer { + public: + // Approximation of pure silence expressed in decibels. + static const double kSilenceDb = -200.0; + + enum State { + UNINITIALIZED = 0, + INITIALIZING, + READY, + SHUTTING_DOWN, + IN_ERROR, + }; + + AudioMixer() {} + virtual ~AudioMixer() {} + + // Non-Blocking call. Connect to Mixer, find a default device, then call the + // callback when complete with success code. + typedef Callback1<bool>::Type InitDoneCallback; + virtual void Init(InitDoneCallback* callback) = 0; + + // Call may block. Mixer will be connected before returning, unless error. + virtual bool InitSync() = 0; + + // Call may block. Returns a default of kSilenceDb on error. + virtual double GetVolumeDb() const = 0; + + // Non-Blocking call. Returns the actual volume limits possible according + // to the mixer. Limits are left unchanged on error + virtual bool GetVolumeLimits(double* vol_min, double* vol_max) = 0; + + // Non-blocking call. + virtual void SetVolumeDb(double vol_db) = 0; + + // Call may block. Gets the mute state of the default device (true == mute). + // Returns a default of false on error. + virtual bool IsMute() const = 0; + + // Non-Blocking call. + virtual void SetMute(bool mute) = 0; + + // Returns READY if we have a valid working connection to the Mixer. + // This can return IN_ERROR if we lose the connection, even after an original + // successful init. Non-blocking call. + virtual State GetState() const = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(AudioMixer); +}; + +} // namespace chromeos + +#endif // CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_H_ + diff --git a/chrome/browser/chromeos/audio_mixer_alsa.cc b/chrome/browser/chromeos/audio_mixer_alsa.cc new file mode 100644 index 0000000..7f96fa4 --- /dev/null +++ b/chrome/browser/chromeos/audio_mixer_alsa.cc @@ -0,0 +1,366 @@ +// 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_alsa.h" + +#include <alsa/asoundlib.h> + +#include "base/logging.h" +#include "base/task.h" + +namespace chromeos { + +// Connect to the ALSA mixer using their simple element API. Init is performed +// asynchronously on the worker thread. +// +// To get a wider range and finer control over volume levels, first the Master +// level is set, then if the PCM element exists, the total level is refined by +// adjusting that as well. If the PCM element has more volume steps, it allows +// for finer granularity in the total volume. + +// TODO(davej): Serialize volume/mute to preserve settings when restarting. + +typedef long alsa_long_t; // 'long' is required for ALSA API calls. + +namespace { + +const char* kMasterVolume = "Master"; +const char* kPCMVolume = "PCM"; +const double kDefaultMinVolume = -90.0; +const double kDefaultMaxVolume = 0.0; + +} // namespace + +AudioMixerAlsa::AudioMixerAlsa() + : min_volume_(kDefaultMinVolume), + max_volume_(kDefaultMaxVolume), + save_volume_(0), + mixer_state_(UNINITIALIZED), + alsa_mixer_(NULL), + elem_master_(NULL), + elem_pcm_(NULL) { +} + +AudioMixerAlsa::~AudioMixerAlsa() { + FreeAlsaMixer(); + if (thread_ != NULL) { + thread_->Stop(); + thread_.reset(); + } +} + +void AudioMixerAlsa::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, &AudioMixerAlsa::DoInit, callback)); +} + +bool AudioMixerAlsa::InitSync() { + if (!InitThread()) + return false; + return InitializeAlsaMixer(); +} + +double AudioMixerAlsa::GetVolumeDb() const { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != READY) + return kSilenceDb; + + return DoGetVolumeDb_Locked(); +} + +bool AudioMixerAlsa::GetVolumeLimits(double* vol_min, double* vol_max) { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != READY) + return false; + if (vol_min) + *vol_min = min_volume_; + if (vol_max) + *vol_max = max_volume_; + return true; +} + +void AudioMixerAlsa::SetVolumeDb(double vol_db) { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != READY) + return; + DoSetVolumeDb_Locked(vol_db); +} + +bool AudioMixerAlsa::IsMute() const { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != READY) + return false; + return GetElementMuted_Locked(elem_master_); +} + +void AudioMixerAlsa::SetMute(bool mute) { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != READY) + return; + + // Set volume to minimum on mute, since switching the element off does not + // always mute as it should. + + // TODO(davej): Setting volume to minimum can be removed once switching the + // element off can be guaranteed to work. + + bool old_value = GetElementMuted_Locked(elem_master_); + + if (old_value != mute) { + if (mute) { + save_volume_ = DoGetVolumeDb_Locked(); + DoSetVolumeDb_Locked(min_volume_); + } else { + DoSetVolumeDb_Locked(save_volume_); + } + } + + SetElementMuted_Locked(elem_master_, mute); + if (elem_pcm_) + SetElementMuted_Locked(elem_pcm_, mute); +} + +AudioMixer::State AudioMixerAlsa::GetState() const { + AutoLock lock(mixer_state_lock_); + // If we think it's ready, verify it is actually so. + if ((mixer_state_ == READY) && (alsa_mixer_ == NULL)) + mixer_state_ = IN_ERROR; + return mixer_state_; +} + +//////////////////////////////////////////////////////////////////////////////// +// Private functions follow + +void AudioMixerAlsa::DoInit(InitDoneCallback* callback) { + bool success = InitializeAlsaMixer(); + + if (callback) { + callback->Run(success); + delete callback; + } +} + +bool AudioMixerAlsa::InitThread() { + AutoLock lock(mixer_state_lock_); + + if (mixer_state_ != UNINITIALIZED) + return false; + + if (thread_ == NULL) { + thread_.reset(new base::Thread("AudioMixerAlsa")); + if (!thread_->Start()) { + thread_.reset(); + return false; + } + } + + mixer_state_ = INITIALIZING; + return true; +} + +bool AudioMixerAlsa::InitializeAlsaMixer() { + AutoLock lock(mixer_state_lock_); + if (mixer_state_ != INITIALIZING) + return false; + + int err; + snd_mixer_t* handle = NULL; + const char* card = "default"; + + if ((err = snd_mixer_open(&handle, 0)) < 0) { + LOG(ERROR) << "ALSA mixer " << card << " open error: " << snd_strerror(err); + return false; + } + + if ((err = snd_mixer_attach(handle, card)) < 0) { + LOG(ERROR) << "ALSA Attach to card " << card << " failed: " + << snd_strerror(err); + snd_mixer_close(handle); + return false; + } + + if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { + LOG(ERROR) << "ALSA mixer register error: " << snd_strerror(err); + snd_mixer_close(handle); + return false; + } + + if ((err = snd_mixer_load(handle)) < 0) { + LOG(ERROR) << "ALSA mixer " << card << " load error: %s" + << snd_strerror(err); + snd_mixer_close(handle); + return false; + } + + VLOG(1) << "Opened ALSA mixer " << card << " OK"; + + elem_master_ = FindElementWithName_Locked(handle, kMasterVolume); + if (elem_master_) { + alsa_long_t long_lo, long_hi; + snd_mixer_selem_get_playback_dB_range(elem_master_, &long_lo, &long_hi); + min_volume_ = static_cast<double>(long_lo) / 100.0; + max_volume_ = static_cast<double>(long_hi) / 100.0; + } else { + LOG(ERROR) << "Cannot find 'Master' ALSA mixer element on " << card; + snd_mixer_close(handle); + return false; + } + + elem_pcm_ = FindElementWithName_Locked(handle, kPCMVolume); + if (elem_pcm_) { + alsa_long_t long_lo, long_hi; + snd_mixer_selem_get_playback_dB_range(elem_pcm_, &long_lo, &long_hi); + min_volume_ += static_cast<double>(long_lo) / 100.0; + max_volume_ += static_cast<double>(long_hi) / 100.0; + } + + VLOG(1) << "ALSA volume range is " << min_volume_ << " dB to " + << max_volume_ << " dB"; + + alsa_mixer_ = handle; + mixer_state_ = READY; + return true; +} + +void AudioMixerAlsa::FreeAlsaMixer() { + AutoLock lock(mixer_state_lock_); + mixer_state_ = SHUTTING_DOWN; + if (alsa_mixer_) { + snd_mixer_close(alsa_mixer_); + alsa_mixer_ = NULL; + } +} + +double AudioMixerAlsa::DoGetVolumeDb_Locked() const { + double vol_total = 0.0; + GetElementVolume_Locked(elem_master_, &vol_total); + + double vol_pcm = 0.0; + if (elem_pcm_ && (GetElementVolume_Locked(elem_pcm_, &vol_pcm))) + vol_total += vol_pcm; + + return vol_total; +} + +void AudioMixerAlsa::DoSetVolumeDb_Locked(double vol_db) { + double actual_vol = 0.0; + + // If a PCM volume slider exists, then first set the Master volume to the + // nearest volume >= requested volume, then adjust PCM volume down to get + // closer to the requested volume. + + if (elem_pcm_) { + SetElementVolume_Locked(elem_master_, vol_db, &actual_vol, 0.9999f); + SetElementVolume_Locked(elem_pcm_, vol_db - actual_vol, NULL, 0.5f); + } else { + SetElementVolume_Locked(elem_master_, vol_db, NULL, 0.5f); + } +} + +snd_mixer_elem_t* AudioMixerAlsa::FindElementWithName_Locked( + snd_mixer_t* handle, + const char* element_name) const { + snd_mixer_selem_id_t* sid; + + // Using id_malloc/id_free API instead of id_alloca since the latter gives the + // warning: the address of 'sid' will always evaluate as 'true' + if (snd_mixer_selem_id_malloc(&sid)) + return NULL; + + snd_mixer_selem_id_set_index(sid, 0); + snd_mixer_selem_id_set_name(sid, element_name); + snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + LOG(ERROR) << "ALSA unable to find simple control " + << snd_mixer_selem_id_get_name(sid); + } + + snd_mixer_selem_id_free(sid); + return elem; +} + +bool AudioMixerAlsa::GetElementVolume_Locked(snd_mixer_elem_t* elem, + double* current_vol) const { + alsa_long_t long_vol = 0; + snd_mixer_selem_get_playback_dB(elem, + static_cast<snd_mixer_selem_channel_id_t>(0), + &long_vol); + *current_vol = static_cast<double>(long_vol) / 100.0; + + return true; +} + +bool AudioMixerAlsa::SetElementVolume_Locked(snd_mixer_elem_t* elem, + double new_vol, + double* actual_vol, + double rounding_bias) { + alsa_long_t vol_lo = 0; + alsa_long_t vol_hi = 0; + snd_mixer_selem_get_playback_volume_range(elem, &vol_lo, &vol_hi); + alsa_long_t vol_range = vol_hi - vol_lo; + if (vol_range <= 0) + return false; + + alsa_long_t db_lo_int = 0; + alsa_long_t db_hi_int = 0; + snd_mixer_selem_get_playback_dB_range(elem, &db_lo_int, &db_hi_int); + double db_lo = static_cast<double>(db_lo_int) / 100.0; + double db_hi = static_cast<double>(db_hi_int) / 100.0; + double db_step = static_cast<double>(db_hi - db_lo) / vol_range; + if (db_step <= 0.0) + return false; + + if (new_vol < db_lo) + new_vol = db_lo; + + alsa_long_t value = static_cast<alsa_long_t>(rounding_bias + + (new_vol - db_lo) / db_step) + vol_lo; + snd_mixer_selem_set_playback_volume_all(elem, value); + + VLOG(1) << "Set volume " << snd_mixer_selem_get_name(elem) + << " to " << new_vol << " ==> " << (value - vol_lo) * db_step + db_lo + << " dB"; + + if (actual_vol) { + alsa_long_t volume; + snd_mixer_selem_get_playback_volume( + elem, + static_cast<snd_mixer_selem_channel_id_t>(0), + &volume); + *actual_vol = db_lo + (volume - vol_lo) * db_step; + + VLOG(1) << "Actual volume " << snd_mixer_selem_get_name(elem) + << " now " << *actual_vol << " dB"; + } + return true; +} + +bool AudioMixerAlsa::GetElementMuted_Locked(snd_mixer_elem_t* elem) const { + int enabled; + snd_mixer_selem_get_playback_switch( + elem, + static_cast<snd_mixer_selem_channel_id_t>(0), + &enabled); + return (enabled) ? false : true; +} + +void AudioMixerAlsa::SetElementMuted_Locked(snd_mixer_elem_t* elem, bool mute) { + int enabled = mute ? 0 : 1; + snd_mixer_selem_set_playback_switch_all(elem, enabled); + + VLOG(1) << "Set playback switch " << snd_mixer_selem_get_name(elem) + << " to " << enabled; +} + +} // namespace chromeos + diff --git a/chrome/browser/chromeos/audio_mixer_alsa.h b/chrome/browser/chromeos/audio_mixer_alsa.h new file mode 100644 index 0000000..d365a22 --- /dev/null +++ b/chrome/browser/chromeos/audio_mixer_alsa.h @@ -0,0 +1,100 @@ +// 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. + +#ifndef CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_ALSA_H_ +#define CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_ALSA_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/lock.h" +#include "base/scoped_ptr.h" +#include "base/threading/thread.h" +#include "chrome/browser/chromeos/audio_mixer.h" + +struct _snd_mixer_elem; +struct _snd_mixer; + +namespace chromeos { + +class AudioMixerAlsa : public AudioMixer { + public: + AudioMixerAlsa(); + virtual ~AudioMixerAlsa(); + + // Implementation of AudioMixer + virtual void Init(InitDoneCallback* callback); + virtual bool InitSync(); + virtual double GetVolumeDb() const; + virtual bool GetVolumeLimits(double* vol_min, double* vol_max); + virtual void SetVolumeDb(double vol_db); + virtual bool IsMute() const; + virtual void SetMute(bool mute); + virtual State GetState() const; + + private: + // Called to do initialization in background from worker thread. + void DoInit(InitDoneCallback* callback); + + // Helper function to just get our message loop thread going. + bool InitThread(); + + // Try to connect to the ALSA mixer through their simple controls interface, + // and cache mixer handle and mixer elements we'll be using. + bool InitializeAlsaMixer(); + void FreeAlsaMixer(); + + // All these internal volume commands must be called with the lock held. + double DoGetVolumeDb_Locked() const; + void DoSetVolumeDb_Locked(double vol_db); + + _snd_mixer_elem* FindElementWithName_Locked(_snd_mixer* handle, + const char* element_name) const; + + bool GetElementVolume_Locked(_snd_mixer_elem* elem, + double* current_vol) const; + + // Since volume is done in steps, we may not get the exact volume asked for, + // so actual_vol will contain the true volume that was set. This information + // can be used to further refine the volume by adjust a different mixer + // element. The rounding_bias is added in before rounding to the nearest + // volume step (use 0.5 to round to nearest). + bool SetElementVolume_Locked(_snd_mixer_elem* elem, + double new_vol, + double* actual_vol, + double rounding_bias); + + // In ALSA, the mixer element's 'switch' is turned off to mute. + bool GetElementMuted_Locked(_snd_mixer_elem* elem) const; + void SetElementMuted_Locked(_snd_mixer_elem* elem, bool mute); + + // Volume range limits are computed once during InitializeAlsaMixer. + double min_volume_; + double max_volume_; + + // Muting is done by setting volume to minimum, so we must save the original. + // This is the only state information kept in this object. In some cases, + // ALSA can report it has a volume switch and we can turn it off, but it has + // no effect. + double save_volume_; + + mutable Lock mixer_state_lock_; + mutable State mixer_state_; + + // Cached contexts for use in ALSA calls. + _snd_mixer* alsa_mixer_; + _snd_mixer_elem* elem_master_; + _snd_mixer_elem* elem_pcm_; + + scoped_ptr<base::Thread> thread_; + + DISALLOW_COPY_AND_ASSIGN(AudioMixerAlsa); +}; + +} // namespace chromeos + +DISABLE_RUNNABLE_METHOD_REFCOUNT(chromeos::AudioMixerAlsa); + +#endif // CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_ALSA_H_ + diff --git a/chrome/browser/chromeos/pulse_audio_mixer.cc b/chrome/browser/chromeos/audio_mixer_pulse.cc index a58548e..0001548 100644 --- a/chrome/browser/chromeos/pulse_audio_mixer.cc +++ b/chrome/browser/chromeos/audio_mixer_pulse.cc @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "chrome/browser/chromeos/pulse_audio_mixer.h" +#include "chrome/browser/chromeos/audio_mixer_pulse.h" #include <pulse/pulseaudio.h> @@ -26,9 +26,14 @@ 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 { - PulseAudioMixer* instance; + AudioMixerPulse* instance; bool done; void* userdata; }; @@ -37,66 +42,66 @@ struct CallbackWrapper { // AudioInfo contains all the values we care about when getting info for a // Sink (output device) used by GetAudioInfo(). -struct PulseAudioMixer::AudioInfo { +struct AudioMixerPulse::AudioInfo { pa_cvolume cvolume; bool muted; }; -PulseAudioMixer::PulseAudioMixer() +AudioMixerPulse::AudioMixerPulse() : device_id_(kInvalidDeviceId), last_channels_(0), mainloop_lock_count_(0), - mixer_state_lock_(), mixer_state_(UNINITIALIZED), pa_context_(NULL), - pa_mainloop_(NULL), - thread_(NULL) { + pa_mainloop_(NULL) { } -PulseAudioMixer::~PulseAudioMixer() { +AudioMixerPulse::~AudioMixerPulse() { PulseAudioFree(); - thread_->Stop(); - thread_.reset(); + if (thread_ != NULL) { + thread_->Stop(); + thread_.reset(); + } } -bool PulseAudioMixer::Init(InitDoneCallback* callback) { - if (!InitThread()) - return false; +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, &PulseAudioMixer::DoInit, callback)); - return true; + NewRunnableMethod(this, &AudioMixerPulse::DoInit, callback)); } -bool PulseAudioMixer::InitSync() { +bool AudioMixerPulse::InitSync() { if (!InitThread()) return false; return PulseAudioInit(); } -double PulseAudioMixer::GetVolumeDb() const { +double AudioMixerPulse::GetVolumeDb() const { if (!MainloopLockIfReady()) - return pa_sw_volume_to_dB(0); // this returns -inf. + return AudioMixer::kSilenceDb; AudioInfo data; GetAudioInfo(&data); MainloopUnlock(); return pa_sw_volume_to_dB(data.cvolume.values[0]); } -bool PulseAudioMixer::GetVolumeDbAsync(GetVolumeCallback* callback, - void* user) { - if (CheckState() != READY) - return false; - thread_->message_loop()->PostTask(FROM_HERE, - NewRunnableMethod(this, - &PulseAudioMixer::DoGetVolume, - callback, user)); +bool AudioMixerPulse::GetVolumeLimits(double* vol_min, double* vol_max) { + if (vol_min) + *vol_min = kMinVolumeDb; + if (vol_max) + *vol_max = kMaxVolumeDb; return true; } -void PulseAudioMixer::SetVolumeDb(double vol_db) { +void AudioMixerPulse::SetVolumeDb(double vol_db) { if (!MainloopLockIfReady()) return; @@ -115,9 +120,10 @@ void PulseAudioMixer::SetVolumeDb(double vol_db) { &cvolume, NULL, NULL); pa_operation_unref(pa_op); MainloopUnlock(); + VLOG(1) << "Set volume to " << vol_db << " dB"; } -bool PulseAudioMixer::IsMute() const { +bool AudioMixerPulse::IsMute() const { if (!MainloopLockIfReady()) return false; AudioInfo data; @@ -126,7 +132,7 @@ bool PulseAudioMixer::IsMute() const { return data.muted; } -void PulseAudioMixer::SetMute(bool mute) { +void AudioMixerPulse::SetMute(bool mute) { if (!MainloopLockIfReady()) return; pa_operation* pa_op; @@ -134,9 +140,10 @@ void PulseAudioMixer::SetMute(bool mute) { mute ? 1 : 0, NULL, NULL); pa_operation_unref(pa_op); MainloopUnlock(); + VLOG(1) << "Set mute to " << mute; } -PulseAudioMixer::State PulseAudioMixer::CheckState() const { +AudioMixer::State AudioMixerPulse::GetState() const { AutoLock lock(mixer_state_lock_); // If we think it's ready, verify it is actually so. if ((mixer_state_ == READY) && @@ -148,25 +155,20 @@ PulseAudioMixer::State PulseAudioMixer::CheckState() const { //////////////////////////////////////////////////////////////////////////////// // Private functions follow -void PulseAudioMixer::DoInit(InitDoneCallback* callback) { +void AudioMixerPulse::DoInit(InitDoneCallback* callback) { bool success = PulseAudioInit(); callback->Run(success); delete callback; } -void PulseAudioMixer::DoGetVolume(GetVolumeCallback* callback, - void* user) { - callback->Run(GetVolumeDb(), user); - delete callback; -} - -bool PulseAudioMixer::InitThread() { +bool AudioMixerPulse::InitThread() { AutoLock lock(mixer_state_lock_); if (mixer_state_ != UNINITIALIZED) return false; + if (thread_ == NULL) { - thread_.reset(new base::Thread("PulseAudioMixer")); + thread_.reset(new base::Thread("AudioMixerPulse")); if (!thread_->Start()) { thread_.reset(); return false; @@ -177,14 +179,14 @@ bool PulseAudioMixer::InitThread() { } // static -void PulseAudioMixer::ConnectToPulseCallbackThunk( +void AudioMixerPulse::ConnectToPulseCallbackThunk( pa_context* context, void* userdata) { CallbackWrapper* data = static_cast<CallbackWrapper*>(userdata); data->instance->OnConnectToPulseCallback(context, &data->done); } -void PulseAudioMixer::OnConnectToPulseCallback( +void AudioMixerPulse::OnConnectToPulseCallback( pa_context* context, bool* connect_done) { pa_context_state_t state = pa_context_get_state(context); if (state == PA_CONTEXT_READY || @@ -196,7 +198,7 @@ void PulseAudioMixer::OnConnectToPulseCallback( } } -bool PulseAudioMixer::PulseAudioInit() { +bool AudioMixerPulse::PulseAudioInit() { pa_context_state_t state = PA_CONTEXT_FAILED; { @@ -298,13 +300,14 @@ bool PulseAudioMixer::PulseAudioInit() { return false; } -void PulseAudioMixer::PulseAudioFree() { +void AudioMixerPulse::PulseAudioFree() { { 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; } @@ -329,7 +332,7 @@ void PulseAudioMixer::PulseAudioFree() { } } -void PulseAudioMixer::CompleteOperation(pa_operation* pa_op, +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 @@ -348,7 +351,7 @@ void PulseAudioMixer::CompleteOperation(pa_operation* pa_op, } // Must be called with mainloop lock held -void PulseAudioMixer::GetDefaultPlaybackDevice() { +void AudioMixerPulse::GetDefaultPlaybackDevice() { DCHECK_GT(mainloop_lock_count_, 0); DCHECK(pa_context_); DCHECK(pa_context_get_state(pa_context_) == PA_CONTEXT_READY); @@ -362,7 +365,7 @@ void PulseAudioMixer::GetDefaultPlaybackDevice() { return; } -void PulseAudioMixer::OnEnumerateDevices(const pa_sink_info* sink_info, +void AudioMixerPulse::OnEnumerateDevices(const pa_sink_info* sink_info, int eol, bool* done) { if (device_id_ != kInvalidDeviceId) return; @@ -378,7 +381,7 @@ void PulseAudioMixer::OnEnumerateDevices(const pa_sink_info* sink_info, } // static -void PulseAudioMixer::EnumerateDevicesCallback(pa_context* unused, +void AudioMixerPulse::EnumerateDevicesCallback(pa_context* unused, const pa_sink_info* sink_info, int eol, void* userdata) { @@ -388,9 +391,9 @@ void PulseAudioMixer::EnumerateDevicesCallback(pa_context* unused, } // Must be called with lock held -void PulseAudioMixer::GetAudioInfo(AudioInfo* info) const { +void AudioMixerPulse::GetAudioInfo(AudioInfo* info) const { DCHECK_GT(mainloop_lock_count_, 0); - CallbackWrapper data = {const_cast<PulseAudioMixer*>(this), false, info}; + CallbackWrapper data = {const_cast<AudioMixerPulse*>(this), false, info}; pa_operation* pa_op = pa_context_get_sink_info_by_index(pa_context_, device_id_, GetAudioInfoCallback, @@ -399,7 +402,7 @@ void PulseAudioMixer::GetAudioInfo(AudioInfo* info) const { } // static -void PulseAudioMixer::GetAudioInfoCallback(pa_context* unused, +void AudioMixerPulse::GetAudioInfoCallback(pa_context* unused, const pa_sink_info* sink_info, int eol, void* userdata) { @@ -415,38 +418,39 @@ void PulseAudioMixer::GetAudioInfoCallback(pa_context* unused, data->instance->MainloopSignal(); } -inline void PulseAudioMixer::MainloopLock() const { +inline void AudioMixerPulse::MainloopLock() const { pa_threaded_mainloop_lock(pa_mainloop_); ++mainloop_lock_count_; } -inline void PulseAudioMixer::MainloopUnlock() const { +inline void AudioMixerPulse::MainloopUnlock() const { --mainloop_lock_count_; pa_threaded_mainloop_unlock(pa_mainloop_); } // Must be called with the lock held. -inline void PulseAudioMixer::MainloopWait() const { +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 PulseAudioMixer::MainloopSignal() const { +inline void AudioMixerPulse::MainloopSignal() const { DCHECK_GT(mainloop_lock_count_, 0); pa_threaded_mainloop_signal(pa_mainloop_, 0); } -inline bool PulseAudioMixer::MainloopSafeLock() const { +inline bool AudioMixerPulse::MainloopSafeLock() const { 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 PulseAudioMixer::MainloopLockIfReady() const { +inline bool AudioMixerPulse::MainloopLockIfReady() const { AutoLock lock(mixer_state_lock_); if (mixer_state_ != READY) return false; diff --git a/chrome/browser/chromeos/pulse_audio_mixer.h b/chrome/browser/chromeos/audio_mixer_pulse.h index f01e738..8ce8274 100644 --- a/chrome/browser/chromeos/pulse_audio_mixer.h +++ b/chrome/browser/chromeos/audio_mixer_pulse.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef CHROME_BROWSER_CHROMEOS_PULSE_AUDIO_MIXER_H_ -#define CHROME_BROWSER_CHROMEOS_PULSE_AUDIO_MIXER_H_ +#ifndef CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_PULSE_H_ +#define CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_PULSE_H_ #pragma once #include "base/basictypes.h" @@ -11,6 +11,7 @@ #include "base/lock.h" #include "base/scoped_ptr.h" #include "base/threading/thread.h" +#include "chrome/browser/chromeos/audio_mixer.h" struct pa_context; struct pa_cvolume; @@ -20,56 +21,27 @@ struct pa_sink_info; namespace chromeos { -class PulseAudioMixer { +class AudioMixerPulse : public AudioMixer { public: - enum State { - UNINITIALIZED = 0, - INITIALIZING, - READY, - SHUTTING_DOWN, - IN_ERROR - }; - PulseAudioMixer(); - ~PulseAudioMixer(); + AudioMixerPulse(); + virtual ~AudioMixerPulse(); - // Non-blocking, connect to PulseAudio and find a default device, and call - // callback when complete with success code. - typedef Callback1<bool>::Type InitDoneCallback; - bool Init(InitDoneCallback* callback); - - // Blocking init call guarantees PulseAudio is started before returning. - bool InitSync(); - - // Blocking call. Returns a default of -inf on error. - double GetVolumeDb() const; - - // Non-blocking, volume sent in as first param to callback. The callback is - // only called if the function returns true. - typedef Callback2<double, void*>::Type GetVolumeCallback; - bool GetVolumeDbAsync(GetVolumeCallback* callback, void* user); - - // Non-blocking call. - void SetVolumeDb(double vol_db); - - // Gets the mute state of the default device (true == mute). Blocking call. - // Returns a default of false on error. - bool IsMute() const; - - // Non-Blocking call. - void SetMute(bool mute); - - // Returns READY if we have a valid working connection to PulseAudio. - // This can return IN_ERROR if we lose the connection, even after an original - // successful init. Non-blocking call. - State CheckState() const; + // Implementation of AudioMixer + virtual void Init(InitDoneCallback* callback); + virtual bool InitSync(); + virtual double GetVolumeDb() const; + virtual bool GetVolumeLimits(double* vol_min, double* vol_max); + virtual void SetVolumeDb(double vol_db); + virtual bool IsMute() const; + virtual void SetMute(bool mute); + virtual State GetState() const; private: struct AudioInfo; - // These are the tasks to be run in the background on the worker thread. + // The initialization task is run in the background on the worker thread. void DoInit(InitDoneCallback* callback); - void DoGetVolume(GetVolumeCallback* callback, void* user); static void ConnectToPulseCallbackThunk(pa_context* c, void* userdata); void OnConnectToPulseCallback(pa_context* c, bool* connect_done); @@ -137,12 +109,12 @@ class PulseAudioMixer { scoped_ptr<base::Thread> thread_; - DISALLOW_COPY_AND_ASSIGN(PulseAudioMixer); + DISALLOW_COPY_AND_ASSIGN(AudioMixerPulse); }; } // namespace chromeos -DISABLE_RUNNABLE_METHOD_REFCOUNT(chromeos::PulseAudioMixer); +DISABLE_RUNNABLE_METHOD_REFCOUNT(chromeos::AudioMixerPulse); -#endif // CHROME_BROWSER_CHROMEOS_PULSE_AUDIO_MIXER_H_ +#endif // CHROME_BROWSER_CHROMEOS_AUDIO_MIXER_PULSE_H_ diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index d708af3..c0c00b7 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -404,6 +404,11 @@ 'browser/chromeos/notifications/system_notification_factory.h', 'browser/chromeos/audio_handler.cc', 'browser/chromeos/audio_handler.h', + 'browser/chromeos/audio_mixer.h', + 'browser/chromeos/audio_mixer_alsa.cc', + 'browser/chromeos/audio_mixer_alsa.h', + 'browser/chromeos/audio_mixer_pulse.cc', + 'browser/chromeos/audio_mixer_pulse.h', 'browser/chromeos/boot_times_loader.cc', 'browser/chromeos/boot_times_loader.h', 'browser/chromeos/brightness_bubble.cc', @@ -739,8 +744,6 @@ 'browser/chromeos/proxy_config_service_impl.h', 'browser/chromeos/proxy_cros_settings_provider.cc', 'browser/chromeos/proxy_cros_settings_provider.h', - 'browser/chromeos/pulse_audio_mixer.cc', - 'browser/chromeos/pulse_audio_mixer.h', 'browser/chromeos/setting_level_bubble.cc', 'browser/chromeos/setting_level_bubble.h', 'browser/chromeos/setting_level_bubble_view.cc', |