Bug 698079 - When using the WASAPI backend, always output audio to the default audio device. r=kinetik, a=sledru
authorPaul Adenot <paul@paul.cx>
Mon, 19 Jan 2015 14:16:36 +0100
changeset 243000 20f7d44346da
parent 242999 53f55825252a
child 243001 0411d20465b4
push id4358
push userryanvm@gmail.com
push date2015-01-22 19:57 +0000
treeherdermozilla-beta@62f7b8ea571f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskinetik, sledru
bugs698079
milestone36.0
Bug 698079 - When using the WASAPI backend, always output audio to the default audio device. r=kinetik, a=sledru This is implemented by detecting when the default audio output device changes, stopping the current stream, and starting a new one using the new default audio output device.
media/libcubeb/src/cubeb_wasapi.cpp
--- a/media/libcubeb/src/cubeb_wasapi.cpp
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -21,17 +21,19 @@
 #include "cubeb_resampler.h"
 #include <stdio.h>
 
 /**Taken from winbase.h, Not in MinGW.*/
 #ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
 #define STACK_SIZE_PARAM_IS_A_RESERVATION   0x00010000    // Threads only
 #endif
 
-#if 1
+#define LOGGING_ENABLED
+
+#ifdef LOGGING_ENABLED
 #  define LOG(...) do {         \
   fprintf(stderr, __VA_ARGS__); \
   fprintf(stderr, "\n");        \
 } while(0);
 #else
 #  define LOG(...)
 #endif
 
@@ -102,47 +104,174 @@ private:
   bool need_uninit;
 };
 
 typedef HANDLE (WINAPI *set_mm_thread_characteristics_function)(
                                       const char* TaskName, LPDWORD TaskIndex);
 typedef BOOL (WINAPI *revert_mm_thread_characteristics_function)(HANDLE handle);
 
 extern cubeb_ops const wasapi_ops;
+
+int wasapi_stream_stop(cubeb_stream * stm);
+int wasapi_stream_start(cubeb_stream * stm);
+void close_wasapi_stream(cubeb_stream * stm);
+HRESULT setup_wasapi_stream(cubeb_stream * stm);
+
 }
 
+struct cubeb_stream;
+
+
 struct cubeb
 {
   cubeb_ops const * ops;
   /* Library dynamically opened to increase the render
    * thread priority, and the two function pointers we need. */
   HMODULE mmcss_module;
   set_mm_thread_characteristics_function set_mm_thread_characteristics;
   revert_mm_thread_characteristics_function revert_mm_thread_characteristics;
 };
 
