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.
4.6 kB · 133 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Plays a test tone using the push API: audio is written into the stream's//! internal ring from the main thread and drained by the real-time thread.//!//! zig build run-tone -- [seconds] [hz] [channels]
const std = @import("std");const pw = @import("pipewire");
fn logLine(_: *anyopaque, level: pw.LogLevel, msg: []const u8) void { std.debug.print("[pipewire {t}] {s}\n", .{ level, msg });}
pub fn main(init: std.process.Init) !void { const gpa = init.gpa;
var args = init.minimal.args.iterate(); _ = args.next(); // program name const seconds = parseFloat(args.next(), 3.0); const tone_hz = parseFloat(args.next(), 440.0); const channels = parseInt(args.next(), 2);
var log_ctx: u8 = 0; const stream = try pw.Stream.open(gpa, .{ .name = "zig-pipewire tone", .media_name = "Test tone", .channels = channels, .rate = 48000, .environ = init.minimal.environ, .log = .{ .ctx = @ptrCast(&log_ctx), .func = logLine }, }); defer stream.close();
// The tone is generated at the rate we asked for; the graph is free to run // at another, in which case the pitch shifts by that ratio. const nominal_rate: f32 = 48000; const total: u64 = @intFromFloat(seconds * nominal_rate); const chunk_frames = 512; const chunk = try gpa.alloc(f32, chunk_frames * channels); defer gpa.free(chunk);
var tone: Tone = .{ .step = 2.0 * std.math.pi * tone_hz / nominal_rate, .fade_frames = @intFromFloat(nominal_rate / 20), .total = total, };
// The graph starts pulling the moment it links, so fill the ring first: // whatever is queued by then is heard without a gap at the start. var written: u64 = 0; while (written < total and stream.writable() >= chunk_frames) { written = tone.fill(chunk, channels, written, stream, .nonblocking); }
if (!stream.waitStreaming(5000)) { std.debug.print("stream did not start (state: {t})\n", .{stream.getState()}); return error.NotStreaming; }
std.debug.print("playing {d} Hz for {d} s: {d} ch in, {d} ch out at {d} Hz, quantum {d}\n", .{ tone_hz, seconds, channels, stream.graphChannels(), stream.rate(), stream.quantum(), });
while (written < total) { written = tone.fill(chunk, channels, written, stream, .blocking); }
stream.drain(2000); // A quantum or so of this is the silence after the tone ends, not a fault. std.debug.print("done; {d} underrun frames\n", .{stream.underruns()});}
fn parseFloat(arg: ?[:0]const u8, default: f32) f32 { return std.fmt.parseFloat(f32, arg orelse return default) catch default;}
fn parseInt(arg: ?[:0]const u8, default: u32) u32 { return std.fmt.parseInt(u32, arg orelse return default, 10) catch default;}
/// A fading sine, generated a chunk at a time.const Tone = struct { phase: f32 = 0, step: f32, fade_frames: u64, total: u64,
const Mode = enum { blocking, nonblocking };
/// Generate from frame `written` onward and hand it to the stream. Returns /// the new total; in nonblocking mode that may be short of a full chunk. fn fill( t: *Tone, chunk: []f32, channels: u32, written: u64, stream: *pw.Stream, mode: Mode, ) u64 { const room: usize = switch (mode) { .blocking => chunk.len / channels, .nonblocking => @min(chunk.len / channels, stream.writable()), }; const n: usize = @intCast(@min(@as(u64, room), t.total - written)); if (n == 0) return written;
for (0..n) |i| { // Fade in and out so the tone starts and stops without a click. const from_start = written + i; const to_end = t.total - from_start; const fade = @as(f32, @floatFromInt(@min(@min(from_start, to_end), t.fade_frames))) / @as(f32, @floatFromInt(t.fade_frames)); const sample = 0.25 * fade * @sin(t.phase); for (0..channels) |c| chunk[i * channels + c] = sample; t.phase += t.step; if (t.phase > 2.0 * std.math.pi) t.phase -= 2.0 * std.math.pi; }
const samples = chunk[0 .. n * channels]; switch (mode) { .blocking => stream.writeAll(samples) catch return t.total, .nonblocking => _ = stream.write(samples), } return written + n; }};