diff --git a/.gitmodules b/.gitmodules index 0a38ff00..996b801f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,9 +15,6 @@ [submodule "glfw/upstream"] path = libs/glfw/upstream url = https://github.com/hexops/glfw -[submodule "sysaudio/upstream"] - path = libs/sysaudio/upstream - url = https://github.com/hexops/soundio [submodule "basisu/upstream"] path = libs/basisu/upstream url = https://github.com/hexops/basisu diff --git a/libs/sysaudio/.gitmodules b/libs/sysaudio/.gitmodules deleted file mode 100644 index 1c514e49..00000000 --- a/libs/sysaudio/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "upstream"] - path = upstream - url = https://github.com/hexops/soundio diff --git a/libs/sysaudio/build.zig b/libs/sysaudio/build.zig index 4d07f851..a7e15679 100644 --- a/libs/sysaudio/build.zig +++ b/libs/sysaudio/build.zig @@ -15,12 +15,12 @@ pub fn build(b: *std.build.Builder) void { test_step.dependOn(&sysaudio.testStep(b, mode, target).step); inline for ([_][]const u8{ - "soundio-sine-wave", + "sine-wave", }) |example| { const example_exe = b.addExecutable("example-" ++ example, "examples/" ++ example ++ ".zig"); example_exe.setBuildMode(mode); example_exe.setTarget(target); - example_exe.addPackage(sysaudio.soundio_pkg); + example_exe.addPackage(sysaudio.pkg); sysaudio.link(b, example_exe, .{}); example_exe.install(); diff --git a/libs/sysaudio/examples/sine-wave.zig b/libs/sysaudio/examples/sine-wave.zig new file mode 100644 index 00000000..b15d49b4 --- /dev/null +++ b/libs/sysaudio/examples/sine-wave.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const sysaudio = @import("sysaudio"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var a = try sysaudio.Context.init(null, allocator, .{ .deviceChangeFn = deviceChange }); + defer a.deinit(); + try a.refresh(); + + const device = a.defaultDevice(.playback) orelse return error.NoDevice; + + var p = try a.createPlayer(device, writeCallback, .{}); + defer p.deinit(); + try p.start(); + + try p.setVolume(0.85); + + var buf: [16]u8 = undefined; + while (true) { + std.debug.print("( paused = {}, volume = {d} )\n> ", .{ p.paused(), try p.volume() }); + const line = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')) orelse break; + var iter = std.mem.split(u8, line, ":"); + const cmd = std.mem.trimRight(u8, iter.first(), &std.ascii.whitespace); + if (std.mem.eql(u8, cmd, "vol")) { + var vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace)); + try p.setVolume(vol); + } else if (std.mem.eql(u8, cmd, "pause")) { + try p.pause(); + try std.testing.expect(p.paused()); + } else if (std.mem.eql(u8, cmd, "play")) { + try p.play(); + try std.testing.expect(!p.paused()); + } else if (std.mem.eql(u8, cmd, "exit")) { + break; + } + } +} + +const pitch = 440.0; +const radians_per_second = pitch * 2.0 * std.math.pi; +var seconds_offset: f32 = 0.0; +fn writeCallback(self_opaque: *anyopaque, n_frame: usize) void { + var self = @ptrCast(*sysaudio.Player, @alignCast(@alignOf(sysaudio.Player), self_opaque)); + + const seconds_per_frame = 1.0 / @intToFloat(f32, self.sampleRate()); + var frame: usize = 0; + while (frame < n_frame) : (frame += 1) { + const sample = std.math.sin((seconds_offset + @intToFloat(f32, frame) * seconds_per_frame) * radians_per_second); + self.writeAll(frame, sample); + } + seconds_offset = @mod(seconds_offset + seconds_per_frame * @intToFloat(f32, n_frame), 1.0); +} + +fn deviceChange(self: ?*anyopaque) void { + _ = self; + std.debug.print("Device change detected!\n", .{}); +} diff --git a/libs/sysaudio/examples/soundio-sine-wave.zig b/libs/sysaudio/examples/soundio-sine-wave.zig deleted file mode 100644 index 933bd198..00000000 --- a/libs/sysaudio/examples/soundio-sine-wave.zig +++ /dev/null @@ -1,76 +0,0 @@ -const std = @import("std"); -const soundio = @import("soundio"); -const c = soundio.c; -const SoundIo = soundio.SoundIo; -const OutStream = soundio.OutStream; - -var seconds_offset: f32 = 0; - -fn write_callback( - maybe_outstream: ?[*]c.SoundIoOutStream, - frame_count_min: c_int, - frame_count_max: c_int, -) callconv(.C) void { - _ = frame_count_min; - const outstream = OutStream{ .handle = @ptrCast(*c.SoundIoOutStream, maybe_outstream) }; - const layout = outstream.layout(); - const float_sample_rate = outstream.sampleRate(); - const seconds_per_frame = 1.0 / @intToFloat(f32, float_sample_rate); - var frames_left = frame_count_max; - - while (frames_left > 0) { - var frame_count = frames_left; - - var areas: [*]c.SoundIoChannelArea = undefined; - outstream.beginWrite( - @ptrCast([*]?[*]c.SoundIoChannelArea, &areas), - &frame_count, - ) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)}); - - if (frame_count == 0) break; - - const pitch = 440.0; - const radians_per_second = pitch * 2.0 * std.math.pi; - var frame: c_int = 0; - while (frame < frame_count) : (frame += 1) { - const sample = std.math.sin((seconds_offset + @intToFloat(f32, frame) * - seconds_per_frame) * radians_per_second); - { - var channel: usize = 0; - while (channel < @intCast(usize, layout.channelCount())) : (channel += 1) { - const channel_ptr = areas[channel].ptr; - const sample_ptr = &channel_ptr[@intCast(usize, areas[channel].step * frame)]; - @ptrCast(*f32, @alignCast(@alignOf(f32), sample_ptr)).* = sample; - } - } - } - seconds_offset += seconds_per_frame * @intToFloat(f32, frame_count); - outstream.endWrite() catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)}); - frames_left -= frame_count; - } -} - -pub fn main() !void { - const sio = try SoundIo.init(); - defer sio.deinit(); - try sio.connect(); - sio.flushEvents(); - - const default_output_index = sio.defaultOutputDeviceIndex() orelse return error.NoOutputDeviceFound; - - const device = sio.getOutputDevice(default_output_index) orelse return error.OutOfMemory; - defer device.unref(); - - std.debug.print("Output device: {s}\n", .{device.name()}); - - const outstream = try device.createOutStream(); - defer outstream.deinit(); - - outstream.setFormat(.float32LE); - outstream.setWriteCallback(write_callback); - - try outstream.open(); - try outstream.start(); - - while (true) sio.waitEvents(); -} diff --git a/libs/sysaudio/sdk.zig b/libs/sysaudio/sdk.zig index f881341d..fe6ec747 100644 --- a/libs/sysaudio/sdk.zig +++ b/libs/sysaudio/sdk.zig @@ -2,17 +2,10 @@ const std = @import("std"); pub fn Sdk(comptime deps: anytype) type { return struct { - const soundio_path = sdkPath("/upstream/soundio"); - pub const pkg = std.build.Pkg{ .name = "sysaudio", .source = .{ .path = sdkPath("/src/main.zig") }, - .dependencies = &.{ deps.sysjs.pkg, soundio_pkg }, - }; - - pub const soundio_pkg = std.build.Pkg{ - .name = "soundio", - .source = .{ .path = sdkPath("/soundio/main.zig") }, + .dependencies = &.{deps.sysjs.pkg}, }; pub const Options = struct { @@ -23,82 +16,32 @@ pub fn Sdk(comptime deps: anytype) type { }; pub fn testStep(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget) *std.build.RunStep { - const soundio_tests = b.addTestExe("soundio-tests", sdkPath("/soundio/main.zig")); - soundio_tests.setBuildMode(mode); - soundio_tests.setTarget(target); - link(b, soundio_tests, .{}); - soundio_tests.install(); - const main_tests = b.addTestExe("sysaudio-tests", sdkPath("/src/main.zig")); main_tests.setBuildMode(mode); main_tests.setTarget(target); - main_tests.addPackage(soundio_pkg); link(b, main_tests, .{}); main_tests.install(); - - const main_tests_run = main_tests.run(); - main_tests_run.step.dependOn(&soundio_tests.run().step); - return main_tests_run; + return main_tests.run(); } pub fn link(b: *std.build.Builder, step: *std.build.LibExeObjStep, options: Options) void { if (step.target.toTarget().cpu.arch != .wasm32) { // TODO(build-system): pass system SDK options through - const soundio_lib = buildSoundIo(b, step.build_mode, step.target, options); - step.linkLibrary(soundio_lib); - step.addIncludePath(soundio_path); - deps.system_sdk.include(b, step, options.system_sdk); + deps.system_sdk.include(b, step, .{}); + if (step.target.toTarget().isDarwin()) { + step.linkFramework("AudioToolbox"); + step.linkFramework("CoreFoundation"); + step.linkFramework("CoreAudio"); + } else if (step.target.toTarget().os.tag == .linux) { + step.linkSystemLibrary("asound"); + step.linkSystemLibrary("pulse"); + step.linkSystemLibrary("jack"); + step.linkLibC(); + } } - } - - fn buildSoundIo(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget, options: Options) *std.build.LibExeObjStep { - // TODO(build-system): https://github.com/hexops/mach/issues/229#issuecomment-1100958939 - ensureDependencySubmodule(b.allocator, "upstream") catch unreachable; - - const config_base = - \\#ifndef SOUNDIO_CONFIG_H - \\#define SOUNDIO_CONFIG_H - \\#define SOUNDIO_VERSION_MAJOR 2 - \\#define SOUNDIO_VERSION_MINOR 0 - \\#define SOUNDIO_VERSION_PATCH 0 - \\#define SOUNDIO_VERSION_STRING "2.0.0" - \\ - ; - - var config_file = std.fs.cwd().createFile(soundio_path ++ "/src/config.h", .{}) catch unreachable; - defer config_file.close(); - config_file.writeAll(config_base) catch unreachable; - - const lib = b.addStaticLibrary("soundio", null); - lib.setBuildMode(mode); - lib.setTarget(target); - lib.linkLibC(); - lib.addIncludePath(soundio_path); - lib.addCSourceFiles(soundio_sources, &.{}); - deps.system_sdk.include(b, lib, options.system_sdk); - - const target_info = (std.zig.system.NativeTargetInfo.detect(target) catch unreachable).target; - if (target_info.isDarwin()) { - lib.addCSourceFile(soundio_path ++ "/src/coreaudio.c", &.{}); - lib.linkFramework("AudioToolbox"); - lib.linkFramework("CoreFoundation"); - lib.linkFramework("CoreAudio"); - config_file.writeAll("#define SOUNDIO_HAVE_COREAUDIO\n") catch unreachable; - } else if (target_info.os.tag == .linux) { - lib.addCSourceFile(soundio_path ++ "/src/alsa.c", &.{}); - lib.linkSystemLibrary("asound"); - config_file.writeAll("#define SOUNDIO_HAVE_ALSA\n") catch unreachable; - } else if (target_info.os.tag == .windows) { - lib.addCSourceFile(soundio_path ++ "/src/wasapi.c", &.{}); - lib.linkSystemLibrary("ole32"); - config_file.writeAll("#define SOUNDIO_HAVE_WASAPI\n") catch unreachable; + if (options.install_libs) { + step.install(); } - - config_file.writeAll("#endif\n") catch unreachable; - - if (options.install_libs) - lib.install(); - return lib; } fn ensureDependencySubmodule(allocator: std.mem.Allocator, path: []const u8) !void { @@ -121,14 +64,5 @@ pub fn Sdk(comptime deps: anytype) type { break :blk root_dir ++ suffix; }; } - - const soundio_sources = &[_][]const u8{ - soundio_path ++ "/src/soundio.c", - soundio_path ++ "/src/util.c", - soundio_path ++ "/src/os.c", - soundio_path ++ "/src/dummy.c", - soundio_path ++ "/src/channel_layout.c", - soundio_path ++ "/src/ring_buffer.c", - }; }; } diff --git a/libs/sysaudio/soundio/ChannelLayout.zig b/libs/sysaudio/soundio/ChannelLayout.zig deleted file mode 100644 index e31c8aea..00000000 --- a/libs/sysaudio/soundio/ChannelLayout.zig +++ /dev/null @@ -1,11 +0,0 @@ -const c = @import("c.zig"); -const intToError = @import("error.zig").intToError; -const Error = @import("error.zig").Error; - -const ChannelLayout = @This(); - -handle: c.SoundIoChannelLayout, - -pub fn channelCount(self: ChannelLayout) i32 { - return self.handle.channel_count; -} diff --git a/libs/sysaudio/soundio/Device.zig b/libs/sysaudio/soundio/Device.zig deleted file mode 100644 index c619d1d1..00000000 --- a/libs/sysaudio/soundio/Device.zig +++ /dev/null @@ -1,33 +0,0 @@ -const std = @import("std"); -const c = @import("c.zig"); -const InStream = @import("InStream.zig"); -const OutStream = @import("OutStream.zig"); -const Format = @import("enums.zig").Format; - -const Device = @This(); - -handle: *c.SoundIoDevice, - -pub fn unref(self: Device) void { - c.soundio_device_unref(self.handle); -} - -pub fn id(self: Device) [:0]const u8 { - return std.mem.span(self.handle.*.id); -} - -pub fn name(self: Device) [:0]const u8 { - return std.mem.span(self.handle.*.name); -} - -pub fn createInStream(self: Device) error{OutOfMemory}!InStream { - return InStream{ .handle = c.soundio_instream_create(self.handle) orelse return error.OutOfMemory }; -} - -pub fn createOutStream(self: Device) error{OutOfMemory}!OutStream { - return OutStream{ .handle = c.soundio_outstream_create(self.handle) orelse return error.OutOfMemory }; -} - -pub fn supportsFormat(self: Device, format: Format) bool { - return c.soundio_device_supports_format(self.handle, @enumToInt(format)); -} diff --git a/libs/sysaudio/soundio/InStream.zig b/libs/sysaudio/soundio/InStream.zig deleted file mode 100644 index aa1dc21c..00000000 --- a/libs/sysaudio/soundio/InStream.zig +++ /dev/null @@ -1,59 +0,0 @@ -const c = @import("c.zig"); -const intToError = @import("error.zig").intToError; -const Error = @import("error.zig").Error; -const Format = @import("enums.zig").Format; -const ChannelLayout = @import("ChannelLayout.zig"); - -const InStream = @This(); - -pub const WriteCallback = *const fn (stream: ?[*]c.SoundIoInStream, frame_count_min: c_int, frame_count_max: c_int) callconv(.C) void; - -handle: *c.SoundIoInStream, - -pub fn deinit(self: InStream) void { - c.soundio_instream_destroy(self.handle); -} - -pub fn open(self: InStream) Error!void { - try intToError(c.soundio_instream_open(self.handle)); -} - -pub fn start(self: InStream) Error!void { - try intToError(c.soundio_instream_start(self.handle)); -} - -pub fn pause(self: InStream, pause_state: bool) Error!void { - try intToError(c.soundio_instream_pause(self.handle, pause_state)); -} - -pub fn beginWrite(self: InStream, areas: [*]?[*]c.SoundIoChannelArea, frame_count: *i32) Error!void { - try intToError(c.soundio_instream_begin_write( - self.handle, - areas, - frame_count, - )); -} - -pub fn endWrite(self: InStream) Error!void { - try intToError(c.soundio_instream_end_write(self.handle)); -} - -pub fn setFormat(self: InStream, format: Format) void { - self.handle.*.format = @enumToInt(format); -} - -pub fn setWriteCallback(self: InStream, callback: WriteCallback) void { - self.handle.*.write_callback = callback; -} - -pub fn layout(self: InStream) ChannelLayout { - return ChannelLayout{ .handle = self.handle.*.layout }; -} - -pub fn sampleRate(self: InStream) i32 { - return self.handle.*.sample_rate; -} - -pub fn layoutError(self: InStream) Error!void { - try intToError(self.handle.*.layout_error); -} diff --git a/libs/sysaudio/soundio/OutStream.zig b/libs/sysaudio/soundio/OutStream.zig deleted file mode 100644 index e03ea3be..00000000 --- a/libs/sysaudio/soundio/OutStream.zig +++ /dev/null @@ -1,59 +0,0 @@ -const c = @import("c.zig"); -const intToError = @import("error.zig").intToError; -const Error = @import("error.zig").Error; -const Format = @import("enums.zig").Format; -const ChannelLayout = @import("ChannelLayout.zig"); - -const OutStream = @This(); - -pub const WriteCallback = *const fn (stream: ?[*]c.SoundIoOutStream, frame_count_min: c_int, frame_count_max: c_int) callconv(.C) void; - -handle: *c.SoundIoOutStream, - -pub fn deinit(self: OutStream) void { - c.soundio_outstream_destroy(self.handle); -} - -pub fn open(self: OutStream) Error!void { - try intToError(c.soundio_outstream_open(self.handle)); -} - -pub fn start(self: OutStream) Error!void { - try intToError(c.soundio_outstream_start(self.handle)); -} - -pub fn pause(self: OutStream, pause_state: bool) Error!void { - try intToError(c.soundio_outstream_pause(self.handle, pause_state)); -} - -pub fn beginWrite(self: OutStream, areas: [*]?[*]c.SoundIoChannelArea, frame_count: *i32) Error!void { - try intToError(c.soundio_outstream_begin_write( - self.handle, - areas, - frame_count, - )); -} - -pub fn endWrite(self: OutStream) Error!void { - try intToError(c.soundio_outstream_end_write(self.handle)); -} - -pub fn setFormat(self: OutStream, format: Format) void { - self.handle.*.format = @enumToInt(format); -} - -pub fn setWriteCallback(self: OutStream, callback: WriteCallback) void { - self.handle.*.write_callback = callback; -} - -pub fn layout(self: OutStream) ChannelLayout { - return ChannelLayout{ .handle = self.handle.*.layout }; -} - -pub fn sampleRate(self: OutStream) i32 { - return self.handle.*.sample_rate; -} - -pub fn layoutError(self: OutStream) Error!void { - try intToError(self.handle.*.layout_error); -} diff --git a/libs/sysaudio/soundio/SoundIo.zig b/libs/sysaudio/soundio/SoundIo.zig deleted file mode 100644 index 18b60e29..00000000 --- a/libs/sysaudio/soundio/SoundIo.zig +++ /dev/null @@ -1,86 +0,0 @@ -const c = @import("c.zig"); -const intToError = @import("error.zig").intToError; -const Error = @import("error.zig").Error; -const Aim = @import("enums.zig").Aim; -const Backend = @import("enums.zig").Backend; -const Device = @import("Device.zig"); - -const SoundIo = @This(); - -handle: *c.SoundIo, - -pub fn init() error{OutOfMemory}!SoundIo { - return SoundIo{ .handle = c.soundio_create() orelse return error.OutOfMemory }; -} - -pub fn deinit(self: SoundIo) void { - c.soundio_destroy(self.handle); -} - -pub fn connect(self: SoundIo) Error!void { - try intToError(c.soundio_connect(self.handle)); -} - -pub fn connectBackend(self: SoundIo, backend: Backend) Error!void { - try intToError(c.soundio_connect_backend(self.handle, @enumToInt(backend))); -} - -pub fn disconnect(self: SoundIo) void { - c.soundio_disconnect(self.handle); -} - -pub fn flushEvents(self: SoundIo) void { - c.soundio_flush_events(self.handle); -} - -pub fn waitEvents(self: SoundIo) void { - c.soundio_wait_events(self.handle); -} - -pub fn wakeup(self: SoundIo) void { - c.soundio_wakeup(self.handle); -} - -pub fn defaultInputDeviceIndex(self: SoundIo) ?u16 { - const index = c.soundio_default_input_device_index(self.handle); - return if (index < 0) null else @intCast(u16, index); -} - -pub fn defaultOutputDeviceIndex(self: SoundIo) ?u16 { - const index = c.soundio_default_output_device_index(self.handle); - return if (index < 0) null else @intCast(u16, index); -} - -pub fn inputDeviceCount(self: SoundIo) ?u16 { - const count = c.soundio_input_device_count(self.handle); - return if (count < 0) null else @intCast(u16, count); -} - -pub fn outputDeviceCount(self: SoundIo) ?u16 { - const count = c.soundio_output_device_count(self.handle); - return if (count < 0) null else @intCast(u16, count); -} - -pub fn getInputDevice(self: SoundIo, index: u16) ?Device { - return Device{ - .handle = c.soundio_get_input_device(self.handle, index) orelse return null, - }; -} - -pub fn getOutputDevice(self: SoundIo, index: u16) ?Device { - return Device{ - .handle = c.soundio_get_output_device(self.handle, index) orelse return null, - }; -} - -pub fn getInputDeviceFromID(self: SoundIo, id: [:0]const u8, is_raw: bool) ?Device { - return Device{ - .handle = c.soundio_get_input_device_from_id(self.handle, id.ptr, is_raw) orelse return null, - }; -} - -pub fn getOutputDeviceFromID(self: SoundIo, id: [:0]const u8, is_raw: bool) ?Device { - return Device{ - .handle = c.soundio_get_output_device_from_id(self.handle, id.ptr, is_raw) orelse return null, - }; -} diff --git a/libs/sysaudio/soundio/c.zig b/libs/sysaudio/soundio/c.zig deleted file mode 100644 index 8f3b9fe8..00000000 --- a/libs/sysaudio/soundio/c.zig +++ /dev/null @@ -1,3 +0,0 @@ -pub usingnamespace @cImport({ - @cInclude("soundio/soundio.h"); -}); diff --git a/libs/sysaudio/soundio/enums.zig b/libs/sysaudio/soundio/enums.zig deleted file mode 100644 index 20504459..00000000 --- a/libs/sysaudio/soundio/enums.zig +++ /dev/null @@ -1,149 +0,0 @@ -const c = @import("c.zig"); - -pub const ChannelId = enum(u7) { - invalid = c.SoundIoChannelIdInvalid, - - front_left = c.SoundIoChannelIdFrontLeft, - front_right = c.SoundIoChannelIdFrontRight, - front_center = c.SoundIoChannelIdFrontCenter, - lfe = c.SoundIoChannelIdLfe, - back_left = c.SoundIoChannelIdBackLeft, - back_right = c.SoundIoChannelIdBackRight, - front_left_center = c.SoundIoChannelIdFrontLeftCenter, - front_right_center = c.SoundIoChannelIdFrontRightCenter, - back_center = c.SoundIoChannelIdBackCenter, - side_left = c.SoundIoChannelIdSideLeft, - side_right = c.SoundIoChannelIdSideRight, - top_center = c.SoundIoChannelIdTopCenter, - top_front_left = c.SoundIoChannelIdTopFrontLeft, - top_front_center = c.SoundIoChannelIdTopFrontCenter, - top_front_right = c.SoundIoChannelIdTopFrontRight, - top_back_left = c.SoundIoChannelIdTopBackLeft, - top_back_center = c.SoundIoChannelIdTopBackCenter, - top_back_right = c.SoundIoChannelIdTopBackRight, - - back_left_center = c.SoundIoChannelIdBackLeftCenter, - back_right_center = c.SoundIoChannelIdBackRightCenter, - front_left_wide = c.SoundIoChannelIdFrontLeftWide, - front_right_wide = c.SoundIoChannelIdFrontRightWide, - front_left_high = c.SoundIoChannelIdFrontLeftHigh, - front_center_high = c.SoundIoChannelIdFrontCenterHigh, - front_right_high = c.SoundIoChannelIdFrontRightHigh, - top_front_left_center = c.SoundIoChannelIdTopFrontLeftCenter, - top_front_right_center = c.SoundIoChannelIdTopFrontRightCenter, - top_side_left = c.SoundIoChannelIdTopSideLeft, - top_side_right = c.SoundIoChannelIdTopSideRight, - left_lfe = c.SoundIoChannelIdLeftLfe, - right_lfe = c.SoundIoChannelIdRightLfe, - lfe2 = c.SoundIoChannelIdLfe2, - bottom_center = c.SoundIoChannelIdBottomCenter, - bottom_left_center = c.SoundIoChannelIdBottomLeftCenter, - bottom_right_center = c.SoundIoChannelIdBottomRightCenter, - - // Mid/side recording - ms_mid = c.SoundIoChannelIdMsMid, - ms_side = c.SoundIoChannelIdMsSide, - - // first order ambisonic channels - ambisonic_w = c.SoundIoChannelIdAmbisonicW, - ambisonic_x = c.SoundIoChannelIdAmbisonicX, - ambisonic_y = c.SoundIoChannelIdAmbisonicY, - ambisonic_z = c.SoundIoChannelIdAmbisonicZ, - - // X-Y Recording - x_y_x = c.SoundIoChannelIdXyX, - x_y_y = c.SoundIoChannelIdXyY, - - headphones_left = c.SoundIoChannelIdHeadphonesLeft, - headphones_right = c.SoundIoChannelIdHeadphonesRight, - click_track = c.SoundIoChannelIdClickTrack, - foreign_language = c.SoundIoChannelIdForeignLanguage, - hearing_impaired = c.SoundIoChannelIdHearingImpaired, - narration = c.SoundIoChannelIdNarration, - haptic = c.SoundIoChannelIdHaptic, - dialog_centric_mix = c.SoundIoChannelIdDialogCentricMix, - - aux = c.SoundIoChannelIdAux, - aux0 = c.SoundIoChannelIdAux0, - aux1 = c.SoundIoChannelIdAux1, - aux2 = c.SoundIoChannelIdAux2, - aux3 = c.SoundIoChannelIdAux3, - aux4 = c.SoundIoChannelIdAux4, - aux5 = c.SoundIoChannelIdAux5, - aux6 = c.SoundIoChannelIdAux6, - aux7 = c.SoundIoChannelIdAux7, - aux8 = c.SoundIoChannelIdAux8, - aux9 = c.SoundIoChannelIdAux9, - aux10 = c.SoundIoChannelIdAux10, - aux11 = c.SoundIoChannelIdAux11, - aux12 = c.SoundIoChannelIdAux12, - aux13 = c.SoundIoChannelIdAux13, - aux14 = c.SoundIoChannelIdAux14, - aux15 = c.SoundIoChannelIdAux15, -}; - -pub const ChannelLayoutId = enum(u5) { - mono = c.SoundIoChannelLayoutIdMono, - stereo = c.SoundIoChannelLayoutIdStereo, - _2point1 = c.SoundIoChannelLayoutId2Point1, - _3point0 = c.SoundIoChannelLayoutId3Point0, - _3point0_back = c.SoundIoChannelLayoutId3Point0Back, - _3point1 = c.SoundIoChannelLayoutId3Point1, - _4point0 = c.SoundIoChannelLayoutId4Point0, - quad = c.SoundIoChannelLayoutIdQuad, - quadSide = c.SoundIoChannelLayoutIdQuadSide, - _4point1 = c.SoundIoChannelLayoutId4Point1, - _5point0_back = c.SoundIoChannelLayoutId5Point0Back, - _5point0_side = c.SoundIoChannelLayoutId5Point0Side, - _5point1 = c.SoundIoChannelLayoutId5Point1, - _5point1_back = c.SoundIoChannelLayoutId5Point1Back, - _6point0_side = c.SoundIoChannelLayoutId6Point0Side, - _6point0_front = c.SoundIoChannelLayoutId6Point0Front, - hexagonal = c.SoundIoChannelLayoutIdHexagonal, - _6point1 = c.SoundIoChannelLayoutId6Point1, - _6point1_back = c.SoundIoChannelLayoutId6Point1Back, - _6point1_front = c.SoundIoChannelLayoutId6Point1Front, - _7point0 = c.SoundIoChannelLayoutId7Point0, - _7point0_front = c.SoundIoChannelLayoutId7Point0Front, - _7point1 = c.SoundIoChannelLayoutId7Point1, - _7point1_wide = c.SoundIoChannelLayoutId7Point1Wide, - _7point1_wide_back = c.SoundIoChannelLayoutId7Point1WideBack, - octagonal = c.SoundIoChannelLayoutIdOctagonal, -}; - -pub const Backend = enum(u3) { - none = c.SoundIoBackendNone, - jack = c.SoundIoBackendJack, - pulseaudio = c.SoundIoBackendPulseAudio, - alsa = c.SoundIoBackendAlsa, - coreaudio = c.SoundIoBackendCoreAudio, - wasapi = c.SoundIoBackendWasapi, - dummy = c.SoundIoBackendDummy, -}; - -pub const Aim = enum(u1) { - input = c.SoundIoDeviceAimInput, - output = c.SoundIoDeviceAimOutput, -}; - -pub const Format = enum(u5) { - invalid = c.SoundIoFormatInvalid, - S8 = c.SoundIoFormatS8, - U8 = c.SoundIoFormatU8, - S16LE = c.SoundIoFormatS16LE, - S16BE = c.SoundIoFormatS16BE, - U16LE = c.SoundIoFormatU16LE, - U16BE = c.SoundIoFormatU16BE, - S24LE = c.SoundIoFormatS24LE, - S24BE = c.SoundIoFormatS24BE, - U24LE = c.SoundIoFormatU24LE, - U24BE = c.SoundIoFormatU24BE, - S32LE = c.SoundIoFormatS32LE, - S32BE = c.SoundIoFormatS32BE, - U32LE = c.SoundIoFormatU32LE, - U32BE = c.SoundIoFormatU32BE, - float32LE = c.SoundIoFormatFloat32LE, - float32BE = c.SoundIoFormatFloat32BE, - float64LE = c.SoundIoFormatFloat64LE, - float64BE = c.SoundIoFormatFloat64BE, -}; diff --git a/libs/sysaudio/soundio/error.zig b/libs/sysaudio/soundio/error.zig deleted file mode 100644 index e367f957..00000000 --- a/libs/sysaudio/soundio/error.zig +++ /dev/null @@ -1,62 +0,0 @@ -const std = @import("std"); -const c = @import("c.zig"); - -pub const Error = error{ - OutOfMemory, - /// The backend does not appear to be active or running. - InitAudioBackend, - /// A system resource other than memory was not available. - SystemResources, - /// Attempted to open a device and failed. - OpeningDevice, - NoSuchDevice, - /// The programmer did not comply with the API. - Invalid, - /// libsoundio was compiled without support for that backend. - BackendUnavailable, - /// An open stream had an error that can only be recovered from by - /// destroying the stream and creating it again. - Streaming, - /// Attempted to use a device with parameters it cannot support. - IncompatibleDevice, - /// When JACK returns `JackNoSuchClient` - NoSuchClient, - /// Attempted to use parameters that the backend cannot support. - IncompatibleBackend, - /// Backend server shutdown or became inactive. - BackendDisconnected, - Interrupted, - /// Buffer underrun occurred. - Underflow, - /// Unable to convert to or from UTF-8 to the native string format. - EncodingString, -}; - -pub fn intToError(err: c_int) Error!void { - return switch (err) { - c.SoundIoErrorNone => {}, - c.SoundIoErrorNoMem => Error.OutOfMemory, - c.SoundIoErrorInitAudioBackend => Error.InitAudioBackend, - c.SoundIoErrorSystemResources => Error.SystemResources, - c.SoundIoErrorOpeningDevice => Error.OpeningDevice, - c.SoundIoErrorNoSuchDevice => Error.NoSuchDevice, - c.SoundIoErrorInvalid => Error.Invalid, - c.SoundIoErrorBackendUnavailable => Error.BackendUnavailable, - c.SoundIoErrorStreaming => Error.Streaming, - c.SoundIoErrorIncompatibleDevice => Error.IncompatibleDevice, - c.SoundIoErrorNoSuchClient => Error.NoSuchClient, - c.SoundIoErrorIncompatibleBackend => Error.IncompatibleBackend, - c.SoundIoErrorBackendDisconnected => Error.BackendDisconnected, - c.SoundIoErrorInterrupted => Error.Interrupted, - c.SoundIoErrorUnderflow => Error.Underflow, - c.SoundIoErrorEncodingString => Error.EncodingString, - else => unreachable, - }; -} - -test "error convertion" { - const expectError = @import("std").testing.expectError; - - try intToError(c.SoundIoErrorNone); - try expectError(Error.OutOfMemory, intToError(c.SoundIoErrorNoMem)); -} diff --git a/libs/sysaudio/soundio/main.zig b/libs/sysaudio/soundio/main.zig deleted file mode 100644 index d392914b..00000000 --- a/libs/sysaudio/soundio/main.zig +++ /dev/null @@ -1,16 +0,0 @@ -pub usingnamespace @import("enums.zig"); -pub const c = @import("c.zig"); -pub const SoundIo = @import("SoundIo.zig"); -pub const Device = @import("Device.zig"); -pub const InStream = @import("InStream.zig"); -pub const OutStream = @import("OutStream.zig"); -pub const Error = @import("error.zig").Error; - -const std = @import("std"); - -test { - std.testing.refAllDeclsRecursive(@import("SoundIo.zig")); - std.testing.refAllDeclsRecursive(@import("Device.zig")); - std.testing.refAllDeclsRecursive(@import("OutStream.zig")); - std.testing.refAllDeclsRecursive(@import("ChannelLayout.zig")); -} diff --git a/libs/sysaudio/src/alsa.zig b/libs/sysaudio/src/alsa.zig new file mode 100644 index 00000000..5eae818e --- /dev/null +++ b/libs/sysaudio/src/alsa.zig @@ -0,0 +1,649 @@ +const std = @import("std"); +const c = @cImport(@cInclude("alsa/asoundlib.h")); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); +const inotify_event = std.os.linux.inotify_event; +const is_little = @import("builtin").cpu.arch.endian() == .Little; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.DeviceChangeFn, + userdata: ?*anyopaque, + thread: std.Thread, + aborted: std.atomic.Atomic(bool), + notify_fd: std.os.fd_t, + notify_wd: std.os.fd_t, + notify_pipe_fd: [2]std.os.fd_t, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + _ = c.snd_lib_error_set_handler(@ptrCast(c.snd_lib_error_handler_t, &util.doNothing)); + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .watcher = blk: { + if (options.deviceChangeFn) |deviceChangeFn| { + const notify_fd = std.os.inotify_init1(std.os.linux.IN.NONBLOCK) catch |err| switch (err) { + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + => return error.SystemResources, + error.Unexpected => unreachable, + }; + errdefer std.os.close(notify_fd); + + const notify_wd = std.os.inotify_add_watch( + notify_fd, + "/dev/snd", + std.os.linux.IN.CREATE | std.os.linux.IN.DELETE, + ) catch |err| switch (err) { + error.AccessDenied => return error.AccessDenied, + error.UserResourceLimitReached, + error.NotDir, + error.FileNotFound, + error.SystemResources, + => return error.SystemResources, + error.NameTooLong, + error.WatchAlreadyExists, + error.Unexpected, + => unreachable, + }; + errdefer std.os.inotify_rm_watch(notify_fd, notify_wd); + + const notify_pipe_fd = std.os.pipe2(std.os.O.NONBLOCK) catch |err| switch (err) { + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + => return error.SystemResources, + error.Unexpected => unreachable, + }; + errdefer { + std.os.close(notify_pipe_fd[0]); + std.os.close(notify_pipe_fd[1]); + } + + break :blk .{ + .deviceChangeFn = deviceChangeFn, + .userdata = options.userdata, + .aborted = .{ .value = false }, + .notify_fd = notify_fd, + .notify_wd = notify_wd, + .notify_pipe_fd = notify_pipe_fd, + .thread = std.Thread.spawn(.{}, deviceEventsLoop, .{self}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + error.Unexpected => unreachable, + }, + }; + } + + break :blk null; + }, + }; + + return .{ .alsa = self }; + } + + pub fn deinit(self: *Context) void { + if (self.watcher) |*watcher| { + watcher.aborted.store(true, .Unordered); + _ = std.os.write(watcher.notify_pipe_fd[1], "a") catch {}; + watcher.thread.join(); + + std.os.close(watcher.notify_pipe_fd[0]); + std.os.close(watcher.notify_pipe_fd[1]); + std.os.inotify_rm_watch(watcher.notify_fd, watcher.notify_wd); + std.os.close(watcher.notify_fd); + } + + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + fn deviceEventsLoop(self: *Context) void { + var watcher = self.watcher.?; + var scan = false; + var last_crash: ?i64 = null; + var buf: [2048]u8 = undefined; + var fds = [2]std.os.pollfd{ + .{ + .fd = watcher.notify_fd, + .events = std.os.POLL.IN, + .revents = 0, + }, + .{ + .fd = watcher.notify_pipe_fd[0], + .events = std.os.POLL.IN, + .revents = 0, + }, + }; + + while (!watcher.aborted.load(.Unordered)) { + _ = std.os.poll(&fds, -1) catch |err| switch (err) { + error.NetworkSubsystemFailed, + error.SystemResources, + => { + const ts = std.time.milliTimestamp(); + if (last_crash) |lc| { + if (ts - lc < 500) return; + } + last_crash = ts; + continue; + }, + error.Unexpected => unreachable, + }; + if (watcher.notify_fd & std.os.POLL.IN != 0) { + while (true) { + const len = std.os.read(watcher.notify_fd, &buf) catch |err| { + if (err == error.WouldBlock) break; + const ts = std.time.milliTimestamp(); + if (last_crash) |lc| { + if (ts - lc < 500) return; + } + last_crash = ts; + break; + }; + if (len == 0) break; + + var i: usize = 0; + var evt: *inotify_event = undefined; + while (i < buf.len) : (i += @sizeOf(inotify_event) + evt.len) { + evt = @ptrCast(*inotify_event, @alignCast(4, buf[i..])); + const evt_name = @ptrCast([*]u8, buf[i..])[@sizeOf(inotify_event) .. @sizeOf(inotify_event) + 8]; + + if (evt.mask & std.os.linux.IN.ISDIR != 0 or !std.mem.startsWith(u8, evt_name, "pcm")) + continue; + + scan = true; + } + } + } + + if (scan) { + watcher.deviceChangeFn(self.watcher.?.userdata); + scan = false; + } + } + } + + pub fn refresh(self: *Context) !void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + var pcm_info: ?*c.snd_pcm_info_t = null; + _ = c.snd_pcm_info_malloc(&pcm_info); + defer c.snd_pcm_info_free(pcm_info); + + var card_idx: c_int = -1; + if (c.snd_card_next(&card_idx) < 0) + return error.SystemResources; + + while (card_idx >= 0) { + var card_id_buf: [8]u8 = undefined; + const card_id = std.fmt.bufPrintZ(&card_id_buf, "hw:{d}", .{card_idx}) catch break; + + var ctl: ?*c.snd_ctl_t = undefined; + _ = switch (-c.snd_ctl_open(&ctl, card_id.ptr, 0)) { + 0 => {}, + @enumToInt(std.os.E.NOENT) => break, + else => return error.OpeningDevice, + }; + defer _ = c.snd_ctl_close(ctl); + + var dev_idx: c_int = -1; + if (c.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0) + return error.SystemResources; + + c.snd_pcm_info_set_device(pcm_info, @intCast(c_uint, dev_idx)); + c.snd_pcm_info_set_subdevice(pcm_info, 0); + const name = std.mem.span(c.snd_pcm_info_get_name(pcm_info) orelse continue); + + for (&[_]main.Device.Mode{ .playback, .capture }) |mode| { + const snd_stream = modeToStream(mode); + c.snd_pcm_info_set_stream(pcm_info, snd_stream); + const err = c.snd_ctl_pcm_info(ctl, pcm_info); + switch (@intToEnum(std.os.E, -err)) { + .SUCCESS => {}, + .NOENT, + .NXIO, + .NODEV, + => break, + else => return error.SystemResources, + } + + var buf: [9]u8 = undefined; // 'hw' + max(card|device) * 2 + ':' + \0 + const id = std.fmt.bufPrintZ(&buf, "hw:{d},{d}", .{ card_idx, dev_idx }) catch continue; + + var pcm: ?*c.snd_pcm_t = null; + if (c.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0) + continue; + defer _ = c.snd_pcm_close(pcm); + + var params: ?*c.snd_pcm_hw_params_t = null; + _ = c.snd_pcm_hw_params_malloc(¶ms); + defer c.snd_pcm_hw_params_free(params); + if (c.snd_pcm_hw_params_any(pcm, params) < 0) + continue; + + if (c.snd_pcm_hw_params_can_pause(params) == 0) + continue; + + const device = main.Device{ + .mode = mode, + .channels = blk: { + const chmap = c.snd_pcm_query_chmaps(pcm); + if (chmap) |_| { + defer c.snd_pcm_free_chmaps(chmap); + + if (chmap[0] == null) continue; + + var channels = try self.allocator.alloc(main.Channel, chmap.*.*.map.channels); + for (channels) |*ch, i| + ch.*.id = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice; + break :blk channels; + } else { + continue; + } + }, + .formats = blk: { + var fmt_mask: ?*c.snd_pcm_format_mask_t = null; + _ = c.snd_pcm_format_mask_malloc(&fmt_mask); + defer c.snd_pcm_format_mask_free(fmt_mask); + c.snd_pcm_format_mask_none(fmt_mask); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE); + c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE); + c.snd_pcm_hw_params_get_format_mask(params, fmt_mask); + + var fmt_arr = std.ArrayList(main.Format).init(self.allocator); + inline for (std.meta.tags(main.Format)) |format| { + if (c.snd_pcm_format_mask_test( + fmt_mask, + toAlsaFormat(format) catch unreachable, + ) != 0) { + try fmt_arr.append(format); + } + } + + break :blk try fmt_arr.toOwnedSlice(); + }, + .sample_rate = blk: { + var rate_min: c_uint = 0; + var rate_max: c_uint = 0; + if (c.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0) + continue; + if (c.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0) + continue; + break :blk .{ + .min = @intCast(u24, rate_min), + .max = @intCast(u24, rate_max), + }; + }, + .id = try self.allocator.dupeZ(u8, id), + .name = try self.allocator.dupeZ(u8, name), + }; + + try self.devices_info.list.append(self.allocator, device); + + if (self.devices_info.default(mode) == null and dev_idx == 0) { + self.devices_info.setDefault(mode, self.devices_info.list.items.len - 1); + } + } + + if (c.snd_card_next(&card_idx) < 0) + return error.SystemResources; + } + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + pub fn createPlayer(self: Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate); + var pcm: ?*c.snd_pcm_t = null; + var mixer: ?*c.snd_mixer_t = null; + var selem: ?*c.snd_mixer_selem_id_t = null; + var mixer_elm: ?*c.snd_mixer_elem_t = null; + var period_size: c_ulong = 0; + + if (c.snd_pcm_open(&pcm, device.id.ptr, modeToStream(device.mode), 0) < 0) + return error.OpeningDevice; + errdefer _ = c.snd_pcm_close(pcm); + { + var hw_params: ?*c.snd_pcm_hw_params_t = null; + + if ((c.snd_pcm_set_params( + pcm, + toAlsaFormat(format) catch unreachable, + c.SND_PCM_ACCESS_RW_INTERLEAVED, + @intCast(c_uint, device.channels.len), + sample_rate, + 1, + main.default_latency, + )) < 0) + return error.OpeningDevice; + errdefer _ = c.snd_pcm_hw_free(pcm); + + if (c.snd_pcm_hw_params_malloc(&hw_params) < 0) + return error.OpeningDevice; + defer c.snd_pcm_hw_params_free(hw_params); + + if (c.snd_pcm_hw_params_current(pcm, hw_params) < 0) + return error.OpeningDevice; + + if (c.snd_pcm_hw_params_get_period_size(hw_params, &period_size, null) < 0) + return error.OpeningDevice; + } + + { + var chmap: c.snd_pcm_chmap_t = .{ .channels = @intCast(c_uint, device.channels.len) }; + + for (device.channels) |ch, i| + chmap.pos()[i] = toCHMAP(ch.id); + + if (c.snd_pcm_set_chmap(pcm, &chmap) < 0) + return error.IncompatibleDevice; + } + + { + if (c.snd_mixer_open(&mixer, 0) < 0) + return error.OutOfMemory; + + const card_id = try self.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ',')); + defer self.allocator.free(card_id); + + if (c.snd_mixer_attach(mixer, card_id.ptr) < 0) + return error.IncompatibleDevice; + + if (c.snd_mixer_selem_register(mixer, null, null) < 0) + return error.OpeningDevice; + + if (c.snd_mixer_load(mixer) < 0) + return error.OpeningDevice; + + if (c.snd_mixer_selem_id_malloc(&selem) < 0) + return error.OutOfMemory; + errdefer c.snd_mixer_selem_id_free(selem); + + c.snd_mixer_selem_id_set_index(selem, 0); + c.snd_mixer_selem_id_set_name(selem, "Master"); + + mixer_elm = c.snd_mixer_find_selem(mixer, selem) orelse + return error.IncompatibleDevice; + } + + return .{ + .alsa = .{ + .allocator = self.allocator, + .thread = undefined, + .mutex = .{}, + ._channels = device.channels, + ._format = format, + .sample_rate = sample_rate, + .writeFn = writeFn, + .aborted = .{ .value = false }, + .sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)), + .period_size = period_size, + .pcm = pcm.?, + .mixer = mixer.?, + .selem = selem.?, + .mixer_elm = mixer_elm.?, + }, + }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + thread: std.Thread, + mutex: std.Thread.Mutex, + _channels: []main.Channel, + _format: main.Format, + sample_rate: u24, + writeFn: main.WriteFn, + aborted: std.atomic.Atomic(bool), + sample_buffer: []u8, + period_size: c_ulong, + pcm: *c.snd_pcm_t, + mixer: *c.snd_mixer_t, + selem: *c.snd_mixer_selem_id_t, + mixer_elm: *c.snd_mixer_elem_t, + + pub fn deinit(self: *Player) void { + self.aborted.store(true, .Unordered); + self.thread.join(); + + _ = c.snd_mixer_close(self.mixer); + c.snd_mixer_selem_id_free(self.selem); + _ = c.snd_pcm_close(self.pcm); + _ = c.snd_pcm_hw_free(self.pcm); + + self.allocator.free(self.sample_buffer); + } + + pub fn start(self: *Player) !void { + self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + error.Unexpected => unreachable, + }; + } + + fn writeLoop(self: *Player) void { + var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); + + for (self.channels()) |*ch, i| { + ch.*.ptr = self.sample_buffer.ptr + self.format().frameSize(i); + } + + while (!self.aborted.load(.Unordered)) { + var frames_left = self.period_size; + while (frames_left > 0) { + self.writeFn(parent, frames_left); + const n = c.snd_pcm_writei(self.pcm, self.sample_buffer.ptr, frames_left); + if (n < 0) { + if (c.snd_pcm_recover(self.pcm, @intCast(c_int, n), 1) < 0) { + if (std.debug.runtime_safety) unreachable; + return; + } + return; + } + frames_left -= @intCast(c_uint, n); + } + } + } + + pub fn play(self: Player) !void { + if (c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED) { + if (c.snd_pcm_pause(self.pcm, 0) < 0) + return error.CannotPlay; + } + } + + pub fn pause(self: Player) !void { + if (c.snd_pcm_state(self.pcm) != c.SND_PCM_STATE_PAUSED) { + if (c.snd_pcm_pause(self.pcm, 1) < 0) + return error.CannotPause; + } + } + + pub fn paused(self: Player) bool { + return c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + var min_vol: c_long = 0; + var max_vol: c_long = 0; + if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotSetVolume; + + const dist = @intToFloat(f32, max_vol - min_vol); + if (c.snd_mixer_selem_set_playback_volume_all( + self.mixer_elm, + @floatToInt(c_long, dist * vol) + min_vol, + ) < 0) + return error.CannotSetVolume; + } + + pub fn volume(self: *Player) !f32 { + self.mutex.lock(); + defer self.mutex.unlock(); + + var vol: c_long = 0; + var channel: c_int = 0; + + while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) { + if (c.snd_mixer_selem_has_playback_channel(self.mixer_elm, channel) == 1) { + if (c.snd_mixer_selem_get_playback_volume(self.mixer_elm, channel, &vol) == 0) + break; + } + } + + if (channel == c.SND_MIXER_SCHN_LAST) + return error.CannotGetVolume; + + var min_vol: c_long = 0; + var max_vol: c_long = 0; + if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotGetVolume; + + return @intToFloat(f32, vol) / @intToFloat(f32, max_vol - min_vol); + } + + pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { + var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len); + std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; + } + + pub fn channels(self: Player) []main.Channel { + return self._channels; + } + + pub fn format(self: Player) main.Format { + return self._format; + } + + pub fn sampleRate(self: Player) u24 { + return self.sample_rate; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.id); + allocator.free(device.name); + allocator.free(device.formats); + allocator.free(device.channels); +} + +pub fn modeToStream(mode: main.Device.Mode) c_uint { + return switch (mode) { + .playback => c.SND_PCM_STREAM_PLAYBACK, + .capture => c.SND_PCM_STREAM_CAPTURE, + }; +} + +pub fn toAlsaFormat(format: main.Format) !c.snd_pcm_format_t { + return switch (format) { + .u8 => c.SND_PCM_FORMAT_U8, + .i8 => c.SND_PCM_FORMAT_S8, + .i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE, + .i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE, + .i24_4b => if (is_little) c.SND_PCM_FORMAT_S24_LE else c.SND_PCM_FORMAT_S24_BE, + .i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE, + .f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE, + .f64 => if (is_little) c.SND_PCM_FORMAT_FLOAT64_LE else c.SND_PCM_FORMAT_FLOAT64_BE, + }; +} + +pub fn fromAlsaChannel(pos: c_uint) !main.Channel.Id { + return switch (pos) { + c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid, + c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center, + c.SND_CHMAP_FL => .front_left, + c.SND_CHMAP_FR => .front_right, + c.SND_CHMAP_LFE => .lfe, + c.SND_CHMAP_SL => .side_left, + c.SND_CHMAP_SR => .side_right, + c.SND_CHMAP_RC => .back_center, + c.SND_CHMAP_FLC => .front_left_center, + c.SND_CHMAP_FRC => .front_right_center, + c.SND_CHMAP_TC => .top_center, + c.SND_CHMAP_TFL => .top_front_left, + c.SND_CHMAP_TFR => .top_front_right, + c.SND_CHMAP_TFC => .top_front_center, + c.SND_CHMAP_TRL => .top_back_left, + c.SND_CHMAP_TRR => .top_back_right, + c.SND_CHMAP_TRC => .top_back_center, + + else => return error.Invalid, + }; +} + +pub fn toCHMAP(pos: main.Channel.Id) c_uint { + return switch (pos) { + .front_center => c.SND_CHMAP_FC, + .front_left => c.SND_CHMAP_FL, + .front_right => c.SND_CHMAP_FR, + .lfe => c.SND_CHMAP_LFE, + .side_left => c.SND_CHMAP_SL, + .side_right => c.SND_CHMAP_SR, + .back_center => c.SND_CHMAP_RC, + .front_left_center => c.SND_CHMAP_FLC, + .front_right_center => c.SND_CHMAP_FRC, + .top_center => c.SND_CHMAP_TC, + .top_front_left => c.SND_CHMAP_TFL, + .top_front_right => c.SND_CHMAP_TFR, + .top_front_center => c.SND_CHMAP_TFC, + .top_back_left => c.SND_CHMAP_TRL, + .top_back_right => c.SND_CHMAP_TRR, + .top_back_center => c.SND_CHMAP_TRC, + }; +} + +test { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/libs/sysaudio/src/backends.zig b/libs/sysaudio/src/backends.zig new file mode 100644 index 00000000..10508468 --- /dev/null +++ b/libs/sysaudio/src/backends.zig @@ -0,0 +1,48 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +pub const Backend = std.meta.Tag(BackendContext); +pub const BackendContext = switch (builtin.os.tag) { + .linux => union(enum) { + pulseaudio: *@import("pulseaudio.zig").Context, + alsa: *@import("alsa.zig").Context, + jack: *@import("jack.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .freebsd, .netbsd, .openbsd, .solaris => union(enum) { + pulseaudio: *@import("pulseaudio.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .macos, .ios, .watchos, .tvos => union(enum) { + dummy: *@import("dummy.zig").Context, + }, + .windows => union(enum) { + wasapi: *@import("wasapi.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + else => union(enum) { + dummy: *@import("dummy.zig").Context, + }, +}; +pub const BackendPlayer = switch (builtin.os.tag) { + .linux => union(enum) { + pulseaudio: @import("pulseaudio.zig").Player, + alsa: @import("alsa.zig").Player, + jack: @import("jack.zig").Player, + dummy: @import("dummy.zig").Player, + }, + .freebsd, .netbsd, .openbsd, .solaris => union(enum) { + pulseaudio: @import("pulseaudio.zig").Player, + dummy: @import("dummy.zig").Player, + }, + .macos, .ios, .watchos, .tvos => union(enum) { + dummy: @import("dummy.zig").Player, + }, + .windows => union(enum) { + wasapi: @import("wasapi.zig").Player, + dummy: @import("dummy.zig").Player, + }, + else => union(enum) { + dummy: @import("dummy.zig").Player, + }, +}; diff --git a/libs/sysaudio/src/dummy.zig b/libs/sysaudio/src/dummy.zig new file mode 100644 index 00000000..1328982b --- /dev/null +++ b/libs/sysaudio/src/dummy.zig @@ -0,0 +1,158 @@ +const std = @import("std"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +pub const min_sample_rate = 8_000; // Hz +pub const max_sample_rate = 5_644_800; // Hz + +const dummy_playback = main.Device{ + .id = "dummy-playback", + .name = "Dummy Device", + .mode = .playback, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = min_sample_rate, + .max = max_sample_rate, + }, +}; + +const dummy_capture = main.Device{ + .id = "dummy-capture", + .name = "Dummy Device", + .mode = .capture, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = min_sample_rate, + .max = max_sample_rate, + }, +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + _ = options; + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + }; + + try self.devices_info.list.append(self.allocator, dummy_playback); + try self.devices_info.list.append(self.allocator, dummy_capture); + self.devices_info.list.items[0].channels = try allocator.alloc(main.Channel, 1); + self.devices_info.list.items[0].channels[0] = .{ + .id = .front_center, + }; + self.devices_info.list.items[1].channels = try allocator.alloc(main.Channel, 1); + self.devices_info.list.items[1].channels[0] = .{ + .id = .front_center, + }; + self.devices_info.setDefault(.playback, 0); + self.devices_info.setDefault(.capture, 1); + + return .{ .dummy = self }; + } + + pub fn deinit(self: *Context) void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + _ = self; + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + _ = self; + _ = writeFn; + return .{ + .dummy = .{ + ._channels = device.channels, + ._format = options.format, + .sample_rate = options.sample_rate, + .is_paused = false, + .vol = 1.0, + }, + }; + } +}; + +pub const Player = struct { + _channels: []main.Channel, + _format: main.Format, + sample_rate: u24, + is_paused: bool, + vol: f32, + + pub fn deinit(self: Player) void { + _ = self; + } + + pub fn start(self: Player) !void { + _ = self; + } + + pub fn play(self: *Player) !void { + self.is_paused = false; + } + + pub fn pause(self: *Player) !void { + self.is_paused = true; + } + + pub fn paused(self: Player) bool { + return self.is_paused; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + self.vol = vol; + } + + pub fn volume(self: Player) !f32 { + return self.vol; + } + + pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void { + _ = self; + _ = channel; + _ = frame; + _ = sample; + } + + pub fn channels(self: Player) []main.Channel { + return self._channels; + } + + pub fn format(self: Player) main.Format { + return self._format; + } + + pub fn sampleRate(self: Player) u24 { + return self.sample_rate; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +} + +test { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/libs/sysaudio/src/jack.zig b/libs/sysaudio/src/jack.zig new file mode 100644 index 00000000..98fa70aa --- /dev/null +++ b/libs/sysaudio/src/jack.zig @@ -0,0 +1,274 @@ +const std = @import("std"); +const c = @cImport(@cInclude("jack/jack.h")); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + client: *c.jack_client_t, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.DeviceChangeFn, + userdata: ?*anyopaque, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + c.jack_set_error_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing)); + c.jack_set_info_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing)); + + var status: c.jack_status_t = 0; + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .client = c.jack_client_open(options.app_name.ptr, c.JackNoStartServer, &status) orelse { + std.debug.assert(status & c.JackInvalidOption == 0); + return if (status & c.JackShmFailure != 0) + error.SystemResources + else + error.ConnectionRefused; + }, + .watcher = if (options.deviceChangeFn) |deviceChangeFn| .{ + .deviceChangeFn = deviceChangeFn, + .userdata = options.userdata, + } else null, + }; + + if (options.deviceChangeFn) |_| { + if (c.jack_set_sample_rate_callback(self.client, sampleRateCallback, self) != 0 or + c.jack_set_port_registration_callback(self.client, portRegistrationCallback, self) != 0 or + c.jack_set_port_rename_callback(self.client, portRenameCalllback, self) != 0) + return error.ConnectionRefused; + } + + return .{ .jack = self }; + } + + pub fn deinit(self: *Context) void { + for (self.devices_info.list.items) |device| + freeDevice(self.allocator, device); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + const sample_rate = @intCast(u24, c.jack_get_sample_rate(self.client)); + + const port_names = c.jack_get_ports(self.client, null, null, 0) orelse + return error.OutOfMemory; + defer c.jack_free(@ptrCast(?*anyopaque, port_names)); + + var i: usize = 0; + outer: while (port_names[i] != null) : (i += 1) { + const port = c.jack_port_by_name(self.client, port_names[i]) orelse break; + const port_type = c.jack_port_type(port)[0..@intCast(usize, c.jack_port_type_size())]; + if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE)) + continue; + + const flags = c.jack_port_flags(port); + const mode: main.Device.Mode = if (flags & c.JackPortIsInput != 0) .capture else .playback; + + const name = std.mem.span(port_names[i]); + const id = std.mem.sliceTo(name, ':'); + + for (self.devices_info.list.items) |*dev| { + if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) { + const new_ch = main.Channel{ + .id = @intToEnum(main.Channel.Id, dev.channels.len), + }; + dev.channels = try self.allocator.realloc(dev.channels, dev.channels.len + 1); + dev.channels[dev.channels.len - 1] = new_ch; + break :outer; + } + } + + var device = main.Device{ + .id = try self.allocator.dupeZ(u8, id), + .name = name, + .mode = mode, + .channels = blk: { + var channels = try self.allocator.alloc(main.Channel, 1); + channels[0] = .{ .id = @intToEnum(main.Channel.Id, 0) }; + break :blk channels; + }, + .formats = &.{.f32}, + .sample_rate = .{ + .min = sample_rate, + .max = sample_rate, + }, + }; + + try self.devices_info.list.append(self.allocator, device); + if (std.mem.eql(u8, "system", id)) { + self.devices_info.setDefault(device.mode, self.devices_info.list.items.len - 1); + } + } + } + + fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); + self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + return 0; + } + + fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); + self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + } + + fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); + self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + } + + pub fn devices(self: *Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: *Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + _ = options; + + var ports = try self.allocator.alloc(*c.jack_port_t, device.channels.len); + var dest_ports = try self.allocator.alloc([:0]const u8, ports.len); + var buf: [64]u8 = undefined; + for (device.channels) |_, i| { + const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable; + const dest_name = try std.fmt.allocPrintZ(self.allocator, "{s}:{s}", .{ device.id, port_name }); + ports[i] = c.jack_port_register(self.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse + return error.OpeningDevice; + dest_ports[i] = dest_name; + } + + return .{ + .jack = .{ + .allocator = self.allocator, + .mutex = .{}, + .cond = .{}, + .device = device, + .writeFn = writeFn, + .client = self.client, + .ports = ports, + .dest_ports = dest_ports, + }, + }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + mutex: std.Thread.Mutex, + cond: std.Thread.Condition, + device: main.Device, + writeFn: main.WriteFn, + client: *c.jack_client_t, + ports: []const *c.jack_port_t, + dest_ports: []const [:0]const u8, + + pub fn deinit(self: *Player) void { + self.allocator.free(self.ports); + for (self.dest_ports) |d| + self.allocator.free(d); + self.allocator.free(self.dest_ports); + } + + pub fn start(self: *Player) !void { + if (c.jack_set_process_callback(self.client, processCallback, self) != 0) + return error.CannotPlay; + + if (c.jack_activate(self.client) != 0) + return error.CannotPlay; + + for (self.ports) |port, i| { + if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0) + return error.CannotPlay; + } + } + + fn processCallback(n_frames: c.jack_nframes_t, self_opaque: ?*anyopaque) callconv(.C) c_int { + const self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?)); + var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); + for (self.channels()) |*ch, i| { + ch.*.ptr = @ptrCast([*]u8, c.jack_port_get_buffer(self.ports[i], n_frames)); + } + self.writeFn(parent, n_frames); + return 0; + } + + pub fn play(self: *Player) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + for (self.ports) |port, i| { + if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0) + return error.CannotPlay; + } + } + + pub fn pause(self: *Player) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + for (self.ports) |port, i| { + if (c.jack_disconnect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0) + return error.CannotPause; + } + } + + pub fn paused(self: *Player) bool { + self.mutex.lock(); + defer self.mutex.unlock(); + for (self.ports) |port, i| { + if (c.jack_port_connected_to(port, self.dest_ports[i].ptr) == 1) + return false; + } + return true; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + _ = self; + _ = vol; + @panic("incompatible backend"); + } + + pub fn volume(self: *Player) !f32 { + _ = self; + @panic("incompatible backend"); + } + + pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { + var ptr = channel.ptr + frame * self.format().size(); + std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; + } + + pub fn channels(self: Player) []main.Channel { + return self.device.channels; + } + + pub fn format(self: Player) main.Format { + _ = self; + return .f32; + } + + pub fn sampleRate(self: Player) u24 { + return @intCast(u24, c.jack_get_sample_rate(self.client)); + } +}; + +pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.id); + allocator.free(device.channels); +} + +test { + std.testing.refAllDeclsRecursive(@This()); +} \ No newline at end of file diff --git a/libs/sysaudio/src/main.zig b/libs/sysaudio/src/main.zig index 40572806..1f0712d0 100644 --- a/libs/sysaudio/src/main.zig +++ b/libs/sysaudio/src/main.zig @@ -1,134 +1,461 @@ -const std = @import("std"); -const mem = std.mem; -const testing = std.testing; const builtin = @import("builtin"); -const Backend = if (builtin.cpu.arch == .wasm32) @import("webaudio.zig") else switch (builtin.os.tag) { - .linux, - .windows, - .macos, - .ios, - => @import("soundio.zig"), - else => @compileError("unsupported os"), -}; -pub const Error = Backend.Error; -pub const Device = Backend.Device; -pub const DeviceIterator = Backend.DeviceIterator; +const std = @import("std"); +const util = @import("util.zig"); +const backends = @import("backends.zig"); -pub const DataCallback = *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; +pub const default_sample_rate = 44_100; // Hz +pub const default_latency = 500 * std.time.us_per_ms; // μs -pub const Mode = enum { - input, - output, +pub const Backend = backends.Backend; +pub const DeviceChangeFn = *const fn (self: ?*anyopaque) void; +pub const ConnectError = error{ + OutOfMemory, + AccessDenied, + SystemResources, + ConnectionRefused, }; -pub const Format = enum { - U8, - S16, - S24, - S32, - F32, -}; - -const Audio = @This(); - -backend: Backend, - -pub fn init() Error!Audio { - return Audio{ - .backend = try Backend.init(), +pub const Context = struct { + pub const Options = struct { + app_name: [:0]const u8 = "Mach Game", + deviceChangeFn: ?DeviceChangeFn = null, + userdata: ?*anyopaque = null, }; -} -pub fn deinit(self: Audio) void { - self.backend.deinit(); -} + data: backends.BackendContext, -pub fn waitEvents(self: Audio) void { - self.backend.waitEvents(); -} + pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) ConnectError!Context { + var data: backends.BackendContext = blk: { + if (backend) |b| { + break :blk try @typeInfo( + std.meta.fieldInfo(backends.BackendContext, b).field_type, + ).Pointer.child.init(allocator, options); + } else { + inline for (std.meta.fields(Backend)) |b, i| { + if (@typeInfo( + std.meta.fieldInfo(backends.BackendContext, @intToEnum(Backend, b.value)).field_type, + ).Pointer.child.init(allocator, options)) |d| { + break :blk d; + } else |err| { + if (i == std.meta.fields(Backend).len - 1) + return err; + } + } + unreachable; + } + }; -pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: Device.Options) Error!*Device { - return self.backend.requestDevice(allocator, config); -} + return .{ .data = data }; + } -pub fn inputDeviceIterator(self: Audio) DeviceIterator { - return self.backend.inputDeviceIterator(); -} + pub fn deinit(self: Context) void { + switch (self.data) { + inline else => |b| b.deinit(), + } + } -pub fn outputDeviceIterator(self: Audio) DeviceIterator { - return self.backend.outputDeviceIterator(); -} + pub const RefreshError = error{ + OutOfMemory, + SystemResources, + OpeningDevice, + }; -test "list devices" { - const a = try init(); - defer a.deinit(); + pub fn refresh(self: Context) RefreshError!void { + return switch (self.data) { + inline else => |b| b.refresh(), + }; + } - var iter = a.inputDeviceIterator(); - while (try iter.next()) |_| {} -} + pub fn devices(self: Context) []const Device { + return switch (self.data) { + inline else => |b| b.devices(), + }; + } -// TODO(sysaudio): get this test passing on CI -test "connect to device" { - return error.SkipZigTest; + pub fn defaultDevice(self: Context, mode: Device.Mode) ?Device { + return switch (self.data) { + inline else => |b| b.defaultDevice(mode), + }; + } - // const a = try init(); - // defer a.deinit(); + pub const CreateStreamError = error{ + OutOfMemory, + SystemResources, + OpeningDevice, + IncompatibleDevice, + }; - // const d = try a.requestDevice(std.testing.allocator, .{ .mode = .output }); - // defer d.deinit(std.testing.allocator); -} + pub fn createPlayer(self: Context, device: Device, writeFn: WriteFn, options: Player.Options) CreateStreamError!Player { + std.debug.assert(device.mode == .playback); -// TODO(sysaudio): get this test passing on CI -test "connect to device from descriptor" { - return error.SkipZigTest; + return .{ + .userdata = options.userdata, + .data = switch (self.data) { + inline else => |b| try b.createPlayer(device, writeFn, options), + }, + }; + } +}; - // const a = try init(); - // defer a.deinit(); +// TODO: `*Player` instead `*anyopaque` +// https://github.com/ziglang/zig/issues/12325 +pub const WriteFn = *const fn (self: *anyopaque, frame_count_max: usize) void; - // var iter = a.outputDeviceIterator(); - // while (try iter.next()) |desc| { - // if (mem.eql(u8, desc.name orelse "", "default")) { - // const d = try a.requestDevice(std.testing.allocator, desc); - // defer d.deinit(std.testing.allocator); - // return; +pub const Player = struct { + pub const Options = struct { + format: Format = .f32, + sample_rate: u24 = default_sample_rate, + userdata: ?*anyopaque = null, + }; + + userdata: ?*anyopaque, + data: backends.BackendPlayer, + + pub fn deinit(self: *Player) void { + return switch (self.data) { + inline else => |*b| b.deinit(), + }; + } + + pub const StartError = error{ + CannotPlay, + OutOfMemory, + SystemResources, + }; + + pub fn start(self: *Player) StartError!void { + return switch (self.data) { + inline else => |*b| b.start(), + }; + } + + pub const PlayError = error{ + CannotPlay, + OutOfMemory, + }; + + pub fn play(self: *Player) PlayError!void { + return switch (self.data) { + inline else => |*b| b.play(), + }; + } + + pub const PauseError = error{ + CannotPause, + OutOfMemory, + }; + + pub fn pause(self: *Player) PauseError!void { + return switch (self.data) { + inline else => |*b| b.pause(), + }; + } + + pub fn paused(self: *Player) bool { + return switch (self.data) { + inline else => |*b| b.paused(), + }; + } + + pub const SetVolumeError = error{ + CannotSetVolume, + }; + + // confidence interval (±) depends on the device + pub fn setVolume(self: *Player, vol: f32) SetVolumeError!void { + std.debug.assert(vol <= 1.0); + return switch (self.data) { + inline else => |*b| b.setVolume(vol), + }; + } + + pub const GetVolumeError = error{ + CannotGetVolume, + }; + + // confidence interval (±) depends on the device + pub fn volume(self: *Player) GetVolumeError!f32 { + return switch (self.data) { + inline else => |*b| b.volume(), + }; + } + + pub fn writeRaw(self: *Player, channel: Channel, frame: usize, sample: anytype) void { + return switch (self.data) { + inline else => |*b| b.writeRaw(channel, frame, sample), + }; + } + + pub fn writeAll(self: *Player, frame: usize, value: anytype) void { + for (self.channels()) |ch| + self.write(ch, frame, value); + } + + pub fn write(self: *Player, channel: Channel, frame: usize, sample: anytype) void { + switch (@TypeOf(sample)) { + u8 => self.writeU8(channel, frame, sample), + i16 => self.writeI16(channel, frame, sample), + i24 => self.writeI24(channel, frame, sample), + i32 => self.writeI32(channel, frame, sample), + f32 => self.writeF32(channel, frame, sample), + f64 => self.writeF64(channel, frame, sample), + else => @compileError( + \\invalid sample type. supported types are: + \\u8, i8, i16, i24, i32, f32, f32, f64 + ), + } + } + + pub fn writeU8(self: *Player, channel: Channel, frame: usize, sample: u8) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, sample), + .i8 => self.writeRaw(channel, frame, unsignedToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, unsignedToSigned(i16, sample)), + .i24 => self.writeRaw(channel, frame, unsignedToSigned(i24, sample)), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, unsignedToSigned(i32, sample)), + .f32 => self.writeRaw(channel, frame, unsignedToFloat(f32, sample)), + .f64 => self.writeRaw(channel, frame, unsignedToFloat(f64, sample)), + } + } + + pub fn writeI16(self: *Player, channel: Channel, frame: usize, sample: i16) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), + .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, sample), + .i24 => self.writeRaw(channel, frame, sample), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, sample), + .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), + .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), + } + } + + pub fn writeI24(self: *Player, channel: Channel, frame: usize, sample: i24) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), + .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)), + .i24 => self.writeRaw(channel, frame, sample), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, sample), + .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), + .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), + } + } + + pub fn writeI32(self: *Player, channel: Channel, frame: usize, sample: i32) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), + .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)), + .i24 => self.writeRaw(channel, frame, signedToSigned(i24, sample)), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, sample), + .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), + .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), + } + } + + pub fn writeF32(self: *Player, channel: Channel, frame: usize, sample: f32) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)), + .i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)), + .i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)), + .f32 => self.writeRaw(channel, frame, sample), + .f64 => self.writeRaw(channel, frame, sample), + } + } + + pub fn writeF64(self: *Player, channel: Channel, frame: usize, sample: f64) void { + switch (self.format()) { + .u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)), + .i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)), + .i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)), + .i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)), + .i24_4b => @panic("TODO"), + .i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)), + .f32 => self.writeRaw(channel, frame, sample), + .f64 => self.writeRaw(channel, frame, sample), + } + } + + fn unsignedToSigned(comptime T: type, sample: anytype) T { + const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1); + const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample)); + return @intCast(T, sample -% half) << trunc; + } + + fn unsignedToFloat(comptime T: type, sample: anytype) T { + const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; + return (@intToFloat(T, sample) - max_int) * 1.0 / max_int; + } + + fn signedToSigned(comptime T: type, sample: anytype) T { + const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); + return @intCast(T, sample >> trunc); + } + + fn signedToUnsigned(comptime T: type, sample: anytype) T { + const half = 1 << (@bitSizeOf(T) - 1); + const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); + return @intCast(T, (sample >> trunc) + half); + } + + fn signedToFloat(comptime T: type, sample: anytype) T { + const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; + return @intToFloat(T, sample) * 1.0 / max_int; + } + + fn floatToSigned(comptime T: type, sample: f64) T { + return @floatToInt(T, sample * std.math.maxInt(T)); + } + + fn floatToUnsigned(comptime T: type, sample: f64) T { + const half = 1 << @bitSizeOf(T) - 1; + return @floatToInt(T, sample * (half - 1) + half); + } + + // TODO: needs test + // fn f32Toi24_4b(sample: f32) i32 { + // const scaled = sample * std.math.maxInt(i32); + // if (builtin.cpu.arch.endian() == .Little) { + // return @floatToInt(i32, scaled); + // } else { + // var res: [4]u8 = undefined; + // std.mem.writeIntSliceBig(i32, &res, @floatToInt(i32, res)); + // return @bitCast(i32, res); // } // } - // return error.SkipZigTest; -} - -// TODO(sysaudio): get this test passing on CI -test "requestDevice behavior: null is_raw" { - return error.SkipZigTest; - - // const a = try init(); - // defer a.deinit(); - - // var iter = a.outputDeviceIterator(); - // var device_conf = (try iter.next()) orelse return error.NoDeviceFound; - - // const bad_conf = Device.Options{ - // .is_raw = null, - // .mode = device_conf.mode, - // .id = device_conf.id, - // }; - // try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_conf)); -} - -// TODO(sysaudio): get this test passing on CI -test "requestDevice behavior: invalid id" { - return error.SkipZigTest; - - // const a = try init(); - // defer a.deinit(); - - // var iter = a.outputDeviceIterator(); - // var device_conf = (try iter.next()) orelse return error.NoDeviceFound; - - // const bad_conf = Device.Options{ - // .is_raw = device_conf.is_raw, - // .mode = device_conf.mode, - // .id = "wrong-id", - // }; - // try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf)); + pub fn channels(self: Player) []Channel { + return switch (self.data) { + inline else => |*b| b.channels(), + }; + } + + pub fn format(self: Player) Format { + return switch (self.data) { + inline else => |*b| b.format(), + }; + } + + pub fn sampleRate(self: Player) u24 { + return switch (self.data) { + inline else => |*b| b.sampleRate(), + }; + } + + pub fn frameSize(self: Player) u8 { + return self.format().frameSize(self.channels().len); + } +}; + +pub const Device = struct { + id: [:0]const u8, + name: [:0]const u8, + mode: Mode, + channels: []Channel, + formats: []const Format, + sample_rate: util.Range(u24), + + pub const Mode = enum { + playback, + capture, + }; + + pub fn preferredFormat(self: Device, format: ?Format) Format { + if (format) |f| { + for (self.formats) |fmt| { + if (f == fmt) { + return fmt; + } + } + } + + var best: Format = self.formats[0]; + for (self.formats) |fmt| { + if (fmt.size() >= best.size()) { + if (fmt == .i24_4b and best == .i24) + continue; + best = fmt; + } + } + return best; + } +}; + +pub const Channel = struct { + ptr: [*]u8 = undefined, + id: Id, + + pub const Id = enum { + front_center, + front_left, + front_right, + front_left_center, + front_right_center, + back_center, + side_left, + side_right, + top_center, + top_front_center, + top_front_left, + top_front_right, + top_back_center, + top_back_left, + top_back_right, + lfe, + }; +}; + +pub const Format = enum { + u8, + i8, + i16, + i24, + i24_4b, + i32, + f32, + f64, + + pub fn size(self: Format) u8 { + return switch (self) { + .u8, .i8 => 1, + .i16 => 2, + .i24 => 3, + .i24_4b, .i32, .f32 => 4, + .f64 => 8, + }; + } + + pub fn validSize(self: Format) u8 { + return switch (self) { + .u8, .i8 => 1, + .i16 => 2, + .i24, .i24_4b => 3, + .i32, .f32 => 4, + .f64 => 8, + }; + } + + pub fn sizeBits(self: Format) u8 { + return self.size() * 8; + } + + pub fn validSizeBits(self: Format) u8 { + return self.validSize() * 8; + } + + pub fn frameSize(self: Format, ch_count: usize) u8 { + return self.size() * @intCast(u5, ch_count); + } +}; + +test { + std.testing.refAllDeclsRecursive(@This()); } diff --git a/libs/sysaudio/src/pulseaudio.zig b/libs/sysaudio/src/pulseaudio.zig new file mode 100644 index 00000000..bb61dd54 --- /dev/null +++ b/libs/sysaudio/src/pulseaudio.zig @@ -0,0 +1,590 @@ +const std = @import("std"); +const c = @cImport(@cInclude("pulse/pulseaudio.h")); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); +const is_little = @import("builtin").cpu.arch.endian() == .Little; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + app_name: [:0]const u8, + main_loop: *c.pa_threaded_mainloop, + ctx: *c.pa_context, + ctx_state: c.pa_context_state_t, + default_sink: ?[:0]const u8, + default_source: ?[:0]const u8, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.DeviceChangeFn, + userdata: ?*anyopaque, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + const main_loop = c.pa_threaded_mainloop_new() orelse + return error.OutOfMemory; + errdefer c.pa_threaded_mainloop_free(main_loop); + var main_loop_api = c.pa_threaded_mainloop_get_api(main_loop); + + const ctx = c.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse + return error.OutOfMemory; + errdefer c.pa_context_unref(ctx); + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = Context{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .app_name = options.app_name, + .main_loop = main_loop, + .ctx = ctx, + .ctx_state = c.PA_CONTEXT_UNCONNECTED, + .default_sink = null, + .default_source = null, + .watcher = if (options.deviceChangeFn) |dcf| .{ + .deviceChangeFn = dcf, + .userdata = options.userdata, + } else null, + }; + + if (c.pa_context_connect(ctx, null, 0, null) != 0) + return error.ConnectionRefused; + errdefer c.pa_context_disconnect(ctx); + c.pa_context_set_state_callback(ctx, contextStateOp, self); + + if (c.pa_threaded_mainloop_start(main_loop) != 0) + return error.SystemResources; + errdefer c.pa_threaded_mainloop_stop(main_loop); + + c.pa_threaded_mainloop_lock(main_loop); + defer c.pa_threaded_mainloop_unlock(main_loop); + + while (true) { + switch (self.ctx_state) { + // The context hasn't been connected yet. + c.PA_CONTEXT_UNCONNECTED, + // A connection is being established. + c.PA_CONTEXT_CONNECTING, + // The client is authorizing itself to the daemon. + c.PA_CONTEXT_AUTHORIZING, + // The client is passing its application name to the daemon. + c.PA_CONTEXT_SETTING_NAME, + => c.pa_threaded_mainloop_wait(main_loop), + + // The connection is established, the context is ready to execute operations. + c.PA_CONTEXT_READY => break, + + // The connection was terminated cleanly. + c.PA_CONTEXT_TERMINATED, + // The connection failed or was disconnected. + c.PA_CONTEXT_FAILED, + => return error.ConnectionRefused, + + else => unreachable, + } + } + + // subscribe to events + if (options.deviceChangeFn != null) { + c.pa_context_set_subscribe_callback(ctx, subscribeOp, self); + const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE; + const subscribe_op = c.pa_context_subscribe(ctx, events, null, self) orelse + return error.OutOfMemory; + c.pa_operation_unref(subscribe_op); + } + + return .{ .pulseaudio = self }; + } + + fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + } + + fn contextStateOp(ctx: ?*c.pa_context, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + + self.ctx_state = c.pa_context_get_state(ctx); + c.pa_threaded_mainloop_signal(self.main_loop, 0); + } + + pub fn deinit(self: *Context) void { + c.pa_context_set_subscribe_callback(self.ctx, null, null); + c.pa_context_set_state_callback(self.ctx, null, null); + c.pa_context_disconnect(self.ctx); + c.pa_context_unref(self.ctx); + c.pa_threaded_mainloop_stop(self.main_loop); + c.pa_threaded_mainloop_free(self.main_loop); + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + const list_sink_op = c.pa_context_get_sink_info_list(self.ctx, sinkInfoOp, self); + const list_source_op = c.pa_context_get_source_info_list(self.ctx, sourceInfoOp, self); + const server_info_op = c.pa_context_get_server_info(self.ctx, serverInfoOp, self); + + performOperation(self.main_loop, list_sink_op); + performOperation(self.main_loop, list_source_op); + performOperation(self.main_loop, server_info_op); + + defer { + if (self.default_sink) |d| + self.allocator.free(d); + if (self.default_source) |d| + self.allocator.free(d); + } + for (self.devices_info.list.items) |device, i| { + if ((device.mode == .playback and + self.default_sink != null and + std.mem.eql(u8, device.id, self.default_sink.?)) or + // + (device.mode == .capture and + self.default_source != null and + std.mem.eql(u8, device.id, self.default_source.?))) + { + self.devices_info.setDefault(device.mode, i); + break; + } + } + } + + fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + + defer c.pa_threaded_mainloop_signal(self.main_loop, 0); + self.default_sink = self.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return; + self.default_source = self.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch { + self.allocator.free(self.default_sink.?); + return; + }; + } + + fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + if (eol != 0) { + c.pa_threaded_mainloop_signal(self.main_loop, 0); + return; + } + + self.deviceInfoOp(info, .playback) catch return; + } + + fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + if (eol != 0) { + c.pa_threaded_mainloop_signal(self.main_loop, 0); + return; + } + + self.deviceInfoOp(info, .capture) catch return; + } + + fn deviceInfoOp(self: *Context, info: anytype, mode: main.Device.Mode) !void { + var id = try self.allocator.dupeZ(u8, std.mem.span(info.*.name)); + errdefer self.allocator.free(id); + var name = try self.allocator.dupeZ(u8, std.mem.span(info.*.description)); + errdefer self.allocator.free(name); + + var device = main.Device{ + .mode = mode, + .channels = blk: { + var channels = try self.allocator.alloc(main.Channel, info.*.channel_map.channels); + for (channels) |*ch, i| + ch.*.id = fromPAChannelPos(info.*.channel_map.map[i]) catch unreachable; + break :blk channels; + }, + .formats = available_formats, + .sample_rate = .{ + .min = @intCast(u24, info.*.sample_spec.rate), + .max = @intCast(u24, info.*.sample_spec.rate), + }, + .id = id, + .name = name, + }; + + try self.devices_info.list.append(self.allocator, device); + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate); + + const sample_spec = c.pa_sample_spec{ + .format = toPAFormat(format) catch unreachable, + .rate = sample_rate, + .channels = @intCast(u5, device.channels.len), + }; + + const channel_map = try toPAChannelMap(device.channels); + + var stream = c.pa_stream_new(self.ctx, self.app_name.ptr, &sample_spec, &channel_map); + if (stream == null) + return error.OutOfMemory; + errdefer c.pa_stream_unref(stream); + + var status: StreamStatus = .{ .main_loop = self.main_loop, .status = .unknown }; + c.pa_stream_set_state_callback(stream, streamStateOp, &status); + + const buf_attr = c.pa_buffer_attr{ + .maxlength = std.math.maxInt(u32), + .tlength = std.math.maxInt(u32), + .prebuf = 0, + .minreq = std.math.maxInt(u32), + .fragsize = std.math.maxInt(u32), + }; + + const flags = + c.PA_STREAM_START_CORKED | + c.PA_STREAM_AUTO_TIMING_UPDATE | + c.PA_STREAM_INTERPOLATE_TIMING | + c.PA_STREAM_ADJUST_LATENCY; + + if (c.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) { + return error.OpeningDevice; + } + errdefer _ = c.pa_stream_disconnect(stream); + + while (true) { + switch (status.status) { + .unknown => c.pa_threaded_mainloop_wait(self.main_loop), + .ready => break, + .failure => return error.OpeningDevice, + } + } + + return .{ + .pulseaudio = .{ + .main_loop = self.main_loop, + .ctx = self.ctx, + .stream = stream.?, + ._channels = device.channels, + ._format = format, + .sample_rate = sample_rate, + .writeFn = writeFn, + .write_ptr = undefined, + .vol = 1.0, + }, + }; + } + + const StreamStatus = struct { + main_loop: *c.pa_threaded_mainloop, + status: enum(u8) { + unknown, + ready, + failure, + }, + }; + + fn streamStateOp(stream: ?*c.pa_stream, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*StreamStatus, @alignCast(@alignOf(*StreamStatus), userdata.?)); + + switch (c.pa_stream_get_state(stream)) { + c.PA_STREAM_UNCONNECTED, + c.PA_STREAM_CREATING, + c.PA_STREAM_TERMINATED, + => {}, + c.PA_STREAM_READY => { + self.status = .ready; + c.pa_threaded_mainloop_signal(self.main_loop, 0); + }, + c.PA_STREAM_FAILED => { + self.status = .failure; + c.pa_threaded_mainloop_signal(self.main_loop, 0); + }, + else => unreachable, + } + } +}; + +pub const Player = struct { + main_loop: *c.pa_threaded_mainloop, + ctx: *c.pa_context, + stream: *c.pa_stream, + _channels: []main.Channel, + _format: main.Format, + sample_rate: u24, + writeFn: main.WriteFn, + write_ptr: [*]u8, + vol: f32, + + pub fn deinit(self: *Player) void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + c.pa_stream_set_write_callback(self.stream, null, null); + c.pa_stream_set_state_callback(self.stream, null, null); + c.pa_stream_set_underflow_callback(self.stream, null, null); + c.pa_stream_set_overflow_callback(self.stream, null, null); + _ = c.pa_stream_disconnect(self.stream); + c.pa_stream_unref(self.stream); + } + + pub fn start(self: *Player) !void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + const op = c.pa_stream_cork(self.stream, 0, null, null) orelse + return error.CannotPlay; + c.pa_operation_unref(op); + c.pa_stream_set_write_callback(self.stream, playbackStreamWriteOp, self); + } + + fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); + var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); + + var frames_left = nbytes; + while (frames_left > 0) { + var chunk_size = frames_left; + if (c.pa_stream_begin_write( + self.stream, + @ptrCast( + [*c]?*anyopaque, + @alignCast(@alignOf([*c]?*anyopaque), &self.write_ptr), + ), + &chunk_size, + ) != 0) { + if (std.debug.runtime_safety) unreachable; + return; + } + + for (self.channels()) |*ch, i| { + ch.*.ptr = self.write_ptr + self.format().frameSize(i); + } + + const frames = chunk_size / self.format().frameSize(self.channels().len); + self.writeFn(parent, frames); + + if (c.pa_stream_write(self.stream, self.write_ptr, chunk_size, null, 0, c.PA_SEEK_RELATIVE) != 0) { + if (std.debug.runtime_safety) unreachable; + return; + } + + frames_left -= chunk_size; + } + } + + pub fn play(self: *Player) !void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + if (c.pa_stream_is_corked(self.stream) > 0) { + const op = c.pa_stream_cork(self.stream, 0, null, null) orelse + return error.CannotPlay; + c.pa_operation_unref(op); + } + } + + pub fn pause(self: *Player) !void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + if (c.pa_stream_is_corked(self.stream) == 0) { + const op = c.pa_stream_cork(self.stream, 1, null, null) orelse + return error.CannotPause; + c.pa_operation_unref(op); + } + } + + pub fn paused(self: *Player) bool { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + return c.pa_stream_is_corked(self.stream) > 0; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + var cvolume: c.pa_cvolume = undefined; + _ = c.pa_cvolume_init(&cvolume); + _ = c.pa_cvolume_set(&cvolume, @intCast(c_uint, self.channels().len), c.pa_sw_volume_from_linear(vol)); + + performOperation( + self.main_loop, + c.pa_context_set_sink_input_volume( + self.ctx, + c.pa_stream_get_index(self.stream), + &cvolume, + successOp, + self, + ), + ); + } + + fn successOp(_: ?*c.pa_context, success: c_int, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); + if (success == 1) + c.pa_threaded_mainloop_signal(self.main_loop, 0); + } + + pub fn volume(self: *Player) !f32 { + c.pa_threaded_mainloop_lock(self.main_loop); + defer c.pa_threaded_mainloop_unlock(self.main_loop); + + performOperation( + self.main_loop, + c.pa_context_get_sink_input_info( + self.ctx, + c.pa_stream_get_index(self.stream), + sinkInputInfoOp, + self, + ), + ); + + return self.vol; + } + + fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); + + if (eol != 0) { + c.pa_threaded_mainloop_signal(self.main_loop, 0); + return; + } + + self.vol = @intToFloat(f32, info.*.volume.values[0]) / @intToFloat(f32, c.PA_VOLUME_NORM); + } + + pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { + var ptr = channel.ptr + self.format().frameSize(self.channels().len) * frame; + std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; + } + + pub fn channels(self: Player) []main.Channel { + return self._channels; + } + + pub fn format(self: Player) main.Format { + return self._format; + } + + pub fn sampleRate(self: Player) u24 { + return self.sample_rate; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.id); + allocator.free(device.name); + allocator.free(device.channels); +} + +fn performOperation(main_loop: *c.pa_threaded_mainloop, op: ?*c.pa_operation) void { + while (true) { + switch (c.pa_operation_get_state(op)) { + c.PA_OPERATION_RUNNING => c.pa_threaded_mainloop_wait(main_loop), + c.PA_OPERATION_DONE => return c.pa_operation_unref(op), + c.PA_OPERATION_CANCELLED => { + std.debug.assert(false); + c.pa_operation_unref(op); + return; + }, + else => unreachable, + } + } +} + +pub const available_formats = &[_]main.Format{ + .u8, .i16, + .i24, .i24_4b, + .i32, .f32, +}; + +pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.Channel.Id { + return switch (pos) { + c.PA_CHANNEL_POSITION_MONO => .front_center, + c.PA_CHANNEL_POSITION_FRONT_LEFT => .front_left, // PA_CHANNEL_POSITION_LEFT + c.PA_CHANNEL_POSITION_FRONT_RIGHT => .front_right, // PA_CHANNEL_POSITION_RIGHT + c.PA_CHANNEL_POSITION_FRONT_CENTER => .front_center, // PA_CHANNEL_POSITION_CENTER + c.PA_CHANNEL_POSITION_REAR_CENTER => .back_center, + c.PA_CHANNEL_POSITION_LFE => .lfe, // PA_CHANNEL_POSITION_SUBWOOFER + c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER => .front_left_center, + c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER => .front_right_center, + c.PA_CHANNEL_POSITION_SIDE_LEFT => .side_left, + c.PA_CHANNEL_POSITION_SIDE_RIGHT => .side_right, + + // TODO: .front_center? + c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => error.Invalid, + + c.PA_CHANNEL_POSITION_TOP_CENTER => .top_center, + c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT => .top_front_left, + c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT => .top_front_right, + c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER => .top_front_center, + c.PA_CHANNEL_POSITION_TOP_REAR_LEFT => .top_back_left, + c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT => .top_back_right, + c.PA_CHANNEL_POSITION_TOP_REAR_CENTER => .top_back_center, + + else => error.Invalid, + }; +} + +pub fn toPAFormat(format: main.Format) !c.pa_sample_format_t { + return switch (format) { + .u8 => c.PA_SAMPLE_U8, + .i16 => if (is_little) c.PA_SAMPLE_S16LE else c.PA_SAMPLE_S16BE, + .i24 => if (is_little) c.PA_SAMPLE_S24LE else c.PA_SAMPLE_S24LE, + .i24_4b => if (is_little) c.PA_SAMPLE_S24_32LE else c.PA_SAMPLE_S24_32BE, + .i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE, + .f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE, + + .f64, .i8 => error.Invalid, + }; +} + +pub fn toPAChannelMap(channels: []const main.Channel) !c.pa_channel_map { + var channel_map: c.pa_channel_map = undefined; + channel_map.channels = @intCast(u5, channels.len); + for (channels) |ch, i| + channel_map.map[i] = try toPAChannelPos(ch.id); + return channel_map; +} + +fn toPAChannelPos(channel_id: main.Channel.Id) !c.pa_channel_position_t { + return switch (channel_id) { + .front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT, + .front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT, + .front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER, + .lfe => c.PA_CHANNEL_POSITION_LFE, + .front_left_center => c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER, + .front_right_center => c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER, + .back_center => c.PA_CHANNEL_POSITION_REAR_CENTER, + .side_left => c.PA_CHANNEL_POSITION_SIDE_LEFT, + .side_right => c.PA_CHANNEL_POSITION_SIDE_RIGHT, + .top_center => c.PA_CHANNEL_POSITION_TOP_CENTER, + .top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT, + .top_front_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER, + .top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, + .top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT, + .top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER, + .top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT, + }; +} + +test { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/libs/sysaudio/src/soundio.zig b/libs/sysaudio/src/soundio.zig deleted file mode 100644 index 06a674ed..00000000 --- a/libs/sysaudio/src/soundio.zig +++ /dev/null @@ -1,376 +0,0 @@ -const std = @import("std"); -const Mode = @import("main.zig").Mode; -const Format = @import("main.zig").Format; -const DataCallback = @import("main.zig").DataCallback; -const c = @import("soundio").c; -const Aim = @import("soundio").Aim; -const SoundIo = @import("soundio").SoundIo; -const SoundIoFormat = @import("soundio").Format; -const SoundIoDevice = @import("soundio").Device; -const SoundIoInStream = @import("soundio").InStream; -const SoundIoOutStream = @import("soundio").OutStream; - -const SoundIoStream = union(Mode) { - input: SoundIoInStream, - output: SoundIoOutStream, -}; - -const Audio = @This(); - -const default_buffer_size_per_channel = 1024; // 21.33ms - -pub const Device = struct { - properties: Properties, - - // Internal fields. - handle: SoundIoStream, - data_callback: ?DataCallback = null, - user_data: ?*anyopaque = null, - planar_buffer: [512000]u8 = undefined, - started: bool = false, - - pub const Options = struct { - mode: Mode = .output, - format: ?Format = null, - is_raw: ?bool = null, - channels: ?u8 = null, - sample_rate: ?u32 = null, - id: ?[:0]const u8 = null, - name: ?[]const u8 = null, - }; - - pub const Properties = struct { - mode: Mode, - format: Format, - is_raw: bool, - channels: u8, - sample_rate: u32, - id: [:0]const u8, - name: []const u8, - }; - - pub fn deinit(self: *Device, allocator: std.mem.Allocator) void { - switch (self.handle) { - .input => |d| d.deinit(), - .output => |d| d.deinit(), - } - allocator.destroy(self); - } - - pub fn setCallback(self: *Device, callback: DataCallback, data: *anyopaque) void { - self.data_callback = callback; - self.user_data = data; - switch (self.handle) { - .input => |_| @panic("input not supported yet"), - .output => |d| { - // TODO(sysaudio): support other formats - d.setFormat(.float32LE); - - d.setWriteCallback((struct { - fn cCallback( - c_outstream: ?[*]c.SoundIoOutStream, - frame_count_min: c_int, - frame_count_max: c_int, - ) callconv(.C) void { - const outstream = SoundIoOutStream{ .handle = @ptrCast(*c.SoundIoOutStream, c_outstream) }; - const device = @ptrCast(*Device, @alignCast(@alignOf(Device), outstream.handle.userdata)); - - // TODO(sysaudio): provide callback with outstream.sampleRate() - - // TODO(sysaudio): according to issue tracker and PR from mason (did we include it?) - // there may be issues with frame_count_max being way too large on Windows. May need - // to artificially limit it or use Mason's PR. - - // The data callback gives us planar data, e.g. in AAAABBBB format for channels - // A and B. WebAudio similarly requires data in planar format. libsoundio however - // does not guarantee planar data format, it may be in interleaved format ABABABAB. - // Invoke our data callback with a temporary buffer, this involves one copy later - // but it's such a small amount of memory it is entirely negligible. - const layout = outstream.layout(); - - const desired_frame_count = default_buffer_size_per_channel * layout.channelCount(); - const total_frame_count = if (frame_count_max > desired_frame_count) - if (frame_count_min <= desired_frame_count) - @intCast(usize, desired_frame_count) - else - @intCast(usize, frame_count_min) - else - @intCast(usize, frame_count_max); - - const buffer_size: usize = @sizeOf(f32) * total_frame_count * @intCast(usize, layout.channelCount()); - const addr = @ptrToInt(&device.planar_buffer); - const aligned_addr = std.mem.alignForward(addr, @alignOf(f32)); - const padding = aligned_addr - addr; - const planar_buffer = device.planar_buffer[padding .. padding + buffer_size]; - device.data_callback.?(device, device.user_data.?, planar_buffer); - - var frames_left = total_frame_count; - var frame_offset: usize = 0; - while (frames_left > 0) { - var frame_count: i32 = @intCast(i32, frames_left); - - var areas: [*]c.SoundIoChannelArea = undefined; - // TODO(sysaudio): improve error handling - outstream.beginWrite( - @ptrCast([*]?[*]c.SoundIoChannelArea, &areas), - &frame_count, - ) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)}); - - if (frame_count == 0) break; - - var channel: usize = 0; - while (channel < @intCast(usize, layout.channelCount())) : (channel += 1) { - const channel_ptr = areas[channel].ptr; - var frame: c_int = 0; - while (frame < frame_count) : (frame += 1) { - const sample_start = (channel * total_frame_count * @sizeOf(f32)) + ((frame_offset + @intCast(usize, frame)) * @sizeOf(f32)); - const src = @ptrCast(*f32, @alignCast(@alignOf(f32), &planar_buffer[sample_start])); - const dst = &channel_ptr[@intCast(usize, areas[channel].step * frame)]; - @ptrCast(*f32, @alignCast(@alignOf(f32), dst)).* = src.*; - } - } - // TODO(sysaudio): improve error handling - outstream.endWrite() catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)}); - frames_left -= @intCast(usize, frame_count); - frame_offset += @intCast(usize, frame_count); - } - } - }).cCallback); - }, - } - } - - pub fn pause(device: *Device) Error!void { - if (!device.started) return; - return (switch (device.handle) { - .input => |d| d.pause(true), - .output => |d| d.pause(true), - }) catch |err| { - return switch (err) { - error.OutOfMemory => error.OutOfMemory, - else => @panic(@errorName(err)), - }; - }; - } - - pub fn start(device: *Device) Error!void { - // TODO(sysaudio): after pause, may need to call d.pause(false) instead of d.start()? - if (!device.started) { - device.started = true; - return (switch (device.handle) { - .input => |d| d.start(), - .output => |d| d.start(), - }) catch |err| { - return switch (err) { - error.OutOfMemory => error.OutOfMemory, - else => @panic(@errorName(err)), - }; - }; - } else { - return (switch (device.handle) { - .input => |d| d.pause(false), - .output => |d| d.pause(false), - }) catch |err| { - return switch (err) { - error.OutOfMemory => error.OutOfMemory, - else => @panic(@errorName(err)), - }; - }; - } - } -}; - -pub const DeviceIterator = struct { - ctx: Audio, - mode: Mode, - device_len: u16, - index: u16, - - pub fn next(self: *DeviceIterator) IteratorError!?Device.Options { - if (self.index < self.device_len) { - const device_desc = switch (self.mode) { - .input => self.ctx.handle.getInputDevice(self.index) orelse return null, - .output => self.ctx.handle.getOutputDevice(self.index) orelse return null, - }; - self.index += 1; - return Device.Options{ - .mode = switch (@intToEnum(Aim, device_desc.handle.aim)) { - .input => .input, - .output => .output, - }, - .is_raw = device_desc.handle.is_raw, - .id = device_desc.id(), - .name = device_desc.name(), - }; - } - return null; - } -}; - -// TODO(sysaudio): standardize errors across backends -pub const IteratorError = error{OutOfMemory}; -pub const Error = error{ - OutOfMemory, - InvalidDeviceID, - InvalidParameter, - NoDeviceFound, - AlreadyConnected, - CannotConnect, - UnsupportedOS, - UnsupportedBackend, - DeviceUnavailable, - Invalid, - OpeningDevice, - BackendDisconnected, - SystemResources, - NoSuchClient, - IncompatibleBackend, - IncompatibleDevice, - InitAudioBackend, - NoSuchDevice, - BackendUnavailable, - Streaming, - Interrupted, - Underflow, - EncodingString, -}; - -handle: SoundIo, - -pub fn init() Error!Audio { - var self = Audio{ - .handle = try SoundIo.init(), - }; - self.handle.connect() catch |err| { - return switch (err) { - error.SystemResources, error.NoSuchClient => error.CannotConnect, - error.Invalid => error.AlreadyConnected, - error.OutOfMemory => error.OutOfMemory, - else => unreachable, - }; - }; - self.handle.flushEvents(); - return self; -} - -pub fn deinit(self: Audio) void { - self.handle.deinit(); -} - -pub fn waitEvents(self: Audio) void { - self.handle.waitEvents(); -} - -pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device { - var sio_device: SoundIoDevice = undefined; - - if (options.id) |id| { - if (options.is_raw == null) - return error.InvalidParameter; - - sio_device = switch (options.mode) { - .input => self.handle.getInputDeviceFromID(id, options.is_raw.?), - .output => self.handle.getOutputDeviceFromID(id, options.is_raw.?), - } orelse { - return if (switch (options.mode) { - .input => self.handle.inputDeviceCount().?, - .output => self.handle.outputDeviceCount().?, - } == 0) - error.NoDeviceFound - else - error.DeviceUnavailable; - }; - } else { - const id = switch (options.mode) { - .input => self.handle.defaultInputDeviceIndex(), - .output => self.handle.defaultOutputDeviceIndex(), - } orelse return error.NoDeviceFound; - sio_device = switch (options.mode) { - .input => self.handle.getInputDevice(id), - .output => self.handle.getOutputDevice(id), - } orelse return error.DeviceUnavailable; - } - - const handle = switch (options.mode) { - .input => SoundIoStream{ .input = try sio_device.createInStream() }, - .output => SoundIoStream{ .output = try sio_device.createOutStream() }, - }; - - switch (handle) { - .input => |d| try d.open(), - .output => |d| try d.open(), - } - - const device = try allocator.create(Device); - switch (handle) { - .input => |d| d.handle.userdata = device, - .output => |d| d.handle.userdata = device, - } - - // TODO(sysaudio): handle big endian architectures - const format: Format = switch (handle) { - .input => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) { - .U8 => .U8, - .S16LE => .S16, - .S24LE => .S24, - .S32LE => .S32, - .float32LE => .F32, - else => return error.InvalidParameter, - }, - .output => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) { - .U8 => .U8, - .S16LE => .S16, - .S24LE => .S24, - .S32LE => .S32, - .float32LE => .F32, - else => return error.InvalidParameter, - }, - }; - - // TODO(sysaudio): Get the device name. Calling span or sliceTo on the name is causing segfaults on NixOS - // const name_ptr = switch(handle) { - // .input => |d| d.handle.name, - // .output => |d| d.handle.name, - // }; - // const name = std.mem.sliceTo(name_ptr, 0); - - var properties = Device.Properties{ - .is_raw = options.is_raw orelse false, - .format = format, - .mode = options.mode, - .id = std.mem.span(sio_device.handle.id), - .name = "", - .channels = @intCast(u8, switch (handle) { - .input => |d| d.layout().channelCount(), - .output => |d| d.layout().channelCount(), - }), - .sample_rate = @intCast(u32, switch (handle) { - .input => |d| d.sampleRate(), - .output => |d| d.sampleRate(), - }), - }; - - device.* = .{ - .properties = properties, - .handle = handle, - }; - return device; -} - -pub fn outputDeviceIterator(self: Audio) DeviceIterator { - return .{ - .ctx = self, - .mode = .output, - .device_len = self.handle.outputDeviceCount() orelse 0, - .index = 0, - }; -} - -pub fn inputDeviceIterator(self: Audio) DeviceIterator { - return .{ - .ctx = self, - .mode = .input, - .device_len = self.handle.inputDeviceCount() orelse 0, - .index = 0, - }; -} diff --git a/libs/sysaudio/src/util.zig b/libs/sysaudio/src/util.zig new file mode 100644 index 00000000..7ea21ff6 --- /dev/null +++ b/libs/sysaudio/src/util.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const main = @import("main.zig"); + +pub const DevicesInfo = struct { + list: std.ArrayListUnmanaged(main.Device), + default_output: ?usize, + default_input: ?usize, + + pub fn init() DevicesInfo { + return .{ + .list = .{}, + .default_output = null, + .default_input = null, + }; + } + + pub fn clear(self: *DevicesInfo, allocator: std.mem.Allocator) void { + self.default_output = null; + self.default_input = null; + self.list.clearAndFree(allocator); + } + + pub fn get(self: DevicesInfo, i: usize) main.Device { + return self.list.items[i]; + } + + pub fn default(self: DevicesInfo, mode: main.Device.Mode) ?main.Device { + const index = switch (mode) { + .playback => self.default_output, + .capture => self.default_input, + } orelse return null; + return self.get(index); + } + + pub fn setDefault(self: *DevicesInfo, mode: main.Device.Mode, i: usize) void { + switch (mode) { + .playback => self.default_output = i, + .capture => self.default_input = i, + } + } +}; + +pub fn Range(comptime T: type) type { + return struct { + const Self = @This(); + + min: T, + max: T, + + pub fn clamp(self: Self, val: T) T { + return std.math.clamp(val, self.min, self.max); + } + }; +} + +pub fn doNothing() callconv(.C) void {} diff --git a/libs/sysaudio/src/wasapi.zig b/libs/sysaudio/src/wasapi.zig new file mode 100644 index 00000000..0b514e99 --- /dev/null +++ b/libs/sysaudio/src/wasapi.zig @@ -0,0 +1,779 @@ +const std = @import("std"); +const win32 = @import("wasapi/win32.zig"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + enumerator: ?*win32.IMMDeviceEnumerator, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.DeviceChangeFn, + userdata: ?*anyopaque, + notif_client: win32.IMMNotificationClient, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + const flags = win32.COINIT_APARTMENTTHREADED | win32.COINIT_DISABLE_OLE1DDE; + var hr = win32.CoInitializeEx(null, flags); + switch (hr) { + win32.S_OK, + win32.S_FALSE, + win32.RPC_E_CHANGED_MODE, + => {}, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.E_UNEXPECTED => return error.SystemResources, + else => unreachable, + } + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .enumerator = blk: { + var enumerator: ?*win32.IMMDeviceEnumerator = null; + hr = win32.CoCreateInstance( + win32.CLSID_MMDeviceEnumerator, + null, + win32.CLSCTX_ALL, + win32.IID_IMMDeviceEnumerator, + @ptrCast(*?*anyopaque, &enumerator), + ); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.CLASS_E_NOAGGREGATION => return error.SystemResources, + win32.REGDB_E_CLASSNOTREG => unreachable, + else => unreachable, + } + break :blk enumerator; + }, + .watcher = if (options.deviceChangeFn) |deviceChangeFn| .{ + .deviceChangeFn = deviceChangeFn, + .userdata = options.userdata, + .notif_client = win32.IMMNotificationClient{ + .vtable = &.{ + .base = .{ + .QueryInterface = queryInterfaceCB, + .AddRef = addRefCB, + .Release = releaseCB, + }, + .OnDeviceStateChanged = onDeviceStateChangedCB, + .OnDeviceAdded = onDeviceAddedCB, + .OnDeviceRemoved = onDeviceRemovedCB, + .OnDefaultDeviceChanged = onDefaultDeviceChangedCB, + .OnPropertyValueChanged = onPropertyValueChangedCB, + }, + }, + } else null, + }; + + if (options.deviceChangeFn) |_| { + hr = self.enumerator.?.RegisterEndpointNotificationCallback(&self.watcher.?.notif_client); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.SystemResources, + } + } + + return .{ .wasapi = self }; + } + + fn queryInterfaceCB(self: *const win32.IUnknown, riid: ?*const win32.Guid, ppv: ?*?*anyopaque) callconv(std.os.windows.WINAPI) win32.HRESULT { + if (riid.?.eql(win32.IID_IUnknown.*) or riid.?.eql(win32.IID_IMMNotificationClient.*)) { + ppv.?.* = @intToPtr(?*anyopaque, @ptrToInt(self)); + _ = self.AddRef(); + return win32.S_OK; + } else { + ppv.?.* = null; + return win32.E_NOINTERFACE; + } + } + + fn addRefCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 { + return 1; + } + + fn releaseCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 { + return 1; + } + + fn onDeviceStateChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", self); + watcher.deviceChangeFn(watcher.userdata); + return win32.S_OK; + } + + fn onDeviceAddedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", self); + watcher.deviceChangeFn(watcher.userdata); + return win32.S_OK; + } + + fn onDeviceRemovedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", self); + watcher.deviceChangeFn(watcher.userdata); + return win32.S_OK; + } + + fn onDefaultDeviceChangedCB(self: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", self); + watcher.deviceChangeFn(watcher.userdata); + return win32.S_OK; + } + + fn onPropertyValueChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", self); + watcher.deviceChangeFn(watcher.userdata); + return win32.S_OK; + } + + pub fn deinit(self: *Context) void { + if (self.watcher) |*watcher| { + _ = self.enumerator.?.UnregisterEndpointNotificationCallback(&watcher.notif_client); + } + _ = self.enumerator.?.Release(); + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + // get default devices id + var default_playback_device: ?*win32.IMMDevice = null; + var hr = self.enumerator.?.GetDefaultAudioEndpoint(.render, .multimedia, &default_playback_device); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + // TODO: win32.E_NOTFOUND!? + else => return error.OpeningDevice, + } + defer _ = default_playback_device.?.Release(); + + var default_capture_device: ?*win32.IMMDevice = null; + hr = self.enumerator.?.GetDefaultAudioEndpoint(.capture, .multimedia, &default_capture_device); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + // TODO: win32.E_NOTFOUND!? + else => return error.OpeningDevice, + } + defer _ = default_capture_device.?.Release(); + + var default_playback_id_u16: ?[*:0]u16 = undefined; + hr = default_playback_device.?.GetId(&default_playback_id_u16); + defer win32.CoTaskMemFree(default_playback_id_u16); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + const default_playback_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_playback_id_u16.?)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + defer self.allocator.free(default_playback_id); + + var default_capture_id_u16: ?[*:0]u16 = undefined; + hr = default_capture_device.?.GetId(&default_capture_id_u16); + defer win32.CoTaskMemFree(default_capture_id_u16); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + const default_capture_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_capture_id_u16.?)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + defer self.allocator.free(default_capture_id); + + // enumerate + var collection: ?*win32.IMMDeviceCollection = null; + hr = self.enumerator.?.EnumAudioEndpoints( + win32.DataFlow.all, + win32.DEVICE_STATE_ACTIVE, + &collection, + ); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + defer _ = collection.?.Release(); + + var device_count: u32 = 0; + hr = collection.?.GetCount(&device_count); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + else => return error.OpeningDevice, + } + + var i: u32 = 0; + while (i < device_count) : (i += 1) { + var imm_device: ?*win32.IMMDevice = null; + hr = collection.?.Item(i, &imm_device); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + else => return error.OpeningDevice, + } + defer _ = imm_device.?.Release(); + + var property_store: ?*win32.IPropertyStore = null; + var variant: win32.PROPVARIANT = undefined; + hr = imm_device.?.OpenPropertyStore(win32.STGM_READ, &property_store); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + defer _ = property_store.?.Release(); + + hr = property_store.?.GetValue(&win32.PKEY_AudioEngine_DeviceFormat, &variant); + switch (hr) { + win32.S_OK, win32.INPLACE_S_TRUNCATED => {}, + else => return error.OpeningDevice, + } + var wf = @ptrCast( + *win32.WAVEFORMATEXTENSIBLE, + variant.anon.anon.anon.blob.pBlobData, + ); + defer win32.CoTaskMemFree(variant.anon.anon.anon.blob.pBlobData); + + var device = main.Device{ + .mode = blk: { + var endpoint: ?*win32.IMMEndpoint = null; + hr = imm_device.?.QueryInterface(win32.IID_IMMEndpoint, @ptrCast(?*?*anyopaque, &endpoint)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_NOINTERFACE => unreachable, + else => unreachable, + } + defer _ = endpoint.?.Release(); + + var dataflow: win32.DataFlow = undefined; + hr = endpoint.?.GetDataFlow(&dataflow); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + else => return error.OpeningDevice, + } + + break :blk switch (dataflow) { + .render => .playback, + .capture => .capture, + else => unreachable, + }; + }, + .channels = blk: { + var chn_arr = std.ArrayList(main.Channel).init(self.allocator); + var channel: u32 = win32.SPEAKER_FRONT_LEFT; + while (channel < win32.SPEAKER_ALL) : (channel <<= 1) { + if (wf.dwChannelMask & channel != 0) + try chn_arr.append(.{ .id = fromWASApiChannel(channel) }); + } + break :blk try chn_arr.toOwnedSlice(); + }, + .sample_rate = .{ + .min = @intCast(u24, wf.Format.nSamplesPerSec), + .max = @intCast(u24, wf.Format.nSamplesPerSec), + }, + .formats = blk: { + var audio_client: ?*win32.IAudioClient = null; + hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable, + else => return error.OpeningDevice, + } + defer _ = audio_client.?.Release(); + + var fmt_arr = std.ArrayList(main.Format).init(self.allocator); + var closest_match: ?*win32.WAVEFORMATEX = null; + for (std.meta.tags(main.Format)) |format| { + setWaveFormatFormat(wf, format) catch continue; + if (audio_client.?.IsFormatSupported( + .SHARED, + @ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), wf)), + &closest_match, + ) == win32.S_OK) { + try fmt_arr.append(format); + } + } + + break :blk try fmt_arr.toOwnedSlice(); + }, + .id = blk: { + var id_u16: ?[*:0]u16 = undefined; + hr = imm_device.?.GetId(&id_u16); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + defer win32.CoTaskMemFree(id_u16); + + break :blk std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(id_u16.?)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + }, + .name = blk: { + hr = property_store.?.GetValue(&win32.PKEY_Device_FriendlyName, &variant); + switch (hr) { + win32.S_OK, win32.INPLACE_S_TRUNCATED => {}, + else => return error.OpeningDevice, + } + defer win32.CoTaskMemFree(variant.anon.anon.anon.pwszVal); + + break :blk std.unicode.utf16leToUtf8AllocZ( + self.allocator, + std.mem.span(variant.anon.anon.anon.pwszVal.?), + ) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + }, + }; + + try self.devices_info.list.append(self.allocator, device); + if (self.devices_info.default(device.mode) == null) { + switch (device.mode) { + .playback => if (std.mem.eql(u8, device.id, default_playback_id)) { + self.devices_info.setDefault(.playback, self.devices_info.list.items.len - 1); + }, + .capture => if (std.mem.eql(u8, device.id, default_capture_id)) { + self.devices_info.setDefault(.capture, self.devices_info.list.items.len - 1); + }, + } + } + } + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + fn fromWASApiChannel(speaker: u32) main.Channel.Id { + return switch (speaker) { + win32.SPEAKER_FRONT_CENTER => .front_center, + win32.SPEAKER_FRONT_LEFT => .front_left, + win32.SPEAKER_FRONT_RIGHT => .front_right, + win32.SPEAKER_FRONT_LEFT_OF_CENTER => .front_left_center, + win32.SPEAKER_FRONT_RIGHT_OF_CENTER => .front_right_center, + win32.SPEAKER_BACK_CENTER => .back_center, + win32.SPEAKER_SIDE_LEFT => .side_left, + win32.SPEAKER_SIDE_RIGHT => .side_right, + win32.SPEAKER_TOP_CENTER => .top_center, + win32.SPEAKER_TOP_FRONT_CENTER => .top_front_center, + win32.SPEAKER_TOP_FRONT_LEFT => .top_front_left, + win32.SPEAKER_TOP_FRONT_RIGHT => .top_front_right, + win32.SPEAKER_TOP_BACK_CENTER => .top_back_center, + win32.SPEAKER_TOP_BACK_LEFT => .top_back_left, + win32.SPEAKER_TOP_BACK_RIGHT => .top_back_right, + win32.SPEAKER_LOW_FREQUENCY => .lfe, + else => unreachable, + }; + } + + fn setWaveFormatFormat(wf: *win32.WAVEFORMATEXTENSIBLE, format: main.Format) !void { + switch (format) { + .u8, .i16, .i24, .i24_4b, .i32 => { + wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*; + }, + .f32, .f64 => { + wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*; + }, + .i8 => return error.Invalid, + } + wf.Format.wBitsPerSample = format.sizeBits(); + wf.Samples.wValidBitsPerSample = format.validSizeBits(); + } + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + var imm_device: ?*win32.IMMDevice = null; + var id_u16 = std.unicode.utf8ToUtf16LeWithNull(self.allocator, device.id) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + defer self.allocator.free(id_u16); + var hr = self.enumerator.?.GetDevice(id_u16, &imm_device); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + // TODO: win32.E_NOTFOUND!? + else => return error.OpeningDevice, + } + + var audio_client: ?*win32.IAudioClient = null; + hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + else => return error.OpeningDevice, + } + + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate); + + const wave_format = win32.WAVEFORMATEXTENSIBLE{ + .Format = .{ + .wFormatTag = win32.WAVE_FORMAT_EXTENSIBLE, + .nChannels = @intCast(u16, device.channels.len), + .nSamplesPerSec = sample_rate, + .nAvgBytesPerSec = sample_rate * format.frameSize(device.channels.len), + .nBlockAlign = format.frameSize(device.channels.len), + .wBitsPerSample = format.sizeBits(), + .cbSize = 0x16, + }, + .Samples = .{ + .wValidBitsPerSample = format.validSizeBits(), + }, + .dwChannelMask = toChannelMask(device.channels), + .SubFormat = toSubFormat(format) catch return error.OpeningDevice, + }; + + hr = audio_client.?.Initialize( + .SHARED, + win32.AUDCLNT_STREAMFLAGS_NOPERSIST, + 0, + 0, + @ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)), + null, + ); + switch (hr) { + win32.S_OK => {}, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable, + win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, + win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better + win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice, + win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_IN_USE => unreachable, + win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice, + win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice, + win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable, + win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable, + win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } + errdefer _ = audio_client.?.Release(); + + var render_client: ?*win32.IAudioRenderClient = null; + hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @ptrCast(?*?*anyopaque, &render_client)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } + + var simple_volume: ?*win32.ISimpleAudioVolume = null; + hr = audio_client.?.GetService(win32.IID_ISimpleAudioVolume, @ptrCast(?*?*anyopaque, &simple_volume)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } + + return .{ + .wasapi = .{ + .thread = undefined, + .mutex = .{}, + ._channels = device.channels, + ._format = format, + .sample_rate = sample_rate, + .writeFn = writeFn, + .audio_client = audio_client, + .simple_volume = simple_volume, + .imm_device = imm_device, + .render_client = render_client, + .is_paused = false, + .vol = 1.0, + .aborted = .{ .value = false }, + }, + }; + } + + fn toSubFormat(format: main.Format) !win32.Guid { + return switch (format) { + .u8 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .i16 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .i24 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .i24_4b => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .i32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*, + .f64 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*, + else => error.Invalid, + }; + } + + fn toChannelMask(channels: []const main.Channel) u32 { + var mask: u32 = 0; + for (channels) |ch| { + mask |= switch (ch.id) { + .front_center => win32.SPEAKER_FRONT_CENTER, + .front_left => win32.SPEAKER_FRONT_LEFT, + .front_right => win32.SPEAKER_FRONT_RIGHT, + .front_left_center => win32.SPEAKER_FRONT_LEFT_OF_CENTER, + .front_right_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, + .top_center => win32.SPEAKER_TOP_CENTER, + .top_front_center => win32.SPEAKER_TOP_FRONT_CENTER, + .top_front_left => win32.SPEAKER_TOP_FRONT_LEFT, + .top_front_right => win32.SPEAKER_TOP_FRONT_RIGHT, + .top_back_center => win32.SPEAKER_TOP_BACK_CENTER, + .top_back_left => win32.SPEAKER_TOP_BACK_LEFT, + .top_back_right => win32.SPEAKER_TOP_BACK_RIGHT, + .lfe => win32.SPEAKER_LOW_FREQUENCY, + }; + } + return mask; + } +}; + +pub const Player = struct { + thread: std.Thread, + mutex: std.Thread.Mutex, + _channels: []main.Channel, + _format: main.Format, + sample_rate: u24, + writeFn: main.WriteFn, + simple_volume: ?*win32.ISimpleAudioVolume, + imm_device: ?*win32.IMMDevice, + audio_client: ?*win32.IAudioClient, + render_client: ?*win32.IAudioRenderClient, + aborted: std.atomic.Atomic(bool), + is_paused: bool, + vol: f32, + + pub fn deinit(self: *Player) void { + self.aborted.store(true, .Unordered); + self.thread.join(); + _ = self.simple_volume.?.Release(); + _ = self.render_client.?.Release(); + _ = self.audio_client.?.Release(); + _ = self.imm_device.?.Release(); + } + + pub fn start(self: *Player) !void { + self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + error.Unexpected => unreachable, + }; + } + + fn writeLoop(self: *Player) void { + var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); + + var hr = self.audio_client.?.Start(); + switch (hr) { + win32.S_OK => {}, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_NOT_STOPPED => unreachable, + win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return, + else => unreachable, + } + + while (!self.aborted.load(.Unordered)) { + var frames_buf: u32 = 0; + hr = self.audio_client.?.GetBufferSize(&frames_buf); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return, + else => unreachable, + } + + var frames_used: u32 = 0; + hr = self.audio_client.?.GetCurrentPadding(&frames_used); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return, + else => unreachable, + } + const writable_frame_count = frames_buf - frames_used; + if (writable_frame_count > 0) { + var data: [*]u8 = undefined; + hr = self.render_client.?.GetBuffer(writable_frame_count, @ptrCast(?*?*u8, &data)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.AUDCLNT_E_BUFFER_ERROR => unreachable, + win32.AUDCLNT_E_BUFFER_TOO_LARGE => unreachable, + win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable, + win32.AUDCLNT_E_OUT_OF_ORDER => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return, + win32.AUDCLNT_E_BUFFER_OPERATION_PENDING => continue, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return, + else => unreachable, + } + + for (self.channels()) |*ch, i| { + ch.*.ptr = data + self.format().frameSize(i); + } + + self.writeFn(parent, writable_frame_count); + hr = self.render_client.?.ReleaseBuffer(writable_frame_count, 0); + switch (hr) { + win32.S_OK => {}, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_INVALID_SIZE => unreachable, + win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable, + win32.AUDCLNT_E_OUT_OF_ORDER => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return, + else => unreachable, + } + } + } + } + + pub fn play(self: *Player) !void { + if (self.paused()) { + const hr = self.audio_client.?.Start(); + switch (hr) { + win32.S_OK => {}, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_NOT_STOPPED => unreachable, + win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPlay, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPlay, + else => unreachable, + } + self.is_paused = false; + } + } + + pub fn pause(self: *Player) !void { + if (!self.paused()) { + const hr = self.audio_client.?.Stop(); + switch (hr) { + win32.S_OK => {}, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPause, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPause, + else => unreachable, + } + self.is_paused = true; + } + } + + pub fn paused(self: Player) bool { + return self.is_paused; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + const hr = self.simple_volume.?.SetMasterVolume(vol, null); + switch (hr) { + win32.S_OK => {}, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotSetVolume, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotSetVolume, + else => return error.CannotSetVolume, + } + } + + pub fn volume(self: Player) !f32 { + var vol: f32 = 0; + const hr = self.simple_volume.?.GetMasterVolume(&vol); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotGetVolume, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotGetVolume, + else => return error.CannotGetVolume, + } + return vol; + } + + pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void { + var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len); + std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; + } + + pub fn channels(self: Player) []main.Channel { + return self._channels; + } + + pub fn format(self: Player) main.Format { + return self._format; + } + + pub fn sampleRate(self: Player) u24 { + return self.sample_rate; + } +}; + +pub fn freeDevice(allocator: std.mem.Allocator, self: main.Device) void { + allocator.free(self.id); + allocator.free(self.name); + allocator.free(self.formats); + allocator.free(self.channels); +} + +test { + std.testing.refAllDeclsRecursive(@This()); +} \ No newline at end of file diff --git a/libs/sysaudio/src/wasapi/win32.zig b/libs/sysaudio/src/wasapi/win32.zig new file mode 100644 index 00000000..1a5dd63f --- /dev/null +++ b/libs/sysaudio/src/wasapi/win32.zig @@ -0,0 +1,1645 @@ +const WINAPI = @import("std").os.windows.WINAPI; +pub const Guid = extern union { + Ints: extern struct { + a: u32, + b: u16, + c: u16, + d: [8]u8, + }, + Bytes: [16]u8, + const hex_offsets = switch (@import("builtin").target.cpu.arch.endian()) { + .Big => [16]u6{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 }, + .Little => [16]u6{ 6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34 }, + }; + pub fn initString(s: []const u8) Guid { + var guid = Guid{ .Bytes = undefined }; + for (hex_offsets) |hex_offset, i| { + guid.Bytes[i] = decodeHexByte([2]u8{ s[hex_offset], s[hex_offset + 1] }); + } + return guid; + } + fn hexVal(c: u8) u4 { + if (c <= '9') return @intCast(u4, c - '0'); + if (c >= 'a') return @intCast(u4, c + 10 - 'a'); + return @intCast(u4, c + 10 - 'A'); + } + fn decodeHexByte(hex: [2]u8) u8 { + return @intCast(u8, hexVal(hex[0])) << 4 | hexVal(hex[1]); + } + pub fn eql(riid1: Guid, riid2: Guid) bool { + return riid1.Ints.a == riid2.Ints.a and + riid1.Ints.b == riid2.Ints.b and + riid1.Ints.c == riid2.Ints.c and + @import("std").mem.eql(u8, &riid1.Ints.d, &riid2.Ints.d) and + @import("std").mem.eql(u8, &riid1.Bytes, &riid2.Bytes); + } +}; +pub const PROPERTYKEY = extern struct { + fmtid: Guid, + pid: u32, +}; +pub const DECIMAL = extern struct { + wReserved: u16, + anon1: extern union { + anon: extern struct { + scale: u8, + sign: u8, + }, + signscale: u16, + }, + Hi32: u32, + anon2: extern union { + anon: extern struct { + Lo32: u32, + Mid32: u32, + }, + Lo64: u64, + }, +}; +pub const LARGE_INTEGER = extern union { + anon: extern struct { + LowPart: u32, + HighPart: i32, + }, + u: extern struct { + LowPart: u32, + HighPart: i32, + }, + QuadPart: i64, +}; +pub const ULARGE_INTEGER = extern union { + anon: extern struct { + LowPart: u32, + HighPart: u32, + }, + u: extern struct { + LowPart: u32, + HighPart: u32, + }, + QuadPart: u64, +}; +pub const FILETIME = extern struct { + dwLowDateTime: u32, + dwHighDateTime: u32, +}; +pub const BOOL = i32; +pub const BSTR = *u16; +pub const PSTR = [*:0]u8; +pub const PWSTR = [*:0]u16; +pub const CHAR = u8; +pub const HRESULT = i32; +pub const S_OK = 0; +pub const S_FALSE = 1; +pub const E_NOTIMPL = -2147467263; +pub const E_OUTOFMEMORY = -2147024882; +pub const E_INVALIDARG = -2147024809; +pub const E_FAIL = -2147467259; +pub const E_UNEXPECTED = -2147418113; +pub const E_NOINTERFACE = -2147467262; +pub const E_POINTER = -2147467261; +pub const E_HANDLE = -2147024890; +pub const E_ABORT = -2147467260; +pub const E_ACCESSDENIED = -2147024891; +pub const E_BOUNDS = -2147483637; +pub const E_CHANGED_STATE = -2147483636; +pub const E_ILLEGAL_STATE_CHANGE = -2147483635; +pub const E_ILLEGAL_METHOD_CALL = -2147483634; +pub const CLASS_E_NOAGGREGATION = -2147221232; +pub const CLASS_E_CLASSNOTAVAILABLE = -2147221231; +pub const CLASS_E_NOTLICENSED = -2147221230; +pub const REGDB_E_CLASSNOTREG = -2147221164; +pub const RPC_E_CHANGED_MODE = -2147417850; +pub const SAFEARRAYBOUND = extern struct { + cElements: u32, + lLbound: i32, +}; +pub const SAFEARRAY = extern struct { + cDims: u16, + fFeatures: u16, + cbElements: u32, + cLocks: u32, + pvData: ?*anyopaque, + rgsabound: [1]SAFEARRAYBOUND, +}; +pub const CLIPDATA = extern struct { + cbSize: u32, + ulClipFmt: i32, + pClipData: ?*u8, +}; +pub const VERSIONEDSTREAM = extern struct { + guidVersion: Guid, + pStream: ?*IStream, +}; +pub const STREAM_SEEK = enum(u32) { + SET = 0, + CUR = 1, + END = 2, +}; +pub const STATSTG = extern struct { + pwcsName: ?PWSTR, + type: u32, + cbSize: ULARGE_INTEGER, + mtime: FILETIME, + ctime: FILETIME, + atime: FILETIME, + grfMode: u32, + grfLocksSupported: u32, + clsid: Guid, + grfStateBits: u32, + reserved: u32, +}; +pub const IStream = extern struct { + pub const VTable = extern struct { + base: ISequentialStream.VTable, + Seek: *const fn ( + self: *const IStream, + dlibMove: LARGE_INTEGER, + dwOrigin: STREAM_SEEK, + plibNewPosition: ?*ULARGE_INTEGER, + ) callconv(WINAPI) HRESULT, + SetSize: *const fn ( + self: *const IStream, + libNewSize: ULARGE_INTEGER, + ) callconv(WINAPI) HRESULT, + CopyTo: *const fn ( + self: *const IStream, + pstm: ?*IStream, + cb: ULARGE_INTEGER, + pcbRead: ?*ULARGE_INTEGER, + pcbWritten: ?*ULARGE_INTEGER, + ) callconv(WINAPI) HRESULT, + Commit: *const fn ( + self: *const IStream, + grfCommitFlags: u32, + ) callconv(WINAPI) HRESULT, + Revert: *const fn ( + self: *const IStream, + ) callconv(WINAPI) HRESULT, + LockRegion: *const fn ( + self: *const IStream, + libOffset: ULARGE_INTEGER, + cb: ULARGE_INTEGER, + dwLockType: u32, + ) callconv(WINAPI) HRESULT, + UnlockRegion: *const fn ( + self: *const IStream, + libOffset: ULARGE_INTEGER, + cb: ULARGE_INTEGER, + dwLockType: u32, + ) callconv(WINAPI) HRESULT, + Stat: *const fn ( + self: *const IStream, + pstatstg: ?*STATSTG, + grfStatFlag: u32, + ) callconv(WINAPI) HRESULT, + Clone: *const fn ( + self: *const IStream, + ppstm: ?*?*IStream, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const COINIT = u32; +pub const COINIT_MULTITHREADED = 0x0; +pub const COINIT_APARTMENTTHREADED = 0x2; +pub const COINIT_DISABLE_OLE1DDE = 0x4; +pub const COINIT_SPEED_OVER_MEMORY = 0x8; +pub const CLSCTX = u32; +pub const CLSCTX_ALL = 23; +pub extern "ole32" fn CoInitializeEx( + pvReserved: ?*anyopaque, + dwCoInit: COINIT, +) callconv(WINAPI) HRESULT; +pub extern "ole32" fn CoCreateInstance( + rclsid: ?*const Guid, + pUnkOuter: ?*IUnknown, + dwClsContext: CLSCTX, + riid: *const Guid, + ppv: ?*?*anyopaque, +) callconv(WINAPI) HRESULT; +pub const IID_IUnknown = &Guid.initString("00000000-0000-0000-c000-000000000046"); +pub const IUnknown = extern struct { + pub const VTable = extern struct { + QueryInterface: *const fn ( + self: *const IUnknown, + riid: ?*const Guid, + ppvObject: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + AddRef: *const fn ( + self: *const IUnknown, + ) callconv(WINAPI) u32, + Release: *const fn ( + self: *const IUnknown, + ) callconv(WINAPI) u32, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub inline fn QueryInterface(self: *const T, riid: ?*const Guid, ppvObject: ?*?*anyopaque) HRESULT { + return @ptrCast(*const IUnknown.VTable, self.vtable).QueryInterface(@ptrCast(*const IUnknown, self), riid, ppvObject); + } + pub inline fn AddRef(self: *const T) u32 { + return @ptrCast(*const IUnknown.VTable, self.vtable).AddRef(@ptrCast(*const IUnknown, self)); + } + pub inline fn Release(self: *const T) u32 { + return @ptrCast(*const IUnknown.VTable, self.vtable).Release(@ptrCast(*const IUnknown, self)); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const ISequentialStream = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + Read: *const fn ( + self: *const ISequentialStream, + pv: ?*anyopaque, + cb: u32, + pcbRead: ?*u32, + ) callconv(WINAPI) HRESULT, + Write: *const fn ( + self: *const ISequentialStream, + pv: ?*const anyopaque, + cb: u32, + pcbWritten: ?*u32, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const CY = extern union { + anon: extern struct { + Lo: u32, + Hi: i32, + }, + int64: i64, +}; +pub const CAC = extern struct { + cElems: u32, + pElems: ?PSTR, +}; +pub const CAUB = extern struct { + cElems: u32, + pElems: ?*u8, +}; +pub const CAI = extern struct { + cElems: u32, + pElems: ?*i16, +}; +pub const CAUI = extern struct { + cElems: u32, + pElems: ?*u16, +}; +pub const CAL = extern struct { + cElems: u32, + pElems: ?*i32, +}; +pub const CAUL = extern struct { + cElems: u32, + pElems: ?*u32, +}; +pub const CAFLT = extern struct { + cElems: u32, + pElems: ?*f32, +}; +pub const CADBL = extern struct { + cElems: u32, + pElems: ?*f64, +}; +pub const CACY = extern struct { + cElems: u32, + pElems: ?*CY, +}; +pub const CADATE = extern struct { + cElems: u32, + pElems: ?*f64, +}; +pub const CABSTR = extern struct { + cElems: u32, + pElems: ?*?BSTR, +}; +pub const BSTRBLOB = extern struct { + cbSize: u32, + pData: ?*u8, +}; +pub const CABSTRBLOB = extern struct { + cElems: u32, + pElems: ?*BSTRBLOB, +}; +pub const CABOOL = extern struct { + cElems: u32, + pElems: ?*i16, +}; +pub const CASCODE = extern struct { + cElems: u32, + pElems: ?*i32, +}; +pub const CAPROPVARIANT = extern struct { + cElems: u32, + pElems: ?*PROPVARIANT, +}; +pub const CAH = extern struct { + cElems: u32, + pElems: ?*LARGE_INTEGER, +}; +pub const CAUH = extern struct { + cElems: u32, + pElems: ?*ULARGE_INTEGER, +}; +pub const CALPSTR = extern struct { + cElems: u32, + pElems: ?*?PSTR, +}; +pub const CALPWSTR = extern struct { + cElems: u32, + pElems: ?*?PWSTR, +}; +pub const CAFILETIME = extern struct { + cElems: u32, + pElems: ?*FILETIME, +}; +pub const CACLIPDATA = extern struct { + cElems: u32, + pElems: ?*CLIPDATA, +}; +pub const CACLSID = extern struct { + cElems: u32, + pElems: ?*Guid, +}; +pub const BLOB = extern struct { + cbSize: u32, + pBlobData: ?*u8, +}; +pub const INVOKEKIND = enum(i32) { + FUNC = 1, + PROPERTYGET = 2, + PROPERTYPUT = 4, + PROPERTYPUTREF = 8, +}; +pub const IDLDESC = extern struct { + dwReserved: usize, + wIDLFlags: u16, +}; +pub const VARIANT = extern struct { + anon: extern union { + anon: extern struct { + vt: u16, + wReserved1: u16, + wReserved2: u16, + wReserved3: u16, + anon: extern union { + llVal: i64, + lVal: i32, + bVal: u8, + iVal: i16, + fltVal: f32, + dblVal: f64, + boolVal: i16, + __OBSOLETE__VARIANT_BOOL: i16, + scode: i32, + cyVal: CY, + date: f64, + bstrVal: ?BSTR, + punkVal: ?*IUnknown, + pdispVal: ?*IDispatch, + parray: ?*SAFEARRAY, + pbVal: ?*u8, + piVal: ?*i16, + plVal: ?*i32, + pllVal: ?*i64, + pfltVal: ?*f32, + pdblVal: ?*f64, + pboolVal: ?*i16, + __OBSOLETE__VARIANT_PBOOL: ?*i16, + pscode: ?*i32, + pcyVal: ?*CY, + pdate: ?*f64, + pbstrVal: ?*?BSTR, + ppunkVal: ?*?*IUnknown, + ppdispVal: ?*?*IDispatch, + pparray: ?*?*SAFEARRAY, + pvarVal: ?*VARIANT, + byref: ?*anyopaque, + cVal: CHAR, + uiVal: u16, + ulVal: u32, + ullVal: u64, + intVal: i32, + uintVal: u32, + pdecVal: ?*DECIMAL, + pcVal: ?PSTR, + puiVal: ?*u16, + pulVal: ?*u32, + pullVal: ?*u64, + pintVal: ?*i32, + puintVal: ?*u32, + anon: extern struct { + pvRecord: ?*anyopaque, + pRecInfo: ?*IRecordInfo, + }, + }, + }, + decVal: DECIMAL, + }, +}; +pub const IRecordInfo = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + RecordInit: *const fn ( + self: *const IRecordInfo, + pvNew: ?*anyopaque, + ) callconv(WINAPI) HRESULT, + RecordClear: *const fn ( + self: *const IRecordInfo, + pvExisting: ?*anyopaque, + ) callconv(WINAPI) HRESULT, + RecordCopy: *const fn ( + self: *const IRecordInfo, + pvExisting: ?*anyopaque, + pvNew: ?*anyopaque, + ) callconv(WINAPI) HRESULT, + GetGuid: *const fn ( + self: *const IRecordInfo, + pguid: ?*Guid, + ) callconv(WINAPI) HRESULT, + GetName: *const fn ( + self: *const IRecordInfo, + pbstrName: ?*?BSTR, + ) callconv(WINAPI) HRESULT, + GetSize: *const fn ( + self: *const IRecordInfo, + pcbSize: ?*u32, + ) callconv(WINAPI) HRESULT, + GetTypeInfo: *const fn ( + self: *const IRecordInfo, + ppTypeInfo: ?*?*ITypeInfo, + ) callconv(WINAPI) HRESULT, + GetField: *const fn ( + self: *const IRecordInfo, + pvData: ?*anyopaque, + szFieldName: ?[*:0]const u16, + pvarField: ?*VARIANT, + ) callconv(WINAPI) HRESULT, + GetFieldNoCopy: *const fn ( + self: *const IRecordInfo, + pvData: ?*anyopaque, + szFieldName: ?[*:0]const u16, + pvarField: ?*VARIANT, + ppvDataCArray: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + PutField: *const fn ( + self: *const IRecordInfo, + wFlags: u32, + pvData: ?*anyopaque, + szFieldName: ?[*:0]const u16, + pvarField: ?*VARIANT, + ) callconv(WINAPI) HRESULT, + PutFieldNoCopy: *const fn ( + self: *const IRecordInfo, + wFlags: u32, + pvData: ?*anyopaque, + szFieldName: ?[*:0]const u16, + pvarField: ?*VARIANT, + ) callconv(WINAPI) HRESULT, + GetFieldNames: *const fn ( + self: *const IRecordInfo, + pcNames: ?*u32, + rgBstrNames: [*]?BSTR, + ) callconv(WINAPI) HRESULT, + IsMatchingType: *const fn ( + self: *const IRecordInfo, + pRecordInfo: ?*IRecordInfo, + ) callconv(WINAPI) BOOL, + RecordCreate: *const fn ( + self: *const IRecordInfo, + ) callconv(WINAPI) ?*anyopaque, + RecordCreateCopy: *const fn ( + self: *const IRecordInfo, + pvSource: ?*anyopaque, + ppvDest: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + RecordDestroy: *const fn ( + self: *const IRecordInfo, + pvRecord: ?*anyopaque, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const PARAMDESCEX = extern struct { + cBytes: u32, + varDefaultValue: VARIANT, +}; +pub const PARAMDESC = extern struct { + pparamdescex: ?*PARAMDESCEX, + wParamFlags: u16, +}; +pub const ARRAYDESC = extern struct { + tdescElem: TYPEDESC, + cDims: u16, + rgbounds: [1]SAFEARRAYBOUND, +}; +pub const TYPEDESC = extern struct { + anon: extern union { + lptdesc: ?*TYPEDESC, + lpadesc: ?*ARRAYDESC, + hreftype: u32, + }, + vt: u16, +}; +pub const ELEMDESC = extern struct { + tdesc: TYPEDESC, + anon: extern union { + idldesc: IDLDESC, + paramdesc: PARAMDESC, + }, +}; +pub const CALLCONV = enum(i32) { + FASTCALL = 0, + CDECL = 1, + MSCPASCAL = 2, + MACPASCAL = 3, + STDCALL = 4, + FPFASTCALL = 5, + SYSCALL = 6, + MPWCDECL = 7, + MPWPASCAL = 8, + MAX = 9, +}; +pub const FUNCKIND = enum(i32) { + VIRTUAL = 0, + PUREVIRTUAL = 1, + NONVIRTUAL = 2, + STATIC = 3, + DISPATCH = 4, +}; +pub const FUNCDESC = extern struct { + memid: i32, + lprgscode: ?*i32, + lprgelemdescParam: ?*ELEMDESC, + funckind: FUNCKIND, + invkind: INVOKEKIND, + @"callconv": CALLCONV, + cParams: i16, + cParamsOpt: i16, + oVft: i16, + cScodes: i16, + elemdescFunc: ELEMDESC, + wFuncFlags: u16, +}; +pub const TYPEATTR = extern struct { + guid: Guid, + lcid: u32, + dwReserved: u32, + memidConstructor: i32, + memidDestructor: i32, + lpstrSchema: ?PWSTR, + cbSizeInstance: u32, + typekind: TYPEKIND, + cFuncs: u16, + cVars: u16, + cImplTypes: u16, + cbSizeVft: u16, + cbAlignment: u16, + wTypeFlags: u16, + wMajorVerNum: u16, + wMinorVerNum: u16, + tdescAlias: TYPEDESC, + idldescType: IDLDESC, +}; +pub const TYPEKIND = enum(i32) { + ENUM = 0, + RECORD = 1, + MODULE = 2, + INTERFACE = 3, + DISPATCH = 4, + COCLASS = 5, + ALIAS = 6, + UNION = 7, + MAX = 8, +}; +pub const DESCKIND = enum(i32) { + NONE = 0, + FUNCDESC = 1, + VARDESC = 2, + TYPECOMP = 3, + IMPLICITAPPOBJ = 4, + MAX = 5, +}; +pub const BINDPTR = extern union { + lpfuncdesc: ?*FUNCDESC, + lpvardesc: ?*VARDESC, + lptcomp: ?*ITypeComp, +}; +pub const VARDESC = extern struct { + memid: i32, + lpstrSchema: ?PWSTR, + anon: extern union { + oInst: u32, + lpvarValue: ?*VARIANT, + }, + elemdescVar: ELEMDESC, + wVarFlags: u16, + varkind: VARKIND, +}; +pub const VARKIND = enum(i32) { + PERINSTANCE = 0, + STATIC = 1, + CONST = 2, + DISPATCH = 3, +}; +pub const ITypeComp = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + Bind: *const fn ( + self: *const ITypeComp, + szName: ?PWSTR, + lHashVal: u32, + wFlags: u16, + ppTInfo: ?*?*ITypeInfo, + pDescKind: ?*DESCKIND, + pBindPtr: ?*BINDPTR, + ) callconv(WINAPI) HRESULT, + BindType: *const fn ( + self: *const ITypeComp, + szName: ?PWSTR, + lHashVal: u32, + ppTInfo: ?*?*ITypeInfo, + ppTComp: ?*?*ITypeComp, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const DISPPARAMS = extern struct { + rgvarg: ?*VARIANT, + rgdispidNamedArgs: ?*i32, + cArgs: u32, + cNamedArgs: u32, +}; +pub const EXCEPINFO = extern struct { + wCode: u16, + wReserved: u16, + bstrSource: ?BSTR, + bstrDescription: ?BSTR, + bstrHelpFile: ?BSTR, + dwHelpContext: u32, + pvReserved: ?*anyopaque, + pfnDeferredFillIn: ?LPEXCEPFINO_DEFERRED_FILLIN, + scode: i32, +}; +pub const LPEXCEPFINO_DEFERRED_FILLIN = *const fn ( + pExcepInfo: ?*EXCEPINFO, +) callconv(WINAPI) HRESULT; +pub const ITypeInfo = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetTypeAttr: *const fn ( + self: *const ITypeInfo, + ppTypeAttr: ?*?*TYPEATTR, + ) callconv(WINAPI) HRESULT, + GetTypeComp: *const fn ( + self: *const ITypeInfo, + ppTComp: ?*?*ITypeComp, + ) callconv(WINAPI) HRESULT, + GetFuncDesc: *const fn ( + self: *const ITypeInfo, + index: u32, + ppFuncDesc: ?*?*FUNCDESC, + ) callconv(WINAPI) HRESULT, + GetVarDesc: *const fn ( + self: *const ITypeInfo, + index: u32, + ppVarDesc: ?*?*VARDESC, + ) callconv(WINAPI) HRESULT, + GetNames: *const fn ( + self: *const ITypeInfo, + memid: i32, + rgBstrNames: [*]?BSTR, + cMaxNames: u32, + pcNames: ?*u32, + ) callconv(WINAPI) HRESULT, + GetRefTypeOfImplType: *const fn ( + self: *const ITypeInfo, + index: u32, + pRefType: ?*u32, + ) callconv(WINAPI) HRESULT, + GetImplTypeFlags: *const fn ( + self: *const ITypeInfo, + index: u32, + pImplTypeFlags: ?*i32, + ) callconv(WINAPI) HRESULT, + GetIDsOfNames: *const fn ( + self: *const ITypeInfo, + rgszNames: [*]?PWSTR, + cNames: u32, + pMemId: [*]i32, + ) callconv(WINAPI) HRESULT, + Invoke: *const fn ( + self: *const ITypeInfo, + pvInstance: ?*anyopaque, + memid: i32, + wFlags: u16, + pDispParams: ?*DISPPARAMS, + pVarResult: ?*VARIANT, + pExcepInfo: ?*EXCEPINFO, + puArgErr: ?*u32, + ) callconv(WINAPI) HRESULT, + GetDocumentation: *const fn ( + self: *const ITypeInfo, + memid: i32, + pBstrName: ?*?BSTR, + pBstrDocString: ?*?BSTR, + pdwHelpContext: ?*u32, + pBstrHelpFile: ?*?BSTR, + ) callconv(WINAPI) HRESULT, + GetDllEntry: *const fn ( + self: *const ITypeInfo, + memid: i32, + invKind: INVOKEKIND, + pBstrDllName: ?*?BSTR, + pBstrName: ?*?BSTR, + pwOrdinal: ?*u16, + ) callconv(WINAPI) HRESULT, + GetRefTypeInfo: *const fn ( + self: *const ITypeInfo, + hRefType: u32, + ppTInfo: ?*?*ITypeInfo, + ) callconv(WINAPI) HRESULT, + AddressOfMember: *const fn ( + self: *const ITypeInfo, + memid: i32, + invKind: INVOKEKIND, + ppv: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + CreateInstance: *const fn ( + self: *const ITypeInfo, + pUnkOuter: ?*IUnknown, + riid: ?*const Guid, + ppvObj: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + GetMops: *const fn ( + self: *const ITypeInfo, + memid: i32, + pBstrMops: ?*?BSTR, + ) callconv(WINAPI) HRESULT, + GetContainingTypeLib: *const fn ( + self: *const ITypeInfo, + ppTLib: ?*?*ITypeLib, + pIndex: ?*u32, + ) callconv(WINAPI) HRESULT, + ReleaseTypeAttr: *const fn ( + self: *const ITypeInfo, + pTypeAttr: ?*TYPEATTR, + ) callconv(WINAPI) void, + ReleaseFuncDesc: *const fn ( + self: *const ITypeInfo, + pFuncDesc: ?*FUNCDESC, + ) callconv(WINAPI) void, + ReleaseVarDesc: *const fn ( + self: *const ITypeInfo, + pVarDesc: ?*VARDESC, + ) callconv(WINAPI) void, + }; + vtable: *const VTable, +}; +pub const SYSKIND = enum(i32) { + WIN16 = 0, + WIN32 = 1, + MAC = 2, + WIN64 = 3, +}; +pub const TLIBATTR = extern struct { + guid: Guid, + lcid: u32, + syskind: SYSKIND, + wMajorVerNum: u16, + wMinorVerNum: u16, + wLibFlags: u16, +}; +pub const ITypeLib = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetTypeInfoCount: *const fn ( + self: *const ITypeLib, + ) callconv(WINAPI) u32, + GetTypeInfo: *const fn ( + self: *const ITypeLib, + index: u32, + ppTInfo: ?*?*ITypeInfo, + ) callconv(WINAPI) HRESULT, + GetTypeInfoType: *const fn ( + self: *const ITypeLib, + index: u32, + pTKind: ?*TYPEKIND, + ) callconv(WINAPI) HRESULT, + GetTypeInfoOfGuid: *const fn ( + self: *const ITypeLib, + guid: ?*const Guid, + ppTinfo: ?*?*ITypeInfo, + ) callconv(WINAPI) HRESULT, + GetLibAttr: *const fn ( + self: *const ITypeLib, + ppTLibAttr: ?*?*TLIBATTR, + ) callconv(WINAPI) HRESULT, + GetTypeComp: *const fn ( + self: *const ITypeLib, + ppTComp: ?*?*ITypeComp, + ) callconv(WINAPI) HRESULT, + GetDocumentation: *const fn ( + self: *const ITypeLib, + index: i32, + pBstrName: ?*?BSTR, + pBstrDocString: ?*?BSTR, + pdwHelpContext: ?*u32, + pBstrHelpFile: ?*?BSTR, + ) callconv(WINAPI) HRESULT, + IsName: *const fn ( + self: *const ITypeLib, + szNameBuf: ?PWSTR, + lHashVal: u32, + pfName: ?*BOOL, + ) callconv(WINAPI) HRESULT, + FindName: *const fn ( + self: *const ITypeLib, + szNameBuf: ?PWSTR, + lHashVal: u32, + ppTInfo: [*]?*ITypeInfo, + rgMemId: [*]i32, + pcFound: ?*u16, + ) callconv(WINAPI) HRESULT, + ReleaseTLibAttr: *const fn ( + self: *const ITypeLib, + pTLibAttr: ?*TLIBATTR, + ) callconv(WINAPI) void, + }; + vtable: *const VTable, +}; +pub const IDispatch = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetTypeInfoCount: *const fn ( + self: *const IDispatch, + pctinfo: ?*u32, + ) callconv(WINAPI) HRESULT, + GetTypeInfo: *const fn ( + self: *const IDispatch, + iTInfo: u32, + lcid: u32, + ppTInfo: ?*?*ITypeInfo, + ) callconv(WINAPI) HRESULT, + GetIDsOfNames: *const fn ( + self: *const IDispatch, + riid: ?*const Guid, + rgszNames: [*]?PWSTR, + cNames: u32, + lcid: u32, + rgDispId: [*]i32, + ) callconv(WINAPI) HRESULT, + Invoke: *const fn ( + self: *const IDispatch, + dispIdMember: i32, + riid: ?*const Guid, + lcid: u32, + wFlags: u16, + pDispParams: ?*DISPPARAMS, + pVarResult: ?*VARIANT, + pExcepInfo: ?*EXCEPINFO, + puArgErr: ?*u32, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const IEnumSTATSTG = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + Next: *const fn ( + self: *const IEnumSTATSTG, + celt: u32, + rgelt: [*]STATSTG, + pceltFetched: ?*u32, + ) callconv(WINAPI) HRESULT, + Skip: *const fn ( + self: *const IEnumSTATSTG, + celt: u32, + ) callconv(WINAPI) HRESULT, + Reset: *const fn ( + self: *const IEnumSTATSTG, + ) callconv(WINAPI) HRESULT, + Clone: *const fn ( + self: *const IEnumSTATSTG, + ppenum: ?*?*IEnumSTATSTG, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const IStorage = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + CreateStream: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + grfMode: u32, + reserved1: u32, + reserved2: u32, + ppstm: ?*?*IStream, + ) callconv(WINAPI) HRESULT, + OpenStream: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + reserved1: ?*anyopaque, + grfMode: u32, + reserved2: u32, + ppstm: ?*?*IStream, + ) callconv(WINAPI) HRESULT, + CreateStorage: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + grfMode: u32, + reserved1: u32, + reserved2: u32, + ppstg: ?*?*IStorage, + ) callconv(WINAPI) HRESULT, + OpenStorage: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + pstgPriority: ?*IStorage, + grfMode: u32, + snbExclude: ?*?*u16, + reserved: u32, + ppstg: ?*?*IStorage, + ) callconv(WINAPI) HRESULT, + CopyTo: *const fn ( + self: *const IStorage, + ciidExclude: u32, + rgiidExclude: ?[*]const Guid, + snbExclude: ?*?*u16, + pstgDest: ?*IStorage, + ) callconv(WINAPI) HRESULT, + MoveElementTo: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + pstgDest: ?*IStorage, + pwcsNewName: ?[*:0]const u16, + grfFlags: u32, + ) callconv(WINAPI) HRESULT, + Commit: *const fn ( + self: *const IStorage, + grfCommitFlags: u32, + ) callconv(WINAPI) HRESULT, + Revert: *const fn ( + self: *const IStorage, + ) callconv(WINAPI) HRESULT, + EnumElements: *const fn ( + self: *const IStorage, + reserved1: u32, + reserved2: ?*anyopaque, + reserved3: u32, + ppenum: ?*?*IEnumSTATSTG, + ) callconv(WINAPI) HRESULT, + DestroyElement: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + ) callconv(WINAPI) HRESULT, + RenameElement: *const fn ( + self: *const IStorage, + pwcsOldName: ?[*:0]const u16, + pwcsNewName: ?[*:0]const u16, + ) callconv(WINAPI) HRESULT, + SetElementTimes: *const fn ( + self: *const IStorage, + pwcsName: ?[*:0]const u16, + pctime: ?*const FILETIME, + patime: ?*const FILETIME, + pmtime: ?*const FILETIME, + ) callconv(WINAPI) HRESULT, + SetClass: *const fn ( + self: *const IStorage, + clsid: ?*const Guid, + ) callconv(WINAPI) HRESULT, + SetStateBits: *const fn ( + self: *const IStorage, + grfStateBits: u32, + grfMask: u32, + ) callconv(WINAPI) HRESULT, + Stat: *const fn ( + self: *const IStorage, + pstatstg: ?*STATSTG, + grfStatFlag: u32, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, +}; +pub const PROPVARIANT = extern struct { + anon: extern union { + anon: extern struct { + vt: u16, + wReserved1: u16, + wReserved2: u16, + wReserved3: u16, + anon: extern union { + cVal: CHAR, + bVal: u8, + iVal: i16, + uiVal: u16, + lVal: i32, + ulVal: u32, + intVal: i32, + uintVal: u32, + hVal: LARGE_INTEGER, + uhVal: ULARGE_INTEGER, + fltVal: f32, + dblVal: f64, + boolVal: i16, + __OBSOLETE__VARIANT_BOOL: i16, + scode: i32, + cyVal: CY, + date: f64, + filetime: FILETIME, + puuid: ?*Guid, + pclipdata: ?*CLIPDATA, + bstrVal: ?BSTR, + bstrblobVal: BSTRBLOB, + blob: BLOB, + pszVal: ?PSTR, + pwszVal: ?PWSTR, + punkVal: ?*IUnknown, + pdispVal: ?*IDispatch, + pStream: ?*IStream, + pStorage: ?*IStorage, + pVersionedStream: ?*VERSIONEDSTREAM, + parray: ?*SAFEARRAY, + cac: CAC, + caub: CAUB, + cai: CAI, + caui: CAUI, + cal: CAL, + caul: CAUL, + cah: CAH, + cauh: CAUH, + caflt: CAFLT, + cadbl: CADBL, + cabool: CABOOL, + cascode: CASCODE, + cacy: CACY, + cadate: CADATE, + cafiletime: CAFILETIME, + cauuid: CACLSID, + caclipdata: CACLIPDATA, + cabstr: CABSTR, + cabstrblob: CABSTRBLOB, + calpstr: CALPSTR, + calpwstr: CALPWSTR, + capropvar: CAPROPVARIANT, + pcVal: ?PSTR, + pbVal: ?*u8, + piVal: ?*i16, + puiVal: ?*u16, + plVal: ?*i32, + pulVal: ?*u32, + pintVal: ?*i32, + puintVal: ?*u32, + pfltVal: ?*f32, + pdblVal: ?*f64, + pboolVal: ?*i16, + pdecVal: ?*DECIMAL, + pscode: ?*i32, + pcyVal: ?*CY, + pdate: ?*f64, + pbstrVal: ?*?BSTR, + ppunkVal: ?*?*IUnknown, + ppdispVal: ?*?*IDispatch, + pparray: ?*?*SAFEARRAY, + pvarVal: ?*PROPVARIANT, + }, + }, + decVal: DECIMAL, + }, +}; +pub const WAVEFORMATEX = extern struct { + wFormatTag: u16 align(1), + nChannels: u16 align(1), + nSamplesPerSec: u32 align(1), + nAvgBytesPerSec: u32 align(1), + nBlockAlign: u16 align(1), + wBitsPerSample: u16 align(1), + cbSize: u16 align(1), +}; +pub const WAVEFORMATEXTENSIBLE = extern struct { + Format: WAVEFORMATEX align(1), + Samples: extern union { + wValidBitsPerSample: u16 align(1), + wSamplesPerBlock: u16 align(1), + wReserved: u16 align(1), + }, + dwChannelMask: u32 align(1), + SubFormat: Guid align(1), +}; +pub const CLSID_MMDeviceEnumerator = &Guid.initString("bcde0395-e52f-467c-8e3d-c4579291692e"); +pub const DIRECTX_AUDIO_ACTIVATION_PARAMS = extern struct { + cbDirectXAudioActivationParams: u32, + guidAudioSession: Guid, + dwAudioStreamFlags: u32, +}; +pub const DataFlow = enum(i32) { + render = 0, + capture = 1, + all = 2, +}; +pub const Role = enum(i32) { + console = 0, + multimedia = 1, + communications = 2, +}; +pub const AUDCLNT_SHAREMODE = enum(i32) { + SHARED = 0, + EXCLUSIVE = 1, +}; +pub const HANDLE = @import("std").os.windows.HANDLE; +pub const IID_IAudioClient = &Guid.initString("1cb9ad4c-dbfa-4c32-b178-c2f568a703b2"); +pub const IAudioClient = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + Initialize: *const fn ( + self: *const IAudioClient, + ShareMode: AUDCLNT_SHAREMODE, + StreamFlags: u32, + hnsBufferDuration: i64, + hnsPeriodicity: i64, + pFormat: ?*const WAVEFORMATEX, + AudioSessionGuid: ?*const Guid, + ) callconv(WINAPI) HRESULT, + GetBufferSize: *const fn ( + self: *const IAudioClient, + pNumBufferFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + GetStreamLatency: *const fn ( + self: *const IAudioClient, + phnsLatency: ?*i64, + ) callconv(WINAPI) HRESULT, + GetCurrentPadding: *const fn ( + self: *const IAudioClient, + pNumPaddingFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + IsFormatSupported: *const fn ( + self: *const IAudioClient, + ShareMode: AUDCLNT_SHAREMODE, + pFormat: ?*const WAVEFORMATEX, + ppClosestMatch: ?*?*WAVEFORMATEX, + ) callconv(WINAPI) HRESULT, + GetMixFormat: *const fn ( + self: *const IAudioClient, + ppDeviceFormat: ?*?*WAVEFORMATEX, + ) callconv(WINAPI) HRESULT, + GetDevicePeriod: *const fn ( + self: *const IAudioClient, + phnsDefaultDevicePeriod: ?*i64, + phnsMinimumDevicePeriod: ?*i64, + ) callconv(WINAPI) HRESULT, + Start: *const fn ( + self: *const IAudioClient, + ) callconv(WINAPI) HRESULT, + Stop: *const fn ( + self: *const IAudioClient, + ) callconv(WINAPI) HRESULT, + Reset: *const fn ( + self: *const IAudioClient, + ) callconv(WINAPI) HRESULT, + SetEventHandle: *const fn ( + self: *const IAudioClient, + eventHandle: ?HANDLE, + ) callconv(WINAPI) HRESULT, + GetService: *const fn ( + self: *const IAudioClient, + riid: ?*const Guid, + ppv: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn Initialize(self: *const T, ShareMode: AUDCLNT_SHAREMODE, StreamFlags: u32, hnsBufferDuration: i64, hnsPeriodicity: i64, pFormat: ?*const WAVEFORMATEX, AudioSessionGuid: ?*const Guid) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).Initialize(@ptrCast(*const IAudioClient, self), ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat, AudioSessionGuid); + } + pub inline fn GetBufferSize(self: *const T, pNumBufferFrames: ?*u32) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetBufferSize(@ptrCast(*const IAudioClient, self), pNumBufferFrames); + } + pub inline fn GetStreamLatency(self: *const T, phnsLatency: ?*i64) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetStreamLatency(@ptrCast(*const IAudioClient, self), phnsLatency); + } + pub inline fn GetCurrentPadding(self: *const T, pNumPaddingFrames: ?*u32) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetCurrentPadding(@ptrCast(*const IAudioClient, self), pNumPaddingFrames); + } + pub inline fn IsFormatSupported(self: *const T, ShareMode: AUDCLNT_SHAREMODE, pFormat: ?*const WAVEFORMATEX, ppClosestMatch: ?*?*WAVEFORMATEX) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).IsFormatSupported(@ptrCast(*const IAudioClient, self), ShareMode, pFormat, ppClosestMatch); + } + pub inline fn GetMixFormat(self: *const T, ppDeviceFormat: ?*?*WAVEFORMATEX) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetMixFormat(@ptrCast(*const IAudioClient, self), ppDeviceFormat); + } + pub inline fn GetDevicePeriod(self: *const T, phnsDefaultDevicePeriod: ?*i64, phnsMinimumDevicePeriod: ?*i64) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetDevicePeriod(@ptrCast(*const IAudioClient, self), phnsDefaultDevicePeriod, phnsMinimumDevicePeriod); + } + pub inline fn Start(self: *const T) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).Start(@ptrCast(*const IAudioClient, self)); + } + pub inline fn Stop(self: *const T) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).Stop(@ptrCast(*const IAudioClient, self)); + } + pub inline fn Reset(self: *const T) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).Reset(@ptrCast(*const IAudioClient, self)); + } + pub inline fn SetEventHandle(self: *const T, eventHandle: ?HANDLE) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).SetEventHandle(@ptrCast(*const IAudioClient, self), eventHandle); + } + pub inline fn GetService(self: *const T, riid: ?*const Guid, ppv: ?*?*anyopaque) HRESULT { + return @ptrCast(*const IAudioClient.VTable, self.vtable).GetService(@ptrCast(*const IAudioClient, self), riid, ppv); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(WINAPI) void; +pub const IID_IAudioRenderClient = &Guid.initString("f294acfc-3146-4483-a7bf-addca7c260e2"); +pub const IAudioRenderClient = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetBuffer: *const fn ( + self: *const IAudioRenderClient, + NumFramesRequested: u32, + ppData: ?*?*u8, + ) callconv(WINAPI) HRESULT, + ReleaseBuffer: *const fn ( + self: *const IAudioRenderClient, + NumFramesWritten: u32, + dwFlags: u32, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn GetBuffer(self: *const T, NumFramesRequested: u32, ppData: ?*?*u8) HRESULT { + return @ptrCast(*const IAudioRenderClient.VTable, self.vtable).GetBuffer(@ptrCast(*const IAudioRenderClient, self), NumFramesRequested, ppData); + } + pub inline fn ReleaseBuffer(self: *const T, NumFramesWritten: u32, dwFlags: u32) HRESULT { + return @ptrCast(*const IAudioRenderClient.VTable, self.vtable).ReleaseBuffer(@ptrCast(*const IAudioRenderClient, self), NumFramesWritten, dwFlags); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_ISimpleAudioVolume = &Guid.initString("87ce5498-68d6-44e5-9215-6da47ef883d8"); +pub const ISimpleAudioVolume = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + SetMasterVolume: *const fn ( + self: *const ISimpleAudioVolume, + fLevel: f32, + EventContext: ?*const Guid, + ) callconv(WINAPI) HRESULT, + GetMasterVolume: *const fn ( + self: *const ISimpleAudioVolume, + pfLevel: ?*f32, + ) callconv(WINAPI) HRESULT, + SetMute: *const fn ( + self: *const ISimpleAudioVolume, + bMute: BOOL, + EventContext: ?*const Guid, + ) callconv(WINAPI) HRESULT, + GetMute: *const fn ( + self: *const ISimpleAudioVolume, + pbMute: ?*BOOL, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn SetMasterVolume(self: *const T, fLevel: f32, EventContext: ?*const Guid) HRESULT { + return @ptrCast(*const ISimpleAudioVolume.VTable, self.vtable).SetMasterVolume(@ptrCast(*const ISimpleAudioVolume, self), fLevel, EventContext); + } + pub inline fn GetMasterVolume(self: *const T, pfLevel: ?*f32) HRESULT { + return @ptrCast(*const ISimpleAudioVolume.VTable, self.vtable).GetMasterVolume(@ptrCast(*const ISimpleAudioVolume, self), pfLevel); + } + pub inline fn SetMute(self: *const T, bMute: BOOL, EventContext: ?*const Guid) HRESULT { + return @ptrCast(*const ISimpleAudioVolume.VTable, self.vtable).SetMute(@ptrCast(*const ISimpleAudioVolume, self), bMute, EventContext); + } + pub inline fn GetMute(self: *const T, pbMute: ?*BOOL) HRESULT { + return @ptrCast(*const ISimpleAudioVolume.VTable, self.vtable).GetMute(@ptrCast(*const ISimpleAudioVolume, self), pbMute); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IPropertyStore = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetCount: *const fn ( + self: *const IPropertyStore, + cProps: ?*u32, + ) callconv(WINAPI) HRESULT, + GetAt: *const fn ( + self: *const IPropertyStore, + iProp: u32, + pkey: ?*PROPERTYKEY, + ) callconv(WINAPI) HRESULT, + GetValue: *const fn ( + self: *const IPropertyStore, + key: ?*const PROPERTYKEY, + pv: ?*PROPVARIANT, + ) callconv(WINAPI) HRESULT, + SetValue: *const fn ( + self: *const IPropertyStore, + key: ?*const PROPERTYKEY, + propvar: ?*const PROPVARIANT, + ) callconv(WINAPI) HRESULT, + Commit: *const fn ( + self: *const IPropertyStore, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn GetCount(self: *const T, cProps: ?*u32) HRESULT { + return @ptrCast(*const IPropertyStore.VTable, self.vtable).GetCount(@ptrCast(*const IPropertyStore, self), cProps); + } + pub inline fn GetAt(self: *const T, iProp: u32, pkey: ?*PROPERTYKEY) HRESULT { + return @ptrCast(*const IPropertyStore.VTable, self.vtable).GetAt(@ptrCast(*const IPropertyStore, self), iProp, pkey); + } + pub inline fn GetValue(self: *const T, key: ?*const PROPERTYKEY, pv: ?*PROPVARIANT) HRESULT { + return @ptrCast(*const IPropertyStore.VTable, self.vtable).GetValue(@ptrCast(*const IPropertyStore, self), key, pv); + } + pub inline fn SetValue(self: *const T, key: ?*const PROPERTYKEY, propvar: ?*const PROPVARIANT) HRESULT { + return @ptrCast(*const IPropertyStore.VTable, self.vtable).SetValue(@ptrCast(*const IPropertyStore, self), key, propvar); + } + pub inline fn Commit(self: *const T) HRESULT { + return @ptrCast(*const IPropertyStore.VTable, self.vtable).Commit(@ptrCast(*const IPropertyStore, self)); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +const IID_IMMDevice = &Guid.initString("d666063f-1587-4e43-81f1-b948e807363f"); +pub const IMMDevice = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + Activate: *const fn ( + self: *const IMMDevice, + iid: ?*const Guid, + dwClsCtx: u32, + pActivationParams: ?*PROPVARIANT, + ppInterface: ?*?*anyopaque, + ) callconv(WINAPI) HRESULT, + OpenPropertyStore: *const fn ( + self: *const IMMDevice, + stgmAccess: u32, + ppProperties: ?*?*IPropertyStore, + ) callconv(WINAPI) HRESULT, + GetId: *const fn ( + self: *const IMMDevice, + ppstrId: ?*?PWSTR, + ) callconv(WINAPI) HRESULT, + GetState: *const fn ( + self: *const IMMDevice, + pdwState: ?*u32, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn Activate(self: *const T, iid: ?*const Guid, dwClsCtx: u32, pActivationParams: ?*PROPVARIANT, ppInterface: ?*?*anyopaque) HRESULT { + return @ptrCast(*const IMMDevice.VTable, self.vtable).Activate(@ptrCast(*const IMMDevice, self), iid, dwClsCtx, pActivationParams, ppInterface); + } + pub inline fn OpenPropertyStore(self: *const T, stgmAccess: u32, ppProperties: ?*?*IPropertyStore) HRESULT { + return @ptrCast(*const IMMDevice.VTable, self.vtable).OpenPropertyStore(@ptrCast(*const IMMDevice, self), stgmAccess, ppProperties); + } + pub inline fn GetId(self: *const T, ppstrId: ?*?PWSTR) HRESULT { + return @ptrCast(*const IMMDevice.VTable, self.vtable).GetId(@ptrCast(*const IMMDevice, self), ppstrId); + } + pub inline fn GetState(self: *const T, pdwState: ?*u32) HRESULT { + return @ptrCast(*const IMMDevice.VTable, self.vtable).GetState(@ptrCast(*const IMMDevice, self), pdwState); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IMMNotificationClient = &Guid.initString("7991eec9-7e89-4d85-8390-6c703cec60c0"); +pub const IMMNotificationClient = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + OnDeviceStateChanged: *const fn ( + self: *const IMMNotificationClient, + pwstrDeviceId: ?[*:0]const u16, + dwNewState: u32, + ) callconv(WINAPI) HRESULT, + OnDeviceAdded: *const fn ( + self: *const IMMNotificationClient, + pwstrDeviceId: ?[*:0]const u16, + ) callconv(WINAPI) HRESULT, + OnDeviceRemoved: *const fn ( + self: *const IMMNotificationClient, + pwstrDeviceId: ?[*:0]const u16, + ) callconv(WINAPI) HRESULT, + OnDefaultDeviceChanged: *const fn ( + self: *const IMMNotificationClient, + flow: DataFlow, + role: Role, + pwstrDefaultDeviceId: ?[*:0]const u16, + ) callconv(WINAPI) HRESULT, + OnPropertyValueChanged: *const fn ( + self: *const IMMNotificationClient, + pwstrDeviceId: ?[*:0]const u16, + key: PROPERTYKEY, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn OnDeviceStateChanged(self: *const T, pwstrDeviceId: ?[*:0]const u16, dwNewState: u32) HRESULT { + return @ptrCast(*const IMMNotificationClient.VTable, self.vtable).OnDeviceStateChanged(@ptrCast(*const IMMNotificationClient, self), pwstrDeviceId, dwNewState); + } + pub inline fn OnDeviceAdded(self: *const T, pwstrDeviceId: ?[*:0]const u16) HRESULT { + return @ptrCast(*const IMMNotificationClient.VTable, self.vtable).OnDeviceAdded(@ptrCast(*const IMMNotificationClient, self), pwstrDeviceId); + } + pub inline fn OnDeviceRemoved(self: *const T, pwstrDeviceId: ?[*:0]const u16) HRESULT { + return @ptrCast(*const IMMNotificationClient.VTable, self.vtable).OnDeviceRemoved(@ptrCast(*const IMMNotificationClient, self), pwstrDeviceId); + } + pub inline fn OnDefaultDeviceChanged(self: *const T, flow: DataFlow, role: Role, pwstrDefaultDeviceId: ?[*:0]const u16) HRESULT { + return @ptrCast(*const IMMNotificationClient.VTable, self.vtable).OnDefaultDeviceChanged(@ptrCast(*const IMMNotificationClient, self), flow, role, pwstrDefaultDeviceId); + } + pub inline fn OnPropertyValueChanged(self: *const T, pwstrDeviceId: ?[*:0]const u16, key: PROPERTYKEY) HRESULT { + return @ptrCast(*const IMMNotificationClient.VTable, self.vtable).OnPropertyValueChanged(@ptrCast(*const IMMNotificationClient, self), pwstrDeviceId, key); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IMMDeviceCollection = &Guid.initString("0bd7a1be-7a1a-44db-8397-cc5392387b5e"); +pub const IMMDeviceCollection = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetCount: *const fn ( + self: *const IMMDeviceCollection, + pcDevices: ?*u32, + ) callconv(WINAPI) HRESULT, + Item: *const fn ( + self: *const IMMDeviceCollection, + nDevice: u32, + ppDevice: ?*?*IMMDevice, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn GetCount(self: *const T, pcDevices: ?*u32) HRESULT { + return @ptrCast(*const IMMDeviceCollection.VTable, self.vtable).GetCount(@ptrCast(*const IMMDeviceCollection, self), pcDevices); + } + pub inline fn Item(self: *const T, nDevice: u32, ppDevice: ?*?*IMMDevice) HRESULT { + return @ptrCast(*const IMMDeviceCollection.VTable, self.vtable).Item(@ptrCast(*const IMMDeviceCollection, self), nDevice, ppDevice); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IMMDeviceEnumerator = &Guid.initString("a95664d2-9614-4f35-a746-de8db63617e6"); +pub const IMMDeviceEnumerator = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + EnumAudioEndpoints: *const fn ( + self: *const IMMDeviceEnumerator, + dataFlow: DataFlow, + dwStateMask: u32, + ppDevices: ?*?*IMMDeviceCollection, + ) callconv(WINAPI) HRESULT, + GetDefaultAudioEndpoint: *const fn ( + self: *const IMMDeviceEnumerator, + dataFlow: DataFlow, + role: Role, + ppEndpoint: ?*?*IMMDevice, + ) callconv(WINAPI) HRESULT, + GetDevice: *const fn ( + self: *const IMMDeviceEnumerator, + pwstrId: ?[*:0]const u16, + ppDevice: ?*?*IMMDevice, + ) callconv(WINAPI) HRESULT, + RegisterEndpointNotificationCallback: *const fn ( + self: *const IMMDeviceEnumerator, + pClient: ?*IMMNotificationClient, + ) callconv(WINAPI) HRESULT, + UnregisterEndpointNotificationCallback: *const fn ( + self: *const IMMDeviceEnumerator, + pClient: ?*IMMNotificationClient, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn EnumAudioEndpoints(self: *const T, dataFlow: DataFlow, dwStateMask: u32, ppDevices: ?*?*IMMDeviceCollection) HRESULT { + return @ptrCast(*const IMMDeviceEnumerator.VTable, self.vtable).EnumAudioEndpoints(@ptrCast(*const IMMDeviceEnumerator, self), dataFlow, dwStateMask, ppDevices); + } + pub inline fn GetDefaultAudioEndpoint(self: *const T, dataFlow: DataFlow, role: Role, ppEndpoint: ?*?*IMMDevice) HRESULT { + return @ptrCast(*const IMMDeviceEnumerator.VTable, self.vtable).GetDefaultAudioEndpoint(@ptrCast(*const IMMDeviceEnumerator, self), dataFlow, role, ppEndpoint); + } + pub inline fn GetDevice(self: *const T, pwstrId: ?[*:0]const u16, ppDevice: ?*?*IMMDevice) HRESULT { + return @ptrCast(*const IMMDeviceEnumerator.VTable, self.vtable).GetDevice(@ptrCast(*const IMMDeviceEnumerator, self), pwstrId, ppDevice); + } + pub inline fn RegisterEndpointNotificationCallback(self: *const T, pClient: ?*IMMNotificationClient) HRESULT { + return @ptrCast(*const IMMDeviceEnumerator.VTable, self.vtable).RegisterEndpointNotificationCallback(@ptrCast(*const IMMDeviceEnumerator, self), pClient); + } + pub inline fn UnregisterEndpointNotificationCallback(self: *const T, pClient: ?*IMMNotificationClient) HRESULT { + return @ptrCast(*const IMMDeviceEnumerator.VTable, self.vtable).UnregisterEndpointNotificationCallback(@ptrCast(*const IMMDeviceEnumerator, self), pClient); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IMMEndpoint = &Guid.initString("1be09788-6894-4089-8586-9a2a6c265ac5"); +pub const IMMEndpoint = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetDataFlow: *const fn ( + self: *const IMMEndpoint, + pDataFlow: ?*DataFlow, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IUnknown.MethodMixin(T); + pub inline fn GetDataFlow(self: *const T, pDataFlow: ?*DataFlow) HRESULT { + return @ptrCast(*const IMMEndpoint.VTable, self.vtable).GetDataFlow(@ptrCast(*const IMMEndpoint, self), pDataFlow); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const AUDCLNT_STREAMFLAGS_NOPERSIST = 524288; +pub const PKEY_Device_FriendlyName = PROPERTYKEY{ .fmtid = Guid.initString("a45c254e-df1c-4efd-8020-67d146a850e0"), .pid = 14 }; +pub const CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = &Guid.initString("00000003-0000-0010-8000-00aa00389b71"); +pub const SPEAKER_FRONT_LEFT = 1; +pub const SPEAKER_FRONT_RIGHT = 2; +pub const SPEAKER_FRONT_CENTER = 4; +pub const SPEAKER_LOW_FREQUENCY = 8; +pub const SPEAKER_BACK_LEFT = 16; +pub const SPEAKER_BACK_RIGHT = 32; +pub const SPEAKER_FRONT_LEFT_OF_CENTER = 64; +pub const SPEAKER_FRONT_RIGHT_OF_CENTER = 128; +pub const SPEAKER_BACK_CENTER = 256; +pub const SPEAKER_SIDE_LEFT = 512; +pub const SPEAKER_SIDE_RIGHT = 1024; +pub const SPEAKER_TOP_CENTER = 2048; +pub const SPEAKER_TOP_FRONT_LEFT = 4096; +pub const SPEAKER_TOP_FRONT_CENTER = 8192; +pub const SPEAKER_TOP_FRONT_RIGHT = 16384; +pub const SPEAKER_TOP_BACK_LEFT = 32768; +pub const SPEAKER_TOP_BACK_CENTER = 65536; +pub const SPEAKER_TOP_BACK_RIGHT = 131072; +pub const SPEAKER_RESERVED = @as(u32, 2147221504); +pub const SPEAKER_ALL = @as(u32, 2147483648); +pub const CLSID_KSDATAFORMAT_SUBTYPE_PCM = &Guid.initString("00000001-0000-0010-8000-00aa00389b71"); +pub const INPLACE_S_TRUNCATED = 262560; +pub const PKEY_AudioEngine_DeviceFormat = PROPERTYKEY{ .fmtid = Guid.initString("f19f064d-082c-4e27-bc73-6882a1bb8e4c"), .pid = 0 }; +pub const WAVE_FORMAT_EXTENSIBLE = 65534; +pub const STGM_READ = 0; +pub const DEVICE_STATE_ACTIVE = 1; +pub const AUDCLNT_E_NOT_INITIALIZED = -2004287487; +pub const AUDCLNT_E_ALREADY_INITIALIZED = -2004287486; +pub const AUDCLNT_E_WRONG_ENDPOINT_TYPE = -2004287485; +pub const AUDCLNT_E_DEVICE_INVALIDATED = -2004287484; +pub const AUDCLNT_E_NOT_STOPPED = -2004287483; +pub const AUDCLNT_E_BUFFER_TOO_LARGE = -2004287482; +pub const AUDCLNT_E_OUT_OF_ORDER = -2004287481; +pub const AUDCLNT_E_UNSUPPORTED_FORMAT = -2004287480; +pub const AUDCLNT_E_INVALID_SIZE = -2004287479; +pub const AUDCLNT_E_DEVICE_IN_USE = -2004287478; +pub const AUDCLNT_E_BUFFER_OPERATION_PENDING = -2004287477; +pub const AUDCLNT_E_THREAD_NOT_REGISTERED = -2004287476; +pub const AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED = -2004287474; +pub const AUDCLNT_E_ENDPOINT_CREATE_FAILED = -2004287473; +pub const AUDCLNT_E_SERVICE_NOT_RUNNING = -2004287472; +pub const AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED = -2004287471; +pub const AUDCLNT_E_EXCLUSIVE_MODE_ONLY = -2004287470; +pub const AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL = -2004287469; +pub const AUDCLNT_E_EVENTHANDLE_NOT_SET = -2004287468; +pub const AUDCLNT_E_INCORRECT_BUFFER_SIZE = -2004287467; +pub const AUDCLNT_E_BUFFER_SIZE_ERROR = -2004287466; +pub const AUDCLNT_E_CPUUSAGE_EXCEEDED = -2004287465; +pub const AUDCLNT_E_BUFFER_ERROR = -2004287464; +pub const AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = -2004287463; +pub const AUDCLNT_E_INVALID_DEVICE_PERIOD = -2004287456; +pub const AUDCLNT_E_INVALID_STREAM_FLAG = -2004287455; +pub const AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE = -2004287454; +pub const AUDCLNT_E_OUT_OF_OFFLOAD_RESOURCES = -2004287453; +pub const AUDCLNT_E_OFFLOAD_MODE_ONLY = -2004287452; +pub const AUDCLNT_E_NONOFFLOAD_MODE_ONLY = -2004287451; +pub const AUDCLNT_E_RESOURCES_INVALIDATED = -2004287450; +pub const AUDCLNT_E_RAW_MODE_UNSUPPORTED = -2004287449; +pub const AUDCLNT_E_ENGINE_PERIODICITY_LOCKED = -2004287448; +pub const AUDCLNT_E_ENGINE_FORMAT_LOCKED = -2004287447; +pub const AUDCLNT_E_HEADTRACKING_ENABLED = -2004287440; +pub const AUDCLNT_E_HEADTRACKING_UNSUPPORTED = -2004287424; +pub const AUDCLNT_E_EFFECT_NOT_AVAILABLE = -2004287423; +pub const AUDCLNT_E_EFFECT_STATE_READ_ONLY = -2004287422; diff --git a/libs/sysaudio/src/webaudio.zig b/libs/sysaudio/src/webaudio.zig deleted file mode 100644 index 2925481f..00000000 --- a/libs/sysaudio/src/webaudio.zig +++ /dev/null @@ -1,213 +0,0 @@ -const std = @import("std"); -const Mode = @import("main.zig").Mode; -const Format = @import("main.zig").Format; -const DataCallback = @import("main.zig").DataCallback; -const js = @import("sysjs"); - -const Audio = @This(); - -pub const Device = struct { - properties: Properties, - - // Internal fields. - context: js.Object, - - pub const Options = struct { - mode: Mode = .output, - format: ?Format = null, - is_raw: ?bool = null, - channels: ?u8 = null, - sample_rate: ?u32 = null, - id: ?[:0]const u8 = null, - name: ?[]const u8 = null, - }; - - pub const Properties = struct { - mode: Mode, - format: Format, - is_raw: bool, - channels: u8, - sample_rate: u32, - id: [:0]const u8, - name: []const u8, - }; - - pub fn deinit(device: *Device, allocator: std.mem.Allocator) void { - device.context.deinit(); - allocator.destroy(device); - } - - pub fn setCallback(device: *Device, callback: DataCallback, user_data: ?*anyopaque) void { - device.context.set("device", js.createNumber(@intToFloat(f64, @ptrToInt(device)))); - device.context.set("callback", js.createNumber(@intToFloat(f64, @ptrToInt(callback)))); - if (user_data) |ud| - device.context.set("user_data", js.createNumber(@intToFloat(f64, @ptrToInt(ud)))); - } - - pub fn pause(device: *Device) Error!void { - _ = device.context.call("suspend", &.{}); - } - - pub fn start(device: *Device) Error!void { - _ = device.context.call("resume", &.{}); - } -}; - -pub const DeviceIterator = struct { - ctx: *Audio, - mode: Mode, - - pub fn next(_: DeviceIterator) IteratorError!?Device.Properties { - return null; - } -}; - -pub const IteratorError = error{}; - -pub const Error = error{ - OutOfMemory, - AudioUnsupported, -}; - -context_constructor: js.Function, - -pub fn init() Error!Audio { - const context = js.global().get("AudioContext"); - if (context.is(.undefined)) - return error.AudioUnsupported; - - return Audio{ .context_constructor = context.view(.func) }; -} - -pub fn deinit(audio: Audio) void { - audio.context_constructor.deinit(); -} - -// TODO(sysaudio): implement waitEvents for WebAudio, will a WASM process terminate without this? -pub fn waitEvents(_: Audio) void {} - -const default_channel_count = 2; -const default_sample_rate = 48000; -const default_buffer_size_per_channel = 1024; // 21.33ms - -pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device { - // NOTE: WebAudio only supports F32 audio format, so options.format is unused - const mode = options.mode; - const channels = options.channels orelse default_channel_count; - const sample_rate = options.sample_rate orelse default_sample_rate; - - const context_options = js.createMap(); - defer context_options.deinit(); - context_options.set("sampleRate", js.createNumber(@intToFloat(f64, sample_rate))); - - const context = audio.context_constructor.construct(&.{context_options.toValue()}); - _ = context.call("suspend", &.{}); - - const input_channels = if (mode == .input) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined(); - const output_channels = if (mode == .output) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined(); - - const node = context.call("createScriptProcessor", &.{ js.createNumber(default_buffer_size_per_channel), input_channels, output_channels }).view(.object); - defer node.deinit(); - - context.set("node", node.toValue()); - - { - // TODO(sysaudio): this capture leaks for now, we need a better way to pass captures via sysjs - // that passes by value I think. - const captures = std.heap.page_allocator.alloc(js.Value, 1) catch unreachable; - captures[0] = context.toValue(); - const audio_process_event = js.createFunction(audioProcessEvent, captures); - - // TODO(sysaudio): this leaks, we need a good place to clean this up. - // defer audio_process_event.deinit(); - node.set("onaudioprocess", audio_process_event.toValue()); - } - - { - const destination = context.get("destination").view(.object); - defer destination.deinit(); - _ = node.call("connect", &.{destination.toValue()}); - } - - // TODO(sysaudio): Figure out ID/name or make optional again - var properties = Device.Properties{ - .id = "0", - .name = "WebAudio", - .format = .F32, - .mode = options.mode, - .is_raw = false, - .channels = options.channels orelse default_channel_count, - .sample_rate = options.sample_rate orelse default_sample_rate, - }; - - const device = try allocator.create(Device); - device.* = .{ - .properties = properties, - .context = context, - }; - return device; -} - -fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value { - const device_context = captures[0].view(.object); - - const audio_event = args.getIndex(0).view(.object); - defer audio_event.deinit(); - const output_buffer = audio_event.get("outputBuffer").view(.object); - defer output_buffer.deinit(); - const num_channels = @floatToInt(usize, output_buffer.get("numberOfChannels").view(.num)); - - const buffer_length = default_buffer_size_per_channel * num_channels * @sizeOf(f32); - // TODO(sysaudio): reuse buffer, do not allocate in this hot path - const buffer = std.heap.page_allocator.alloc(u8, buffer_length) catch unreachable; - defer std.heap.page_allocator.free(buffer); - - const callback = device_context.get("callback"); - if (!callback.is(.undefined)) { - var dev = @intToPtr(*Device, @floatToInt(usize, device_context.get("device").view(.num))); - const cb = @intToPtr(DataCallback, @floatToInt(usize, callback.view(.num))); - const user_data = device_context.get("user_data"); - const ud = if (user_data.is(.undefined)) null else @intToPtr(*anyopaque, @floatToInt(usize, user_data.view(.num))); - - // TODO(sysaudio): do not reconstruct Uint8Array (expensive) - const source = js.constructType("Uint8Array", &.{js.createNumber(@intToFloat(f64, buffer_length))}); - defer source.deinit(); - - cb(dev, ud, buffer[0..]); - source.copyBytes(buffer[0..]); - - const float_source = js.constructType("Float32Array", &.{ - source.get("buffer"), - source.get("byteOffset"), - js.createNumber(source.get("byteLength").view(.num) / 4), - }); - defer float_source.deinit(); - - js.global().set("source", source.toValue()); - js.global().set("float_source", float_source.toValue()); - js.global().set("output_buffer", output_buffer.toValue()); - - var channel: usize = 0; - while (channel < num_channels) : (channel += 1) { - // TODO(sysaudio): investigate if using copyToChannel would be better? - //_ = output_buffer.call("copyToChannel", &.{ float_source.toValue(), js.createNumber(@intToFloat(f64, channel)) }); - const output_data = output_buffer.call("getChannelData", &.{js.createNumber(@intToFloat(f64, channel))}).view(.object); - defer output_data.deinit(); - const channel_slice = float_source.call("slice", &.{ - js.createNumber(@intToFloat(f64, channel * default_buffer_size_per_channel)), - js.createNumber(@intToFloat(f64, (channel + 1) * default_buffer_size_per_channel)), - }); - _ = output_data.call("set", &.{channel_slice}); - } - } - - return js.createUndefined(); -} - -pub fn outputDeviceIterator(audio: Audio) DeviceIterator { - return .{ .audio = audio, .mode = .output }; -} - -pub fn inputDeviceIterator(audio: Audio) DeviceIterator { - return .{ .audio = audio, .mode = .input }; -} diff --git a/libs/sysaudio/upstream b/libs/sysaudio/upstream deleted file mode 160000 index 56c1f298..00000000 --- a/libs/sysaudio/upstream +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 56c1f298c25388e78bedd48085c385aed945d1e5