diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h index cc51f9ef1..ca17221b5 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,35 @@ 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, + 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) +#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_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, +}; /** * spa_device_methods: @@ -134,7 +163,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 +255,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 +302,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 1658594b4..6a10c1819 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 @@ -36,6 +37,27 @@ 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 }, + { SPA_DEVICE_COMMAND_VolumeControl, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "VolumeControl", 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 }, + + { 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 }, +}; + /** * \} */ 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, diff --git a/spa/include/spa/support/dbus.h b/spa/include/spa/support/dbus.h index d781bc1ba..f468b9bec 100644 --- a/spa/include/spa/support/dbus.h +++ b/spa/include/spa/support/dbus.h @@ -122,10 +122,9 @@ struct spa_dbus_methods { * * \param dbus the dbus manager * \param type the bus type to wrap - * \param error location for the DBusError * \return a new dbus connection wrapper or NULL on error */ - struct spa_dbus_connection * (*get_connection) (void *object, + struct spa_dbus_connection * (*get_connection) (void *dbus, enum spa_dbus_type type); }; diff --git a/spa/include/spa/support/varlink.h b/spa/include/spa/support/varlink.h new file mode 100644 index 000000000..52d93218c --- /dev/null +++ b/spa/include/spa/support/varlink.h @@ -0,0 +1,163 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VARLINK_H +#define SPA_VARLINK_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SPA_API_VARLINK + #ifdef SPA_API_IMPL + #define SPA_API_VARLINK SPA_API_IMPL + #else + #define SPA_API_VARLINK static inline + #endif +#endif + +/** \defgroup spa_varlink Varlink + * Varlink communication + */ + +/** + * \addtogroup spa_varlink + * \{ + */ + +#define SPA_TYPE_INTERFACE_Varlink SPA_TYPE_INFO_INTERFACE_BASE "Varlink" + +typedef void (*spa_varlink_reply_func_t) (void *data, const char *params, + const char *error, size_t len, bool continues); + +struct spa_varlink_client_events { +#define SPA_VERSION_VARLINK_CLIENT_EVENTS 0 + uint32_t version; + + /** The client was destroyed. */ + void (*destroy) (void *data); + + /** The client was disconnected. */ + void (*disconnect) (void *data); +}; + +struct spa_varlink_client { +#define SPA_VERSION_VARLINK_CLIENT 0 + uint32_t version; + + /** Add a listener fo events */ + void (*add_listener) (void *object, struct spa_hook *listener, + const struct spa_varlink_client_events *events, + void *data); + + /** Call a single- or no-reply method on a varlink client. + * + * \param method Fully qualified (`interface.Method`) method name to + * call. + * \param params Method parameters as a string (must be a valid JSON + * object). + * \param oneway Signal that the server should not send a reply. + * \param more Expect multiple replies from the server for this method + * call. + * \param cb Callback to invok when a reply to this call arrives. + * \param userdata Userdata to supply to the callback. + * \return 0 on success, or a negative error code on failure. + */ + int (*call) (void *object, const char *method, const char *params, + bool oneway, bool more, spa_varlink_reply_func_t cb, + void *userdata); + + /** Call a single-reply method and block until a reply is received. + * + * \param method Fully qualified (`interface.Method`) method name to + * call. + * \param params Method parameters as a string (must be a valid JSON + * object). + * \param reply The reply string. The caller is responsible for freeing + * this data with `free()` + * \return 0 on success, or a negative error code on failure. + */ + int (*call_sync) (void *object, const char *method, const char *params, + char **reply); + + /** Destroy a varlink client. + * + * \param client The client to destroy + */ + void (*destroy) (void *object); +}; + +/** \copydoc spa_varlink_client_methods.add_listener + * \sa spa_varlink_client_methods.add_listener */ +SPA_API_VARLINK void +spa_varlink_client_add_listener(struct spa_varlink_client *client, + struct spa_hook *listener, + const struct spa_varlink_client_events *events, + void *data) +{ + spa_api_func_v(client, add_listener, 0, listener, events, data); +} + +/** \copydoc spa_varlink_client_methods.call + * \sa spa_varlink_client_methods.call */ +SPA_API_VARLINK int +spa_varlink_client_call(struct spa_varlink_client *client, const char *method, + const char *params, bool oneway, bool more, + spa_varlink_reply_func_t cb, void *userdata) +{ + return spa_api_func_r(int, -EINVAL, client, call, 0, method, params, + oneway, more, cb, userdata); +} + +/** \copydoc spa_varlink_client_methods.call_sync + * \sa spa_varlink_client_methods.call_sync */ +SPA_API_VARLINK int +spa_varlink_client_call_sync(struct spa_varlink_client *client, const char *method, + const char *params, char **reply) +{ + return spa_api_func_r(int, -EINVAL, client, call_sync, 0, method, + params, reply); +} + +/** \copydoc spa_varlink_client_methods.destroy + * \sa spa_varlink_client_methods.destroy */ +SPA_API_VARLINK void +spa_varlink_client_destroy(struct spa_varlink_client *client) +{ + spa_api_func_v(client, destroy, 0); +} + +#define SPA_VERSION_VARLINK 0 +struct spa_varlink { struct spa_interface iface; }; + +struct spa_varlink_methods { +#define SPA_VERSION_VARLINK_METHODS 0 + uint32_t version; + + /** + * Connect to a varlink service. + * + * \param path Path to connect to, for example `unix:/path/to/socket` + * \return A `spa_varlink_client` on success, NULL on failure. + */ + struct spa_varlink_client * (*connect) (void *object, const char *path); +}; + +/** \copydoc spa_varlink_methods.connect + * \sa spa_varlink_methods.connect */ +SPA_API_VARLINK void * +spa_varlink_connect(struct spa_varlink *varlink, const char *path) +{ + return spa_api_method_r(struct spa_varlink_client *, NULL, spa_varlink, + &varlink->iface, connect, 0, path); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VARLINK_H */ diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h index a3f519011..2ed079583 100644 --- a/spa/include/spa/utils/names.h +++ b/spa/include/spa/utils/names.h @@ -25,6 +25,7 @@ extern "C" { #define SPA_NAME_SUPPORT_LOOP "support.loop" /**< A Loop/LoopControl/LoopUtils * interface */ #define SPA_NAME_SUPPORT_SYSTEM "support.system" /**< A System interface */ +#define SPA_NAME_SUPPORT_VARLINK "support.varlink" /**< A Varlink interface */ #define SPA_NAME_SUPPORT_NODE_DRIVER "support.node.driver" /**< A dummy driver node */ 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 }, diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 8f5ea18c2..a41c216ac 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -5,18 +5,23 @@ #include "acp.h" #include "alsa-mixer.h" #include "alsa-ucm.h" +#include "ext-volume.h" #include #include +#include #include #include #include +#include +#include int _acp_log_level = 1; acp_log_func _acp_log_func; void *_acp_log_data; struct spa_i18n *acp_i18n; +struct spa_varlink *acp_varlink; #define DEFAULT_CHANNELS 255u #define DEFAULT_RATE 48000u @@ -1245,6 +1250,30 @@ static void init_eld_ctls(pa_card *impl) } } +static void init_ext_volume(pa_card *impl) +{ + struct acp_card *card = &impl->card; + int res; + + if (!impl->ext_volume_path) + return; + + if (!acp_varlink) { + pa_log_error("External volume control requires support.varlink = true"); + return; + } + + res = spa_acp_ext_volume_init(&card->ext_volume, acp_varlink, impl->ext_volume_path, impl->name, NULL); + if (res < 0) { + goto error; + } + + /* FIXME: Set up monitor callback */ + +error: + return; +} + uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name) { uint32_t i; @@ -1356,21 +1385,47 @@ static int read_volume(pa_alsa_device *dev) return 0; } - if (!dev->mixer_handle) + if (impl->card.ext_volume.client && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME)) { + /* Externally managed volume */ + pa_cvolume ext_vol; + + if ((res = spa_acp_ext_volume_read_volume(&impl->card.ext_volume, impl->name, + dev->active_port->name, &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; @@ -1393,12 +1448,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; @@ -1407,52 +1460,99 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) return; } - if (!dev->mixer_handle) - return; + if (impl->card.ext_volume.client) { + 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->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, 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; + } } } @@ -1470,22 +1570,31 @@ static int read_mute(pa_alsa_device *dev) return 0; } - if (!dev->mixer_handle) + if (impl->card.ext_volume.client && + (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE)) { + /* Externally managed mute state */ + if (spa_acp_ext_volume_read_mute(&impl->card.ext_volume, impl->name, + dev->active_port->name, &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; @@ -1500,7 +1609,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) @@ -1510,27 +1619,52 @@ static void set_mute(pa_alsa_device *dev, bool mute) return; } - if (!dev->mixer_handle) - return; + if (impl->card.ext_volume.client) { + 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, 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) < 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); } } @@ -1538,7 +1672,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_path) { + 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, " @@ -1586,7 +1728,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_path) { + 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; @@ -1596,7 +1746,7 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) dev->read_mute = read_mute; dev->set_mute = set_mute; pa_log_info("Using hardware %smute control.", - dev->mixer_path->has_volume_mute ? "volume-" : ""); + dev->mixer_path->has_volume_mute ? "volume-" : NULL); dev->device.flags |= ACP_DEVICE_HW_MUTE; } } @@ -1655,6 +1805,12 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB) return 0; } + if (impl->ext_volume_path) { + /* 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? */ @@ -1677,6 +1833,7 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB) else pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev); } + return 0; } @@ -1972,6 +2129,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_path = strdup(s); } #if SND_LIB_VERSION < 0x10207 @@ -2057,6 +2216,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) return &impl->card; error: pa_alsa_refcnt_dec(); + free(impl->ext_volume_path); free(impl); errno = -res; return NULL; @@ -2089,6 +2249,10 @@ void acp_card_destroy(struct acp_card *card) pa_alsa_ucm_free(&impl->ucm); pa_proplist_free(impl->proplist); pa_alsa_refcnt_dec(); + if (impl->ext_volume_path) { + spa_acp_ext_volume_destroy(&impl->card.ext_volume); + free(impl->ext_volume_path); + } free(impl); } @@ -2322,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.client) + 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, up ? 1.0 : -1.0); +} + static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume) { uint32_t i; @@ -2357,6 +2539,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; } @@ -2374,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.client) + 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); +} + 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 5fb5b96f7..491c96991 100644 --- a/spa/plugins/alsa/acp/acp.h +++ b/spa/plugins/alsa/acp/acp.h @@ -11,6 +11,11 @@ #include #include +#include +#include + +#include "ext-volume.h" + #ifdef __cplusplus extern "C" { #else @@ -239,6 +244,8 @@ 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 +284,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); @@ -299,9 +308,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/acp/card.h b/spa/plugins/alsa/acp/card.h index f75222dbf..399e4f665 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; + + char *ext_volume_path; }; 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..a4e3638b7 --- /dev/null +++ b/spa/plugins/alsa/acp/ext-volume.c @@ -0,0 +1,414 @@ +/* External Volume Control */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include "alsa-mixer.h" +#include "channelmap.h" +#include "volume.h" +#include "ext-volume.h" + +#define VOLUME_CONTROL_BASE "org.pipewire.ExternalVolume." +#define METHOD_GET_CAPABILITIES VOLUME_CONTROL_BASE "GetCapabilities" +#define METHOD_READ_VOLUME VOLUME_CONTROL_BASE "ReadVolume" +#define METHOD_WRITE_VOLUME_ABSOLUTE VOLUME_CONTROL_BASE "WriteVolumeAbsolute" +#define METHOD_WRITE_VOLUME_RELATIVE VOLUME_CONTROL_BASE "WriteVolumeRelative" +#define METHOD_READ_MUTE VOLUME_CONTROL_BASE "ReadMute" +#define METHOD_WRITE_MUTE_VALUE VOLUME_CONTROL_BASE "WriteMuteValue" +#define METHOD_WRITE_MUTE_TOGGLE VOLUME_CONTROL_BASE "WriteMuteToggle" + +static void client_disconnect(void *userdata) +{ + /* TODO: reconnect? bail? */ +} + +static void client_destroy(void *userdata) +{ + pa_log_debug("varlink client destroyed\n"); +} + +static struct spa_varlink_client_events client_events = { + .disconnect = client_disconnect, + .destroy = client_destroy, +}; + +static void check_reply(void *data, const char *params, const char *error, + size_t len, bool continues) +{ + const char *method = (const char *) data; + + if (error != NULL) + pa_log_error("Error calling %s: %s", method, error); +} + +int +spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, struct spa_varlink *varlink, + const char *path, const char *device, const char *route) +{ + struct spa_json_builder params_json; + struct spa_json reply_json; + spa_autofree char *params = NULL; + spa_autofree char *reply = NULL; + char key[64]; + const char *str; + size_t len; + int res; + + pa_log_info("Connecting to external volume control at '%s'", path); + + ext_volume->client = spa_varlink_connect(varlink, path); + + if (!ext_volume->client) { + pa_log_error("Could not connect to volume control service at: %s", path); + return -EINVAL; + } + + spa_varlink_client_add_listener(ext_volume->client, &ext_volume->listener, &client_events, ext_volume); + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call_sync(ext_volume->client, METHOD_GET_CAPABILITIES, params, &reply); + + if (res < 0) { + pa_log_error("Failed to query volume control capabilities: %s", spa_strerror(res)); + return res; + } + + spa_json_begin_object(&reply_json, reply, res); + + while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) { + struct spa_json sub; + char key2[64]; + const char *val; + int len; + bool value; + + if (spa_streq(key, "error")) { + pa_log_error("Error reading volume control capabilities: %s", str); + res = -EINVAL; + goto done; + } + + if (!spa_streq(key, "parameters")) + continue; + + len = spa_json_container_len(&reply_json, str, res); + spa_json_begin_object(&sub, str, len); + + while ((len = spa_json_object_next(&sub, key2, sizeof(key2), &val)) > 0) { + if (spa_streq(key2, "readVolume") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME; + if (spa_streq(key2, "writeVolumeAbsolute") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE; + if (spa_streq(key2, "writeVolumeRelative") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN; + if (spa_streq(key2, "readMute") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_MUTE; + if (spa_streq(key2, "writeMuteValue") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE; + if (spa_streq(key2, "writeMuteToggle") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE; + if (spa_streq(key2, "readBalance") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE; + if (spa_streq(key2, "writeBalance") && spa_json_parse_bool(val, len, &value) && value) + ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE; + } + + break; + } + + res = 0; +done: + return res; +} + +void +spa_acp_ext_volume_destroy(struct spa_acp_ext_volume *ext_volume) +{ + if (ext_volume->client) { + spa_varlink_client_destroy(ext_volume->client); + ext_volume->client = NULL; + } +} + +int +spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, pa_cvolume *cvol) +{ + struct spa_json_builder params_json; + struct spa_json reply_json; + spa_autofree char *params = NULL; + spa_autofree char *reply = NULL; + char key[64]; + const char *str; + size_t len; + int res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call_sync(ext_volume->client, METHOD_READ_VOLUME, params, &reply); + + if (res < 0) { + pa_log_error("Failed to query volume : %s", spa_strerror(res)); + return res; + } + + spa_json_begin_object(&reply_json, reply, res); + + while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) { + struct spa_json sub = {0,}, volume_json; + float v = 0; + + if (spa_streq(key, "error")) { + pa_log_error("Error reading volume: %s", str); + res = -EINVAL; + goto done; + } + + if (!spa_streq(key, "parameters")) + continue; + + if (spa_json_enter_object(&reply_json, &sub) < 0) { + pa_log_error("Could not read volume parameters"); + res = -EINVAL; + goto done; + } + + res = spa_json_object_find(&sub, "volume", &str); + + if (res < 0) { + pa_log_error("Could not read volume"); + res = -EINVAL; + goto done; + } + + spa_json_enter_array(&sub, &volume_json); + + cvol->channels = 0; + while (spa_json_get_float(&volume_json, &v) && cvol->channels < PA_CHANNELS_MAX) { + cvol->values[cvol->channels++] = lrint(v * PA_VOLUME_NORM); + } + + break; + } + + res = 0; +done: + return res; +} + +int +spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, pa_cvolume *cvol) +{ + struct spa_json_builder params_json; + spa_autofree char *params = NULL; + size_t len; + int i, res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + + spa_json_builder_object_push(¶ms_json, "volume", "["); + + /* FIXME: scale to the range from capabilities */ + if (ext_volume->flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE) { + /* Write all channels */ + for (i = 0; i < (int)cvol->channels; i++) + spa_json_builder_array_double(¶ms_json, + (double) cvol->values[i] / PA_VOLUME_NORM); + } else { + /* Single volume */ + spa_json_builder_array_double(¶ms_json, + (double) pa_cvolume_max(cvol) / PA_VOLUME_NORM); + } + + spa_json_builder_pop(¶ms_json, "]"); + spa_json_builder_pop(¶ms_json, "}"); + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_VOLUME_ABSOLUTE, + params, false, false, check_reply, METHOD_WRITE_VOLUME_ABSOLUTE); + + if (res < 0) { + pa_log_error("Failed to write volume: %s", spa_strerror(res)); + return res; + } + + return res; +} + +int +spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, float step) +{ + struct spa_json_builder params_json; + spa_autofree char *params = NULL; + size_t len; + int res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + spa_json_builder_object_double(¶ms_json, "step", step); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_VOLUME_RELATIVE, + params, false, false, check_reply, METHOD_WRITE_VOLUME_RELATIVE); + + if (res < 0) { + pa_log_error("Failed to write volume: %s", spa_strerror(res)); + return res; + } + + return res; +} + +int +spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, bool *mute) +{ + struct spa_json_builder params_json; + struct spa_json reply_json; + spa_autofree char *params = NULL; + spa_autofree char *reply = NULL; + char key[64]; + const char *str; + size_t len; + int res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call_sync(ext_volume->client, METHOD_READ_MUTE, params, &reply); + + if (res < 0) { + pa_log_error("Failed to query mute state: %s", spa_strerror(res)); + return res; + } + + spa_json_begin_object(&reply_json, reply, res); + + while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) { + struct spa_json sub = { 0, }; + + if (spa_streq(key, "error")) { + pa_log_error("Error reading mute state: %s", str); + res = -EINVAL; + goto done; + } + + if (!spa_streq(key, "parameters")) + continue; + + if (spa_json_enter_object(&reply_json, &sub) < 0) { + pa_log_error("Could not read mute parameters"); + res = -EINVAL; + goto done; + } + + res = spa_json_object_find(&sub, "mute", &str); + + if (res < 0 || spa_json_get_bool(&reply_json, mute) < 0) { + pa_log_error("Could not read mute"); + res = -EINVAL; + goto done; + } + + break; + } + + res = 0; +done: + return res; +} + +int +spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, bool mute) +{ + struct spa_json_builder params_json; + spa_autofree char *params = NULL; + size_t len; + int res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + spa_json_builder_object_bool(¶ms_json, "mute", mute); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_MUTE_VALUE, + params, false, false, check_reply, METHOD_WRITE_MUTE_VALUE); + + if (res < 0) { + pa_log_error("Failed to write mute state: %s", spa_strerror(res)); + return res; + } + + return res; +} + +int +spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route) +{ + struct spa_json_builder params_json; + spa_autofree char *params = NULL; + size_t len; + int res; + + spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0); + + spa_json_builder_object_push(¶ms_json, NULL, "{"); + spa_json_builder_object_string(¶ms_json, "device", device ? device : ""); + spa_json_builder_object_string(¶ms_json, "route", route ? route : ""); + spa_json_builder_pop(¶ms_json, "}"); + + spa_json_builder_close(¶ms_json); + + res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_MUTE_TOGGLE, + params, false, false, check_reply, METHOD_WRITE_MUTE_TOGGLE); + + if (res < 0) { + pa_log_error("Failed to write mute toggle: %s", spa_strerror(res)); + return res; + } + + 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..6879ad30d --- /dev/null +++ b/spa/plugins/alsa/acp/ext-volume.h @@ -0,0 +1,47 @@ +/* External Volume Control */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#ifndef ACP_EXT_VOLUME_H +#define ACP_EXT_VOLUME_H + +#include + +#include +#include + +typedef struct pa_cvolume pa_cvolume; + +struct acp_card; + +struct spa_acp_ext_volume { + struct spa_varlink_client *client; + struct spa_hook listener; + + enum spa_audio_volume_control_flags flags; +}; + +int spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, struct spa_varlink *varlink, + const char *path, const char *device, const char *route); + +void spa_acp_ext_volume_destroy(struct spa_acp_ext_volume *ext_volume); + +int spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, pa_cvolume *cvol); + +int spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, pa_cvolume *cvol); + +int spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, float step); + +int spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, bool *mute); + +int spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route, bool mute); + +int spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume, + const char *device, const char *route); + +#endif /* ACP_EXT_VOLUME_H */ diff --git a/spa/plugins/alsa/acp/meson.build b/spa/plugins/alsa/acp/meson.build index 0ec97e2b4..c907e6fbc 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 = [ @@ -17,6 +18,6 @@ acp_lib = static_library( acp_sources, c_args : acp_c_args, include_directories : [configinc, includes_inc ], - dependencies : [ spa_dep, alsa_dep, mathlib, ] + dependencies : [ spa_dep, alsa_dep, mathlib ] ) acp_dep = declare_dependency(link_with: acp_lib) diff --git a/spa/plugins/alsa/acp/org.pipewire.ExternalVolume.varlink b/spa/plugins/alsa/acp/org.pipewire.ExternalVolume.varlink new file mode 100644 index 000000000..526595bbb --- /dev/null +++ b/spa/plugins/alsa/acp/org.pipewire.ExternalVolume.varlink @@ -0,0 +1,89 @@ +# Allows an external service to provide volume control for a PipeWire device. +interface org.pipewire.ExternalVolume + +# Describes what kind of volume control operations are supported for a given +# device. +type Capabilities ( + # Whether the current volume value can be read + readVolume: bool, + + # Whether volume values are reported per-channel + readBalance: bool, + + # The range of valid volume values and the granularity of steps + volumeRange: ( + min: float, + max: float, + step: float + ), + + # Whether the volume can be set as an absolute value + writeVolumeAbsolute: bool, + + # Whether volume adjustments can be made relative to the current volume + writeVolumeRelative: bool, + + # The size of relative volume adjustments, if known + writeVolumeRelativeStep: (min: float, max: float), + + # Whether per-channel volumes can be written + writeBalance: bool, + + # Whether the current mute state can be read + readMute: bool, + + # Whether the current mute state can be set + writeMuteValue: bool, + + # Whether the current mute state can be toggled + writeMuteToggle: bool, + + # The known set of routes for the device + routes: []string +) + +# Query volume control capabilities for the given device. +method GetCapabilities(device: string) -> (capabilities: Capabilities) + +# Query the volume for the given device route. If the volume can be read, the +# returned value will be an array of floats (if per-channel volumes are not +# supported, the array will have one float value). +method ReadVolume(device: string, route: string) -> (volume: []float) + +# Query the mute state for the given device route. +method ReadMute(device: string, route: string) -> (mute: bool) + +# Monitor changes to volume or mute state. Volume changes will be signalled by +# a non-empty volume array (with a single value if per-channel volumes are not +# supported). Mute state changes will be signalled by a non-null mute value. +method Monitor(device: string) -> ( + route: string, + volume: []float, + mute: ?bool +) + +# Set the volume of the given device route. If supported, the provided value +# can be an array of per-channel float values. If per-channel volumes are not +# supported, the array should consist of a single value. +method WriteVolumeAbsolute( + device: string, + route: string, + volume: []float +) -> () + +# Increase or decrease the volume of the device route by the given amount. +method WriteVolumeRelative( + device: string, + route: string, + step: float +) -> () + +# Set the mute state for the given device route. +method WriteMuteValue( + device: string, + route: string, + mute: bool +) -> () + +# Toggle the mute state for the given device route. +method WriteMuteToggle(device: string, route: string) -> () diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c index 44342a7a3..38daf6e4b 100644 --- a/spa/plugins/alsa/alsa-acp-device.c +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "acp/acp.h" extern struct spa_i18n *acp_i18n; +extern struct spa_varlink *acp_varlink; #define MAX_POLL 16 #define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS @@ -63,6 +65,7 @@ struct impl { struct spa_log *log; struct spa_loop *loop; + struct spa_varlink *varlink; uint32_t info_all; struct spa_device_info info; @@ -490,6 +493,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); @@ -885,12 +893,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.client == NULL) + 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) @@ -1152,6 +1222,7 @@ impl_init(const struct spa_handle_factory *factory, this->loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); acp_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N); + acp_varlink = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Varlink); if (this->loop == NULL) { spa_log_error(this->log, "a Loop interface is needed"); return -EINVAL; diff --git a/spa/plugins/support/meson.build b/spa/plugins/support/meson.build index 678845774..3adc9e3d6 100644 --- a/spa/plugins/support/meson.build +++ b/spa/plugins/support/meson.build @@ -25,6 +25,17 @@ spa_support_lib = shared_library('spa-support', install_dir : spa_plugindir / 'support') spa_support_dep = declare_dependency(link_with: spa_support_lib) +spa_varlink_sources = [ + 'varlink.c' +] + +spa_varlink_lib = shared_library('spa-varlink', + spa_varlink_sources, + dependencies : [ spa_dep ], + install : true, + install_dir : spa_plugindir / 'support') +spa_varlink_dep = declare_dependency(link_with: spa_varlink_lib) + if get_option('evl').allowed() evl_inc = include_directories('/usr/include') evl_lib = cc.find_library('evl', diff --git a/spa/plugins/support/varlink.c b/spa/plugins/support/varlink.c new file mode 100644 index 000000000..aa13f16b2 --- /dev/null +++ b/spa/plugins/support/varlink.c @@ -0,0 +1,623 @@ +/* Spa Varlink plugin */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.varlink"); + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + struct spa_handle handle; + struct spa_varlink varlink; + + struct spa_log *log; + struct spa_system *system; + struct spa_loop_utils *loop_utils; + + struct spa_list clients; +}; + +enum spa_varlink_transport { + SPA_VARLINK_TRANSPORT_UNIX, +}; + +#define BUFFER_SIZE 16384 + +struct client { + struct spa_list link; + struct spa_varlink_client client; + + struct impl *impl; /* The owning impl */ + int fd; /* The socket fd */ + struct spa_source *source; /* I/O source if we have a call_more() call */ + bool reply_pending; /* A call was made for which a reply is pending */ + bool more_pending; /* A call with more=true was made, and a reply is pending */ + + spa_varlink_reply_func_t cb; /* Callback for the next reply we receive */ + void *cb_userdata; + + struct spa_hook_list listeners; + + size_t pos; + size_t avail; + char buf[BUFFER_SIZE]; +}; + +static const char *varlink_parse_path(const char *path, + enum spa_varlink_transport *transport) +{ + if (spa_strstartswith(path, "unix:")) { + *transport = SPA_VARLINK_TRANSPORT_UNIX; + return &path[5]; + } + + return NULL; +} + +static void +client_add_listener(void *object, struct spa_hook *listener, + const struct spa_varlink_client_events *events, void *data) +{ + struct client *this = SPA_CONTAINER_OF(object, struct client, client); + spa_hook_list_append(&this->listeners, listener, events, data); +} + +static char * +build_call(const char *method, const char *params, bool oneway, bool more, + size_t *len) +{ + struct spa_json_builder b; + char *json; + + spa_json_builder_memstream(&b, &json, len, 0); + + /* Top-level object */ + spa_json_builder_object_push(&b, NULL, "{"); + + spa_json_builder_object_string(&b, "method", method); + spa_json_builder_object_value(&b, true, "parameters", params); + spa_json_builder_object_bool(&b, "oneway", oneway); + spa_json_builder_object_bool(&b, "more", more); + + spa_json_builder_pop(&b, "}"); + + spa_json_builder_close(&b); + + return json; +} + +static int +client_call(void *object, const char *method, const char *params, + bool oneway, bool more, spa_varlink_reply_func_t cb, + void *userdata) +{ + struct client *this = SPA_CONTAINER_OF(object, struct client, client); + struct impl *impl = this->impl; + char *buf; + size_t len, pos = 0; + int res; + + if (this->reply_pending) { + res = -EINVAL; + goto done; + } + + buf = build_call(method, params, oneway, more, &len); + + spa_log_trace(impl->log, "sending message: %s", buf); + + /* We write the whole string including the NULL terminator */ + do { + res = spa_system_write(this->impl->system, this->fd, &buf[pos], len - pos + 1); + pos += res; + } while ((res > 0 && len >= pos) || res == -EAGAIN || res == -EWOULDBLOCK); + + free(buf); + + if (res < 0) { + spa_log_error(impl->log, "Error writing message: %s", + spa_strerror(res)); + goto done; + } + + if (!oneway) { + this->reply_pending = true; + this->cb = cb; + this->cb_userdata = userdata; + } + if (more) + this->more_pending = true; + + res = 0; + +done: + return res; +} + +static int +client_call_sync(void *object, const char *method, const char *params, + char **reply) +{ + struct client *this = SPA_CONTAINER_OF(object, struct client, client); + struct impl *impl = this->impl; + char *buf; + size_t len, pos = 0; + int res = 0; + + if (this->reply_pending || this->more_pending || + (this->pos != 0 && this->avail != 0)) { + spa_log_error(impl->log, "Invalid state for sync call"); + res = -EINVAL; + } + + if (res < 0) + return res; + + spa_loop_utils_update_io(impl->loop_utils, this->source, + this->source->mask & ~SPA_IO_IN); + + fcntl(this->fd, F_SETFL, fcntl(this->fd, F_GETFL) & ~O_NONBLOCK); + + buf = build_call(method, params, false, false, &len); + + spa_log_trace(impl->log, "sending message: %s", buf); + /* We write the whole string including the NULL terminator */ + do { + res = spa_system_write(this->impl->system, this->fd, &buf[pos], len - pos + 1); + pos += res; + } while (res > 0 && len >= pos); + + free(buf); + + if (res < 0) { + spa_log_error(impl->log, "Error writing message: %s", + spa_strerror(res)); + goto done; + } + + while (true) { + size_t eom; + + res = spa_system_read(impl->system, this->fd, &this->buf[this->avail], + BUFFER_SIZE - this->avail); + if (res <= 0) { + if (res == -EINTR) + continue; + + if (res == -EAGAIN || res == -EWOULDBLOCK) + continue; + + spa_log_error(impl->log, "Error reading from socket: %s", + spa_strerror(res)); + + goto done; + } + + spa_log_debug(impl->log, "Got %d bytes (avail: %zu)", res, this->avail); + this->avail += res; + + for (eom = 0; eom < this->avail; eom++) { + if (this->buf[eom] == '\0') + break; + } + + /* We need more data */ + if (eom == this->avail) + continue; + + if (eom != this->avail - 1) + spa_log_warn(impl->log, "Received more than one reply"); + + spa_log_debug(impl->log, "Consuming %zu bytes", eom + 1); + + res = eom; + *reply = strndup(this->buf, res + 1); + + this->pos = this->avail = 0; + break; + } + +done: + fcntl(this->fd, F_SETFL, fcntl(this->fd, F_GETFL) | O_NONBLOCK); + + spa_loop_utils_update_io(impl->loop_utils, this->source, + this->source->mask | SPA_IO_IN); + + return res; +} + +static void +client_disconnect(struct client *this) +{ + if (this->source) { + spa_loop_utils_destroy_source(this->impl->loop_utils, this->source); + this->source = NULL; + + spa_system_close(this->impl->system, this->fd); + + spa_hook_list_call(&this->listeners, struct spa_varlink_client_events, + disconnect, SPA_VERSION_VARLINK_CLIENT_EVENTS); + } +} + +static void do_destroy(struct client *client) +{ + client_disconnect(client); + + spa_hook_list_call(&client->listeners, struct spa_varlink_client_events, + destroy, SPA_VERSION_VARLINK_CLIENT_EVENTS); + + spa_hook_list_clean(&client->listeners); +} + +static void +client_destroy(void *object) +{ + struct client *this = SPA_CONTAINER_OF(object, struct client, client); + do_destroy(this); +} + +static void +process_message(struct client *this, const char *msg, size_t len) +{ + struct impl *impl = this->impl; + struct spa_json json; + char key[64]; + const char *val, *error = NULL, *params = NULL; + int res; + bool continues = false; + + spa_log_trace(impl->log, "got message: %.*s", (int)len, msg); + + spa_json_begin_object(&json, msg, len); + + while ((res = spa_json_object_next(&json, key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "error")) { + len = spa_json_container_len(&json, val, res); + error = val; + break; + } + + if (spa_streq(key, "continues")) { + res = spa_json_get_bool(&json, &continues); + if (res < 0) + goto error; + } + + if (spa_streq(key, "parameters")) { + len = spa_json_container_len(&json, val, res); + params = val; + break; + } + } + + if (this->cb) + this->cb(this->cb_userdata, params, error, len, continues); + + if (!this->more_pending || !continues) { + /* We didn't ask for more, but server might send */ + if (continues) + spa_log_warn(impl->log, "Unexpected continues"); + + this->reply_pending = false; + this->more_pending = false; + this->cb = NULL; + this->cb_userdata = NULL; + } + + return; + +error: + spa_log_error(impl->log, "Could not parse: %.*s", (int)len, msg); +} + +static void +client_on_reply(void *userdata, int fd, uint32_t mask) +{ + struct client *this = (struct client *)userdata; + struct impl *impl = this->impl; + int res; + + if (mask & SPA_IO_HUP) { + spa_log_info(impl->log, "Got hangup event"); + client_disconnect(this); + return; + } + + if (mask & SPA_IO_ERR) { + spa_log_info(impl->log, "Got error event"); + client_disconnect(this); + return; + } + + spa_log_debug(impl->log, "Ready to read"); + + /* SPA_IO_IN */ + while (this->pos <= this->avail) { + size_t eom; + + res = spa_system_read(impl->system, fd, &this->buf[this->avail], + BUFFER_SIZE - this->avail); + if (res <= 0) { + if (res == -EINTR) + continue; + + if (res == 0 || res == -EAGAIN || res == -EWOULDBLOCK) + return; + + spa_log_error(impl->log, "Error reading from socket: %s", + spa_strerror(res)); + client_disconnect(this); + + return; + } + + spa_log_debug(impl->log, "Got %d bytes (avail: %zu)", res, this->avail); + this->avail += res; + + for (eom = this->pos; eom < this->avail; eom++) { + if (this->buf[eom] == '\0') + break; + } + + if (eom == this->avail) { + if (eom == BUFFER_SIZE) { + spa_log_error(impl->log, "Message larger than available buffer"); + client_disconnect(this); + } + + spa_log_debug(impl->log, "Need more data"); + return; + } + + process_message(this, &this->buf[this->pos], eom - this->pos); + + spa_log_debug(impl->log, "Consumed %zu bytes", eom + 1 - this->pos); + + /* Consume message so far and NUL terminator */ + this->pos = eom + 1; + + if (this->pos == this->avail) { + /* All data has been consumed, reset */ + this->pos = this->avail = 0; + break; + } + + } +} + +static const struct spa_varlink_client impl_client = { + SPA_VERSION_VARLINK_CLIENT, + .add_listener = client_add_listener, + .call = client_call, + .call_sync = client_call_sync, + .destroy = client_destroy, +}; + +static struct spa_varlink_client * +impl_connect(void *object, const char *path) +{ + struct impl *this = object; + struct client *client; + const char *socket_path; + struct sockaddr_un sa; + enum spa_varlink_transport transport; + int res = 0; + + client = calloc(1, sizeof(struct client)); + if (client == NULL) + return NULL; + + socket_path = varlink_parse_path(path, &transport); + /* We only support UNIX sockets for now */ + if (socket_path == NULL || transport != SPA_VARLINK_TRANSPORT_UNIX) { + spa_log_error(this->log, "Could not connect to socket path '%s'", + path); + goto error; + } + + client->fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (client->fd < 0) { + spa_log_error(this->log, "Could not create socket %s", + spa_strerror(res)); + goto error; + } + + sa.sun_family = AF_UNIX; + res = snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", socket_path); + if (res < 0 || res > (int) sizeof(sa.sun_path)) { + spa_log_error(this->log, "Socket path too long: %s", socket_path); + goto error; + } + + res = connect(client->fd, (struct sockaddr *)&sa, sizeof(sa)); + if (res < 0) { + spa_log_error(this->log, "Could not open socket %s: %s", + socket_path, spa_strerror(res)); + goto error; + } + + client->client = impl_client; + client->impl = this; + client->pos = 0; + client->avail = 0; + client->reply_pending = false; + client->more_pending = false; + client->cb = NULL; + client->cb_userdata = NULL; + spa_hook_list_init(&client->listeners); + + client->source = spa_loop_utils_add_io(this->loop_utils, client->fd, + SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP, false, + client_on_reply, client); + if (client->source == NULL) { + spa_log_error(this->log, "Could not create source"); + goto error; + } + + spa_list_append(&this->clients, &client->link); + + spa_log_debug(this->log, "new client %p", client); + + return &client->client; + +error: + free(client); + errno = -res; + return NULL; +} + +static const struct spa_varlink_methods impl_varlink = { + SPA_VERSION_VARLINK_METHODS, + .connect = impl_connect, +}; + +static int +impl_get_interface(struct spa_handle *handle, const char *type, + void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Varlink)) + *interface = &this->varlink; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + struct client *client; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + spa_list_consume(client, &this->clients, link) + do_destroy(client); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *)handle; + spa_list_init(&this->clients); + + this->varlink.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Varlink, + SPA_VERSION_VARLINK, + &impl_varlink, this); + + this->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + if (this->loop_utils == NULL) { + spa_log_error(this->log, "Loop utils is required for varlink support"); + return -EINVAL; + } + + spa_log_debug(this->log, "%p: varlink iface initialised", this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Varlink,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + + (*index)++; + + return 0; +} + +static const struct spa_handle_factory varlink_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_VARLINK, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &varlink_factory; + break; + default: + return 0; + } + + (*index)++; + + return 1; +} diff --git a/spa/tests/meson.build b/spa/tests/meson.build index 81635dea9..79f1dcaa1 100644 --- a/spa/tests/meson.build +++ b/spa/tests/meson.build @@ -27,6 +27,17 @@ if find.found() endforeach endif +utils = [ + ['varlink-call', []], +] + +foreach a : utils + executable('spa-' + a[0], a[0] + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib ] + a[1], + include_directories : [configinc], + ) +endforeach + benchmark_apps = [ ['stress-ringbuffer', []], ['benchmark-pod', []], diff --git a/spa/tests/varlink-call.c b/spa/tests/varlink-call.c new file mode 100644 index 000000000..e422c723e --- /dev/null +++ b/spa/tests/varlink-call.c @@ -0,0 +1,260 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct data { + const char *plugin_dir; + + struct spa_support support[16]; + uint32_t n_support; + struct spa_log *log; + struct spa_loop *loop; + struct spa_loop_control *loop_control; + struct spa_loop_utils *loop_utils; + struct spa_system *system; + + struct spa_varlink *varlink; + struct spa_varlink_client *client; + struct spa_hook listener; + + bool running; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const + char *lib, const char *name, struct spa_dict *info) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + char *path; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) + return -ENOMEM; + + hnd = dlopen(path, RTLD_NOW); + free(path); + + if (hnd == NULL) { + printf("can't load %s: %s\n", lib, dlerror()); + return -ENOENT; + } + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + res = -ENOENT; + goto exit_cleanup; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (factory->version < 1) + continue; + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + info, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + goto exit_cleanup; + } + return 0; + } + return -EBADF; + +exit_cleanup: + dlclose(hnd); + return res; +} + +static int init(struct data *data) +{ + struct spa_handle *handle; + struct spa_dict_item items[1]; + struct spa_dict info; + const char *str; + void *iface; + int res; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data->plugin_dir = str; + + /* enable the debug messages in SPA */ + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "true"); + info = SPA_DICT_ARRAY(items); + if ((res = load_handle(data, &handle, "support/libspa-support.so", + SPA_NAME_SUPPORT_LOG, &info)) < 0) + return res; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Log, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + + data->log = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log); + + if ((str = getenv("SPA_DEBUG"))) + data->log->level = atoi(str); + + /* load and set support system */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM, NULL)) < 0) + return res; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data->system = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data->system); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data->system); + + /* load and set support loop and loop control */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP, NULL)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data->loop); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data->loop); + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop_control = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopControl, data->loop_control); + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopUtils, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop_utils = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, data->loop_utils); + + /* load varlink */ + if ((res = load_handle(data, &handle, + "support/libspa-varlink.so", + SPA_NAME_SUPPORT_VARLINK, NULL)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Varlink, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->varlink = iface; + + return 0; +} + +static void +on_reply(void *userdata, const char *params, const char *error, size_t len, bool continues) +{ + struct data *data = userdata; + + if (params) { + printf("Got reply: params: %.*s, continues: %s\n", + (int) len, params, continues ? "true" : "false"); + } else { + printf("Got reply: error: %.*s\n", (int) len, error); + } + + data->running = false; +} + +static void on_disconnect(void *userdata) +{ + struct data *data = userdata; + printf("Disconnected\n"); + data->running = false; +} + +static void on_destroy(void *userdata) +{ + printf("Destroyed\n"); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + struct spa_varlink_client_events events = { + .disconnect = on_disconnect, + .destroy = on_destroy, + }; + int res; + bool sync = true; + + if (argc < 4 || argc > 5) { + printf("usage: %s \n", argv[0]); + return -1; + } + + res = init(&data); + if (res < 0) + return res; + + if (argc == 5) + sync = argv[4][0] == '1'; + + data.running = true; + + data.client = spa_varlink_connect(data.varlink, argv[1]); + if (data.client == NULL) { + printf("Could not connect to socket: %s\n", spa_strerror(errno)); + return -1; + } + + spa_varlink_client_add_listener(data.client, &data.listener, &events, &data); + + if (sync) { + char *reply; + res = spa_varlink_client_call_sync(data.client, argv[2], argv[3], &reply); + if (res < 0) { + printf("Call failed: %s\n", spa_strerror(res)); + return -1; + } + printf("Got reply (%d): %s\n", res, reply); + } else { + res = spa_varlink_client_call(data.client, argv[2], argv[3], false, false, on_reply, &data); + if (res < 0) { + printf("Could not connect to socket: %s\n", spa_strerror(res)); + return -1; + } + + while (data.running) { + spa_loop_control_iterate(data.loop_control, 1000); + } + } + + spa_varlink_client_destroy(data.client); + + return 0; +} 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/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index b44d4f422..80aefbeb1 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 = 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]; @@ -285,4 +340,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; + } } diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 24251942a..1bd2b0f60 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -3692,7 +3692,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; @@ -3706,6 +3706,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; @@ -3804,7 +3824,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 e53f967ec..a0658e495 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 @@ -86,6 +87,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; }; diff --git a/src/pipewire/context.c b/src/pipewire/context.c index e3f65ad41..3aa7a38b6 100644 --- a/src/pipewire/context.c +++ b/src/pipewire/context.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ struct data_loop { struct impl { struct pw_context this; struct spa_handle *dbus_handle; + struct spa_handle *varlink_handle; struct spa_plugin_loader plugin_loader; unsigned int recalc:1; unsigned int recalc_pending:1; @@ -402,7 +404,7 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop, struct impl *impl; struct pw_context *this; const char *lib, *str; - void *dbus_iface = NULL; + void *dbus_iface = NULL, *varlink_iface = NULL; uint32_t i, n_support, vm_type; struct pw_properties *conf; struct spa_cpu *cpu; @@ -554,6 +556,26 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop, this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DBus, dbus_iface); } } + /* Only load varlink support if explicitly requested */ + if ((str = pw_properties_get(properties, "support.varlink")) != NULL && + pw_properties_parse_bool(str)) { + lib = pw_properties_get(properties, PW_KEY_LIBRARY_NAME_VARLINK); + if (lib == NULL) + lib = "support/libspa-varlink"; + + impl->varlink_handle = pw_load_spa_handle(lib, + SPA_NAME_SUPPORT_VARLINK, NULL, + n_support, this->support); + + if (impl->varlink_handle == NULL) { + pw_log_warn("%p: can't load varlink library: %s", this, lib); + } else if ((res = spa_handle_get_interface(impl->varlink_handle, + SPA_TYPE_INTERFACE_Varlink, &varlink_iface)) < 0) { + pw_log_warn("%p: can't load varlink interface: %s", this, spa_strerror(res)); + } else { + this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Varlink, varlink_iface); + } + } this->n_support = n_support; spa_assert(n_support <= SPA_N_ELEMENTS(this->support)); 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 53b76b236..9a8b999a6 100644 --- a/src/pipewire/impl-device.c +++ b/src/pipewire/impl-device.c @@ -529,11 +529,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/pipewire/keys.h b/src/pipewire/keys.h index 08e6fb2df..1845c98c4 100644 --- a/src/pipewire/keys.h +++ b/src/pipewire/keys.h @@ -50,6 +50,7 @@ extern "C" { #define PW_KEY_LIBRARY_NAME_SYSTEM "library.name.system" /**< name of the system library to use */ #define PW_KEY_LIBRARY_NAME_LOOP "library.name.loop" /**< name of the loop library to use */ #define PW_KEY_LIBRARY_NAME_DBUS "library.name.dbus" /**< name of the dbus library to use */ +#define PW_KEY_LIBRARY_NAME_VARLINK "library.name.varlink" /**< name of the varlink library to use */ /** object properties */ #define PW_KEY_OBJECT_PATH "object.path" /**< unique path to construct the object */ 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 88356cb8f..ab1be3ea0 100644 --- a/src/tools/pw-cli.c +++ b/src/tools/pw-cli.c @@ -2047,6 +2047,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); @@ -2054,7 +2056,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) { @@ -2067,7 +2069,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; }