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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Record the default microphone into a WAV file, showing a level meter as it goes.//!//! ```console//! $ zig build run-record -- 5 voice.wav//! recording Microphone Array (Realtek(R) Audio)//! 48000 Hz, 2 ch, f32, mask 0x3//! [######################## ] -12.4 dB//! wrote voice.wav: 240000 frames, 0 lost//! ```//!//! The only difference from `loopback.zig` is `.source = .microphone`. Everything else --//! the queue, the reading, the conversion -- is the same code.//!//! Windows will refuse this if microphone access is switched off in the privacy settings,//! which arrives as `error.AccessDenied`. That is a setting rather than a fault, so it is//! reported as such rather than as a failure of the audio stack.
const std = @import("std");const wasapi = @import("wasapi");const wav = @import("wav");
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: f32 = if (args.len > 1) std.fmt.parseFloat(f32, args[1]) catch 5 else 5; const path = if (args.len > 2) args[2] else "record.wav";
const capture = wasapi.Capture.open(gpa, io, .{ .name = "zig-wsapi record", .source = .microphone, }) catch |err| switch (err) { error.AccessDenied => { try stdout.print( "microphone access is switched off for this machine or this program;\n" ++ "Settings > Privacy & security > Microphone controls it\n", .{}, ); return; }, error.DeviceNotFound => { try stdout.print("this machine has no capture endpoint\n", .{}); return; }, else => |e| return e, }; defer capture.close(io);
if (!try capture.waitStreaming(io, timeout(5))) { try stdout.print("the capture never started\n", .{}); return; }
const format = capture.capturedFormat(); try stdout.print("recording\n {f}\n", .{format}); try stdout.flush();
var file: wav.Writer = try .create(io, .cwd(), path, .{ .rate = format.rate, .channels = format.channels, }); errdefer _ = file.finish(io) catch {};
const channels = capture.channels(); const block = try gpa.alloc(f32, @as(usize, format.rate / 10) * channels); defer gpa.free(block);
var frames_left: u64 = @intFromFloat(seconds * @as(f32, @floatFromInt(format.rate))); while (frames_left > 0) { const wanted: u64 = @min(frames_left, block.len / channels); const got = try capture.readAll(io, block[0 .. wanted * channels], timeout(5)); if (got == 0) break;
const samples = block[0 .. @as(usize, got) * channels]; try file.write(io, samples); frames_left -= got;
// A level meter, redrawn in place. Peak rather than average, because that is what // tells somebody whether they are about to clip. try drawMeter(stdout, peakOf(samples)); try stdout.flush(); }
const frames_written = try file.finish(io); try stdout.print("\nwrote {s}: {d} frames, {d} lost\n", .{ path, frames_written, capture.overruns(), });}
fn peakOf(samples: []const f32) f32 { var peak: f32 = 0; for (samples) |s| peak = @max(peak, @abs(s)); return peak;}
fn drawMeter(stdout: *std.Io.Writer, peak: f32) !void { const width = 48; // Decibels rather than the raw amplitude, because hearing is logarithmic and a linear // meter spends most of its length on sounds nobody can hear. const db: f32 = if (peak > 0) 20.0 * @log10(peak) else -120.0; // A 60 dB window, which is what a recording meter conventionally shows. const filled: usize = @intFromFloat(std.math.clamp( (db + 60.0) / 60.0 * @as(f32, width), 0, @as(f32, width), ));
try stdout.print("\r [", .{}); for (0..width) |i| try stdout.writeByte(if (i < filled) '#' else ' '); if (peak > 0) { try stdout.print("] {d:>6.1} dB", .{db}); } else { try stdout.print("] silent", .{}); }}
fn timeout(s: i64) std.Io.Timeout { return .{ .duration = .{ .raw = .fromSeconds(s), .clock = .awake } };}