Something went wrong. Try again.
Play and record audio on Windows from Zig through WASAPI and COM, without linking a C library.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Play a chord through the pull API: a `Process` callback filling one plane per//! channel, once per graph cycle.//!//! ```console//! $ zig build run-chord -- 3//! 48000 Hz, 2 ch, f32, mask 0x3, 480 frame period//! playing an A major triad for 3 s from the process callback//! 612 cycles, period 10000 us, wake 0/181/2210 us, busy 21/34/96 us//! ```//!//! The difference from `tone.zig` is where the audio comes from: there is no queue//! and no writing thread, and the callback runs inside the graph cycle that plays//! what it produces. That is the lower-latency shape, and the one a synthesiser//! wants -- at the cost of having to produce audio on the engine's schedule and//! inside its deadline.
const std = @import("std");const wasapi = @import("wasapi");
/// Three sine oscillators: A4, and the major third and fifth above it.////// Quiet enough that three of them together do not clip -- 0.15 each peaks at 0.45/// when the phases line up, which they do at the start.const Chord = struct { phases: [3]f32 = @splat(0), steps: [3]f32,
const frequencies = [3]f32{ 440.0, 554.365, 659.255 }; const amplitude = 0.15;
fn init(rate: u32) Chord { var steps: [3]f32 = undefined; for (&steps, frequencies) |*step, frequency| { step.* = 2.0 * std.math.pi * frequency / @as(f32, @floatFromInt(rate)); } return .{ .steps = steps }; }
/// The callback. Runs on the render thread, against a real-time deadline: no /// allocation, no locks, no input or output. Three multiply-adds per sample is /// comfortably inside that. fn fill(ctx: *anyopaque, planes: []const []f32, frames: u32) void { const self: *Chord = @ptrCast(@alignCast(ctx));
for (0..frames) |i| { var sample: f32 = 0; for (&self.phases, self.steps) |*phase, step| { sample += @sin(phase.*) * amplitude; phase.* += step; if (phase.* > 2.0 * std.math.pi) phase.* -= 2.0 * std.math.pi; } // Every plane must be filled completely; whatever is left unwritten is // whatever the previous cycle put there. for (planes) |plane| plane[i] = sample; } }};
pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io;
var stdout_buffer: [4096]u8 = undefined; var stdout_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer); const stdout = &stdout_writer.interface; defer stdout.flush() catch {};
const args = try init.minimal.args.toSlice(init.arena.allocator()); const seconds: i64 = if (args.len > 1) std.fmt.parseInt(i64, args[1], 10) catch 3 else 3;
// The oscillators have to know the rate, and the rate is not known until the // stream is open -- so the callback is pointed at storage that outlives both and // is filled in once the stream has said. var chord: Chord = .init(48000);
const stream = try wasapi.Stream.open(gpa, io, .{ .name = "zig-wsapi chord", .channels = 2, .rate = 48000, .process = .{ .ctx = &chord, .func = &Chord.fill }, }); defer stream.close(io);
if (!try stream.waitStreaming(io, timeout(5))) { try stdout.print("the stream never started\n", .{}); return; }
// The endpoint may have settled on another rate, in which case the oscillators // were stepping at the wrong one for the first cycle or two. Retuning here costs // nothing and keeps the pitch right. if (stream.rate() != 48000) chord = .init(stream.rate());
try stdout.print("{f}, {d} frame period\n", .{ stream.endpointFormat(), stream.quantum() }); try stdout.print("playing an A major triad for {d} s from the process callback\n", .{seconds}); try stdout.flush();
// Pull mode has nothing to write, so this is simply how long to let it run. try io.sleep(.fromSeconds(seconds), .awake);
try stdout.print(" {f}\n", .{stream.stats()});}
fn timeout(s: i64) std.Io.Timeout { return .{ .duration = .{ .raw = .fromSeconds(s), .clock = .awake } };}