+
+class wasapi_endpoint_notification_client : public IMMNotificationClient
+{
+public:
+  /* The implementation of MSCOM was copied from MSDN. */
+  ULONG STDMETHODCALLTYPE
+  AddRef()
+  {
+    return InterlockedIncrement(&ref_count);
+  }
+
+  ULONG STDMETHODCALLTYPE
+  Release()
+  {
+    ULONG ulRef = InterlockedDecrement(&ref_count);
+    if (0 == ulRef) {
+      delete this;
+    }
+    return ulRef;
+  }
+
+  HRESULT STDMETHODCALLTYPE
+  QueryInterface(REFIID riid, VOID **ppvInterface)
+  {
+    if (IID_IUnknown == riid) {
+      AddRef();
+      *ppvInterface = (IUnknown*)this;
+    } else if (__uuidof(IMMNotificationClient) == riid) {
+      AddRef();
+      *ppvInterface = (IMMNotificationClient*)this;
+    } else {
+      *ppvInterface = NULL;
+      return E_NOINTERFACE;
+    }
+    return S_OK;
+  }
+
+  wasapi_endpoint_notification_client(cubeb_stream * stm)
+    : ref_count(1)
+    , stm(stm)
+  { }
+
+  HRESULT STDMETHODCALLTYPE
+  OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id)
+  {
+    /* we don't support capture for now. */
+    if (flow == eCapture) {
+      return S_OK;
+    }
+    /* all our streams are eMultimedia for now */
+    if (role != eMultimedia) {
+      return S_OK;
+    }
+
+    auto_com autocom;
+
+    /* Close the stream */
+    wasapi_stream_stop(stm);
+    close_wasapi_stream(stm);
+    /* Reopen a stream and start it immediately. This will automatically pick the
+     * new default device for this role. */
+    setup_wasapi_stream(stm);
+    wasapi_stream_start(stm);
+
+    return S_OK;
+  }
+
+  /* The remaining methods are not implemented, they simply log when called (if
+   * log is enabled), for debugging. */
+  HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device_id)
+  {
+    LOG("Audio device added.");
+    return S_OK;
+  };
+
+  HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id)
+  {
+    LOG("Audio device removed.");
+    return S_OK;
+  }
+
+  HRESULT STDMETHODCALLTYPE
+  OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state)
+  {
+    LOG("Audio device state changed.");
+    return S_OK;
+  }
+
+  HRESULT STDMETHODCALLTYPE
+  OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key)
+  {
+    LOG("Audio device property value changed.");
+    return S_OK;
+  }
+private:
+  /* refcount for this instance, necessary to implement MSCOM semantics. */
+  LONG ref_count;
+  /* Pointer to the stream. It is guaranteed that this pointer is
+   * always valid. */
+  cubeb_stream * stm;
+};
+
 struct cubeb_stream
 {
   cubeb * context;
   /* Mixer pameters. We need to convert the input
    * stream to this samplerate/channel layout, as WASAPI
    * does not resample nor upmix itself. */
   cubeb_stream_params mix_params;
   cubeb_stream_params stream_params;
+  /* The latency initially requested for this stream. */
+  unsigned latency;
   cubeb_state_callback state_callback;
   cubeb_data_callback data_callback;
   void * user_ptr;
+
+  /* Lifetime considerations:
+   * - client, render_client, audio_clock and audio_stream_volume are interface
+   *   pointer to the IAudioClient.
+   * - The lifetime for device_enumerator and notification_client, resampler,
+   *   mix_buffer are the same as the cubeb_stream instance. */
+
   /* Main handle on the WASAPI stream. */
   IAudioClient * client;
   /* Interface pointer to use the event-driven interface. */
   IAudioRenderClient * render_client;
   /* Interface pointer to use the clock facilities. */
   IAudioClock * audio_clock;
   /* Interface pointer to use the volume facilities. */
   IAudioStreamVolume * audio_stream_volume;
+  /* Device enumerator to be able to be notified when the default
+   * device change. */
+  IMMDeviceEnumerator * device_enumerator;
+  /* Device notification client, to be able to be notified when the default
+   * audio device changes and route the audio to the new default audio output
+   * device */
+  wasapi_endpoint_notification_client * notification_client;
   /* This event is set by the stream_stop and stream_destroy
    * function, so the render loop can exit properly. */
   HANDLE shutdown_event;
   /* This is set by WASAPI when we should refill the stream. */
   HANDLE refill_event;
   /* Each cubeb_stream has its own thread. */
   HANDLE thread;
   uint64_t clock_freq;
@@ -367,33 +496,76 @@ HANDLE WINAPI set_mm_thread_characterist
   return (HANDLE)1;
 }
 
 BOOL WINAPI revert_mm_thread_characteristics_noop(HANDLE mmcss_handle)
 {
   return true;
 }
 
+HRESULT register_notification_client(cubeb_stream * stm)
+{
+  HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+                                NULL, CLSCTX_INPROC_SERVER,
+                                IID_PPV_ARGS(&stm->device_enumerator));
+
+  if (FAILED(hr)) {
+    LOG("Could not get device enumerator: %x", hr);
+    return hr;
+  }
+
+  stm->notification_client = new wasapi_endpoint_notification_client(stm);
+
+  hr = stm->device_enumerator->RegisterEndpointNotificationCallback(stm->notification_client);
+
+  if (FAILED(hr)) {
+    LOG("Could not register endpoint notification callback: %x", hr);
+    return hr;
+  }
+
+  return hr;
+}
+
+HRESULT unregister_notification_client(cubeb_stream * stm)
+{
+  HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+                                NULL, CLSCTX_INPROC_SERVER,
+                                IID_PPV_ARGS(&stm->device_enumerator));
+
+  if (FAILED(hr)) {
+    LOG("Could not get device enumerator: %x", hr);
+    return hr;
+  }
+
+  stm->device_enumerator->UnregisterEndpointNotificationCallback(stm->notification_client);
+
+  SafeRelease(stm->notification_client);
+  SafeRelease(stm->device_enumerator);
+
+  return S_OK;
+}
+
 HRESULT get_default_endpoint(IMMDevice ** device)
 {
   IMMDeviceEnumerator * enumerator;
   HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
                                 NULL, CLSCTX_INPROC_SERVER,
                                 IID_PPV_ARGS(&enumerator));
