298 lines
11 KiB
Markdown
298 lines
11 KiB
Markdown
# 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 `<warppipe/warppipe.hpp>`.
|
|
|
|
## Quick Start
|
|
|
|
```cpp
|
|
#include <warppipe/warppipe.hpp>
|
|
|
|
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<T>`.
|
|
|
|
```cpp
|
|
struct Status {
|
|
StatusCode code;
|
|
std::string message;
|
|
bool ok() const;
|
|
};
|
|
|
|
template<typename T>
|
|
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<std::string> remote_name; // Connect to non-default PipeWire
|
|
std::optional<std::string> 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<std::vector<NodeInfo>> ListNodes();
|
|
Result<std::vector<PortInfo>> ListPorts(NodeId node);
|
|
Result<std::vector<Link>> 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<VirtualSink> CreateVirtualSink(std::string_view name, const VirtualNodeOptions& opts);
|
|
Result<VirtualSource> 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<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
|
|
Result<Link> CreateLink(PortId output, PortId input, const LinkOptions& opts);
|
|
Result<Link> 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<RuleId> AddRouteRule(const RouteRule& rule);
|
|
Status RemoveRouteRule(RuleId id);
|
|
Result<std::vector<RouteRule>> 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<MetadataInfo> 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 <node-id>
|
|
warppipe_cli list-links
|
|
warppipe_cli list-rules
|
|
warppipe_cli create-sink <name> [--rate N] [--channels N]
|
|
warppipe_cli create-source <name> [--rate N] [--channels N]
|
|
warppipe_cli link <out-node> <out-port> <in-node> <in-port> [--passive] [--linger]
|
|
warppipe_cli unlink <link-id>
|
|
warppipe_cli add-rule --app <name> --target <node> [--process <bin>] [--role <role>]
|
|
warppipe_cli remove-rule <rule-id>
|
|
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.
|