spa: alsa: autodetect supported iec958 codecs via ELD info

The alsa/acp code already supports getting a user-friendly monitor name
using the EDID-Like Data (ELD) information available from cards that follow
the Intel HDA specification.

This patch adds support for also parsing the SAD fields of the ELD, and
exposing the results as a "iec958.codecs.detected" property on the
corresponding node, which should make it possible to provide more
user-friendly configuration UIs and defaults.

The default value will take effect if the session manager does not set a
different value.

Brief example:
test@test:~/checkouts/pipewire$ pw-dump | grep -E "(iec958.codecs.detected|iec958.codecs)\":"
        "iec958.codecs": "[\"PCM\",\"AC3\",\"EAC3\",\"TrueHD\"]",
        "iec958.codecs.detected": "[\"PCM\",\"AC3\",\"EAC3\",\"TrueHD\"]",

<after powering on my receiver>

test@test:~/checkouts/pipewire$ pw-dump | grep -E "(iec958.codecs.detected|iec958.codecs)\":"
        "iec958.codecs": "[\"PCM\",\"DTS\",\"AC3\",\"EAC3\",\"TrueHD\",\"DTS-HD\"]",
        "iec958.codecs.detected": "[\"PCM\",\"DTS\",\"AC3\",\"EAC3\",\"TrueHD\",\"DTS-HD\"]",

Big thanks to Pauli Virtanen <pav@iki.fi>, who also wrote large paths of the
code for this patch.
This commit is contained in:
David Härdeman 2024-11-25 22:35:28 +01:00
parent 0570d1dd00
commit f33e1bc8c3
5 changed files with 144 additions and 6 deletions

View file

@ -8,6 +8,7 @@
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/param/audio/iec958-types.h>
int _acp_log_level = 1;
acp_log_func _acp_log_func;
@ -951,12 +952,58 @@ static pa_device_port* find_port_with_eld_device(pa_card *impl, int device)
return NULL;
}
static void acp_iec958_codec_mask_to_json(uint64_t codecs, char *buf, size_t maxsize)
{
struct spa_strbuf b;
const struct spa_type_info *info;
spa_strbuf_init(&b, buf, maxsize);
for (info = spa_type_audio_iec958_codec; info->name; ++info)
if ((codecs & (1ULL << info->type)) && info->type != SPA_AUDIO_IEC958_CODEC_UNKNOWN)
spa_strbuf_append(&b, "%s\"%s\"", (b.pos ? "," : "["),
spa_type_audio_iec958_codec_to_short_name(info->type));
if (b.pos)
spa_strbuf_append(&b, "]");
}
void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize)
{
struct spa_strbuf b;
spa_strbuf_init(&b, buf, maxsize);
spa_strbuf_append(&b, "[");
for (size_t i = 0; i < n_codecs; ++i)
spa_strbuf_append(&b, "%s\"%s\"", (i ? "," : ""),
spa_type_audio_iec958_codec_to_short_name(codecs[i]));
spa_strbuf_append(&b, "]");
}
size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs)
{
struct spa_json it;
char v[256];
size_t n_codecs = 0;
if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0)
return 0;
while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
uint32_t type = spa_type_audio_iec958_codec_from_short_name(v);
if (type != SPA_AUDIO_IEC958_CODEC_UNKNOWN)
codecs[n_codecs++] = type;
if (n_codecs >= max_codecs)
break;
}
return n_codecs;
}
static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask)
{
pa_card *impl = snd_mixer_elem_get_callback_private(melem);
snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem;
int device, i;
const char *old_monitor_name;
const char *old_monitor_name, *old_iec958_codec_list;
pa_device_port *p;
pa_hdmi_eld eld;
bool changed = false;
@ -994,6 +1041,18 @@ static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask)
changed |= (old_monitor_name == NULL) || (!spa_streq(old_monitor_name, eld.monitor_name));
pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);
}
old_iec958_codec_list = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
if (eld.iec958_codecs == 0) {
changed |= old_iec958_codec_list != NULL;
pa_proplist_unset(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
} else {
char codecs[512];
acp_iec958_codec_mask_to_json(eld.iec958_codecs, codecs, sizeof(codecs));
changed |= (old_iec958_codec_list == NULL) || (!spa_streq(old_iec958_codec_list, codecs));
pa_proplist_sets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED, codecs);
}
pa_proplist_as_dict(p->proplist, &p->port.props);
if (changed && mask != 0 && impl->events && impl->events->props_changed)
@ -1452,6 +1511,9 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
{
const char *mod_name;
uint32_t i, port_index;
const char *codecs;
pa_device_port *p;
void *state = NULL;
int res;
if (impl->use_ucm &&
@ -1471,7 +1533,7 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
/* Synchronize priority values, as it may have changed when setting the profile */
for (i = 0; i < impl->card.n_ports; i++) {
pa_device_port *p = (pa_device_port *)impl->card.ports[i];
p = (pa_device_port *)impl->card.ports[i];
p->port.priority = p->priority;
}
@ -1502,6 +1564,15 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device
else
dev->muted = false;
while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) {
codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED);
if (codecs) {
dev->device.n_codecs = acp_iec958_codecs_from_json(codecs, dev->device.codecs,
ACP_N_ELEMENTS(dev->device.codecs));
break;
}
}
return 0;
}
@ -1757,11 +1828,11 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
if (!impl->auto_profile && profile == NULL)
profile = "off";
init_eld_ctls(impl);
profile_index = acp_card_find_best_profile_index(&impl->card, profile);
acp_card_set_profile(&impl->card, profile_index, 0);
init_eld_ctls(impl);
return &impl->card;
error:
pa_alsa_refcnt_dec();

