Bug 698079 - Synthetize the clock when using WASAPI to prevent A/V desynchronization issues when switching the default audio output device. r=kinetik, a=sledru
authorPaul Adenot <paul@paul.cx>
Mon, 19 Jan 2015 14:17:52 +0100
changeset 243001 0411d20465b4
parent 243000 20f7d44346da
child 243002 e35e98044772
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 - Synthetize the clock when using WASAPI to prevent A/V desynchronization issues when switching the default audio output device. r=kinetik, a=sledru
media/libcubeb/src/cubeb_wasapi.cpp
--- a/media/libcubeb/src/cubeb_wasapi.cpp
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -21,22 +21,22 @@
 #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
 
-#define LOGGING_ENABLED
+// #define LOGGING_ENABLED
 
 #ifdef LOGGING_ENABLED
 #  define LOG(...) do {         \
-  fprintf(stderr, __VA_ARGS__); \
-  fprintf(stderr, "\n");        \
+  fprintf(stdout, __VA_ARGS__); \
+  fprintf(stdout, "\n");        \
 } while(0);
 #else
 #  define LOG(...)
 #endif
 
 #define ARRAY_LENGTH(array_) \
   (sizeof(array_) / sizeof(array_[0]))
 
@@ -70,16 +70,30 @@ SafeRelease(HANDLE handle)
 template <typename T>
 void SafeRelease(T * ptr)
 {
   if (ptr) {
     ptr->Release();
   }
 }
 
