[pulseaudio-discuss] [RFC] [PATCH] alsa: Extract supported formats from HDMI ELD

Arun Raghavan arun.raghavan at collabora.co.uk
Fri Jun 14 04:56:28 PDT 2013


This parses the CEA SAD field from the ELD data we get from HDMI
receivers. The interesting bits are related to non-PCM formats, since
this allows us to automaticall detect what receivers support and let
applications then decide whether they need to perform decoding or not.
---

(I haven't been able to test this one properly since my HDMI/DP port is
refusing to output audio. The ELD parsing should be correct thanks to David
sharing some ELD blobs from his hardware.

This isn't good to go yet -- there are some open issues such as conflict with
manually set formats and the user impact of this change, especially when
applications transparently choose what format to use.)

 src/Makefile.am                     |   1 +
 src/modules/alsa/alsa-util.c        | 172 +++++++++++++++++++++++++++++++++++-
 src/modules/alsa/alsa-util.h        |  17 +++-
 src/modules/alsa/module-alsa-card.c |  53 ++++++++++-
 src/pulse/proplist.h                |   3 +
 src/pulsecore/format-util.c         |  30 +++++++
 src/pulsecore/format-util.h         |  36 ++++++++
 7 files changed, 307 insertions(+), 5 deletions(-)
 create mode 100644 src/pulsecore/format-util.c
 create mode 100644 src/pulsecore/format-util.h

diff --git a/src/Makefile.am b/src/Makefile.am
index 2521670..ca37437 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -620,6 +620,7 @@ libpulsecommon_ at PA_MAJORMINOR@_la_SOURCES = \
 		pulsecore/dynarray.c pulsecore/dynarray.h \
 		pulsecore/endianmacros.h \
 		pulsecore/flist.c pulsecore/flist.h \
+		pulsecore/format-util.c pulsecore/format-util.h \
 		pulsecore/g711.c pulsecore/g711.h \
 		pulsecore/hashmap.c pulsecore/hashmap.h \
 		pulsecore/i18n.c pulsecore/i18n.h \
diff --git a/src/modules/alsa/alsa-util.c b/src/modules/alsa/alsa-util.c
index b556349..235f2ad 100644
--- a/src/modules/alsa/alsa-util.c
+++ b/src/modules/alsa/alsa-util.c
@@ -1598,7 +1598,7 @@ int pa_alsa_get_hdmi_eld(snd_hctl_t *hctl, int device, pa_hdmi_eld *eld) {
     snd_ctl_elem_info_t *info;
     snd_ctl_elem_value_t *value;
     uint8_t *elddata;
-    unsigned int eldsize, mnl;
+    unsigned int eldsize, mnl, i;
 
     pa_assert(eld != NULL);
 
@@ -1640,5 +1640,175 @@ int pa_alsa_get_hdmi_eld(snd_hctl_t *hctl, int device, pa_hdmi_eld *eld) {
     if (mnl)
         pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device);
 
+    eld->sad_count = elddata[5] >> 4;
+    if (eld->sad_count > PA_CEA_SAD_MAX) {
+        pa_log_debug("SAD_Count too high (device=%d, sad_count=%d", device, eld->sad_count);
+        return -1;
+    }
+
+    for (i = 0; i < eld->sad_count; i++) {
+        unsigned int offset = 20 + mnl + (i * 3);
+
+        if (eldsize < offset + 3) {
+            pa_log_debug("ELD smaller than required for SAD_Count (device=%d, sad_count=%d", device, eld->sad_count);
+            return -1;
+        }
+
+        eld->sad[i].format = (elddata[offset] & 0x7f) >> 3;
+        eld->sad[i].channels = (elddata[offset] & 0x7) + 1;
+        eld->sad[i].rates = elddata[offset + 1] & 0x7f;
+        eld->sad[i].data = elddata[offset + 2];
+    }
+
     return 0;
 }
