warp-pipe/docs/api.md
2026-01-29 21:13:18 -07:00

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 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>.

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

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_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

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.

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_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.

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