mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-03-25 09:05:57 -04:00
Currently enabled at device creation and delegated to an external entity via a varlink protocol.
414 lines
12 KiB
C
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(¶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;
|
|
}
|