Bug 1136360 - Take into account the output device latency in the clock, and be more robust about rounding error accumulation, in cubeb_wasapi.cpp. r=kinetik, a=sledru
authorPaul Adenot <paul@paul.cx>
Mon, 16 Mar 2015 18:12:38 +0100
changeset 258521 fff936b47a9f
parent 258520 69e54b268783
child 258522 6a5c3aa5b912
push id4686
push userryanvm@gmail.com
push date2015-04-17 16:50 +0000
treeherdermozilla-beta@6a5c3aa5b912 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskinetik, sledru
bugs1136360
milestone38.0
Bug 1136360 - Take into account the output device latency in the clock, and be more robust about rounding error accumulation, in cubeb_wasapi.cpp. r=kinetik, a=sledru
media/libcubeb/src/cubeb_wasapi.cpp
--- a/media/libcubeb/src/cubeb_wasapi.cpp
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -210,16 +210,18 @@ struct cubeb_stream
   /* 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 the clock, to estimate latency. */
+  IAudioClock * audio_clock;
   /* 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
@@ -231,18 +233,26 @@ struct cubeb_stream
   HANDLE shutdown_event;
   /* Set by OnDefaultDeviceChanged when a stream reconfiguration is required.
      The reconfiguration is handled by the render loop thread. */
   HANDLE reconfigure_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 synthesize our clock from the callbacks. */
-  LONG64 clock;
+  /* We synthesize our clock from the callbacks. This in fractional frames, in
+   * the stream samplerate. */
+  double clock;
+  UINT64 prev_position;
+  /* This is the clock value last time we reset the stream. This is in
+   * fractional frames, in the stream samplerate. */
+  double base_clock;
+  /* The latency in frames of the stream */
+  UINT32 latency_frames;
+  UINT64 device_frequency;
   owned_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;
