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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The constants a `WAVEFORMATEXTENSIBLE` needs, gathered under the names//! Microsoft uses for them.//!//! Not one of these is missing from `zigwin32`. They are, however, spread across//! four of its namespaces and two of them are renamed on the way, so a reader//! who knows what `KSDATAFORMAT_SUBTYPE_PCM` is will not find it by looking//! where it ought to be. This file is the translation table, and it exists so//! that `format.zig` reads like the documentation it implements://!//! | what the headers call it | where `zigwin32` filed it |//! |---|---|//! | `WAVE_FORMAT_PCM` | `media.audio` |//! | `WAVE_FORMAT_IEEE_FLOAT` | `media.multimedia` |//! | `WAVE_FORMAT_EXTENSIBLE` | `media.kernel_streaming`, and as a `u32` |//! | `KSDATAFORMAT_SUBTYPE_PCM` | `media.kernel_streaming`, as `CLSID_…` |//! | `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT` | `media.multimedia`, as `CLSID_…` |//! | `SPEAKER_*` | `media.kernel_streaming` |//!//! The `CLSID_` prefix on the two subformat GUIDs is the binding generator's//! doing: it prefixes every GUID that is not an interface id that way, and a//! `KSDATAFORMAT_SUBTYPE_*` is a format identifier rather than a class id. They//! are aliased here under their real names rather than used through the//! generated ones, because a `CLSID_` that is not a class id misleads at every//! call site it appears in.
const std = @import("std");
const win32 = @import("win32").everything;const Guid = win32.Guid;
/// `WAVE_FORMAT_PCM`, for a `WAVEFORMATEX.wFormatTag`.pub const wave_format_pcm: u16 = win32.WAVE_FORMAT_PCM;
/// `WAVE_FORMAT_IEEE_FLOAT`, for a `WAVEFORMATEX.wFormatTag`.pub const wave_format_ieee_float: u16 = @intCast(win32.WAVE_FORMAT_IEEE_FLOAT);
/// `WAVE_FORMAT_EXTENSIBLE` (0xFFFE), for a `WAVEFORMATEX.wFormatTag`.////// Narrowed from the `u32` the bindings declare, because the field it goes in is/// a `u16`. The `@intCast` is here, once, rather than at each use.pub const wave_format_extensible: u16 = @intCast(win32.WAVE_FORMAT_EXTENSIBLE);
/// `KSDATAFORMAT_SUBTYPE_PCM`, for a `WAVEFORMATEXTENSIBLE.SubFormat`.pub const ksdataformat_subtype_pcm: *const Guid = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM;
/// `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT`, for a `WAVEFORMATEXTENSIBLE.SubFormat`.pub const ksdataformat_subtype_ieee_float: *const Guid = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
/// The speaker-position bits of a `dwChannelMask`, in ascending bit order --/// which is also the order the samples of a frame must be interleaved in.////// Gathered into an enum rather than left as eleven loose `u32` constants/// because the *order* is the part that matters and a list of constants does not/// show it. `channel.zig` iterates this to build a wire permutation, and the/// declaration order here is load-bearing for that.pub const Speaker = enum(u32) { front_left = win32.SPEAKER_FRONT_LEFT, front_right = win32.SPEAKER_FRONT_RIGHT, front_center = win32.SPEAKER_FRONT_CENTER, low_frequency = win32.SPEAKER_LOW_FREQUENCY, back_left = win32.SPEAKER_BACK_LEFT, back_right = win32.SPEAKER_BACK_RIGHT, front_left_of_center = win32.SPEAKER_FRONT_LEFT_OF_CENTER, front_right_of_center = win32.SPEAKER_FRONT_RIGHT_OF_CENTER, back_center = win32.SPEAKER_BACK_CENTER, side_left = win32.SPEAKER_SIDE_LEFT, side_right = win32.SPEAKER_SIDE_RIGHT,};
test "the subformat guids are the ones the headers document" { // These are aliases through two different namespaces under two different // names, so an upstream rename would silently point them elsewhere. The // byte patterns are from `ksmedia.h` and are not going to change. const expect_float = Guid.initString("00000003-0000-0010-8000-00aa00389b71"); const expect_pcm = Guid.initString("00000001-0000-0010-8000-00aa00389b71");
try std.testing.expectEqualSlices( u8, &expect_float.Bytes, &ksdataformat_subtype_ieee_float.Bytes, ); try std.testing.expectEqualSlices(u8, &expect_pcm.Bytes, &ksdataformat_subtype_pcm.Bytes);}
test "the format tags are the documented numbers" { try std.testing.expectEqual(@as(u16, 0x0001), wave_format_pcm); try std.testing.expectEqual(@as(u16, 0x0003), wave_format_ieee_float); try std.testing.expectEqual(@as(u16, 0xFFFE), wave_format_extensible);}
test "the speaker bits ascend in declaration order" { // `channel.zig` builds the interleaving order by walking this enum, so a // reordering here would silently produce audio in the wrong channels -- // which is the one class of bug a listener catches and a test usually does // not. This is that test. var previous: u32 = 0; for (std.enums.values(Speaker)) |speaker| { const bit = @backingInt(speaker); try std.testing.expect(bit > previous); previous = bit; }
// And the low four are the ones every layout starts with. try std.testing.expectEqual(@as(u32, 0x1), @backingInt(Speaker.front_left)); try std.testing.expectEqual(@as(u32, 0x2), @backingInt(Speaker.front_right)); try std.testing.expectEqual(@as(u32, 0x4), @backingInt(Speaker.front_center)); try std.testing.expectEqual(@as(u32, 0x8), @backingInt(Speaker.low_frequency));}
test "channel.zig's hand-written speaker bits are the ones zigwin32 has" { // `channel.zig` is in the half of the library with no operating system in // it, so it cannot import these constants and writes the numbers out // instead. This is the test that makes that safe: a typo there fails the // Windows build here rather than quietly moving somebody's centre channel. // // It lives in this file rather than in `channel.zig` because this is the // file that is allowed to know what `zigwin32` calls things. const channel = @import("../channel.zig");
const pairs = .{ .{ channel.Channel.fl, win32.SPEAKER_FRONT_LEFT }, .{ channel.Channel.fr, win32.SPEAKER_FRONT_RIGHT }, .{ channel.Channel.fc, win32.SPEAKER_FRONT_CENTER }, .{ channel.Channel.lfe, win32.SPEAKER_LOW_FREQUENCY }, .{ channel.Channel.rl, win32.SPEAKER_BACK_LEFT }, .{ channel.Channel.rr, win32.SPEAKER_BACK_RIGHT }, .{ channel.Channel.flc, win32.SPEAKER_FRONT_LEFT_OF_CENTER }, .{ channel.Channel.frc, win32.SPEAKER_FRONT_RIGHT_OF_CENTER }, .{ channel.Channel.rc, win32.SPEAKER_BACK_CENTER }, .{ channel.Channel.sl, win32.SPEAKER_SIDE_LEFT }, .{ channel.Channel.sr, win32.SPEAKER_SIDE_RIGHT }, // Windows has no separate mono position; mono is the centre channel. .{ channel.Channel.mono, win32.SPEAKER_FRONT_CENTER }, };
inline for (pairs) |pair| { try std.testing.expectEqual(@as(u32, pair[1]), pair[0].mask()); }}
test "format.zig's hand-written subtype bytes are zigwin32's guids" { // Same arrangement as the speaker bits: `format.zig` has to stay free of any // operating system so its encoder can be tested on a Linux host, so it spells // the two `KSDATAFORMAT_SUBTYPE_*` GUIDs out as bytes. This is what proves // the byte order -- a GUID's mixed-endianness is exactly the kind of thing to // get subtly wrong, and the failure mode is that every format this library // offers comes back `AUDCLNT_E_UNSUPPORTED_FORMAT` with no further hint. const format = @import("../format.zig");
try std.testing.expectEqualSlices( u8, &ksdataformat_subtype_ieee_float.Bytes, &format.subtype_ieee_float, ); try std.testing.expectEqualSlices( u8, &ksdataformat_subtype_pcm.Bytes, &format.subtype_pcm, );}
test "format.zig's hand-written format tags are zigwin32's" { const format = @import("../format.zig"); try std.testing.expectEqual(wave_format_pcm, format.wave_format_pcm); try std.testing.expectEqual(wave_format_ieee_float, format.wave_format_ieee_float); try std.testing.expectEqual(wave_format_extensible, format.wave_format_extensible);}
test "format.zig's structure sizes match the bindings' own structs" { // The encoder writes 18 and 40 bytes because that is what the headers say. // If `zigwin32`'s structs ever disagree, the `@ptrCast` that hands these // bytes to WASAPI would be reading a different shape than it wrote. const format = @import("../format.zig"); try std.testing.expectEqual(@sizeOf(win32.WAVEFORMATEX), format.base_len); try std.testing.expectEqual(@sizeOf(win32.WAVEFORMATEXTENSIBLE), format.extensible_len); try std.testing.expectEqual( @sizeOf(win32.WAVEFORMATEXTENSIBLE) - @sizeOf(win32.WAVEFORMATEX), format.extension_len, );
// And both are byte-aligned, which is what makes the cast legal in either // direction with no `@alignCast`. try std.testing.expectEqual(@as(u29, 1), @alignOf(win32.WAVEFORMATEX)); try std.testing.expectEqual(@as(u29, 1), @alignOf(win32.WAVEFORMATEXTENSIBLE));}
test "an encoded format is readable through the bindings' own struct" { // The end-to-end check that the pure encoder and the real structure agree: // encode as bytes, read back through `WAVEFORMATEXTENSIBLE`, and compare // field by field. This is the closest a test can get to what WASAPI does // with the pointer. const format = @import("../format.zig"); const fmt: format.Format = .{ .rate = 48000, .channels = 6, .sample = .f32 };
var bytes: [format.extensible_len]u8 align(8) = undefined; _ = fmt.encodeExtensible(&bytes);
const ext: *const win32.WAVEFORMATEXTENSIBLE = @ptrCast(&bytes); try std.testing.expectEqual(wave_format_extensible, ext.Format.wFormatTag); try std.testing.expectEqual(@as(u16, 6), ext.Format.nChannels); try std.testing.expectEqual(@as(u32, 48000), ext.Format.nSamplesPerSec); try std.testing.expectEqual(@as(u16, 24), ext.Format.nBlockAlign); try std.testing.expectEqual(@as(u16, 32), ext.Format.wBitsPerSample); try std.testing.expectEqual(@as(u16, 22), ext.Format.cbSize); try std.testing.expectEqual(@as(u16, 32), ext.Samples.wValidBitsPerSample); try std.testing.expectEqual(@as(u32, 0x3F), ext.dwChannelMask); try std.testing.expectEqualSlices( u8, &ksdataformat_subtype_ieee_float.Bytes, &ext.SubFormat.Bytes, );
// And the plain `WAVEFORMATEX` view of the same bytes, which is the pointer // type every WASAPI entry point actually takes. const base: *const win32.WAVEFORMATEX = @ptrCast(&bytes); try std.testing.expectEqual(ext.Format.nChannels, base.nChannels);}
test "the Speaker enum here and channel.zig's order agree" { // Two lists of the same eleven bits in the same order, in two files that // cannot import each other. This asserts they have not drifted. const channel = @import("../channel.zig"); var buf: [channel.max_channels]channel.Channel = undefined;
var every_bit: u32 = 0; for (std.enums.values(Speaker)) |speaker| every_bit |= @backingInt(speaker);
const decoded = try channel.mapFromMask(every_bit, &buf); try std.testing.expectEqual(std.enums.values(Speaker).len, decoded.len); for (std.enums.values(Speaker), decoded) |speaker, ch| { try std.testing.expectEqual(@as(u32, @backingInt(speaker)), ch.mask()); }}
test { std.testing.refAllDecls(@This());}