spa: acp: get and emit UCM information for SplitPCM devices

When api.alsa.split-enable=true for ACP device, instruct UCM to not
use alsa-lib plugins for SplitPCM devices.

Grab the information from UCM for the intended channel remapping, and
add the splitting information to the nodes emitted.

Session manager can then look at that, and load nodes to do the channel
splitting.
This commit is contained in:
Pauli Virtanen 2024-12-07 13:27:09 +02:00
parent 7c7a54dd87
commit dcccfcab7f
12 changed files with 422 additions and 22 deletions

View file

@ -326,6 +326,90 @@ static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink)
return dev_name;
}
static PA_PRINTF_FUNC(2,3) const char *ucm_get_string(snd_use_case_mgr_t *uc_mgr, const char *fmt, ...)
{
char *id;
const char *value;
va_list args;
int err;
va_start(args, fmt);
id = pa_vsprintf_malloc(fmt, args);
va_end(args);
err = snd_use_case_get(uc_mgr, id, &value);
if (err >= 0)
pa_log_debug("Got %s: %s", id, value);
pa_xfree(id);
if (err < 0) {
errno = -err;
return NULL;
}
return value;
}
static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd_use_case_mgr_t *uc_mgr, const char *prefix) {
pa_alsa_ucm_split *split;
const char *value;
const char *device_name;
int i;
uint32_t hw_channels;
device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
if (!device_name)
return NULL;
value = ucm_get_string(uc_mgr, "%sChannels/%s", prefix, device_name);
if (pa_atou(value, &hw_channels) < 0)
return NULL;
split = pa_xnew0(pa_alsa_ucm_split, 1);
for (i = 0; i < PA_CHANNELS_MAX; i++) {
uint32_t idx;
snd_pcm_chmap_t *map;
value = ucm_get_string(uc_mgr, "%sChannel%d/%s", prefix, i, device_name);
if (pa_atou(value, &idx) < 0)
break;
if (idx >= hw_channels)
goto fail;
value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name);
if (!value)
goto fail;
map = snd_pcm_chmap_parse_string(value);
if (!map)
goto fail;
if (map->channels == 1) {
pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)",
prefix, (int)idx, device_name, i, value, map->pos[0]);
split->idx[i] = idx;
split->pos[i] = map->pos[0];
free(map);
} else {
free(map);
goto fail;
}
}
if (i == 0) {
pa_xfree(split);
return NULL;
}
split->channels = i;
split->hw_channels = hw_channels;
return split;
fail:
pa_log_warn("Invalid SplitPCM ALSA UCM rule for device %s", device_name);
pa_xfree(split);
return NULL;
}
/* Create a property list for this ucm device */
static int ucm_get_device_property(
pa_alsa_ucm_device *device,
@ -470,6 +554,9 @@ static int ucm_get_device_property(
pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
}
device->playback_split = ucm_get_split_channels(device, uc_mgr, "Playback");
device->capture_split = ucm_get_split_channels(device, uc_mgr, "Capture");
if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
/* get priority from static table */
for (i = 0; dev_info[i].id; i++) {
@ -868,21 +955,30 @@ int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) {
char *card_name;
const char **verb_list, *value;
int num_verbs, i, err = 0;
const char *split_prefix = ucm->split_enable ? "<<<SplitPCM=1>>>" : "";
/* support multiple card instances, address card directly by index */
card_name = pa_sprintf_malloc("hw:%i", card_index);
card_name = pa_sprintf_malloc("%shw:%i", split_prefix, card_index);
if (card_name == NULL)
return -PA_ALSA_ERR_UNSPECIFIED;
err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
if (err < 0) {
char *ucm_card_name;
/* fallback longname: is UCM available for this card ? */
pa_xfree(card_name);
err = snd_card_get_name(card_index, &card_name);
err = snd_card_get_name(card_index, &ucm_card_name);
if (err < 0) {
pa_log("Card can't get card_name from card_index %d", card_index);
err = -PA_ALSA_ERR_UNSPECIFIED;
goto name_fail;
}
card_name = pa_sprintf_malloc("%s%s", split_prefix, ucm_card_name);
free(ucm_card_name);
if (card_name == NULL) {
err = -PA_ALSA_ERR_UNSPECIFIED;
goto name_fail;
}
err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
if (err < 0) {
@ -955,6 +1051,54 @@ name_fail:
return err;
}
static void ucm_verb_set_split_leaders(pa_alsa_ucm_verb *verb) {
pa_alsa_ucm_device *d, *d2;
/* Set first virtual device in each split HW PCM as the split leader */
PA_LLIST_FOREACH(d, verb->devices) {
if (d->playback_split)
d->playback_split->leader = true;
if (d->capture_split)
d->capture_split->leader = true;
}
PA_LLIST_FOREACH(d, verb->devices) {
const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (d->playback_split) {
if (!sink)
d->playback_split->leader = false;
if (d->playback_split->leader) {
PA_LLIST_FOREACH(d2, verb->devices) {
const char *sink2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SINK);
if (d == d2 || !d2->playback_split || !sink || !sink2 || !pa_streq(sink, sink2))
continue;
d2->playback_split->leader = false;
}
}
}
if (d->capture_split) {
if (!source)
d->capture_split->leader = false;
if (d->capture_split->leader) {
PA_LLIST_FOREACH(d2, verb->devices) {
const char *source2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (d == d2 || !d2->capture_split || !source || !source2 || !pa_streq(source, source2))
continue;
d2->capture_split->leader = false;
}
}
}
}
}
int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) {
pa_alsa_ucm_device *d;
pa_alsa_ucm_modifier *mod;
@ -994,6 +1138,9 @@ int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, cons
/* Devices properties */
ucm_get_device_property(d, uc_mgr, verb, dev_name);
}
ucm_verb_set_split_leaders(verb);
/* make conflicting or supported device mutual */
PA_LLIST_FOREACH(d, verb->devices)
append_lost_relationship(d);
@ -1372,15 +1519,19 @@ static bool devset_supports_device(pa_idxset *devices, pa_alsa_ucm_device *dev)
if (!pa_idxset_contains(d->supported_devices, dev))
return false;
/* PlaybackPCM must not be the same as any selected device */
/* PlaybackPCM must not be the same as any selected device, except when both split */
sink2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
if (sink && sink2 && pa_streq(sink, sink2))
return false;
if (sink && sink2 && pa_streq(sink, sink2)) {
if (!(dev->playback_split && d->playback_split))
return false;
}
/* CapturePCM must not be the same as any selected device */
/* CapturePCM must not be the same as any selected device, except when both split */
source2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
if (source && source2 && pa_streq(source, source2))
return false;
if (source && source2 && pa_streq(source, source2)) {
if (!(dev->capture_split && d->capture_split))
return false;
}
}
return true;
@ -1753,6 +1904,69 @@ static pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_pr
return m;
}
static const struct {
enum snd_pcm_chmap_position pos;
pa_channel_position_t channel;
} chmap_info[] = {
[SND_CHMAP_MONO] = { SND_CHMAP_MONO, PA_CHANNEL_POSITION_MONO },
[SND_CHMAP_FL] = { SND_CHMAP_FL, PA_CHANNEL_POSITION_FRONT_LEFT },
[SND_CHMAP_FR] = { SND_CHMAP_FR, PA_CHANNEL_POSITION_FRONT_RIGHT },
[SND_CHMAP_RL] = { SND_CHMAP_RL, PA_CHANNEL_POSITION_REAR_LEFT },
[SND_CHMAP_RR] = { SND_CHMAP_RR, PA_CHANNEL_POSITION_REAR_RIGHT },
[SND_CHMAP_FC] = { SND_CHMAP_FC, PA_CHANNEL_POSITION_FRONT_CENTER },
[SND_CHMAP_LFE] = { SND_CHMAP_LFE, PA_CHANNEL_POSITION_LFE },
[SND_CHMAP_SL] = { SND_CHMAP_SL, PA_CHANNEL_POSITION_SIDE_LEFT },
[SND_CHMAP_SR] = { SND_CHMAP_SR, PA_CHANNEL_POSITION_SIDE_RIGHT },
[SND_CHMAP_RC] = { SND_CHMAP_RC, PA_CHANNEL_POSITION_REAR_CENTER },
[SND_CHMAP_FLC] = { SND_CHMAP_FLC, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER },
[SND_CHMAP_FRC] = { SND_CHMAP_FRC, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER },
/* XXX: missing channel positions, mapped to aux... */
/* [SND_CHMAP_RLC] = { SND_CHMAP_RLC, PA_CHANNEL_POSITION_REAR_LEFT_OF_CENTER }, */
/* [SND_CHMAP_RRC] = { SND_CHMAP_RRC, PA_CHANNEL_POSITION_REAR_RIGHT_OF_CENTER }, */
/* [SND_CHMAP_FLW] = { SND_CHMAP_FLW, PA_CHANNEL_POSITION_FRONT_LEFT_WIDE }, */
/* [SND_CHMAP_FRW] = { SND_CHMAP_FRW, PA_CHANNEL_POSITION_FRONT_RIGHT_WIDE }, */
/* [SND_CHMAP_FLH] = { SND_CHMAP_FLH, PA_CHANNEL_POSITION_FRONT_LEFT_HIGH }, */
/* [SND_CHMAP_FCH] = { SND_CHMAP_FCH, PA_CHANNEL_POSITION_FRONT_CENTER_HIGH }, */
/* [SND_CHMAP_FRH] = { SND_CHMAP_FRH, PA_CHANNEL_POSITION_FRONT_RIGHT_HIGH }, */
[SND_CHMAP_TC] = { SND_CHMAP_TC, PA_CHANNEL_POSITION_TOP_CENTER },
[SND_CHMAP_TFL] = { SND_CHMAP_TFL, PA_CHANNEL_POSITION_TOP_FRONT_LEFT },
[SND_CHMAP_TFR] = { SND_CHMAP_TFR, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT },
[SND_CHMAP_TFC] = { SND_CHMAP_TFC, PA_CHANNEL_POSITION_TOP_FRONT_CENTER },
[SND_CHMAP_TRL] = { SND_CHMAP_TRL, PA_CHANNEL_POSITION_TOP_REAR_LEFT },
[SND_CHMAP_TRR] = { SND_CHMAP_TRR, PA_CHANNEL_POSITION_TOP_REAR_RIGHT },
[SND_CHMAP_TRC] = { SND_CHMAP_TRC, PA_CHANNEL_POSITION_TOP_REAR_CENTER },
/* [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, PA_CHANNEL_POSITION_TOP_FRONT_LEFT_OF_CENTER }, */
/* [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT_OF_CENTER }, */
/* [SND_CHMAP_TSL] = { SND_CHMAP_TSL, PA_CHANNEL_POSITION_TOP_SIDE_LEFT }, */
/* [SND_CHMAP_TSR] = { SND_CHMAP_TSR, PA_CHANNEL_POSITION_TOP_SIDE_RIGHT }, */
/* [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, PA_CHANNEL_POSITION_LEFT_LFE }, */
/* [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, PA_CHANNEL_POSITION_RIGHT_LFE }, */
/* [SND_CHMAP_BC] = { SND_CHMAP_BC, PA_CHANNEL_POSITION_BOTTOM_CENTER }, */
/* [SND_CHMAP_BLC] = { SND_CHMAP_BLC, PA_CHANNEL_POSITION_BOTTOM_LEFT_OF_CENTER }, */
/* [SND_CHMAP_BRC] = { SND_CHMAP_BRC, PA_CHANNEL_POSITION_BOTTOM_RIGHT_OF_CENTER }, */
};
static void ucm_split_to_channel_map(pa_channel_map *m, const pa_alsa_ucm_split *s)
{
const int n = sizeof(chmap_info) / sizeof(chmap_info[0]);
int i;
int aux = 0;
for (i = 0; i < s->channels; ++i) {
int p = s->pos[i];
if (p >= 0 && p < n && (int)chmap_info[p].pos == p)
m->map[i] = chmap_info[p].channel;
else
m->map[i] = PA_CHANNEL_POSITION_AUX0 + aux++;
if (aux >= 32)
break;
}
m->channels = i;
}
static int ucm_create_mapping_direction(
pa_alsa_ucm_config *ucm,
pa_alsa_profile_set *ps,
@ -1797,6 +2011,14 @@ static int ucm_create_mapping_direction(
if (channels < m->channel_map.channels)
pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
if (is_sink && device->playback_split) {
m->split = pa_xmemdup(device->playback_split, sizeof(*m->split));
ucm_split_to_channel_map(&m->channel_map, m->split);
} else if (!is_sink && device->capture_split) {
m->split = pa_xmemdup(device->capture_split, sizeof(*m->split));
ucm_split_to_channel_map(&m->channel_map, m->split);
}
alsa_mapping_add_ucm_device(m, device);
return 0;
@ -2168,11 +2390,22 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m,
snd_pcm_uframes_t try_period_size, try_buffer_size;
bool exact_channels = m->channel_map.channels > 0;
if (exact_channels) {
try_map = m->channel_map;
try_ss.channels = try_map.channels;
} else
pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA);
if (!m->split) {
if (exact_channels) {
try_map = m->channel_map;
try_ss.channels = try_map.channels;
} else
pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA);
} else {
if (!m->split->leader) {
errno = EINVAL;
return NULL;
}
exact_channels = true;
try_ss.channels = m->split->hw_channels;
pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX);
}
try_period_size =
pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
@ -2191,6 +2424,32 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m,
return pcm;
}
static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction)
{
pa_proplist *props = pa_proplist_new();
uint32_t idx;
pa_alsa_mapping *m;
if (direction == PA_DIRECTION_OUTPUT)
pa_alsa_init_proplist_pcm(NULL, props, leader->output_pcm);
else
pa_alsa_init_proplist_pcm(NULL, props, leader->input_pcm);
PA_IDXSET_FOREACH(m, mappings, idx) {
if (!m->split)
continue;
if (!pa_streq(m->device_strings[0], leader->device_strings[0]))
continue;
if (direction == PA_DIRECTION_OUTPUT)
pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props);
else
pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props);
}
pa_proplist_free(props);
}
static void profile_finalize_probing(pa_alsa_profile *p) {
pa_alsa_mapping *m;
uint32_t idx;
@ -2202,7 +2461,11 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
if (!m->output_pcm)
continue;
pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
if (!m->split)
pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
else
pa_alsa_init_proplist_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT);
pa_alsa_close(&m->output_pcm);
}
@ -2213,7 +2476,11 @@ static void profile_finalize_probing(pa_alsa_profile *p) {
if (!m->input_pcm)
continue;
pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
if (!m->split)
pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
else
pa_alsa_init_proplist_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT);
pa_alsa_close(&m->input_pcm);
}
}
@ -2266,6 +2533,9 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *
continue;
}
if (m->split && !m->split->leader)
continue;
m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK);
if (!m->output_pcm) {
p->supported = false;
@ -2281,6 +2551,9 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *
continue;
}
if (m->split && !m->split->leader)
continue;
m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE);
if (!m->input_pcm) {
p->supported = false;
@ -2370,6 +2643,9 @@ static void free_verb(pa_alsa_ucm_verb *verb) {
pa_xfree(di->eld_mixer_device_name);
pa_xfree(di->playback_split);
pa_xfree(di->capture_split);
pa_xfree(di);
}