feat: built-in volume management

- Volume slider on every node (green bar, draggable)
- Mute toggle button (M/m) on every node
- Backend: read volume/mute from PipeWire node props
- Backend: POST /api/volume {node_id, volume} to set volume
- Backend: POST /api/mute {node_id, mute} to toggle mute
- Graph JSON includes volume and mute fields per node
- Slider supports drag-to-adjust with mouse
This commit is contained in:
joren
2026-03-29 23:21:39 +02:00
parent 8c6a1f44c1
commit 65db5daa7c
7 changed files with 206 additions and 5 deletions

View File

@@ -3,6 +3,10 @@
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
#include <spa/utils/list.h>
#include <spa/param/props.h>
#include <spa/pod/pod.h>
#include <spa/pod/builder.h>
#include <spa/pod/iter.h>
#include <cstring>
#include <algorithm>
@@ -65,16 +69,30 @@ static void on_node_info(void *data, const struct pw_node_info *info) {
info = pw_node_info_update((struct pw_node_info*)obj->info, info);
obj->info = (void*)info;
auto *nobj = static_cast<GraphEngine::NodeObj*>(obj);
if (info && (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)) {
auto *nobj = static_cast<GraphEngine::NodeObj*>(obj);
if (info->props) {
const char *media_name = spa_dict_lookup(info->props, PW_KEY_MEDIA_NAME);
if (media_name && strlen(media_name) > 0)
nobj->node.media_name = media_name;
// Read volume from props
const char *vol_str = spa_dict_lookup(info->props, "volume");
if (vol_str) {
nobj->node.volume = pw_properties_parse_float(vol_str);
}
// Read mute from props
const char *mute_str = spa_dict_lookup(info->props, "mute");
if (mute_str) {
nobj->node.mute = pw_properties_parse_bool(mute_str);
}
}
nobj->node.changed = true;
nobj->node.ready = true;
}
nobj->node.changed = true;
nobj->node.ready = true;
}
static const struct pw_node_events node_events = {
@@ -770,4 +788,65 @@ bool GraphEngine::disconnectPorts(uint32_t output_port_id, uint32_t input_port_i
return found;
}
// ============================================================================
// Volume control
// ============================================================================
bool GraphEngine::setNodeVolume(uint32_t node_id, float volume) {
if (!m_pw.loop) return false;
pw_thread_loop_lock(m_pw.loop);
NodeObj *nobj = findNode(node_id);
if (!nobj || !nobj->proxy) {
pw_thread_loop_unlock(m_pw.loop);
return false;
}
// Build Props param with volume
uint8_t buf[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
struct spa_pod *param = (struct spa_pod*)spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
SPA_PROP_volume, SPA_POD_Float(volume));
pw_node_set_param((pw_node*)nobj->proxy,
SPA_PARAM_Props, 0, param);
nobj->node.volume = volume;
nobj->node.changed = true;
pw_thread_loop_unlock(m_pw.loop);
return true;
}
bool GraphEngine::setNodeMute(uint32_t node_id, bool mute) {
if (!m_pw.loop) return false;
pw_thread_loop_lock(m_pw.loop);
NodeObj *nobj = findNode(node_id);
if (!nobj || !nobj->proxy) {
pw_thread_loop_unlock(m_pw.loop);
return false;
}
uint8_t buf[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
struct spa_pod *param = (struct spa_pod*)spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
SPA_PROP_mute, SPA_POD_Bool(mute));
pw_node_set_param((pw_node*)nobj->proxy,
SPA_PARAM_Props, 0, param);
nobj->node.mute = mute;
nobj->node.changed = true;
pw_thread_loop_unlock(m_pw.loop);
return true;
}
// end of graph_engine.cpp