+
+/* Values taken from: https://en.wikipedia.org/wiki/Extended_display_identification_data
+ * and Linux kernel sources: sound/pci/hda/hda_eld.c */
+#define CEA_SAD_FORMAT_LPCM     1
+#define CEA_SAD_FORMAT_AC3      2
+#define CEA_SAD_FORMAT_MPEG1    3
+#define CEA_SAD_FORMAT_MP3      4
+#define CEA_SAD_FORMAT_MPEG2    5
+#define CEA_SAD_FORMAT_AAC      6
+#define CEA_SAD_FORMAT_DTS      7
+#define CEA_SAD_FORMAT_ATRAC    8
+#define CEA_SAD_FORMAT_SACD     9
+#define CEA_SAD_FORMAT_EAC3     10
+#define CEA_SAD_FORMAT_DTSHD    11
+#define CEA_SAD_FORMAT_DOLBY_TRUEHD 12
+#define CEA_SAD_FORMAT_DST      13
+#define CEA_SAD_FORMAT_WMAPRO   14
+
+/* Returns: a valid pa_format_info if everything went well, an invalid
+ * pa_format_info if the format was unknown to us, and NULL if there was an
+ * error in parsing the SAD */
+static pa_format_info* cea_sad_to_format_info(pa_cea_sad *sad) {
+    pa_format_info *format = pa_format_info_new();
+
+    switch (sad->format) {
+        case CEA_SAD_FORMAT_LPCM:
+            format->encoding = PA_ENCODING_PCM;
+            break;
+
+        case CEA_SAD_FORMAT_AC3:
+            format->encoding = PA_ENCODING_AC3_IEC61937;
+            break;
+
+        case CEA_SAD_FORMAT_EAC3:
+            format->encoding = PA_ENCODING_EAC3_IEC61937;
+            break;
+
+        case CEA_SAD_FORMAT_DTS:
+            format->encoding = PA_ENCODING_DTS_IEC61937;
+            break;
+
+        case CEA_SAD_FORMAT_MPEG1:
+        case CEA_SAD_FORMAT_MP3:
+            format->encoding = PA_ENCODING_MPEG_IEC61937;
+            break;
+
+        case CEA_SAD_FORMAT_MPEG2:
+            format->encoding = PA_ENCODING_MPEG2_AAC_IEC61937;
+            break;
+
+        default:
+            /* Unsupported format */
+            format->encoding = PA_ENCODING_INVALID;
+            goto out;
+    }
+
+    if (format->encoding == PA_ENCODING_PCM) {
+        /* Currently, we do not export details of PCM rates, sample formats,
+         * etc. supported in sink/source formats since the core will plug in
+         * format conversion, resampling and remapping as needed. This may
+         * change in the future if we decided to add more fine-grained format
+         * negotiation. */
+    } else {
+        int i = 0, rates[8] = { 0, };
+
+        /* The SAD channels is an "upto" number */
+        pa_format_info_set_prop_int_range(format, PA_PROP_FORMAT_CHANNELS, 1, sad->channels);
+
+        if ((sad->rates & 0x7f) == 0) {
+            pa_log_debug("No known rates set in SAD");
+            goto error;
+        }
+
+        if (sad->rates & 0x1)
+            rates[i++] = 32000;
+        if (sad->rates & 0x2)
+            rates[i++] = 44100;
+        if (sad->rates & 0x4)
+            rates[i++] = 48000;
+        if (sad->rates & 0x8)
+            rates[i++] = 88200;
+        if (sad->rates & 0xc)
+            rates[i++] = 96000;
+        if (sad->rates & 0x10)
+            rates[i++] = 176000;
+        if (sad->rates & 0x20)
+            rates[i++] = 192000;
+
+        pa_format_info_set_prop_int_array(format, PA_PROP_FORMAT_RATE, rates, i);
+
+        /* For all the compressed formats we support, if the data field is the
+         * maximum supported bitrate. */
+        if (sad->data)
+            pa_format_info_set_prop_int_range(format, PA_PROP_FORMAT_BITRATE, 0, sad->data * 8000);
+    }
+
+out:
+    return format;
+
+error:
+    pa_format_info_free(format);
+    format = NULL;
+
+    goto out;
+}
+
+pa_idxset* pa_alsa_formats_from_eld(pa_hdmi_eld *eld) {
+    pa_idxset *ret = pa_idxset_new(NULL, NULL);
+    unsigned int i;
+    bool have_pcm = false;
+
+    if (eld->sad_count == 0)
+        goto out;
+
+    for (i = 0; i < eld->sad_count; i++) {
+        pa_format_info *format = cea_sad_to_format_info(&eld->sad[i]);
+
+        if (!format) {
+            pa_log_debug("Error while parsing SAD");
+            goto error;
+        }
+
+        if (format->encoding == PA_ENCODING_INVALID) {
+            /* Skip unknown format */
+            pa_format_info_free(format);
+            continue;
+        }
+
+        if (format->encoding == PA_ENCODING_PCM) {
+            /* Since we don't distinguish between different PCM SADs, only use the first */
+            if (have_pcm) {
+                pa_format_info_free(format);
+                continue;
+            } else
+                have_pcm = true;
+        }
+        pa_idxset_put(ret, format, NULL);
+    }
+
+out:
+    return ret;
+
+error:
+    if (ret) {
+        pa_idxset_free(ret, (pa_free_cb_t) pa_format_info_free);
+        ret = NULL;
+    }
+
+    goto out;
+}
diff --git a/src/modules/alsa/alsa-util.h b/src/modules/alsa/alsa-util.h
index 5b0a0bd..738ba19 100644
--- a/src/modules/alsa/alsa-util.h
+++ b/src/modules/alsa/alsa-util.h
@@ -147,11 +147,22 @@ snd_hctl_elem_t* pa_alsa_find_eld_ctl(snd_hctl_t *hctl, int device);
 
 snd_mixer_t *pa_alsa_open_mixer(int alsa_card_index, char **ctl_device, snd_hctl_t **hctl);
 
