wasapi: Implement support for >2 channels
authorNirbheek Chauhan <nirbheek@centricular.com>
Wed, 24 Jan 2018 02:50:38 +0000 (08:20 +0530)
committerNirbheek Chauhan <nirbheek@centricular.com>
Wed, 31 Jan 2018 09:28:21 +0000 (14:58 +0530)
We need to parse the WAVEFORMATEXTENSIBLE structure, figure out what
positions the channels have (if they are positional), and reorder them
as necessary.

https://bugzilla.gnome.org/show_bug.cgi?id=792897

sys/wasapi/gstwasapisink.c
sys/wasapi/gstwasapisink.h
sys/wasapi/gstwasapisrc.c
sys/wasapi/gstwasapisrc.h
sys/wasapi/gstwasapiutil.c
sys/wasapi/gstwasapiutil.h

index da54d8e..7a89cca 100644 (file)
@@ -48,10 +48,7 @@ GST_DEBUG_CATEGORY_STATIC (gst_wasapi_sink_debug);
 static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
     GST_PAD_SINK,
     GST_PAD_ALWAYS,
-    GST_STATIC_CAPS ("audio/x-raw, "
-        "format = (string) " GST_AUDIO_FORMATS_ALL ", "
-        "layout = (string) interleaved, "
-        "rate = " GST_AUDIO_RATE_RANGE ", channels = (int) [1, 2]"));
+    GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS));
 
 #define DEFAULT_ROLE    GST_WASAPI_DEVICE_ROLE_CONSOLE
 #define DEFAULT_MUTE    FALSE
@@ -184,6 +181,7 @@ gst_wasapi_sink_finalize (GObject * object)
     self->cached_caps = NULL;
   }
 
+  g_clear_pointer (&self->positions, g_free);
   g_clear_pointer (&self->device, g_free);
   self->mute = FALSE;
 
@@ -267,14 +265,20 @@ gst_wasapi_sink_get_caps (GstBaseSink * bsink, GstCaps * filter)
       goto out;
     }
 
-    caps =
-        gst_wasapi_util_waveformatex_to_caps ((WAVEFORMATEXTENSIBLE *) format,
-        template_caps);
+    gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format,
+        template_caps, &caps, &self->positions);
     if (caps == NULL) {
       GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL), ("unknown format"));
       goto out;
     }
 
+    {
+      gchar *pos_str = gst_audio_channel_positions_to_string (self->positions,
+          format->nChannels);
+      GST_INFO_OBJECT (self, "positions are: %s", pos_str);
+      g_free (pos_str);
+    }
+
     self->mix_format = format;
     gst_caps_replace (&self->cached_caps, caps);
     gst_caps_unref (template_caps);
@@ -400,6 +404,9 @@ gst_wasapi_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec)
   self->render_client = render_client;
   render_client = NULL;
 
+  gst_audio_ring_buffer_set_channel_positions (
+      GST_AUDIO_BASE_SINK (self)->ringbuffer, self->positions);
+
   res = TRUE;
 
 beach:
index 4078fb3..8af96c0 100644 (file)
@@ -50,6 +50,10 @@ struct _GstWasapiSink
   WAVEFORMATEX *mix_format;
   /* The probed caps that we can accept */
   GstCaps *cached_caps;
+  /* The channel positions in the data to be written to the device we
+   * will pass this to GstAudioRingbuffer so it can to it translate
+   * from the native GStreamer channel layout. */
+  GstAudioChannelPosition *positions;
 
   /* properties */
   gint role;
index 0914d1c..3825cf8 100644 (file)
@@ -46,10 +46,7 @@ GST_DEBUG_CATEGORY_STATIC (gst_wasapi_src_debug);
 static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
     GST_PAD_SRC,
     GST_PAD_ALWAYS,
-    GST_STATIC_CAPS ("audio/x-raw, "
-        "format = (string) " GST_AUDIO_FORMATS_ALL ", "
-        "layout = (string) interleaved, "
-        "rate = " GST_AUDIO_RATE_RANGE ", channels = (int) [1, 2]"));
+    GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS));
 
 #define DEFAULT_ROLE    GST_WASAPI_DEVICE_ROLE_CONSOLE
 
@@ -185,6 +182,7 @@ gst_wasapi_src_finalize (GObject * object)
   CoUninitialize ();
 
   g_clear_pointer (&self->cached_caps, gst_caps_unref);
+  g_clear_pointer (&self->positions, g_free);
   g_clear_pointer (&self->device, g_free);
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
@@ -261,14 +259,20 @@ gst_wasapi_src_get_caps (GstBaseSrc * bsrc, GstCaps * filter)
       goto out;
     }
 
-    caps =
-        gst_wasapi_util_waveformatex_to_caps ((WAVEFORMATEXTENSIBLE *) format,
-        template_caps);
+    gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format,
+        template_caps, &caps, &self->positions);
     if (caps == NULL) {
       GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL), ("unknown format"));
       goto out;
     }
 
+    {
+      gchar *pos_str = gst_audio_channel_positions_to_string (self->positions,
+          format->nChannels);
+      GST_INFO_OBJECT (self, "positions are: %s", pos_str);
+      g_free (pos_str);
+    }
+
     self->mix_format = format;
     gst_caps_replace (&self->cached_caps, caps);
     gst_caps_unref (template_caps);
