spa: alsa: Add a mechanism for external volume control

Currently enabled at device creation and delegated to an external entity
via spa_device events.
This commit is contained in:
Julian Bouzas 2026-05-15 10:11:13 -04:00
parent 122bfd712b
commit ef6f5194e3
9 changed files with 867 additions and 86 deletions

View file

@ -70,7 +70,8 @@ struct impl {
#define IDX_Profile 1
#define IDX_EnumRoute 2
#define IDX_Route 3
struct spa_param_info params[4];
#define IDX_Props 4
struct spa_param_info params[5];
struct spa_hook_list hooks;
@ -490,6 +491,11 @@ static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
dev->n_codecs, dev->codecs);
}
if (dev->ext_volume_flags) {
spa_pod_builder_prop(b, SPA_PROP_volumeControlFlags, 0);
spa_pod_builder_id(b, dev->ext_volume_flags);
}
spa_pod_builder_pop(b, &f[1]);
}
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
@ -507,6 +513,41 @@ static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
return spa_pod_builder_pop(b, &f[0]);
}
static struct spa_pod *build_props(struct spa_pod_builder *b, struct acp_card *card, struct acp_port *p)
{
struct spa_pod_frame f[4];
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
spa_pod_builder_prop(b, SPA_PROP_params, 0);
spa_pod_builder_push_struct(b, &f[1]);
spa_pod_builder_string(b, "ext-control-caps-reply");
spa_pod_builder_push_struct(b, &f[2]);
spa_pod_builder_int(b, card->ext_volume.caps_reply.res);
spa_pod_builder_int(b, card->ext_volume.caps_reply.caps);
spa_pod_builder_pop(b, &f[2]);
spa_pod_builder_string(b, "ext-control-vols-reply");
spa_pod_builder_push_struct(b, &f[2]);
spa_pod_builder_string(b, p->name);
spa_pod_builder_int(b, card->ext_volume.vols_replies[p->index].res);
spa_pod_builder_push_array(b, &f[3]);
for (uint32_t i = 0; i < card->ext_volume.vols_replies[p->index].channels; i++)
spa_pod_builder_double(b, card->ext_volume.vols_replies[p->index].values[i]);
spa_pod_builder_pop(b, &f[3]);
spa_pod_builder_pop(b, &f[2]);
spa_pod_builder_string(b, "ext-control-mute-reply");
spa_pod_builder_push_struct(b, &f[2]);
spa_pod_builder_string(b, p->name);
spa_pod_builder_int(b, card->ext_volume.mute_replies[p->index].res);
spa_pod_builder_bool(b, card->ext_volume.mute_replies[p->index].mute);
spa_pod_builder_pop(b, &f[2]);
spa_pod_builder_pop(b, &f[1]);
return spa_pod_builder_pop(b, &f[0]);
}
static struct acp_port *find_port_for_device(struct acp_card *card, struct acp_device *dev)
{
uint32_t i;
@ -601,6 +642,16 @@ static int impl_enum_params(void *object, int seq,
return -errno;
break;
case SPA_PARAM_Props:
if (result.index >= card->n_ports)
return 0;
p = card->ports[result.index];
if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_HIDDEN))
goto next;
param = build_props(&b.b, card, p);
break;
default:
return -ENOENT;
}
@ -793,6 +844,165 @@ static bool check_active_profile_port(struct impl *this, uint32_t device, uint32
return true;
}
static int parse_ext_control_caps_reply (struct acp_card *card, struct spa_pod_struct *reply)
{
int changed = 0;
struct spa_pod *it;
bool res_parsed = false, caps_parsed = false;
int res = 0, caps = 0;
int field = 0;
SPA_POD_STRUCT_FOREACH(reply, it) {
switch (field++) {
case 0:
if (spa_pod_get_int(it, &res) >= 0)
res_parsed = true;
break;
case 1:
if (spa_pod_get_int(it, &caps) >= 0)
caps_parsed = true;
break;
default:
break;
}
}
if (res_parsed && caps_parsed) {
card->ext_volume.caps_reply.res = res;
card->ext_volume.caps_reply.caps = caps;
changed++;
}
return changed;
}
static int parse_ext_control_vols_reply (struct acp_card *card, struct spa_pod_struct *reply)
{
int changed = 0;
struct spa_pod *it;
const char *route_name = NULL;
bool res_parsed = false, vols_parsed = false;
int res = 0;
uint32_t channels;
int64_t *vols;
uint32_t port_index = ACP_INVALID_INDEX;
int field = 0;
SPA_POD_STRUCT_FOREACH(reply, it) {
switch (field++) {
case 0:
spa_pod_get_string(it, &route_name);
break;
case 1:
if (spa_pod_get_int(it, &res) >= 0)
res_parsed = true;
break;
case 2:
vols_parsed = true;
channels = SPA_POD_ARRAY_N_VALUES(SPA_POD_BODY(it));
vols = SPA_POD_ARRAY_VALUES(SPA_POD_BODY(it));
break;
default:
break;
}
}
port_index = find_route_by_name(card, route_name);
if (port_index != ACP_INVALID_INDEX && res_parsed && vols_parsed) {
card->ext_volume.vols_replies[port_index].res = res;
card->ext_volume.vols_replies[port_index].channels = channels;
for (uint32_t i = 0; i < channels; i++)
card->ext_volume.vols_replies[port_index].values[i] = vols[i];
changed++;
}
return changed;
}
static int parse_ext_control_mute_reply (struct acp_card *card, struct spa_pod_struct *reply)
{
int changed = 0;
struct spa_pod *it;
const char *route_name = NULL;
bool res_parsed = false, mute_parsed = false, mute = false;
int res = 0;
uint32_t port_index = ACP_INVALID_INDEX;
int field = 0;
SPA_POD_STRUCT_FOREACH(reply, it) {
switch (field++) {
case 0:
spa_pod_get_string(it, &route_name);
break;
case 1:
if (spa_pod_get_int(it, &res) >= 0)
res_parsed = true;
break;
case 2:
if (spa_pod_get_bool(it, &mute) >= 0)
mute_parsed = true;
break;
default:
break;
}
}
port_index = find_route_by_name(card, route_name);
if (port_index != ACP_INVALID_INDEX && res_parsed && mute_parsed) {
card->ext_volume.mute_replies[port_index].res = res;
card->ext_volume.mute_replies[port_index].mute = mute;
changed++;
}
return changed;
}
static int parse_prop_params(struct impl *this, struct spa_pod *params)
{
struct acp_card *card = this->card;
struct spa_pod_parser prs;
struct spa_pod_frame f;
int changed = 0;
if (params == NULL)
return 0;
spa_pod_parser_pod(&prs, params);
if (spa_pod_parser_push_struct(&prs, &f) < 0)
return 0;
while (true) {
const char *name;
struct spa_pod *pod;
if (spa_pod_parser_get_string(&prs, &name) < 0)
break;
if (spa_pod_parser_get_pod(&prs, &pod) < 0)
break;
if (spa_streq(name, "ext-control-caps-reply") && spa_pod_is_struct(pod)) {
parse_ext_control_caps_reply(card, (struct spa_pod_struct *)pod);
} else if (spa_streq(name, "ext-control-vols-reply") && spa_pod_is_struct(pod)) {
parse_ext_control_vols_reply(card, (struct spa_pod_struct *)pod);
} else if (spa_streq(name, "ext-control-mute-reply") && spa_pod_is_struct(pod)) {
parse_ext_control_mute_reply(card, (struct spa_pod_struct *)pod);
} else
continue;
changed++;
}
if (changed > 0) {
this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
this->params[IDX_Props].user++;
}
return changed;
}
static int impl_set_param(void *object,
uint32_t id, uint32_t flags,
const struct spa_pod *param)
@ -879,6 +1089,25 @@ static int impl_set_param(void *object,
emit_info(this, false);
break;
}
case SPA_PARAM_Props:
{
struct spa_pod *params = NULL;
if (param == NULL)
return 0;
if ((res = spa_pod_parse_object(param,
SPA_TYPE_OBJECT_Props, NULL,
SPA_PROP_params, SPA_POD_OPT_Pod(&params))) < 0) {
spa_log_warn(this->log, "can't parse props");
spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
return res;
}
parse_prop_params(this, params);
break;
}
default:
return -ENOENT;
}
@ -1083,6 +1312,12 @@ static void on_mute_changed(void *data, struct acp_device *dev)
spa_device_emit_event(&this->hooks, event);
}
static void on_ext_vol_event_available(void *data, struct spa_event *event)
{
struct impl *this = data;
spa_device_emit_event(&this->hooks, event);
}
static const struct acp_card_events card_events = {
ACP_VERSION_CARD_EVENTS,
.props_changed = card_props_changed,
@ -1092,6 +1327,7 @@ static const struct acp_card_events card_events = {
.port_available = card_port_available,
.volume_changed = on_volume_changed,
.mute_changed = on_mute_changed,
.ext_vol_event_available = on_ext_vol_event_available,
};
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)