Bug 1136360 - Account for stream and device latency in stream position calculation. r=jesup
authorMatthew Gregan <kinetik@flim.org>
Fri, 21 Aug 2015 13:53:21 +1200
changeset 258984 ece893a2e16fdd0b903520d434d1aa454f70e1c8
parent 258983 e7c52ea2f221d8c082c463a60e2d9a26f72f6902
child 258985 e7f6748ee57d45b441a9fde36ec9a31da60d6118
push id29268
push userryanvm@gmail.com
push dateTue, 25 Aug 2015 00:37:23 +0000
treeherdermozilla-central@08015770c9d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup
bugs1136360
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1136360 - Account for stream and device latency in stream position calculation. r=jesup
media/libcubeb/src/cubeb_wasapi.cpp
--- a/media/libcubeb/src/cubeb_wasapi.cpp
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -215,16 +215,27 @@ struct cubeb_stream
    *   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;
+  /* Interface pointer to use the stream audio clock. */
+  IAudioClock * audio_clock;
+  /* Frames written to the stream since it was opened. Reset on device
+   * change. Uses mix_params.rate. */
+  UINT64 frames_written;
+  /* Frames written to the (logical) stream since it was first
+   * created. Updated on device change. Uses stream_params.rate. */
+  UINT64 total_frames_written;
+  /* Last valid reported stream position.  Used to ensure the position
+   * reported by stream_get_position increases monotonically. */
+  UINT64 prev_position;
   /* 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
@@ -232,18 +243,19 @@ 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;
+  /* The lock protects all members that are touched by the render thread or
+     change during a device reset, including: audio_clock, audio_stream_volume,
+     client, frames_written, mix_params, total_frames_written, prev_position. */
   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;
@@ -338,40 +350,30 @@ 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)
-{
-  InterlockedExchangeAdd64(&stm->clock, value);
-}
-
-LONG64 clock_get(cubeb_stream * stm)
-{
-  return InterlockedExchangeAdd64(&stm->clock, 0);
-}
-
 bool should_upmix(cubeb_stream * stream)
 {
   return stream->mix_params.channels > stream->stream_params.channels;
 }
 
 bool should_downmix(cubeb_stream * stream)
 {
   return stream->mix_params.channels < stream->stream_params.channels;
 }
 
-float stream_to_mix_samplerate_ratio(cubeb_stream * stream)
+double stream_to_mix_samplerate_ratio(cubeb_stream * stream)
 {
-  auto_lock lock(stream->stream_reset_lock);
-  return float(stream->stream_params.rate) / stream->mix_params.rate;
+  stream->stream_reset_lock->assert_current_thread_owns();
+  return double(stream->stream_params.rate) / stream->mix_params.rate;
 }
 
 /* Upmix function, copies a mono channel into L and R */
 template<typename T>
 void
 mono_to_stereo(T * in, long insamples, T * out, int32_t out_channels)
 {
   for (int i = 0, j = 0; i < insamples; ++i, j += out_channels) {
@@ -447,17 +449,20 @@ 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)));
+  {
+    auto_lock lock(stm->stream_reset_lock);
+    stm->frames_written += out_frames;
+  }
 
   /* 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) {
@@ -683,16 +688,43 @@ HRESULT get_default_endpoint(IMMDevice *
     SafeRelease(enumerator);
     return hr;
   }
 
   SafeRelease(enumerator);
 
   return ERROR_SUCCESS;
 }
+
+double
+current_stream_delay(cubeb_stream * stm)
+{
+  stm->stream_reset_lock->assert_current_thread_owns();
+
+  UINT64 freq;
+  HRESULT hr = stm->audio_clock->GetFrequency(&freq);
+  if (FAILED(hr)) {
+    LOG("GetFrequency failed: %x\n", hr);
+    return 0;
+  }
+
+  UINT64 pos;
+  hr = stm->audio_clock->GetPosition(&pos, NULL);
+  if (FAILED(hr)) {
+    LOG("GetPosition failed: %x\n", hr);
+    return 0;
+  }
+
+  double cur_pos = static_cast<double>(pos) / freq;
+  double max_pos = static_cast<double>(stm->frames_written)  / stm->mix_params.rate;
+  double delay = max_pos - cur_pos;
+  XASSERT(delay >= 0);
+
+  return delay;
+}
 } // namespace anonymous
 
 extern "C" {
 int wasapi_init(cubeb ** context, char const * context_name)
 {
   HRESULT hr;
   auto_com com;
   if (!com.ok()) {
@@ -1064,16 +1096,24 @@ int setup_wasapi_stream(cubeb_stream * s
 
   hr = stm->client->GetService(__uuidof(IAudioStreamVolume),
                                (void **)&stm->audio_stream_volume);
   if (FAILED(hr)) {
     LOG("Could not get the IAudioStreamVolume %x.\n", hr);
     return CUBEB_ERROR;
   }
 
+  XASSERT(stm->frames_written == 0);
+  hr = stm->client->GetService(__uuidof(IAudioClock),
+                               (void **)&stm->audio_clock);
+  if (FAILED(hr)) {
+    LOG("Could not get the IAudioClock %x.\n", hr);
+    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,
@@ -1108,25 +1148,16 @@ 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;
-
-  /* Null out WASAPI-specific state */
-  stm->resampler = NULL;
-  stm->client = 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);
   if (!stm->reconfigure_event) {
     LOG("Can't create the reconfigure event, error: %x\n", GetLastError());
     wasapi_stream_destroy(stm);
     return CUBEB_ERROR;
@@ -1173,16 +1204,21 @@ void close_wasapi_stream(cubeb_stream * 
   stm->client = NULL;
 
   SafeRelease(stm->render_client);
   stm->render_client = NULL;
 
   SafeRelease(stm->audio_stream_volume);
   stm->audio_stream_volume = NULL;
 
+  SafeRelease(stm->audio_clock);
+  stm->audio_clock = NULL;
+  stm->total_frames_written += round(stm->frames_written * stream_to_mix_samplerate_ratio(stm));
+  stm->frames_written = 0;
+
   if (stm->resampler) {
     cubeb_resampler_destroy(stm->resampler);
     stm->resampler = NULL;
   }
 
   free(stm->mix_buffer);
   stm->mix_buffer = NULL;
 }
@@ -1278,18 +1314,34 @@ int wasapi_stream_stop(cubeb_stream * st
   stop_and_join_render_thread(stm);
 
   return CUBEB_OK;
 }
 
 int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position)
 {
   XASSERT(stm && position);
+  auto_lock lock(stm->stream_reset_lock);
 
-  *position = clock_get(stm);
+  /* Calculate how far behind the current stream head the playback cursor is. */
+  uint64_t stream_delay = current_stream_delay(stm) * stm->stream_params.rate;
+
+  /* Calculate the logical stream head in frames at the stream sample rate. */
+  uint64_t max_pos = stm->total_frames_written +
+                     round(stm->frames_written * stream_to_mix_samplerate_ratio(stm));
+
+  *position = max_pos;
+  if (stream_delay <= *position) {
+    *position -= stream_delay;
+  }
+
+  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);