From 122bfd712b5f6434c4f3376d7025afc95a3871eb Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Wed, 4 Feb 2026 16:58:21 -0800 Subject: [PATCH 1/8] spa: Add a param for audio volume control flags Should be usable to signal to clients what volume control capabilities a device supports. --- spa/include/spa/param/audio/volume.h | 29 ++++++++++++++++++++++++++++ spa/include/spa/param/props-types.h | 1 + spa/include/spa/param/props.h | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 spa/include/spa/param/audio/volume.h diff --git a/spa/include/spa/param/audio/volume.h b/spa/include/spa/param/audio/volume.h new file mode 100644 index 000000000..56df18064 --- /dev/null +++ b/spa/include/spa/param/audio/volume.h @@ -0,0 +1,29 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_VOLUME_H +#define SPA_AUDIO_VOLUME_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** Flags for volume control capabilities */ +enum spa_audio_volume_control_flags { + SPA_AUDIO_VOLUME_CONTROL_NONE = 0, /**<< No flags */ + SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME = (1 << 0), /**<< Volume value can be read */ + SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE = (1 << 1), /**<< Volume value can be set */ + SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN = (1 << 2), /**<< Volume value can incremented/decremented */ + SPA_AUDIO_VOLUME_CONTROL_READ_MUTE = (1 << 3), /**<< Mute state can be read */ + SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE = (1 << 4), /**<< Mute state can be set */ + SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE = (1 << 5), /**<< Mute state can be toggled */ + SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE = (1 << 6), /**<< Per-channel volumes can be read */ + SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE = (1 << 7), /**<< Per-channel volumes can be set */ +}; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_VOLUME_H */ diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h index 0a7c916b8..239770e59 100644 --- a/spa/include/spa/param/props-types.h +++ b/spa/include/spa/param/props-types.h @@ -66,6 +66,7 @@ static const struct spa_type_info spa_type_props[] = { { SPA_PROP_volumeRampTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampTime", NULL }, { SPA_PROP_volumeRampStepTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepTime", NULL }, { SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", spa_type_audio_volume_ramp_scale }, + { SPA_PROP_volumeControlFlags, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeControlFlags", NULL }, { SPA_PROP_brightness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL }, { SPA_PROP_contrast, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL }, diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h index 48338b870..94862ea88 100644 --- a/spa/include/spa/param/props.h +++ b/spa/include/spa/param/props.h @@ -98,6 +98,8 @@ enum spa_prop { * to ramp the */ SPA_PROP_volumeRampScale, /**< the scale or graph to used to ramp the * volume */ + SPA_PROP_volumeControlFlags, /**< Available volume control features + * (Id enum spa_audio_volume_control_flags) */ SPA_PROP_START_Video = 0x20000, /**< video related properties */ SPA_PROP_brightness, From ef6f5194e31a0f8a3aba3fd463a0c40e6b491f5f Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Fri, 15 May 2026 10:11:13 -0400 Subject: [PATCH 2/8] spa: alsa: Add a mechanism for external volume control Currently enabled at device creation and delegated to an external entity via spa_device events. --- spa/include/spa/monitor/event.h | 1 + spa/include/spa/monitor/type-info.h | 1 + spa/plugins/alsa/acp/acp.c | 332 +++++++++++++++++++++------- spa/plugins/alsa/acp/acp.h | 11 + spa/plugins/alsa/acp/card.h | 2 + spa/plugins/alsa/acp/ext-volume.c | 298 +++++++++++++++++++++++++ spa/plugins/alsa/acp/ext-volume.h | 69 ++++++ spa/plugins/alsa/acp/meson.build | 1 + spa/plugins/alsa/alsa-acp-device.c | 238 +++++++++++++++++++- 9 files changed, 867 insertions(+), 86 deletions(-) create mode 100644 spa/plugins/alsa/acp/ext-volume.c create mode 100644 spa/plugins/alsa/acp/ext-volume.h diff --git a/spa/include/spa/monitor/event.h b/spa/include/spa/monitor/event.h index accf8382f..740b6266b 100644 --- a/spa/include/spa/monitor/event.h +++ b/spa/include/spa/monitor/event.h @@ -19,6 +19,7 @@ extern "C" { /* object id of SPA_TYPE_EVENT_Device */ enum spa_device_event { SPA_DEVICE_EVENT_ObjectConfig, + SPA_DEVICE_EVENT_ExtVolumeControl, }; #define SPA_DEVICE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Device) diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h index 1658594b4..5ea1098d8 100644 --- a/spa/include/spa/monitor/type-info.h +++ b/spa/include/spa/monitor/type-info.h @@ -26,6 +26,7 @@ extern "C" { static const struct spa_type_info spa_type_device_event_id[] = { { SPA_DEVICE_EVENT_ObjectConfig, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ObjectConfig", NULL }, + { SPA_DEVICE_EVENT_ExtVolumeControl, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ExtVolumeControl", NULL }, { 0, 0, NULL, NULL }, }; diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index fa4c199b2..5c9bd119c 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -5,6 +5,7 @@ #include "acp.h" #include "alsa-mixer.h" #include "alsa-ucm.h" +#include "ext-volume.h" #include #include @@ -1246,6 +1247,28 @@ static void init_eld_ctls(pa_card *impl) } } +static void ext_volume_notifier_cb (void *data, struct spa_event *event) { + struct pa_card *impl = data; + + if (impl->events && impl->events->ext_vol_event_available) + impl->events->ext_vol_event_available(impl->user_data, event); +} + +static void init_ext_volume(pa_card *impl) +{ + struct acp_card *card = &impl->card; + int res; + + if (!impl->ext_volume_ctrl) + return; + + res = spa_acp_ext_volume_init(&card->ext_volume, impl->name, ext_volume_notifier_cb, impl); + if (res < 0) { + pa_log_notice("failed to init ACP external volume: %s", snd_strerror(res)); + return; + } +} + uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name) { uint32_t i; @@ -1357,21 +1380,50 @@ static int read_volume(pa_alsa_device *dev) return 0; } - if (!dev->mixer_handle) + if (impl->card.ext_volume.initialized && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME)) { + pa_cvolume ext_vol; + + if (!dev->active_port) + return -EINVAL; + + /* Externally managed volume */ + if ((res = spa_acp_ext_volume_read_volume(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index, &ext_vol)) < 0) { + pa_log_error("Could not read volume: %s", snd_strerror(res)); + return 0; + } + + /* FIXME: scale to the range from capabilities */ + if (ext_vol.channels == 1) { + r.channels = dev->device.format.channels; + for (unsigned int i = 0; i < r.channels; i++) + r.values[i] = ext_vol.values[0]; + } else if (ext_vol.channels == dev->device.format.channels) { + r = ext_vol; + } else { + pa_log_error("Mismatch channel count: device %u != volume %u", + dev->device.format.channels, ext_vol.channels); + return 0; + } + } else if (dev->mixer_handle) { + /* ALSA mixer for volume */ + if (dev->mixer_path->has_volume_mute && dev->muted) { + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->hardware_volume, dev->base_volume); + pa_log_debug("Reading cached volume only."); + } else { + if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, + &dev->mapping->channel_map, &r)) < 0) + return res; + } + + /* Shift down by the base volume, so that 0dB becomes maximum volume */ + pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); + + } else return 0; - if (dev->mixer_path->has_volume_mute && dev->muted) { - /* Shift up by the base volume */ - pa_sw_cvolume_divide_scalar(&r, &dev->hardware_volume, dev->base_volume); - pa_log_debug("Reading cached volume only."); - } else { - if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, - &dev->mapping->channel_map, &r)) < 0) - return res; - } - - /* Shift down by the base volume, so that 0dB becomes maximum volume */ - pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); if (pa_cvolume_equal(&dev->hardware_volume, &r)) return 0; @@ -1394,12 +1446,10 @@ static int read_volume(pa_alsa_device *dev) static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) { + pa_card *impl = dev->card; pa_cvolume r; bool write_to_hw; - if (v != &dev->real_volume) - dev->real_volume = *v; - if (dev->ucm_context) { if (!dev->active_port) return; @@ -1408,52 +1458,100 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) return; } - if (!dev->mixer_handle) - return; + if (impl->card.ext_volume.initialized) { + if (!dev->active_port) + return; + if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE) { + /* External volume control by value */ + if (spa_acp_ext_volume_write_volume_absolute(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index, &dev->real_volume) < 0) { + pa_log_error("Could not write volume"); + return; + } - /* Shift up by the base volume */ - pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); + /* Update volume if we were successful */ + if (v != &dev->real_volume) + dev->real_volume = *v; + } else if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME) && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN)) { + /* External volume control by increment/decrement only */ + pa_volume_t cur, new; + float step; + int i; - write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted); + cur = pa_cvolume_max(&dev->real_volume); + new = pa_cvolume_max(v); - if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, - &r, false, write_to_hw) < 0) - return; + if (cur < new) + step = 1; + else if (cur > new) + step = -1; + else + return; - /* Shift down by the base volume, so that 0dB becomes maximum volume */ - pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); + if (spa_acp_ext_volume_write_volume_relative(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index, step) < 0) { + pa_log_error("Could not write volume"); + return; + } - dev->hardware_volume = r; + /* Update volume if we were successful */ + dev->real_volume.channels = v->channels; + for (i = 0; i < (int)v->channels; i++) + dev->real_volume.values[i] = v->values[i] + (int)step; + } else { + pa_log_debug("Ignoring volume setting, ext volume control does not support it"); + } + } else if (dev->mixer_handle) { + /* ALSA mixer control for volume */ - if (dev->mixer_path->has_dB) { - pa_cvolume new_soft_volume; - bool accurate_enough; + if (v != &dev->real_volume) + dev->real_volume = *v; - /* Match exactly what the user requested by software */ - pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume); + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); - /* If the adjustment to do in software is only minimal we - * can skip it. That saves us CPU at the expense of a bit of - * accuracy */ - accurate_enough = - (pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) && - (pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY)); + write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted); - pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume)); - pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume)); - pa_log_debug("Calculated software volume: %d (accurate-enough=%s)", - pa_cvolume_max(&new_soft_volume), - pa_yes_no(accurate_enough)); + if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, write_to_hw) < 0) + return; - if (accurate_enough) - pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels); + /* Shift down by the base volume, so that 0dB becomes maximum volume */ + pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); - dev->soft_volume = new_soft_volume; - } else { - pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r)); - /* We can't match exactly what the user requested, hence let's - * at least tell the user about it */ - dev->real_volume = r; + dev->hardware_volume = r; + + if (dev->mixer_path->has_dB) { + pa_cvolume new_soft_volume; + bool accurate_enough; + + /* Match exactly what the user requested by software */ + pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume); + + /* If the adjustment to do in software is only minimal we + * can skip it. That saves us CPU at the expense of a bit of + * accuracy */ + accurate_enough = + (pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) && + (pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY)); + + pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume)); + pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume)); + pa_log_debug("Calculated software volume: %d (accurate-enough=%s)", + pa_cvolume_max(&new_soft_volume), + pa_yes_no(accurate_enough)); + + if (accurate_enough) + pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels); + + dev->soft_volume = new_soft_volume; + } else { + pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r)); + /* We can't match exactly what the user requested, hence let's + * at least tell the user about it */ + dev->real_volume = r; + } } } @@ -1471,22 +1569,34 @@ static int read_mute(pa_alsa_device *dev) return 0; } - if (!dev->mixer_handle) + if (impl->card.ext_volume.initialized && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE)) { + if (!dev->active_port) + return -EINVAL; + + /* Externally managed mute state */ + if (spa_acp_ext_volume_read_mute(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index, &mute) < 0) { + pa_log_error("Could not read mute state"); + return 0; + } + } else if (dev->mixer_handle) { + /* ALSA mixer for mute state */ + if (dev->mixer_path->has_volume_mute) { + pa_cvolume mute_vol; + pa_cvolume r; + + pa_cvolume_mute(&mute_vol, dev->mapping->channel_map.channels); + if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0) + return res; + mute = pa_cvolume_equal(&mute_vol, &r); + } else { + if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0) + return res; + } + } else return 0; - if (dev->mixer_path->has_volume_mute) { - pa_cvolume mute_vol; - pa_cvolume r; - - pa_cvolume_mute(&mute_vol, dev->mapping->channel_map.channels); - if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0) - return res; - mute = pa_cvolume_equal(&mute_vol, &r); - } else { - if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0) - return res; - } - if (mute == dev->muted) return 0; @@ -1501,7 +1611,7 @@ static int read_mute(pa_alsa_device *dev) static void set_mute(pa_alsa_device *dev, bool mute) { - dev->muted = mute; + pa_card *impl = dev->card; if (dev->ucm_context) { if (!dev->active_port) @@ -1511,27 +1621,54 @@ static void set_mute(pa_alsa_device *dev, bool mute) return; } - if (!dev->mixer_handle) - return; + if (impl->card.ext_volume.initialized) { + if (!dev->active_port) + return; + if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE)) { + /* Externally managed mute state by value*/ + if (spa_acp_ext_volume_write_mute_value(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index, mute) < 0) { + pa_log_error("Could not write mute state"); + return; + } - if (dev->mixer_path->has_volume_mute) { - pa_cvolume r; + dev->muted = mute; + } else if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE) && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE)) { + /* Externally managed mute state toggle */ + if (spa_acp_ext_volume_write_mute_toggle(&impl->card.ext_volume, impl->name, + dev->active_port->name, dev->active_port->port.index) < 0) { + pa_log_error("Could not write mute toggle"); + return; + } - if (mute) { - pa_cvolume_mute(&r, dev->mapping->channel_map.channels); - pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, - &r, false, true); + dev->muted = mute; } else { - /* Shift up by the base volume */ - pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); - pa_log_debug("Restoring volume: %d", pa_cvolume_max(&dev->real_volume)); - if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, - &r, false, true) < 0) - pa_log_error("Unable to restore volume %d during unmute", - pa_cvolume_max(&dev->real_volume)); + pa_log_debug("Ignoring mute setting, ext volume control does not support it"); + } + } else if (dev->mixer_handle) { + /* ALSA mixer for mute state */ + dev->muted = mute; + + if (dev->mixer_path->has_volume_mute) { + pa_cvolume r; + + if (mute) { + pa_cvolume_mute(&r, dev->mapping->channel_map.channels); + pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, true); + } else { + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); + pa_log_debug("Restoring volume: %d", pa_cvolume_max(&dev->real_volume)); + if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, true) < 0) + pa_log_error("Unable to restore volume %d during unmute", + pa_cvolume_max(&dev->real_volume)); + } + } else { + pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute); } - } else { - pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute); } } @@ -1539,7 +1676,15 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) { pa_assert(dev); - if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) { + if (impl->ext_volume_ctrl) { + dev->device.flags |= ACP_DEVICE_HW_VOLUME; + if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME) + dev->read_volume = read_volume; + if (impl->card.ext_volume.flags & + (SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE | + SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN)) + dev->set_volume = set_volume; + } else if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) { dev->read_volume = NULL; dev->set_volume = NULL; pa_log_info("Driver does not support hardware volume control, " @@ -1587,7 +1732,15 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) dev->device.base_volume = (float)pa_sw_volume_to_linear(dev->base_volume); dev->device.volume_step = 1.0f / dev->n_volume_steps; - if (impl->soft_mixer || !dev->mixer_path || + if (impl->ext_volume_ctrl) { + dev->device.flags |= ACP_DEVICE_HW_MUTE; + if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE) + dev->read_mute = read_mute; + if (impl->card.ext_volume.flags & + (SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE | + SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE)) + dev->set_mute = set_mute; + } else if (impl->soft_mixer || !dev->mixer_path || (!dev->mixer_path->has_mute && !dev->mixer_path->has_volume_mute)) { dev->read_mute = NULL; dev->set_mute = NULL; @@ -1656,6 +1809,12 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB) return 0; } + if (impl->ext_volume_ctrl) { + /* We've been told to use an external service to manage volume */ + init_ext_volume(impl); + dev->device.ext_volume_flags = impl->card.ext_volume.flags; + } + mixer_volume_init(impl, dev); /* Will we need to register callbacks? */ @@ -1975,6 +2134,8 @@ 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.alsa.external-volume-control")) != NULL) + impl->ext_volume_ctrl = spa_atob(s); } #if SND_LIB_VERSION < 0x10207 @@ -2360,6 +2521,7 @@ int acp_device_set_mute(struct acp_device *dev, bool mute) if (d->set_mute) { d->set_mute(d, mute); + mute = d->muted; } else { d->muted = mute; } diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h index 5fb5b96f7..7d7e90f21 100644 --- a/spa/plugins/alsa/acp/acp.h +++ b/spa/plugins/alsa/acp/acp.h @@ -11,6 +11,10 @@ #include #include +#include + +#include "ext-volume.h" + #ifdef __cplusplus extern "C" { #else @@ -188,6 +192,8 @@ struct acp_card_events { void (*volume_changed) (void *data, struct acp_device *dev); void (*mute_changed) (void *data, struct acp_device *dev); + + void (*ext_vol_event_available) (void *data, struct spa_event *event); }; struct acp_port { @@ -239,6 +245,9 @@ struct acp_device { int64_t latency_ns; uint32_t codecs[32]; uint32_t n_codecs; + + uint32_t ext_volume_flags; + }; struct acp_card_profile { @@ -277,6 +286,8 @@ struct acp_card { struct acp_port **ports; uint32_t preferred_input_port_index; uint32_t preferred_output_port_index; + + struct spa_acp_ext_volume ext_volume; }; struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props); diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h index f75222dbf..25a2eeb1d 100644 --- a/spa/plugins/alsa/acp/card.h +++ b/spa/plugins/alsa/acp/card.h @@ -68,6 +68,8 @@ struct pa_card { const struct acp_card_events *events; void *user_data; + + bool ext_volume_ctrl; }; bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card); diff --git a/spa/plugins/alsa/acp/ext-volume.c b/spa/plugins/alsa/acp/ext-volume.c new file mode 100644 index 000000000..ae7bb0c39 --- /dev/null +++ b/spa/plugins/alsa/acp/ext-volume.c @@ -0,0 +1,298 @@ +/* External Volume Control */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-FileCopyrightText: Copyright © 2026 Julian Bouzas */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include "alsa-mixer.h" +#include "channelmap.h" +#include "volume.h" +#include "ext-volume.h" + +int +spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, const char *device, + ext_volume_notifier_t notifier_cb, void *notifier_data) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + ext_volume->initialized = false; + ext_volume->flags = 0; + + ext_volume->notifier_cb = notifier_cb; + ext_volume->notifier_data = notifier_data; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "GetCapabilities"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'GetCapabilities' ext-vol-control event to %s", device); + + /* Emit */ + ext_volume->caps_reply.res = -EINVAL; + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + pa_log_info("reply after emitting event: res:%d caps:%d", + ext_volume->caps_reply.res, ext_volume->caps_reply.caps); + + /* Check reply */ + if (ext_volume->caps_reply.res < 0) + return ext_volume->caps_reply.res; + + ext_volume->flags = ext_volume->caps_reply.caps; + ext_volume->initialized = true; + return ext_volume->caps_reply.res; +} + +int +spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "ReadVolume"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'ReadVolume' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->vols_replies[port_index].res = -EINVAL; + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + pa_log_info("reply after emitting event: res:%d channels:%u", + ext_volume->vols_replies[port_index].res, ext_volume->vols_replies[port_index].channels); + + /* Check reply */ + if (ext_volume->vols_replies[port_index].res < 0) + return ext_volume->vols_replies[port_index].res; + + cvol->channels = ext_volume->vols_replies[port_index].channels; + for (uint32_t i = 0; i < cvol->channels && i < PA_CHANNELS_MAX && i < SPA_AUDIO_MAX_CHANNELS; i++) { + double v = ext_volume->vols_replies[port_index].values[i]; + cvol->values[i] = lrint(v * PA_VOLUME_NORM); + } + return ext_volume->vols_replies[port_index].res; +} + +int +spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[4]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "WriteVolumeAbsolute"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + + /* FIXME: scale to the range from capabilities */ + spa_pod_builder_push_array(&b, &f[3]); + if (ext_volume->flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE) { + /* Write all channels */ + for (uint32_t i = 0; i < cvol->channels; i++) + spa_pod_builder_double(&b, cvol->values[i] / PA_VOLUME_NORM); + } else { + /* Single volume */ + spa_pod_builder_double(&b, pa_cvolume_max(cvol) / PA_VOLUME_NORM); + } + spa_pod_builder_pop(&b, &f[3]); + + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'WriteVolumeAbsolute' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + return 0; +} + +int +spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, float step) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "WriteVolumeRelative"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + spa_pod_builder_float(&b, step); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'WriteVolumeRelative' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + return 0; +} + +int +spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, bool *mute) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "ReadMute"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'ReadMute' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->mute_replies[port_index].res = EINVAL; + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + pa_log_info("reply after emitting event: res:%d mute:%u", + ext_volume->mute_replies[port_index].res, ext_volume->mute_replies[port_index].mute); + + /* Check reply */ + if (ext_volume->mute_replies[port_index].res < 0) + return ext_volume->mute_replies[port_index].res; + + *mute = ext_volume->mute_replies[port_index].mute; + return ext_volume->mute_replies[port_index].res; +} + +int +spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, bool mute) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "WriteMuteValue"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + spa_pod_builder_bool(&b, mute); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'WriteMuteValue' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + return 0; +} + +int +spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index) +{ + struct spa_event *ev; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[3]; + + /* Create event */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_push_object(&b, &f[1], + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[2]); + spa_pod_builder_string(&b, "WriteMuteToggle"); + spa_pod_builder_string(&b, device ? device : ""); + spa_pod_builder_string(&b, port_name ? port_name : ""); + spa_pod_builder_pop(&b, &f[2]); + spa_pod_builder_pop(&b, &f[1]); + ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]); + + pa_log_info("sending 'WriteMuteToggle' ext-vol-control event to %s on port %s", device, port_name); + + /* Emit */ + ext_volume->notifier_cb (ext_volume->notifier_data, ev); + + return 0; +} diff --git a/spa/plugins/alsa/acp/ext-volume.h b/spa/plugins/alsa/acp/ext-volume.h new file mode 100644 index 000000000..a8d5bcaba --- /dev/null +++ b/spa/plugins/alsa/acp/ext-volume.h @@ -0,0 +1,69 @@ +/* External Volume Control */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-FileCopyrightText: Copyright © 2026 Julian Bouzas */ +/* SPDX-License-Identifier: MIT */ + +#ifndef ACP_EXT_VOLUME_H +#define ACP_EXT_VOLUME_H + +#include + +#include +#include + +#define MAX_PORTS 256 + +typedef struct pa_cvolume pa_cvolume; + +typedef void (*ext_volume_notifier_t) (void *data, struct spa_event *event); + +struct caps_reply { + int res; + enum spa_audio_volume_control_flags caps; +}; + +struct mute_reply { + int res; + bool mute; +}; + +struct vols_reply { + int res; + uint32_t channels; + double values[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct spa_acp_ext_volume { + bool initialized; + enum spa_audio_volume_control_flags flags; + + ext_volume_notifier_t notifier_cb; + void *notifier_data; + + struct caps_reply caps_reply; + struct mute_reply mute_replies[MAX_PORTS]; + struct vols_reply vols_replies[MAX_PORTS]; +}; + +int spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, const char *device, + ext_volume_notifier_t notifier_cb, void *notifier_data); + +int spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol); + +int spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol); + +int spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, float step); + +int spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, bool *mute); + +int spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index, bool mute); + +int spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *port_name, uint32_t port_index); + +#endif /* ACP_EXT_VOLUME_H */ diff --git a/spa/plugins/alsa/acp/meson.build b/spa/plugins/alsa/acp/meson.build index 0ec97e2b4..777ba5c01 100644 --- a/spa/plugins/alsa/acp/meson.build +++ b/spa/plugins/alsa/acp/meson.build @@ -5,6 +5,7 @@ acp_sources = [ 'alsa-ucm.c', 'alsa-util.c', 'conf-parser.c', + 'ext-volume.c', ] acp_c_args = [ diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c index 47ff03c99..2ce5404a3 100644 --- a/spa/plugins/alsa/alsa-acp-device.c +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -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(¶ms))) < 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) From dadc9f2840e9f0cee24badc935ca555599e28ec0 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 3 Mar 2026 12:17:25 -0800 Subject: [PATCH 3/8] spa: device: Add a send_command() method We don't currently have a mechanism to send commands to devices, which can be handy. --- spa/include/spa/monitor/device.h | 48 +++++++++++++++++++++++++++-- spa/include/spa/monitor/type-info.h | 19 +++++++++++- spa/include/spa/utils/type-info.h | 2 +- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h index cc51f9ef1..57c84a963 100644 --- a/spa/include/spa/monitor/device.h +++ b/spa/include/spa/monitor/device.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #ifdef __cplusplus @@ -126,7 +127,27 @@ struct spa_device_events { #define SPA_DEVICE_METHOD_SYNC 1 #define SPA_DEVICE_METHOD_ENUM_PARAMS 2 #define SPA_DEVICE_METHOD_SET_PARAM 3 -#define SPA_DEVICE_METHOD_NUM 4 +#define SPA_DEVICE_METHOD_SEND_COMMAND 4 +#define SPA_DEVICE_METHOD_NUM 5 + +/* object id of SPA_TYPE_COMMAND_Device */ +enum spa_device_commands { + SPA_DEVICE_COMMAND_User, +}; + +#define SPA_DEVICE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Device) +#define SPA_DEVICE_COMMAND_INIT(id) SPA_COMMAND_INIT(SPA_TYPE_COMMAND_Device, id) + + +/* properties for SPA_TYPE_COMMAND_Device */ +enum spa_command_device { + SPA_COMMAND_DEVICE_START, + + SPA_COMMAND_DEVICE_START_User = 0x1000, + SPA_COMMAND_DEVICE_extra, /** extra info (String) */ + + SPA_COMMAND_DEVICE_START_CUSTOM = 0x1000000, +}; /** * spa_device_methods: @@ -134,7 +155,7 @@ struct spa_device_events { struct spa_device_methods { /* the version of the methods. This can be used to expand this * structure in the future */ -#define SPA_VERSION_DEVICE_METHODS 0 +#define SPA_VERSION_DEVICE_METHODS 1 uint32_t version; /** @@ -226,6 +247,23 @@ struct spa_device_methods { int (*set_param) (void *object, uint32_t id, uint32_t flags, const struct spa_pod *param); + + /** + * Send a command to a device. + * + * This function must be called from the main thread. + * + * \param object a \ref spa_device + * \param command a \ref spa_command of type \ref spa_command_device + * \return 0 on success + * -EINVAL when node or command is NULL + * -ENOTSUP when this node can't process commands + * -EINVAL \a command is an invalid command + * + * \since 1 + */ + int (*send_command) (void *object, const struct spa_command *command); + }; SPA_API_DEVICE int spa_device_add_listener(struct spa_device *object, @@ -256,6 +294,12 @@ SPA_API_DEVICE int spa_device_set_param(struct spa_device *object, return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, set_param, 0, id, flags, param); } +SPA_API_DEVICE int spa_device_send_command(struct spa_device *object, + const struct spa_command *command) +{ + return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, send_command, 1, + command); +} #define SPA_KEY_DEVICE_ENUM_API "device.enum.api" /**< the api used to discover this * device */ diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h index 5ea1098d8..24ca15653 100644 --- a/spa/include/spa/monitor/type-info.h +++ b/spa/include/spa/monitor/type-info.h @@ -5,8 +5,9 @@ #ifndef SPA_DEVICE_TYPE_INFO_H #define SPA_DEVICE_TYPE_INFO_H -#include +#include +#include #include #ifdef __cplusplus @@ -37,6 +38,22 @@ static const struct spa_type_info spa_type_device_event[] = { { 0, 0, NULL, NULL }, }; +#define SPA_TYPE_INFO_DeviceCommand SPA_TYPE_INFO_COMMAND_BASE "Device" +#define SPA_TYPE_INFO_DEVICE_COMMAND_BASE SPA_TYPE_INFO_DeviceCommand ":" + +static const struct spa_type_info spa_type_device_command_id[] = { + { SPA_DEVICE_COMMAND_User, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "User", NULL }, + { 0, 0, NULL, NULL }, +}; + +static const struct spa_type_info spa_type_device_command[] = { + { SPA_COMMAND_DEVICE_START, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE, spa_type_device_command_id }, + + { SPA_COMMAND_DEVICE_extra, SPA_TYPE_String, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "extra", NULL }, + + { 0, 0, NULL, NULL }, +}; + /** * \} */ diff --git a/spa/include/spa/utils/type-info.h b/spa/include/spa/utils/type-info.h index 5eece4db2..eefff174a 100644 --- a/spa/include/spa/utils/type-info.h +++ b/spa/include/spa/utils/type-info.h @@ -62,7 +62,7 @@ static const struct spa_type_info spa_types[] = { { SPA_TYPE_EVENT_Node, SPA_TYPE_Object, SPA_TYPE_INFO_EVENT_BASE "Node", spa_type_node_event }, { SPA_TYPE_COMMAND_START, SPA_TYPE_Object, SPA_TYPE_INFO_Command, NULL }, - { SPA_TYPE_COMMAND_Device, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Device", NULL }, + { SPA_TYPE_COMMAND_Device, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Device", spa_type_device_command }, { SPA_TYPE_COMMAND_Node, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Node", spa_type_node_command }, { SPA_TYPE_OBJECT_START, SPA_TYPE_Object, SPA_TYPE_INFO_Object, NULL }, From e51fbeedafdf13b31d9c26c8d353739066ddb9f8 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 3 Mar 2026 12:34:45 -0800 Subject: [PATCH 4/8] pipewire: device: Add a send_command method For now, useful for sending custom commands to devices. Using this from the command line might look something like: ```sh $ pw-cli send-command VolumeControl '{"volumeUp": }' ``` --- .../module-client-device/protocol-native.c | 34 ++++++++++++++++++- .../module-protocol-native/protocol-native.c | 30 ++++++++++++++++ src/pipewire/device.h | 22 ++++++++++-- src/pipewire/impl-device.c | 21 +++++++++++- src/tests/test-interfaces.c | 4 ++- src/tools/pw-cli.c | 11 ++++-- 6 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/modules/module-client-device/protocol-native.c b/src/modules/module-client-device/protocol-native.c index cbcca0c9a..b5b20f190 100644 --- a/src/modules/module-client-device/protocol-native.c +++ b/src/modules/module-client-device/protocol-native.c @@ -200,6 +200,36 @@ static int device_demarshal_set_param(void *object, const struct pw_protocol_nat return 0; } +static int device_marshal_send_command(void *object, + const struct spa_command *command) +{ + struct pw_resource *resource = object; + struct spa_pod_builder *b; + + b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SEND_COMMAND, NULL); + + spa_pod_builder_add_struct(b, + SPA_POD_Pod(command)); + + return pw_protocol_native_end_resource(resource, b); +} + +static int device_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg) +{ + struct pw_proxy *proxy = object; + struct spa_pod_parser prs; + struct spa_command *command; + + spa_pod_parser_init(&prs, msg->data, msg->size); + if (spa_pod_parser_get_struct(&prs, + SPA_POD_Pod(&command)) < 0) + return -EINVAL; + + pw_proxy_notify(proxy, struct spa_device_methods, send_command, 1, + command); + return 0; +} + static void device_marshal_info(void *data, const struct spa_device_info *info) { @@ -482,7 +512,8 @@ static const struct spa_device_methods pw_protocol_native_device_method_marshal .add_listener = &device_marshal_add_listener, .sync = &device_marshal_sync, .enum_params = &device_marshal_enum_params, - .set_param = &device_marshal_set_param + .set_param = &device_marshal_set_param, + .send_command = &device_marshal_send_command, }; static const struct pw_protocol_native_demarshal @@ -492,6 +523,7 @@ pw_protocol_native_device_method_demarshal[SPA_DEVICE_METHOD_NUM] = [SPA_DEVICE_METHOD_SYNC] = { &device_demarshal_sync, 0 }, [SPA_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0 }, [SPA_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, 0 }, + [SPA_DEVICE_METHOD_SEND_COMMAND] = { &device_demarshal_send_command, 0 }, }; static const struct spa_device_events pw_protocol_native_device_event_marshal = { diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c index 3a8580efb..78f2f7a59 100644 --- a/src/modules/module-protocol-native/protocol-native.c +++ b/src/modules/module-protocol-native/protocol-native.c @@ -1056,6 +1056,34 @@ static int device_demarshal_set_param(void *object, const struct pw_protocol_nat return pw_resource_notify(resource, struct pw_device_methods, set_param, 0, id, flags, param); } +static int device_marshal_send_command(void *object, const struct spa_command *command) +{ + struct pw_proxy *proxy = object; + struct spa_pod_builder *b; + + b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SEND_COMMAND, NULL); + spa_pod_builder_add_struct(b, + SPA_POD_Pod(command)); + return pw_protocol_native_end_proxy(proxy, b); +} + +static int device_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg) +{ + struct pw_resource *resource = object; + struct spa_pod_parser prs; + struct spa_command *command; + + spa_pod_parser_init(&prs, msg->data, msg->size); + if (spa_pod_parser_get_struct(&prs, + SPA_POD_Pod(&command)) < 0) + return -EINVAL; + + if (command == NULL) + return -EINVAL; + + return pw_resource_notify(resource, struct pw_device_methods, send_command, 1, command); +} + static int factory_method_marshal_add_listener(void *object, struct spa_hook *listener, const struct pw_factory_events *events, @@ -2110,6 +2138,7 @@ static const struct pw_device_methods pw_protocol_native_device_method_marshal = .subscribe_params = &device_marshal_subscribe_params, .enum_params = &device_marshal_enum_params, .set_param = &device_marshal_set_param, + .send_command = &device_marshal_send_command, }; static const struct pw_protocol_native_demarshal @@ -2118,6 +2147,7 @@ pw_protocol_native_device_method_demarshal[PW_DEVICE_METHOD_NUM] = { [PW_DEVICE_METHOD_SUBSCRIBE_PARAMS] = { &device_demarshal_subscribe_params, 0, }, [PW_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0, }, [PW_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, PW_PERM_W, }, + [PW_DEVICE_METHOD_SEND_COMMAND] = { &device_demarshal_send_command, PW_PERM_W, }, }; static const struct pw_device_events pw_protocol_native_device_event_marshal = { diff --git a/src/pipewire/device.h b/src/pipewire/device.h index bff40530f..fbaa4465d 100644 --- a/src/pipewire/device.h +++ b/src/pipewire/device.h @@ -5,6 +5,7 @@ #ifndef PIPEWIRE_DEVICE_H #define PIPEWIRE_DEVICE_H +#include #include #include @@ -92,11 +93,12 @@ struct pw_device_events { #define PW_DEVICE_METHOD_SUBSCRIBE_PARAMS 1 #define PW_DEVICE_METHOD_ENUM_PARAMS 2 #define PW_DEVICE_METHOD_SET_PARAM 3 -#define PW_DEVICE_METHOD_NUM 4 +#define PW_DEVICE_METHOD_SEND_COMMAND 4 +#define PW_DEVICE_METHOD_NUM 5 /** Device methods */ struct pw_device_methods { -#define PW_VERSION_DEVICE_METHODS 0 +#define PW_VERSION_DEVICE_METHODS 1 uint32_t version; int (*add_listener) (void *object, @@ -143,6 +145,15 @@ struct pw_device_methods { */ int (*set_param) (void *object, uint32_t id, uint32_t flags, const struct spa_pod *param); + + /** + * Send a command to the device + * + * \param command the command to send + * + * This requires X and W permissions on the device. + */ + int (*send_command) (void *object, const struct spa_command *command); }; /** \copydoc pw_device_methods.add_listener @@ -183,6 +194,13 @@ PW_API_DEVICE_IMPL int pw_device_set_param(struct pw_device *object, uint32_t id pw_device, (struct spa_interface*)object, set_param, 0, id, flags, param); } +/** \copydoc pw_device_methods.send_command + * \sa pw_device_methods.send_command */ +PW_API_DEVICE_IMPL int pw_device_send_command(struct pw_device *object, const struct spa_command *command) +{ + return spa_api_method_r(int, -ENOTSUP, + pw_device, (struct spa_interface*)object, send_command, 0, command); +} /** * \} diff --git a/src/pipewire/impl-device.c b/src/pipewire/impl-device.c index 4dd7bfa00..33e41731f 100644 --- a/src/pipewire/impl-device.c +++ b/src/pipewire/impl-device.c @@ -533,11 +533,30 @@ static int device_set_param(void *object, uint32_t id, uint32_t flags, return res; } +static int device_send_command(void *object, const struct spa_command *command) +{ + struct resource_data *data = object; + struct pw_impl_device *device = data->device; + uint32_t id = SPA_DEVICE_COMMAND_ID(command); + int res; + + pw_log_debug("%p: got command %d (%s)", device, id, + spa_debug_type_find_name(spa_type_device_command_id, id)); + + switch (id) { + default: + res = spa_device_send_command(device->device, command); + break; + } + return res; +} + static const struct pw_device_methods device_methods = { PW_VERSION_DEVICE_METHODS, .subscribe_params = device_subscribe_params, .enum_params = device_enum_params, - .set_param = device_set_param + .set_param = device_set_param, + .send_command = device_send_command, }; static int diff --git a/src/tests/test-interfaces.c b/src/tests/test-interfaces.c index 3b3f47ab9..8569b198a 100644 --- a/src/tests/test-interfaces.c +++ b/src/tests/test-interfaces.c @@ -153,6 +153,7 @@ static void test_device_abi(void) const struct spa_pod *filter); int (*set_param) (void *object, uint32_t id, uint32_t flags, const struct spa_pod *param); + int (*send_command) (void *object, const struct spa_command *command); } methods = { PW_VERSION_DEVICE_METHODS, }; struct { uint32_t version; @@ -167,7 +168,8 @@ static void test_device_abi(void) TEST_FUNC(m, methods, subscribe_params); TEST_FUNC(m, methods, enum_params); TEST_FUNC(m, methods, set_param); - spa_assert_se(PW_VERSION_DEVICE_METHODS == 0); + TEST_FUNC(m, methods, send_command); + spa_assert_se(PW_VERSION_DEVICE_METHODS == 1); spa_assert_se(sizeof(m) == sizeof(methods)); TEST_FUNC(e, events, version); diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c index a5723fce7..160e116c5 100644 --- a/src/tools/pw-cli.c +++ b/src/tools/pw-cli.c @@ -2059,6 +2059,8 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) { ti = spa_debug_type_find_short(spa_type_node_command_id, a[1]); + } else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device)) { + ti = spa_debug_type_find_short(spa_type_device_command_id, a[1]); } else { *error = spa_aprintf("send-command not implemented on object %d type:%s", atoi(a[0]), global->type); @@ -2066,7 +2068,7 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char } if (ti == NULL) { - *error = spa_aprintf("%s: unknown node command type: %s", cmd, a[1]); + *error = spa_aprintf("%s: unknown command type: %s", cmd, a[1]); return false; } if ((res = spa_json_to_pod(&b.b, 0, ti, a[2], strlen(a[2]))) < 0) { @@ -2079,7 +2081,12 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char } spa_debug_pod(0, NULL, pod); - pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod); + if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) { + pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod); + } else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device)) { + pw_device_send_command((struct pw_device*)global->proxy, (struct spa_command*)pod); + } + return true; } From 400645566d4a020f98c8bbb7bc639cc9ef10d0ad Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 3 Mar 2026 15:44:22 -0800 Subject: [PATCH 5/8] spa: device: Commands for (external) volume control --- spa/include/spa/monitor/device.h | 8 ++++++++ spa/include/spa/monitor/type-info.h | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h index 57c84a963..ca17221b5 100644 --- a/spa/include/spa/monitor/device.h +++ b/spa/include/spa/monitor/device.h @@ -133,6 +133,9 @@ struct spa_device_events { /* object id of SPA_TYPE_COMMAND_Device */ enum spa_device_commands { SPA_DEVICE_COMMAND_User, + SPA_DEVICE_COMMAND_VolumeControl, /**< Volume control for devices which do not + * support standard volume control via route + * props */ }; #define SPA_DEVICE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Device) @@ -146,6 +149,11 @@ enum spa_command_device { SPA_COMMAND_DEVICE_START_User = 0x1000, SPA_COMMAND_DEVICE_extra, /** extra info (String) */ + SPA_COMMAND_DEVICE_START_VolumeControl = 0x2000, + SPA_COMMAND_DEVICE_volumeUp, /** Send a volume up command. route id (Id) */ + SPA_COMMAND_DEVICE_volumeDown, /** Send a volume down command. route id (Id) */ + SPA_COMMAND_DEVICE_muteToggle, /** Send a mute toggle command. route id (Id) */ + SPA_COMMAND_DEVICE_START_CUSTOM = 0x1000000, }; diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h index 24ca15653..80f0d2796 100644 --- a/spa/include/spa/monitor/type-info.h +++ b/spa/include/spa/monitor/type-info.h @@ -43,6 +43,7 @@ static const struct spa_type_info spa_type_device_event[] = { static const struct spa_type_info spa_type_device_command_id[] = { { SPA_DEVICE_COMMAND_User, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "User", NULL }, + { SPA_DEVICE_COMMAND_VolumeControl, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "VolumeControl", NULL }, { 0, 0, NULL, NULL }, }; @@ -51,6 +52,10 @@ static const struct spa_type_info spa_type_device_command[] = { { SPA_COMMAND_DEVICE_extra, SPA_TYPE_String, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "extra", NULL }, + { SPA_COMMAND_DEVICE_volumeUp, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "volumeUp", NULL }, + { SPA_COMMAND_DEVICE_volumeDown, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "volumeDown", NULL }, + { SPA_COMMAND_DEVICE_muteToggle, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "muteToggle", NULL }, + { 0, 0, NULL, NULL }, }; From 06e2b1d26fbbcae9b88d88c950cfa6944035614c Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Tue, 3 Mar 2026 15:44:44 -0800 Subject: [PATCH 6/8] spa: alsa: Support volume control commands for external volume control --- spa/plugins/alsa/acp/acp.c | 35 +++++++++++++++++ spa/plugins/alsa/acp/acp.h | 2 + spa/plugins/alsa/alsa-acp-device.c | 62 ++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 5c9bd119c..2441a01c2 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -2486,6 +2486,24 @@ int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t return 0; } +int acp_device_set_volume_updown(struct acp_device *dev, bool up) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + pa_card *impl = d->card; + + if (!impl->card.ext_volume.initialized) + return -EINVAL; + + if (!d->active_port) + return -EINVAL; + + pa_log_info("Volume %s", up ? "up" : "down"); + + /* TODO: use step size from capabilities */ + return spa_acp_ext_volume_write_volume_relative(&impl->card.ext_volume, impl->name, + d->active_port->name, d->active_port->port.index, up ? 1.0 : -1.0); +} + static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume) { uint32_t i; @@ -2539,6 +2557,23 @@ int acp_device_get_mute(struct acp_device *dev, bool *mute) return 0; } +int acp_device_toggle_mute(struct acp_device *dev) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + pa_card *impl = d->card; + + if (!impl->card.ext_volume.initialized) + return -EINVAL; + + if (!d->active_port) + return -EINVAL; + + pa_log_info("Toggle mute"); + + return spa_acp_ext_volume_write_mute_toggle(&impl->card.ext_volume, impl->name, + d->active_port->name, d->active_port->port.index); +} + void acp_set_log_func(acp_log_func func, void *data) { _acp_log_func = func; diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h index 7d7e90f21..6c4a8fa13 100644 --- a/spa/plugins/alsa/acp/acp.h +++ b/spa/plugins/alsa/acp/acp.h @@ -310,9 +310,11 @@ uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *nam int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags); int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume); +int acp_device_set_volume_updown(struct acp_device *dev, bool up); int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume); int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume); int acp_device_set_mute(struct acp_device *dev, bool mute); +int acp_device_toggle_mute(struct acp_device *dev); int acp_device_get_mute(struct acp_device *dev, bool *mute); typedef void (*acp_log_func) (void *data, diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c index 2ce5404a3..edb1137f3 100644 --- a/spa/plugins/alsa/alsa-acp-device.c +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -1114,12 +1114,74 @@ static int impl_set_param(void *object, return 0; } +static int impl_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct acp_card *card = this->card; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_DEVICE_COMMAND_ID(command)) { + case SPA_DEVICE_COMMAND_VolumeControl: + { + const struct spa_pod_prop *prop; + + /* We support some volume control commands for external volumes */ + if (!card->ext_volume.initialized) + return -ENOTSUP; + + SPA_POD_OBJECT_FOREACH(&command->body, prop) { + struct acp_port *port; + struct acp_device *dev = NULL; + uint32_t id, i; + + if (spa_pod_get_id(&prop->value, &id) < 0 || id > card->n_ports) + return -EINVAL; + + port = card->ports[id]; + + for (i = 0; i < port->n_devices; i++) { + if (port->devices[i]->flags & ACP_DEVICE_ACTIVE) { + dev = port->devices[i]; + break; + } + } + + if (dev == NULL) + return -EINVAL; + + switch (prop->key) { + case SPA_COMMAND_DEVICE_volumeUp: + acp_device_set_volume_updown(dev, true); + break; + case SPA_COMMAND_DEVICE_volumeDown: + acp_device_set_volume_updown(dev, false); + break; + case SPA_COMMAND_DEVICE_muteToggle: + acp_device_toggle_mute(dev); + break; + default: + return -ENOTSUP; + } + } + + break; + } + default: + return -ENOTSUP; + } + + return 0; +} + static const struct spa_device_methods impl_device = { SPA_VERSION_DEVICE_METHODS, .add_listener = impl_add_listener, .sync = impl_sync, .enum_params = impl_enum_params, .set_param = impl_set_param, + .send_command = impl_send_command, }; static void card_props_changed(void *data) From 2d420aa5feb7318f95c74f1edd87367a6df0aef3 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Wed, 4 Mar 2026 18:52:06 -0800 Subject: [PATCH 7/8] pulse-server: Surface volume control flags as properties We could udpate the libpulse protocol to add more flag types, but this seems simpler than requiring a protocol and ABI update. --- .../module-protocol-pulse/pulse-server.c | 24 +++++++++++++++++-- src/modules/module-protocol-pulse/volume.c | 24 +++++++++++++++++++ src/modules/module-protocol-pulse/volume.h | 9 +++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 1fa6318b8..b66ed086e 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -3729,7 +3729,7 @@ static int fill_card_info(struct client *client, struct message *m, } static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sink_props, - const struct pw_manager_object *card) + const struct device_info *dev_info, const struct pw_manager_object *card) { struct pw_device_info *card_info = card ? card->info : NULL; spa_autoptr(pw_properties) props = NULL; @@ -3743,6 +3743,26 @@ static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sin sink_props = &props->dict; } + if (dev_info->volume_info.flags & VOLUME_CONTROL_MASK) { + props = pw_properties_new_dict(sink_props); + if (props == NULL) + return -ENOMEM; + +#define SET_PROP(k, f) pw_properties_set(props, k, dev_info->volume_info.flags & (f) ? "true" : "false") + + SET_PROP("device.volume.read-volume", VOLUME_READ); + SET_PROP("device.volume.write-volume-value", VOLUME_WRITE); + SET_PROP("device.volume.write-volume-updown", VOLUME_UPDOWN); + SET_PROP("device.volume.read-mute", VOLUME_READ_MUTE); + SET_PROP("device.volume.write-mute-value", VOLUME_WRITE_MUTE); + SET_PROP("device.volume.write-mute-toggle", VOLUME_TOGGLE_MUTE); + SET_PROP("device.volume.read-balance", VOLUME_READ_BALANCE); + SET_PROP("device.volume.write-balance", VOLUME_WRITE_BALANCE); + + pw_properties_add(props, card_info->props); + sink_props = &props->dict; + } + message_put(m, TAG_PROPLIST, sink_props, TAG_INVALID); return 0; @@ -3841,7 +3861,7 @@ static int fill_sink_info(struct client *client, struct message *m, if (client->version >= 13) { int res; - if ((res = fill_sink_info_proplist(m, info->props, card)) < 0) + if ((res = fill_sink_info_proplist(m, info->props, &dev_info, card)) < 0) return res; message_put(m, TAG_USEC, 0LL, /* requested latency */ diff --git a/src/modules/module-protocol-pulse/volume.c b/src/modules/module-protocol-pulse/volume.c index c124b227f..3e3a247a4 100644 --- a/src/modules/module-protocol-pulse/volume.c +++ b/src/modules/module-protocol-pulse/volume.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -89,6 +90,29 @@ int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bo info->map.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, info->map.map, SPA_N_ELEMENTS(info->map.map)); break; + case SPA_PROP_volumeControlFlags: + { + uint32_t flags; + if (spa_pod_get_id(&prop->value, &flags) >= 0) { + SPA_FLAG_UPDATE(info->flags, VOLUME_READ, + flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME); + SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE, + flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE); + SPA_FLAG_UPDATE(info->flags, VOLUME_UPDOWN, + flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN); + SPA_FLAG_UPDATE(info->flags, VOLUME_READ_MUTE, + flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE); + SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE_MUTE, + flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE); + SPA_FLAG_UPDATE(info->flags, VOLUME_TOGGLE_MUTE, + flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE); + SPA_FLAG_UPDATE(info->flags, VOLUME_READ_BALANCE, + flags & SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE); + SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE_BALANCE, + flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE); + } + break; + } default: break; } diff --git a/src/modules/module-protocol-pulse/volume.h b/src/modules/module-protocol-pulse/volume.h index dcb198803..4cfb6d927 100644 --- a/src/modules/module-protocol-pulse/volume.h +++ b/src/modules/module-protocol-pulse/volume.h @@ -32,6 +32,15 @@ struct volume_info { uint32_t steps; #define VOLUME_HW_VOLUME (1<<0) #define VOLUME_HW_MUTE (1<<1) +#define VOLUME_READ (1<<2) +#define VOLUME_WRITE (1<<3) +#define VOLUME_UPDOWN (1<<4) +#define VOLUME_READ_MUTE (1<<5) +#define VOLUME_WRITE_MUTE (1<<6) +#define VOLUME_TOGGLE_MUTE (1<<7) +#define VOLUME_READ_BALANCE (1<<8) +#define VOLUME_WRITE_BALANCE (1<<9) +#define VOLUME_CONTROL_MASK (0x01FC) uint32_t flags; }; From e1cad35b314104ceb4354a6a1236893463de94e0 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Wed, 4 Mar 2026 19:15:42 -0800 Subject: [PATCH 8/8] pulse-server: Expose commands for sink volume control Useful for things like HDMI CEC. --- .../module-protocol-pulse/message-handler.c | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index 0cb63c902..6f5f430cd 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -23,8 +24,62 @@ #include "log.h" #include "manager.h" #include "module.h" +#include "pipewire/properties.h" #include "message-handler.h" +static int sink_object_message_handler(struct client *client, struct pw_manager_object *o, const char *message, const char *params, FILE *response) +{ + struct device_info dev_info; + struct pw_manager_object *card = NULL; + char buf[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); + struct spa_pod_frame f[1]; + struct spa_command *command; + uint32_t card_id, command_id; + + pw_log_debug(": sink %p object message:'%s' params:'%s'", o, message, params); + + get_device_info(o, &dev_info, PW_DIRECTION_OUTPUT, false); + + if (spa_streq(message, "volume-up")) + command_id = SPA_COMMAND_DEVICE_volumeUp; + else if (spa_streq(message, "volume-down")) + command_id = SPA_COMMAND_DEVICE_volumeDown; + else if (spa_streq(message, "mute-toggle")) + command_id = SPA_COMMAND_DEVICE_muteToggle; + else { + fprintf(response, "Unknown message %s (must be volume-up, volume-down or mute-toggle)", message); + return -EINVAL; + } + + card_id = pw_properties_get_uint32(o->props, PW_KEY_DEVICE_ID, SPA_ID_INVALID); + + spa_list_for_each(o, &client->manager->object_list, link) { + if (o->id == card_id) { + card = o; + break; + } + } + + if (!card) { + pw_log_error("Could not find card"); + return -EINVAL; + } + + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_COMMAND_Device, SPA_DEVICE_COMMAND_VolumeControl); + spa_pod_builder_add(&b, + command_id, SPA_POD_Id(dev_info.active_port), 0); + + command = (struct spa_command *)spa_pod_builder_pop(&b, &f[0]); + + pw_device_send_command((struct pw_device *)card->proxy, command); + + fprintf(response, "true"); + + return 0; +} + static int bluez_card_object_message_handler(struct client *client, struct pw_manager_object *o, const char *message, const char *params, FILE *response) { struct transport_codec_info codecs[64]; @@ -289,4 +344,14 @@ void register_object_message_handlers(struct pw_manager_object *o) } return; } + + if (pw_manager_object_is_sink(o)) { + str = pw_properties_get(o->props, PW_KEY_NODE_NAME); + if (str) { + free(o->message_object_path); + o->message_object_path = spa_aprintf("/sink/%s/volume-control", str); + o->message_handler = sink_object_message_handler; + } + return; + } }