Commit Graph

29 Commits

Author SHA1 Message Date
joren
da92a53c73 feat: graph region fix, toggle button, segmented VU meters
- Fix graph escaping toolbar: switch .wrap to flexbox + .toolbar to
  position:relative so the SVG canvas sits strictly below the toolbar
- Add "Graph" toggle button in toolbar to show/hide the canvas
- Replace thin 3px meter bar with 20-segment horizontal LED bar
  (10px tall, 1.5px gaps); green 0-75%, yellow 75-90%, red 90-100%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 23:17:32 +02:00
joren
cbc5083490 feat: real-time dB level meters per node
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>
2026-04-02 19:48:02 +02:00
joren
68307556e9 feat: save and restore virtual nodes (null-sinks, loopbacks) in profiles
- Add VirtualNodeDef type; track it in PatchbayState.virtual_nodes
- createNullSink/createLoopback now register the node in the global
  virtual_nodes registry on success
- Destroying a node via context menu removes it from the registry
- saveProfile snapshots virtual_nodes into the profile
- loadProfile recreates any missing virtual nodes before applying
  connections (waits 1.5s for graph to settle after creation)
- Backward compat: virtual_nodes defaults to [] for old save files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:27:59 +02:00
joren
db48781221 fix: optimistically update nodes store when loading profile volumes
setNodeVolume/setNodeMute only send API requests; the nodes store
wasn't updated until the backend broadcast a graph change, so sliders
showed stale values. Now the store is updated immediately before
firing the API calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 19:16:55 +02:00
joren
31d8191672 feat: regex hide rules (full-match) + right-click Hide button
- Hide rules now use anchored regex (^rule$) so plain text like
  "Speaker" matches exactly "Speaker", not "Gaming Speaker".
  To match a substring, use e.g. ".*Speaker.*". Rules that already
  start with ^ or end with $ are used as-is.
- Add "Hide" to node right-click context menu — adds the node's
  display name as a hide rule immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:44:00 +02:00
joren
a58df9cdaa fix: store volumes/mutes in profiles + fix hide rule matching
- Snapshot node volumes and mutes when saving a profile; restore
  them on load via setNodeVolume/setNodeMute for each matching node
- Fix hide rules to do exact case-insensitive match on display name
  (alias/nick) instead of regex substring match on PW name, so
  "Speaker" no longer accidentally hides "Gaming Speaker"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:39:28 +02:00
joren
cb87cd34ba feat: custom node display names (aliases)
Right-click any node → Rename to set a custom label. The alias is shown
in the node header and Properties dialog instead of the raw PipeWire name.
Leave the input blank (or press Reset) to revert to the PW name.

