This commit is contained in:
Joey Yakimowich-Payne 2026-01-31 11:21:28 -07:00
commit 9d57ba5d25
9 changed files with 235 additions and 19 deletions

View file

@ -1,4 +1,4 @@
# Warppipe GUI Plan (Qt6 Node-Based Audio Router)
# Warp Pipe GUI Plan (Qt6 Node-Based Audio Router)
## Overview
A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library. Visualizes PipeWire audio nodes, ports, and links as draggable nodes with connection lines. Supports creating virtual sinks/sources via context menu and displays ephemeral sources with visual fade when inactive.
@ -395,7 +395,7 @@ Active vs Ghost:
option(BUILD_GUI "Build Qt6 GUI application" ON)
if(BUILD_GUI)
if(WARPPIPE_BUILD_GUI)
find_package(Qt6 6.2 REQUIRED COMPONENTS Core Widgets)
set(CMAKE_AUTOMOC ON)
@ -441,6 +441,7 @@ if(BUILD_GUI)
target_link_libraries(warppipe-gui-tests PRIVATE
warppipe
Qt6::Core
Qt6::Widgets
QtNodes
Catch2::Catch2WithMain
@ -464,9 +465,8 @@ endif()
### Dependencies
- Qt6 >= 6.2 (Core, Widgets)
- Qt6::Test (for GUI test target)
- QtNodes (nodeeditor) — fetched via CMake FetchContent
- Catch2 v3 — fetched via CMake FetchContent (shared with existing tests)
- Catch2 v3 — required for GUI tests (via `find_package`)
- warppipe library (existing)
---

View file

@ -1,4 +1,4 @@
# Warppipe Plan (C++ libpipewire library)
# Warp Pipe Plan (C++ libpipewire library)
- [x] Milestone 0 - Groundwork and constraints
- [x] Choose build system: CMake (confirmed). Define minimal library target and example app target.

View file

@ -1,6 +1,6 @@
# warppipe
A C++17 static library wrapping libpipewire for virtual audio node management, link routing, and per-app routing policy.
Warp Pipe is a C++20 static library wrapping libpipewire for virtual audio node management, link routing, and per-app routing policy.
## Features
@ -8,6 +8,8 @@ A C++17 static library wrapping libpipewire for virtual audio node management, l
- **Link management** — connect and disconnect ports by node+port name or ID, with passive and linger options
- **Per-app routing rules** — match applications by name, process binary, or media role and auto-route to target sinks
- **Persistence** — JSON config with atomic writes, auto-save on change, auto-load on startup
- **Volume control** — set per-node volume and mute state
- **Audio metering** — per-node and master peak meters
- **Metadata integration** — read and set default audio sink/source via PipeWire metadata
- **Policy-only mode** — observe and set metadata without creating links (avoids fighting WirePlumber)
- **Thread-safe** — dedicated PipeWire thread loop; all public methods callable from any thread
@ -18,20 +20,24 @@ Requirements:
- CMake 3.20+
- pkg-config
- libpipewire-0.3 development files
- C++17 compiler
- C++20 compiler
- Qt6 6.2+ (for the GUI target; disable with `-DWARPPIPE_BUILD_GUI=OFF`)
- Catch2 v3 (for tests; required when `-DWARPPIPE_BUILD_TESTS=ON`)
```sh
cmake -S . -B build
cmake --build build
```
Targets: `warppipe` (library), `warppipe_example`, `warppipe_cli`, `warppipe_tests`, `warppipe_perf`
Targets: `warppipe` (library), `warppipe_example`, `warppipe_cli`, `warppipe_tests`, `warppipe_perf`, `warppipe-gui`, `warppipe-gui-tests`
### Dependencies
- [libpipewire-0.3](https://pipewire.org/) — system package
- [nlohmann/json](https://github.com/nlohmann/json) — fetched automatically via CMake FetchContent
- [Catch2 v3](https://github.com/catchorg/Catch2) — fetched automatically via CMake FetchContent
- [nlohmann/json](https://github.com/nlohmann/json) — fetched automatically if not installed
- [Catch2 v3](https://github.com/catchorg/Catch2) — required for tests (via `find_package`)
- [Qt6](https://www.qt.io/) — required for `warppipe-gui` (default on)
- [QtNodes](https://github.com/paceholder/nodeeditor) — fetched automatically when GUI is enabled
## Quick Start
@ -77,11 +83,13 @@ warppipe_cli load-config <path>
warppipe_cli defaults
```
`link` uses node and port names (not IDs). `--passive` sets `PW_KEY_LINK_PASSIVE`, and `--linger` sets `PW_KEY_OBJECT_LINGER` so links persist after the client exits. `defaults` prints the current and configured default sink/source names.
`create-sink` and `create-source` block until interrupted with Ctrl-C.
## Configuration
Config is JSON. Set `ConnectionOptions::config_path` to enable auto-save/load, or use `SaveConfig`/`LoadConfig` manually.
Config is JSON. Set `ConnectionOptions::config_path` to enable auto-save/load, or use `SaveConfig`/`LoadConfig` manually. The config persists virtual nodes, routing rules, saved links, and per-node volume/mute state.
```json
{
@ -93,7 +101,9 @@ Config is JSON. Set `ConnectionOptions::config_path` to enable auto-save/load, o
"rate": 48000,
"channels": 2,
"loopback": false,
"target_node": ""
"target_node": "",
"volume": 1.0,
"mute": false
}
],
"route_rules": [
@ -105,6 +115,14 @@ Config is JSON. Set `ConnectionOptions::config_path` to enable auto-save/load, o
},
"target_node": "warppipe-gaming-sink"
}
],
"links": [
{
"out_node": "Firefox",
"out_port": "output_FL",
"in_node": "warppipe-gaming-sink",
"in_port": "input_FL"
}
]
}
```
@ -126,6 +144,7 @@ In this mode the policy engine still evaluates rules but does not create links.
- [API Reference](docs/api.md) — full API, threading model, error model, performance notes
- [Config Schema](docs/config-schema.md) — JSON configuration format and persistence behavior
- [GUI Usage](docs/gui-usage.md) — how to run the Qt GUI and capture screenshots
## Tests

View file

@ -1,8 +1,8 @@
# Warppipe API
# Warp Pipe API
## Overview
Warppipe is a C++17 library wrapping libpipewire for virtual audio node management, link routing, and per-app routing policy. The entire public API is in `<warppipe/warppipe.hpp>`.
Warp Pipe is a C++20 library wrapping libpipewire for virtual audio node management, link routing, and per-app routing policy. The entire public API is in `<warppipe/warppipe.hpp>`.
## Quick Start
@ -84,6 +84,19 @@ struct ConnectionOptions {
};
```
Notes:
- `ThreadingMode::kCallerThread` returns `StatusCode::kNotImplemented`.
- When `autoconnect` is false, operations return `kUnavailable` after a disconnect instead of reconnecting.
- `remote_name` connects to a non-default PipeWire instance.
## Lifecycle
```cpp
Status Shutdown();
```
`Shutdown()` stops the PipeWire thread loop and releases resources. It is called automatically by the destructor, but you can invoke it explicitly to tear down early.
### Policy-Only Mode
When `policy_only = true`, the policy engine observes the registry and matches rules, but does not create links. This avoids conflicts with WirePlumber or other session managers.
@ -117,8 +130,73 @@ Result<VirtualSource> CreateVirtualSource(std::string_view name, const VirtualNo
Status RemoveNode(NodeId node);
```
```cpp
struct VirtualNodeOptions {
AudioFormat format; // rate/channels
VirtualBehavior behavior; // kNull (default) or kLoopback
std::optional<std::string> target_node; // Required for kLoopback
std::optional<std::string> media_class_override; // Override PW_KEY_MEDIA_CLASS
std::string display_name; // PW_KEY_MEDIA_NAME / PW_KEY_NODE_DESCRIPTION
std::string group; // PW_KEY_NODE_GROUP
};
```
Virtual nodes are PipeWire streams with `PW_KEY_NODE_VIRTUAL = true`. They live as long as the `Client` (or until explicitly removed). Null behavior discards audio; loopback behavior forwards to a target node.
Behavior and validation:
- `format.rate` and `format.channels` must be non-zero (`kInvalidArgument` otherwise).
- `behavior = kLoopback` requires `target_node` and the target must exist (`kInvalidArgument`/`kNotFound`).
- `media_class_override` overrides the PipeWire media class (cannot be an empty string).
- `display_name` and `group` map to `PW_KEY_MEDIA_NAME` / `PW_KEY_NODE_DESCRIPTION` and `PW_KEY_NODE_GROUP`.
Example loopback node:
```cpp
warppipe::VirtualNodeOptions opts;
opts.behavior = warppipe::VirtualBehavior::kLoopback;
opts.target_node = "alsa_output.pci-0000_00_1f.3.analog-stereo";
auto sink = client->CreateVirtualSink("warppipe-loopback", opts);
```
## Volume Control
```cpp
Status SetNodeVolume(NodeId node, float volume, bool mute);
Result<VolumeState> GetNodeVolume(NodeId node) const;
struct VolumeState {
float volume; // 0.0 - 1.5
bool mute;
};
```
`SetNodeVolume` clamps `volume` to `[0.0, 1.5]` and stores the last known volume/mute state. `GetNodeVolume` returns the cached state (defaults to 1.0/false when unknown). Volume and mute are persisted in config files when saved.
## Audio Metering
```cpp
Status EnsureNodeMeter(NodeId node);
Status DisableNodeMeter(NodeId node);
Result<MeterState> NodeMeterPeak(NodeId node) const;
Result<MeterState> MeterPeak() const;
struct MeterState {
float peak_left;
float peak_right;
};
```
Call `EnsureNodeMeter` before querying `NodeMeterPeak`; otherwise you may receive `kNotFound` for unmetered nodes. `MeterPeak` returns master peak levels gathered by a monitor stream.
## Change Notifications
```cpp
using ChangeCallback = std::function<void()>;
void SetChangeCallback(ChangeCallback callback);
```
The callback fires when the registry cache changes (nodes/ports/links) and when volume/mute updates are observed. It is invoked on the PipeWire thread loop; do not call `Client` methods inside the callback to avoid deadlocks.
## Link Management
```cpp
@ -131,6 +209,8 @@ Status RemoveLink(LinkId link);
Links are created via `link-factory` (`pw_core_create_object`). Use `linger = true` so links persist after the creating client disconnects. Use `passive = true` for monitoring/side-chain links that don't affect the session manager's routing decisions.
`CreateLinkByName` matches by node and port name and returns `kNotFound` if no matching ports are found.
## Route Rules (Policy Engine)
```cpp
@ -148,6 +228,8 @@ Match criteria (all non-empty fields must match):
When a matching node appears, the engine waits for ports to register (via PipeWire sync), then creates links pairwise by port name order.
`AddRouteRule` returns `kInvalidArgument` if no match criteria are provided or if the target node name is empty.
Adding a rule also scans existing nodes for matches.
## Metadata
@ -173,7 +255,13 @@ JSON format. See `docs/config-schema.md` for the schema.
When `config_path` is set in `ConnectionOptions`:
- Config is loaded automatically after connection.
- Config is saved automatically after mutations (add/remove rules, create/remove virtual nodes).
- Config is saved automatically after mutations (add/remove rules, create/remove virtual nodes, `CreateLink`/`RemoveLink`, `SetNodeVolume`).
Saved configs include virtual nodes, routing rules, saved links, and per-node volume/mute state.
## Testing Hooks
When compiled with `-DWARPPIPE_TESTING`, additional helpers are available for tests (node/port/link injection, forcing disconnects, manual policy triggers, and meter/volume overrides). These APIs are not intended for production use.
## Performance
@ -206,3 +294,5 @@ warppipe_cli save-config <path>
warppipe_cli load-config <path>
warppipe_cli defaults
```
`link` uses node/port names (not IDs). `defaults` prints current and configured default sink/source names.

View file

@ -1,6 +1,6 @@
# Warppipe Configuration Schema
# Warp Pipe Configuration Schema
Warppipe uses JSON for configuration persistence. The config file stores virtual nodes and routing rules using stable identifiers (names, not serial IDs).
Warp Pipe uses JSON for configuration persistence. The config file stores virtual nodes, routing rules, saved links, and per-node volume/mute using stable identifiers (names, not serial IDs).
## Schema Version 1
@ -14,7 +14,9 @@ Warppipe uses JSON for configuration persistence. The config file stores virtual
"rate": 48000,
"channels": 2,
"loopback": false,
"target_node": ""
"target_node": "",
"volume": 1.0,
"mute": false
},
{
"name": "warppipe-mic-source",
@ -50,6 +52,14 @@ Warppipe uses JSON for configuration persistence. The config file stores virtual
},
"target_node": "alsa_output.usb-headset"
}
],
"links": [
{
"out_node": "Firefox",
"out_port": "output_FL",
"in_node": "warppipe-gaming-sink",
"in_port": "input_FL"
}
]
}
```
@ -64,6 +74,8 @@ Warppipe uses JSON for configuration persistence. The config file stores virtual
- `channels` (integer, default 2): Channel count
- `loopback` (boolean, default false): Whether node forwards to a target
- `target_node` (string, optional): Required when loopback is true
- `volume` (number, default 1.0): Stored node volume (0.0 - 1.5)
- `mute` (boolean, default false): Stored node mute state
### route_rules
@ -72,15 +84,27 @@ Rules match ephemeral audio sources to target sinks by stable application metada
- `match.application_name` (string): Match PW_KEY_APP_NAME
- `match.process_binary` (string): Match PW_KEY_APP_PROCESS_BINARY
- `match.media_role` (string): Match PW_KEY_MEDIA_ROLE
- `id` (integer, optional): Auto-saved rule id (ignored on load)
- `target_node` (string, required): Destination node name
All non-empty match fields must match (AND logic). At least one match field must be non-empty.
### links
Saved links are recreated by matching node and port names when both endpoints appear.
- `out_node` (string, required): Output node name
- `out_port` (string, required): Output port name
- `in_node` (string, required): Input node name
- `in_port` (string, required): Input port name
## Persistence Behavior
- **Auto-save**: When `ConnectionOptions::config_path` is set, config is saved after:
- Virtual node created/removed
- Routing rule added/removed
- `SetNodeVolume` updates
- `CreateLink`/`RemoveLink` updates
- **Load on startup**: When `config_path` is set and the file exists, it is loaded during `Client::Create()` after connection is established.

33
docs/gui-usage.md Normal file
View file

@ -0,0 +1,33 @@
# Warp Pipe GUI Usage
## Build
```
cmake -S . -B build
cmake --build build
```
Disable the GUI target with `-DWARPPIPE_BUILD_GUI=OFF`.
## Run
```
./build/warppipe-gui
```
## Configuration
The GUI uses `ConnectionOptions::config_path` pointed at Qt's `AppConfigLocation` with `/config.json` appended. On Linux this is typically under `~/.config/Warppipe/config.json`.
## Command-line options
- `--screenshot <path>`: capture a PNG screenshot and exit
- `--screenshot-delay <ms>`: delay before capture (default 800)
- `--debug-screenshot-dir <dir>`: save a timestamped screenshot on every graph update
- `--offscreen`: run with `QT_QPA_PLATFORM=offscreen`
- `--help`, `--version`
## Shortcuts
- `F12`: capture a screenshot to `~/Pictures/warppipe` with a timestamp
- `Ctrl+Q`: quit

View file

@ -3,7 +3,7 @@
## Build system
- CMake 3.20+
- C++17
- C++20
- pkg-config
- libpipewire-0.3 development files

View file

@ -11,6 +11,21 @@ cmake --build build
## Run
Usage:
```
./build/warppipe_perf --mode create-destroy|registry|links|policy|e2e [options]
```
Options:
- `--type` sink|source|both (default `sink`)
- `--count` N (default 200; per-type when `--type both`)
- `--events` N (registry mode, default 100)
- `--links` N (links mode, default 200)
- `--batch` N (links mode batch size)
- `--rate` N (default 48000)
- `--channels` N (default 2)
- `--target` <node-name> (loopback target)
Create/destroy microbenchmark (milestone 0/2):
```
./build/warppipe_perf --mode create-destroy --count 200 --type sink
@ -28,6 +43,16 @@ Link creation + removal (milestone 3):
./build/warppipe_perf --mode links --links 200 --batch 50
```
Policy auto-routing (milestone 4):
```
./build/warppipe_perf --mode policy --count 200
```
End-to-end (milestone 5):
```
./build/warppipe_perf --mode e2e --count 200
```
Optional format and loopback:
```
./build/warppipe_perf --mode create-destroy --count 200 --type sink --rate 48000 --channels 2

View file

@ -2,6 +2,31 @@
Milestone 0 test instructions are tracked in docs/milestone-0.md.
## Build
```
cmake -S . -B build -DWARPPIPE_BUILD_TESTS=ON
cmake --build build
```
Catch2 v3 is required when tests are enabled.
## Run
```
./build/warppipe_tests
```
GUI tests (requires Qt6, `-DWARPPIPE_BUILD_GUI=ON`):
```
./build/warppipe-gui-tests
```
## Notes
When tests are enabled, the library is compiled with `WARPPIPE_TESTING`, which exposes test-only helpers in the public header.
Planned coverage:
- Missing PipeWire daemon
- Missing link-factory module