From fb74ab9054cb625aeff4e271e9134b0fd0cdcfde Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 8 Jun 2026 10:21:46 +0200 Subject: [PATCH] alsa: add volume limits Add 3 levels of volume limits. 1. Add api.acp.min-volume and api.acp.max-valume on the ACP devices that is applied to all noded from this device 2. Add api.acp.device..min-volume and api.acp.device..max-volume that is applied to all nodes from the device with the given node-name. 3. Add api.acp.port..min-volume and api.acp.port..max-volume that is applied to all ports from the device with the given port-name. The volume settings on an ALSA nodes can either go through the device on the Routes (ports) to control the hardware mixer volumes and then the remainder is performed on the nodes in software by the channel mixer. We need to set the limits on the channel mixer when the hardware mixer does not have routes. This is not an easy way to set the volume limits but it provides a static configuration option to enforce the limits. An easier configuration option will also make it easier to change/bypass the limits, which these options can guard against. See #5266, #4323, #1517 --- doc/dox/config/pipewire-props.7.md | 25 +++++++++++++ spa/plugins/alsa/acp/acp.c | 60 +++++++++++++++++++++++++++++- spa/plugins/alsa/acp/card.h | 1 + spa/plugins/alsa/acp/compat.c | 4 ++ spa/plugins/alsa/acp/device-port.h | 1 + 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index c6fb632f6..4b93e1637 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -974,6 +974,31 @@ Disable the "Pro Audio" profile for this device. Use the channel count and mapping the connected HDMI device provides via ELD information. +@PAR@ device-prop api.acp.min-volume = 0.0 # float +The minimum default volume for all the nodes from this device. + +@PAR@ device-prop api.acp.max-volume = FLT_MAX # float +The maximum default volume for all the nodes from this device. + +@PAR@ device-prop api.acp.device..min-volume = 0.0 # float +The default minimum volume for the node with the given node-name. This +overrides the api.acp.min-volume for the device. + +@PAR@ device-prop api.acp.device..max-volume = FLT_MAX # float +The default maximum volume for the node with the given node-name. This +overrides the api.acp.max-volume for the device. + +@PAR@ device-prop api.acp.port..min-volume = FLT_MAX # float +The minimum volume for the port with the given port-name. This overrides +the api.acp.device..min-volume for the node. The port name +can be found as the name property in the EnumRoute param of the device. + +@PAR@ device-prop api.acp.port..max-volume = FLT_MAX # float +The maximum volume for the port with the given port-name. This overrides +the api.acp.device..max-volume for the node. The port name +can be found as the name property in the EnumRoute param of the device. + + ## Node properties @PAR@ node-prop audio.channels # integer diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 6518f8b1f..cb16f3242 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -526,8 +526,9 @@ static void add_profiles(pa_card *impl) pa_alsa_device *dev; int n_profiles, n_ports, n_devices; uint32_t idx; - const char *arr; + const char *arr, *str; bool broken_ucm = false; + char name[512]; n_devices = 0; pa_dynarray_init(&impl->out.devices, device_free); @@ -641,15 +642,56 @@ static void add_profiles(pa_card *impl) dp->port.n_profiles = n_profiles; dp->port.profiles = dp->prof.array.data; + snprintf(name, sizeof(name), "api.acp.port.%s.min-volume", dp->name); + if ((str = pa_proplist_gets(impl->proplist, name))) + spa_atof(str, &dp->volume_range[0]); + else + dp->volume_range[0] = impl->volume_range[0]; + + snprintf(name, sizeof(name), "api.acp.port.%s.max-volume", dp->name); + if ((str = pa_proplist_gets(impl->proplist, name))) + spa_atof(str, &dp->volume_range[1]); + else + dp->volume_range[1] = impl->volume_range[1]; + pa_proplist_setf(dp->proplist, "card.profile.port", "%u", dp->port.index); pa_proplist_as_dict(dp->proplist, &dp->port.props); pa_dynarray_append(&impl->out.ports, dp); } PA_DYNARRAY_FOREACH(dev, &impl->out.devices, idx) { + float vol_range[2] = { 0.0f, FLT_MAX }; + n_ports = 0; + + snprintf(name, sizeof(name), "api.acp.device.%s.min-volume", dev->device.name); + if ((str = pa_proplist_gets(impl->proplist, name))) + spa_atof(str, &vol_range[0]); + else + vol_range[0] = impl->volume_range[0]; + + snprintf(name, sizeof(name), "api.acp.device.%s.max-volume", dev->device.name); + if ((str = pa_proplist_gets(impl->proplist, name))) + spa_atof(str, &vol_range[1]); + else + vol_range[1] = impl->volume_range[1]; + PA_HASHMAP_FOREACH(dp, dev->ports, state) { + if (n_ports == 0) { + vol_range[0] = dp->volume_range[0]; + vol_range[1] = dp->volume_range[1]; + } else { + vol_range[0] = fminf(vol_range[0], dp->volume_range[0]); + vol_range[1] = fmaxf(vol_range[1], dp->volume_range[1]); + } pa_dynarray_append(&dev->port_array, dp); pa_dynarray_append(&dp->devices, dev); + n_ports++; } + if (n_ports == 0) { + pa_proplist_setf(dev->proplist, "channelmix.min-volume", "%f", vol_range[0]); + pa_proplist_setf(dev->proplist, "channelmix.max-volume", "%f", vol_range[1]); + } + pa_proplist_as_dict(dev->proplist, &dev->device.props); + dev->device.ports = dev->port_array.array.data; dev->device.n_ports = pa_dynarray_size(&dev->port_array); } @@ -1948,6 +1990,8 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) impl->ignore_dB = false; impl->rate = DEFAULT_RATE; impl->pro_channels = DEFAULT_CHANNELS; + impl->volume_range[0] = 0.0f; + impl->volume_range[1] = FLT_MAX; if (props) { if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL) @@ -1976,6 +2020,10 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) impl->disable_pro_audio = spa_atob(s); if ((s = acp_dict_lookup(props, "api.acp.use-eld-channels")) != NULL) impl->use_eld_channels = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.acp.min-volume")) != NULL) + spa_atof(s, &impl->volume_range[0]); + if ((s = acp_dict_lookup(props, "api.acp.max-volume")) != NULL) + spa_atof(s, &impl->volume_range[1]); } #if SND_LIB_VERSION < 0x10207 @@ -2297,15 +2345,23 @@ int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t pa_card *impl = d->card; uint32_t i; pa_cvolume v, old_volume; + pa_device_port *p; + float *volume_range; if (n_volume == 0) return -EINVAL; old_volume = d->real_volume; + if ((p = d->active_port) != NULL) + volume_range = p->volume_range; + else + volume_range = impl->volume_range; + v.channels = d->mapping->channel_map.channels; for (i = 0; i < v.channels; i++) - v.values[i] = pa_sw_volume_from_linear(volume[i % n_volume]); + v.values[i] = pa_sw_volume_from_linear( + SPA_CLAMPF(volume[i % n_volume], volume_range[0], volume_range[1])); pa_log_info("Set %s volume: min:%d max:%d", d->set_volume ? "hardware" : "software", diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h index f75222dbf..351637814 100644 --- a/spa/plugins/alsa/acp/card.h +++ b/spa/plugins/alsa/acp/card.h @@ -52,6 +52,7 @@ struct pa_card { bool use_eld_channels; uint32_t rate; uint32_t pro_channels; + float volume_range[2]; pa_alsa_ucm_config ucm; pa_alsa_profile_set *profile_set; diff --git a/spa/plugins/alsa/acp/compat.c b/spa/plugins/alsa/acp/compat.c index 050f2ec54..646046764 100644 --- a/spa/plugins/alsa/acp/compat.c +++ b/spa/plugins/alsa/acp/compat.c @@ -20,6 +20,8 @@ #include "config.h" +#include + #include #include @@ -140,6 +142,8 @@ pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, si p->port.direction = data->direction == PA_DIRECTION_OUTPUT ? ACP_DIRECTION_PLAYBACK : ACP_DIRECTION_CAPTURE; p->type = data->type; + p->volume_range[0] = 0.0f; + p->volume_range[1] = FLT_MAX; p->proplist = pa_proplist_new(); pa_proplist_sets(p->proplist, ACP_KEY_PORT_TYPE, str_port_type(data->type)); diff --git a/spa/plugins/alsa/acp/device-port.h b/spa/plugins/alsa/acp/device-port.h index d0224b4e0..b5aa6854b 100644 --- a/spa/plugins/alsa/acp/device-port.h +++ b/spa/plugins/alsa/acp/device-port.h @@ -76,6 +76,7 @@ struct pa_device_port { pa_direction_t direction; int64_t latency_offset; + float volume_range[2]; pa_proplist *proplist; pa_hashmap *profiles;