# Warp Pipe API ## Overview 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 ``. ## Quick Start ```cpp #include warppipe::ConnectionOptions opts; opts.application_name = "my-app"; opts.config_path = "/home/user/.config/warppipe/config.json"; auto result = warppipe::Client::Create(opts); if (!result.ok()) { // handle error: result.status.code, result.status.message } auto& client = result.value; // Create a virtual sink auto sink = client->CreateVirtualSink("my-sink"); // Route Firefox audio to it warppipe::RouteRule rule; rule.match.application_name = "Firefox"; rule.target_node = "my-sink"; client->AddRouteRule(rule); // The policy engine auto-links Firefox when it starts playing ``` ## Threading Model `Client` creates a dedicated PipeWire thread loop (`pw_thread_loop`) that runs PipeWire event dispatch. All callbacks (registry events, stream state, link proxy events, metadata changes) execute on this thread. Public methods are thread-safe. They lock the PipeWire thread loop for operations that call into libpipewire, and use a separate `cache_mutex` for reading/writing the in-memory registry cache. **Rules:** - All public `Client` methods can be called from any thread. - Do not call `Client` methods from PipeWire callbacks (deadlock). - The policy engine runs on the PipeWire thread. Auto-links are created asynchronously without blocking. ## Error Model Every fallible operation returns `Status` or `Result`. ```cpp struct Status { StatusCode code; std::string message; bool ok() const; }; template struct Result { Status status; T value; bool ok() const; }; ``` `StatusCode` values: - `kOk` — success - `kInvalidArgument` — bad input (empty name, missing match criteria, corrupted config) - `kNotFound` — object doesn't exist (port, rule, config file) - `kUnavailable` — PipeWire daemon down, stream failed, metadata not available - `kPermissionDenied` — access denied - `kTimeout` — PipeWire sync timed out - `kInternal` — allocation or I/O failure - `kNotImplemented` — caller thread mode ## Connection Options ```cpp struct ConnectionOptions { ThreadingMode threading; // kThreadLoop (default) or kCallerThread (not impl) bool autoconnect; // Reconnect on disconnect (default true) bool policy_only; // Observe-only mode, no auto-links (default false) std::string application_name; // PW_KEY_APP_NAME (default "warppipe") std::optional remote_name; // Connect to non-default PipeWire std::optional config_path; // Auto-load on start, auto-save on change }; ``` 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. Use this when: - Running alongside WirePlumber and you want to set metadata defaults instead of forcing links. - Building a monitoring/observation tool. You can still create links manually via `CreateLink` / `CreateLinkByName`. ## Registry Queries ```cpp Result> ListNodes(); Result> ListPorts(NodeId node); Result> ListLinks(); ``` `NodeInfo` includes stable identity fields used for rule matching: - `name` — PW_KEY_NODE_NAME - `media_class` — PW_KEY_MEDIA_CLASS - `application_name` — PW_KEY_APP_NAME - `process_binary` — PW_KEY_APP_PROCESS_BINARY - `media_role` — PW_KEY_MEDIA_ROLE ## Virtual Nodes ```cpp Result CreateVirtualSink(std::string_view name, const VirtualNodeOptions& opts); Result CreateVirtualSource(std::string_view name, const VirtualNodeOptions& opts); Status RemoveNode(NodeId node); ``` ```cpp struct VirtualNodeOptions { AudioFormat format; // rate/channels VirtualBehavior behavior; // kNull (default) or kLoopback std::optional target_node; // Required for kLoopback std::optional 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 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 NodeMeterPeak(NodeId node) const; Result 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 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 Result CreateLink(PortId output, PortId input, const LinkOptions& opts); Result CreateLinkByName(std::string_view out_node, std::string_view out_port, std::string_view in_node, std::string_view in_port, const LinkOptions& opts); 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 Result AddRouteRule(const RouteRule& rule); Status RemoveRouteRule(RuleId id); Result> ListRouteRules(); ``` Rules match newly-appearing nodes by application metadata and auto-link them to target sinks. Match criteria (all non-empty fields must match): - `application_name` — PW_KEY_APP_NAME - `process_binary` — PW_KEY_APP_PROCESS_BINARY - `media_role` — PW_KEY_MEDIA_ROLE 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 ```cpp Result GetDefaults(); Status SetDefaultSink(std::string_view node_name); Status SetDefaultSource(std::string_view node_name); ``` Metadata is read from PipeWire's "default" metadata object (created by the session manager). `SetDefaultSink`/`SetDefaultSource` set `default.configured.audio.sink`/`source`, which persists across sessions. Returns `kUnavailable` if no metadata object is found (e.g., no session manager running). ## Persistence ```cpp Status SaveConfig(std::string_view path); Status LoadConfig(std::string_view path); ``` 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, `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 Measured on PipeWire 1.4.10, Fedora, warm connection: | Operation | Count | Time | Budget | |-----------|-------|------|--------| | Create/destroy virtual sinks | 200 | ~390ms | <1000ms | | Registry snapshot + 100 events | 200 nodes | ~218ms | <1000ms | | Policy: 200 ephemeral sources | 200 | ~370ms | <1000ms | Rule lookup is O(n) over rules per node appearance, O(n) over ports for link matching. Typical rule sets (<100 rules) complete in microseconds. ## CLI The `warppipe_cli` binary provides manual access to all operations: ``` warppipe_cli list-nodes warppipe_cli list-ports warppipe_cli list-links warppipe_cli list-rules warppipe_cli create-sink [--rate N] [--channels N] warppipe_cli create-source [--rate N] [--channels N] warppipe_cli link [--passive] [--linger] warppipe_cli unlink warppipe_cli add-rule --app --target [--process ] [--role ] warppipe_cli remove-rule warppipe_cli save-config warppipe_cli load-config warppipe_cli defaults ``` `link` uses node/port names (not IDs). `defaults` prints current and configured default sink/source names.