-typedef struct pa_hdmi_eld pa_hdmi_eld;
-struct pa_hdmi_eld {
+#define PA_CEA_SAD_MAX 15
+
+typedef struct pa_cea_sad {
+    uint8_t format;     /* format */
+    uint8_t channels;   /* channel count (actual, not off-by-one as in the SAD itself) */
+    uint8_t rates;      /* bitfield of supported sampling rates as in the SAD */
+    uint8_t data;       /* bit depths for LPCM, profile for WMApro, maximum bitrate for others */
+} pa_cea_sad;
+
+typedef struct pa_hdmi_eld {
     char monitor_name[17];
-};
+    unsigned int sad_count;
+    pa_cea_sad sad[PA_CEA_SAD_MAX];
+} pa_hdmi_eld;
 
 int pa_alsa_get_hdmi_eld(snd_hctl_t *hctl, int device, pa_hdmi_eld *eld);
+pa_idxset* pa_alsa_formats_from_eld(pa_hdmi_eld *eld);
 
 #endif
diff --git a/src/modules/alsa/module-alsa-card.c b/src/modules/alsa/module-alsa-card.c
index fe05e3d..acb925b 100644
--- a/src/modules/alsa/module-alsa-card.c
+++ b/src/modules/alsa/module-alsa-card.c
@@ -26,6 +26,7 @@
 #include <pulse/xmalloc.h>
 
 #include <pulsecore/core-util.h>
+#include <pulsecore/format-util.h>
 #include <pulsecore/i18n.h>
 #include <pulsecore/modargs.h>
 #include <pulsecore/queue.h>
