| // Copyright (c) 2012 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/audio_mixer_alsa.h" |
| |
| #include <unistd.h> |
| |
| #include <alsa/asoundlib.h> |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/chromeos/chromeos_version.h" |
| #include "base/logging.h" |
| #include "base/message_loop.h" |
| #include "base/threading/thread.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| typedef long alsa_long_t; // 'long' is required for ALSA API calls. |
| |
| using content::BrowserThread; |
| using std::string; |
| |
| namespace chromeos { |
| |
| namespace { |
| |
| // Name of the ALSA card to which we connect. |
| const char kCardName[] = "default"; |
| |
| // Mixer element names. We'll use the first master element from the list that |
| // exists. |
| const char* const kMasterElementNames[] = { |
| "Master", // x86 |
| "Digital", // ARM |
| }; |
| const char kPCMElementName[] = "PCM"; |
| |
| const char kMicElementName[] = "Mic"; |
| const char kFrontMicElementName[] = "Front Mic"; |
| |
| // Default minimum and maximum volume (before we've loaded the actual range from |
| // ALSA), in decibels. |
| const double kDefaultMinVolumeDb = -90.0; |
| const double kDefaultMaxVolumeDb = 0.0; |
| |
| // Default volume as a percentage in the range [0.0, 100.0]. |
| const double kDefaultVolumePercent = 75.0; |
| |
| // A value of less than 1.0 adjusts quieter volumes in larger steps (giving |
| // finer resolution in the higher volumes). |
| // TODO(derat): Choose a better mapping between percent and decibels. The |
| // bottom twenty-five percent or so is useless on a CR-48's internal speakers; |
| // it's all inaudible. |
| const double kVolumeBias = 0.5; |
| |
| // Number of seconds that we'll sleep between each connection attempt. |
| const int kConnectionRetrySleepSec = 1; |
| |
| // Connection attempt number (1-indexed) for which we'll log an error if we're |
| // still failing. This is set high enough to give the ALSA modules some time to |
| // be loaded into the kernel. We want to log an error eventually if something |
| // is broken, but we don't want to continue spamming the log indefinitely. |
| const int kConnectionAttemptToLogFailure = 10; |
| |
| } // namespace |
| |
| AudioMixerAlsa::AudioMixerAlsa() |
| : min_volume_db_(kDefaultMinVolumeDb), |
| max_volume_db_(kDefaultMaxVolumeDb), |
| volume_db_(kDefaultMinVolumeDb), |
| is_muted_(false), |
| apply_is_pending_(true), |
| initial_volume_percent_(kDefaultVolumePercent), |
| alsa_mixer_(NULL), |
| pcm_element_(NULL), |
| mic_element_(NULL), |
| front_mic_element_(NULL), |
| disconnected_event_(true, false), |
| num_connection_attempts_(0) { |
| } |
| |
| AudioMixerAlsa::~AudioMixerAlsa() { |
| if (!thread_.get()) |
| return; |
| DCHECK(MessageLoop::current() != thread_->message_loop()); |
| |
| thread_->message_loop()->PostTask( |
| FROM_HERE, base::Bind(&AudioMixerAlsa::Disconnect, |
| base::Unretained(this))); |
| { |
| // http://crbug.com/125206 |
| base::ThreadRestrictions::ScopedAllowWait allow_wait; |
| disconnected_event_.Wait(); |
| } |
| |
| base::ThreadRestrictions::ScopedAllowIO allow_io_for_thread_join; |
| thread_->Stop(); |
| thread_.reset(); |
| } |
| |
| void AudioMixerAlsa::Init() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(!thread_.get()) << "Init() called twice"; |
| thread_.reset(new base::Thread("AudioMixerAlsa")); |
| CHECK(thread_->Start()); |
| thread_->message_loop()->PostTask( |
| FROM_HERE, base::Bind(&AudioMixerAlsa::Connect, base::Unretained(this))); |
| } |
| |
| double AudioMixerAlsa::GetVolumePercent() { |
| base::AutoLock lock(lock_); |
| return !alsa_mixer_ ? |
| initial_volume_percent_ : |
| DbToPercent(volume_db_); |
| } |
| |
| void AudioMixerAlsa::SetVolumePercent(double percent) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| if (isnan(percent)) |
| percent = 0.0; |
| percent = std::max(std::min(percent, 100.0), 0.0); |
| |
| base::AutoLock lock(lock_); |
| if (!alsa_mixer_) { |
| initial_volume_percent_ = percent; |
| } else { |
| volume_db_ = PercentToDb(percent); |
| ApplyStateIfNeeded(); |
| } |
| } |
| |
| bool AudioMixerAlsa::IsMuted() { |
| base::AutoLock lock(lock_); |
| return is_muted_; |
| } |
| |
| void AudioMixerAlsa::SetMuted(bool mute) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock lock(lock_); |
| if (is_mute_locked_) { |
| NOTREACHED() << "Capture mute has been locked!"; |
| return; |
| } |
| is_muted_ = mute; |
| ApplyStateIfNeeded(); |
| } |
| |
| bool AudioMixerAlsa::IsMuteLocked() { |
| base::AutoLock lock(lock_); |
| return is_mute_locked_; |
| } |
| |
| void AudioMixerAlsa::SetMuteLocked(bool locked) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock lock(lock_); |
| is_mute_locked_ = locked; |
| } |
| |
| bool AudioMixerAlsa::IsCaptureMuted() { |
| base::AutoLock lock(lock_); |
| return is_capture_muted_; |
| } |
| |
| void AudioMixerAlsa::SetCaptureMuted(bool mute) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock lock(lock_); |
| if (is_capture_mute_locked_) { |
| NOTREACHED() << "Capture mute has been locked!"; |
| return; |
| } |
| is_capture_muted_ = mute; |
| ApplyStateIfNeeded(); |
| } |
| |
| bool AudioMixerAlsa::IsCaptureMuteLocked() { |
| base::AutoLock lock(lock_); |
| return is_capture_mute_locked_; |
| } |
| |
| void AudioMixerAlsa::SetCaptureMuteLocked(bool locked) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| base::AutoLock lock(lock_); |
| is_capture_mute_locked_ = locked; |
| } |
| |
| void AudioMixerAlsa::Connect() { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| DCHECK(!alsa_mixer_); |
| |
| if (disconnected_event_.IsSignaled()) |
| return; |
| |
| // Do not attempt to connect if we're not on the device. |
| if (!base::chromeos::IsRunningOnChromeOS()) |
| return; |
| |
| if (!ConnectInternal()) { |
| thread_->message_loop()->PostDelayedTask(FROM_HERE, |
| base::Bind(&AudioMixerAlsa::Connect, base::Unretained(this)), |
| base::TimeDelta::FromSeconds(kConnectionRetrySleepSec)); |
| } |
| } |
| |
| bool AudioMixerAlsa::ConnectInternal() { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| num_connection_attempts_++; |
| int err; |
| snd_mixer_t* handle = NULL; |
| |
| if ((err = snd_mixer_open(&handle, 0)) < 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "Mixer open error: " << snd_strerror(err); |
| return false; |
| } |
| |
| if ((err = snd_mixer_attach(handle, kCardName)) < 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "Attach to card " << kCardName << " 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, |
| kCardName, |
| SND_PCM_STREAM_PLAYBACK, |
| 0)) >= 0) { |
| snd_pcm_close(pcm_out_handle); |
| } else { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "PCM open failed: " << snd_strerror(err); |
| } |
| |
| if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "Mixer register error: " << snd_strerror(err); |
| snd_mixer_close(handle); |
| return false; |
| } |
| |
| if ((err = snd_mixer_load(handle)) < 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "Mixer " << kCardName << " load error: %s" |
| << snd_strerror(err); |
| snd_mixer_close(handle); |
| return false; |
| } |
| |
| VLOG(1) << "Opened mixer " << kCardName << " successfully"; |
| |
| double min_volume_db = kDefaultMinVolumeDb; |
| double max_volume_db = kDefaultMaxVolumeDb; |
| |
| snd_mixer_elem_t* master_element = NULL; |
| for (size_t i = 0; i < arraysize(kMasterElementNames); ++i) { |
| master_element = FindElementWithName(handle, kMasterElementNames[i]); |
| if (master_element) |
| break; |
| } |
| |
| if (!master_element) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "Unable to find a master element on " << kCardName; |
| snd_mixer_close(handle); |
| return false; |
| } |
| |
| alsa_long_t long_low = static_cast<alsa_long_t>(kDefaultMinVolumeDb * 100); |
| alsa_long_t long_high = static_cast<alsa_long_t>(kDefaultMaxVolumeDb * 100); |
| err = snd_mixer_selem_get_playback_dB_range( |
| master_element, &long_low, &long_high); |
| if (err != 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed:" |
| << snd_strerror(err); |
| snd_mixer_close(handle); |
| return false; |
| } |
| min_volume_db = static_cast<double>(long_low) / 100.0; |
| max_volume_db = static_cast<double>(long_high) / 100.0; |
| |
| snd_mixer_elem_t* pcm_element = FindElementWithName(handle, kPCMElementName); |
| if (pcm_element) { |
| alsa_long_t long_low = static_cast<alsa_long_t>(kDefaultMinVolumeDb * 100); |
| alsa_long_t long_high = static_cast<alsa_long_t>(kDefaultMaxVolumeDb * 100); |
| err = snd_mixer_selem_get_playback_dB_range( |
| pcm_element, &long_low, &long_high); |
| if (err != 0) { |
| if (num_connection_attempts_ == kConnectionAttemptToLogFailure) |
| LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed for " |
| << kPCMElementName << ": " << snd_strerror(err); |
| snd_mixer_close(handle); |
| return false; |
| } |
| min_volume_db += static_cast<double>(long_low) / 100.0; |
| max_volume_db += static_cast<double>(long_high) / 100.0; |
| } |
| |
| VLOG(1) << "Volume range is " << min_volume_db << " dB to " |
| << max_volume_db << " dB"; |
| |
| snd_mixer_elem_t* mic_element = FindElementWithName(handle, kMicElementName); |
| snd_mixer_elem_t* front_mic_element = |
| FindElementWithName(handle, kFrontMicElementName); |
| { |
| base::AutoLock lock(lock_); |
| alsa_mixer_ = handle; |
| master_element_ = master_element; |
| pcm_element_ = pcm_element; |
| mic_element_ = mic_element; |
| front_mic_element_ = front_mic_element; |
| min_volume_db_ = min_volume_db; |
| max_volume_db_ = max_volume_db; |
| volume_db_ = PercentToDb(initial_volume_percent_); |
| } |
| |
| ApplyState(); |
| return true; |
| } |
| |
| void AudioMixerAlsa::Disconnect() { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| if (alsa_mixer_) { |
| snd_mixer_close(alsa_mixer_); |
| alsa_mixer_ = NULL; |
| } |
| disconnected_event_.Signal(); |
| } |
| |
| void AudioMixerAlsa::ApplyState() { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| if (!alsa_mixer_) |
| return; |
| |
| bool should_mute = false; |
| bool should_mute_capture = false; |
| double new_volume_db = 0; |
| { |
| base::AutoLock lock(lock_); |
| should_mute = is_muted_; |
| should_mute_capture = is_capture_muted_; |
| new_volume_db = should_mute ? min_volume_db_ : volume_db_; |
| apply_is_pending_ = false; |
| } |
| |
| if (pcm_element_) { |
| // 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. |
| SetElementVolume(master_element_, new_volume_db, 0.9999f); |
| |
| double pcm_volume_db = 0.0; |
| double master_volume_db = 0.0; |
| if (GetElementVolume(master_element_, &master_volume_db)) |
| pcm_volume_db = new_volume_db - master_volume_db; |
| SetElementVolume(pcm_element_, pcm_volume_db, 0.5f); |
| } else { |
| SetElementVolume(master_element_, new_volume_db, 0.5f); |
| } |
| |
| SetElementMuted(master_element_, should_mute); |
| if (mic_element_) |
| SetElementMuted(mic_element_, should_mute_capture); |
| if (front_mic_element_) |
| SetElementMuted(front_mic_element_, should_mute_capture); |
| } |
| |
| snd_mixer_elem_t* AudioMixerAlsa::FindElementWithName( |
| snd_mixer_t* handle, const string& element_name) const { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| snd_mixer_selem_id_t* sid = NULL; |
| |
| // 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.c_str()); |
| snd_mixer_elem_t* element = snd_mixer_find_selem(handle, sid); |
| if (!element) |
| VLOG(1) << "Unable to find control " << snd_mixer_selem_id_get_name(sid); |
| |
| snd_mixer_selem_id_free(sid); |
| return element; |
| } |
| |
| bool AudioMixerAlsa::GetElementVolume(snd_mixer_elem_t* element, |
| double* current_volume_db) { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| alsa_long_t long_volume = 0; |
| int alsa_result = snd_mixer_selem_get_playback_dB( |
| element, static_cast<snd_mixer_selem_channel_id_t>(0), &long_volume); |
| if (alsa_result != 0) { |
| LOG(WARNING) << "snd_mixer_selem_get_playback_dB() failed: " |
| << snd_strerror(alsa_result); |
| return false; |
| } |
| *current_volume_db = static_cast<double>(long_volume) / 100.0; |
| return true; |
| } |
| |
| bool AudioMixerAlsa::SetElementVolume(snd_mixer_elem_t* element, |
| double new_volume_db, |
| double rounding_bias) { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| alsa_long_t volume_low = 0; |
| alsa_long_t volume_high = 0; |
| int alsa_result = snd_mixer_selem_get_playback_volume_range( |
| element, &volume_low, &volume_high); |
| if (alsa_result != 0) { |
| LOG(WARNING) << "snd_mixer_selem_get_playback_volume_range() failed: " |
| << snd_strerror(alsa_result); |
| return false; |
| } |
| alsa_long_t volume_range = volume_high - volume_low; |
| if (volume_range <= 0) |
| return false; |
| |
| alsa_long_t db_low_int = 0; |
| alsa_long_t db_high_int = 0; |
| alsa_result = |
| snd_mixer_selem_get_playback_dB_range(element, &db_low_int, &db_high_int); |
| if (alsa_result != 0) { |
| LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed: " |
| << snd_strerror(alsa_result); |
| return false; |
| } |
| |
| double db_low = static_cast<double>(db_low_int) / 100.0; |
| double db_high = static_cast<double>(db_high_int) / 100.0; |
| double db_step = static_cast<double>(db_high - db_low) / volume_range; |
| if (db_step <= 0.0) |
| return false; |
| |
| if (new_volume_db < db_low) |
| new_volume_db = db_low; |
| |
| alsa_long_t value = static_cast<alsa_long_t>( |
| rounding_bias + (new_volume_db - db_low) / db_step) + volume_low; |
| alsa_result = snd_mixer_selem_set_playback_volume_all(element, 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(element) |
| << " to " << new_volume_db << " ==> " |
| << (value - volume_low) * db_step + db_low << " dB"; |
| |
| return true; |
| } |
| |
| void AudioMixerAlsa::SetElementMuted(snd_mixer_elem_t* element, bool mute) { |
| DCHECK(MessageLoop::current() == thread_->message_loop()); |
| int alsa_result = snd_mixer_selem_set_playback_switch_all(element, !mute); |
| 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(element) |
| << " to " << mute; |
| } |
| } |
| |
| double AudioMixerAlsa::DbToPercent(double db) const { |
| lock_.AssertAcquired(); |
| if (db < min_volume_db_) |
| return 0.0; |
| return 100.0 * pow((db - min_volume_db_) / |
| (max_volume_db_ - min_volume_db_), 1/kVolumeBias); |
| } |
| |
| double AudioMixerAlsa::PercentToDb(double percent) const { |
| lock_.AssertAcquired(); |
| return pow(percent / 100.0, kVolumeBias) * |
| (max_volume_db_ - min_volume_db_) + min_volume_db_; |
| } |
| |
| void AudioMixerAlsa::ApplyStateIfNeeded() { |
| lock_.AssertAcquired(); |
| if (!apply_is_pending_) { |
| thread_->message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&AudioMixerAlsa::ApplyState, base::Unretained(this))); |
| } |
| } |
| |
| } // namespace chromeos |