pipewire/spa/plugins/alsa/acp/ext-volume.c
Arun Raghavan 283c091b71 spa: alsa: Add a mechanism for external volume control
Currently enabled at device creation and delegated to an external entity
via a varlink protocol.
2026-03-09 16:52:27 -07:00

414 lines
12 KiB
C

/* 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;
}