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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! COM, reduced to what this library needs from it: apartments, object//! creation, reference counting, and getting a string out of a property store.//!//! ```zig//! var apartment: com.Apartment = try .enter(com.coinit_multithreaded);//! defer apartment.leave();//!//! const enumerator = try com.create(//! audio.IMMDeviceEnumerator,//! audio.CLSID_MMDeviceEnumerator,//! audio.IID_IMMDeviceEnumerator,//! );//! defer com.release(enumerator);//! ```//!//! ## Apartments, which are the thing to get right//!//! Every thread that touches COM has to be in an apartment, and a library does//! not get to choose the caller's. `Apartment.enter` therefore treats three//! different answers from `CoInitializeEx` as success and remembers which one//! it got, because what `leave` must do differs in each case -- see//! `Apartment.State`. Getting this wrong is the most likely way for this//! library to fail inside somebody else's application, and it fails in the//! worst way: not at the call that was wrong, but later, somewhere else.//!//! The library's own threads sidestep all of it. A `Stream`'s COM objects are//! created, used and released entirely on its render thread, which is a thread//! this library made and put in the multithreaded apartment itself, so no//! interface pointer ever crosses a thread boundary and no marshalling is ever//! needed. Only `Session`, which is synchronous on the caller's thread by//! nature, has to cope with an apartment it did not choose.
const std = @import("std");
const errors = @import("../errors.zig");
pub const hresult = @import("hresult.zig");pub const check = hresult.check;pub const classifyOnly = hresult.classifyOnly;pub const eql = hresult.eql;pub const failed = hresult.failed;pub const succeeded = hresult.succeeded;
pub const missing = @import("missing.zig");pub const sys = @import("sys.zig");pub const Event = sys.Event;pub const Clock = sys.Clock;pub const ProAudio = sys.ProAudio;
pub const enumerator = @import("enumerator.zig");pub const activate = @import("activate.zig");pub const notify = @import("notify.zig");pub const NotificationClient = notify.NotificationClient;pub const NotificationFlags = notify.Flags;pub const Enumerator = enumerator.Enumerator;pub const Endpoint = enumerator.Endpoint;pub const Flow = enumerator.Flow;pub const Role = enumerator.Role;/// Named `EndpointState` rather than `State` because `Apartment.State` is also/// here and a bare `State` in this namespace would be ambiguous -- which the/// compiler says outright, and which a reader would have to guess about.pub const EndpointState = enumerator.State;
const log = std.log.scoped(.wasapi);
const win32 = @import("win32").everything;const Guid = win32.Guid;const HRESULT = win32.HRESULT;
/// `COINIT_MULTITHREADED`.////// Spelled as a named constant because it is the all-zeroes value of a packed/// struct whose only named low bit is `APARTMENTTHREADED`, so the obvious/// "explicit" spelling is a bug: `.{ .APARTMENTTHREADED = 1 }` asks for a/// single-threaded apartment, quietly, and everything still appears to work/// until a notification arrives on the wrong thread.pub const coinit_multithreaded: win32.COINIT = .{};
/// `COINIT_APARTMENTTHREADED`, for completeness and for the test below. This/// library never asks for an STA.pub const coinit_apartmentthreaded: win32.COINIT = .{ .APARTMENTTHREADED = 1 };
/// `CLSCTX_ALL`. Every audio object this library creates is in-process, so this/// is broader than strictly needed, but it is what the Core Audio samples pass/// and there is no benefit in differing from them.pub const clsctx_all: win32.CLSCTX = .{ .INPROC_SERVER = 1, .INPROC_HANDLER = 1, .LOCAL_SERVER = 1, .REMOTE_SERVER = 1,};
/// `STGM_READ`, which -- like `coinit_multithreaded` -- is the all-zeroes value/// of a flags struct and so deserves a name rather than a `.{}` at the call/// site that a reader would have to look up.pub const stgm_read: win32.STGM = .{};
/// The apartment this library put a thread into, and what leaving it requires.////// A `CoInitializeEx`/`CoUninitialize` pair has to balance per *call*, not per/// thread, and the third case below must not be balanced at all -- so the/// outcome has to be remembered rather than recomputed.pub const Apartment = struct { state: State,
pub const State = enum { /// `S_OK`: this call initialised the apartment. `CoUninitialize` on the /// way out. initialised,
/// `S_FALSE`: the thread was already in the apartment we asked for. /// `CoUninitialize` on the way out anyway -- the reference count is per /// call, and skipping it here leaks an apartment reference that keeps /// COM loaded for the life of the process. already,
/// `RPC_E_CHANGED_MODE`: the thread is in a *different* apartment, /// which for a library means the host application got there first -- /// any GUI application's main thread is in a single-threaded apartment. /// /// This is a success, not a failure. Every object this library creates /// is in-process and usable from an STA; what is not allowed is /// `CoUninitialize`, because this call added no reference and undoing /// somebody else's initialisation would break them. /// /// This is the *common* case for `Session.open` in a GUI application, /// so it is emphatically not an edge case to be rejected. borrowed, };
/// Put the current thread in an apartment, or note that it is already in /// one. pub fn enter(which: win32.COINIT) errors.Error!Apartment { const hr = win32.CoInitializeEx(null, which); if (eql(hr, hresult.s_ok)) return .{ .state = .initialised }; if (eql(hr, hresult.s_false)) return .{ .state = .already }; if (eql(hr, win32.RPC_E_CHANGED_MODE)) return .{ .state = .borrowed }; try check("CoInitializeEx", hr); unreachable; // `check` returns an error for every `hr` not handled above. }
/// Undo `enter`, if there is anything to undo. pub fn leave(self: *Apartment) void { switch (self.state) { .initialised, .already => win32.CoUninitialize(), .borrowed => {}, } self.* = undefined; }};
/// `CoCreateInstance`, typed.////// Wraps the `**anyopaque` out-parameter, which is the shape every COM creation/// and activation function has in these bindings and which is easy to get/// subtly wrong -- `@ptrCast(&ptr)` where `ptr` is already a pointer is not the/// same as `&ptr`, and the compiler accepts both.pub fn create(comptime T: type, clsid: *const Guid, iid: *const Guid) errors.Error!*T { var ptr: ?*T = null; try check("CoCreateInstance", win32.CoCreateInstance( clsid, null, clsctx_all, iid, @ptrCast(&ptr), )); return ptr orelse { // A COM object that reports success and hands back null is broken, but // the type system cannot rule it out and a null dereference in a render // loop is a poor way to find out. log.warn("CoCreateInstance succeeded but returned null", .{}); return error.Unexpected; };}
/// `QueryInterface`, typed, for casting an interface this library holds to/// another it implements.pub fn queryInterface(comptime T: type, obj: anytype, iid: *const Guid) errors.Error!*T { var ptr: ?*T = null; const hr = obj.IUnknown.QueryInterface(iid, @ptrCast(&ptr)); if (failed(hr)) return classifyOnly(hr); return ptr orelse error.Unexpected;}
/// `Release`, for any of these bindings' interfaces.////// Takes a nullable so that a teardown path can release whatever it managed to/// acquire without a null check per interface, which is the shape the reopen/// path in `Stream` wants.pub fn release(obj: anytype) void { switch (@typeInfo(@TypeOf(obj))) { .optional => if (obj) |o| { _ = o.IUnknown.Release(); }, else => _ = obj.IUnknown.Release(), }}
/// Whether two GUIDs are the same.////// These bindings' `Guid` is an `extern union` of an integer layout and sixteen/// bytes, with no equality function of its own, so this compares the bytes --/// which is well defined for a union where both members are the same sixteen/// bytes read two ways.pub fn guidEql(a: *const Guid, b: *const Guid) bool { return std.mem.eql(u8, &a.Bytes, &b.Bytes);}
/// A string COM allocated and this library must free.////// A type rather than a bare pointer so that `defer s.deinit()` is available/// and so that the `CoTaskMemFree` cannot be forgotten -- every endpoint id and/// every device format comes back this way, and the leak is silent.pub const TaskMem = struct { ptr: ?*anyopaque,
pub fn deinit(self: *TaskMem) void { if (self.ptr) |p| win32.CoTaskMemFree(p); self.* = undefined; }};
/// Copy a null-terminated UTF-16 string into `out` as UTF-8.////// Returns the prefix of `out` that was written. `error.NameTooLong` rather than/// a truncation: an endpoint id that does not fit is one this library cannot/// later hand back to `IMMDeviceEnumerator.GetDevice`, and half an id is worse/// than an error because it would silently name a different device -- or none,/// which the caller would then report as the device having gone away.////// Encodes a code point at a time rather than calling/// `std.unicode.utf16LeToUtf8`, which documents that it *asserts* the output/// buffer is big enough and so crashes rather than failing when it is not. The/// alternative would be to size `out` at three bytes per code unit and never be/// wrong, which for a 256-character endpoint id means a 768-byte buffer on a/// render thread's stack to hold a name that is almost always ASCII.pub fn utf8FromWide(out: []u8, wide: [*:0]const u16) error{NameTooLong}![]const u8 { var written: usize = 0; var units: std.unicode.Utf16LeIterator = .init(wide[0..std.mem.len(wide)]); while (units.nextCodepoint() catch return error.NameTooLong) |codepoint| { const need = std.unicode.utf8CodepointSequenceLength(codepoint) catch return error.NameTooLong; if (written + need > out.len) return error.NameTooLong; written += std.unicode.utf8Encode(codepoint, out[written..]) catch return error.NameTooLong; } return out[0..written];}
/// Read one string property out of a device's property store.////// Null when the device has no value for that key, which is a real answer: not/// every endpoint has a friendly name, and the caller should fall back to/// another key rather than treat it as a failure.////// ## Why this is the only place `PROPVARIANT` appears////// These bindings represent it as three levels of anonymous union, so the tag/// is at `pv.Anonymous.Anonymous.vt` and the string at/// `pv.Anonymous.Anonymous.Anonymous.pwszVal`. Worse, it must be zeroed before/// the call -- `GetValue` does not fully initialise it, and reading the union/// of an uninitialised one is how the upstream zigwin32 WASAPI example crashes./// One helper, zeroing and clearing correctly, is cheaper than remembering that/// at four call sites.pub fn propString( store: *win32.IPropertyStore, key: *const win32.PROPERTYKEY, out: []u8,) (errors.Error || error{NameTooLong})!?[]const u8 { var pv: win32.PROPVARIANT = std.mem.zeroes(win32.PROPVARIANT); try check("IPropertyStore::GetValue", store.GetValue(key, &pv)); defer _ = win32.PropVariantClear(&pv);
if (pv.Anonymous.Anonymous.vt != win32.VT_LPWSTR) return null; const wide = pv.Anonymous.Anonymous.Anonymous.pwszVal orelse return null; return try utf8FromWide(out, wide);}
test "the multithreaded apartment is the all-zeroes COINIT" { // The bug this constant exists to prevent: `COINIT` names // `APARTMENTTHREADED` as a bit, so an "explicit" multithreaded literal is // an STA. If these two ever compare equal, something has renamed a field // and every thread in this library is in the wrong apartment. try std.testing.expectEqual(@as(u32, 0), @as(u32, @bitCast(coinit_multithreaded))); try std.testing.expect( @as(u32, @bitCast(coinit_multithreaded)) != @as(u32, @bitCast(coinit_apartmentthreaded)), );}
test "a borrowed apartment is not uninitialised on the way out" { // The distinction that keeps this library from breaking a GUI host: three // states, and only two of them balance the call. var borrowed: Apartment = .{ .state = .borrowed }; borrowed.leave(); // Must not call CoUninitialize; reaching here is the test.
// And the two that do are distinguishable from it. try std.testing.expect(Apartment.State.initialised != Apartment.State.borrowed); try std.testing.expect(Apartment.State.already != Apartment.State.borrowed);}
test "entering and leaving the multithreaded apartment works on a fresh thread" { // The test runner's thread may already be in an apartment, so this says // nothing about which state comes back -- only that every state `enter` can // return is one `leave` accepts. var apartment: Apartment = try .enter(coinit_multithreaded); apartment.leave();}
test "guids compare by value" { const a = Guid.initString("00000003-0000-0010-8000-00aa00389b71"); const b = Guid.initString("00000003-0000-0010-8000-00aa00389b71"); const c = Guid.initString("00000001-0000-0010-8000-00aa00389b71"); try std.testing.expect(guidEql(&a, &b)); try std.testing.expect(!guidEql(&a, &c));}
test "a wide string too long for the buffer is an error rather than a truncation" { // Half an endpoint id would name a different device, or no device, and // `GetDevice` would be asked for it in either case. const wide = std.unicode.utf8ToUtf16LeStringLiteral("a rather long endpoint identifier"); var small: [8]u8 = undefined; try std.testing.expectError(error.NameTooLong, utf8FromWide(&small, wide));
var big: [64]u8 = undefined; try std.testing.expectEqualStrings( "a rather long endpoint identifier", try utf8FromWide(&big, wide), );}
test { std.testing.refAllDecls(@This());}