@@ -409,6 +413,9 @@ gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec)
   self->client_clock_freq = client_clock_freq;
   self->capture_client = capture_client;
 
+  gst_audio_ring_buffer_set_channel_positions (
+      GST_AUDIO_BASE_SRC (self)->ringbuffer, self->positions);
+
   res = TRUE;
 
 beach:
index 1fa9390..88be532 100644 (file)
@@ -52,6 +52,10 @@ struct _GstWasapiSrc
   WAVEFORMATEX *mix_format;
   /* The probed caps that we can accept */
   GstCaps *cached_caps;
+  /* The channel positions in the data read from the device
+   * we will pass this to GstAudioRingbuffer so it can
+   * translate it to the native GStreamer channel layout. */
+  GstAudioChannelPosition *positions;
 
   /* properties */
   gint role;
index ed9f508..170de45 100644 (file)
@@ -61,6 +61,31 @@ const IID IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483,
 };
 #endif
 
+static struct {
+  guint64 wasapi_pos;
+  GstAudioChannelPosition gst_pos;
+} wasapi_to_gst_pos[] = {
+  {SPEAKER_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT},
+  {SPEAKER_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT},
+  {SPEAKER_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER},
+  {SPEAKER_LOW_FREQUENCY, GST_AUDIO_CHANNEL_POSITION_LFE1},
+  {SPEAKER_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_REAR_LEFT},
+  {SPEAKER_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT},
+  {SPEAKER_FRONT_LEFT_OF_CENTER, GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER},
+  {SPEAKER_FRONT_RIGHT_OF_CENTER, GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER},
+  {SPEAKER_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_REAR_CENTER},
+  /* Enum values diverge from this point onwards */
+  {SPEAKER_SIDE_LEFT, GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT},
+  {SPEAKER_SIDE_RIGHT, GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT},
+  {SPEAKER_TOP_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_CENTER},
+  {SPEAKER_TOP_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT},
+  {SPEAKER_TOP_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER},
+  {SPEAKER_TOP_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT},
+  {SPEAKER_TOP_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT},
+  {SPEAKER_TOP_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER},
+  {SPEAKER_TOP_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT},
+};
+
 GType
 gst_wasapi_device_role_get_type (void)
 {
@@ -212,7 +237,7 @@ gst_wasapi_util_hresult_to_string (HRESULT hr)
 
 gboolean
 gst_wasapi_util_get_device_client (GstElement * element,
-    gboolean capture, gint role, const wchar_t * device_name,
+    gboolean capture, gint role, const wchar_t * device_strid,
     IAudioClient ** ret_client)
 {
   gboolean res = FALSE;
@@ -224,12 +249,12 @@ gst_wasapi_util_get_device_client (GstElement * element,
   hr = CoCreateInstance (&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
       &IID_IMMDeviceEnumerator, (void **) &enumerator);
   if (hr != S_OK) {
-    GST_ERROR_OBJECT (element, "CoCreateInstance (MMDeviceEnumerator) failed:"
-        "%s", gst_wasapi_util_hresult_to_string (hr));
+    GST_ERROR_OBJECT (element, "CoCreateInstance (MMDeviceEnumerator) failed"
+        "%s", gst_wasapi_util_hresult_to_string (hr));
     goto beach;
   }
 
-  if (!device_name) {
+  if (!device_strid) {
     hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint (enumerator,
         capture ? eCapture : eRender, role, &device);
     if (hr != S_OK) {
@@ -239,10 +264,10 @@ gst_wasapi_util_get_device_client (GstElement * element,
       goto beach;
     }
   } else {
-    hr = IMMDeviceEnumerator_GetDevice (enumerator, device_name, &device);
+    hr = IMMDeviceEnumerator_GetDevice (enumerator, device_strid, &device);
     if (hr != S_OK) {
       GST_ERROR_OBJECT (element, "IMMDeviceEnumerator::GetDevice (%S) failed"
-          ": %s", device_name, gst_wasapi_util_hresult_to_string (hr));
+          ": %s", device_strid, gst_wasapi_util_hresult_to_string (hr));
       goto beach;
     }
   }
@@ -379,13 +404,71 @@ gst_waveformatex_to_audio_format (WAVEFORMATEXTENSIBLE * format)
   return fmt_str;
 }
 
-GstCaps *
-gst_wasapi_util_waveformatex_to_caps (WAVEFORMATEXTENSIBLE * format,
-    GstCaps * template_caps)
+static void
+gst_wasapi_util_channel_position_all_none (guint channels,
+    GstAudioChannelPosition * position)
+{
+  int ii;
+  for (ii = 0; ii < channels; ii++)
+    position[ii] = GST_AUDIO_CHANNEL_POSITION_NONE;
+}
+
+/* Parse WAVEFORMATEX to get the gstreamer channel mask, and the wasapi channel
+ * positions so GstAudioRingbuffer can reorder the audio data to match the
+ * gstreamer channel order. */
+static guint64
+gst_wasapi_util_waveformatex_to_channel_mask (WAVEFORMATEXTENSIBLE * format,
+    GstAudioChannelPosition ** out_position)
+{
+  int ii;
+  guint64 mask = 0;
+  WORD nChannels = format->Format.nChannels;
+  DWORD dwChannelMask = format->dwChannelMask;
+  GstAudioChannelPosition *pos = NULL;
+
+  pos = g_new (GstAudioChannelPosition, nChannels);
+  gst_wasapi_util_channel_position_all_none (nChannels, pos);
+
+  /* Too many channels, have to assume that they are all non-positional */
+  if (nChannels > G_N_ELEMENTS (wasapi_to_gst_pos)) {
+    GST_INFO ("wasapi: got too many (%i) channels, assuming non-positional",
+        nChannels);
+    goto out;
+  }
+
+  /* Too many bits in the channel mask, and the bits don't match nChannels */
+  if (dwChannelMask >> (G_N_ELEMENTS (wasapi_to_gst_pos) + 1) != 0) {
+    GST_WARNING ("wasapi: too many bits in channel mask (%lu), assuming "
+        "non-positional", dwChannelMask);
+    goto out;
+  }
+
+  /* Map WASAPI's channel mask to Gstreamer's channel mask and positions.
+   * If the no. of bits in the mask > nChannels, we will ignore the extra. */
+  for (ii = 0; ii < nChannels; ii++) {
+    if (!(dwChannelMask & wasapi_to_gst_pos[ii].wasapi_pos))
+      /* Non-positional or unknown position, warn? */
+      continue;
+    mask |= G_GUINT64_CONSTANT(1) << wasapi_to_gst_pos[ii].gst_pos;
+    pos[ii] = wasapi_to_gst_pos[ii].gst_pos;
+  }
+
+out:
+  if (out_position)
+    *out_position = pos;
+  return mask;
+}
+
+gboolean
+gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format,
+    GstCaps * template_caps, GstCaps ** out_caps,
+    GstAudioChannelPosition ** out_positions)
 {
   int ii;
   const gchar *afmt;
-  GstCaps *caps = gst_caps_copy (template_caps);
+  guint64 channel_mask;
+
+  *out_caps = NULL;
 
   /* TODO: handle SPDIF and other encoded formats */
 
@@ -395,23 +478,31 @@ gst_wasapi_util_waveformatex_to_caps (WAVEFORMATEXTENSIBLE * format,
       format->Format.wFormatTag != WAVE_FORMAT_IEEE_FLOAT &&
       format->Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE)
     /* Unhandled format tag */
-    return NULL;
+    return FALSE;
 
   /* WASAPI can only tell us one canonical mix format that it will accept. The
    * alternative is calling IsFormatSupported on all combinations of formats.
    * Instead, it's simpler and faster to require conversion inside gstreamer */
   afmt = gst_waveformatex_to_audio_format (format);
   if (afmt == NULL)
-    return NULL;
+    return FALSE;
+
+  *out_caps = gst_caps_copy (template_caps);
+
+  /* This will always return something that might be usable */
+  channel_mask =
+      gst_wasapi_util_waveformatex_to_channel_mask (format, out_positions);
 
-  for (ii = 0; ii < gst_caps_get_size (caps); ii++) {
-    GstStructure *s = gst_caps_get_structure (caps, ii);
+  for (ii = 0; ii < gst_caps_get_size (*out_caps); ii++) {
+    GstStructure *s = gst_caps_get_structure (*out_caps, ii);
 
     gst_structure_set (s,
         "format", G_TYPE_STRING, afmt,
         "channels", G_TYPE_INT, format->Format.nChannels,
-        "rate", G_TYPE_INT, format->Format.nSamplesPerSec, NULL);
+        "rate", G_TYPE_INT, format->Format.nSamplesPerSec,
+        "channel-mask", GST_TYPE_BITMASK, channel_mask,
+        NULL);
   }
 
-  return caps;
+  return TRUE;
 }
index 0e6dc8b..cf8d884 100644 (file)
 
 #include <audioclient.h>
 
+/* Static Caps shared between source, sink, and device provider */
+#define GST_WASAPI_STATIC_CAPS "audio/x-raw, " \
+        "format = (string) " GST_AUDIO_FORMATS_ALL ", " \
+        "layout = (string) interleaved, " \
+        "rate = " GST_AUDIO_RATE_RANGE ", " \
+        "channels = " GST_AUDIO_CHANNELS_RANGE
+
 /* Device role enum property */
 typedef enum
 {
@@ -59,7 +66,8 @@ gboolean gst_wasapi_util_get_capture_client (GstElement * element,
 gboolean gst_wasapi_util_get_clock (GstElement * element,
     IAudioClient * client, IAudioClock ** ret_clock);
 
-GstCaps *gst_wasapi_util_waveformatex_to_caps (WAVEFORMATEXTENSIBLE * format,
-    GstCaps * template_caps);
+gboolean gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format,
+    GstCaps * template_caps, GstCaps ** out_caps,
+    GstAudioChannelPosition ** out_positions);
 
 #endif /* __GST_WASAPI_UTIL_H__ */