7.2 KiB
Warppipe 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>.
Quick Start
#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
Clientmethods can be called from any thread. - Do not call
Clientmethods 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>.
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— successkInvalidArgument— 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 availablekPermissionDenied— access deniedkTimeout— PipeWire sync timed outkInternal— allocation or I/O failurekNotImplemented— caller thread mode
Connection Options
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
};
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
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_NAMEmedia_class— PW_KEY_MEDIA_CLASSapplication_name— PW_KEY_APP_NAMEprocess_binary— PW_KEY_APP_PROCESS_BINARYmedia_role— PW_KEY_MEDIA_ROLE
Virtual Nodes
Result<VirtualSink> CreateVirtualSink(std::string_view name, const VirtualNodeOptions& opts);
Result<VirtualSource> CreateVirtualSource(std::string_view name, const VirtualNodeOptions& opts);
Status RemoveNode(NodeId node);
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.
Link Management
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.
Route Rules (Policy Engine)
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_NAMEprocess_binary— PW_KEY_APP_PROCESS_BINARYmedia_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.
Adding a rule also scans existing nodes for matches.
Metadata
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
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).
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