// 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 "chrome/browser/chromeos/audio_mixer_alsa.h" #include #include #include #include "base/logging.h" #include "base/message_loop.h" #include "base/task.h" #include "base/threading/thread_restrictions.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/common/pref_names.h" #include "content/browser/browser_thread.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. 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; const double kPrefVolumeInvalid = -999.0; const int kPrefMuteOff = 0; const int kPrefMuteOn = 1; const int kPrefMuteInvalid = 2; // Maximum number of times that we'll attempt to initialize the mixer. // We'll fail until the ALSA modules have been loaded; see // http://crosbug.com/13162. const int kMaxInitAttempts = 20; // Number of seconds that we'll sleep between each initialization attempt. const int kInitRetrySleepSec = 1; } // namespace // static const double AudioMixer::kSilenceDb = -200.0; AudioMixerAlsa::AudioMixerAlsa() : min_volume_(kDefaultMinVolume), max_volume_(kDefaultMaxVolume), save_volume_(0), mixer_state_(UNINITIALIZED), alsa_mixer_(NULL), elem_master_(NULL), elem_pcm_(NULL), prefs_(NULL), done_event_(true, false) { } AudioMixerAlsa::~AudioMixerAlsa() { if (thread_ != NULL) { { base::AutoLock lock(mixer_state_lock_); mixer_state_ = SHUTTING_DOWN; thread_->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &AudioMixerAlsa::FreeAlsaMixer)); } done_event_.Wait(); 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 AudioMixerAlsa::Init(InitDoneCallback* callback) { DCHECK(callback); if (!InitThread()) { callback->Run(false); delete callback; return; } InitPrefs(); { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ == SHUTTING_DOWN) return; // Post the task of starting up, which may block on the order of ms, // 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; InitPrefs(); return InitializeAlsaMixer(); } double AudioMixerAlsa::GetVolumeDb() const { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ != READY) return kSilenceDb; return DoGetVolumeDb_Locked(); } bool AudioMixerAlsa::GetVolumeLimits(double* vol_min, double* vol_max) { base::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) { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ != READY) return; if (vol_db < kSilenceDb || isnan(vol_db)) { if (isnan(vol_db)) LOG(WARNING) << "Got request to set volume to NaN"; vol_db = kSilenceDb; } DoSetVolumeDb_Locked(vol_db); prefs_->SetDouble(prefs::kAudioVolume, vol_db); } bool AudioMixerAlsa::IsMute() const { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ != READY) return false; return GetElementMuted_Locked(elem_master_); } // To indicate the volume is not valid yet, a very low volume value is stored. // We compare against a slightly higher value in case of rounding errors. static bool PrefVolumeValid(double volume) { return (volume > kPrefVolumeInvalid + 0.1); } void AudioMixerAlsa::SetMute(bool mute) { base::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): Remove save_volume_ and setting volume to minimum if // switching the element off can be guaranteed to mute it. Currently mute // is done by setting the volume to min_volume_. 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); prefs_->SetInteger(prefs::kAudioMute, mute ? kPrefMuteOn : kPrefMuteOff); } AudioMixer::State AudioMixerAlsa::GetState() const { base::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_; } // static void AudioMixerAlsa::RegisterPrefs(PrefService* local_state) { if (!local_state->FindPreference(prefs::kAudioVolume)) local_state->RegisterDoublePref(prefs::kAudioVolume, kPrefVolumeInvalid, PrefService::UNSYNCABLE_PREF); if (!local_state->FindPreference(prefs::kAudioMute)) local_state->RegisterIntegerPref(prefs::kAudioMute, kPrefMuteInvalid, PrefService::UNSYNCABLE_PREF); } //////////////////////////////////////////////////////////////////////////////// // Private functions follow void AudioMixerAlsa::DoInit(InitDoneCallback* callback) { bool success = false; for (int num_attempts = 0; num_attempts < kMaxInitAttempts; ++num_attempts) { success = InitializeAlsaMixer(); if (success) { break; } else { // If the destructor has reset the state, give up. { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ != INITIALIZING) break; } sleep(kInitRetrySleepSec); } } if (success) { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, NewRunnableMethod(this, &AudioMixerAlsa::RestoreVolumeMuteOnUIThread)); } if (callback) { callback->Run(success); delete callback; } } bool AudioMixerAlsa::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("AudioMixerAlsa")); if (!thread_->Start()) { thread_.reset(); return false; } } mixer_state_ = INITIALIZING; return true; } void AudioMixerAlsa::InitPrefs() { prefs_ = g_browser_process->local_state(); } bool AudioMixerAlsa::InitializeAlsaMixer() { // We can block; make sure that we're not on the UI thread. DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); base::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; } // Verify PCM can be opened, which also instantiates the PCM mixer element // which is needed for finer volume control and for muting by setting to zero. // If it fails, we can still try to use the mixer as best we can. snd_pcm_t* pcm_out_handle; if ((err = snd_pcm_open(&pcm_out_handle, card, SND_PCM_STREAM_PLAYBACK, 0)) >= 0) { snd_pcm_close(pcm_out_handle); } else { LOG(WARNING) << "ALSA PCM open: " << snd_strerror(err); } 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 = static_cast(kDefaultMinVolume * 100); alsa_long_t long_hi = static_cast(kDefaultMaxVolume * 100); err = snd_mixer_selem_get_playback_dB_range( elem_master_, &long_lo, &long_hi); if (err != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed " << "for master: " << snd_strerror(err); snd_mixer_close(handle); return false; } min_volume_ = static_cast(long_lo) / 100.0; max_volume_ = static_cast(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 = static_cast(kDefaultMinVolume * 100); alsa_long_t long_hi = static_cast(kDefaultMaxVolume * 100); err = snd_mixer_selem_get_playback_dB_range(elem_pcm_, &long_lo, &long_hi); if (err != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed for PCM: " << snd_strerror(err); snd_mixer_close(handle); return false; } min_volume_ += static_cast(long_lo) / 100.0; max_volume_ += static_cast(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() { if (alsa_mixer_) { snd_mixer_close(alsa_mixer_); alsa_mixer_ = NULL; } done_event_.Signal(); } void AudioMixerAlsa::DoSetVolumeMute(double pref_volume, int pref_mute) { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ != READY) return; // If volume or mute are invalid, set them now to the current actual values. if (!PrefVolumeValid(pref_volume)) pref_volume = DoGetVolumeDb_Locked(); bool mute = false; if (pref_mute == kPrefMuteInvalid) mute = GetElementMuted_Locked(elem_master_); else mute = (pref_mute == kPrefMuteOn) ? true : false; VLOG(1) << "Setting volume to " << pref_volume << " and mute to " << mute; if (mute) { save_volume_ = pref_volume; DoSetVolumeDb_Locked(min_volume_); } else { DoSetVolumeDb_Locked(pref_volume); } SetElementMuted_Locked(elem_master_, mute); } void AudioMixerAlsa::RestoreVolumeMuteOnUIThread() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // This happens during init, so set the volume off the UI thread. int mute = prefs_->GetInteger(prefs::kAudioMute); double vol = prefs_->GetDouble(prefs::kAudioVolume); { base::AutoLock lock(mixer_state_lock_); if (mixer_state_ == SHUTTING_DOWN) return; thread_->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &AudioMixerAlsa::DoSetVolumeMute, vol, mute)); } } double AudioMixerAlsa::DoGetVolumeDb_Locked() const { double vol_total = 0.0; if (!GetElementVolume_Locked(elem_master_, &vol_total)) return 0.0; 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; int alsa_result = snd_mixer_selem_get_playback_dB( elem, static_cast(0), &long_vol); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_dB() failed: " << snd_strerror(alsa_result); return false; } *current_vol = static_cast(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; int alsa_result = snd_mixer_selem_get_playback_volume_range(elem, &vol_lo, &vol_hi); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_volume_range() failed: " << snd_strerror(alsa_result); return false; } 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; alsa_result = snd_mixer_selem_get_playback_dB_range(elem, &db_lo_int, &db_hi_int); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed: " << snd_strerror(alsa_result); return false; } double db_lo = static_cast(db_lo_int) / 100.0; double db_hi = static_cast(db_hi_int) / 100.0; double db_step = static_cast(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(rounding_bias + (new_vol - db_lo) / db_step) + vol_lo; alsa_result = snd_mixer_selem_set_playback_volume_all(elem, value); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_set_playback_volume_all() failed: " << snd_strerror(alsa_result); return false; } 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 = vol_lo; alsa_result = snd_mixer_selem_get_playback_volume( elem, static_cast(0), &volume); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_volume() failed: " << snd_strerror(alsa_result); return false; } *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 = 0; int alsa_result = snd_mixer_selem_get_playback_switch( elem, static_cast(0), &enabled); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_get_playback_switch() failed: " << snd_strerror(alsa_result); return false; } return (enabled) ? false : true; } void AudioMixerAlsa::SetElementMuted_Locked(snd_mixer_elem_t* elem, bool mute) { int enabled = mute ? 0 : 1; int alsa_result = snd_mixer_selem_set_playback_switch_all(elem, enabled); if (alsa_result != 0) { LOG(WARNING) << "snd_mixer_selem_set_playback_switch_all() failed: " << snd_strerror(alsa_result); } else { VLOG(1) << "Set playback switch " << snd_mixer_selem_get_name(elem) << " to " << enabled; } } } // namespace chromeos