• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/chromeos/audio_mixer_alsa.h"
6 
7 #include <cmath>
8 #include <unistd.h>
9 
10 #include <alsa/asoundlib.h>
11 
12 #include "base/logging.h"
13 #include "base/message_loop.h"
14 #include "base/task.h"
15 #include "base/threading/thread_restrictions.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/prefs/pref_service.h"
18 #include "chrome/common/pref_names.h"
19 #include "content/browser/browser_thread.h"
20 
21 namespace chromeos {
22 
23 // Connect to the ALSA mixer using their simple element API.  Init is performed
24 // asynchronously on the worker thread.
25 //
26 // To get a wider range and finer control over volume levels, first the Master
27 // level is set, then if the PCM element exists, the total level is refined by
28 // adjusting that as well.  If the PCM element has more volume steps, it allows
29 // for finer granularity in the total volume.
30 
31 typedef long alsa_long_t;  // 'long' is required for ALSA API calls.
32 
33 namespace {
34 
35 const char kMasterVolume[] = "Master";
36 const char kPCMVolume[] = "PCM";
37 const double kDefaultMinVolume = -90.0;
38 const double kDefaultMaxVolume = 0.0;
39 const double kPrefVolumeInvalid = -999.0;
40 const int kPrefMuteOff = 0;
41 const int kPrefMuteOn = 1;
42 const int kPrefMuteInvalid = 2;
43 
44 // Maximum number of times that we'll attempt to initialize the mixer.
45 // We'll fail until the ALSA modules have been loaded; see
46 // http://crosbug.com/13162.
47 const int kMaxInitAttempts = 20;
48 
49 // Number of seconds that we'll sleep between each initialization attempt.
50 const int kInitRetrySleepSec = 1;
51 
52 }  // namespace
53 
AudioMixerAlsa()54 AudioMixerAlsa::AudioMixerAlsa()
55     : min_volume_(kDefaultMinVolume),
56       max_volume_(kDefaultMaxVolume),
57       save_volume_(0),
58       mixer_state_(UNINITIALIZED),
59       alsa_mixer_(NULL),
60       elem_master_(NULL),
61       elem_pcm_(NULL),
62       prefs_(NULL),
63       done_event_(true, false) {
64 }
65 
~AudioMixerAlsa()66 AudioMixerAlsa::~AudioMixerAlsa() {
67   if (thread_ != NULL) {
68     {
69       base::AutoLock lock(mixer_state_lock_);
70       mixer_state_ = SHUTTING_DOWN;
71       thread_->message_loop()->PostTask(FROM_HERE,
72           NewRunnableMethod(this, &AudioMixerAlsa::FreeAlsaMixer));
73     }
74     done_event_.Wait();
75 
76     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
77     // A ScopedAllowIO object is required to join the thread when calling Stop.
78     // The worker thread should be idle at this time.
79     // See http://crosbug.com/11110 for discussion.
80     base::ThreadRestrictions::ScopedAllowIO allow_io_for_thread_join;
81     thread_->message_loop()->AssertIdle();
82 
83     thread_->Stop();
84     thread_.reset();
85   }
86 }
87 
Init(InitDoneCallback * callback)88 void AudioMixerAlsa::Init(InitDoneCallback* callback) {
89   DCHECK(callback);
90   if (!InitThread()) {
91     callback->Run(false);
92     delete callback;
93     return;
94   }
95   InitPrefs();
96 
97   {
98     base::AutoLock lock(mixer_state_lock_);
99     if (mixer_state_ == SHUTTING_DOWN)
100       return;
101 
102     // Post the task of starting up, which may block on the order of ms,
103     // so best not to do it on the caller's thread.
104     thread_->message_loop()->PostTask(FROM_HERE,
105         NewRunnableMethod(this, &AudioMixerAlsa::DoInit, callback));
106   }
107 }
108 
InitSync()109 bool AudioMixerAlsa::InitSync() {
110   if (!InitThread())
111     return false;
112   InitPrefs();
113   return InitializeAlsaMixer();
114 }
115 
GetVolumeDb() const116 double AudioMixerAlsa::GetVolumeDb() const {
117   base::AutoLock lock(mixer_state_lock_);
118   if (mixer_state_ != READY)
119     return kSilenceDb;
120 
121   return DoGetVolumeDb_Locked();
122 }
123 
GetVolumeLimits(double * vol_min,double * vol_max)124 bool AudioMixerAlsa::GetVolumeLimits(double* vol_min, double* vol_max) {
125   base::AutoLock lock(mixer_state_lock_);
126   if (mixer_state_ != READY)
127     return false;
128   if (vol_min)
129     *vol_min = min_volume_;
130   if (vol_max)
131     *vol_max = max_volume_;
132   return true;
133 }
134 
SetVolumeDb(double vol_db)135 void AudioMixerAlsa::SetVolumeDb(double vol_db) {
136   base::AutoLock lock(mixer_state_lock_);
137   if (mixer_state_ != READY)
138     return;
139 
140   if (vol_db < kSilenceDb || isnan(vol_db)) {
141     if (isnan(vol_db))
142       LOG(WARNING) << "Got request to set volume to NaN";
143     vol_db = kSilenceDb;
144   }
145 
146   DoSetVolumeDb_Locked(vol_db);
147   prefs_->SetDouble(prefs::kAudioVolume, vol_db);
148 }
149 
IsMute() const150 bool AudioMixerAlsa::IsMute() const {
151   base::AutoLock lock(mixer_state_lock_);
152   if (mixer_state_ != READY)
153     return false;
154   return GetElementMuted_Locked(elem_master_);
155 }
156 
157 // To indicate the volume is not valid yet, a very low volume value is stored.
158 // We compare against a slightly higher value in case of rounding errors.
PrefVolumeValid(double volume)159 static bool PrefVolumeValid(double volume) {
160   return (volume > kPrefVolumeInvalid + 0.1);
161 }
162 
SetMute(bool mute)163 void AudioMixerAlsa::SetMute(bool mute) {
164   base::AutoLock lock(mixer_state_lock_);
165   if (mixer_state_ != READY)
166     return;
167 
168   // Set volume to minimum on mute, since switching the element off does not
169   // always mute as it should.
170 
171   // TODO(davej): Remove save_volume_ and setting volume to minimum if
172   // switching the element off can be guaranteed to mute it.  Currently mute
173   // is done by setting the volume to min_volume_.
174 
175   bool old_value = GetElementMuted_Locked(elem_master_);
176 
177   if (old_value != mute) {
178     if (mute) {
179       save_volume_ = DoGetVolumeDb_Locked();
180       DoSetVolumeDb_Locked(min_volume_);
181     } else {
182       DoSetVolumeDb_Locked(save_volume_);
183     }
184   }
185 
186   SetElementMuted_Locked(elem_master_, mute);
187   prefs_->SetInteger(prefs::kAudioMute, mute ? kPrefMuteOn : kPrefMuteOff);
188 }
189 
GetState() const190 AudioMixer::State AudioMixerAlsa::GetState() const {
191   base::AutoLock lock(mixer_state_lock_);
192   // If we think it's ready, verify it is actually so.
193   if ((mixer_state_ == READY) && (alsa_mixer_ == NULL))
194     mixer_state_ = IN_ERROR;
195   return mixer_state_;
196 }
197 
198 // static
RegisterPrefs(PrefService * local_state)199 void AudioMixerAlsa::RegisterPrefs(PrefService* local_state) {
200   if (!local_state->FindPreference(prefs::kAudioVolume))
201     local_state->RegisterDoublePref(prefs::kAudioVolume, kPrefVolumeInvalid);
202   if (!local_state->FindPreference(prefs::kAudioMute))
203     local_state->RegisterIntegerPref(prefs::kAudioMute, kPrefMuteInvalid);
204 }
205 
206 ////////////////////////////////////////////////////////////////////////////////
207 // Private functions follow
208 
DoInit(InitDoneCallback * callback)209 void AudioMixerAlsa::DoInit(InitDoneCallback* callback) {
210   bool success = false;
211   for (int num_attempts = 0; num_attempts < kMaxInitAttempts; ++num_attempts) {
212     success = InitializeAlsaMixer();
213     if (success) {
214       break;
215     } else {
216       // If the destructor has reset the state, give up.
217       {
218         base::AutoLock lock(mixer_state_lock_);
219         if (mixer_state_ != INITIALIZING)
220           break;
221       }
222       sleep(kInitRetrySleepSec);
223     }
224   }
225 
226   if (success) {
227     BrowserThread::PostTask(
228         BrowserThread::UI, FROM_HERE,
229         NewRunnableMethod(this, &AudioMixerAlsa::RestoreVolumeMuteOnUIThread));
230   }
231 
232   if (callback) {
233     callback->Run(success);
234     delete callback;
235   }
236 }
237 
InitThread()238 bool AudioMixerAlsa::InitThread() {
239   base::AutoLock lock(mixer_state_lock_);
240 
241   if (mixer_state_ != UNINITIALIZED)
242     return false;
243 
244   if (thread_ == NULL) {
245     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
246     thread_.reset(new base::Thread("AudioMixerAlsa"));
247     if (!thread_->Start()) {
248       thread_.reset();
249       return false;
250     }
251   }
252 
253   mixer_state_ = INITIALIZING;
254   return true;
255 }
256 
InitPrefs()257 void AudioMixerAlsa::InitPrefs() {
258   prefs_ = g_browser_process->local_state();
259 }
260 
InitializeAlsaMixer()261 bool AudioMixerAlsa::InitializeAlsaMixer() {
262   // We can block; make sure that we're not on the UI thread.
263   DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
264 
265   base::AutoLock lock(mixer_state_lock_);
266   if (mixer_state_ != INITIALIZING)
267     return false;
268 
269   int err;
270   snd_mixer_t* handle = NULL;
271   const char* card = "default";
272 
273   if ((err = snd_mixer_open(&handle, 0)) < 0) {
274     LOG(ERROR) << "ALSA mixer " << card << " open error: " << snd_strerror(err);
275     return false;
276   }
277 
278   if ((err = snd_mixer_attach(handle, card)) < 0) {
279     LOG(ERROR) << "ALSA Attach to card " << card << " failed: "
280                << snd_strerror(err);
281     snd_mixer_close(handle);
282     return false;
283   }
284 
285   // Verify PCM can be opened, which also instantiates the PCM mixer element
286   // which is needed for finer volume control and for muting by setting to zero.
287   // If it fails, we can still try to use the mixer as best we can.
288   snd_pcm_t* pcm_out_handle;
289   if ((err = snd_pcm_open(&pcm_out_handle,
290                           card,
291                           SND_PCM_STREAM_PLAYBACK,
292                           0)) >= 0) {
293     snd_pcm_close(pcm_out_handle);
294   } else {
295     LOG(WARNING) << "ALSA PCM open: " << snd_strerror(err);
296   }
297 
298   if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) {
299     LOG(ERROR) << "ALSA mixer register error: " << snd_strerror(err);
300     snd_mixer_close(handle);
301     return false;
302   }
303 
304   if ((err = snd_mixer_load(handle)) < 0) {
305     LOG(ERROR) << "ALSA mixer " << card << " load error: %s"
306                << snd_strerror(err);
307     snd_mixer_close(handle);
308     return false;
309   }
310 
311   VLOG(1) << "Opened ALSA mixer " << card << " OK";
312 
313   elem_master_ = FindElementWithName_Locked(handle, kMasterVolume);
314   if (elem_master_) {
315     alsa_long_t long_lo = static_cast<alsa_long_t>(kDefaultMinVolume * 100);
316     alsa_long_t long_hi = static_cast<alsa_long_t>(kDefaultMaxVolume * 100);
317     err = snd_mixer_selem_get_playback_dB_range(
318         elem_master_, &long_lo, &long_hi);
319     if (err != 0) {
320       LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed "
321                    << "for master: " << snd_strerror(err);
322       snd_mixer_close(handle);
323       return false;
324     }
325     min_volume_ = static_cast<double>(long_lo) / 100.0;
326     max_volume_ = static_cast<double>(long_hi) / 100.0;
327   } else {
328     LOG(ERROR) << "Cannot find 'Master' ALSA mixer element on " << card;
329     snd_mixer_close(handle);
330     return false;
331   }
332 
333   elem_pcm_ = FindElementWithName_Locked(handle, kPCMVolume);
334   if (elem_pcm_) {
335     alsa_long_t long_lo = static_cast<alsa_long_t>(kDefaultMinVolume * 100);
336     alsa_long_t long_hi = static_cast<alsa_long_t>(kDefaultMaxVolume * 100);
337     err = snd_mixer_selem_get_playback_dB_range(elem_pcm_, &long_lo, &long_hi);
338     if (err != 0) {
339       LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed for PCM: "
340                    << snd_strerror(err);
341       snd_mixer_close(handle);
342       return false;
343     }
344     min_volume_ += static_cast<double>(long_lo) / 100.0;
345     max_volume_ += static_cast<double>(long_hi) / 100.0;
346   }
347 
348   VLOG(1) << "ALSA volume range is " << min_volume_ << " dB to "
349           << max_volume_ << " dB";
350 
351   alsa_mixer_ = handle;
352   mixer_state_ = READY;
353   return true;
354 }
355 
FreeAlsaMixer()356 void AudioMixerAlsa::FreeAlsaMixer() {
357   if (alsa_mixer_) {
358     snd_mixer_close(alsa_mixer_);
359     alsa_mixer_ = NULL;
360   }
361   done_event_.Signal();
362 }
363 
DoSetVolumeMute(double pref_volume,int pref_mute)364 void AudioMixerAlsa::DoSetVolumeMute(double pref_volume, int pref_mute) {
365   base::AutoLock lock(mixer_state_lock_);
366   if (mixer_state_ != READY)
367     return;
368 
369   // If volume or mute are invalid, set them now to the current actual values.
370   if (!PrefVolumeValid(pref_volume))
371     pref_volume = DoGetVolumeDb_Locked();
372   bool mute = false;
373   if (pref_mute == kPrefMuteInvalid)
374     mute = GetElementMuted_Locked(elem_master_);
375   else
376     mute = (pref_mute == kPrefMuteOn) ? true : false;
377 
378   VLOG(1) << "Setting volume to " << pref_volume << " and mute to " << mute;
379 
380   if (mute) {
381     save_volume_ = pref_volume;
382     DoSetVolumeDb_Locked(min_volume_);
383   } else {
384     DoSetVolumeDb_Locked(pref_volume);
385   }
386 
387   SetElementMuted_Locked(elem_master_, mute);
388 }
389 
RestoreVolumeMuteOnUIThread()390 void AudioMixerAlsa::RestoreVolumeMuteOnUIThread() {
391   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
392   // This happens during init, so set the volume off the UI thread.
393   int mute = prefs_->GetInteger(prefs::kAudioMute);
394   double vol = prefs_->GetDouble(prefs::kAudioVolume);
395   {
396     base::AutoLock lock(mixer_state_lock_);
397     if (mixer_state_ == SHUTTING_DOWN)
398       return;
399     thread_->message_loop()->PostTask(FROM_HERE,
400         NewRunnableMethod(this, &AudioMixerAlsa::DoSetVolumeMute, vol, mute));
401   }
402 }
403 
DoGetVolumeDb_Locked() const404 double AudioMixerAlsa::DoGetVolumeDb_Locked() const {
405   double vol_total = 0.0;
406   if (!GetElementVolume_Locked(elem_master_, &vol_total))
407     return 0.0;
408 
409   double vol_pcm = 0.0;
410   if (elem_pcm_ && GetElementVolume_Locked(elem_pcm_, &vol_pcm))
411     vol_total += vol_pcm;
412 
413   return vol_total;
414 }
415 
DoSetVolumeDb_Locked(double vol_db)416 void AudioMixerAlsa::DoSetVolumeDb_Locked(double vol_db) {
417   double actual_vol = 0.0;
418 
419   // If a PCM volume slider exists, then first set the Master volume to the
420   // nearest volume >= requested volume, then adjust PCM volume down to get
421   // closer to the requested volume.
422   if (elem_pcm_) {
423     SetElementVolume_Locked(elem_master_, vol_db, &actual_vol, 0.9999f);
424     SetElementVolume_Locked(elem_pcm_, vol_db - actual_vol, NULL, 0.5f);
425   } else {
426     SetElementVolume_Locked(elem_master_, vol_db, NULL, 0.5f);
427   }
428 }
429 
FindElementWithName_Locked(snd_mixer_t * handle,const char * element_name) const430 snd_mixer_elem_t* AudioMixerAlsa::FindElementWithName_Locked(
431     snd_mixer_t* handle,
432     const char* element_name) const {
433   snd_mixer_selem_id_t* sid;
434 
435   // Using id_malloc/id_free API instead of id_alloca since the latter gives the
436   // warning: the address of 'sid' will always evaluate as 'true'.
437   if (snd_mixer_selem_id_malloc(&sid))
438     return NULL;
439 
440   snd_mixer_selem_id_set_index(sid, 0);
441   snd_mixer_selem_id_set_name(sid, element_name);
442   snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);
443   if (!elem) {
444     LOG(ERROR) << "ALSA unable to find simple control "
445                << snd_mixer_selem_id_get_name(sid);
446   }
447 
448   snd_mixer_selem_id_free(sid);
449   return elem;
450 }
451 
GetElementVolume_Locked(snd_mixer_elem_t * elem,double * current_vol) const452 bool AudioMixerAlsa::GetElementVolume_Locked(snd_mixer_elem_t* elem,
453                                              double* current_vol) const {
454   alsa_long_t long_vol = 0;
455   int alsa_result = snd_mixer_selem_get_playback_dB(
456       elem, static_cast<snd_mixer_selem_channel_id_t>(0), &long_vol);
457   if (alsa_result != 0) {
458     LOG(WARNING) << "snd_mixer_selem_get_playback_dB() failed: "
459                  << snd_strerror(alsa_result);
460     return false;
461   }
462 
463   *current_vol = static_cast<double>(long_vol) / 100.0;
464   return true;
465 }
466 
SetElementVolume_Locked(snd_mixer_elem_t * elem,double new_vol,double * actual_vol,double rounding_bias)467 bool AudioMixerAlsa::SetElementVolume_Locked(snd_mixer_elem_t* elem,
468                                              double new_vol,
469                                              double* actual_vol,
470                                              double rounding_bias) {
471   alsa_long_t vol_lo = 0;
472   alsa_long_t vol_hi = 0;
473   int alsa_result =
474       snd_mixer_selem_get_playback_volume_range(elem, &vol_lo, &vol_hi);
475   if (alsa_result != 0) {
476     LOG(WARNING) << "snd_mixer_selem_get_playback_volume_range() failed: "
477                  << snd_strerror(alsa_result);
478     return false;
479   }
480   alsa_long_t vol_range = vol_hi - vol_lo;
481   if (vol_range <= 0)
482     return false;
483 
484   alsa_long_t db_lo_int = 0;
485   alsa_long_t db_hi_int = 0;
486   alsa_result =
487       snd_mixer_selem_get_playback_dB_range(elem, &db_lo_int, &db_hi_int);
488   if (alsa_result != 0) {
489     LOG(WARNING) << "snd_mixer_selem_get_playback_dB_range() failed: "
490                  << snd_strerror(alsa_result);
491     return false;
492   }
493 
494   double db_lo = static_cast<double>(db_lo_int) / 100.0;
495   double db_hi = static_cast<double>(db_hi_int) / 100.0;
496   double db_step = static_cast<double>(db_hi - db_lo) / vol_range;
497   if (db_step <= 0.0)
498     return false;
499 
500   if (new_vol < db_lo)
501     new_vol = db_lo;
502 
503   alsa_long_t value = static_cast<alsa_long_t>(rounding_bias +
504       (new_vol - db_lo) / db_step) + vol_lo;
505   alsa_result = snd_mixer_selem_set_playback_volume_all(elem, value);
506   if (alsa_result != 0) {
507     LOG(WARNING) << "snd_mixer_selem_set_playback_volume_all() failed: "
508                  << snd_strerror(alsa_result);
509     return false;
510   }
511 
512   VLOG(1) << "Set volume " << snd_mixer_selem_get_name(elem)
513           << " to " << new_vol << " ==> " << (value - vol_lo) * db_step + db_lo
514           << " dB";
515 
516   if (actual_vol) {
517     alsa_long_t volume = vol_lo;
518     alsa_result = snd_mixer_selem_get_playback_volume(
519         elem, static_cast<snd_mixer_selem_channel_id_t>(0), &volume);
520     if (alsa_result != 0) {
521       LOG(WARNING) << "snd_mixer_selem_get_playback_volume() failed: "
522                    << snd_strerror(alsa_result);
523       return false;
524     }
525     *actual_vol = db_lo + (volume - vol_lo) * db_step;
526 
527     VLOG(1) << "Actual volume " << snd_mixer_selem_get_name(elem)
528             << " now " << *actual_vol << " dB";
529   }
530   return true;
531 }
532 
GetElementMuted_Locked(snd_mixer_elem_t * elem) const533 bool AudioMixerAlsa::GetElementMuted_Locked(snd_mixer_elem_t* elem) const {
534   int enabled = 0;
535   int alsa_result = snd_mixer_selem_get_playback_switch(
536       elem, static_cast<snd_mixer_selem_channel_id_t>(0), &enabled);
537   if (alsa_result != 0) {
538     LOG(WARNING) << "snd_mixer_selem_get_playback_switch() failed: "
539                  << snd_strerror(alsa_result);
540     return false;
541   }
542   return (enabled) ? false : true;
543 }
544 
SetElementMuted_Locked(snd_mixer_elem_t * elem,bool mute)545 void AudioMixerAlsa::SetElementMuted_Locked(snd_mixer_elem_t* elem, bool mute) {
546   int enabled = mute ? 0 : 1;
547   int alsa_result = snd_mixer_selem_set_playback_switch_all(elem, enabled);
548   if (alsa_result != 0) {
549     LOG(WARNING) << "snd_mixer_selem_set_playback_switch_all() failed: "
550                  << snd_strerror(alsa_result);
551   } else {
552     VLOG(1) << "Set playback switch " << snd_mixer_selem_get_name(elem)
553             << " to " << enabled;
554   }
555 }
556 
557 }  // namespace chromeos
558