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