Something went wrong. Try again.
A pure Zig PipeWire client library that speaks the native wire protocol to play audio and manage the graph, with no libpipewire and no libc.
Something went wrong. Try again.
14 kB · 508 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! SPA (Simple Plugin API) constants and shared-memory layouts.//!//! The values here are part of the PipeWire wire protocol and the layout of the//! memory the server shares with a client, so they are fixed by the daemon we//! talk to rather than by us. They mirror `spa/include/spa/**` and//! `src/pipewire/private.h` from PipeWire 1.6.
const std = @import("std");
/// A sentinel meaning "no id"; appears in place of ids all over the protocol.pub const id_invalid: u32 = 0xffffffff;
/// POD type tags, from `enum spa_type`.pub const Type = enum(u32) { none = 1, bool = 2, id = 3, int = 4, long = 5, float = 6, double = 7, string = 8, bytes = 9, rectangle = 10, fraction = 11, bitmap = 12, array = 13, @"struct" = 14, object = 15, sequence = 16, pointer = 17, fd = 18, choice = 19, pod = 20, _,};
/// Object body types, from the `SPA_TYPE_OBJECT_*` block of `enum spa_type`.pub const object_type = struct { pub const prop_info: u32 = 0x40001; pub const props: u32 = 0x40002; pub const format: u32 = 0x40003; pub const param_buffers: u32 = 0x40004; pub const param_meta: u32 = 0x40005; pub const param_io: u32 = 0x40006; pub const param_port_config: u32 = 0x40008; pub const param_route: u32 = 0x40009; pub const param_latency: u32 = 0x4000b; pub const param_tag: u32 = 0x4000d;};
/// `SPA_TYPE_COMMAND_Node`, the object type of the commands the server sends us.pub const command_node: u32 = 0x30002;
/// Choice kinds, from `enum spa_choice_type`.pub const Choice = enum(u32) { none = 0, range = 1, step = 2, @"enum" = 3, flags = 4,};
/// Param ids, from `enum spa_param_type`.pub const Param = enum(u32) { invalid = 0, prop_info = 1, props = 2, enum_format = 3, format = 4, buffers = 5, meta = 6, io = 7, enum_profile = 8, profile = 9, enum_port_config = 10, port_config = 11, enum_route = 12, route = 13, control = 14, latency = 15, process_latency = 16, tag = 17, _,};
/// Flags in a `spa_param_info` entry.pub const param_info = struct { pub const serial: u32 = 1 << 0; pub const read: u32 = 1 << 1; pub const write: u32 = 1 << 2; pub const readwrite: u32 = read | write;};
/// Property keys of a `SPA_TYPE_OBJECT_Format` object.pub const format = struct { pub const media_type: u32 = 1; pub const media_subtype: u32 = 2; pub const audio_format: u32 = 0x10001; pub const audio_rate: u32 = 0x10003; pub const audio_channels: u32 = 0x10004; pub const audio_position: u32 = 0x10005;};
/// Property keys of a `SPA_TYPE_OBJECT_ParamLatency` object, from/// `enum spa_param_latency`.pub const latency = struct { pub const direction: u32 = 1; pub const min_quantum: u32 = 2; pub const max_quantum: u32 = 3; pub const min_rate: u32 = 4; pub const max_rate: u32 = 5; pub const min_ns: u32 = 6; pub const max_ns: u32 = 7;};
/// `enum spa_media_type`.pub const media_type = struct { pub const audio: u32 = 1; pub const application: u32 = 6;};
/// `enum spa_media_subtype`.pub const media_subtype = struct { pub const raw: u32 = 1; pub const dsp: u32 = 2; pub const control: u32 = 0x60001;};
/// `enum spa_audio_format`. Only the entries this library can produce.pub const audio_format = struct { pub const f32p: u32 = 0x206; /// PipeWire's DSP audio format: one plane of native-endian f32 per port. pub const dsp_f32: u32 = f32p;};
/// `enum spa_audio_channel`: speaker positions, used in a format's `position`/// array. Only the entries this library's `Channel` maps to.pub const audio_channel = struct { pub const unknown: u32 = 0; pub const na: u32 = 1; pub const mono: u32 = 2; pub const fl: u32 = 3; pub const fr: u32 = 4; pub const fc: u32 = 5; pub const lfe: u32 = 6; pub const sl: u32 = 7; pub const sr: u32 = 8; pub const flc: u32 = 9; pub const frc: u32 = 10; pub const rc: u32 = 11; pub const rl: u32 = 12; pub const rr: u32 = 13;};
/// Property keys of a `SPA_TYPE_OBJECT_Props` object, from `enum spa_prop`.////// Only the audio block, which is what a session manager sets on a node: a/// volume is a linear amplitude where 1.0 is unattenuated, and `channel_volumes`/// carries one per channel of the node's negotiated layout.pub const prop = struct { pub const volume: u32 = 0x10003; pub const mute: u32 = 0x10004; pub const channel_volumes: u32 = 0x10008; pub const volume_base: u32 = 0x10009; pub const volume_step: u32 = 0x1000a; pub const channel_map: u32 = 0x1000b; pub const monitor_mute: u32 = 0x1000c; pub const monitor_volumes: u32 = 0x1000d; pub const soft_mute: u32 = 0x1000f; pub const soft_volumes: u32 = 0x10010;};
/// Property keys of a `SPA_TYPE_OBJECT_ParamBuffers` object.pub const param_buffers = struct { pub const buffers: u32 = 1; pub const blocks: u32 = 2; pub const size: u32 = 3; pub const stride: u32 = 4; pub const @"align": u32 = 5; pub const data_type: u32 = 6; pub const meta_type: u32 = 7;};
/// Property keys of a `SPA_TYPE_OBJECT_ParamPortConfig` object.pub const param_port_config = struct { pub const direction: u32 = 1; pub const mode: u32 = 2; pub const monitor: u32 = 3; pub const control: u32 = 4; pub const format: u32 = 5;};
/// `enum spa_param_port_config_mode`.pub const PortConfigMode = enum(u32) { none = 0, passthrough = 1, convert = 2, /// One mono port per channel, in PipeWire's planar f32 DSP format. dsp = 3, _,};
/// Property keys of a `SPA_TYPE_OBJECT_ParamRoute` object.////// A route is one way in or out of a device — a headphone jack, a speaker pair/// — and it is where a sound card's volume actually lives: the node in the/// graph carries none of it, so a mixer that sets the node's `Props` moves a/// slider nothing is listening to.pub const param_route = struct { pub const index: u32 = 1; pub const direction: u32 = 2; /// Which of the card's devices this route belongs to, matching a node's /// `card.profile.device` property. pub const device: u32 = 3; pub const name: u32 = 4; pub const description: u32 = 5; pub const priority: u32 = 6; pub const available: u32 = 7; pub const info: u32 = 8; pub const profiles: u32 = 9; /// A `SPA_TYPE_OBJECT_Props` object: the volume and mute of this route. pub const props: u32 = 10; pub const devices: u32 = 11; pub const profile: u32 = 12; /// Whether the session manager should remember the change. pub const save: u32 = 13;};
/// Property keys of a `SPA_TYPE_OBJECT_ParamIO` object.pub const param_io = struct { pub const id: u32 = 1; pub const size: u32 = 2;};
/// `enum spa_io_type`: the kinds of shared area the server can hand a node.pub const IoType = enum(u32) { invalid = 0, buffers = 1, range = 2, clock = 3, latency = 4, control = 5, notify = 6, position = 7, rate_match = 8, memory = 9, async_buffers = 10, _,};
/// `enum spa_node_command`.pub const NodeCommand = enum(u32) { @"suspend" = 0, pause = 1, start = 2, enable = 3, disable = 4, flush = 5, drain = 6, marker = 7, param_begin = 8, param_end = 9, request_process = 10, user = 11, _,};
/// `enum spa_data_type`: how the bytes behind a `Data` block are reached.pub const DataType = enum(u32) { invalid = 0, mem_ptr = 1, mem_fd = 2, dma_buf = 3, mem_id = 4, sync_obj = 5, _,};
/// Status bits shared through an `IoBuffers` area, from `spa_io_buffers`.pub const status = struct { pub const ok: i32 = 0; pub const need_data: i32 = 1 << 0; pub const have_data: i32 = 1 << 1; pub const stopped: i32 = 1 << 2; pub const drained: i32 = 1 << 3;};
/// `struct spa_node_info` flags.pub const node_flag = struct { pub const rt: u64 = 1 << 0;};
/// `struct spa_node_info` change mask bits.pub const node_change = struct { pub const flags: u64 = 1 << 0; pub const props: u64 = 1 << 1; pub const params: u64 = 1 << 2;};
/// `struct spa_port_info` flags.pub const port_flag = struct { pub const no_ref: u64 = 1 << 4;};
/// `struct spa_port_info` change mask bits.pub const port_change = struct { pub const flags: u64 = 1 << 0; pub const rate: u64 = 1 << 1; pub const props: u64 = 1 << 2; pub const params: u64 = 1 << 3;};
/// Which way data flows through a port.////// Non-exhaustive, like every other enum in this file that is decoded from the/// wire: the value arrives as a `u32` from another process, and `@enumFromInt`/// into an exhaustive enum is illegal behaviour for anything outside its range/// rather than a value that can be checked and rejected.pub const Direction = enum(u32) { input = 0, output = 1, _,};
pub const Rectangle = extern struct { width: u32, height: u32 };pub const Fraction = extern struct { num: u32, denom: u32 };
// --- Shared memory layouts -------------------------------------------------//// Everything below is mapped from a file descriptor the server sends us, so the// field order and size must match PipeWire exactly. The `comptime` block at the// bottom of this file asserts the sizes that PipeWire itself hard-codes.
/// `struct spa_io_buffers`: the handshake slot for one link.pub const IoBuffers = extern struct { status: i32, buffer_id: u32,};
/// `struct spa_io_async_buffers`: a pair of `IoBuffers` selected by cycle parity.pub const IoAsyncBuffers = extern struct { buffers: [2]IoBuffers,};
/// `struct spa_io_clock`: the driver's notion of time for the current cycle.pub const IoClock = extern struct { flags: u32, id: u32, name: [64]u8, nsec: u64, rate: Fraction, position: u64, duration: u64, delay: i64, rate_diff: f64, next_nsec: u64, target_rate: Fraction, target_duration: u64, target_seq: u32, cycle: u32, xrun: u64,};
pub const IoVideoSize = extern struct { flags: u32, stride: u32, size: Rectangle, framerate: Fraction, padding: [4]u32,};
pub const IoSegmentBar = extern struct { flags: u32, offset: u32, signature_num: f32, signature_denom: f32, bpm: f64, beat: f64, bar_start_tick: f64, ticks_per_beat: f64, padding: [4]u32,};
pub const IoSegmentVideo = extern struct { flags: u32, offset: u32, framerate: Fraction, hours: u32, minutes: u32, seconds: u32, frames: u32, field_count: u32, padding: [11]u32,};
pub const IoSegment = extern struct { version: u32, flags: u32, start: u64, duration: u64, rate: f64, position: u64, bar: IoSegmentBar, video: IoSegmentVideo,};
/// `struct spa_io_position`: clock plus transport state for the current cycle.pub const IoPosition = extern struct { clock: IoClock, video: IoVideoSize, offset: i64, state: u32, n_segments: u32, segments: [8]IoSegment,};
/// `struct pw_node_activation_state`: the barrier one node waits on.pub const ActivationState = extern struct { status: i32, required: i32, pending: i32,};
/// The `status` field of an `Activation`, from the `PW_NODE_ACTIVATION_*` defines.////// A node starts INACTIVE. Once it is watching its eventfd it stores FINISHED,/// which tells the driver it may be scheduled. Per cycle the driver moves it to/// NOT_TRIGGERED, a peer moves it to TRIGGERED and writes the eventfd, the node/// moves it to AWAKE when it wakes, and back to FINISHED when it is done.pub const activation_status = struct { pub const not_triggered: u32 = 0; pub const triggered: u32 = 1; pub const awake: u32 = 2; pub const finished: u32 = 3; pub const inactive: u32 = 4;};
/// The activation-record version this library implements. Version 1 means the/// `status` transitions use compare-and-swap rather than plain stores.pub const activation_version: u32 = 1;
/// `struct pw_node_activation`: one per node, shared between the daemon and/// every peer that has to wake this node up.pub const Activation = extern struct { status: u32, /// Bitfield in C: `version:1`, `pending_sync:1`, `pending_new_pos:1`. bits: u32, state: [2]ActivationState, signal_time: u64, awake_time: u64, finish_time: u64, prev_signal_time: u64, reposition: IoSegment, segment: IoSegment, segment_owner: [16]u32, prev_awake_time: u64, prev_finish_time: u64, padding: [7]u32, client_version: u32, server_version: u32, active_driver_id: u32, driver_id: u32, flags: u32, position: IoPosition, sync_timeout: u64, sync_left: u64, cpu_load: [3]f32, xrun_count: u32, xrun_time: u64, xrun_delay: u64, max_delay: u64, command: u32, reposition_owner: u32,};
/// `struct spa_chunk`: which part of a data block actually holds samples.pub const Chunk = extern struct { offset: u32, size: u32, stride: i32, flags: i32,};
comptime { // PipeWire hard-codes these sizes when it allocates the shared areas, so a // mismatch here would silently corrupt the graph rather than fail to build. // The numbers come from compiling the daemon's own headers. std.debug.assert(@sizeOf(IoBuffers) == 8); std.debug.assert(@sizeOf(IoAsyncBuffers) == 16); std.debug.assert(@sizeOf(IoClock) == 160); std.debug.assert(@sizeOf(IoVideoSize) == 40); std.debug.assert(@sizeOf(IoSegmentBar) == 64); std.debug.assert(@sizeOf(IoSegmentVideo) == 80); std.debug.assert(@sizeOf(IoSegment) == 184); std.debug.assert(@sizeOf(IoPosition) == 1688); std.debug.assert(@offsetOf(IoPosition, "segments") == 216); std.debug.assert(@sizeOf(Chunk) == 16); std.debug.assert(@sizeOf(ActivationState) == 12); std.debug.assert(@offsetOf(Activation, "segment_owner") == 432); std.debug.assert(@offsetOf(Activation, "client_version") == 540); std.debug.assert(@offsetOf(Activation, "position") == 560); std.debug.assert(@offsetOf(Activation, "command") == 2304); std.debug.assert(@sizeOf(Activation) == 2312);}