+
   if (FAILED(hr)) {
     LOG("Could not get device enumerator.");
     return hr;
   }
   /* eMultimedia is okay for now ("Music, movies, narration, [...]").
    * We will need to change this when we distinguish streams by use-case, other
    * possible values being eConsole ("Games, system notification sounds [...]")
    * and eCommunication ("Voice communication"). */
   hr = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, device);
   if (FAILED(hr)) {
-    LOG("Could not get default audio endpoint.");
+    LOG("Could not get default audio endpoint. %d", __LINE__);
     SafeRelease(enumerator);
     return hr;
   }
 
   SafeRelease(enumerator);
 
   return ERROR_SUCCESS;
 }
@@ -645,38 +817,167 @@ handle_channel_layout(cubeb_stream * stm
      * field with some sound cards. We restore the mix format, and let the rest
      * of the code figure out the right conversion path. */
     **mix_format = hw_mixformat;
   } else if (hr == S_OK) {
     LOG("Requested format accepted by WASAPI.");
   }
 }
 
+int setup_wasapi_stream(cubeb_stream * stm)
+{
+  HRESULT hr;
+  IMMDevice * device;
+  WAVEFORMATEX * mix_format;
+
+  auto_com com;
+
+  hr = get_default_endpoint(&device);
+  if (FAILED(hr)) {
+    LOG("Could not get default endpoint, error: %x", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  /* Get a client. We will get all other interfaces we need from
+   * this pointer. */
+  hr = device->Activate(__uuidof(IAudioClient),
+      CLSCTX_INPROC_SERVER,
+      NULL, (void **)&stm->client);
+  SafeRelease(device);
+  if (FAILED(hr)) {
+    LOG("Could not activate the device to get an audio client: error: %x", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  /* We have to distinguish between the format the mixer uses,
+   * and the format the stream we want to play uses. */
+  hr = stm->client->GetMixFormat(&mix_format);
+  if (FAILED(hr)) {
+    LOG("Could not fetch current mix format from the audio client: error: %x", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  handle_channel_layout(stm, &mix_format, &stm->stream_params);
+
+  /* Shared mode WASAPI always supports float32 sample format, so this
+   * is safe. */
+  stm->mix_params.format = CUBEB_SAMPLE_FLOAT32NE;
+  stm->mix_params.rate = mix_format->nSamplesPerSec;
+  stm->mix_params.channels = mix_format->nChannels;
+
+  hr = stm->client->Initialize(AUDCLNT_SHAREMODE_SHARED,
+      AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
+      AUDCLNT_STREAMFLAGS_NOPERSIST,
+      ms_to_hns(stm->latency),
+      0,
+      mix_format,
+      NULL);
+
+  CoTaskMemFree(mix_format);
+
+  if (FAILED(hr)) {
+    LOG("Unable to initialize audio client: %x.", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->client->GetBufferSize(&stm->buffer_frame_count);
+  if (FAILED(hr)) {
+    LOG("Could not get the buffer size from the client %x.", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  if (should_upmix(stm) || should_downmix(stm)) {
+    stm->mix_buffer = (float *)malloc(frames_to_bytes_before_mix(stm, stm->buffer_frame_count));
+  }
+
+  hr = stm->client->SetEventHandle(stm->refill_event);
+  if (FAILED(hr)) {
+    LOG("Could set the event handle for the client %x.", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->client->GetService(__uuidof(IAudioRenderClient),
+      (void **)&stm->render_client);
+  if (FAILED(hr)) {
+    LOG("Could not get the render client %x.", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->client->GetService(__uuidof(IAudioClock),
+      (void **)&stm->audio_clock);
+  if (FAILED(hr)) {
+    LOG("Could not get the IAudioClock, %x", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->client->GetService(__uuidof(IAudioStreamVolume),
+      (void **)&stm->audio_stream_volume);
+  if (FAILED(hr)) {
+    LOG("Could not get the IAudioStreamVolume %x.", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->audio_clock->GetFrequency(&stm->clock_freq);
+  if (FAILED(hr)) {
+    LOG("failed to get audio clock frequency, %x", hr);
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  /* If we are playing a mono stream, we only resample one channel,
+   * and copy it over, so we are always resampling the number
+   * of channels of the stream, not the number of channels
+   * that WASAPI wants. */
+  stm->resampler = cubeb_resampler_create(stm, stm->stream_params,
+      stm->mix_params.rate,
+      stm->data_callback,
+      stm->buffer_frame_count,
+      stm->user_ptr,
+      CUBEB_RESAMPLER_QUALITY_DESKTOP);
+  if (!stm->resampler) {
+    LOG("Could not get a resampler");
+    wasapi_stream_destroy(stm);
+    return CUBEB_ERROR;
+  }
+
+  return CUBEB_OK;
+}
+
 int
 wasapi_stream_init(cubeb * context, cubeb_stream ** stream,
                    char const * stream_name, cubeb_stream_params stream_params,
                    unsigned int latency, cubeb_data_callback data_callback,
                    cubeb_state_callback state_callback, void * user_ptr)
 {
   HRESULT hr;
-  WAVEFORMATEX * mix_format;
+  int rv;
   auto_com com;
 
   assert(context && stream);
 
   cubeb_stream * stm = (cubeb_stream *)calloc(1, sizeof(cubeb_stream));
 
   assert(stm);
 
   stm->context = context;
   stm->data_callback = data_callback;
   stm->state_callback = state_callback;
   stm->user_ptr = user_ptr;
   stm->stream_params = stream_params;
   stm->draining = false;
+  stm->latency = latency;
 
   stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL);
   stm->refill_event = CreateEvent(NULL, 0, 0, NULL);
 
   if (!stm->shutdown_event) {
     LOG("Can't create the shutdown event, error: %x.", GetLastError());
     wasapi_stream_destroy(stm);
     return CUBEB_ERROR;
@@ -684,161 +985,64 @@ wasapi_stream_init(cubeb * context, cube
 
   if (!stm->refill_event) {
     SafeRelease(stm->shutdown_event);
     LOG("Can't create the refill event, error: %x.", GetLastError());
     wasapi_stream_destroy(stm);
     return CUBEB_ERROR;
   }
 
-  IMMDevice * device;
-  hr = get_default_endpoint(&device);
-  if (FAILED(hr)) {
-    LOG("Could not get default endpoint, error: %x", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  /* Get a client. We will get all other interfaces we need from
-   * this pointer. */
-  hr = device->Activate(__uuidof(IAudioClient),
-                        CLSCTX_INPROC_SERVER,
-                        NULL, (void **)&stm->client);
-  SafeRelease(device);
-  if (FAILED(hr)) {
-    LOG("Could not activate the device to get an audio client: error: %x", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  /* We have to distinguish between the format the mixer uses,
-  * and the format the stream we want to play uses. */
-  hr = stm->client->GetMixFormat(&mix_format);
-  if (FAILED(hr)) {
-    LOG("Could not fetch current mix format from the audio client: error: %x", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  handle_channel_layout(stm, &mix_format, &stream_params);
-
-  /* Shared mode WASAPI always supports float32 sample format, so this
-   * is safe. */
-  stm->mix_params.format = CUBEB_SAMPLE_FLOAT32NE;
-  stm->mix_params.rate = mix_format->nSamplesPerSec;
-  stm->mix_params.channels = mix_format->nChannels;
-
-  hr = stm->client->Initialize(AUDCLNT_SHAREMODE_SHARED,
-                               AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
-                               AUDCLNT_STREAMFLAGS_NOPERSIST,
-                               ms_to_hns(latency),
-                               0,
-                               mix_format,
-                               NULL);
-
-  CoTaskMemFree(mix_format);
-
-  if (FAILED(hr)) {
-    LOG("Unable to initialize audio client: %x.", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
+  rv = setup_wasapi_stream(stm);
+  if (rv != CUBEB_OK) {
+    return rv;
   }
 
-  hr = stm->client->GetBufferSize(&stm->buffer_frame_count);
-  if (FAILED(hr)) {
-    LOG("Could not get the buffer size from the client %x.", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  if (should_upmix(stm) || should_downmix(stm)) {
-    stm->mix_buffer = (float *) malloc(frames_to_bytes_before_mix(stm, stm->buffer_frame_count));
-  }
-
-  hr = stm->client->SetEventHandle(stm->refill_event);
-  if (FAILED(hr)) {
-    LOG("Could set the event handle for the client %x.", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  hr = stm->client->GetService(__uuidof(IAudioRenderClient),
-                               (void **)&stm->render_client);
-  if (FAILED(hr)) {
-    LOG("Could not get the render client %x.", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  hr = stm->client->GetService(__uuidof(IAudioClock),
-                               (void **)&stm->audio_clock);
+  hr = register_notification_client(stm);
   if (FAILED(hr)) {
-    LOG("Could not get the IAudioClock, %x", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  hr = stm->client->GetService(__uuidof(IAudioStreamVolume),
-                               (void **)&stm->audio_stream_volume);
-  if (FAILED(hr)) {
-    LOG("Could not get the IAudioStreamVolume %x.", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  hr = stm->audio_clock->GetFrequency(&stm->clock_freq);
-  if (FAILED(hr)) {
-    LOG("failed to get audio clock frequency, %x", hr);
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
-  }
-
-  /* If we are playing a mono stream, we only resample one channel,
-   * and copy it over, so we are always resampling the number
-   * of channels of the stream, not the number of channels
-   * that WASAPI wants. */
-  stm->resampler = cubeb_resampler_create(stm, stream_params,
-                                          stm->mix_params.rate,
-                                          data_callback,
-                                          stm->buffer_frame_count,
-                                          user_ptr,
-                                          CUBEB_RESAMPLER_QUALITY_DESKTOP);
-  if (!stm->resampler) {
-    LOG("Could not get a resampler");
-    wasapi_stream_destroy(stm);
-    return CUBEB_ERROR;
+    /* this is not fatal, we can still play audio, but we won't be able
+     * to keep using the default audio endpoint if it changes. */
+    LOG("failed to register notification client, %x", hr);
   }
 
   *stream = stm;
 
   return CUBEB_OK;
 }
 
+void close_wasapi_stream(cubeb_stream * stm)
+{
+  assert(stm);
+
+  SafeRelease(stm->client);
+  SafeRelease(stm->render_client);
+  SafeRelease(stm->audio_clock);
+
+  cubeb_resampler_destroy(stm->resampler);
+
+  free(stm->mix_buffer);
+}
+
 void wasapi_stream_destroy(cubeb_stream * stm)
 {
   assert(stm);
 
   if (stm->thread) {
     SetEvent(stm->shutdown_event);
     WaitForSingleObject(stm->thread, INFINITE);
     CloseHandle(stm->thread);
     stm->thread = 0;
   }
 
   SafeRelease(stm->shutdown_event);
   SafeRelease(stm->refill_event);
 
-  SafeRelease(stm->client);
-  SafeRelease(stm->render_client);
-  SafeRelease(stm->audio_clock);
-  SafeRelease(stm->audio_stream_volume);
+  close_wasapi_stream(stm);
 
-  cubeb_resampler_destroy(stm->resampler);
+  unregister_notification_client(stm);
 
-  free(stm->mix_buffer);
   free(stm);
 }
 
 int wasapi_stream_start(cubeb_stream * stm)
 {
   HRESULT hr;
 
   assert(stm);