Backend:
- One pw_stream (INPUT, F32) per audio node; for sinks use
stream.capture.sink=true to read the monitor port
- RT process callback computes instantaneous linear peak with no
allocations; stored in std::atomic<float>
- Meter streams are filtered from the registry so they never appear
in the graph or trigger recursive meter creation
- Meter streams are created on first node-ready event, destroyed on
node removal, and cleaned up on engine close
- GET /api/peaks → {node_id: linear_peak} (polled by frontend)
Frontend:
- peaks store polled at 100 ms via setInterval; starts/stops with
initGraph/destroyGraph
- Each node card grows 8 px and shows a 3 px meter bar at the bottom
(green below -12 dB, yellow -12 to -3 dB, red above -3 dB)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 - Mute unmute not sticking (G560 and others):
- Root cause: on_node_info was reading volume/mute from info->props which
contains static initial values only — NOT updated at runtime. When any
node info event fired, it overwrote the correct runtime state with stale
initial data, causing the unmute to revert on the next graph event.
- Fix: Subscribe nodes to SPA_PARAM_Props in addition to SPA_PARAM_Format.
Handle SPA_PARAM_Props in on_node_param to track volume (both SPA_PROP_volume
and SPA_PROP_channelVolumes averaged) and mute from the authoritative live
parameter stream. Remove stale volume/mute reads from on_node_info.
- Also fix mute detection in /api/mute: check "mute":true precisely instead
of searching for bare "true" anywhere in the body.
Bug 2 - Loading profiles does not work:
- loadProfile was only applying connections when already in "activated" mode.
Load now always applies the profile connections immediately.
Bug 3 - No option to update an existing profile:
- Add "Update" button in profile list that overwrites the profile with current
connections (calls saveProfile with the existing name).
- Clear the profile name input after "Save Current" succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Buffer Control:
- GET /api/quantum - reads current graph quantum via pw-metadata
- POST /api/quantum {quantum:N} - sets quantum via pw-metadata
- Toolbar dropdown with presets: 32, 64, 128, 256, 512, 1024, 2048, 4096
- Loads current quantum on startup
Text Selection Fix:
- Added user-select: none to wrap and canvas CSS
- Node text no longer gets selected when dragging
Backend:
- POST /api/create-null-sink {name} - loads null-sink module
- POST /api/create-loopback {name} - loads loopback module
- POST /api/unload-module {module_id} - unloads a module
- Fixed double-proxy-destroy crash in GraphEngine
- Graceful failure when module not available (no crash)
Frontend:
- + Add Device button in toolbar with dropdown menu
- Null Sink option (creates virtual audio output)
- Loopback Device option (creates paired input+output)
- Dropdown closes on outside click
Note: null-sink requires libpipewire-module-null-sink to be installed.
Loopback works on all PipeWire installations.
- 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
C++ backend with SSE streaming (reuses qpwgraph PipeWire callbacks).
Svelte frontend with custom SVG canvas for port-level connections.
Features:
- Live PipeWire graph via SSE
- Drag output->input port to connect
- Double-click or select+Delete to disconnect
- Node positions saved to localStorage
- Pan (drag bg) and zoom (scroll)
- Port type coloring (audio=green, midi=red, video=blue)