+struct auto_lock {
+  auto_lock(CRITICAL_SECTION * lock)
+    :lock(lock)
+  {
+    EnterCriticalSection(lock);
+  }
+  ~auto_lock()
+  {
+    LeaveCriticalSection(lock);
+  }
+private:
+  CRITICAL_SECTION * lock;
+};
+
 struct auto_com {
   auto_com()
   : need_uninit(true) {
     HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
     // This is for information purposes only, in anycase, COM is initialized
     // at the end of the constructor.
     if (hr == RPC_E_CHANGED_MODE) {
       // This is an error, COM was not initialized by this function, so it is
@@ -100,41 +114,95 @@ struct auto_com {
       CoUninitialize();
     }
   }
 private:
   bool need_uninit;
 };
 
 typedef HANDLE (WINAPI *set_mm_thread_characteristics_function)(
-                                      const char* TaskName, LPDWORD TaskIndex);
+                                      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);
+int 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;
+
+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 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;
+  /* We synthetize our clock from the callbacks. */
+  LONG64 clock;
+  CRITICAL_SECTION stream_reset_lock;
+  /* Maximum number of frames we can be requested in a callback. */
+  uint32_t buffer_frame_count;
+  /* Resampler instance. Resampling will only happen if necessary. */
+  cubeb_resampler * resampler;
+  /* Buffer used to downmix or upmix to the number of channels the mixer has.
+   * its size is |frames_to_bytes_before_mix(buffer_frame_count)|. */
+  float * mix_buffer;
+  /* True if the stream is draining. */
+  bool draining;
+};
+
 
 class wasapi_endpoint_notification_client : public IMMNotificationClient
 {
 public:
   /* The implementation of MSCOM was copied from MSDN. */
   ULONG STDMETHODCALLTYPE
   AddRef()
   {
@@ -183,20 +251,23 @@ public:
     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);
+    {
+      auto_lock lock(&stm->stream_reset_lock);
+      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)
@@ -227,70 +298,16 @@ public:
 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;
-  /* Maximum number of frames we can be requested in a callback. */
-  uint32_t buffer_frame_count;
-  /* Resampler instance. Resampling will only happen if necessary. */
-  cubeb_resampler * resampler;
-  /* Buffer used to downmix or upmix to the number of channels the mixer has.
-   * its size is |frames_to_bytes_before_mix(buffer_frame_count)|. */
-  float * mix_buffer;
-  /* True if the stream is draining. */
-  bool draining;
-};
-
 namespace {
 bool should_upmix(cubeb_stream * stream)
 {
   return stream->mix_params.channels > stream->stream_params.channels;
 }
 
 bool should_downmix(cubeb_stream * stream)
 {
@@ -365,16 +382,18 @@ refill(cubeb_stream * stm, float * data,
    * avoid a copy. */
   float * dest;
   if (should_upmix(stm) || should_downmix(stm)) {
     dest = stm->mix_buffer;
   } else {
     dest = data;
   }
 
+  stm->clock = InterlockedAdd64(&stm->clock, frames_needed);
+
   long out_frames = cubeb_resampler_fill(stm->resampler, dest, frames_needed);
 
   /* XXX: Handle this error. */
   if (out_frames < 0) {
     assert(false);
   }
 
   /* Go in draining mode if we got fewer frames than requested. */
@@ -453,17 +472,17 @@ wasapi_stream_render_loop(LPVOID stream)
       }
 
       long available = stm->buffer_frame_count - padding;
 
       if (available == 0) {
         continue;
       }
 
-      BYTE* data;
+      BYTE * data;
       hr = stm->render_client->GetBuffer(available, &data);
       if (SUCCEEDED(hr)) {
         refill(stm, reinterpret_cast<float *>(data), available);
 
         hr = stm->render_client->ReleaseBuffer(available, 0);
         if (FAILED(hr)) {
           LOG("failed to release buffer.");
           is_playing = false;
@@ -624,17 +643,17 @@ namespace {
 void wasapi_destroy(cubeb * context)
 {
   if (context->mmcss_module) {
     FreeLibrary(context->mmcss_module);
   }
   free(context);
 }
 
-char const* wasapi_get_backend_id(cubeb * context)
+char const * wasapi_get_backend_id(cubeb * context)
 {
   return "wasapi";
 }
 
 int
 wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
 {
   HRESULT hr;
@@ -903,39 +922,24 @@ int setup_wasapi_stream(cubeb_stream * s
   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,
@@ -968,16 +972,18 @@ wasapi_stream_init(cubeb * context, cube
 
   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->clock = 0;
+  InitializeCriticalSection(&stm->stream_reset_lock);
 
   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;
@@ -1008,17 +1014,16 @@ wasapi_stream_init(cubeb * context, cube
 }
 
 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)
 {
@@ -1028,28 +1033,31 @@ void wasapi_stream_destroy(cubeb_stream 
     SetEvent(stm->shutdown_event);
     WaitForSingleObject(stm->thread, INFINITE);
     CloseHandle(stm->thread);
     stm->thread = 0;
   }
 
   SafeRelease(stm->shutdown_event);
   SafeRelease(stm->refill_event);
+  DeleteCriticalSection(&stm->stream_reset_lock);
 
   close_wasapi_stream(stm);
 
   unregister_notification_client(stm);
 
   free(stm);
 }
 
 int wasapi_stream_start(cubeb_stream * stm)
 {
   HRESULT hr;
 
+  auto_lock lock(&stm->stream_reset_lock);
+
   assert(stm);
 
   stm->thread = (HANDLE) _beginthreadex(NULL, 256 * 1024, wasapi_stream_render_loop, stm, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
   if (stm->thread == NULL) {
     LOG("could not create WASAPI render thread.");
     return CUBEB_ERROR;
   }
 
@@ -1063,16 +1071,18 @@ int wasapi_stream_start(cubeb_stream * s
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_stop(cubeb_stream * stm)
 {
   assert(stm && stm->shutdown_event);
 
+  auto_lock lock(&stm->stream_reset_lock);
+
   SetEvent(stm->shutdown_event);
 
   HRESULT hr = stm->client->Stop();
   if (FAILED(hr)) {
     LOG("could not stop AudioClient");
   }
 
   if (stm->thread) {
@@ -1085,34 +1095,27 @@ int wasapi_stream_stop(cubeb_stream * st
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position)
 {
   assert(stm && position);
 
-  UINT64 pos;
-  HRESULT hr;
-
-  hr = stm->audio_clock->GetPosition(&pos, NULL);
-  if (FAILED(hr)) {
-    LOG("Could not get accurate position: %x\n", hr);
-    return CUBEB_ERROR;
-  }
-
-  *position = static_cast<uint64_t>(static_cast<double>(pos) / stm->clock_freq * stm->stream_params.rate);
+  *position = InterlockedAdd64(&stm->clock, 0);
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
 {
   assert(stm && latency);
 
+  auto_lock lock(&stm->stream_reset_lock);
+
   /* The GetStreamLatency method only works if the
    * AudioClient has been initialized. */
   if (!stm->client) {
     return CUBEB_ERROR;
   }
 
   REFERENCE_TIME latency_hns;
   stm->client->GetStreamLatency(&latency_hns);
@@ -1124,16 +1127,18 @@ int wasapi_stream_get_latency(cubeb_stre
 
 int wasapi_stream_set_volume(cubeb_stream * stm, float volume)
 {
   HRESULT hr;
   uint32_t channels;
   /* up to 9.1 for now */
   float volumes[10];
 
+  auto_lock lock(&stm->stream_reset_lock);
+
   hr = stm->audio_stream_volume->GetChannelCount(&channels);
   if (hr != S_OK) {
     LOG("could not get the channel count: %x", hr);
     return CUBEB_ERROR;
   }
 
   assert(channels <= 10 && "bump the array size");