diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index e019927eb..2dfe7abae 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -713,6 +713,11 @@ Normally, the maximum amount of channels will be used but with this setting this can be reduced, which can make it possible to use other samplerates on some devices. +@PAR@ device-prop api.alsa.split-enable # boolean +\parblock +\copydoc SPA_KEY_API_ALSA_SPLIT_ENABLE +\endparblock + ## Node properties @PAR@ node-prop audio.channels # integer @@ -796,6 +801,36 @@ UNDOCUMENTED Enable only specific IEC958 codecs. This can be used to disable some codecs the hardware supports. Available values: PCM, AC3, DTS, MPEG, MPEG2-AAC, EAC3, TRUEHD, DTSHD +@PAR@ device-prop api.alsa.split.parent # boolean +\parblock +\copydoc SPA_KEY_API_ALSA_SPLIT_PARENT +\endparblock + +@PAR@ node-prop api.alsa.split.position # JSON +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_POSITION +Informative property. +\endparblock + +@PAR@ node-prop api.alsa.split.leader # boolean +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_LEADER +Informative property. +\endparblock + +@PAR@ node-prop api.alsa.split.hw-channels # int +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_HW_CHANNELS +Informative property. +\endparblock + +@PAR@ node-prop api.alsa.split.hw-position # JSON +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_HW_POSITION +Informative property. +\endparblock + + # BLUETOOTH PROPERTIES @IDX@ props ## Monitor properties diff --git a/spa/include/spa/utils/keys.h b/spa/include/spa/utils/keys.h index 48b94c50a..b377b9127 100644 --- a/spa/include/spa/utils/keys.h +++ b/spa/include/spa/utils/keys.h @@ -45,6 +45,14 @@ extern "C" { #define SPA_KEY_API_ALSA_DISABLE_LONGNAME \ "api.alsa.disable-longname" /**< if card long name should not be passed to MIDI port */ #define SPA_KEY_API_ALSA_BIND_CTLS "api.alsa.bind-ctls" /**< alsa controls to bind as params */ +#define SPA_KEY_API_ALSA_SPLIT_ENABLE "api.alsa.split-enable" /**< For UCM devices with split PCMs, don't split to + * multiple PCMs using alsa-lib plugins, but instead + * add api.alsa.split properties to emitted nodes + * with PCM splitting information. + */ +#define SPA_KEY_API_ALSA_SPLIT_PARENT "api.alsa.split.parent" /**< PCM is UCM SplitPCM parent PCM, + * to be opened with SplitPCM set. + */ /** info from alsa card_info */ #define SPA_KEY_API_ALSA_CARD_ID "api.alsa.card.id" /**< id from card_info */ @@ -67,6 +75,21 @@ extern "C" { #define SPA_KEY_API_ALSA_PCM_SUBCLASS "api.alsa.pcm.subclass" /**< subclass from pcm_info as string */ #define SPA_KEY_API_ALSA_PCM_SYNC_ID "api.alsa.pcm.sync-id" /**< sync id */ +#define SPA_KEY_API_ALSA_SPLIT_POSITION "api.alsa.split.position" /**< (SPA JSON list) If present, this is a + * virtual device corresponding to a subset of + * channels in an underlying PCM, listed in this + * property. The \ref SPA_KEY_API_ALSA_PATH + * contains the underlying split PCM. */ +#define SPA_KEY_API_ALSA_SPLIT_LEADER "api.alsa.split.leader" /**< (bool) This virtual device is split leader: + * one of the split devices for the same split + * PCM is selected as the leader. */ +#define SPA_KEY_API_ALSA_SPLIT_HW_CHANNELS \ + "api.alsa.split.hw-channels" /**< (int) Number of channels in the underlying + * split PCM. */ +#define SPA_KEY_API_ALSA_SPLIT_HW_POSITION \ + "api.alsa.split.hw-position" /**< (SPA JSON list) Channel map of the + * underlying split PCM. */ + /** keys for v4l2 api */ #define SPA_KEY_API_V4L2 "api.v4l2" /**< key for the v4l2 api */ #define SPA_KEY_API_V4L2_PATH "api.v4l2.path" /**< v4l2 device path as can be diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 0f2e1e442..2a596c0c5 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -230,6 +230,28 @@ static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t dev->device.direction = ACP_DIRECTION_CAPTURE; pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist); } + if (m->split) { + char pos[2048]; + struct spa_strbuf b; + int i; + + pa_proplist_sets(dev->proplist, "api.alsa.split.leader", m->split->leader ? "true" : "false"); + pa_proplist_setf(dev->proplist, "api.alsa.split.hw-channels", "%d", m->split->hw_channels); + + spa_strbuf_init(&b, pos, sizeof(pos)); + spa_strbuf_append(&b, "["); + for (i = 0; i < m->split->channels; ++i) + spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), m->split->idx[i]); + spa_strbuf_append(&b, "]"); + pa_proplist_sets(dev->proplist, "api.alsa.split.position", pos); + + spa_strbuf_init(&b, pos, sizeof(pos)); + spa_strbuf_append(&b, "["); + for (i = 0; i < m->split->hw_channels; ++i) + spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), i); + spa_strbuf_append(&b, "]"); + pa_proplist_sets(dev->proplist, "api.alsa.split.hw-position", pos); + } pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name); pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description); pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index); @@ -1758,8 +1780,17 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) impl->rate = atoi(s); if ((s = acp_dict_lookup(props, "api.acp.pro-channels")) != NULL) impl->pro_channels = atoi(s); + if ((s = acp_dict_lookup(props, "api.alsa.split-enable")) != NULL) + impl->ucm.split_enable = spa_atob(s); } +#if SND_LIB_VERSION < 0x10207 + if (impl->ucm.split_enable) + pa_log_info("alsa-lib too old for PipeWire-side UCM SplitPCM"); + + impl->ucm.split_enable = false; /* API addition in 1.2.7 */ +#endif + impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE; impl->ucm.default_sample_spec.rate = impl->rate; impl->ucm.default_sample_spec.channels = 2; diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c index bd4942c7c..1f494ee0b 100644 --- a/spa/plugins/alsa/acp/alsa-mixer.c +++ b/spa/plugins/alsa/acp/alsa-mixer.c @@ -3787,6 +3787,8 @@ void pa_alsa_mapping_free(pa_alsa_mapping *m) { pa_proplist_free(m->input_proplist); pa_proplist_free(m->output_proplist); + pa_xfree(m->split); + pa_assert(!m->input_pcm); pa_assert(!m->output_pcm); diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h index 687e8b53d..cbfac4ab7 100644 --- a/spa/plugins/alsa/acp/alsa-mixer.h +++ b/spa/plugins/alsa/acp/alsa-mixer.h @@ -327,6 +327,8 @@ struct pa_alsa_mapping { pa_sample_spec sample_spec; pa_channel_map channel_map; + pa_alsa_ucm_split *split; + char **device_strings; char **input_path_names; diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index 162269f77..9061a1f26 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -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 ? "<<>>" : ""; /* 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); } diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h index b03e73115..74dbe8219 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.h +++ b/spa/plugins/alsa/acp/alsa-ucm.h @@ -145,6 +145,7 @@ typedef struct pa_alsa_ucm_mapping_context pa_alsa_ucm_mapping_context; typedef struct pa_alsa_ucm_profile_context pa_alsa_ucm_profile_context; typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data; typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume; +typedef struct pa_alsa_ucm_split pa_alsa_ucm_split; int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index); pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map); @@ -177,6 +178,15 @@ void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_ /* UCM - Use Case Manager is available on some audio cards */ +struct pa_alsa_ucm_split { + /* UCM SplitPCM channel remapping */ + bool leader; + int hw_channels; + int channels; + int idx[PA_CHANNELS_MAX]; + enum snd_pcm_chmap_position pos[PA_CHANNELS_MAX]; +}; + struct pa_alsa_ucm_device { PA_LLIST_FIELDS(pa_alsa_ucm_device); @@ -215,6 +225,9 @@ struct pa_alsa_ucm_device { char *eld_mixer_device_name; int eld_device; + + pa_alsa_ucm_split *playback_split; + pa_alsa_ucm_split *capture_split; }; void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device); @@ -254,6 +267,7 @@ struct pa_alsa_ucm_config { pa_channel_map default_channel_map; unsigned default_fragment_size_msec; unsigned default_n_fragments; + bool split_enable; snd_use_case_mgr_t *ucm_mgr; pa_alsa_ucm_verb *active_verb; diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h index 356ef0747..7660e1c27 100644 --- a/spa/plugins/alsa/acp/compat.h +++ b/spa/plugins/alsa/acp/compat.h @@ -414,6 +414,14 @@ static PA_PRINTF_FUNC(1,2) inline char *pa_sprintf_malloc(const char *fmt, ...) return res; } +static PA_PRINTF_FUNC(1,0) inline char *pa_vsprintf_malloc(const char *fmt, va_list args) +{ + char *res; + if (vasprintf(&res, fmt, args) < 0) + res = NULL; + return res; +} + #define pa_fopen_cloexec(f,m) fopen(f,m"e") static inline char *pa_path_get_filename(const char *p) diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c index c3edd3b19..6c340cc6f 100644 --- a/spa/plugins/alsa/alsa-pcm-sink.c +++ b/spa/plugins/alsa/alsa-pcm-sink.c @@ -27,6 +27,7 @@ static void reset_props(struct props *props) { strncpy(props->device, default_device, 64); props->use_chmap = DEFAULT_USE_CHMAP; + spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Sink"); } static int impl_node_enum_params(void *object, int seq, diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c index 43356e526..a86e8aa0e 100644 --- a/spa/plugins/alsa/alsa-pcm-source.c +++ b/spa/plugins/alsa/alsa-pcm-source.c @@ -28,6 +28,7 @@ static void reset_props(struct props *props) { strncpy(props->device, default_device, 64); props->use_chmap = DEFAULT_USE_CHMAP; + spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Source"); } static int impl_node_enum_params(void *object, int seq, diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c index d177f6dc4..0a4c195af 100644 --- a/spa/plugins/alsa/alsa-pcm.c +++ b/spa/plugins/alsa/alsa-pcm.c @@ -33,10 +33,10 @@ static struct card *find_card(uint32_t index) return NULL; } -static struct card *ensure_card(uint32_t index, bool ucm) +static struct card *ensure_card(uint32_t index, bool ucm, bool ucm_split) { struct card *c; - char card_name[64]; + char card_name[128]; const char *alibpref = NULL; int err; @@ -51,7 +51,9 @@ static struct card *ensure_card(uint32_t index, bool ucm) c->index = index; if (ucm) { - snprintf(card_name, sizeof(card_name), "hw:%i", index); + const char *split_prefix = ucm_split ? "<<>>" : ""; + + snprintf(card_name, sizeof(card_name), "%shw:%i", split_prefix, index); err = snd_use_case_mgr_open(&c->ucm, card_name); if (err < 0) { char *name; @@ -59,7 +61,7 @@ static struct card *ensure_card(uint32_t index, bool ucm) if (err < 0) goto error; - snprintf(card_name, sizeof(card_name), "%s", name); + snprintf(card_name, sizeof(card_name), "%s%s", split_prefix, name); free(name); err = snd_use_case_mgr_open(&c->ucm, card_name); @@ -211,6 +213,10 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) } else if (spa_strstartswith(k, "api.alsa.bind-ctl.")) { write_bind_ctl_param(state, k, s); fmt_change++; + } else if (spa_streq(k, SPA_KEY_MEDIA_CLASS)) { + spa_scnprintf(state->props.media_class, sizeof(state->props.media_class), "%s", s); + } else if (spa_streq(k, "api.alsa.split.parent")) { + state->is_split_parent = true; } else return 0; @@ -1020,7 +1026,7 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info) state->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM; } - state->card = ensure_card(state->card_index, state->open_ucm); + state->card = ensure_card(state->card_index, state->open_ucm, state->is_split_parent); state->log_file = fopencookie(state, "w", io_funcs); if (state->log_file == NULL) { @@ -3821,8 +3827,7 @@ void spa_alsa_emit_node_info(struct state *state, bool full) char latency[64] = "", period[64] = "", nperiods[64] = "", headroom[64] = ""; items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, - state->stream == SND_PCM_STREAM_PLAYBACK ? "Audio/Sink" : "Audio/Source"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, state->props.media_class); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); if (state->have_format) diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h index d8ef0bfda..bd77abc2e 100644 --- a/spa/plugins/alsa/alsa-pcm.h +++ b/spa/plugins/alsa/alsa-pcm.h @@ -50,6 +50,7 @@ struct props { char device[64]; char device_name[128]; char card_name[128]; + char media_class[128]; bool use_chmap; }; @@ -153,6 +154,7 @@ struct state { unsigned int disable_mmap:1; unsigned int disable_batch:1; unsigned int disable_tsched:1; + unsigned int is_split_parent:1; char clock_name[64]; uint32_t quantum_limit;