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:
@@ -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
|
||||
|
||||
@@ -44,6 +44,10 @@ public:
|
||||
bool connectPorts(uint32_t output_port_id, uint32_t input_port_id);
|
||||
bool disconnectPorts(uint32_t output_port_id, uint32_t input_port_id);
|
||||
|
||||
// Volume control
|
||||
bool setNodeVolume(uint32_t node_id, float volume);
|
||||
bool setNodeMute(uint32_t node_id, bool mute);
|
||||
|
||||
// PipeWire internal data (exposed for C callbacks)
|
||||
struct PwData {
|
||||
pw_thread_loop *loop;
|
||||
|
||||
@@ -63,10 +63,15 @@ struct Node {
|
||||
bool ready;
|
||||
bool changed;
|
||||
|
||||
// Volume
|
||||
float volume; // 0.0 - 1.0 (linear)
|
||||
bool mute;
|
||||
|
||||
std::vector<uint32_t> port_ids; // child port IDs
|
||||
|
||||
Node() : id(0), mode(PortMode::None), node_type(NodeType::None),
|
||||
mode2(PortMode::None), ready(false), changed(false) {}
|
||||
mode2(PortMode::None), ready(false), changed(false),
|
||||
volume(1.0f), mute(false) {}
|
||||
};
|
||||
|
||||
struct Link;
|
||||
|
||||
@@ -109,6 +109,8 @@ std::string WebServer::buildGraphJson() const {
|
||||
<< ",\"media_name\":\"" << escapeJson(n.media_name) << "\""
|
||||
<< ",\"mode\":\"" << portModeStr(n.mode) << "\""
|
||||
<< ",\"node_type\":\"" << nodeTypeStr(n.node_type) << "\""
|
||||
<< ",\"volume\":" << n.volume
|
||||
<< ",\"mute\":" << (n.mute ? "true" : "false")
|
||||
<< ",\"port_ids\":[";
|
||||
bool first_p = true;
|
||||
for (uint32_t pid : n.port_ids) {
|
||||
@@ -403,6 +405,47 @@ void WebServer::setupRoutes() {
|
||||
res.set_content(buf, "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Volume: POST /api/volume {"node_id": N, "volume": 0.0-1.0}
|
||||
m_http.Post("/api/volume", [this](const httplib::Request &req, httplib::Response &res) {
|
||||
uint32_t node_id = 0;
|
||||
float volume = 0;
|
||||
if (sscanf(req.body.c_str(),
|
||||
"{\"node_id\":%u,\"volume\":%f}", &node_id, &volume) == 2 ||
|
||||
sscanf(req.body.c_str(),
|
||||
"{\"node_id\":%u, \"volume\":%f}", &node_id, &volume) == 2)
|
||||
{
|
||||
if (volume < 0) volume = 0;
|
||||
if (volume > 1.5) volume = 1.5;
|
||||
bool ok = m_engine.setNodeVolume(node_id, volume);
|
||||
if (ok) broadcastGraph();
|
||||
res.set_content(ok ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
|
||||
} else {
|
||||
res.status = 400;
|
||||
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
||||
}
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Mute: POST /api/mute {"node_id": N, "mute": true/false}
|
||||
m_http.Post("/api/mute", [this](const httplib::Request &req, httplib::Response &res) {
|
||||
uint32_t node_id = 0;
|
||||
if (sscanf(req.body.c_str(), "{\"node_id\":%u", &node_id) == 1 ||
|
||||
sscanf(req.body.c_str(), "{\"node_id\": %u", &node_id) == 1)
|
||||
{
|
||||
bool mute = req.body.find("true") != std::string::npos;
|
||||
bool ok = m_engine.setNodeMute(node_id, mute);
|
||||
if (ok) broadcastGraph();
|
||||
res.set_content(ok ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
|
||||
} else {
|
||||
res.status = 400;
|
||||
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
||||
}
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
m_http.Options("/api/volume", cors_handler);
|
||||
m_http.Options("/api/mute", cors_handler);
|
||||
}
|
||||
|
||||
// end of web_server.cpp
|
||||
|
||||
Reference in New Issue
Block a user