@@ -405,9 +406,13 @@ static int hdmi_eld_changed(snd_hctl_elem_t *elem, unsigned int mask) {
     struct userdata *u = snd_hctl_elem_get_callback_private(elem);
     int device = snd_hctl_elem_get_device(elem);
     const char *old_monitor_name;
+    pa_sink *sink;
     pa_device_port *p;
     pa_hdmi_eld eld;
+    pa_idxset *formats, *old_formats;
+    pa_format_info *format;
     bool changed = false;
+    uint32_t idx;
 
     if (mask == SND_CTL_EVENT_MASK_REMOVE)
         return 0;
@@ -418,8 +423,9 @@ static int hdmi_eld_changed(snd_hctl_elem_t *elem, unsigned int mask) {
         return 0;
     }
 
+    pa_zero(eld);
     if (pa_alsa_get_hdmi_eld(u->hctl_handle, device, &eld) < 0)
-        memset(&eld, 0, sizeof(eld));
+        pa_zero(eld);
 
     old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
     if (eld.monitor_name[0] == '\0') {
@@ -430,9 +436,54 @@ static int hdmi_eld_changed(snd_hctl_elem_t *elem, unsigned int mask) {
         pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);
     }
 
+    formats = pa_alsa_formats_from_eld(&eld);
+    if (!formats) {
+        /* If we don't have any formats, assume PCM only */
+        formats = pa_idxset_new(NULL, NULL);
+        format = pa_format_info_new();
+        format->encoding = PA_ENCODING_PCM;
+        pa_idxset_put(formats, format, NULL);
+    }
+
+    /* Note: The assumption here is that HDMI cards will only have one sink.
+     * This is how ALSA currently works. */
+    sink = pa_idxset_first(u->card->sinks, NULL);
+    old_formats = pa_sink_get_formats(sink);
+
+    /* Now check if the format set has changed */
+    if (pa_idxset_size(formats) != pa_idxset_size(old_formats)) {
+        pa_sink_set_formats(sink, formats);
+        changed = true;
+    } else {
+        pa_format_info *old_format;
+        bool equal = true;
+
+        /* Quick and dirty format info comparison - good enough for checking if
+         * there are any changes at all in formats from what we got last */
+        PA_IDXSET_FOREACH(format, formats, idx) {
+            PA_IDXSET_FOREACH(old_format, old_formats, idx) {
+                if (!pa_format_info_identical(format, old_format)) {
+                    equal = false;
+                    break;
+                }
+            }
+
+            if (!equal)
+                break;
+        }
+
+        if (!equal) {
+            pa_sink_set_formats(sink, formats);
+            changed = true;
+        }
+    }
+
     if (changed && mask != 0)
         pa_subscription_post(u->core, PA_SUBSCRIPTION_EVENT_CARD|PA_SUBSCRIPTION_EVENT_CHANGE, u->card->index);
 
+    pa_idxset_free(formats, (pa_free_cb_t) pa_format_info_free);
+    pa_idxset_free(old_formats, (pa_free_cb_t) pa_format_info_free);
+
     return 0;
 }
 
diff --git a/src/pulse/proplist.h b/src/pulse/proplist.h
index dc3cddc..dbadb98 100644
--- a/src/pulse/proplist.h
+++ b/src/pulse/proplist.h
@@ -269,6 +269,9 @@ PA_C_DECL_BEGIN
 /** For PCM formats: the channel map of the stream as returned by pa_channel_map_snprint() \since 1.0 */
 #define PA_PROP_FORMAT_CHANNEL_MAP             "format.channel_map"
 
+/** For compressed formats: the bit rate (unsigned integer) \since 5.0 */
+#define PA_PROP_FORMAT_BITRATE                 "format.bitrate"
+
 /** A property list object. Basically a dictionary with ASCII strings
  * as keys and arbitrary data as values. \since 0.9.11 */
 typedef struct pa_proplist pa_proplist;
diff --git a/src/pulsecore/format-util.c b/src/pulsecore/format-util.c
new file mode 100644
index 0000000..1002a6c
--- /dev/null
+++ b/src/pulsecore/format-util.c
@@ -0,0 +1,30 @@
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2013 Collabora Ltd.
+  Author: Arun Raghavan <arun.raghavan at collabora.co.uk>
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#include <pulsecore/format-util.h>
+#include <pulse/proplist.h>
+
+bool pa_format_info_identical(pa_format_info *f1, pa_format_info *f2) {
+    /* We can do a string comparison on all the proplist values which is faster
+     * than unpacking into json objects if order doesn't matter */
+    return (f1->encoding == f2->encoding && !pa_proplist_equal(f1->plist, f2->plist));
+}
diff --git a/src/pulsecore/format-util.h b/src/pulsecore/format-util.h
new file mode 100644
index 0000000..46bf15c
--- /dev/null
+++ b/src/pulsecore/format-util.h
@@ -0,0 +1,36 @@
+#ifndef fooformatutilhfoo
+#define fooformatutilhfoo
+
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2013 Collabora Ltd.
+  Author: Arun Raghavan <arun.raghavan at collabora.co.uk>
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pulsecore/macro.h>
+#include <pulse/format.h>
+
+/* Check if two formats are equal, including array order */
+bool pa_format_info_identical(pa_format_info *f1, pa_format_info *f2);
+
+#endif
-- 
1.8.2.1



More information about the pulseaudio-discuss mailing list