View file

@ -148,6 +148,12 @@ const char *acp_available_str(enum acp_available status);
* like an ALSA control name, but applications must not assume any such relationship.
* The group naming scheme can change without a warning.
*/
#define ACP_KEY_IEC958_CODECS_DETECTED "iec958.codecs.detected"
/**< A list of IEC958 passthrough formats which have been auto-detected as being
* supported by a given node. This only serves as a hint, as the auto-detected
* values may be incorrect and/or might change, e.g. when external devices such
* as receivers are powered on or off.
*/
struct acp_device;
@ -294,6 +300,9 @@ typedef void (*acp_log_func) (void *data,
void acp_set_log_func(acp_log_func, void *data);
void acp_set_log_level(int level);
void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize);
size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs);
#ifdef __cplusplus
}
#endif

View file

@ -26,6 +26,8 @@
#include "alsa-util.h"
#include "alsa-mixer.h"
#include <spa/param/audio/format.h>
#ifdef HAVE_UDEV
#include <modules/udev-util.h>
#endif
@ -1972,7 +1974,7 @@ int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, 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, sad_count;
unsigned int device;
pa_assert(eld != NULL);
@ -2010,5 +2012,54 @@ int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) {
if (mnl)
pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device);
/* Fetch Short Audio Descriptors */
sad_count = (elddata[5] & 0xf0) >> 4;
pa_log_debug("SAD count in ELD info is %u (for device=%d)", sad_count, device);
if (20 + mnl + 3 * sad_count > eldsize) {
pa_log_debug("Invalid SAD count (%u) in ELD info (for device=%d)", sad_count, device);
sad_count = 0;
}
eld->iec958_codecs = 0;
for (unsigned i = 0; i < sad_count; i++) {
uint8_t *sad = &elddata[20 + mnl + 3 * i];
/* https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Audio_Data_Blocks */
switch ((sad[0] & 0x78) >> 3) {
case 1:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
break;
case 2:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_AC3;
break;
case 3:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
break;
case 4:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
break;
case 5:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG;
break;
case 6:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG2_AAC;
break;
case 7:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTS;
break;
case 10:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_EAC3;
break;
case 11:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTSHD;
break;
case 12:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_TRUEHD;
break;
default:
eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_UNKNOWN;
break;
}
}
return 0;
}

View file

@ -175,6 +175,7 @@ void pa_alsa_mixer_free(pa_alsa_mixer *mixer);
typedef struct pa_hdmi_eld pa_hdmi_eld;
struct pa_hdmi_eld {
char monitor_name[17];
uint64_t iec958_codecs;
};
int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld);

View file

@ -157,6 +157,7 @@ static int emit_node(struct impl *this, struct acp_device *dev)
char device_name[128], path[210], channels[16], ch[12], routes[16];
char card_index[16], card_name[64], *p;
char positions[SPA_AUDIO_MAX_CHANNELS * 12];
char codecs[512];
struct spa_device_object_info info;
struct acp_card *card = this->card;
const char *stream, *card_id;
@ -174,7 +175,7 @@ static int emit_node(struct impl *this, struct acp_device *dev)
info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
items = alloca((dev->props.n_items + 9) * sizeof(*items));
items = alloca((dev->props.n_items + 10) * sizeof(*items));
n_items = 0;
snprintf(card_index, sizeof(card_index), "%d", card->index);
@ -202,6 +203,11 @@ static int emit_node(struct impl *this, struct acp_device *dev)
}
items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions);
if (dev->n_codecs > 0) {
acp_iec958_codecs_to_json(dev->codecs, dev->n_codecs, codecs, sizeof(codecs));
items[n_items++] = SPA_DICT_ITEM_INIT("iec958.codecs", codecs);
}
snprintf(routes, sizeof(routes), "%d", dev->n_ports);
items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes);