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.<node-name>.min-volume and
   api.acp.device.<node-name>.max-volume that is applied to all nodes
   from the device with the given node-name.
3. Add api.acp.port.<port-name>.min-volume and
   api.acp.port.<port-name>.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
This commit is contained in:
Wim Taymans 2026-06-08 10:21:46 +02:00
parent 1272f77eb5
commit fb74ab9054
5 changed files with 89 additions and 2 deletions

View file

@ -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.<node-name>.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.<node-name>.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.<port-name>.min-volume = FLT_MAX # float
The minimum volume for the port with the given port-name. This overrides
the api.acp.device.<node-name>.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.<port-name>.max-volume = FLT_MAX # float
The maximum volume for the port with the given port-name. This overrides
the api.acp.device.<node-name>.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

View file

@ -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",

View file

@ -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;

View file

@ -20,6 +20,8 @@
#include "config.h"
#include <float.h>
#include <spa/utils/string.h>
#include <spa/utils/cleanup.h>
@ -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));

View file

@ -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;