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