// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Says how long after each cycle starts its audio is heard, as the graph //! reckons it for the sink the stream was linked to. It plays silence, so it //! can be run beside anything. //! //! zig build run-latency //! //! An ALSA sink says one quantum; a Bluetooth sink adds its codec's delay, //! which is usually the larger part. Move the stream to another sink with //! `pavucontrol` or `wpctl` while it runs and the figure follows. const std = @import("std"); const pw = @import("pipewire"); var stream_ptr: std.atomic.Value(?*pw.Stream) = .init(null); /// How far ahead of the clock each cycle's first frame is heard, as the /// callback saw it, in microseconds. var ahead_us: std.atomic.Value(i64) = .init(0); fn process(_: *anyopaque, planes: []const []f32, frames: u32) void { for (planes) |plane| @memset(plane[0..frames], 0); const stream = stream_ptr.load(.acquire) orelse return; const t = stream.time(); ahead_us.store(@divTrunc(t.presentationNs() - @as(i64, @intCast(pw.nowNsec())), std.time.ns_per_us), .monotonic); } pub fn main(init: std.process.Init) !void { var ctx: u8 = 0; const stream = try pw.Stream.open(init.gpa, .{ .name = "zig-pipewire latency", .channels = 2, .environ = init.minimal.environ, .process = .{ .ctx = &ctx, .func = process }, }); defer stream.close(); stream_ptr.store(stream, .release); if (!stream.waitStreaming(5000)) return error.NotStreaming; for (0..5) |_| { pw.sleep(std.time.ns_per_s); const t = stream.time(); std.debug.print("{d} Hz, {d} frames a cycle: heard {d} frames ({d:.1} ms) after the cycle starts, {d} us from now\n", .{ t.rate, t.quantum, t.delay, @as(f64, @floatFromInt(t.delayNs())) / std.time.ns_per_ms, ahead_us.load(.monotonic), }); } }