@@ -337,24 +347,38 @@ public:
   }
 private:
   /* refcount for this instance, necessary to implement MSCOM semantics. */
   LONG ref_count;
   HANDLE reconfigure_event;
 };
 
 namespace {
-void clock_add(cubeb_stream * stm, LONG64 value)
+void clock_add(cubeb_stream * stm, double value)
 {
-  InterlockedExchangeAdd64(&stm->clock, value);
+  auto_lock lock(stm->stream_reset_lock);
+  stm->clock += value;
 }
 
-LONG64 clock_get(cubeb_stream * stm)
+UINT64 clock_get(cubeb_stream * stm)
+{
+  auto_lock lock(stm->stream_reset_lock);
+  return UINT64(stm->clock);
+}
+
+void latency_set(cubeb_stream * stm, UINT32 value)
 {
-  return InterlockedExchangeAdd64(&stm->clock, 0);
+  auto_lock lock(stm->stream_reset_lock);
+  stm->latency_frames = value;
+}
+
+UINT32 latency_get(cubeb_stream * stm)
+{
+  auto_lock lock(stm->stream_reset_lock);
+  return stm->latency_frames;
 }
 
 bool should_upmix(cubeb_stream * stream)
 {
   return stream->mix_params.channels > stream->stream_params.channels;
 }
 
 bool should_downmix(cubeb_stream * stream)
@@ -438,18 +462,16 @@ refill(cubeb_stream * stm, float * data,
   if (should_upmix(stm) || should_downmix(stm)) {
     dest = stm->mix_buffer;
   } else {
     dest = data;
   }
 
   long out_frames = cubeb_resampler_fill(stm->resampler, dest, frames_needed);
 
-  clock_add(stm, roundf(frames_needed * stream_to_mix_samplerate_ratio(stm)));
-
   /* XXX: Handle this error. */
   if (out_frames < 0) {
     XASSERT(false);
   }
 
   /* Go in draining mode if we got fewer frames than requested. */
   if (out_frames < frames_needed) {
     LOG("draining.\n");
@@ -511,26 +533,30 @@ wasapi_stream_render_loop(LPVOID stream)
       timeout_count = 0;
     }
     switch (waitResult) {
     case WAIT_OBJECT_0: { /* shutdown */
       is_playing = false;
       /* We don't check if the drain is actually finished here, we just want to
        * shutdown. */
       if (stm->draining) {
+        LOG("DRAINED");
         stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
       }
+
       continue;
     }
     case WAIT_OBJECT_0 + 1: { /* reconfigure */
       /* Close the stream */
       stm->client->Stop();
       {
         auto_lock lock(stm->stream_reset_lock);
         close_wasapi_stream(stm);
+        stm->base_clock = stm->clock;
+        stm->latency_frames = 0;
         /* Reopen a stream and start it immediately. This will automatically pick the
          * new default device for this role. */
         int r = setup_wasapi_stream(stm);
         if (r != CUBEB_OK) {
           /* Don't destroy the stream here, since we expect the caller to do
              so after the error has propagated via the state callback. */
           is_playing = false;
           hr = -1;
@@ -546,26 +572,36 @@ wasapi_stream_render_loop(LPVOID stream)
       hr = stm->client->GetCurrentPadding(&padding);
       if (FAILED(hr)) {
         LOG("Failed to get padding\n");
         is_playing = false;
         continue;
       }
       XASSERT(padding <= stm->buffer_frame_count);
 
+      long available = stm->buffer_frame_count - padding;
+
+      clock_add(stm, available * stream_to_mix_samplerate_ratio(stm));
+
+      UINT64 position = 0;
+      HRESULT hr = stm->audio_clock->GetPosition(&position, NULL);
+      if (SUCCEEDED(hr)) {
+        double playing_frame = stm->mix_params.rate * (double)position / stm->device_frequency;
+        double last_written_frame = stm->clock - stm->base_clock;
+        latency_set(stm, max(last_written_frame - playing_frame, 0));
+      }
+
       if (stm->draining) {
         if (padding == 0) {
           stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
           is_playing = false;
         }
         continue;
       }
 
-      long available = stm->buffer_frame_count - padding;
-
       if (available == 0) {
         continue;
       }
 
       BYTE * data;
       hr = stm->render_client->GetBuffer(available, &data);
       if (SUCCEEDED(hr)) {
         refill(stm, reinterpret_cast<float *>(data), available);
@@ -1040,16 +1076,29 @@ int setup_wasapi_stream(cubeb_stream * s
   }
 
   hr = stm->client->SetEventHandle(stm->refill_event);
   if (FAILED(hr)) {
     LOG("Could set the event handle for the client %x.\n", hr);
     return CUBEB_ERROR;
   }
 
+  hr = stm->client->GetService(__uuidof(IAudioClock),
+                               (void **)&stm->audio_clock);
+  if (FAILED(hr)) {
+    LOG("Could not get IAudioClock: %x.\n", hr);
+    return CUBEB_ERROR;
+  }
+
+  hr = stm->audio_clock->GetFrequency(&stm->device_frequency);
+  if (FAILED(hr)) {
+    LOG("Could not get the device frequency from IAudioClock: %x.\n", hr);
+    return CUBEB_ERROR;
+  }
+
   hr = stm->client->GetService(__uuidof(IAudioRenderClient),
                                (void **)&stm->render_client);
   if (FAILED(hr)) {
     LOG("Could not get the render client %x.\n", hr);
     return CUBEB_ERROR;
   }
 
   hr = stm->client->GetService(__uuidof(IAudioStreamVolume),
@@ -1098,21 +1147,24 @@ 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;
+  stm->clock = 0.0;
+  stm->base_clock = 0.0;
+  stm->latency_frames = 0;
 
   /* Null out WASAPI-specific state */
   stm->resampler = NULL;
   stm->client = NULL;
+  stm->audio_clock = NULL;
   stm->render_client = NULL;
   stm->audio_stream_volume = NULL;
   stm->device_enumerator = NULL;
   stm->notification_client = NULL;
 
   stm->stream_reset_lock = new owned_critical_section();
 
   stm->reconfigure_event = CreateEvent(NULL, 0, 0, NULL);
@@ -1160,16 +1212,19 @@ void close_wasapi_stream(cubeb_stream * 
   stm->stream_reset_lock->assert_current_thread_owns();
 
   SafeRelease(stm->client);
   stm->client = NULL;
 
   SafeRelease(stm->render_client);
   stm->render_client = NULL;
 
+  SafeRelease(stm->audio_clock);
+  stm->audio_clock = NULL;
+
   SafeRelease(stm->audio_stream_volume);
   stm->audio_stream_volume = NULL;
 
   if (stm->resampler) {
     cubeb_resampler_destroy(stm->resampler);
     stm->resampler = NULL;
   }
 
@@ -1269,17 +1324,30 @@ int wasapi_stream_stop(cubeb_stream * st
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position)
 {
   XASSERT(stm && position);
 
-  *position = clock_get(stm);
+  UINT64 clock = clock_get(stm);
+  UINT32 latency = latency_get(stm);
+
+  *position = clock >= latency ? clock - latency : 0;
+
+  /* This can happen if the clock does not increase, for example, because the
+   * WASAPI endpoint buffer is full for now, and the latency naturally decreases
+   * because more samples have been played by the mixer process.
+   * We return the previous value to keep the clock monotonicaly increasing. */
+  if (*position < stm->prev_position) {
+    *position = stm->prev_position;
+  }
+
+  stm->prev_position = *position;
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
 {
   XASSERT(stm && latency);