alsa: Add a mechanism to bind ALSA controls as prop params

This adds an api.alsa.bind-ctls property to alsa-pcm sink and source
nodes, to bind a property to an ALSA PCM ctl. The property is an array
of ctl names that should be bound.

This can be handy, for example, to bind the Playback/Capture Rate
controls on a USB gadget, in order to track the PCM's state via a node
param.

This is currently wired to be read-only, but it should be easy enough to
make it writable.
This commit is contained in:
Arun Raghavan 2023-11-07 14:02:26 -05:00
parent 6bae812ce0
commit 2871a65b1f
3 changed files with 300 additions and 7 deletions

View file

@ -44,6 +44,7 @@ extern "C" {
#define SPA_KEY_API_ALSA_OPEN_UCM "api.alsa.open.ucm" /**< if UCM should be opened card */
#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 */
/** info from alsa card_info */
#define SPA_KEY_API_ALSA_CARD_ID "api.alsa.card.id" /**< id from card_info */

View file

@ -186,6 +186,69 @@ static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, si
return 0;
}
static struct spa_pod *enum_bind_ctl_propinfo(struct state *state, uint32_t idx, struct spa_pod_builder *b)
{
char param_name[1024];
char param_desc[1024];
snd_ctl_elem_info_t *info = state->bound_ctls[idx].info;
if (!info) {
// This will end iteration early, so print a warning
spa_log_warn(state->log, "Don't have prop info for bind ctl, bailing");
return NULL;
}
snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s",
snd_ctl_elem_info_get_name(info));
snprintf(param_desc, sizeof(param_desc), "Value of ALSA control '%s'",
snd_ctl_elem_info_get_name(info));
// We don't have meaningful default values
switch (snd_ctl_elem_info_get_type(info)) {
case SND_CTL_ELEM_TYPE_BOOLEAN:
return spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
SPA_PROP_INFO_name, SPA_POD_String(param_name),
SPA_PROP_INFO_description, SPA_POD_String(param_desc),
SPA_PROP_INFO_type, SPA_POD_Bool(false),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
case SND_CTL_ELEM_TYPE_INTEGER:
return spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
SPA_PROP_INFO_name, SPA_POD_String(param_name),
SPA_PROP_INFO_description, SPA_POD_String(param_desc),
SPA_PROP_INFO_type, SPA_POD_Int(0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case SND_CTL_ELEM_TYPE_INTEGER64:
return spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
SPA_PROP_INFO_name, SPA_POD_String(param_name),
SPA_PROP_INFO_description, SPA_POD_String(param_desc),
SPA_PROP_INFO_type, SPA_POD_Long(0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case SND_CTL_ELEM_TYPE_ENUMERATED:
return spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
SPA_PROP_INFO_name, SPA_POD_String(param_name),
SPA_PROP_INFO_description, SPA_POD_String(param_desc),
SPA_PROP_INFO_type, SPA_POD_Int(0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
default:
// FIXME: we can probably support bytes but the length seems unknown in the API
spa_log_warn(state->log, "%s ctl '%s' not supported",
snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)),
snd_ctl_elem_info_get_name(info));
return NULL;
}
}
struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
uint32_t idx, struct spa_pod_builder *b)
{
@ -348,12 +411,67 @@ struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
// While adding params here, update the math in default too
default:
return NULL;
idx -= 17;
if (idx <= state->num_bind_ctls)
param = enum_bind_ctl_propinfo(state, idx - 1, b);
else
return NULL;
}
return param;
}
static void add_bind_ctl_param(struct state *state, const snd_ctl_elem_value_t *elem, const snd_ctl_elem_info_t *info,
struct spa_pod_builder *b)
{
char param_name[1024];
snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s",
snd_ctl_elem_info_get_name(info));
spa_pod_builder_string(b, param_name);
switch (snd_ctl_elem_info_get_type(info)) {
case SND_CTL_ELEM_TYPE_BOOLEAN:
spa_pod_builder_bool(b, snd_ctl_elem_value_get_boolean(elem, 0));
break;
case SND_CTL_ELEM_TYPE_INTEGER:
spa_pod_builder_int(b, snd_ctl_elem_value_get_integer(elem, 0));
break;
case SND_CTL_ELEM_TYPE_INTEGER64:
spa_pod_builder_long(b, snd_ctl_elem_value_get_integer64(elem, 0));
break;
case SND_CTL_ELEM_TYPE_ENUMERATED:
spa_pod_builder_int(b, snd_ctl_elem_value_get_enumerated(elem, 0));
break;
default:
// FIXME: we can probably support bytes but the length seems unknown in the API
spa_log_warn(state->log, "%s ctl '%s' not supported",
snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)),
snd_ctl_elem_info_get_name(info));
break;
}
}
static void add_bind_ctl_params(struct state *state, struct spa_pod_builder *b)
{
int err;
for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value);
if (err < 0) {
spa_log_warn(state->log, "Could not read elem value for '%s': %s",
state->bound_ctls[i].name, snd_strerror(err));
}
add_bind_ctl_param(state, state->bound_ctls[i].value, state->bound_ctls[i].info, b);
}
}
int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
{
struct spa_pod_frame f[1];
@ -421,6 +539,8 @@ int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
spa_pod_builder_string(b, "clock.name");
spa_pod_builder_string(b, state->clock_name);
add_bind_ctl_params(state, b);
spa_pod_builder_pop(b, &f[0]);
return 0;
}
@ -500,6 +620,123 @@ static void silence_error_handler(const char *file, int line,
{
}
static void fill_device_name(struct state *state, const char *params, char device_name[], size_t len)
{
spa_scnprintf(device_name, len, "%s%s%s",
state->card->ucm_prefix ? state->card->ucm_prefix : "",
state->props.device, params ? params : "");
}
static void bind_ctl_event(struct spa_source *source)
{
// We don't know if a bound element changed or not, so let's find out
struct state *state = source->data;
snd_ctl_elem_value_t *old_value;
bool changed = false;
snd_ctl_elem_value_alloca(&old_value);
for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
int err;
snd_ctl_elem_value_copy(old_value, state->bound_ctls[i].value);
err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value);
if (err < 0) {
spa_log_warn(state->log, "Could not read ctl '%s': %s",
state->bound_ctls[i].name, snd_strerror(err));
continue;
}
if (snd_ctl_elem_value_compare(old_value, state->bound_ctls[i].value) != 0) {
// We don't need to check all the ctls, if one changed,
// we'll emit a notification and they'll be read when
// the props are read
spa_log_debug(state->log, "bound ctl '%s' has changed", state->bound_ctls[i].name);
changed = true;
break;
}
}
if (changed) {
state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
state->params[NODE_Props].user++;
spa_alsa_emit_node_info(state, false);
}
}
static void bind_ctls_for_params(struct state *state)
{
struct pollfd pfds[16];
int err;
if (state->num_bind_ctls == 0)
return;
if (!state->ctl) {
char device_name[256];
fill_device_name(state, NULL, device_name, sizeof(device_name));
err = snd_ctl_open(&state->ctl, device_name, SND_CTL_NONBLOCK);
if (err < 0) {
spa_log_info(state->log, "%s could not find ctl device: %s",
state->props.device, snd_strerror(err));
state->ctl = NULL;
return;
}
}
state->ctl_n_fds = snd_ctl_poll_descriptors_count(state->ctl);
if (state->ctl_n_fds > (int)SPA_N_ELEMENTS(state->ctl_sources)) {
spa_log_warn(state->log, "Too many poll descriptors (%d), listening to a subset", state->ctl_n_fds);
state->ctl_n_fds = SPA_N_ELEMENTS(state->ctl_sources);
}
if ((err = snd_ctl_poll_descriptors(state->ctl, pfds, state->ctl_n_fds)) < 0) {
spa_log_warn(state->log, "Could not get poll descriptors: %s", snd_strerror(err));
return;
}
snd_ctl_subscribe_events(state->ctl, 1);
for (int i = 0; i < state->ctl_n_fds; i++) {
state->ctl_sources[i].func = bind_ctl_event;
state->ctl_sources[i].data = state;
state->ctl_sources[i].fd = pfds[i].fd;
state->ctl_sources[i].mask = SPA_IO_IN;
state->ctl_sources[i].rmask = 0;
spa_loop_add_source(state->data_loop, &state->ctl_sources[i]);
}
for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
snd_ctl_elem_id_t *id;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_id_set_name(id, state->bound_ctls[i].name);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM);
snd_ctl_elem_info_malloc(&state->bound_ctls[i].info);
snd_ctl_elem_info_set_id(state->bound_ctls[i].info, id);
err = snd_ctl_elem_info(state->ctl, state->bound_ctls[i].info);
if (err < 0) {
spa_log_warn(state->log, "Could not read elem info for '%s': %s",
state->bound_ctls[i].name, snd_strerror(err));
snd_ctl_elem_info_free(state->bound_ctls[i].info);
state->bound_ctls[i].info = NULL;
continue;
}
snd_ctl_elem_value_malloc(&state->bound_ctls[i].value);
snd_ctl_elem_value_set_id(state->bound_ctls[i].value, id);
spa_log_debug(state->log, "Binding ctl for '%s'",
snd_ctl_elem_info_get_name(state->bound_ctls[i].info));
}
}
int spa_alsa_init(struct state *state, const struct spa_dict *info)
{
uint32_t i;
@ -527,6 +764,24 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
state->open_ucm = spa_atob(s);
} else if (spa_streq(k, "clock.quantum-limit")) {
spa_atou32(s, &state->quantum_limit, 0);
} else if (spa_streq(k, SPA_KEY_API_ALSA_BIND_CTLS)) {
struct spa_json it[2];
char v[256];
unsigned int i = 0;
/* Read a list of ALSA control names to bind as params */
spa_json_init(&it[0], s, strlen(s));
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
spa_json_init(&it[1], s, strlen(s));
while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
i < SPA_N_ELEMENTS(state->bound_ctls)) {
strncpy(state->bound_ctls[i].name, v, sizeof(state->bound_ctls[i].name));
i++;
}
state->num_bind_ctls = i;
/* We'll do the actual binding after checking the card exists */
} else {
alsa_set_param(state, k, s);
}
@ -560,6 +815,8 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info)
state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
state->rate_limit.burst = 1;
bind_ctls_for_params(state);
return 0;
}
@ -580,6 +837,26 @@ int spa_alsa_clear(struct state *state)
free(state->tag[0]);
free(state->tag[1]);
if (state->ctl) {
for (unsigned int i = 0; i < state->num_bind_ctls; i++) {
if (state->bound_ctls[i].info) {
snd_ctl_elem_info_free(state->bound_ctls[i].info);
state->bound_ctls[i].info = NULL;
}
if (state->bound_ctls[i].value) {
snd_ctl_elem_value_free(state->bound_ctls[i].value);
state->bound_ctls[i].value = NULL;
}
}
for (int i = 0; i < state->ctl_n_fds; i++) {
spa_loop_remove_source(state->data_loop, &state->ctl_sources[i]);
}
snd_ctl_close(state->ctl);
state->ctl = NULL;
}
return err;
}
@ -664,9 +941,7 @@ int spa_alsa_open(struct state *state, const char *params)
if (state->opened)
return 0;
spa_scnprintf(device_name, sizeof(device_name), "%s%s%s",
state->card->ucm_prefix ? state->card->ucm_prefix : "",
props->device, params ? params : "");
fill_device_name(state, params, device_name, sizeof(device_name));
spa_scnprintf(state->name, sizeof(state->name), "%s%s",
props->device, state->stream == SND_PCM_STREAM_CAPTURE ? "c" : "p");
@ -759,8 +1034,11 @@ int spa_alsa_close(struct state *state)
snd_ctl_elem_value_free(state->pitch_elem);
state->pitch_elem = NULL;
snd_ctl_close(state->ctl);
state->ctl = NULL;
// Close it unless we've got some bind_ctls we're listening to
if (state->ctl_n_fds == 0) {
snd_ctl_close(state->ctl);
state->ctl = NULL;
}
}
return err;

View file

@ -92,6 +92,12 @@ struct rt_state {
unsigned int following:1;
};
struct bound_ctl {
char name[256];
snd_ctl_elem_info_t *info;
snd_ctl_elem_value_t *value;
};
struct state {
struct spa_handle handle;
struct spa_node node;
@ -240,11 +246,19 @@ struct state {
struct spa_pod *tag[2];
/* Rate match via an ALSA ctl */
/* for rate match and bind ctls */
snd_ctl_t *ctl;
/* Rate match via an ALSA ctl */
snd_ctl_elem_value_t *pitch_elem;
double last_rate;
/* ALSA ctls exposed as params */
unsigned int num_bind_ctls;
struct bound_ctl bound_ctls[16];
struct spa_source ctl_sources[MAX_POLL];
int ctl_n_fds;
struct spa_list link;
struct spa_list followers;