audio_device_listener_mac.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1// Copyright (c) 2013 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 "media/audio/mac/audio_device_listener_mac.h"
6
7#include "base/bind.h"
8#include "base/files/file_path.h"
9#include "base/logging.h"
10#include "base/mac/libdispatch_task_runner.h"
11#include "base/mac/mac_logging.h"
12#include "base/mac/mac_util.h"
13#include "base/message_loop.h"
14#include "base/pending_task.h"
15#include "media/audio/mac/audio_low_latency_output_mac.h"
16
17namespace media {
18
19// TaskObserver which guarantees that dispatch queue operations such as property
20// listener callbacks are mutually exclusive to operations on the audio thread.
21// TODO(dalecurtis): Instead we should replace the main thread with a dispatch
22// queue.  See http://crbug.com/158170.
23class ExclusiveDispatchQueueTaskObserver : public MessageLoop::TaskObserver {
24 public:
25  ExclusiveDispatchQueueTaskObserver()
26      : property_listener_queue_(new base::mac::LibDispatchTaskRunner(
27            "com.google.chrome.AudioPropertyListenerQueue")),
28        queue_(property_listener_queue_->GetDispatchQueue()),
29        message_loop_(MessageLoop::current()) {
30    // If we're currently on the thread, fire the suspend operation so we don't
31    // end up with an unbalanced resume.
32    if (message_loop_->message_loop_proxy()->BelongsToCurrentThread())
33      SuspendDispatchQueue();
34
35    message_loop_->AddTaskObserver(this);
36  }
37
38  virtual ~ExclusiveDispatchQueueTaskObserver() {
39    message_loop_->RemoveTaskObserver(this);
40
41    // If we're currently on the thread, fire the resume operation so we don't
42    // end up with an unbalanced suspend.
43    if (message_loop_->message_loop_proxy()->BelongsToCurrentThread())
44      ResumeDispatchQueue();
45
46    // This will hang if any listeners are still registered with the queue.
47    property_listener_queue_->Shutdown();
48  }
49
50  virtual void WillProcessTask(const base::PendingTask& pending_task) OVERRIDE {
51    SuspendDispatchQueue();
52  }
53
54  virtual void DidProcessTask(const base::PendingTask& pending_task) OVERRIDE {
55    ResumeDispatchQueue();
56  }
57
58  dispatch_queue_t dispatch_queue() const {
59    return queue_;
60  }
61
62 private:
63  // Issue a synchronous suspend operation.  Benchmarks on a retina 10.8.2
64  // machine show this takes < 20us on average.  dispatch_suspend() is an
65  // asynchronous operation so we need to issue it inside of a synchronous block
66  // to ensure it completes before WillProccesTask() completes.
67  void SuspendDispatchQueue() {
68    dispatch_sync(queue_, ^{
69        dispatch_suspend(queue_);
70    });
71  }
72
73  // Issue an asynchronous resume operation.  Benchmarks on a retina 10.8.2
74  // machine show this takes < 10us on average.
75  void ResumeDispatchQueue() {
76    dispatch_resume(queue_);
77  }
78
79  scoped_refptr<base::mac::LibDispatchTaskRunner> property_listener_queue_;
80  const dispatch_queue_t queue_;
81  MessageLoop* message_loop_;
82
83  DISALLOW_COPY_AND_ASSIGN(ExclusiveDispatchQueueTaskObserver);
84};
85
86// Property address to monitor for device changes.
87const AudioObjectPropertyAddress
88AudioDeviceListenerMac::kDeviceChangePropertyAddress = {
89  kAudioHardwarePropertyDefaultOutputDevice,
90  kAudioObjectPropertyScopeGlobal,
91  kAudioObjectPropertyElementMaster
92};
93
94// Callback from the system when the default device changes; this must be called
95// on the MessageLoop that created the AudioManager.
96// static
97OSStatus AudioDeviceListenerMac::OnDefaultDeviceChanged(
98    AudioObjectID object, UInt32 num_addresses,
99    const AudioObjectPropertyAddress addresses[], void* context) {
100  if (object != kAudioObjectSystemObject)
101    return noErr;
102
103  for (UInt32 i = 0; i < num_addresses; ++i) {
104    if (addresses[i].mSelector == kDeviceChangePropertyAddress.mSelector &&
105        addresses[i].mScope == kDeviceChangePropertyAddress.mScope &&
106        addresses[i].mElement == kDeviceChangePropertyAddress.mElement &&
107        context) {
108      AudioDeviceListenerMac* p_this =
109          static_cast<AudioDeviceListenerMac*>(context);
110      // Device changes on Mac are risky, the OSX API is not thread safe, so
111      // only change devices if we have to.  Again, see http://crbug.com/158170.
112      // TODO(crogers): Remove this once the AUHAL output driver is in.
113      int sample_rate = AUAudioOutputStream::HardwareSampleRate();
114      if (p_this->current_sample_rate_ != sample_rate) {
115        p_this->current_sample_rate_ = sample_rate;
116        p_this->listener_cb_.Run();
117      }
118      break;
119    }
120  }
121
122  return noErr;
123}
124
125AudioDeviceListenerMac::AudioDeviceListenerMac(const base::Closure& listener_cb)
126    : listener_block_(NULL),
127      add_listener_block_func_(NULL),
128      remove_listener_block_func_(NULL),
129      current_sample_rate_(AUAudioOutputStream::HardwareSampleRate()) {
130  // Device changes are hard, lets go shopping!  Sadly OSX does not handle
131  // property listener callbacks in a thread safe manner.  On 10.6 we can set
132  // kAudioHardwarePropertyRunLoop to account for this.  On 10.7 this is broken
133  // and we need to create a dispatch queue to drive callbacks.  However code
134  // running on the dispatch queue must be mutually exclusive with code running
135  // on the audio thread.   ExclusiveDispatchQueueTaskObserver works around this
136  // by pausing and resuming the dispatch queue before and after each pumped
137  // task.  This is not ideal and long term we should replace the audio thread
138  // on OSX with a dispatch queue.  See http://crbug.com/158170 for discussion.
139  // TODO(dalecurtis): Does not fix the cases where
140  // GetDefaultOutputStreamParameters() are called by the browser process.
141  // These are one time events due to renderer side cache and thus unlikely to
142  // occur at the same time as a device callback.  Should be fixed along with
143  // http://crbug.com/137326 using a forced PostTask.
144  if (base::mac::IsOSLionOrLater()) {
145    if (!LoadAudioObjectPropertyListenerBlockFunctions())
146      return;
147
148    task_observer_.reset(new ExclusiveDispatchQueueTaskObserver());
149    listener_block_ = Block_copy(^(
150        UInt32 num_addresses, const AudioObjectPropertyAddress addresses[]) {
151            OnDefaultDeviceChanged(
152                kAudioObjectSystemObject, num_addresses, addresses, this);
153        });
154
155    OSStatus result = add_listener_block_func_(
156        kAudioObjectSystemObject, &kDeviceChangePropertyAddress,
157        task_observer_->dispatch_queue(), listener_block_);
158
159    if (result != noErr) {
160      task_observer_.reset();
161      Block_release(listener_block_);
162      listener_block_ = NULL;
163      OSSTATUS_DLOG(ERROR, result)
164          << "AudioObjectAddPropertyListenerBlock() failed!";
165      return;
166    }
167  } else {
168    const AudioObjectPropertyAddress kRunLoopAddress = {
169      kAudioHardwarePropertyRunLoop,
170      kAudioObjectPropertyScopeGlobal,
171      kAudioObjectPropertyElementMaster
172    };
173
174    CFRunLoopRef run_loop = CFRunLoopGetCurrent();
175    UInt32 size = sizeof(CFRunLoopRef);
176    OSStatus result = AudioObjectSetPropertyData(
177        kAudioObjectSystemObject, &kRunLoopAddress, 0, 0, size, &run_loop);
178    if (result != noErr) {
179      OSSTATUS_DLOG(ERROR, result) << "Failed to set property listener thread.";
180      return;
181    }
182
183    result = AudioObjectAddPropertyListener(
184        kAudioObjectSystemObject, &kDeviceChangePropertyAddress,
185        &AudioDeviceListenerMac::OnDefaultDeviceChanged, this);
186
187    if (result != noErr) {
188      OSSTATUS_DLOG(ERROR, result)
189          << "AudioObjectAddPropertyListener() failed!";
190      return;
191    }
192  }
193
194  listener_cb_ = listener_cb;
195}
196
197AudioDeviceListenerMac::~AudioDeviceListenerMac() {
198  DCHECK(thread_checker_.CalledOnValidThread());
199  if (listener_cb_.is_null())
200    return;
201
202  if (task_observer_) {
203    OSStatus result = remove_listener_block_func_(
204        kAudioObjectSystemObject, &kDeviceChangePropertyAddress,
205        task_observer_->dispatch_queue(), listener_block_);
206
207    OSSTATUS_DLOG_IF(ERROR, result != noErr, result)
208        << "AudioObjectRemovePropertyListenerBlock() failed!";
209
210    // Task observer will wait for all outstanding callbacks to complete.
211    task_observer_.reset();
212    Block_release(listener_block_);
213    return;
214  }
215
216  // Since we're running on the same CFRunLoop, there can be no outstanding
217  // callbacks in flight.
218  OSStatus result = AudioObjectRemovePropertyListener(
219      kAudioObjectSystemObject, &kDeviceChangePropertyAddress,
220      &AudioDeviceListenerMac::OnDefaultDeviceChanged, this);
221  OSSTATUS_DLOG_IF(ERROR, result != noErr, result)
222      << "AudioObjectRemovePropertyListener() failed!";
223}
224
225bool AudioDeviceListenerMac::LoadAudioObjectPropertyListenerBlockFunctions() {
226  // Dynamically load required block functions.
227  // TODO(dalecurtis): Remove once the deployment target is > 10.6.
228  std::string error;
229  base::NativeLibrary core_audio = base::LoadNativeLibrary(base::FilePath(
230      "/System/Library/Frameworks/CoreAudio.framework/Versions/Current/"
231      "CoreAudio"), &error);
232  if (!error.empty()) {
233    LOG(ERROR) << "Could not open CoreAudio library: " << error;
234    return false;
235  }
236
237  core_audio_lib_.Reset(core_audio);
238  add_listener_block_func_ =
239      reinterpret_cast<AudioObjectPropertyListenerBlockT>(
240          core_audio_lib_.GetFunctionPointer(
241              "AudioObjectAddPropertyListenerBlock"));
242  remove_listener_block_func_ =
243      reinterpret_cast<AudioObjectPropertyListenerBlockT>(
244          core_audio_lib_.GetFunctionPointer(
245              "AudioObjectRemovePropertyListenerBlock"));
246
247  // If either function failed to load, skip listener registration.
248  if (!add_listener_block_func_ || !remove_listener_block_func_) {
249    DLOG(ERROR) << "Failed to load audio property listener block functions!";
250    return false;
251  }
252
253  return true;
254}
255
256}  // namespace media
257