Merge branch 'alsa-ext-vol' into 'master'

Add an external volume control mechanism

See merge request pipewire/pipewire!2722
This commit is contained in:
Arun Raghavan 2026-03-24 17:06:16 +00:00
commit cc53257729
33 changed files with 2361 additions and 103 deletions

View file

@ -5,18 +5,23 @@
#include "acp.h"
#include "alsa-mixer.h"
#include "alsa-ucm.h"
#include "ext-volume.h"
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/utils/json-builder.h>
#include <spa/utils/cleanup.h>
#include <spa/param/audio/iec958-types.h>
#include <spa/param/audio/raw.h>
#include <spa/param/audio/volume.h>
#include <spa/support/varlink.h>
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;

View file

@ -11,6 +11,11 @@
#include <poll.h>
#include <string.h>
#include <spa/param/audio/volume.h>
#include <spa/support/varlink.h>
#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,

View file

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

View file

@ -0,0 +1,414 @@
/* External Volume Control */
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
/* SPDX-License-Identifier: MIT */
#include <spa/support/varlink.h>
#include <spa/utils/cleanup.h>
#include <spa/utils/json.h>
#include <spa/utils/json-builder.h>
#include <spa/utils/result.h>
#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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_object_push(&params_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(&params_json,
(double) cvol->values[i] / PA_VOLUME_NORM);
} else {
/* Single volume */
spa_json_builder_array_double(&params_json,
(double) pa_cvolume_max(cvol) / PA_VOLUME_NORM);
}
spa_json_builder_pop(&params_json, "]");
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_object_double(&params_json, "step", step);
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_object_bool(&params_json, "mute", mute);
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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(&params_json, &params, &len, 0);
spa_json_builder_object_push(&params_json, NULL, "{");
spa_json_builder_object_string(&params_json, "device", device ? device : "");
spa_json_builder_object_string(&params_json, "route", route ? route : "");
spa_json_builder_pop(&params_json, "}");
spa_json_builder_close(&params_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;
}

View file

@ -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 <stdint.h>
#include <spa/param/audio/volume.h>
#include <spa/support/varlink.h>
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 */

View file

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

View file

@ -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) -> ()

View file

@ -21,6 +21,7 @@
#include <spa/support/loop.h>
#include <spa/support/plugin.h>
#include <spa/support/i18n.h>
#include <spa/support/varlink.h>
#include <spa/monitor/device.h>
#include <spa/monitor/utils.h>
#include <spa/monitor/event.h>
@ -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;

View file

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

View file

@ -0,0 +1,623 @@
/* Spa Varlink plugin */
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
/* SPDX-License-Identifier: MIT */
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <spa/support/log.h>
#include <spa/support/loop.h>
#include <spa/support/plugin.h>
#include <spa/support/varlink.h>
#include <spa/utils/defs.h>
#include <spa/utils/hook.h>
#include <spa/utils/json-builder.h>
#include <spa/utils/list.h>
#include <spa/utils/names.h>
#include <spa/utils/result.h>
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;
}