warp-pipe/docs/api.md
2026-01-31 11:21:28 -07:00

11 KiB

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

#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
};

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

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

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);
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:

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

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

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

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.

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)

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

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