Aliases are stored in aliases: Record<string,string> inside the patchbay
state (keyed by PW node name, which is stable for hardware devices) and
persisted automatically with the rest of the patchbay config. Old save
files without an aliases key are handled gracefully via the ?? {} fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:08:16 +02:00
joren
b3c81623f1 fix: mute race condition + profile loading + add Update button
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>
2026-03-30 12:20:22 +02:00
joren
3609a50dd2 fix: volume slider click position using element getBoundingClientRect
- Rewrote applyVolumeAtMouse to use hitarea element's screen rect directly
- Works correctly regardless of zoom/pan level
- Made hitarea taller (18px) for easier grabbing
2026-03-30 01:22:30 +02:00
joren
7f214fc2d4 feat: Split toggle - show input/output as separate nodes
- Toggle button next to 'Merge Nodes' in toolbar
- When active: skips merge rules, each PipeWire node shown separately
- Green=output, red=input (no confusing duplex merging)
- Defaults to off (merged mode)
2026-03-30 01:17:10 +02:00
joren
c586f63e8a fix: merged duplex nodes get blue border instead of confusing green/red
- When merge rules combine input+output nodes, mode is set to 'duplex'
- Duplex nodes: blue border (#49a), blue-tinted bg, dark blue header
- Visually distinct from green=output, red=input nodes
2026-03-30 01:14:46 +02:00
joren
f76583a33e feat: buffer/latency control + non-selectable node text
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
2026-03-30 01:10:46 +02:00
joren
535ee058f2 fix: property inspector - always show channels/sample rate, show 'default' if unknown
- Removed duplicated/broken code from property parsing
- Always shows Channels and Sample Rate for audio nodes
- Shows 'default' if sample rate not available on node
- Latency shows ms calculation when sample rate known
- Added fallback: clock.rate, api.alsa.rate
2026-03-30 00:40:09 +02:00
joren
4cc6f554af fix: property inspector - read sample rate from node.latency, not node.rate
- node.rate was a PipeWire internal flag (always 1), not sample rate
- Now reads from node.latency (format: '256/48000') for quantum + rate
- Fallback to clock.rate if latency not available
- Reuses rate field for ALSA period-size
- Shows 'Latency: 256 samples @ 48000 Hz' and 'Period Size: 256'
2026-03-30 00:36:40 +02:00
joren
8c728dcd47 feat: node property inspector
- Right-click node -> Properties shows details dialog
- Shows: ID, name, class, volume, ports, sample rate, channels,
  format, quantum, rate, device, bus, API, priority
- Backend parses extra PipeWire node props (clock.rate, channels, etc.)
- Node type includes all new fields
- stop() already had the SSE cleanup fix
2026-03-30 00:33:41 +02:00
joren
baed725caf fix: network dialog closes immediately on Connect (don't await) 2026-03-30 00:21:47 +02:00
joren
6d3ad50764 feat: network config dialog for TCP tunnels
- TCP Tunnel Sink/Source now open a dialog asking for host:port
- TCP Network Server uses configurable port (shown in dropdown)
- Host defaults to 127.0.0.1, port defaults to 4713
- Config persists across clicks (netHost/netPort state)
2026-03-30 00:19:43 +02:00
joren
b3c7955340 fix: Add Device dropdown was immediately closed by window onclick handler 2026-03-30 00:13:46 +02:00
joren
5a52987fbd fix: Add Device button CSS was malformed (same line as another rule) 2026-03-30 00:05:59 +02:00
joren
444fc43c9c fix: toolbar buttons clickable, add node delete
Fixes:
- SVG mousedown handler now ignores clicks outside SVG (toolbar works)
- Right-click node -> Destroy Node to delete virtual devices
- POST /api/destroy-node {node_id} endpoint (uses pw-cli destroy)

Node context menu shows node name and destroy button.
2026-03-30 00:02:53 +02:00
joren
9dc685acda feat: pactl-based module loading, more device types
Backend:
- Switched null-sink and loopback to use pactl (works on all systems)
- Generic POST /api/load-module {module, args} for any pactl module
- Fixed SVG toolbar click issue (z-index layering)

Frontend:
- + Add Device dropdown now includes:
  - Null Sink (virtual audio output)
  - Loopback Device (paired input+output)
  - TCP Network Server (module-native-protocol-tcp)
  - TCP Tunnel Sink
  - TCP Tunnel Source
- Dropdown header and section separators
- Fixed canvas z-index so toolbar is clickable
2026-03-29 23:55:19 +02:00
joren
2879469d13 feat: create virtual devices (+ Add Device dropdown)
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.
2026-03-29 23:50:01 +02:00
joren
37e7834269 fix: click-to-jump on volume slider
Added transparent hitarea rect over the full slider area (14px tall).
Handle is pointer-events:none so clicks pass through to hitarea.
Clicking anywhere on the slider bar now jumps volume there.
2026-03-29 23:34:52 +02:00
joren
0b50400f69 fix: smaller volume handle, click-to-jump already works 2026-03-29 23:33:15 +02:00
joren
044c5e551c fix: volume slider and mute button race conditions
- Mute button click no longer triggers node drag (priority check in onMouseDown)
- Volume slider clamped to 0-100% in both display and API
- Added draggable circle handle on slider end
- Volume drag state tracked globally, not per-element
- Backend: fixed spa_pod_builder API usage (push_object/pop with frame)
- Volume calculated from SVG coordinates properly via svgPoint conversion
2026-03-29 23:29:40 +02:00
joren
65db5daa7c 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
2026-03-29 23:21:39 +02:00
joren
0acfa6ea73 feat: complete qpwgraph feature parity
Core Visual Routing:
- Dashed lines for inactive connections (unpinned shown dimmer)
- Color-coded streams (audio=green, midi=red, video=blue)
- Pin indicator (yellow dot) on pinned connections

Patchbay - Session Management:
- Save & Restore: named profiles stored on server (~/.config/pwweb/patchbay.json)
- Activated Mode: toggle to auto-restore connections when apps appear
- Exclusive Mode: enforce profile-only links, disconnect everything else
- Pin & Unpin: right-click wire -> Pin/Unpin, yellow dot on pinned wires
- Auto-Pin: automatically pin all new manual connections
- Auto-Disconnect: auto-sever pinned connections when patchbay deactivated
- Rule Management: dialog showing all profile rules with type badges

Graph Customization:
- Node Hiding: add regex patterns to hide nodes (e.g. Dummy|Freewheel)
- Node Merging: merge nodes sharing a common prefix into one block
- All rules persisted to server with the patchbay state

Dialogs: Hide Nodes, Merge Nodes, Profiles, Rules
2026-03-29 22:55:28 +02:00
joren
77e2fdca14 feat: full feature set - filters, context menu, patchbay, position persistence
Backend:
- GET/PUT /api/positions - server-side node position persistence (~/.config/pwweb/)
- GET/PUT /api/patchbay - save/load connection snapshots
- POST /api/patchbay/apply - apply saved connections

Frontend (custom SVG canvas):
- Right-click wire to disconnect (context menu)
- Port type filter toolbar (audio/midi/video/other toggles)
- Save/Load patchbay buttons
- Node positions persisted to localStorage + server
- Node border color by mode (green=output, red=input)
- Type indicator in node header [audio] [midi]
- Selected wire highlight (white, thicker)
- Select wire + Delete to disconnect
- Drag output port to input port to connect
- Pan (drag bg) and zoom (scroll wheel)
- Filtered ports shown as dim dots when hidden
2026-03-29 22:45:10 +02:00
joren
f8c57fbdd3 Initial commit: pwweb - PipeWire WebUI
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)
2026-03-29 22:40:07 +02:00