diff --git a/src/sysaudio/alsa.zig b/src/sysaudio/alsa.zig new file mode 100644 index 00000000..a5fd464c --- /dev/null +++ b/src/sysaudio/alsa.zig @@ -0,0 +1,835 @@ +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; + +const default_sample_rate = 44_100; // Hz + +var lib: Lib = undefined; +const Lib = struct { + handle: std.DynLib, + + snd_lib_error_set_handler: *const fn (c.snd_lib_error_handler_t) callconv(.C) c_int, + snd_pcm_info_malloc: *const fn ([*c]?*c.snd_pcm_info_t) callconv(.C) c_int, + snd_pcm_info_free: *const fn (?*c.snd_pcm_info_t) callconv(.C) void, + snd_pcm_open: *const fn ([*c]?*c.snd_pcm_t, [*c]const u8, c.snd_pcm_stream_t, c_int) callconv(.C) c_int, + snd_pcm_close: *const fn (?*c.snd_pcm_t) callconv(.C) c_int, + snd_pcm_state: *const fn (?*c.snd_pcm_t) callconv(.C) c.snd_pcm_state_t, + snd_pcm_pause: *const fn (?*c.snd_pcm_t, c_int) callconv(.C) c_int, + snd_pcm_writei: *const fn (?*c.snd_pcm_t, ?*const anyopaque, c.snd_pcm_uframes_t) callconv(.C) c.snd_pcm_sframes_t, + snd_pcm_readi: *const fn (?*c.snd_pcm_t, ?*const anyopaque, c.snd_pcm_uframes_t) callconv(.C) c.snd_pcm_sframes_t, + snd_pcm_prepare: *const fn (?*c.snd_pcm_t) callconv(.C) c_int, + snd_pcm_info_set_device: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void, + snd_pcm_info_set_subdevice: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void, + snd_pcm_info_get_name: *const fn (?*const c.snd_pcm_info_t) callconv(.C) [*c]const u8, + snd_pcm_info_set_stream: *const fn (?*c.snd_pcm_info_t, c.snd_pcm_stream_t) callconv(.C) void, + snd_pcm_hw_free: *const fn (?*c.snd_pcm_t) callconv(.C) c_int, + snd_pcm_hw_params_malloc: *const fn ([*c]?*c.snd_pcm_hw_params_t) callconv(.C) c_int, + snd_pcm_hw_params_free: *const fn (?*c.snd_pcm_hw_params_t) callconv(.C) void, + snd_pcm_set_params: *const fn (?*c.snd_pcm_t, c.snd_pcm_format_t, c.snd_pcm_access_t, c_uint, c_uint, c_int, c_uint) callconv(.C) c_int, + snd_pcm_hw_params_any: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int, + snd_pcm_hw_params_can_pause: *const fn (?*const c.snd_pcm_hw_params_t) callconv(.C) c_int, + snd_pcm_hw_params_current: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int, + snd_pcm_hw_params_get_format_mask: *const fn (?*c.snd_pcm_hw_params_t, ?*c.snd_pcm_format_mask_t) callconv(.C) void, + snd_pcm_hw_params_get_rate_min: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int, + snd_pcm_hw_params_get_rate_max: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int, + snd_pcm_hw_params_get_period_size: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c.snd_pcm_uframes_t, [*c]c_int) callconv(.C) c_int, + snd_pcm_query_chmaps: *const fn (?*c.snd_pcm_t) callconv(.C) [*c][*c]c.snd_pcm_chmap_query_t, + snd_pcm_free_chmaps: *const fn ([*c][*c]c.snd_pcm_chmap_query_t) callconv(.C) void, + snd_pcm_format_mask_malloc: *const fn ([*c]?*c.snd_pcm_format_mask_t) callconv(.C) c_int, + snd_pcm_format_mask_free: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void, + snd_pcm_format_mask_none: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void, + snd_pcm_format_mask_set: *const fn (?*c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) void, + snd_pcm_format_mask_test: *const fn (?*const c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) c_int, + snd_card_next: *const fn ([*c]c_int) callconv(.C) c_int, + snd_ctl_open: *const fn ([*c]?*c.snd_ctl_t, [*c]const u8, c_int) callconv(.C) c_int, + snd_ctl_close: *const fn (?*c.snd_ctl_t) callconv(.C) c_int, + snd_ctl_pcm_next_device: *const fn (?*c.snd_ctl_t, [*c]c_int) callconv(.C) c_int, + snd_ctl_pcm_info: *const fn (?*c.snd_ctl_t, ?*c.snd_pcm_info_t) callconv(.C) c_int, + snd_mixer_open: *const fn ([*c]?*c.snd_mixer_t, c_int) callconv(.C) c_int, + snd_mixer_close: *const fn (?*c.snd_mixer_t) callconv(.C) c_int, + snd_mixer_load: *const fn (?*c.snd_mixer_t) callconv(.C) c_int, + snd_mixer_attach: *const fn (?*c.snd_mixer_t, [*c]const u8) callconv(.C) c_int, + snd_mixer_find_selem: *const fn (?*c.snd_mixer_t, ?*const c.snd_mixer_selem_id_t) callconv(.C) ?*c.snd_mixer_elem_t, + snd_mixer_selem_register: *const fn (?*c.snd_mixer_t, [*c]c.struct_snd_mixer_selem_regopt, [*c]?*c.snd_mixer_class_t) callconv(.C) c_int, + snd_mixer_selem_id_malloc: *const fn ([*c]?*c.snd_mixer_selem_id_t) callconv(.C) c_int, + snd_mixer_selem_id_free: *const fn (?*c.snd_mixer_selem_id_t) callconv(.C) void, + snd_mixer_selem_id_set_index: *const fn (?*c.snd_mixer_selem_id_t, c_uint) callconv(.C) void, + snd_mixer_selem_id_set_name: *const fn (?*c.snd_mixer_selem_id_t, [*c]const u8) callconv(.C) void, + snd_mixer_selem_set_playback_volume_all: *const fn (?*c.snd_mixer_elem_t, c_long) callconv(.C) c_int, + snd_mixer_selem_get_playback_volume: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t, [*c]c_long) callconv(.C) c_int, + snd_mixer_selem_get_playback_volume_range: *const fn (?*c.snd_mixer_elem_t, [*c]c_long, [*c]c_long) callconv(.C) c_int, + snd_mixer_selem_has_playback_channel: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t) callconv(.C) c_int, + snd_mixer_selem_set_capture_volume_all: *const fn (?*c.snd_mixer_elem_t, c_long) callconv(.C) c_int, + snd_mixer_selem_get_capture_volume: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t, [*c]c_long) callconv(.C) c_int, + snd_mixer_selem_get_capture_volume_range: *const fn (?*c.snd_mixer_elem_t, [*c]c_long, [*c]c_long) callconv(.C) c_int, + snd_mixer_selem_has_capture_channel: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t) callconv(.C) c_int, + + pub fn load() !void { + lib.handle = std.DynLib.openZ("libasound.so") catch return error.LibraryNotFound; + inline for (@typeInfo(Lib).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + } +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.Context.DeviceChangeFn, + user_data: ?*anyopaque, + thread: std.Thread, + aborted: std.atomic.Value(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.Context { + try Lib.load(); + + _ = lib.snd_lib_error_set_handler(@as(c.snd_lib_error_handler_t, @ptrCast(&util.doNothing))); + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .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, + .user_data = options.user_data, + .aborted = .{ .raw = false }, + .notify_fd = notify_fd, + .notify_wd = notify_wd, + .notify_pipe_fd = notify_pipe_fd, + .thread = std.Thread.spawn(.{}, deviceEventsLoop, .{ctx}) 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 = ctx }; + } + + pub fn deinit(ctx: *Context) void { + if (ctx.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 (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + lib.handle.close(); + } + + fn deviceEventsLoop(ctx: *Context) void { + var watcher = ctx.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 = @as(*inotify_event, @ptrCast(@alignCast(buf[i..]))); + const evt_name = @as([*]u8, @ptrCast(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(ctx.watcher.?.user_data); + scan = false; + } + } + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + var pcm_info: ?*c.snd_pcm_info_t = null; + _ = lib.snd_pcm_info_malloc(&pcm_info); + defer lib.snd_pcm_info_free(pcm_info); + + var card_idx: c_int = -1; + if (lib.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 (-lib.snd_ctl_open(&ctl, card_id.ptr, 0)) { + 0 => {}, + @intFromEnum(std.os.E.NOENT) => break, + else => return error.OpeningDevice, + }; + defer _ = lib.snd_ctl_close(ctl); + + var dev_idx: c_int = -1; + if (lib.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0) + return error.SystemResources; + + lib.snd_pcm_info_set_device(pcm_info, @as(c_uint, @intCast(dev_idx))); + lib.snd_pcm_info_set_subdevice(pcm_info, 0); + const name = std.mem.span(lib.snd_pcm_info_get_name(pcm_info) orelse continue); + + for (&[_]main.Device.Mode{ .playback, .capture }) |mode| { + const snd_stream = modeToStream(mode); + lib.snd_pcm_info_set_stream(pcm_info, snd_stream); + const err = lib.snd_ctl_pcm_info(ctl, pcm_info); + switch (@as(std.os.E, @enumFromInt(-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 (lib.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0) + continue; + defer _ = lib.snd_pcm_close(pcm); + + var params: ?*c.snd_pcm_hw_params_t = null; + _ = lib.snd_pcm_hw_params_malloc(¶ms); + defer lib.snd_pcm_hw_params_free(params); + if (lib.snd_pcm_hw_params_any(pcm, params) < 0) + continue; + + if (lib.snd_pcm_hw_params_can_pause(params) == 0) + continue; + + const device = main.Device{ + .mode = mode, + .channels = blk: { + const chmap = lib.snd_pcm_query_chmaps(pcm); + if (chmap) |_| { + defer lib.snd_pcm_free_chmaps(chmap); + + if (chmap[0] == null) continue; + + const channels = try ctx.allocator.alloc(main.ChannelPosition, chmap.*.*.map.channels); + for (channels, 0..) |*ch, i| + ch.* = 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; + _ = lib.snd_pcm_format_mask_malloc(&fmt_mask); + defer lib.snd_pcm_format_mask_free(fmt_mask); + lib.snd_pcm_format_mask_none(fmt_mask); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE); + lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE); + lib.snd_pcm_hw_params_get_format_mask(params, fmt_mask); + + var fmt_arr = std.ArrayList(main.Format).init(ctx.allocator); + inline for (std.meta.tags(main.Format)) |format| { + if (lib.snd_pcm_format_mask_test(fmt_mask, toAlsaFormat(format)) != 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 (lib.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0) + continue; + if (lib.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0) + continue; + break :blk .{ + .min = @as(u24, @intCast(rate_min)), + .max = @as(u24, @intCast(rate_max)), + }; + }, + .id = try ctx.allocator.dupeZ(u8, id), + .name = try ctx.allocator.dupeZ(u8, name), + }; + + try ctx.devices_info.list.append(ctx.allocator, device); + + if (ctx.devices_info.default(mode) == null and dev_idx == 0) { + ctx.devices_info.setDefault(mode, ctx.devices_info.list.items.len - 1); + } + } + + if (lib.snd_card_next(&card_idx) < 0) + return error.SystemResources; + } + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createStream( + ctx: Context, + device: main.Device, + format: main.Format, + sample_rate: u24, + pcm: *?*c.snd_pcm_t, + mixer: *?*c.snd_mixer_t, + selem: *?*c.snd_mixer_selem_id_t, + mixer_elm: *?*c.snd_mixer_elem_t, + period_size: *c_ulong, + ) !void { + if (lib.snd_pcm_open(pcm, device.id.ptr, modeToStream(device.mode), 0) < 0) + return error.OpeningDevice; + errdefer _ = lib.snd_pcm_close(pcm.*); + { + var hw_params: ?*c.snd_pcm_hw_params_t = null; + + if ((lib.snd_pcm_set_params( + pcm.*, + toAlsaFormat(format), + c.SND_PCM_ACCESS_RW_INTERLEAVED, + @as(c_uint, @intCast(device.channels.len)), + sample_rate, + 1, + main.default_latency, + )) < 0) + return error.OpeningDevice; + errdefer _ = lib.snd_pcm_hw_free(pcm.*); + + if (lib.snd_pcm_hw_params_malloc(&hw_params) < 0) + return error.OpeningDevice; + defer lib.snd_pcm_hw_params_free(hw_params); + + if (lib.snd_pcm_hw_params_current(pcm.*, hw_params) < 0) + return error.OpeningDevice; + + if (lib.snd_pcm_hw_params_get_period_size(hw_params, period_size, null) < 0) + return error.OpeningDevice; + } + + { + if (lib.snd_mixer_open(mixer, 0) < 0) + return error.OutOfMemory; + + const card_id = try ctx.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ',')); + defer ctx.allocator.free(card_id); + + if (lib.snd_mixer_attach(mixer.*, card_id.ptr) < 0) + return error.IncompatibleDevice; + + if (lib.snd_mixer_selem_register(mixer.*, null, null) < 0) + return error.OpeningDevice; + + if (lib.snd_mixer_load(mixer.*) < 0) + return error.OpeningDevice; + + if (lib.snd_mixer_selem_id_malloc(selem) < 0) + return error.OutOfMemory; + errdefer lib.snd_mixer_selem_id_free(selem.*); + + lib.snd_mixer_selem_id_set_index(selem.*, 0); + lib.snd_mixer_selem_id_set_name(selem.*, "Master"); + + mixer_elm.* = lib.snd_mixer_find_selem(mixer.*, selem.*) orelse + return error.IncompatibleDevice; + } + } + + pub fn createPlayer(ctx: Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_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; + try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size); + + const player = try ctx.allocator.create(Player); + player.* = .{ + .allocator = ctx.allocator, + .thread = undefined, + .aborted = .{ .raw = false }, + .sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))), + .period_size = period_size, + .pcm = pcm.?, + .mixer = mixer.?, + .selem = selem.?, + .mixer_elm = mixer_elm.?, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .alsa = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_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; + try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size); + + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .thread = undefined, + .aborted = .{ .raw = false }, + .sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))), + .period_size = period_size, + .pcm = pcm.?, + .mixer = mixer.?, + .selem = selem.?, + .mixer_elm = mixer_elm.?, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .alsa = recorder }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + thread: std.Thread, + aborted: std.atomic.Value(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, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(player: *Player) void { + player.aborted.store(true, .Unordered); + player.thread.join(); + + _ = lib.snd_mixer_close(player.mixer); + lib.snd_mixer_selem_id_free(player.selem); + _ = lib.snd_pcm_close(player.pcm); + _ = lib.snd_pcm_hw_free(player.pcm); + + player.allocator.free(player.sample_buffer); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + player.thread = std.Thread.spawn(.{}, writeThread, .{player}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + error.Unexpected => unreachable, + }; + } + + fn writeThread(player: *Player) void { + var underrun = false; + while (!player.aborted.load(.Unordered)) { + if (!underrun) { + player.writeFn( + player.user_data, + player.sample_buffer[0 .. player.period_size * player.format.frameSize(@intCast(player.channels.len))], + ); + } + underrun = false; + const n = lib.snd_pcm_writei(player.pcm, player.sample_buffer.ptr, player.period_size); + if (n < 0) { + _ = lib.snd_pcm_prepare(player.pcm); + underrun = true; + } + } + } + + pub fn play(player: *Player) !void { + if (lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED) { + if (lib.snd_pcm_pause(player.pcm, 0) < 0) + return error.CannotPlay; + } + } + + pub fn pause(player: *Player) !void { + if (lib.snd_pcm_state(player.pcm) != c.SND_PCM_STATE_PAUSED) { + if (lib.snd_pcm_pause(player.pcm, 1) < 0) + return error.CannotPause; + } + } + + pub fn paused(player: *Player) bool { + return lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + var min_vol: c_long = 0; + var max_vol: c_long = 0; + if (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotSetVolume; + + const dist = @as(f32, @floatFromInt(max_vol - min_vol)); + if (lib.snd_mixer_selem_set_playback_volume_all( + player.mixer_elm, + @as(c_long, @intFromFloat(dist * vol)) + min_vol, + ) < 0) + return error.CannotSetVolume; + } + + pub fn volume(player: *Player) !f32 { + var vol: c_long = 0; + var channel: c_int = 0; + + while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) { + if (lib.snd_mixer_selem_has_playback_channel(player.mixer_elm, channel) == 1) { + if (lib.snd_mixer_selem_get_playback_volume(player.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 (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotGetVolume; + + return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol)); + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + thread: std.Thread, + aborted: std.atomic.Value(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, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(recorder: *Recorder) void { + recorder.aborted.store(true, .Unordered); + recorder.thread.join(); + + _ = lib.snd_mixer_close(recorder.mixer); + lib.snd_mixer_selem_id_free(recorder.selem); + _ = lib.snd_pcm_close(recorder.pcm); + _ = lib.snd_pcm_hw_free(recorder.pcm); + + recorder.allocator.free(recorder.sample_buffer); + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + recorder.thread = std.Thread.spawn(.{}, readThread, .{recorder}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + error.Unexpected => unreachable, + }; + } + + fn readThread(recorder: *Recorder) void { + var underrun = false; + while (!recorder.aborted.load(.Unordered)) { + if (!underrun) { + recorder.readFn(recorder.user_data, recorder.sample_buffer[0..recorder.period_size]); + } + underrun = false; + const n = lib.snd_pcm_readi(recorder.pcm, recorder.sample_buffer.ptr, recorder.period_size); + if (n < 0) { + _ = lib.snd_pcm_prepare(recorder.pcm); + underrun = true; + } + } + } + + pub fn record(recorder: *Recorder) !void { + if (lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED) { + if (lib.snd_pcm_pause(recorder.pcm, 0) < 0) + return error.CannotRecord; + } + } + + pub fn pause(recorder: *Recorder) !void { + if (lib.snd_pcm_state(recorder.pcm) != c.SND_PCM_STATE_PAUSED) { + if (lib.snd_pcm_pause(recorder.pcm, 1) < 0) + return error.CannotPause; + } + } + + pub fn paused(recorder: *Recorder) bool { + return lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + var min_vol: c_long = 0; + var max_vol: c_long = 0; + if (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotSetVolume; + + const dist = @as(f32, @floatFromInt(max_vol - min_vol)); + if (lib.snd_mixer_selem_set_capture_volume_all( + recorder.mixer_elm, + @as(c_long, @intFromFloat(dist * vol)) + min_vol, + ) < 0) + return error.CannotSetVolume; + } + + pub fn volume(recorder: *Recorder) !f32 { + var vol: c_long = 0; + var channel: c_int = 0; + + while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) { + if (lib.snd_mixer_selem_has_capture_channel(recorder.mixer_elm, channel) == 1) { + if (lib.snd_mixer_selem_get_capture_volume(recorder.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 (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0) + return error.CannotGetVolume; + + return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol)); + } +}; + +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, + .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, + .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, + }; +} + +pub fn fromAlsaChannel(pos: c_uint) !main.ChannelPosition { + 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_RLC => .back_left, + c.SND_CHMAP_RRC => .back_right, + 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.ChannelPosition) 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, + .back_left => c.SND_CHMAP_RLC, + .back_right => c.SND_CHMAP_RRC, + .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, + }; +} diff --git a/src/sysaudio/backends.zig b/src/sysaudio/backends.zig new file mode 100644 index 00000000..71e4283b --- /dev/null +++ b/src/sysaudio/backends.zig @@ -0,0 +1,106 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +pub const Backend = std.meta.Tag(Context); + +pub const Context = switch (builtin.os.tag) { + .linux => union(enum) { + pulseaudio: *@import("pulseaudio.zig").Context, + pipewire: *@import("pipewire.zig").Context, + jack: *@import("jack.zig").Context, + alsa: *@import("alsa.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .freebsd, .netbsd, .openbsd, .solaris => union(enum) { + pipewire: *@import("pipewire.zig").Context, + pulseaudio: *@import("pulseaudio.zig").Context, + jack: *@import("jack.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .macos, .ios, .watchos, .tvos => union(enum) { + coreaudio: *@import("coreaudio.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .windows => union(enum) { + wasapi: *@import("wasapi.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + .freestanding => switch (builtin.cpu.arch) { + .wasm32 => union(enum) { + webaudio: *@import("webaudio.zig").Context, + dummy: *@import("dummy.zig").Context, + }, + else => union(enum) { + dummy: *@import("dummy.zig").Context, + }, + }, + else => union(enum) { dummy: *@import("dummy.zig").Context }, +}; + +pub const Player = switch (builtin.os.tag) { + .linux => union(enum) { + pulseaudio: *@import("pulseaudio.zig").Player, + pipewire: *@import("pipewire.zig").Player, + jack: *@import("jack.zig").Player, + alsa: *@import("alsa.zig").Player, + dummy: *@import("dummy.zig").Player, + }, + .freebsd, .netbsd, .openbsd, .solaris => union(enum) { + pipewire: *@import("pipewire.zig").Player, + pulseaudio: *@import("pulseaudio.zig").Player, + jack: *@import("jack.zig").Player, + dummy: *@import("dummy.zig").Player, + }, + .macos, .ios, .watchos, .tvos => union(enum) { + coreaudio: *@import("coreaudio.zig").Player, + dummy: *@import("dummy.zig").Player, + }, + .windows => union(enum) { + wasapi: *@import("wasapi.zig").Player, + dummy: *@import("dummy.zig").Player, + }, + .freestanding => switch (builtin.cpu.arch) { + .wasm32 => union(enum) { + webaudio: *@import("webaudio.zig").Player, + dummy: *@import("dummy.zig").Player, + }, + else => union(enum) { + dummy: *@import("dummy.zig").Player, + }, + }, + else => union(enum) { dummy: *@import("dummy.zig").Player }, +}; + +pub const Recorder = switch (builtin.os.tag) { + .linux => union(enum) { + pulseaudio: *@import("pulseaudio.zig").Recorder, + pipewire: *@import("pipewire.zig").Recorder, + jack: *@import("jack.zig").Recorder, + alsa: *@import("alsa.zig").Recorder, + dummy: *@import("dummy.zig").Recorder, + }, + .freebsd, .netbsd, .openbsd, .solaris => union(enum) { + pipewire: *@import("pipewire.zig").Recorder, + pulseaudio: *@import("pulseaudio.zig").Recorder, + jack: *@import("jack.zig").Recorder, + dummy: *@import("dummy.zig").Recorder, + }, + .macos, .ios, .watchos, .tvos => union(enum) { + coreaudio: *@import("coreaudio.zig").Recorder, + dummy: *@import("dummy.zig").Recorder, + }, + .windows => union(enum) { + wasapi: *@import("wasapi.zig").Recorder, + dummy: *@import("dummy.zig").Recorder, + }, + .freestanding => switch (builtin.cpu.arch) { + .wasm32 => union(enum) { + webaudio: *@import("webaudio.zig").Recorder, + dummy: *@import("dummy.zig").Recorder, + }, + else => union(enum) { + dummy: *@import("dummy.zig").Recorder, + }, + }, + else => union(enum) { dummy: *@import("dummy.zig").Recorder }, +}; diff --git a/src/sysaudio/conv.zig b/src/sysaudio/conv.zig new file mode 100644 index 00000000..2965fa21 --- /dev/null +++ b/src/sysaudio/conv.zig @@ -0,0 +1,349 @@ +const std = @import("std"); +const expectEqual = std.testing.expectEqual; +const shl = std.math.shl; +const shr = std.math.shr; +const maxInt = std.math.maxInt; + +pub fn unsignedToSigned( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const half = (maxInt(SrcType) + 1) / 2; + const trunc = @bitSizeOf(DestType) - @bitSizeOf(SrcType); + dst_sample.* = shl(DestType, @intCast(src_sample.* -% half), trunc); + } +} + +test unsignedToSigned { + var u8_to_i16: [1]i16 = undefined; + var u8_to_i24: [1]i24 = undefined; + var u8_to_i32: [1]i32 = undefined; + + unsignedToSigned(u8, &.{5}, i16, &u8_to_i16); + unsignedToSigned(u8, &.{5}, i24, &u8_to_i24); + unsignedToSigned(u8, &.{5}, i32, &u8_to_i32); + + try expectEqual(@as(i16, -31488), u8_to_i16[0]); + try expectEqual(@as(i24, -8060928), u8_to_i24[0]); + try expectEqual(@as(i32, -2063597568), u8_to_i32[0]); +} + +pub fn unsignedToFloat( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const half = (maxInt(SrcType) + 1) / 2; + dst_sample.* = (@as(DestType, @floatFromInt(src_sample.*)) - half) * 1.0 / half; + } +} + +test unsignedToFloat { + var u8_to_f32: [1]f32 = undefined; + unsignedToFloat(u8, &.{5}, f32, &u8_to_f32); + try expectEqual(@as(f32, -0.9609375), u8_to_f32[0]); +} + +pub fn signedToUnsigned( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const half = (maxInt(DestType) + 1) / 2; + const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType); + dst_sample.* = shr(DestType, @intCast(src_sample.*), trunc) + half; + } +} + +test signedToUnsigned { + var i16_to_u8: [1]u8 = undefined; + var i24_to_u8: [1]u8 = undefined; + var i32_to_u8: [1]u8 = undefined; + + signedToUnsigned(i16, &.{5}, u8, &i16_to_u8); + signedToUnsigned(i24, &.{5}, u8, &i24_to_u8); + signedToUnsigned(i32, &.{5}, u8, &i32_to_u8); + + try expectEqual(@as(u8, 128), i16_to_u8[0]); + try expectEqual(@as(u8, 128), i24_to_u8[0]); + try expectEqual(@as(u8, 128), i32_to_u8[0]); +} + +pub fn signedToSigned( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + // TODO: Uncomment this (zig crashes) + // if (std.simd.suggestVectorSize(SrcType)) |_| { + // signedToSignedSIMD(SrcType, src, DestType, dst); + // } else { + signedToSignedScalar(SrcType, src, DestType, dst); + // } +} + +pub fn signedToSignedScalar( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType); + dst_sample.* = shr(DestType, @intCast(src_sample.*), trunc); + } +} + +test signedToSignedScalar { + var i16_to_i24: [1]i24 = undefined; + var i16_to_i32: [1]i32 = undefined; + var i24_to_i16: [1]i16 = undefined; + var i24_to_i32: [1]i32 = undefined; + var i32_to_i16: [1]i16 = undefined; + var i32_to_i24: [1]i24 = undefined; + + signedToSignedScalar(i24, &.{5}, i16, &i24_to_i16); + signedToSignedScalar(i32, &.{5}, i16, &i32_to_i16); + + signedToSignedScalar(i16, &.{5}, i24, &i16_to_i24); + signedToSignedScalar(i32, &.{5}, i24, &i32_to_i24); + + signedToSignedScalar(i16, &.{5}, i32, &i16_to_i32); + signedToSignedScalar(i24, &.{5}, i32, &i24_to_i32); + + try expectEqual(@as(i24, 1280), i16_to_i24[0]); + try expectEqual(@as(i32, 327680), i16_to_i32[0]); + + try expectEqual(@as(i16, 0), i24_to_i16[0]); + try expectEqual(@as(i32, 1280), i24_to_i32[0]); + + try expectEqual(@as(i16, 0), i32_to_i16[0]); + try expectEqual(@as(i24, 0), i32_to_i24[0]); +} + +pub fn signedToSignedSIMD( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + const vec_size = std.simd.suggestVectorSize(SrcType).?; + const VecSrc = @Vector(vec_size, SrcType); + const VecDst = @Vector(vec_size, DestType); + const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType); + const vec_blocks_len = src.len - (src.len % vec_size); + var i: usize = 0; + while (i < vec_blocks_len) : (i += vec_size) { + const src_vec: VecSrc = src[i..][0..vec_size].*; + dst[i..][0..vec_size].* = shr(VecDst, @intCast(src_vec), trunc); + } + if (i != src.len) signedToSignedScalar(SrcType, src[i..], DestType, dst[i..]); +} + +test signedToSignedSIMD { + var i16_to_i32: [32 + 7]i32 = undefined; + const items = [1]i16{5} ** (32 + 7); + signedToSignedSIMD(i16, &items, i32, &i16_to_i32); + try expectEqual(@as(i32, 327680), i16_to_i32[0]); + try expectEqual(i16_to_i32[0], i16_to_i32[i16_to_i32.len - 1]); +} + +pub fn signedToFloat( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + if (std.simd.suggestVectorSize(SrcType)) |_| { + signedToFloatSIMD(SrcType, src, DestType, dst); + } else { + signedToFloatScalar(SrcType, src, DestType, dst); + } +} + +pub fn signedToFloatScalar( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + const max: comptime_float = maxInt(SrcType) + 1; + const div_by_max = 1.0 / max; + for (src, dst) |*src_sample, *dst_sample| { + dst_sample.* = @as(DestType, @floatFromInt(src_sample.*)) * div_by_max; + } +} + +test signedToFloatScalar { + var i16_to_f32: [1]f32 = undefined; + var i24_to_f32: [1]f32 = undefined; + var i32_to_f32: [1]f32 = undefined; + + signedToFloatScalar(i16, &.{5}, f32, &i16_to_f32); + signedToFloatScalar(i24, &.{5}, f32, &i24_to_f32); + signedToFloatScalar(i32, &.{5}, f32, &i32_to_f32); + + try expectEqual(@as(f32, 1.52587890625e-4), i16_to_f32[0]); + try expectEqual(@as(f32, 5.9604644775391e-7), i24_to_f32[0]); + try expectEqual(@as(f32, 2.32830643e-09), i32_to_f32[0]); +} + +pub fn signedToFloatSIMD( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + const vec_size = std.simd.suggestVectorSize(SrcType).?; + const VecSrc = @Vector(vec_size, SrcType); + const VecDst = @Vector(vec_size, DestType); + const div_by_max: VecDst = @splat(1.0 / @as(comptime_float, maxInt(SrcType) + 1)); + const vec_blocks_len = src.len - (src.len % vec_size); + var i: usize = 0; + while (i < vec_blocks_len) : (i += vec_size) { + const src_vec: VecSrc = src[i..][0..vec_size].*; + dst[i..][0..vec_size].* = @as(VecDst, @floatFromInt(src_vec)) * div_by_max; + } + if (i != src.len) signedToFloatScalar(SrcType, src[i..], DestType, dst[i..]); +} + +test signedToFloatSIMD { + var i32_to_f32: [32 + 7]f32 = undefined; + const items = [1]i32{5} ** (32 + 7); + signedToFloatSIMD(i32, &items, f32, &i32_to_f32); + try expectEqual(@as(f32, 2.32830643e-09), i32_to_f32[0]); + try expectEqual(i32_to_f32[0], i32_to_f32[i32_to_f32.len - 1]); +} + +pub fn floatToUnsigned( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + if (std.simd.suggestVectorSize(SrcType)) |_| { + floatToUnsignedSIMD(SrcType, src, DestType, dst); + } else { + floatToUnsignedScalar(SrcType, src, DestType, dst); + } +} + +pub fn floatToUnsignedScalar( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const half = maxInt(DestType) / 2; + dst_sample.* = @intFromFloat(src_sample.* * half + (half + 1)); + } +} + +test floatToUnsignedScalar { + var f32_to_u8: [1]u8 = undefined; + floatToUnsignedScalar(f32, &.{0.5}, u8, &f32_to_u8); + try expectEqual(@as(u8, 191), f32_to_u8[0]); +} + +pub fn floatToUnsignedSIMD( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + const vec_size = std.simd.suggestVectorSize(SrcType).?; + const VecSrc = @Vector(vec_size, SrcType); + const VecDst = @Vector(vec_size, DestType); + const half: VecSrc = @splat(maxInt(DestType) / 2); + const half_plus_one: VecSrc = @splat(maxInt(DestType) / 2 + 1); + const vec_blocks_len = src.len - (src.len % vec_size); + var i: usize = 0; + while (i < vec_blocks_len) : (i += vec_size) { + const src_vec: VecSrc = src[i..][0..vec_size].*; + dst[i..][0..vec_size].* = @as(VecDst, @intFromFloat(src_vec * half + half_plus_one)); + } + if (i != src.len) floatToUnsignedScalar(SrcType, src[i..], DestType, dst[i..]); +} + +test floatToUnsignedSIMD { + var f32_to_u8: [32 + 7]u8 = undefined; + const items = [1]f32{0.5} ** (32 + 7); + floatToUnsignedSIMD(f32, &items, u8, &f32_to_u8); + try expectEqual(@as(u8, 191), f32_to_u8[0]); + try expectEqual(f32_to_u8[0], f32_to_u8[f32_to_u8.len - 1]); +} + +pub fn floatToSigned( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + if (std.simd.suggestVectorSize(SrcType)) |_| { + floatToSignedSIMD(SrcType, src, DestType, dst); + } else { + floatToSignedScalar(SrcType, src, DestType, dst); + } +} + +pub fn floatToSignedScalar( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + for (src, dst) |*src_sample, *dst_sample| { + const max = maxInt(DestType) + 1; + dst_sample.* = @truncate(@as(i32, @intFromFloat(src_sample.* * max))); + } +} + +test floatToSignedScalar { + var f32_to_i16: [1]i16 = undefined; + var f32_to_i24: [1]i24 = undefined; + var f32_to_i32: [1]i32 = undefined; + + floatToSignedScalar(f32, &.{0.5}, i16, &f32_to_i16); + floatToSignedScalar(f32, &.{0.5}, i24, &f32_to_i24); + floatToSignedScalar(f32, &.{0.5}, i32, &f32_to_i32); + + try expectEqual(@as(i16, 16384), f32_to_i16[0]); + try expectEqual(@as(i24, 4194304), f32_to_i24[0]); + try expectEqual(@as(i32, 1073741824), f32_to_i32[0]); +} + +pub fn floatToSignedSIMD( + comptime SrcType: type, + src: []const SrcType, + comptime DestType: type, + dst: []DestType, +) void { + const vec_size = std.simd.suggestVectorSize(SrcType).?; + const VecSrc = @Vector(vec_size, SrcType); + const VecDst = @Vector(vec_size, DestType); + const max: VecSrc = @splat(maxInt(DestType) + 1); + const vec_blocks_len = src.len - (src.len % vec_size); + var i: usize = 0; + while (i < vec_blocks_len) : (i += vec_size) { + const src_vec: VecSrc = src[i..][0..vec_size].*; + dst[i..][0..vec_size].* = @as(VecDst, @intFromFloat(src_vec * max)); + } + if (i != src.len) floatToSignedScalar(SrcType, src[i..], DestType, dst[i..]); +} + +test floatToSignedSIMD { + var f32_to_i16: [32 + 7]i16 = undefined; + const items = [1]f32{0.5} ** (32 + 7); + floatToSignedSIMD(f32, &items, i16, &f32_to_i16); + try expectEqual(@as(i16, 16384), f32_to_i16[0]); + try expectEqual(f32_to_i16[0], f32_to_i16[f32_to_i16.len - 1]); +} diff --git a/src/sysaudio/coreaudio.zig b/src/sysaudio/coreaudio.zig new file mode 100644 index 00000000..5fa0fb2f --- /dev/null +++ b/src/sysaudio/coreaudio.zig @@ -0,0 +1,769 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); +const c = @cImport({ + @cInclude("CoreAudio/CoreAudio.h"); + @cInclude("AudioUnit/AudioUnit.h"); +}); +const avaudio = @import("objc").avf_audio.avaudio; + +const native_endian = builtin.cpu.arch.endian(); +var is_darling = false; + +const default_sample_rate = 44_100; // Hz + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + _ = options; + + if (std.fs.accessAbsolute("/usr/lib/darling", .{})) { + is_darling = true; + } else |_| {} + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + }; + + return .{ .coreaudio = ctx }; + } + + pub fn deinit(ctx: *Context) void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + var prop_address = c.AudioObjectPropertyAddress{ + .mSelector = c.kAudioHardwarePropertyDevices, + .mScope = c.kAudioObjectPropertyScopeGlobal, + .mElement = c.kAudioObjectPropertyElementMain, + }; + + var io_size: u32 = 0; + if (c.AudioObjectGetPropertyDataSize( + c.kAudioObjectSystemObject, + &prop_address, + 0, + null, + &io_size, + ) != c.noErr) { + return error.OpeningDevice; + } + + const devices_count = io_size / @sizeOf(c.AudioObjectID); + if (devices_count == 0) return; + + const devs = try ctx.allocator.alloc(c.AudioObjectID, devices_count); + defer ctx.allocator.free(devs); + if (c.AudioObjectGetPropertyData( + c.kAudioObjectSystemObject, + &prop_address, + 0, + null, + &io_size, + @as(*anyopaque, @ptrCast(devs)), + ) != c.noErr) { + return error.OpeningDevice; + } + + var default_input_id: c.AudioObjectID = undefined; + var default_output_id: c.AudioObjectID = undefined; + + io_size = @sizeOf(c.AudioObjectID); + if (c.AudioHardwareGetProperty( + c.kAudioHardwarePropertyDefaultInputDevice, + &io_size, + &default_input_id, + ) != c.noErr) { + return error.OpeningDevice; + } + + io_size = @sizeOf(c.AudioObjectID); + if (c.AudioHardwareGetProperty( + c.kAudioHardwarePropertyDefaultOutputDevice, + &io_size, + &default_output_id, + ) != c.noErr) { + return error.OpeningDevice; + } + + for (devs) |id| { + const buf_list = try ctx.allocator.create(c.AudioBufferList); + defer ctx.allocator.destroy(buf_list); + + for (std.meta.tags(main.Device.Mode)) |mode| { + io_size = 0; + prop_address.mSelector = c.kAudioDevicePropertyStreamConfiguration; + prop_address.mScope = switch (mode) { + .playback => c.kAudioObjectPropertyScopeOutput, + .capture => c.kAudioObjectPropertyScopeInput, + }; + if (c.AudioObjectGetPropertyDataSize( + id, + &prop_address, + 0, + null, + &io_size, + ) != c.noErr) { + continue; + } + + if (c.AudioObjectGetPropertyData( + id, + &prop_address, + 0, + null, + &io_size, + buf_list, + ) != c.noErr) { + return error.OpeningDevice; + } + + if (buf_list.mBuffers[0].mNumberChannels == 0) break; + + const audio_buffer_list_property_address = c.AudioObjectPropertyAddress{ + .mSelector = c.kAudioDevicePropertyStreamConfiguration, + .mScope = switch (mode) { + .playback => c.kAudioDevicePropertyScopeOutput, + .capture => c.kAudioDevicePropertyScopeInput, + }, + .mElement = c.kAudioObjectPropertyElementMain, + }; + var output_audio_buffer_list: c.AudioBufferList = undefined; + var audio_buffer_list_size: c_uint = undefined; + + if (c.AudioObjectGetPropertyDataSize( + id, + &audio_buffer_list_property_address, + 0, + null, + &audio_buffer_list_size, + ) != c.noErr) { + return error.OpeningDevice; + } + + if (c.AudioObjectGetPropertyData( + id, + &prop_address, + 0, + null, + &audio_buffer_list_size, + &output_audio_buffer_list, + ) != c.noErr) { + return error.OpeningDevice; + } + + var output_channel_count: usize = 0; + for (0..output_audio_buffer_list.mNumberBuffers) |mBufferIndex| { + output_channel_count += output_audio_buffer_list.mBuffers[mBufferIndex].mNumberChannels; + } + + const channels = try ctx.allocator.alloc(main.ChannelPosition, output_channel_count); + + prop_address.mSelector = c.kAudioDevicePropertyNominalSampleRate; + io_size = @sizeOf(f64); + var sample_rate: f64 = undefined; + if (c.AudioObjectGetPropertyData( + id, + &prop_address, + 0, + null, + &io_size, + &sample_rate, + ) != c.noErr) { + return error.OpeningDevice; + } + + io_size = @sizeOf([*]const u8); + if (c.AudioDeviceGetPropertyInfo( + id, + 0, + 0, + c.kAudioDevicePropertyDeviceName, + &io_size, + null, + ) != c.noErr) { + return error.OpeningDevice; + } + + const name = try ctx.allocator.allocSentinel(u8, io_size, 0); + errdefer ctx.allocator.free(name); + if (c.AudioDeviceGetProperty( + id, + 0, + 0, + c.kAudioDevicePropertyDeviceName, + &io_size, + name.ptr, + ) != c.noErr) { + return error.OpeningDevice; + } + const id_str = try std.fmt.allocPrintZ(ctx.allocator, "{d}", .{id}); + errdefer ctx.allocator.free(id_str); + + const dev = main.Device{ + .id = id_str, + .name = name, + .mode = mode, + .channels = channels, + .formats = &.{ .i16, .i32, .f32 }, + .sample_rate = .{ + .min = @as(u24, @intFromFloat(@floor(sample_rate))), + .max = @as(u24, @intFromFloat(@floor(sample_rate))), + }, + }; + + try ctx.devices_info.list.append(ctx.allocator, dev); + if (id == default_output_id and mode == .playback) { + ctx.devices_info.default_output = ctx.devices_info.list.items.len - 1; + } + + if (id == default_input_id and mode == .capture) { + ctx.devices_info.default_input = ctx.devices_info.list.items.len - 1; + } + } + } + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + const player = try ctx.allocator.create(Player); + errdefer ctx.allocator.destroy(player); + + // obtain an AudioOutputUnit using an AUHAL component description + var component_desc = c.AudioComponentDescription{ + .componentType = c.kAudioUnitType_Output, + .componentSubType = c.kAudioUnitSubType_HALOutput, + .componentManufacturer = c.kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0, + }; + const component = c.AudioComponentFindNext(null, &component_desc); + if (component == null) return error.OpeningDevice; + + // instantiate the audio unit + var audio_unit: c.AudioComponentInstance = undefined; + if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice; + + // Initialize the AUHAL before making any changes or using it. Note that an AUHAL may need + // to be initialized twice, e.g. before and after making changes to it, as an AUHAL needs to + // be initialized *before* anything is done to it. + if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice; + errdefer _ = c.AudioUnitUninitialize(audio_unit); + + const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch return error.OpeningDevice; + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioOutputUnitProperty_CurrentDevice, + c.kAudioUnitScope_Input, + 0, + &device_id, + @sizeOf(c.AudioDeviceID), + ) != c.noErr) { + return error.OpeningDevice; + } + + const stream_desc = try createStreamDesc(options.format, options.sample_rate orelse default_sample_rate, device.channels.len); + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioUnitProperty_StreamFormat, + c.kAudioUnitScope_Input, + 0, + &stream_desc, + @sizeOf(c.AudioStreamBasicDescription), + ) != c.noErr) { + return error.OpeningDevice; + } + + const render_callback = c.AURenderCallbackStruct{ + .inputProc = Player.renderCallback, + .inputProcRefCon = player, + }; + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioUnitProperty_SetRenderCallback, + c.kAudioUnitScope_Input, + 0, + &render_callback, + @sizeOf(c.AURenderCallbackStruct), + ) != c.noErr) { + return error.OpeningDevice; + } + + player.* = .{ + .allocator = ctx.allocator, + .audio_unit = audio_unit.?, + .is_paused = false, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = options.format, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .coreaudio = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + // Request permission to record via requestRecordPermission. If permission was previously + // granted, it will immediately return. Otherwise the function will block, the OS will display + // an " wants to access the Microphone. [Allow] [Deny]" menu, then the callback will be + // invoked and requestRecordPermission will return. + const audio_session = avaudio.AVAudioSession.sharedInstance(); + const PermissionContext = void; + const perm_ctx: PermissionContext = {}; + audio_session.requestRecordPermission(perm_ctx, (struct { + pub fn callback(perm_ctx_2: PermissionContext, permission_granted: bool) void { + _ = permission_granted; + _ = perm_ctx_2; + // Note: in the event permission was NOT granted by the user, we could capture that here + // and surface it as an error. + // + // However, in this situation the OS will simply replace all audio samples with zero-value + // (silence) ones - so there's no harm for us in doing nothing in that case: the user would + // find out we're recording only silence, and they would need to correct it in their System + // Preferences by granting the app permission to record. + } + }).callback); + + const recorder = try ctx.allocator.create(Recorder); + errdefer ctx.allocator.destroy(recorder); + + const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch return error.OpeningDevice; + var io_size: u32 = 0; + var prop_address = c.AudioObjectPropertyAddress{ + .mSelector = c.kAudioDevicePropertyStreamConfiguration, + .mScope = c.kAudioObjectPropertyScopeInput, + .mElement = c.kAudioObjectPropertyElementMain, + }; + + if (c.AudioObjectGetPropertyDataSize( + device_id, + &prop_address, + 0, + null, + &io_size, + ) != c.noErr) { + return error.OpeningDevice; + } + + std.debug.assert(io_size == @sizeOf(c.AudioBufferList)); + const buf_list = try ctx.allocator.create(c.AudioBufferList); + errdefer ctx.allocator.destroy(buf_list); + + if (c.AudioObjectGetPropertyData( + device_id, + &prop_address, + 0, + null, + &io_size, + @as(*anyopaque, @ptrCast(buf_list)), + ) != c.noErr) { + return error.OpeningDevice; + } + + // obtain an AudioOutputUnit using an AUHAL component description + var component_desc = c.AudioComponentDescription{ + .componentType = c.kAudioUnitType_Output, + .componentSubType = c.kAudioUnitSubType_HALOutput, + .componentManufacturer = c.kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0, + }; + const component = c.AudioComponentFindNext(null, &component_desc); + if (component == null) return error.OpeningDevice; + + // instantiate the audio unit + var audio_unit: c.AudioComponentInstance = undefined; + if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice; + + // Initialize the AUHAL before making any changes or using it. Note that an AUHAL may need + // to be initialized twice, e.g. before and after making changes to it, as an AUHAL needs to + // be initialized *before* anything is done to it. + if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice; + errdefer _ = c.AudioUnitUninitialize(audio_unit); + + // To obtain the device input, we must enable IO on the input scope of the Audio Unit. Input + // must be explicitly enabled with kAudioOutputUnitProperty_EnableIO on Element 1 of the + // AUHAL, and this *must be done before* setting the AUHAL's current device. We must also + // disable IO on the output scope of the AUHAL, since it can be used for both. + + // Enable AUHAL input + const enable_io: u32 = 1; + const au_element_input: u32 = 1; + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioOutputUnitProperty_EnableIO, + c.kAudioUnitScope_Input, + au_element_input, + &enable_io, + @sizeOf(u32), + ) != c.noErr) { + return error.OpeningDevice; + } + + // Disable AUHAL output + const disable_io: u32 = 0; + const au_element_output: u32 = 0; + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioOutputUnitProperty_EnableIO, + c.kAudioUnitScope_Output, + au_element_output, + &disable_io, + @sizeOf(u32), + ) != c.noErr) { + return error.OpeningDevice; + } + + // Set the audio device to be Audio Unit's current device. A device can only be associated + // with an AUHAL after enabling IO. + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioOutputUnitProperty_CurrentDevice, + c.kAudioUnitScope_Global, + au_element_output, + &device_id, + @sizeOf(c.AudioDeviceID), + ) != c.noErr) { + return error.OpeningDevice; + } + + // Register the capture callback for the AUHAL; this will be called when the AUHAL has + // received new data from the input device. + const capture_callback = c.AURenderCallbackStruct{ + .inputProc = Recorder.captureCallback, + .inputProcRefCon = recorder, + }; + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioOutputUnitProperty_SetInputCallback, + c.kAudioUnitScope_Global, + au_element_output, + &capture_callback, + @sizeOf(c.AURenderCallbackStruct), + ) != c.noErr) { + return error.OpeningDevice; + } + + // Set the desired output format. + const sample_rate = blk: { + if (options.sample_rate) |rate| { + if (rate < device.sample_rate.min or rate > device.sample_rate.max) return error.OpeningDevice; + break :blk rate; + } + break :blk device.sample_rate.max; + }; + const stream_desc = try createStreamDesc(options.format, sample_rate, device.channels.len); + if (c.AudioUnitSetProperty( + audio_unit, + c.kAudioUnitProperty_StreamFormat, + c.kAudioUnitScope_Output, + au_element_input, + &stream_desc, + @sizeOf(c.AudioStreamBasicDescription), + ) != c.noErr) { + return error.OpeningDevice; + } + + // Now that we are done with modifying the AUHAL, initialize it once more to ensure that it + // is ready to use. + if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice; + errdefer _ = c.AudioUnitUninitialize(audio_unit); + + recorder.* = .{ + .allocator = ctx.allocator, + .audio_unit = audio_unit.?, + .is_paused = false, + .vol = 1.0, + .buf_list = buf_list, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = options.format, + .sample_rate = sample_rate, + }; + return .{ .coreaudio = recorder }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + audio_unit: c.AudioUnit, + is_paused: bool, + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn renderCallback( + player_opaque: ?*anyopaque, + action_flags: [*c]c.AudioUnitRenderActionFlags, + time_stamp: [*c]const c.AudioTimeStamp, + bus_number: u32, + frames_left: u32, + buf: [*c]c.AudioBufferList, + ) callconv(.C) c.OSStatus { + _ = action_flags; + _ = time_stamp; + _ = bus_number; + _ = frames_left; + + const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + + const frames = buf.*.mBuffers[0].mDataByteSize; + player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.mBuffers[0].mData.?))[0..frames]); + + return c.noErr; + } + + pub fn deinit(player: *Player) void { + _ = c.AudioOutputUnitStop(player.audio_unit); + _ = c.AudioUnitUninitialize(player.audio_unit); + _ = c.AudioComponentInstanceDispose(player.audio_unit); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + try player.play(); + } + + pub fn play(player: *Player) !void { + if (c.AudioOutputUnitStart(player.audio_unit) != c.noErr) { + return error.CannotPlay; + } + player.is_paused = false; + } + + pub fn pause(player: *Player) !void { + if (c.AudioOutputUnitStop(player.audio_unit) != c.noErr) { + return error.CannotPause; + } + player.is_paused = true; + } + + pub fn paused(player: *Player) bool { + return player.is_paused; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + if (c.AudioUnitSetParameter( + player.audio_unit, + c.kHALOutputParam_Volume, + c.kAudioUnitScope_Global, + 0, + vol, + 0, + ) != c.noErr) { + if (is_darling) return; + return error.CannotSetVolume; + } + } + + pub fn volume(player: *Player) !f32 { + var vol: f32 = 0; + if (c.AudioUnitGetParameter( + player.audio_unit, + c.kHALOutputParam_Volume, + c.kAudioUnitScope_Global, + 0, + &vol, + ) != c.noErr) { + if (is_darling) return 1; + return error.CannotGetVolume; + } + return vol; + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + audio_unit: c.AudioUnit, + is_paused: bool, + vol: f32, + buf_list: *c.AudioBufferList, + m_data: ?[]u8 = null, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn captureCallback( + recorder_opaque: ?*anyopaque, + action_flags: [*c]c.AudioUnitRenderActionFlags, + time_stamp: [*c]const c.AudioTimeStamp, + bus_number: u32, + num_frames: u32, + buffer_list: [*c]c.AudioBufferList, + ) callconv(.C) c.OSStatus { + _ = buffer_list; + + const recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?))); + + // We want interleaved multi-channel audio, when multiple channels are available-so we'll + // only use a single buffer. If we wanted non-interleaved audio we would use multiple + // buffers. + var m_buffer = &recorder.buf_list.*.mBuffers[0]; + + // Ensure our buffer matches the size needed for the render operation. Note that the buffer + // may grow (in the case of multi-channel audio during the first render callback) or shrink + // in e.g. the event of the device being unplugged and the default input device switching. + const new_len = num_frames * recorder.format.frameSize(@intCast(recorder.channels.len)); + if (recorder.m_data == null or recorder.m_data.?.len != new_len) { + if (recorder.m_data) |old| recorder.allocator.free(old); + recorder.m_data = recorder.allocator.alloc(u8, new_len) catch return c.noErr; + } + recorder.buf_list.*.mNumberBuffers = 1; + m_buffer.mData = recorder.m_data.?.ptr; + m_buffer.mDataByteSize = @intCast(recorder.m_data.?.len); + m_buffer.mNumberChannels = @intCast(recorder.channels.len); + + const err_no = c.AudioUnitRender( + recorder.audio_unit, + action_flags, + time_stamp, + bus_number, + num_frames, + recorder.buf_list, + ); + if (err_no != c.noErr) { + // TODO: err_no here is rather helpful, we should indicate what it is back to the user + // in this event probably? + return c.noErr; + } + + if (recorder.buf_list.*.mNumberBuffers == 1) { + recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(recorder.buf_list.*.mBuffers[0].mData.?))[0..new_len]); + } else { + @panic("TODO: convert planar to interleaved"); + // for (recorder.channels, 0..) |*ch, i| { + // ch.ptr = @as([*]u8, @ptrCast(recorder.buf_list.*.mBuffers[i].mData.?)); + // } + } + + return c.noErr; + } + + pub fn deinit(recorder: *Recorder) void { + _ = c.AudioOutputUnitStop(recorder.audio_unit); + _ = c.AudioUnitUninitialize(recorder.audio_unit); + _ = c.AudioComponentInstanceDispose(recorder.audio_unit); + recorder.allocator.destroy(recorder.buf_list); + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + try recorder.record(); + } + + pub fn record(recorder: *Recorder) !void { + if (c.AudioOutputUnitStart(recorder.audio_unit) != c.noErr) { + return error.CannotRecord; + } + recorder.is_paused = false; + } + + pub fn pause(recorder: *Recorder) !void { + if (c.AudioOutputUnitStop(recorder.audio_unit) != c.noErr) { + return error.CannotPause; + } + recorder.is_paused = true; + } + + pub fn paused(recorder: *Recorder) bool { + return recorder.is_paused; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + if (c.AudioUnitSetParameter( + recorder.audio_unit, + c.kHALOutputParam_Volume, + c.kAudioUnitScope_Global, + 0, + vol, + 0, + ) != c.noErr) { + if (is_darling) return; + return error.CannotSetVolume; + } + } + + pub fn volume(recorder: *Recorder) !f32 { + var vol: f32 = 0; + if (c.AudioUnitGetParameter( + recorder.audio_unit, + c.kHALOutputParam_Volume, + c.kAudioUnitScope_Global, + 0, + &vol, + ) != c.noErr) { + if (is_darling) return 1; + return error.CannotGetVolume; + } + return vol; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.id); + allocator.free(device.name); + allocator.free(device.channels); +} + +fn createStreamDesc(format: main.Format, sample_rate: u24, ch_count: usize) !c.AudioStreamBasicDescription { + var desc = c.AudioStreamBasicDescription{ + .mSampleRate = @as(f64, @floatFromInt(sample_rate)), + .mFormatID = c.kAudioFormatLinearPCM, + .mFormatFlags = switch (format) { + .i16 => c.kAudioFormatFlagIsSignedInteger, + .i24 => c.kAudioFormatFlagIsSignedInteger, + .i32 => c.kAudioFormatFlagIsSignedInteger, + .f32 => c.kAudioFormatFlagIsFloat, + .u8 => return error.IncompatibleDevice, + }, + .mBytesPerPacket = format.frameSize(@intCast(ch_count)), + .mFramesPerPacket = 1, + .mBytesPerFrame = format.frameSize(@intCast(ch_count)), + .mChannelsPerFrame = @intCast(ch_count), + .mBitsPerChannel = switch (format) { + .i16 => 16, + .i24 => 24, + .i32 => 32, + .f32 => 32, + .u8 => return error.IncompatibleDevice, + }, + .mReserved = 0, + }; + + if (native_endian == .big) { + desc.mFormatFlags |= c.kAudioFormatFlagIsBigEndian; + } + + return desc; +} diff --git a/src/sysaudio/dummy.zig b/src/sysaudio/dummy.zig new file mode 100644 index 00000000..9b71bf45 --- /dev/null +++ b/src/sysaudio/dummy.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +const default_sample_rate = 44_100; // 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 = main.min_sample_rate, + .max = main.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 = main.min_sample_rate, + .max = main.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.Context { + _ = options; + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + }; + + return .{ .dummy = ctx }; + } + + pub fn deinit(ctx: *Context) void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + try ctx.devices_info.list.append(ctx.allocator, dummy_playback); + try ctx.devices_info.list.append(ctx.allocator, dummy_capture); + + ctx.devices_info.setDefault(.playback, 0); + ctx.devices_info.setDefault(.capture, 1); + + ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 1); + ctx.devices_info.list.items[1].channels = try ctx.allocator.alloc(main.ChannelPosition, 1); + + ctx.devices_info.list.items[0].channels[0] = .front_center; + ctx.devices_info.list.items[1].channels[0] = .front_center; + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + _ = writeFn; + const player = try ctx.allocator.create(Player); + player.* = .{ + .allocator = ctx.allocator, + .is_paused = false, + .vol = 1.0, + .channels = device.channels, + .format = options.format, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .dummy = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + _ = readFn; + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .is_paused = false, + .vol = 1.0, + .channels = device.channels, + .format = options.format, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .dummy = recorder }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + is_paused: bool, + vol: f32, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(player: *Player) void { + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + _ = player; + } + + pub fn play(player: *Player) !void { + player.is_paused = false; + } + + pub fn pause(player: *Player) !void { + player.is_paused = true; + } + + pub fn paused(player: *Player) bool { + return player.is_paused; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + player.vol = vol; + } + + pub fn volume(player: *Player) !f32 { + return player.vol; + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + is_paused: bool, + vol: f32, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(recorder: *Recorder) void { + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + _ = recorder; + } + + pub fn record(recorder: *Recorder) !void { + recorder.is_paused = false; + } + + pub fn pause(recorder: *Recorder) !void { + recorder.is_paused = true; + } + + pub fn paused(recorder: *Recorder) bool { + return recorder.is_paused; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + recorder.vol = vol; + } + + pub fn volume(recorder: *Recorder) !f32 { + return recorder.vol; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +} diff --git a/src/sysaudio/examples/record.zig b/src/sysaudio/examples/record.zig new file mode 100644 index 00000000..e3a3b50a --- /dev/null +++ b/src/sysaudio/examples/record.zig @@ -0,0 +1,92 @@ +//! Redirects input device into zig-out/raw_audio file. + +const std = @import("std"); +const sysaudio = @import("mach").sysaudio; + +var recorder: sysaudio.Recorder = undefined; +var file: std.fs.File = undefined; + +// Note: this Info.plist file gets embedded into the final binary __TEXT,__info_plist +// linker section. On macOS this means that NSMicrophoneUsageDescription is set. Without +// that being set, the application would be denied access to the microphone (the prompt +// for microphone access would not even appear.) +// +// The linker is just a convenient way to specify this without building a .app bundle with +// a separate Info.plist file. +export var __info_plist: [663:0]u8 linksection("__TEXT,__info_plist") = + ( + \\ + \\ + \\ + \\ + \\ CFBundleDevelopmentRegion + \\ English + \\ CFBundleIdentifier + \\ com.my.app + \\ CFBundleInfoDictionaryVersion + \\ 6.0 + \\ CFBundleName + \\ myapp + \\ CFBundleDisplayName + \\ My App + \\ CFBundleVersion + \\ 1.0.0 + \\ NSMicrophoneUsageDescription + \\ To record audio from your microphone + \\ + \\ +).*; + +pub fn main() !void { + _ = __info_plist; + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + var ctx = try sysaudio.Context.init(null, gpa.allocator(), .{}); + defer ctx.deinit(); + try ctx.refresh(); + + const device = ctx.defaultDevice(.capture) orelse return error.NoDevice; + + recorder = try ctx.createRecorder(device, readCallback, .{}); + defer recorder.deinit(); + try recorder.start(); + + const zig_out = try std.fs.cwd().makeOpenPath("zig-out", .{}); + file = try zig_out.createFile("raw_audio", .{}); + + std.debug.print( + \\Recording to zig-out/raw_audio using: + \\ + \\ device: {s} + \\ channels: {} + \\ sample_rate: {} + \\ + \\You can play this recording back using e.g.: + \\ $ ffplay -f f32le -ar {} -ac {} zig-out/raw_audio + \\ + , .{ + device.name, + device.channels.len, + recorder.sampleRate(), + recorder.sampleRate(), + device.channels.len, + }); + // Note: you may also use e.g.: + // + // ``` + // paplay -p --format=FLOAT32LE --rate 48000 --raw zig-out/raw_audio + // aplay -f FLOAT_LE -r 48000 zig-out/raw_audio + // ``` + + while (true) {} +} + +fn readCallback(_: ?*anyopaque, input: []const u8) void { + const format_size = recorder.format().size(); + const samples = input.len / format_size; + var buffer: [16 * 1024]f32 = undefined; + sysaudio.convertFrom(f32, buffer[0..samples], recorder.format(), input); + _ = file.write(std.mem.sliceAsBytes(buffer[0 .. input.len / format_size])) catch {}; +} diff --git a/src/sysaudio/examples/sine.zig b/src/sysaudio/examples/sine.zig new file mode 100644 index 00000000..cd1d52ef --- /dev/null +++ b/src/sysaudio/examples/sine.zig @@ -0,0 +1,77 @@ +const std = @import("std"); +const sysaudio = @import("mach").sysaudio; + +var player: sysaudio.Player = undefined; + +pub fn main() !void { + var timer = try std.time.Timer.start(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + var ctx = try sysaudio.Context.init(null, gpa.allocator(), .{ .deviceChangeFn = deviceChange }); + std.log.info("Took {} to initialize the context...", .{std.fmt.fmtDuration(timer.lap())}); + defer ctx.deinit(); + try ctx.refresh(); + std.log.info("Took {} to refresh the context...", .{std.fmt.fmtDuration(timer.lap())}); + + const device = ctx.defaultDevice(.playback) orelse return error.NoDevice; + std.log.info("Took {} to get the default playback device...", .{std.fmt.fmtDuration(timer.lap())}); + + player = try ctx.createPlayer(device, writeCallback, .{}); + std.log.info("Took {} to create a player...", .{std.fmt.fmtDuration(timer.lap())}); + defer player.deinit(); + try player.start(); + std.log.info("Took {} to start the player...", .{std.fmt.fmtDuration(timer.lap())}); + + try player.setVolume(0.85); + std.log.info("Took {} to set the volume...", .{std.fmt.fmtDuration(timer.lap())}); + + var buf: [16]u8 = undefined; + std.log.info("player created & entering i/o loop...", .{}); + while (true) { + std.debug.print("( paused = {}, volume = {d} )\n> ", .{ player.paused(), try player.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")) { + const vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace)); + try player.setVolume(vol); + } else if (std.mem.eql(u8, cmd, "pause")) { + try player.pause(); + try std.testing.expect(player.paused()); + } else if (std.mem.eql(u8, cmd, "play")) { + try player.play(); + try std.testing.expect(!player.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(_: ?*anyopaque, output: []u8) void { + const seconds_per_frame = 1.0 / @as(f32, @floatFromInt(player.sampleRate())); + const frame_size = player.format().frameSize(@intCast(player.channels().len)); + const frames = output.len / frame_size; + + var i: usize = 0; + while (i < output.len) : (i += frame_size) { + const frame_index: f32 = @floatFromInt(i / frame_size); + const sample = @sin((seconds_offset + frame_index * seconds_per_frame) * radians_per_second); + sysaudio.convertTo( + f32, + &.{ sample, sample }, + player.format(), + output[i..][0..frame_size], + ); + } + + seconds_offset = @mod(seconds_offset + seconds_per_frame * @as(f32, @floatFromInt(frames)), 1.0); +} + +fn deviceChange(_: ?*anyopaque) void { + std.log.info("device change detected!", .{}); +} diff --git a/src/sysaudio/jack.zig b/src/sysaudio/jack.zig new file mode 100644 index 00000000..4d923a0b --- /dev/null +++ b/src/sysaudio/jack.zig @@ -0,0 +1,404 @@ +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"); + +var lib: Lib = undefined; +const Lib = struct { + handle: std.DynLib, + + jack_free: *const fn (ptr: ?*anyopaque) callconv(.C) void, + jack_set_error_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void, + jack_set_info_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void, + jack_client_open: *const fn ([*c]const u8, c.jack_options_t, [*c]c.jack_status_t, ...) callconv(.C) ?*c.jack_client_t, + jack_client_close: *const fn (?*c.jack_client_t) callconv(.C) c_int, + jack_connect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int, + jack_disconnect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int, + jack_activate: *const fn (?*c.jack_client_t) callconv(.C) c_int, + jack_deactivate: *const fn (?*c.jack_client_t) callconv(.C) c_int, + jack_port_by_name: *const fn (?*c.jack_client_t, [*c]const u8) callconv(.C) ?*c.jack_port_t, + jack_port_register: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong, c_ulong) callconv(.C) ?*c.jack_port_t, + jack_set_sample_rate_callback: *const fn (?*c.jack_client_t, c.JackSampleRateCallback, ?*anyopaque) callconv(.C) c_int, + jack_set_port_registration_callback: *const fn (?*c.jack_client_t, c.JackPortRegistrationCallback, ?*anyopaque) callconv(.C) c_int, + jack_set_process_callback: *const fn (?*c.jack_client_t, c.JackProcessCallback, ?*anyopaque) callconv(.C) c_int, + jack_set_port_rename_callback: *const fn (?*c.jack_client_t, c.JackPortRenameCallback, ?*anyopaque) callconv(.C) c_int, + jack_get_sample_rate: *const fn (?*c.jack_client_t) callconv(.C) c.jack_nframes_t, + jack_get_ports: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong) callconv(.C) [*c][*c]const u8, + jack_port_type: *const fn (port: ?*const c.jack_port_t) callconv(.C) [*c]const u8, + jack_port_flags: *const fn (port: ?*const c.jack_port_t) callconv(.C) c_int, + jack_port_name: *const fn (?*const c.jack_port_t) callconv(.C) [*c]const u8, + jack_port_get_buffer: *const fn (?*c.jack_port_t, c.jack_nframes_t) callconv(.C) ?*anyopaque, + jack_port_connected_to: *const fn (?*const c.jack_port_t, [*c]const u8) callconv(.C) c_int, + jack_port_type_size: *const fn () c_int, + + pub fn load() !void { + lib.handle = std.DynLib.openZ("libjack.so") catch return error.LibraryNotFound; + inline for (@typeInfo(Lib).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + } +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + client: *c.jack_client_t, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.Context.DeviceChangeFn, + user_data: ?*anyopaque, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + try Lib.load(); + + lib.jack_set_error_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing))); + lib.jack_set_info_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing))); + + var status: c.jack_status_t = 0; + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .client = lib.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, + .user_data = options.user_data, + } else null, + }; + + if (options.deviceChangeFn) |_| { + if (lib.jack_set_sample_rate_callback(ctx.client, sampleRateCallback, ctx) != 0 or + lib.jack_set_port_registration_callback(ctx.client, portRegistrationCallback, ctx) != 0 or + lib.jack_set_port_rename_callback(ctx.client, portRenameCalllback, ctx) != 0) + return error.ConnectionRefused; + } + + return .{ .jack = ctx }; + } + + pub fn deinit(ctx: *Context) void { + for (ctx.devices_info.list.items) |device| + freeDevice(ctx.allocator, device); + ctx.devices_info.list.deinit(ctx.allocator); + _ = lib.jack_client_close(ctx.client); + ctx.allocator.destroy(ctx); + lib.handle.close(); + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + const sample_rate = @as(u24, @intCast(lib.jack_get_sample_rate(ctx.client))); + + const port_names = lib.jack_get_ports(ctx.client, null, null, 0) orelse + return error.OutOfMemory; + defer lib.jack_free(@as(?*anyopaque, @ptrCast(port_names))); + + var i: usize = 0; + outer: while (port_names[i] != null) : (i += 1) { + const port = lib.jack_port_by_name(ctx.client, port_names[i]) orelse break; + const port_type = lib.jack_port_type(port)[0..@as(usize, @intCast(lib.jack_port_type_size()))]; + if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE)) + continue; + + const flags = lib.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 (ctx.devices_info.list.items) |*dev| { + if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) { + const new_ch: main.ChannelPosition = @enumFromInt(dev.channels.len); + dev.channels = try ctx.allocator.realloc(dev.channels, dev.channels.len + 1); + dev.channels[dev.channels.len - 1] = new_ch; + break :outer; + } + } + + const device = main.Device{ + .id = try ctx.allocator.dupeZ(u8, id), + .name = name, + .mode = mode, + .channels = blk: { + var channels = try ctx.allocator.alloc(main.ChannelPosition, 1); + channels[0] = .front_center; + break :blk channels; + }, + .formats = &.{.f32}, + .sample_rate = .{ + .min = sample_rate, + .max = sample_rate, + }, + }; + + try ctx.devices_info.list.append(ctx.allocator, device); + if (ctx.devices_info.default(mode) == null) { + ctx.devices_info.setDefault(mode, ctx.devices_info.list.items.len - 1); + } + } + } + + fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int { + var ctx = @as(*Context, @ptrCast(@alignCast(arg.?))); + ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data); + return 0; + } + + fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(arg.?))); + ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data); + } + + fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(arg.?))); + ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data); + } + + pub fn devices(ctx: *Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: *Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + var ports = try ctx.allocator.alloc(*c.jack_port_t, device.channels.len); + var dest_ports = try ctx.allocator.alloc([:0]const u8, ports.len); + var buf: [64]u8 = undefined; + for (device.channels, 0..) |_, i| { + const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable; + const dest_name = try std.fmt.allocPrintZ(ctx.allocator, "{s}:{s}", .{ device.id, port_name }); + ports[i] = lib.jack_port_register(ctx.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse + return error.OpeningDevice; + dest_ports[i] = dest_name; + } + + const player = try ctx.allocator.create(Player); + player.* = .{ + .allocator = ctx.allocator, + .client = ctx.client, + .ports = ports, + .dest_ports = dest_ports, + .device = device, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + }; + return .{ .jack = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + var ports = try ctx.allocator.alloc(*c.jack_port_t, device.channels.len); + var dest_ports = try ctx.allocator.alloc([:0]const u8, ports.len); + var buf: [64]u8 = undefined; + for (device.channels, 0..) |_, i| { + const port_name = std.fmt.bufPrintZ(&buf, "capture_{d}", .{i + 1}) catch unreachable; + const dest_name = try std.fmt.allocPrintZ(ctx.allocator, "{s}:{s}", .{ device.id, port_name }); + ports[i] = lib.jack_port_register(ctx.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsInput, 0) orelse + return error.OpeningDevice; + dest_ports[i] = dest_name; + } + + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .client = ctx.client, + .ports = ports, + .dest_ports = dest_ports, + .device = device, + .vol = 1.0, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + }; + return .{ .jack = recorder }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + client: *c.jack_client_t, + ports: []const *c.jack_port_t, + dest_ports: []const [:0]const u8, + device: main.Device, + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + + pub fn deinit(player: *Player) void { + player.allocator.free(player.ports); + for (player.dest_ports) |d| + player.allocator.free(d); + player.allocator.free(player.dest_ports); + _ = lib.jack_deactivate(player.client); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + if (lib.jack_set_process_callback(player.client, processCallback, player) != 0) + return error.CannotPlay; + + if (lib.jack_activate(player.client) != 0) + return error.CannotPlay; + + for (player.ports, 0..) |port, i| { + if (lib.jack_connect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0) + return error.CannotPlay; + } + } + + fn processCallback(n_frames: c.jack_nframes_t, player_opaque: ?*anyopaque) callconv(.C) c_int { + const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + + if (true) @panic("TODO: convert planar to interleaved"); + // for (player.channels, 0..) |*ch, i| { + // ch.*.ptr = @as([*]u8, @ptrCast(lib.jack_port_get_buffer(player.ports[i], n_frames))); + // } + player.writeFn(player.user_data, undefined, n_frames); + + return 0; + } + + pub fn play(player: *Player) !void { + for (player.ports, 0..) |port, i| { + if (lib.jack_connect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0) + return error.CannotPlay; + } + } + + pub fn pause(player: *Player) !void { + for (player.ports, 0..) |port, i| { + if (lib.jack_disconnect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0) + return error.CannotPause; + } + } + + pub fn paused(player: *Player) bool { + for (player.ports, 0..) |port, i| { + if (lib.jack_port_connected_to(port, player.dest_ports[i].ptr) == 1) + return false; + } + return true; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + player.vol = vol; + } + + pub fn volume(player: *Player) !f32 { + return player.vol; + } + + pub fn sampleRate(player: *Player) u24 { + return @as(u24, @intCast(lib.jack_get_sample_rate(player.client))); + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + client: *c.jack_client_t, + ports: []const *c.jack_port_t, + dest_ports: []const [:0]const u8, + device: main.Device, + vol: f32, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + + pub fn deinit(recorder: *Recorder) void { + recorder.allocator.free(recorder.ports); + for (recorder.dest_ports) |d| + recorder.allocator.free(d); + recorder.allocator.free(recorder.dest_ports); + _ = lib.jack_deactivate(recorder.client); + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + if (lib.jack_set_process_callback(recorder.client, processCallback, recorder) != 0) + return error.CannotRecord; + + if (lib.jack_activate(recorder.client) != 0) + return error.CannotRecord; + + for (recorder.ports, 0..) |port, i| { + if (lib.jack_connect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0) + return error.CannotRecord; + } + } + + fn processCallback(n_frames: c.jack_nframes_t, recorder_opaque: ?*anyopaque) callconv(.C) c_int { + const recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?))); + + if (true) @panic("TODO: convert planar to interleaved"); + // for (recorder.channels, 0..) |*ch, i| { + // ch.*.ptr = @as([*]u8, @ptrCast(lib.jack_port_get_buffer(recorder.ports[i], n_frames))); + // } + recorder.readFn(recorder.user_data, n_frames); + + return 0; + } + + pub fn record(recorder: *Recorder) !void { + for (recorder.ports, 0..) |port, i| { + if (lib.jack_connect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0) + return error.CannotRecord; + } + } + + pub fn pause(recorder: *Recorder) !void { + for (recorder.ports, 0..) |port, i| { + if (lib.jack_disconnect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0) + return error.CannotPause; + } + } + + pub fn paused(recorder: *Recorder) bool { + for (recorder.ports, 0..) |port, i| { + if (lib.jack_port_connected_to(port, recorder.dest_ports[i].ptr) == 1) + return false; + } + return true; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + recorder.vol = vol; + } + + pub fn volume(recorder: *Recorder) !f32 { + return recorder.vol; + } + + pub fn sampleRate(recorder: *Recorder) u24 { + return @as(u24, @intCast(lib.jack_get_sample_rate(recorder.client))); + } +}; + +pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.id); + allocator.free(device.channels); +} diff --git a/src/sysaudio/main.zig b/src/sysaudio/main.zig new file mode 100644 index 00000000..330ffc62 --- /dev/null +++ b/src/sysaudio/main.zig @@ -0,0 +1,496 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const util = @import("util.zig"); +const backends = @import("backends.zig"); +const conv = @import("conv.zig"); + +pub const Backend = backends.Backend; +pub const Range = util.Range; + +pub const default_latency = 500 * std.time.us_per_ms; // μs +pub const min_sample_rate = 8_000; // Hz +pub const max_sample_rate = 5_644_800; // Hz + +pub const Context = struct { + pub const DeviceChangeFn = *const fn (userdata: ?*anyopaque) void; + pub const Options = struct { + app_name: [:0]const u8 = "Mach Game", + deviceChangeFn: ?DeviceChangeFn = null, + user_data: ?*anyopaque = null, + }; + + data: backends.Context, + + pub const InitError = error{ + OutOfMemory, + AccessDenied, + LibraryNotFound, + SymbolLookup, + SystemResources, + ConnectionRefused, + }; + + pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) InitError!Context { + const data: backends.Context = blk: { + if (backend) |b| { + break :blk try @typeInfo( + std.meta.fieldInfo(backends.Context, b).type, + ).Pointer.child.init(allocator, options); + } else { + inline for (std.meta.fields(Backend), 0..) |b, i| { + if (@typeInfo( + std.meta.fieldInfo(backends.Context, @as(Backend, @enumFromInt(b.value))).type, + ).Pointer.child.init(allocator, options)) |d| { + break :blk d; + } else |err| { + if (i == std.meta.fields(Backend).len - 1) + return err; + } + } + unreachable; + } + }; + + return .{ .data = data }; + } + + pub inline fn deinit(ctx: Context) void { + switch (ctx.data) { + inline else => |b| b.deinit(), + } + } + + pub const RefreshError = error{ + OutOfMemory, + SystemResources, + OpeningDevice, + }; + + pub inline fn refresh(ctx: Context) RefreshError!void { + return switch (ctx.data) { + inline else => |b| b.refresh(), + }; + } + + pub inline fn devices(ctx: Context) []const Device { + return switch (ctx.data) { + inline else => |b| b.devices(), + }; + } + + pub inline fn defaultDevice(ctx: Context, mode: Device.Mode) ?Device { + return switch (ctx.data) { + inline else => |b| b.defaultDevice(mode), + }; + } + + pub const CreateStreamError = error{ + OutOfMemory, + SystemResources, + OpeningDevice, + IncompatibleDevice, + }; + + pub inline fn createPlayer(ctx: Context, device: Device, writeFn: WriteFn, options: StreamOptions) CreateStreamError!Player { + std.debug.assert(device.mode == .playback); + + return .{ + .data = switch (ctx.data) { + inline else => |b| try b.createPlayer(device, writeFn, options), + }, + }; + } + + pub inline fn createRecorder(ctx: Context, device: Device, readFn: ReadFn, options: StreamOptions) CreateStreamError!Recorder { + std.debug.assert(device.mode == .capture); + + return .{ + .data = switch (ctx.data) { + inline else => |b| try b.createRecorder(device, readFn, options), + }, + }; + } +}; + +pub const StreamOptions = struct { + format: Format = .f32, + sample_rate: ?u24 = null, + media_role: MediaRole = .default, + user_data: ?*anyopaque = null, +}; + +pub const MediaRole = enum { + default, + game, + music, + movie, + communication, +}; + +// TODO: `*Player` instead `*anyopaque` +// https://github.com/ziglang/zig/issues/12325 +pub const WriteFn = *const fn (user_data: ?*anyopaque, output: []u8) void; +// TODO: `*Recorder` instead `*anyopaque` +pub const ReadFn = *const fn (user_data: ?*anyopaque, input: []const u8) void; + +pub const Player = struct { + data: backends.Player, + + pub inline fn deinit(player: *Player) void { + return switch (player.data) { + inline else => |b| b.deinit(), + }; + } + + pub const StartError = error{ + CannotPlay, + OutOfMemory, + SystemResources, + }; + + pub inline fn start(player: *Player) StartError!void { + return switch (player.data) { + inline else => |b| b.start(), + }; + } + + pub const PlayError = error{ + CannotPlay, + OutOfMemory, + }; + + pub inline fn play(player: *Player) PlayError!void { + return switch (player.data) { + inline else => |b| b.play(), + }; + } + + pub const PauseError = error{ + CannotPause, + OutOfMemory, + }; + + pub inline fn pause(player: *Player) PauseError!void { + return switch (player.data) { + inline else => |b| b.pause(), + }; + } + + pub inline fn paused(player: *Player) bool { + return switch (player.data) { + inline else => |b| b.paused(), + }; + } + + pub const SetVolumeError = error{ + CannotSetVolume, + }; + + // confidence interval (±) depends on the device + pub inline fn setVolume(player: *Player, vol: f32) SetVolumeError!void { + std.debug.assert(vol <= 1.0); + return switch (player.data) { + inline else => |b| b.setVolume(vol), + }; + } + + pub const GetVolumeError = error{ + CannotGetVolume, + }; + + // confidence interval (±) depends on the device + pub inline fn volume(player: *Player) GetVolumeError!f32 { + return switch (player.data) { + inline else => |b| b.volume(), + }; + } + + pub inline fn sampleRate(player: *Player) u24 { + return if (@hasField(Backend, "jack")) switch (player.data) { + .jack => |b| b.sampleRate(), + inline else => |b| b.sample_rate, + } else switch (player.data) { + inline else => |b| b.sample_rate, + }; + } + + pub inline fn channels(player: *Player) []ChannelPosition { + return switch (player.data) { + inline else => |b| b.channels, + }; + } + + pub inline fn format(player: *Player) Format { + return switch (player.data) { + inline else => |b| b.format, + }; + } +}; + +pub const Recorder = struct { + data: backends.Recorder, + + pub inline fn deinit(recorder: *Recorder) void { + return switch (recorder.data) { + inline else => |b| b.deinit(), + }; + } + + pub const StartError = error{ + CannotRecord, + OutOfMemory, + SystemResources, + }; + + pub inline fn start(recorder: *Recorder) StartError!void { + return switch (recorder.data) { + inline else => |b| b.start(), + }; + } + + pub const RecordError = error{ + CannotRecord, + OutOfMemory, + }; + + pub inline fn record(recorder: *Recorder) RecordError!void { + return switch (recorder.data) { + inline else => |b| b.record(), + }; + } + + pub const PauseError = error{ + CannotPause, + OutOfMemory, + }; + + pub inline fn pause(recorder: *Recorder) PauseError!void { + return switch (recorder.data) { + inline else => |b| b.pause(), + }; + } + + pub inline fn paused(recorder: *Recorder) bool { + return switch (recorder.data) { + inline else => |b| b.paused(), + }; + } + + pub const SetVolumeError = error{ + CannotSetVolume, + }; + + // confidence interval (±) depends on the device + pub inline fn setVolume(recorder: *Recorder, vol: f32) SetVolumeError!void { + std.debug.assert(vol <= 1.0); + return switch (recorder.data) { + inline else => |b| b.setVolume(vol), + }; + } + + pub const GetVolumeError = error{ + CannotGetVolume, + }; + + // confidence interval (±) depends on the device + pub inline fn volume(recorder: *Recorder) GetVolumeError!f32 { + return switch (recorder.data) { + inline else => |b| b.volume(), + }; + } + + pub inline fn sampleRate(recorder: *Recorder) u24 { + return if (@hasField(Backend, "jack")) switch (recorder.data) { + .jack => |b| b.sampleRate(), + inline else => |b| b.sample_rate, + } else switch (recorder.data) { + inline else => |b| b.sample_rate, + }; + } + + pub inline fn channels(recorder: *Recorder) []ChannelPosition { + return switch (recorder.data) { + inline else => |b| b.channels, + }; + } + + pub inline fn format(recorder: *Recorder) Format { + return switch (recorder.data) { + inline else => |b| b.format, + }; + } +}; + +pub fn convertTo(comptime SrcType: type, src: []const SrcType, dst_format: Format, dst: []u8) void { + const dst_len = dst.len / dst_format.size(); + std.debug.assert(dst_len == src.len); + + return switch (dst_format) { + .u8 => switch (SrcType) { + u8 => @memcpy(@as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len], src), + i8, i16, i24, i32 => conv.signedToUnsigned(SrcType, src, u8, @as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len]), + f32 => conv.floatToUnsigned(SrcType, src, u8, @as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len]), + else => unreachable, + }, + .i16 => switch (SrcType) { + i16 => @memcpy(@as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len], src), + u8 => conv.unsignedToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]), + i8, i24, i32 => conv.signedToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]), + f32 => conv.floatToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]), + else => unreachable, + }, + .i24 => switch (SrcType) { + i24 => @memcpy(@as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len], src), + u8 => conv.unsignedToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]), + i8, i16, i32 => conv.signedToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]), + f32 => conv.floatToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]), + else => unreachable, + }, + .i32 => switch (SrcType) { + i32 => @memcpy(@as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len], src), + u8 => conv.unsignedToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]), + i8, i16, i24 => conv.signedToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]), + f32 => conv.floatToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]), + else => unreachable, + }, + .f32 => switch (SrcType) { + f32 => @memcpy(@as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len], src), + u8 => conv.unsignedToFloat(SrcType, src, f32, @as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len]), + i8, i16, i24, i32 => conv.signedToFloat(SrcType, src, f32, @as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len]), + else => unreachable, + }, + }; +} + +pub fn convertFrom(comptime DestType: type, dst: []DestType, src_format: Format, src: []const u8) void { + const src_len = src.len / src_format.size(); + std.debug.assert(src_len == dst.len); + + return switch (src_format) { + .u8 => switch (DestType) { + u8 => @memcpy(dst, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len]), + i8, i16, i24, i32 => conv.unsignedToSigned(u8, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + f32 => conv.unsignedToFloat(u8, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + else => unreachable, + }, + .i16 => switch (DestType) { + i16 => @memcpy(dst, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len]), + u8 => conv.signedToUnsigned(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + i8, i24, i32 => conv.signedToSigned(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + f32 => conv.signedToFloat(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + else => unreachable, + }, + .i24 => switch (DestType) { + i24 => @memcpy(dst, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len]), + u8 => conv.signedToUnsigned(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + i8, i16, i32 => conv.signedToSigned(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + f32 => conv.signedToFloat(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + else => unreachable, + }, + .i32 => switch (DestType) { + i32 => @memcpy(dst, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len]), + u8 => conv.signedToUnsigned(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + i8, i16, i24 => conv.signedToSigned(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + f32 => conv.signedToFloat(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + else => unreachable, + }, + .f32 => switch (DestType) { + f32 => @memcpy(dst, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len]), + u8 => conv.floatToUnsigned(f32, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + i8, i16, i24, i32 => conv.floatToSigned(f32, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst), + else => unreachable, + }, + }; +} + +pub const Device = struct { + id: [:0]const u8, + name: [:0]const u8, + mode: Mode, + channels: []ChannelPosition, + formats: []const Format, + sample_rate: util.Range(u24), + + pub const Mode = enum { + playback, + capture, + }; + + pub fn preferredFormat(device: Device, format: ?Format) Format { + if (format) |f| { + for (device.formats) |fmt| if (f == fmt) return fmt; + } + + var best: Format = device.formats[0]; + for (device.formats) |fmt| { + if (@intFromEnum(fmt) > @intFromEnum(best)) best = fmt; + } + return best; + } +}; + +pub const ChannelPosition = enum { + front_center, + front_left, + front_right, + front_left_center, + front_right_center, + back_center, + back_left, + back_right, + 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(u3) { + u8 = 0, + i16 = 1, + i24 = 2, + i32 = 3, + f32 = 4, + + pub inline fn size(format: Format) u8 { + return switch (format) { + .u8 => 1, + .i16 => 2, + .i24 => 3, + .i32, .f32 => 4, + }; + } + + pub inline fn validSize(format: Format) u8 { + return switch (format) { + .u8 => 1, + .i16 => 2, + .i24 => 3, + .i32, .f32 => 4, + }; + } + + pub inline fn sizeBits(format: Format) u8 { + return format.size() * 8; + } + + pub inline fn validSizeBits(format: Format) u8 { + return format.validSize() * 8; + } + + pub inline fn frameSize(format: Format, channels: u8) u8 { + return format.size() * channels; + } +}; + +test "reference declarations" { + _ = conv; + _ = backends.Context; + _ = backends.Player; + _ = backends.Recorder; +} diff --git a/src/sysaudio/pipewire.zig b/src/sysaudio/pipewire.zig new file mode 100644 index 00000000..2d6dd5e3 --- /dev/null +++ b/src/sysaudio/pipewire.zig @@ -0,0 +1,527 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("pipewire/pipewire.h"); + @cInclude("spa/param/audio/format-utils.h"); +}); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +const default_sample_rate = 44_100; // Hz + +var lib: Lib = undefined; +const Lib = struct { + handle: std.DynLib, + + pw_init: *const fn ([*c]c_int, [*c][*c][*c]u8) callconv(.C) void, + pw_deinit: *const fn () callconv(.C) void, + pw_thread_loop_new: *const fn ([*c]const u8, [*c]const c.spa_dict) callconv(.C) ?*c.pw_thread_loop, + pw_thread_loop_destroy: *const fn (?*c.pw_thread_loop) callconv(.C) void, + pw_thread_loop_start: *const fn (?*c.pw_thread_loop) callconv(.C) c_int, + pw_thread_loop_stop: *const fn (?*c.pw_thread_loop) callconv(.C) void, + pw_thread_loop_signal: *const fn (?*c.pw_thread_loop, bool) callconv(.C) void, + pw_thread_loop_wait: *const fn (?*c.pw_thread_loop) callconv(.C) void, + pw_thread_loop_lock: *const fn (?*c.pw_thread_loop) callconv(.C) void, + pw_thread_loop_unlock: *const fn (?*c.pw_thread_loop) callconv(.C) void, + pw_thread_loop_get_loop: *const fn (?*c.pw_thread_loop) callconv(.C) [*c]c.pw_loop, + pw_properties_new: *const fn ([*c]const u8, ...) callconv(.C) [*c]c.pw_properties, + pw_stream_new_simple: *const fn ([*c]c.pw_loop, [*c]const u8, [*c]c.pw_properties, [*c]const c.pw_stream_events, ?*anyopaque) callconv(.C) ?*c.pw_stream, + pw_stream_destroy: *const fn (?*c.pw_stream) callconv(.C) void, + pw_stream_connect: *const fn (?*c.pw_stream, c.spa_direction, u32, c.pw_stream_flags, [*c][*c]const c.spa_pod, u32) callconv(.C) c_int, + pw_stream_queue_buffer: *const fn (?*c.pw_stream, [*c]c.pw_buffer) callconv(.C) c_int, + pw_stream_dequeue_buffer: *const fn (?*c.pw_stream) callconv(.C) [*c]c.pw_buffer, + pw_stream_get_state: *const fn (?*c.pw_stream, [*c][*c]const u8) callconv(.C) c.pw_stream_state, + + pub fn load() !void { + lib.handle = std.DynLib.openZ("libpipewire-0.3.so") catch return error.LibraryNotFound; + inline for (@typeInfo(Lib).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + } +}; + +const default_playback = main.Device{ + .id = "default-playback", + .name = "Default Device", + .mode = .playback, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = main.min_sample_rate, + .max = main.max_sample_rate, + }, +}; + +const default_capture = main.Device{ + .id = "default-capture", + .name = "Default Device", + .mode = .capture, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = main.min_sample_rate, + .max = main.max_sample_rate, + }, +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + app_name: [:0]const u8, + // watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.Context.DeviceChangeFn, + user_data: ?*anyopaque, + thread: *c.pw_thread_loop, + aborted: std.atomic.Value(bool), + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + try Lib.load(); + + lib.pw_init(null, null); + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .app_name = options.app_name, + // TODO: device change watcher + // .watcher = blk: { + // if (options.deviceChangeFn != null) { + // const thread = c.pw_thread_loop_new("device-change-watcher", null) orelse return error.SystemResources; + // const context = c.pw_context_new(c.pw_thread_loop_get_loop(thread), null, 0); + // const core = c.pw_context_connect(context, null, 0); + // const registry = c.pw_core_get_registry(core, c.PW_VERSION_REGISTRY, 0); + // _ = c.spa_zero(registry); + + // var registry_listener: c.spa_hook = undefined; + // _ = c.pw_registry_add_listener(registry, registry_listener); + + // break :blk .{ + // .deviceChangeFn = options.deviceChangeFn.?, + // .user_data = options.user_data, + // .thread = thread, + // .aborted = .{ .raw = false }, + // }; + // } else break :blk null; + // }, + }; + + return .{ .pipewire = ctx }; + } + + pub fn deinit(ctx: *Context) void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + lib.pw_deinit(); + ctx.allocator.destroy(ctx); + lib.handle.close(); + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + try ctx.devices_info.list.append(ctx.allocator, default_playback); + try ctx.devices_info.list.append(ctx.allocator, default_capture); + + ctx.devices_info.setDefault(.playback, 0); + ctx.devices_info.setDefault(.capture, 1); + + ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 2); + ctx.devices_info.list.items[1].channels = try ctx.allocator.alloc(main.ChannelPosition, 2); + + ctx.devices_info.list.items[0].channels[0] = .front_right; + ctx.devices_info.list.items[0].channels[1] = .front_left; + ctx.devices_info.list.items[1].channels[0] = .front_right; + ctx.devices_info.list.items[1].channels[1] = .front_left; + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + const media_role = switch (options.media_role) { + .default => "Screen", + .game => "Game", + .music => "Music", + .movie => "Movie", + .communication => "Communication", + }; + + var buf: [8]u8 = undefined; + const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate orelse default_sample_rate}) catch unreachable; + + const props = lib.pw_properties_new( + c.PW_KEY_MEDIA_TYPE, + "Audio", + + c.PW_KEY_MEDIA_CATEGORY, + "Playback", + + c.PW_KEY_MEDIA_ROLE, + media_role.ptr, + + c.PW_KEY_MEDIA_NAME, + ctx.app_name.ptr, + + c.PW_KEY_AUDIO_RATE, + audio_rate.ptr, + + @as(*allowzero u0, @ptrFromInt(0)), + ); + + const stream_events = c.pw_stream_events{ + .version = c.PW_VERSION_STREAM_EVENTS, + .process = Player.processCb, + .destroy = null, + .state_changed = stateChangedCb, + .control_info = null, + .io_changed = null, + .param_changed = null, + .add_buffer = null, + .remove_buffer = null, + .drained = null, + .command = null, + .trigger_done = null, + }; + + const player = try ctx.allocator.create(Player); + errdefer ctx.allocator.destroy(player); + + const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources; + const stream = lib.pw_stream_new_simple( + lib.pw_thread_loop_get_loop(thread), + "audio-src", + props, + &stream_events, + player, + ) orelse return error.OpeningDevice; + + var builder_buf: [256]u8 = undefined; + var pod_builder = c.spa_pod_builder{ + .data = &builder_buf, + .size = builder_buf.len, + ._padding = 0, + .state = .{ + .offset = 0, + .flags = 0, + .frame = null, + }, + .callbacks = .{ .funcs = null, .data = null }, + }; + var info = c.spa_audio_info_raw{ + .format = c.SPA_AUDIO_FORMAT_F32, + .channels = @as(u32, @intCast(device.channels.len)), + .rate = options.sample_rate orelse default_sample_rate, + .flags = 0, + .position = undefined, + }; + var params = [1][*c]c.spa_pod{ + sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info), + }; + + if (lib.pw_stream_connect( + stream, + c.PW_DIRECTION_OUTPUT, + c.PW_ID_ANY, + c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS, + ¶ms, + params.len, + ) < 0) return error.OpeningDevice; + + player.* = .{ + .allocator = ctx.allocator, + .thread = thread, + .stream = stream, + .is_paused = .{ .raw = false }, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .pipewire = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + const media_role = switch (options.media_role) { + .default => "Screen", + .game => "Game", + .music => "Music", + .movie => "Movie", + .communication => "Communication", + }; + + var buf: [8]u8 = undefined; + const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate orelse default_sample_rate}) catch unreachable; + + const props = lib.pw_properties_new( + c.PW_KEY_MEDIA_TYPE, + "Audio", + + c.PW_KEY_MEDIA_CATEGORY, + "Capture", + + c.PW_KEY_MEDIA_ROLE, + media_role.ptr, + + c.PW_KEY_MEDIA_NAME, + ctx.app_name.ptr, + + c.PW_KEY_AUDIO_RATE, + audio_rate.ptr, + + @as(*allowzero u0, @ptrFromInt(0)), + ); + + const stream_events = c.pw_stream_events{ + .version = c.PW_VERSION_STREAM_EVENTS, + .process = Recorder.processCb, + .destroy = null, + .state_changed = stateChangedCb, + .control_info = null, + .io_changed = null, + .param_changed = null, + .add_buffer = null, + .remove_buffer = null, + .drained = null, + .command = null, + .trigger_done = null, + }; + + const recorder = try ctx.allocator.create(Recorder); + errdefer ctx.allocator.destroy(recorder); + + const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources; + const stream = lib.pw_stream_new_simple( + lib.pw_thread_loop_get_loop(thread), + "audio-capture", + props, + &stream_events, + recorder, + ) orelse return error.OpeningDevice; + + var builder_buf: [256]u8 = undefined; + var pod_builder = c.spa_pod_builder{ + .data = &builder_buf, + .size = builder_buf.len, + ._padding = 0, + .state = .{ + .offset = 0, + .flags = 0, + .frame = null, + }, + .callbacks = .{ .funcs = null, .data = null }, + }; + var info = c.spa_audio_info_raw{ + .format = c.SPA_AUDIO_FORMAT_F32, + .channels = @as(u32, @intCast(device.channels.len)), + .rate = options.sample_rate orelse default_sample_rate, + .flags = 0, + .position = undefined, + }; + var params = [1][*c]c.spa_pod{ + sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info), + }; + + if (lib.pw_stream_connect( + stream, + c.PW_DIRECTION_INPUT, + c.PW_ID_ANY, + c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS, + ¶ms, + params.len, + ) < 0) return error.OpeningDevice; + + recorder.* = .{ + .allocator = ctx.allocator, + .thread = thread, + .stream = stream, + .is_paused = .{ .raw = false }, + .vol = 1.0, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .pipewire = recorder }; + } +}; + +fn stateChangedCb(player_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void { + _ = old_state; + _ = err; + + const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + + if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) { + lib.pw_thread_loop_signal(player.thread, false); + } +} + +pub const Player = struct { + allocator: std.mem.Allocator, + thread: *c.pw_thread_loop, + stream: *c.pw_stream, + is_paused: std.atomic.Value(bool), + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn processCb(player_opaque: ?*anyopaque) callconv(.C) void { + var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + + const buf = lib.pw_stream_dequeue_buffer(player.stream) orelse return; + if (buf.*.buffer.*.datas[0].data == null) return; + defer _ = lib.pw_stream_queue_buffer(player.stream, buf); + + buf.*.buffer.*.datas[0].chunk.*.offset = 0; + if (player.is_paused.load(.Unordered)) { + buf.*.buffer.*.datas[0].chunk.*.stride = 0; + buf.*.buffer.*.datas[0].chunk.*.size = 0; + return; + } + + const stride = player.format.frameSize(@intCast(player.channels.len)); + const frames = @min(buf.*.requested * stride, buf.*.buffer.*.datas[0].maxsize); + buf.*.buffer.*.datas[0].chunk.*.stride = stride; + buf.*.buffer.*.datas[0].chunk.*.size = @intCast(frames); + + player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]); + } + + pub fn deinit(player: *Player) void { + lib.pw_thread_loop_stop(player.thread); + lib.pw_thread_loop_destroy(player.thread); + lib.pw_stream_destroy(player.stream); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + if (lib.pw_thread_loop_start(player.thread) < 0) return error.SystemResources; + + lib.pw_thread_loop_lock(player.thread); + lib.pw_thread_loop_wait(player.thread); + lib.pw_thread_loop_unlock(player.thread); + + if (lib.pw_stream_get_state(player.stream, null) == c.PW_STREAM_STATE_ERROR) { + return error.CannotPlay; + } + } + + pub fn play(player: *Player) !void { + player.is_paused.store(false, .Unordered); + } + + pub fn pause(player: *Player) !void { + player.is_paused.store(true, .Unordered); + } + + pub fn paused(player: *Player) bool { + return player.is_paused.load(.Unordered); + } + + pub fn setVolume(player: *Player, vol: f32) !void { + player.vol = vol; + } + + pub fn volume(player: *Player) !f32 { + return player.vol; + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + thread: *c.pw_thread_loop, + stream: *c.pw_stream, + is_paused: std.atomic.Value(bool), + vol: f32, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn processCb(recorder_opaque: ?*anyopaque) callconv(.C) void { + var recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?))); + + const buf = lib.pw_stream_dequeue_buffer(recorder.stream) orelse return; + if (buf.*.buffer.*.datas[0].data == null) return; + defer _ = lib.pw_stream_queue_buffer(recorder.stream, buf); + + buf.*.buffer.*.datas[0].chunk.*.offset = 0; + if (recorder.is_paused.load(.Unordered)) { + buf.*.buffer.*.datas[0].chunk.*.stride = 0; + buf.*.buffer.*.datas[0].chunk.*.size = 0; + return; + } + + const frames = buf.*.buffer.*.datas[0].chunk.*.size; + recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]); + } + + pub fn deinit(recorder: *Recorder) void { + lib.pw_thread_loop_stop(recorder.thread); + lib.pw_thread_loop_destroy(recorder.thread); + lib.pw_stream_destroy(recorder.stream); + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + if (lib.pw_thread_loop_start(recorder.thread) < 0) return error.SystemResources; + + lib.pw_thread_loop_lock(recorder.thread); + lib.pw_thread_loop_wait(recorder.thread); + lib.pw_thread_loop_unlock(recorder.thread); + + if (lib.pw_stream_get_state(recorder.stream, null) == c.PW_STREAM_STATE_ERROR) { + return error.CannotRecord; + } + } + + pub fn record(recorder: *Recorder) !void { + recorder.is_paused.store(false, .Unordered); + } + + pub fn pause(recorder: *Recorder) !void { + recorder.is_paused.store(true, .Unordered); + } + + pub fn paused(recorder: *Recorder) bool { + return recorder.is_paused.load(.Unordered); + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + recorder.vol = vol; + } + + pub fn volume(recorder: *Recorder) !f32 { + return recorder.vol; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +} + +extern fn sysaudio_spa_format_audio_raw_build(builder: [*c]c.spa_pod_builder, id: u32, info: [*c]c.spa_audio_info_raw) callconv(.C) [*c]c.spa_pod; diff --git a/src/sysaudio/pipewire/sysaudio.c b/src/sysaudio/pipewire/sysaudio.c new file mode 100644 index 00000000..b940ac77 --- /dev/null +++ b/src/sysaudio/pipewire/sysaudio.c @@ -0,0 +1,11 @@ +#include +#include + +struct spa_pod *sysaudio_spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info) +{ + return spa_format_audio_raw_build(builder, id, info); +} + +void sysaudio_pw_registry_add_listener(struct pw_registry *reg, struct spa_hook *reg_listener, struct pw_registry_events *events) { + pw_registry_add_listener(reg, reg_listener, events, NULL); +} \ No newline at end of file diff --git a/src/sysaudio/pulseaudio.zig b/src/sysaudio/pulseaudio.zig new file mode 100644 index 00000000..58ffdb06 --- /dev/null +++ b/src/sysaudio/pulseaudio.zig @@ -0,0 +1,811 @@ +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; + +const default_sample_rate = 44_100; // Hz + +var lib: Lib = undefined; +const Lib = struct { + handle: std.DynLib, + + pa_threaded_mainloop_new: *const fn () callconv(.C) ?*c.pa_threaded_mainloop, + pa_threaded_mainloop_free: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void, + pa_threaded_mainloop_start: *const fn (?*c.pa_threaded_mainloop) callconv(.C) c_int, + pa_threaded_mainloop_stop: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void, + pa_threaded_mainloop_signal: *const fn (?*c.pa_threaded_mainloop, c_int) callconv(.C) void, + pa_threaded_mainloop_wait: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void, + pa_threaded_mainloop_lock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void, + pa_threaded_mainloop_unlock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void, + pa_threaded_mainloop_get_api: *const fn (?*c.pa_threaded_mainloop) callconv(.C) [*c]c.pa_mainloop_api, + pa_operation_unref: *const fn (?*c.pa_operation) callconv(.C) void, + pa_operation_get_state: *const fn (?*const c.pa_operation) callconv(.C) c.pa_operation_state_t, + pa_context_new_with_proplist: *const fn ([*c]c.pa_mainloop_api, [*c]const u8, ?*const c.pa_proplist) callconv(.C) ?*c.pa_context, + pa_context_unref: *const fn (?*c.pa_context) callconv(.C) void, + pa_context_connect: *const fn (?*c.pa_context, [*c]const u8, c.pa_context_flags_t, [*c]const c.pa_spawn_api) callconv(.C) c_int, + pa_context_disconnect: *const fn (?*c.pa_context) callconv(.C) void, + pa_context_subscribe: *const fn (?*c.pa_context, c.pa_subscription_mask_t, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_context_get_state: *const fn (?*const c.pa_context) callconv(.C) c.pa_context_state_t, + pa_context_set_state_callback: *const fn (?*c.pa_context, c.pa_context_notify_cb_t, ?*anyopaque) callconv(.C) void, + pa_context_set_subscribe_callback: *const fn (?*c.pa_context, c.pa_context_subscribe_cb_t, ?*anyopaque) callconv(.C) void, + pa_context_get_sink_input_info: *const fn (?*c.pa_context, u32, c.pa_sink_input_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_context_get_sink_info_list: *const fn (?*c.pa_context, c.pa_sink_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_context_set_sink_input_volume: *const fn (?*c.pa_context, u32, [*c]const c.pa_cvolume, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_context_get_source_info_list: *const fn (?*c.pa_context, c.pa_source_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_context_get_server_info: *const fn (?*c.pa_context, c.pa_server_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_stream_new: *const fn (?*c.pa_context, [*c]const u8, [*c]const c.pa_sample_spec, [*c]const c.pa_channel_map) callconv(.C) ?*c.pa_stream, + pa_stream_unref: *const fn (?*c.pa_stream) callconv(.C) void, + pa_stream_connect_playback: *const fn (?*c.pa_stream, [*c]const u8, [*c]const c.pa_buffer_attr, c.pa_stream_flags_t, [*c]const c.pa_cvolume, ?*c.pa_stream) callconv(.C) c_int, + pa_stream_connect_record: *const fn (?*c.pa_stream, [*c]const u8, [*c]const c.pa_buffer_attr, c.pa_stream_flags_t) callconv(.C) c_int, + pa_stream_disconnect: *const fn (?*c.pa_stream) callconv(.C) c_int, + pa_stream_cork: *const fn (?*c.pa_stream, c_int, c.pa_stream_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation, + pa_stream_is_corked: *const fn (?*const c.pa_stream) callconv(.C) c_int, + pa_stream_begin_write: *const fn (?*c.pa_stream, [*c]?*anyopaque, [*c]usize) callconv(.C) c_int, + pa_stream_peek: *const fn (?*c.pa_stream, [*c]?*anyopaque, [*c]usize) callconv(.C) c_int, + pa_stream_drop: *const fn (?*c.pa_stream) callconv(.C) c_int, + pa_stream_write: *const fn (?*c.pa_stream, ?*const anyopaque, usize, c.pa_free_cb_t, i64, c.pa_seek_mode_t) callconv(.C) c_int, + pa_stream_get_state: *const fn (?*const c.pa_stream) callconv(.C) c.pa_stream_state_t, + pa_stream_get_index: *const fn (?*const c.pa_stream) callconv(.C) u32, + pa_stream_set_state_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void, + pa_stream_set_read_callback: *const fn (?*c.pa_stream, c.pa_stream_request_cb_t, ?*anyopaque) callconv(.C) void, + pa_stream_set_write_callback: *const fn (?*c.pa_stream, c.pa_stream_request_cb_t, ?*anyopaque) callconv(.C) void, + pa_stream_set_underflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void, + pa_stream_set_overflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void, + pa_cvolume_init: *const fn ([*c]c.pa_cvolume) callconv(.C) [*c]c.pa_cvolume, + pa_cvolume_set: *const fn ([*c]c.pa_cvolume, c_uint, c.pa_volume_t) callconv(.C) [*c]c.pa_cvolume, + pa_sw_volume_from_linear: *const fn (f64) callconv(.C) c.pa_volume_t, + + pub fn load() !void { + lib.handle = std.DynLib.openZ("libpulse.so") catch return error.LibraryNotFound; + inline for (@typeInfo(Lib).Struct.fields[1..]) |field| { + const name = std.fmt.comptimePrint("{s}\x00", .{field.name}); + const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]); + @field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup; + } + } +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + app_name: [:0]const u8, + main_loop: *c.pa_threaded_mainloop, + pulse_ctx: *c.pa_context, + pulse_ctx_state: c.pa_context_state_t, + default_sink: ?[:0]const u8, + default_source: ?[:0]const u8, + watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.Context.DeviceChangeFn, + user_data: ?*anyopaque, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + try Lib.load(); + + const main_loop = lib.pa_threaded_mainloop_new() orelse + return error.OutOfMemory; + errdefer lib.pa_threaded_mainloop_free(main_loop); + const main_loop_api = lib.pa_threaded_mainloop_get_api(main_loop); + + const pulse_ctx = lib.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse + return error.OutOfMemory; + errdefer lib.pa_context_unref(pulse_ctx); + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = Context{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .app_name = options.app_name, + .main_loop = main_loop, + .pulse_ctx = pulse_ctx, + .pulse_ctx_state = c.PA_CONTEXT_UNCONNECTED, + .default_sink = null, + .default_source = null, + .watcher = if (options.deviceChangeFn) |dcf| .{ + .deviceChangeFn = dcf, + .user_data = options.user_data, + } else null, + }; + + if (lib.pa_context_connect(pulse_ctx, null, 0, null) != 0) + return error.ConnectionRefused; + errdefer lib.pa_context_disconnect(pulse_ctx); + lib.pa_context_set_state_callback(pulse_ctx, contextStateOp, ctx); + + if (lib.pa_threaded_mainloop_start(main_loop) != 0) + return error.SystemResources; + errdefer lib.pa_threaded_mainloop_stop(main_loop); + + lib.pa_threaded_mainloop_lock(main_loop); + defer lib.pa_threaded_mainloop_unlock(main_loop); + + while (true) { + switch (ctx.pulse_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, + => lib.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) { + lib.pa_context_set_subscribe_callback(pulse_ctx, subscribeOp, ctx); + const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE; + const subscribe_op = lib.pa_context_subscribe(pulse_ctx, events, null, ctx) orelse + return error.OutOfMemory; + lib.pa_operation_unref(subscribe_op); + } + + return .{ .pulseaudio = ctx }; + } + + fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, ctx_opaque: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(ctx_opaque.?))); + ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data); + } + + fn contextStateOp(pulse_ctx: ?*c.pa_context, ctx_opaque: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(ctx_opaque.?))); + + ctx.pulse_ctx_state = lib.pa_context_get_state(pulse_ctx); + lib.pa_threaded_mainloop_signal(ctx.main_loop, 0); + } + + pub fn deinit(ctx: *Context) void { + lib.pa_context_set_subscribe_callback(ctx.pulse_ctx, null, null); + lib.pa_context_set_state_callback(ctx.pulse_ctx, null, null); + lib.pa_context_disconnect(ctx.pulse_ctx); + lib.pa_context_unref(ctx.pulse_ctx); + lib.pa_threaded_mainloop_stop(ctx.main_loop); + lib.pa_threaded_mainloop_free(ctx.main_loop); + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + lib.handle.close(); + } + + pub fn refresh(ctx: *Context) !void { + lib.pa_threaded_mainloop_lock(ctx.main_loop); + defer lib.pa_threaded_mainloop_unlock(ctx.main_loop); + + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(); + + const list_sink_op = lib.pa_context_get_sink_info_list(ctx.pulse_ctx, sinkInfoOp, ctx); + const list_source_op = lib.pa_context_get_source_info_list(ctx.pulse_ctx, sourceInfoOp, ctx); + const server_info_op = lib.pa_context_get_server_info(ctx.pulse_ctx, serverInfoOp, ctx); + + performOperation(ctx.main_loop, list_sink_op); + performOperation(ctx.main_loop, list_source_op); + performOperation(ctx.main_loop, server_info_op); + + defer { + if (ctx.default_sink) |d| + ctx.allocator.free(d); + if (ctx.default_source) |d| + ctx.allocator.free(d); + } + for (ctx.devices_info.list.items, 0..) |device, i| { + if ((device.mode == .playback and + ctx.default_sink != null and + std.mem.eql(u8, device.id, ctx.default_sink.?)) or + // + (device.mode == .capture and + ctx.default_source != null and + std.mem.eql(u8, device.id, ctx.default_source.?))) + { + ctx.devices_info.setDefault(device.mode, i); + break; + } + } + } + + fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, user_data: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?))); + + defer lib.pa_threaded_mainloop_signal(ctx.main_loop, 0); + ctx.default_sink = ctx.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return; + ctx.default_source = ctx.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch { + ctx.allocator.free(ctx.default_sink.?); + return; + }; + } + + fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?))); + if (eol != 0) { + lib.pa_threaded_mainloop_signal(ctx.main_loop, 0); + return; + } + + ctx.deviceInfoOp(info, .playback) catch return; + } + + fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void { + var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?))); + if (eol != 0) { + lib.pa_threaded_mainloop_signal(ctx.main_loop, 0); + return; + } + + ctx.deviceInfoOp(info, .capture) catch return; + } + + fn deviceInfoOp(ctx: *Context, info: anytype, mode: main.Device.Mode) !void { + const id = try ctx.allocator.dupeZ(u8, std.mem.span(info.*.name)); + errdefer ctx.allocator.free(id); + const name = try ctx.allocator.dupeZ(u8, std.mem.span(info.*.description)); + errdefer ctx.allocator.free(name); + + const device = main.Device{ + .mode = mode, + .channels = blk: { + const channels = try ctx.allocator.alloc(main.ChannelPosition, info.*.channel_map.channels); + for (channels, 0..) |*ch, i| ch.* = try fromPAChannelPos(info.*.channel_map.map[i]); + break :blk channels; + }, + .formats = available_formats, + .sample_rate = .{ + .min = @as(u24, @intCast(info.*.sample_spec.rate)), + .max = @as(u24, @intCast(info.*.sample_spec.rate)), + }, + .id = id, + .name = name, + }; + + try ctx.devices_info.list.append(ctx.allocator, device); + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + lib.pa_threaded_mainloop_lock(ctx.main_loop); + defer lib.pa_threaded_mainloop_unlock(ctx.main_loop); + + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate); + + const sample_spec = c.pa_sample_spec{ + .format = toPAFormat(format), + .rate = sample_rate, + .channels = @as(u5, @intCast(device.channels.len)), + }; + + const channel_map = try toPAChannelMap(device.channels); + + const stream = lib.pa_stream_new(ctx.pulse_ctx, ctx.app_name.ptr, &sample_spec, &channel_map); + if (stream == null) + return error.OutOfMemory; + errdefer lib.pa_stream_unref(stream); + + var status: StreamStatus = .{ .main_loop = ctx.main_loop, .status = .unknown }; + lib.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 (lib.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) { + return error.OpeningDevice; + } + errdefer _ = lib.pa_stream_disconnect(stream); + + while (true) { + switch (status.status) { + .unknown => lib.pa_threaded_mainloop_wait(ctx.main_loop), + .ready => break, + .failure => return error.OpeningDevice, + } + } + + const player = try ctx.allocator.create(Player); + player.* = .{ + .allocator = ctx.allocator, + .main_loop = ctx.main_loop, + .pulse_ctx = ctx.pulse_ctx, + .stream = stream.?, + .write_ptr = undefined, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .pulseaudio = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + lib.pa_threaded_mainloop_lock(ctx.main_loop); + defer lib.pa_threaded_mainloop_unlock(ctx.main_loop); + + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate); + + const sample_spec = c.pa_sample_spec{ + .format = toPAFormat(format), + .rate = sample_rate, + .channels = @as(u5, @intCast(device.channels.len)), + }; + + const channel_map = try toPAChannelMap(device.channels); + + const stream = lib.pa_stream_new(ctx.pulse_ctx, ctx.app_name.ptr, &sample_spec, &channel_map); + if (stream == null) + return error.OutOfMemory; + errdefer lib.pa_stream_unref(stream); + + var status: StreamStatus = .{ .main_loop = ctx.main_loop, .status = .unknown }; + lib.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 (lib.pa_stream_connect_record(stream, device.id.ptr, &buf_attr, flags) != 0) { + return error.OpeningDevice; + } + errdefer _ = lib.pa_stream_disconnect(stream); + + while (true) { + switch (status.status) { + .unknown => lib.pa_threaded_mainloop_wait(ctx.main_loop), + .ready => break, + .failure => return error.OpeningDevice, + } + } + + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .main_loop = ctx.main_loop, + .pulse_ctx = ctx.pulse_ctx, + .stream = stream.?, + .peek_ptr = undefined, + .peek_index = 0, + .vol = 1.0, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .pulseaudio = recorder }; + } + + const StreamStatus = struct { + main_loop: *c.pa_threaded_mainloop, + status: enum(u8) { + unknown, + ready, + failure, + }, + }; + + fn streamStateOp(stream: ?*c.pa_stream, stream_status_opaque: ?*anyopaque) callconv(.C) void { + const stream_status = @as(*StreamStatus, @ptrCast(@alignCast(stream_status_opaque.?))); + switch (lib.pa_stream_get_state(stream)) { + c.PA_STREAM_UNCONNECTED, + c.PA_STREAM_CREATING, + c.PA_STREAM_TERMINATED, + => {}, + c.PA_STREAM_READY => { + stream_status.status = .ready; + lib.pa_threaded_mainloop_signal(stream_status.main_loop, 0); + }, + c.PA_STREAM_FAILED => { + stream_status.status = .failure; + lib.pa_threaded_mainloop_signal(stream_status.main_loop, 0); + }, + else => unreachable, + } + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + main_loop: *c.pa_threaded_mainloop, + pulse_ctx: *c.pa_context, + stream: *c.pa_stream, + write_ptr: [*]u8, + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(player: *Player) void { + lib.pa_threaded_mainloop_lock(player.main_loop); + lib.pa_stream_set_write_callback(player.stream, null, null); + lib.pa_stream_set_state_callback(player.stream, null, null); + lib.pa_stream_set_underflow_callback(player.stream, null, null); + lib.pa_stream_set_overflow_callback(player.stream, null, null); + _ = lib.pa_stream_disconnect(player.stream); + lib.pa_stream_unref(player.stream); + lib.pa_threaded_mainloop_unlock(player.main_loop); + + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + const op = lib.pa_stream_cork(player.stream, 0, null, null) orelse + return error.CannotPlay; + lib.pa_operation_unref(op); + lib.pa_stream_set_write_callback(player.stream, playbackStreamWriteOp, player); + } + + fn playbackStreamWriteOp(stream: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void { + var player = @as(*Player, @ptrCast(@alignCast(user_data.?))); + + var frames_left = nbytes; + if (lib.pa_stream_begin_write( + stream, + @as( + [*c]?*anyopaque, + @ptrCast(@alignCast(&player.write_ptr)), + ), + &frames_left, + ) != 0) return; + + player.writeFn(player.user_data, player.write_ptr[0..frames_left]); + + if (lib.pa_stream_write( + stream, + player.write_ptr, + frames_left, + null, + 0, + c.PA_SEEK_RELATIVE, + ) != 0) return; + } + + pub fn play(player: *Player) !void { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + if (lib.pa_stream_is_corked(player.stream) > 0) { + const op = lib.pa_stream_cork(player.stream, 0, null, null) orelse + return error.CannotPlay; + lib.pa_operation_unref(op); + } + } + + pub fn pause(player: *Player) !void { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + if (lib.pa_stream_is_corked(player.stream) == 0) { + const op = lib.pa_stream_cork(player.stream, 1, null, null) orelse + return error.CannotPause; + lib.pa_operation_unref(op); + } + } + + pub fn paused(player: *Player) bool { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + return lib.pa_stream_is_corked(player.stream) > 0; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + var cvolume: c.pa_cvolume = undefined; + _ = lib.pa_cvolume_init(&cvolume); + _ = lib.pa_cvolume_set(&cvolume, @as(c_uint, @intCast(player.channels.len)), lib.pa_sw_volume_from_linear(vol)); + + performOperation( + player.main_loop, + lib.pa_context_set_sink_input_volume( + player.pulse_ctx, + lib.pa_stream_get_index(player.stream), + &cvolume, + successOp, + player, + ), + ); + } + + pub fn volume(player: *Player) !f32 { + lib.pa_threaded_mainloop_lock(player.main_loop); + defer lib.pa_threaded_mainloop_unlock(player.main_loop); + + performOperation( + player.main_loop, + lib.pa_context_get_sink_input_info( + player.pulse_ctx, + lib.pa_stream_get_index(player.stream), + sinkInputInfoOp, + player, + ), + ); + + return player.vol; + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + main_loop: *c.pa_threaded_mainloop, + pulse_ctx: *c.pa_context, + stream: *c.pa_stream, + peek_ptr: [*]u8, + peek_index: usize, + vol: f32, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(recorder: *Recorder) void { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + lib.pa_stream_set_write_callback(recorder.stream, null, null); + lib.pa_stream_set_state_callback(recorder.stream, null, null); + lib.pa_stream_set_underflow_callback(recorder.stream, null, null); + lib.pa_stream_set_overflow_callback(recorder.stream, null, null); + _ = lib.pa_stream_disconnect(recorder.stream); + lib.pa_stream_unref(recorder.stream); + lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + const op = lib.pa_stream_cork(recorder.stream, 0, null, null) orelse + return error.CannotRecord; + lib.pa_operation_unref(op); + lib.pa_stream_set_read_callback(recorder.stream, playbackStreamReadOp, recorder); + } + + fn playbackStreamReadOp(stream: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void { + var recorder = @as(*Recorder, @ptrCast(@alignCast(user_data.?))); + + var frames_left = nbytes; + var peek_ptr: ?*anyopaque = undefined; + if (lib.pa_stream_peek(stream, &peek_ptr, &frames_left) != 0) { + if (std.debug.runtime_safety) unreachable; + return; + } + + if (peek_ptr) |ptr| { + recorder.peek_ptr = @ptrCast(ptr); + + recorder.readFn(recorder.user_data, (recorder.peek_ptr + recorder.peek_index)[0..frames_left]); + recorder.peek_index += frames_left; + } else { + _ = lib.pa_stream_drop(stream); + } + } + + pub fn record(recorder: *Recorder) !void { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + if (lib.pa_stream_is_corked(recorder.stream) > 0) { + const op = lib.pa_stream_cork(recorder.stream, 0, null, null) orelse + return error.CannotRecord; + lib.pa_operation_unref(op); + } + } + + pub fn pause(recorder: *Recorder) !void { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + if (lib.pa_stream_is_corked(recorder.stream) == 0) { + const op = lib.pa_stream_cork(recorder.stream, 1, null, null) orelse + return error.CannotPause; + lib.pa_operation_unref(op); + } + } + + pub fn paused(recorder: *Recorder) bool { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + return lib.pa_stream_is_corked(recorder.stream) > 0; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + var cvolume: c.pa_cvolume = undefined; + _ = lib.pa_cvolume_init(&cvolume); + _ = lib.pa_cvolume_set(&cvolume, @as(c_uint, @intCast(recorder.channels.len)), lib.pa_sw_volume_from_linear(vol)); + + performOperation( + recorder.main_loop, + lib.pa_context_set_sink_input_volume( + recorder.pulse_ctx, + lib.pa_stream_get_index(recorder.stream), + &cvolume, + successOp, + recorder, + ), + ); + } + + pub fn volume(recorder: *Recorder) !f32 { + lib.pa_threaded_mainloop_lock(recorder.main_loop); + defer lib.pa_threaded_mainloop_unlock(recorder.main_loop); + + performOperation( + recorder.main_loop, + lib.pa_context_get_sink_input_info( + recorder.pulse_ctx, + lib.pa_stream_get_index(recorder.stream), + sinkInputInfoOp, + recorder, + ), + ); + + return recorder.vol; + } +}; + +fn successOp(_: ?*c.pa_context, success: c_int, player_opaque: ?*anyopaque) callconv(.C) void { + const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + if (success == 1) + lib.pa_threaded_mainloop_signal(player.main_loop, 0); +} + +fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, player_opaque: ?*anyopaque) callconv(.C) void { + var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?))); + + if (eol != 0) { + lib.pa_threaded_mainloop_signal(player.main_loop, 0); + return; + } + + player.vol = @as(f32, @floatFromInt(info.*.volume.values[0])) / @as(f32, @floatFromInt(c.PA_VOLUME_NORM)); +} + +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 (lib.pa_operation_get_state(op)) { + c.PA_OPERATION_RUNNING => lib.pa_threaded_mainloop_wait(main_loop), + c.PA_OPERATION_DONE => return lib.pa_operation_unref(op), + c.PA_OPERATION_CANCELLED => return lib.pa_operation_unref(op), + else => unreachable, + } + } +} + +pub const available_formats = &[_]main.Format{ .u8, .i16, .i24, .i32, .f32 }; + +pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.ChannelPosition { + 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_REAR_LEFT => .back_left, + c.PA_CHANNEL_POSITION_REAR_RIGHT => .back_right, + 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, + c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => .front_center, + 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, + .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, + }; +} + +pub fn toPAChannelMap(channels: []const main.ChannelPosition) !c.pa_channel_map { + var channel_map: c.pa_channel_map = undefined; + channel_map.channels = @as(u5, @intCast(channels.len)); + for (channels, 0..) |ch, i| + channel_map.map[i] = try toPAChannelPos(ch); + return channel_map; +} + +fn toPAChannelPos(channel_id: main.ChannelPosition) !c.pa_channel_position_t { + return switch (channel_id) { + .lfe => c.PA_CHANNEL_POSITION_LFE, + .front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER, + .front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT, + .front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT, + .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, + .back_left => c.PA_CHANNEL_POSITION_REAR_LEFT, + .back_right => c.PA_CHANNEL_POSITION_REAR_RIGHT, + .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_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER, + .top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT, + .top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, + .top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER, + .top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT, + .top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT, + }; +} diff --git a/src/sysaudio/util.zig b/src/sysaudio/util.zig new file mode 100644 index 00000000..782e6175 --- /dev/null +++ b/src/sysaudio/util.zig @@ -0,0 +1,61 @@ +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(device_info: *DevicesInfo) void { + device_info.default_output = null; + device_info.default_input = null; + device_info.list.clearRetainingCapacity(); + } + + pub fn get(device_info: DevicesInfo, i: usize) main.Device { + return device_info.list.items[i]; + } + + pub fn default(device_info: DevicesInfo, mode: main.Device.Mode) ?main.Device { + const index = switch (mode) { + .playback => device_info.default_output, + .capture => device_info.default_input, + } orelse { + for (device_info.list.items) |device| { + if (device.mode == mode) { + return device; + } + } + return null; + }; + return device_info.get(index); + } + + pub fn setDefault(device_info: *DevicesInfo, mode: main.Device.Mode, i: usize) void { + switch (mode) { + .playback => device_info.default_output = i, + .capture => device_info.default_input = i, + } + } +}; + +pub fn Range(comptime T: type) type { + return struct { + min: T, + max: T, + + pub fn clamp(range: @This(), val: T) T { + return std.math.clamp(val, range.min, range.max); + } + }; +} + +pub fn doNothing() callconv(.C) void {} diff --git a/src/sysaudio/wasapi.zig b/src/sysaudio/wasapi.zig new file mode 100644 index 00000000..eb990ad3 --- /dev/null +++ b/src/sysaudio/wasapi.zig @@ -0,0 +1,1053 @@ +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, + is_wine: bool, + + const Watcher = struct { + deviceChangeFn: main.Context.DeviceChangeFn, + user_data: ?*anyopaque, + notif_client: win32.IMMNotificationClient, + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + 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_OUTOFMEMORY => return error.OutOfMemory, + win32.E_UNEXPECTED => return error.SystemResources, + win32.E_INVALIDARG => unreachable, + else => unreachable, + } + + var ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .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, + @as(*?*anyopaque, @ptrCast(&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, + .user_data = options.user_data, + .notif_client = win32.IMMNotificationClient{ + .vtable = &.{ + .base = .{ + .QueryInterface = queryInterfaceCB, + .AddRef = addRefCB, + .Release = releaseCB, + }, + .OnDeviceStateChanged = onDeviceStateChangedCB, + .OnDeviceAdded = onDeviceAddedCB, + .OnDeviceRemoved = onDeviceRemovedCB, + .OnDefaultDeviceChanged = onDefaultDeviceChangedCB, + .OnPropertyValueChanged = onPropertyValueChangedCB, + }, + }, + } else null, + .is_wine = blk: { + const hntdll = win32.GetModuleHandleA("ntdll.dll"); + if (hntdll) |_| { + if (win32.GetProcAddress(hntdll, "wine_get_version")) |_| { + break :blk true; + } + } + break :blk false; + }, + }; + + if (options.deviceChangeFn) |_| { + hr = ctx.enumerator.?.RegisterEndpointNotificationCallback(&ctx.watcher.?.notif_client); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.SystemResources, + } + } + + return .{ .wasapi = ctx }; + } + + fn queryInterfaceCB(ctx: *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.?.* = @as(?*anyopaque, @ptrFromInt(@intFromPtr(ctx))); + _ = ctx.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(ctx: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", ctx); + watcher.deviceChangeFn(watcher.user_data); + return win32.S_OK; + } + + fn onDeviceAddedCB(ctx: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", ctx); + watcher.deviceChangeFn(watcher.user_data); + return win32.S_OK; + } + + fn onDeviceRemovedCB(ctx: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", ctx); + watcher.deviceChangeFn(watcher.user_data); + return win32.S_OK; + } + + fn onDefaultDeviceChangedCB(ctx: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", ctx); + watcher.deviceChangeFn(watcher.user_data); + return win32.S_OK; + } + + fn onPropertyValueChangedCB(ctx: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT { + var watcher = @fieldParentPtr(Watcher, "notif_client", ctx); + watcher.deviceChangeFn(watcher.user_data); + return win32.S_OK; + } + + pub fn deinit(ctx: *Context) void { + if (ctx.watcher) |*watcher| { + _ = ctx.enumerator.?.UnregisterEndpointNotificationCallback(&watcher.notif_client); + } + _ = ctx.enumerator.?.Release(); + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + } + + pub fn refresh(ctx: *Context) !void { + // get default devices id + const default_playback_id = try ctx.getDefaultAudioEndpoint(.playback); + defer ctx.allocator.free(default_playback_id.?); + const default_capture_id = try ctx.getDefaultAudioEndpoint(.capture); + defer ctx.allocator.free(default_capture_id.?); + + // enumerate + var collection: ?*win32.IMMDeviceCollection = null; + var hr = ctx.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, + } + const wf: *win32.WAVEFORMATEXTENSIBLE = @ptrCast(variant.anon.anon.anon.blob.pBlobData); + defer win32.CoTaskMemFree(variant.anon.anon.anon.blob.pBlobData); + + const channels = blk: { + var chn_arr = std.ArrayList(main.ChannelPosition).init(ctx.allocator); + var channel: u32 = win32.SPEAKER_FRONT_LEFT; + while (channel < win32.SPEAKER_ALL) : (channel <<= 1) { + if (wf.dwChannelMask & channel != 0) try chn_arr.append(fromWASApiChannel(channel)); + } + break :blk try chn_arr.toOwnedSlice(); + }; + + const sample_rate = util.Range(u24){ + .min = @intCast(wf.Format.nSamplesPerSec), + .max = @intCast(wf.Format.nSamplesPerSec), + }; + + const formats = blk: { + var audio_client: ?*win32.IAudioClient = null; + hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(&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, + } + + var fmt_arr = std.ArrayList(main.Format).init(ctx.allocator); + var closest_match: ?*win32.WAVEFORMATEX = null; + for (std.meta.tags(main.Format)) |format| { + setWaveFormatFormat(wf, format); + if (audio_client.?.IsFormatSupported( + .SHARED, + @as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(wf))), + &closest_match, + ) == win32.S_OK) { + try fmt_arr.append(format); + } + } + + break :blk try fmt_arr.toOwnedSlice(); + }; + + const 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(ctx.allocator, std.mem.span(id_u16.?)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => return error.OpeningDevice, + }; + }; + + const 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( + ctx.allocator, + std.mem.span(variant.anon.anon.anon.pwszVal.?), + ) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => return error.OpeningDevice, + }; + }; + + const dataflow = blk: { + var endpoint: ?*win32.IMMEndpoint = null; + hr = imm_device.?.QueryInterface(win32.IID_IMMEndpoint, @as(?*?*anyopaque, @ptrCast(&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 dataflow; + }; + + const modes: []const main.Device.Mode = switch (dataflow) { + .render => &.{.playback}, + .capture => &.{.capture}, + .all => &.{ .playback, .capture }, + }; + + for (modes) |mode| { + try ctx.devices_info.list.append(ctx.allocator, .{ + .mode = mode, + .channels = channels, + .sample_rate = sample_rate, + .formats = formats, + .id = id, + .name = name, + }); + switch (mode) { + .playback => if (default_playback_id) |default_id| { + if (std.mem.eql(u8, id, default_id)) { + ctx.devices_info.setDefault(.playback, ctx.devices_info.list.items.len - 1); + } + }, + .capture => if (default_capture_id) |default_id| { + if (std.mem.eql(u8, id, default_id)) { + ctx.devices_info.setDefault(.capture, ctx.devices_info.list.items.len - 1); + } + }, + } + } + } + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + fn fromWASApiChannel(speaker: u32) main.ChannelPosition { + 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_BACK_LEFT => .back_left, + win32.SPEAKER_BACK_RIGHT => .back_right, + 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, .i32 => { + wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*; + }, + .f32 => { + wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*; + }, + } + wf.Format.wBitsPerSample = format.sizeBits(); + wf.Samples.wValidBitsPerSample = format.validSizeBits(); + } + + fn getDefaultAudioEndpoint(ctx: *Context, mode: main.Device.Mode) !?[:0]u8 { + var default_playback_device: ?*win32.IMMDevice = null; + var hr = ctx.enumerator.?.GetDefaultAudioEndpoint( + if (mode == .playback) .render else .capture, + if (mode == .playback) .console else .communications, + &default_playback_device, + ); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.E_NOT_FOUND => return null, + else => return error.OpeningDevice, + } + defer _ = default_playback_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, + } + + return std.unicode.utf16leToUtf8AllocZ(ctx.allocator, std.mem.span(default_playback_id_u16.?)) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => return error.OpeningDevice, + }; + } + + fn createAudioClient( + ctx: *Context, + device: main.Device, + format: main.Format, + sample_rate: u24, + imm_device: *?*win32.IMMDevice, + audio_client: *?*win32.IAudioClient, + audio_client3: *?*win32.IAudioClient3, + max_buffer_frames: *u32, + ) !void { + const id_u16 = std.unicode.utf8ToUtf16LeWithNull(ctx.allocator, device.id) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + defer ctx.allocator.free(id_u16); + var hr = ctx.enumerator.?.GetDevice(id_u16, imm_device); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + else => return error.OpeningDevice, + } + + hr = imm_device.*.?.Activate(win32.IID_IAudioClient3, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(audio_client3))); + if (hr == win32.S_OK) { + hr = audio_client3.*.?.QueryInterface(win32.IID_IAudioClient, @as(?*?*anyopaque, @ptrCast(audio_client))); + switch (hr) { + win32.S_OK => {}, + win32.E_NOINTERFACE => unreachable, + win32.E_POINTER => unreachable, + else => return error.OpeningDevice, + } + } else { + hr = imm_device.*.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(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 wave_format = win32.WAVEFORMATEXTENSIBLE{ + .Format = .{ + .wFormatTag = win32.WAVE_FORMAT_EXTENSIBLE, + .nChannels = @as(u16, @intCast(device.channels.len)), + .nSamplesPerSec = sample_rate, + .nAvgBytesPerSec = sample_rate * format.frameSize(@intCast(device.channels.len)), + .nBlockAlign = format.frameSize(@intCast(device.channels.len)), + .wBitsPerSample = format.sizeBits(), + .cbSize = 0x16, + }, + .Samples = .{ + .wValidBitsPerSample = format.validSizeBits(), + }, + .dwChannelMask = toChannelMask(device.channels), + .SubFormat = toSubFormat(format), + }; + + if (!ctx.is_wine and audio_client3.* != null) { + hr = audio_client3.*.?.InitializeSharedAudioStream( + win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + 0, // TODO: use the advantage of AudioClient3 + @as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(&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_CPUUSAGE_EXCEEDED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_IN_USE => unreachable, + win32.AUDCLNT_E_ENGINE_FORMAT_LOCKED => return error.OpeningDevice, + win32.AUDCLNT_E_ENGINE_PERIODICITY_LOCKED => return error.OpeningDevice, + 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_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } + } else { + hr = audio_client.*.?.Initialize( + .SHARED, + win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + 0, + 0, + @as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(&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, + 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, + } + } + + hr = audio_client.*.?.GetBufferSize(max_buffer_frames); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => unreachable, + } + } + + fn createEvent(audio_client: ?*win32.IAudioClient) !?*anyopaque { + const ready_event = win32.CreateEventA(null, 0, 0, null) orelse return error.SystemResources; + const hr = audio_client.?.SetEventHandle(ready_event); + switch (hr) { + win32.S_OK => return ready_event, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED => unreachable, + win32.AUDCLNT_E_NOT_INITIALIZED => unreachable, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } + } + + fn createSimpleVolume(audio_client: ?*win32.IAudioClient) !?*win32.ISimpleAudioVolume { + var simple_volume: ?*win32.ISimpleAudioVolume = null; + const hr = audio_client.?.GetService(win32.IID_ISimpleAudioVolume, @as(?*?*anyopaque, @ptrCast(&simple_volume))); + switch (hr) { + win32.S_OK => return simple_volume, + 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, + } + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.min; + + var imm_device: ?*win32.IMMDevice = null; + var audio_client: ?*win32.IAudioClient = null; + var audio_client3: ?*win32.IAudioClient3 = null; + var max_buffer_frames: u32 = 0; + try ctx.createAudioClient(device, format, sample_rate, &imm_device, &audio_client, &audio_client3, &max_buffer_frames); + + var render_client: ?*win32.IAudioRenderClient = null; + const hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @as(?*?*anyopaque, @ptrCast(&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, + } + + const simple_volume = try createSimpleVolume(audio_client); + const ready_event = try createEvent(audio_client); + + const player = try ctx.allocator.create(Player); + player.* = .{ + .allocator = ctx.allocator, + .thread = undefined, + .audio_client = audio_client, + .audio_client3 = audio_client3, + .simple_volume = simple_volume, + .imm_device = imm_device, + .render_client = render_client, + .ready_event = ready_event, + .max_buffer_frames = max_buffer_frames, + .aborted = .{ .raw = false }, + .is_paused = false, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .wasapi = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + const format = device.preferredFormat(options.format); + const sample_rate = device.sample_rate.min; + + var imm_device: ?*win32.IMMDevice = null; + var audio_client: ?*win32.IAudioClient = null; + var audio_client3: ?*win32.IAudioClient3 = null; + var max_buffer_frames: u32 = 0; + try ctx.createAudioClient(device, format, sample_rate, &imm_device, &audio_client, &audio_client3, &max_buffer_frames); + + var capture_client: ?*win32.IAudioCaptureClient = null; + const hr = audio_client.?.GetService(win32.IID_IAudioCaptureClient, @as(?*?*anyopaque, @ptrCast(&capture_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, + } + + const simple_volume = try createSimpleVolume(audio_client); + const ready_event = try createEvent(audio_client); + + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .thread = undefined, + .audio_client = audio_client, + .audio_client3 = audio_client3, + .simple_volume = simple_volume, + .imm_device = imm_device, + .capture_client = capture_client, + .ready_event = ready_event, + .max_buffer_frames = max_buffer_frames, + .aborted = .{ .raw = false }, + .is_paused = false, + .readFn = readFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .sample_rate = sample_rate, + }; + return .{ .wasapi = recorder }; + } + + fn toSubFormat(format: main.Format) win32.Guid { + return switch (format) { + .u8, + .i16, + .i24, + .i32, + => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*, + }; + } + + fn toChannelMask(channels: []const main.ChannelPosition) u32 { + var mask: u32 = 0; + for (channels) |ch| { + mask |= switch (ch) { + .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, + .back_left => win32.SPEAKER_BACK_LEFT, + .back_right => win32.SPEAKER_BACK_RIGHT, + .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 { + allocator: std.mem.Allocator, + thread: std.Thread, + simple_volume: ?*win32.ISimpleAudioVolume, + imm_device: ?*win32.IMMDevice, + audio_client: ?*win32.IAudioClient, + audio_client3: ?*win32.IAudioClient3, + render_client: ?*win32.IAudioRenderClient, + ready_event: ?*anyopaque, + max_buffer_frames: u32, + aborted: std.atomic.Value(bool), + is_paused: bool, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(player: *Player) void { + player.aborted.store(true, .Unordered); + player.thread.join(); + _ = player.simple_volume.?.Release(); + _ = player.render_client.?.Release(); + _ = player.audio_client.?.Release(); + _ = player.audio_client3.?.Release(); + _ = player.imm_device.?.Release(); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + player.thread = std.Thread.spawn(.{}, writeThread, .{player}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + error.Unexpected, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + }; + } + + fn writeThread(player: *Player) void { + var hr = player.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 (!player.aborted.load(.Unordered)) { + _ = win32.WaitForSingleObject(player.ready_event, win32.INFINITE); + + var padding_frames: u32 = 0; + hr = player.audio_client.?.GetCurrentPadding(&padding_frames); + 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 frames = player.max_buffer_frames - padding_frames; + if (frames > 0) { + var data: [*]u8 = undefined; + hr = player.render_client.?.GetBuffer(frames, @as(?*?*u8, @ptrCast(&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, + } + + player.writeFn( + player.user_data, + data[0 .. frames * player.format.frameSize(@intCast(player.channels.len))], + ); + + hr = player.render_client.?.ReleaseBuffer(frames, 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(player: *Player) !void { + if (player.paused()) { + const hr = player.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, + } + player.is_paused = false; + } + } + + pub fn pause(player: *Player) !void { + if (!player.paused()) { + const hr = player.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, + } + player.is_paused = true; + } + } + + pub fn paused(player: *Player) bool { + return player.is_paused; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + const hr = player.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(player: *Player) !f32 { + var vol: f32 = 0; + const hr = player.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 const Recorder = struct { + allocator: std.mem.Allocator, + thread: std.Thread, + simple_volume: ?*win32.ISimpleAudioVolume, + imm_device: ?*win32.IMMDevice, + audio_client: ?*win32.IAudioClient, + audio_client3: ?*win32.IAudioClient3, + capture_client: ?*win32.IAudioCaptureClient, + ready_event: ?*anyopaque, + max_buffer_frames: u32, + aborted: std.atomic.Value(bool), + is_paused: bool, + readFn: main.ReadFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(recorder: *Recorder) void { + recorder.aborted.store(true, .Unordered); + recorder.thread.join(); + _ = recorder.simple_volume.?.Release(); + _ = recorder.capture_client.?.Release(); + _ = recorder.audio_client.?.Release(); + _ = recorder.audio_client3.?.Release(); + _ = recorder.imm_device.?.Release(); + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + recorder.thread = std.Thread.spawn(.{}, readThread, .{recorder}) catch |err| switch (err) { + error.ThreadQuotaExceeded, + error.SystemResources, + error.LockedMemoryLimitExceeded, + error.Unexpected, + => return error.SystemResources, + error.OutOfMemory => return error.OutOfMemory, + }; + } + + fn readThread(recorder: *Recorder) void { + var hr = recorder.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 (!recorder.aborted.load(.Unordered)) { + _ = win32.WaitForSingleObject(recorder.ready_event, win32.INFINITE); + + var padding_frames: u32 = 0; + hr = recorder.audio_client.?.GetCurrentPadding(&padding_frames); + 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 = recorder.max_buffer_frames - padding_frames; + if (frames > 0) { + var data: [*]u8 = undefined; + var flags: u32 = 0; + hr = recorder.capture_client.?.GetBuffer(@as(?*?*u8, @ptrCast(&data)), &frames, &flags, null, null); + 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, + } + + recorder.readFn( + recorder.user_data, + data[0 .. frames * recorder.format.frameSize(@intCast(recorder.channels.len))], + ); + + hr = recorder.capture_client.?.ReleaseBuffer(frames); + 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 record(recorder: *Recorder) !void { + if (recorder.paused()) { + const hr = recorder.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.CannotRecord, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotRecord, + else => unreachable, + } + recorder.is_paused = false; + } + } + + pub fn pause(recorder: *Recorder) !void { + if (!recorder.paused()) { + const hr = recorder.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, + } + recorder.is_paused = true; + } + } + + pub fn paused(recorder: *Recorder) bool { + return recorder.is_paused; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + const hr = recorder.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(recorder: *Recorder) !f32 { + var vol: f32 = 0; + const hr = recorder.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 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); +} diff --git a/src/sysaudio/wasapi/win32.zig b/src/sysaudio/wasapi/win32.zig new file mode 100644 index 00000000..eaba8ac5 --- /dev/null +++ b/src/sysaudio/wasapi/win32.zig @@ -0,0 +1,1821 @@ +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, 0..) |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 @as(u4, @intCast(c - '0')); + if (c >= 'a') return @as(u4, @intCast(c + 10 - 'a')); + return @as(u4, @intCast(c + 10 - 'A')); + } + fn decodeHexByte(hex: [2]u8) u8 { + return @as(u8, @intCast(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_NOT_FOUND = -2147023728; +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 extern "kernel32" fn CreateEventA( + lpEventAttributes: ?*SECURITY_ATTRIBUTES, + bManualReset: BOOL, + bInitialState: BOOL, + lpName: ?[*:0]const u8, +) callconv(WINAPI) ?*anyopaque; +pub extern "kernel32" fn WaitForSingleObject( + hHandle: ?*anyopaque, + dwMilliseconds: u32, +) callconv(WINAPI) u32; +pub extern "kernel32" fn GetModuleHandleA( + lpModuleName: ?[*:0]const u8, +) callconv(WINAPI) ?*anyopaque; +pub extern "kernel32" fn GetProcAddress( + hModule: ?*anyopaque, + lpProcName: ?[*:0]const u8, +) callconv(WINAPI) ?*const fn () callconv(WINAPI) isize; +pub const INFINITE = 4294967295; +pub const SECURITY_ATTRIBUTES = extern struct { + nLength: u32, + lpSecurityDescriptor: ?*anyopaque, + bInheritHandle: BOOL, +}; +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 @as(*const IUnknown.VTable, @ptrCast(self.vtable)).QueryInterface(@as(*const IUnknown, @ptrCast(self)), riid, ppvObject); + } + pub inline fn AddRef(self: *const T) u32 { + return @as(*const IUnknown.VTable, @ptrCast(self.vtable)).AddRef(@as(*const IUnknown, @ptrCast(self))); + } + pub inline fn Release(self: *const T) u32 { + return @as(*const IUnknown.VTable, @ptrCast(self.vtable)).Release(@as(*const IUnknown, @ptrCast(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 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: ?*anyopaque, + ) 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 @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).Initialize(@as(*const IAudioClient, @ptrCast(self)), ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat, AudioSessionGuid); + } + pub inline fn GetBufferSize(self: *const T, pNumBufferFrames: ?*u32) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetBufferSize(@as(*const IAudioClient, @ptrCast(self)), pNumBufferFrames); + } + pub inline fn GetStreamLatency(self: *const T, phnsLatency: ?*i64) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetStreamLatency(@as(*const IAudioClient, @ptrCast(self)), phnsLatency); + } + pub inline fn GetCurrentPadding(self: *const T, pNumPaddingFrames: ?*u32) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetCurrentPadding(@as(*const IAudioClient, @ptrCast(self)), pNumPaddingFrames); + } + pub inline fn IsFormatSupported(self: *const T, ShareMode: AUDCLNT_SHAREMODE, pFormat: ?*const WAVEFORMATEX, ppClosestMatch: ?*?*WAVEFORMATEX) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).IsFormatSupported(@as(*const IAudioClient, @ptrCast(self)), ShareMode, pFormat, ppClosestMatch); + } + pub inline fn GetMixFormat(self: *const T, ppDeviceFormat: ?*?*WAVEFORMATEX) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetMixFormat(@as(*const IAudioClient, @ptrCast(self)), ppDeviceFormat); + } + pub inline fn GetDevicePeriod(self: *const T, phnsDefaultDevicePeriod: ?*i64, phnsMinimumDevicePeriod: ?*i64) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetDevicePeriod(@as(*const IAudioClient, @ptrCast(self)), phnsDefaultDevicePeriod, phnsMinimumDevicePeriod); + } + pub inline fn Start(self: *const T) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).Start(@as(*const IAudioClient, @ptrCast(self))); + } + pub inline fn Stop(self: *const T) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).Stop(@as(*const IAudioClient, @ptrCast(self))); + } + pub inline fn Reset(self: *const T) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).Reset(@as(*const IAudioClient, @ptrCast(self))); + } + pub inline fn SetEventHandle(self: *const T, eventHandle: ?*anyopaque) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).SetEventHandle(@as(*const IAudioClient, @ptrCast(self)), eventHandle); + } + pub inline fn GetService(self: *const T, riid: ?*const Guid, ppv: ?*?*anyopaque) HRESULT { + return @as(*const IAudioClient.VTable, @ptrCast(self.vtable)).GetService(@as(*const IAudioClient, @ptrCast(self)), riid, ppv); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const AUDCLNT_STREAMOPTIONS = enum(u32) { + NONE = 0, + RAW = 1, + MATCH_FORMAT = 2, + AMBISONICS = 4, +}; +pub const AudioClientProperties = extern struct { + cbSize: u32, + bIsOffload: BOOL, + eCategory: AUDIO_STREAM_CATEGORY, + Options: AUDCLNT_STREAMOPTIONS, +}; +pub const AUDIO_STREAM_CATEGORY = enum(i32) { + Other = 0, + ForegroundOnlyMedia = 1, + Communications = 3, + Alerts = 4, + SoundEffects = 5, + GameEffects = 6, + GameMedia = 7, + GameChat = 8, + Speech = 9, + Movie = 10, + Media = 11, + FarFieldSpeech = 12, + UniformSpeech = 13, + VoiceTyping = 14, +}; +const IID_IAudioClient2 = &Guid.initString("726778cd-f60a-4eda-82de-e47610cd78aa"); +pub const IAudioClient2 = extern struct { + pub const VTable = extern struct { + base: IAudioClient.VTable, + IsOffloadCapable: *const fn ( + self: *const IAudioClient2, + Category: AUDIO_STREAM_CATEGORY, + pbOffloadCapable: ?*BOOL, + ) callconv(WINAPI) HRESULT, + SetClientProperties: *const fn ( + self: *const IAudioClient2, + pProperties: ?*const AudioClientProperties, + ) callconv(WINAPI) HRESULT, + GetBufferSizeLimits: *const fn ( + self: *const IAudioClient2, + pFormat: ?*const WAVEFORMATEX, + bEventDriven: BOOL, + phnsMinBufferDuration: ?*i64, + phnsMaxBufferDuration: ?*i64, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IAudioClient.MethodMixin(T); + pub inline fn IsOffloadCapable(self: *const T, Category: AUDIO_STREAM_CATEGORY, pbOffloadCapable: ?*BOOL) HRESULT { + return @as(*const IAudioClient2.VTable, @ptrCast(self.vtable)).IsOffloadCapable(@as(*const IAudioClient2, @ptrCast(self)), Category, pbOffloadCapable); + } + pub inline fn SetClientProperties(self: *const T, pProperties: ?*const AudioClientProperties) HRESULT { + return @as(*const IAudioClient2.VTable, @ptrCast(self.vtable)).SetClientProperties(@as(*const IAudioClient2, @ptrCast(self)), pProperties); + } + pub inline fn GetBufferSizeLimits(self: *const T, pFormat: ?*const WAVEFORMATEX, bEventDriven: BOOL, phnsMinBufferDuration: ?*i64, phnsMaxBufferDuration: ?*i64) HRESULT { + return @as(*const IAudioClient2.VTable, @ptrCast(self.vtable)).GetBufferSizeLimits(@as(*const IAudioClient2, @ptrCast(self)), pFormat, bEventDriven, phnsMinBufferDuration, phnsMaxBufferDuration); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IAudioClient3 = &Guid.initString("7ed4ee07-8e67-4cd4-8c1a-2b7a5987ad42"); +pub const IAudioClient3 = extern struct { + pub const VTable = extern struct { + base: IAudioClient2.VTable, + GetSharedModeEnginePeriod: *const fn ( + self: *const IAudioClient3, + pFormat: ?*const WAVEFORMATEX, + pDefaultPeriodInFrames: ?*u32, + pFundamentalPeriodInFrames: ?*u32, + pMinPeriodInFrames: ?*u32, + pMaxPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + GetCurrentSharedModeEnginePeriod: *const fn ( + self: *const IAudioClient3, + ppFormat: ?*?*WAVEFORMATEX, + pCurrentPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + InitializeSharedAudioStream: *const fn ( + self: *const IAudioClient3, + StreamFlags: u32, + PeriodInFrames: u32, + pFormat: ?*const WAVEFORMATEX, + AudioSessionGuid: ?*const Guid, + ) callconv(WINAPI) HRESULT, + }; + vtable: *const VTable, + pub fn MethodMixin(comptime T: type) type { + return struct { + pub usingnamespace IAudioClient2.MethodMixin(T); + pub inline fn GetSharedModeEnginePeriod(self: *const T, pFormat: ?*const WAVEFORMATEX, pDefaultPeriodInFrames: ?*u32, pFundamentalPeriodInFrames: ?*u32, pMinPeriodInFrames: ?*u32, pMaxPeriodInFrames: ?*u32) HRESULT { + return @as(*const IAudioClient3.VTable, @ptrCast(self.vtable)).GetSharedModeEnginePeriod(@as(*const IAudioClient3, @ptrCast(self)), pFormat, pDefaultPeriodInFrames, pFundamentalPeriodInFrames, pMinPeriodInFrames, pMaxPeriodInFrames); + } + pub inline fn GetCurrentSharedModeEnginePeriod(self: *const T, ppFormat: ?*?*WAVEFORMATEX, pCurrentPeriodInFrames: ?*u32) HRESULT { + return @as(*const IAudioClient3.VTable, @ptrCast(self.vtable)).GetCurrentSharedModeEnginePeriod(@as(*const IAudioClient3, @ptrCast(self)), ppFormat, pCurrentPeriodInFrames); + } + pub inline fn InitializeSharedAudioStream(self: *const T, StreamFlags: u32, PeriodInFrames: u32, pFormat: ?*const WAVEFORMATEX, AudioSessionGuid: ?*const Guid) HRESULT { + return @as(*const IAudioClient3.VTable, @ptrCast(self.vtable)).InitializeSharedAudioStream(@as(*const IAudioClient3, @ptrCast(self)), StreamFlags, PeriodInFrames, pFormat, AudioSessionGuid); + } + }; + } + 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 @as(*const IAudioRenderClient.VTable, @ptrCast(self.vtable)).GetBuffer(@as(*const IAudioRenderClient, @ptrCast(self)), NumFramesRequested, ppData); + } + pub inline fn ReleaseBuffer(self: *const T, NumFramesWritten: u32, dwFlags: u32) HRESULT { + return @as(*const IAudioRenderClient.VTable, @ptrCast(self.vtable)).ReleaseBuffer(@as(*const IAudioRenderClient, @ptrCast(self)), NumFramesWritten, dwFlags); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const IID_IAudioCaptureClient = &Guid.initString("c8adbd64-e71e-48a0-a4de-185c395cd317"); +pub const IAudioCaptureClient = extern struct { + pub const VTable = extern struct { + base: IUnknown.VTable, + GetBuffer: *const fn ( + self: *const IAudioCaptureClient, + ppData: ?*?*u8, + pNumFramesToRead: ?*u32, + pdwFlags: ?*u32, + pu64DevicePosition: ?*u64, + pu64QPCPosition: ?*u64, + ) callconv(WINAPI) HRESULT, + ReleaseBuffer: *const fn ( + self: *const IAudioCaptureClient, + NumFramesRead: u32, + ) callconv(WINAPI) HRESULT, + GetNextPacketSize: *const fn ( + self: *const IAudioCaptureClient, + pNumFramesInNextPacket: ?*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, ppData: ?*?*u8, pNumFramesToRead: ?*u32, pdwFlags: ?*u32, pu64DevicePosition: ?*u64, pu64QPCPosition: ?*u64) HRESULT { + return @as(*const IAudioCaptureClient.VTable, @ptrCast(self.vtable)).GetBuffer(@as(*const IAudioCaptureClient, @ptrCast(self)), ppData, pNumFramesToRead, pdwFlags, pu64DevicePosition, pu64QPCPosition); + } + pub inline fn ReleaseBuffer(self: *const T, NumFramesRead: u32) HRESULT { + return @as(*const IAudioCaptureClient.VTable, @ptrCast(self.vtable)).ReleaseBuffer(@as(*const IAudioCaptureClient, @ptrCast(self)), NumFramesRead); + } + pub inline fn GetNextPacketSize(self: *const T, pNumFramesInNextPacket: ?*u32) HRESULT { + return @as(*const IAudioCaptureClient.VTable, @ptrCast(self.vtable)).GetNextPacketSize(@as(*const IAudioCaptureClient, @ptrCast(self)), pNumFramesInNextPacket); + } + }; + } + 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 @as(*const ISimpleAudioVolume.VTable, @ptrCast(self.vtable)).SetMasterVolume(@as(*const ISimpleAudioVolume, @ptrCast(self)), fLevel, EventContext); + } + pub inline fn GetMasterVolume(self: *const T, pfLevel: ?*f32) HRESULT { + return @as(*const ISimpleAudioVolume.VTable, @ptrCast(self.vtable)).GetMasterVolume(@as(*const ISimpleAudioVolume, @ptrCast(self)), pfLevel); + } + pub inline fn SetMute(self: *const T, bMute: BOOL, EventContext: ?*const Guid) HRESULT { + return @as(*const ISimpleAudioVolume.VTable, @ptrCast(self.vtable)).SetMute(@as(*const ISimpleAudioVolume, @ptrCast(self)), bMute, EventContext); + } + pub inline fn GetMute(self: *const T, pbMute: ?*BOOL) HRESULT { + return @as(*const ISimpleAudioVolume.VTable, @ptrCast(self.vtable)).GetMute(@as(*const ISimpleAudioVolume, @ptrCast(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 @as(*const IPropertyStore.VTable, @ptrCast(self.vtable)).GetCount(@as(*const IPropertyStore, @ptrCast(self)), cProps); + } + pub inline fn GetAt(self: *const T, iProp: u32, pkey: ?*PROPERTYKEY) HRESULT { + return @as(*const IPropertyStore.VTable, @ptrCast(self.vtable)).GetAt(@as(*const IPropertyStore, @ptrCast(self)), iProp, pkey); + } + pub inline fn GetValue(self: *const T, key: ?*const PROPERTYKEY, pv: ?*PROPVARIANT) HRESULT { + return @as(*const IPropertyStore.VTable, @ptrCast(self.vtable)).GetValue(@as(*const IPropertyStore, @ptrCast(self)), key, pv); + } + pub inline fn SetValue(self: *const T, key: ?*const PROPERTYKEY, propvar: ?*const PROPVARIANT) HRESULT { + return @as(*const IPropertyStore.VTable, @ptrCast(self.vtable)).SetValue(@as(*const IPropertyStore, @ptrCast(self)), key, propvar); + } + pub inline fn Commit(self: *const T) HRESULT { + return @as(*const IPropertyStore.VTable, @ptrCast(self.vtable)).Commit(@as(*const IPropertyStore, @ptrCast(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 @as(*const IMMDevice.VTable, @ptrCast(self.vtable)).Activate(@as(*const IMMDevice, @ptrCast(self)), iid, dwClsCtx, pActivationParams, ppInterface); + } + pub inline fn OpenPropertyStore(self: *const T, stgmAccess: u32, ppProperties: ?*?*IPropertyStore) HRESULT { + return @as(*const IMMDevice.VTable, @ptrCast(self.vtable)).OpenPropertyStore(@as(*const IMMDevice, @ptrCast(self)), stgmAccess, ppProperties); + } + pub inline fn GetId(self: *const T, ppstrId: ?*?PWSTR) HRESULT { + return @as(*const IMMDevice.VTable, @ptrCast(self.vtable)).GetId(@as(*const IMMDevice, @ptrCast(self)), ppstrId); + } + pub inline fn GetState(self: *const T, pdwState: ?*u32) HRESULT { + return @as(*const IMMDevice.VTable, @ptrCast(self.vtable)).GetState(@as(*const IMMDevice, @ptrCast(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 @as(*const IMMNotificationClient.VTable, @ptrCast(self.vtable)).OnDeviceStateChanged(@as(*const IMMNotificationClient, @ptrCast(self)), pwstrDeviceId, dwNewState); + } + pub inline fn OnDeviceAdded(self: *const T, pwstrDeviceId: ?[*:0]const u16) HRESULT { + return @as(*const IMMNotificationClient.VTable, @ptrCast(self.vtable)).OnDeviceAdded(@as(*const IMMNotificationClient, @ptrCast(self)), pwstrDeviceId); + } + pub inline fn OnDeviceRemoved(self: *const T, pwstrDeviceId: ?[*:0]const u16) HRESULT { + return @as(*const IMMNotificationClient.VTable, @ptrCast(self.vtable)).OnDeviceRemoved(@as(*const IMMNotificationClient, @ptrCast(self)), pwstrDeviceId); + } + pub inline fn OnDefaultDeviceChanged(self: *const T, flow: DataFlow, role: Role, pwstrDefaultDeviceId: ?[*:0]const u16) HRESULT { + return @as(*const IMMNotificationClient.VTable, @ptrCast(self.vtable)).OnDefaultDeviceChanged(@as(*const IMMNotificationClient, @ptrCast(self)), flow, role, pwstrDefaultDeviceId); + } + pub inline fn OnPropertyValueChanged(self: *const T, pwstrDeviceId: ?[*:0]const u16, key: PROPERTYKEY) HRESULT { + return @as(*const IMMNotificationClient.VTable, @ptrCast(self.vtable)).OnPropertyValueChanged(@as(*const IMMNotificationClient, @ptrCast(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 @as(*const IMMDeviceCollection.VTable, @ptrCast(self.vtable)).GetCount(@as(*const IMMDeviceCollection, @ptrCast(self)), pcDevices); + } + pub inline fn Item(self: *const T, nDevice: u32, ppDevice: ?*?*IMMDevice) HRESULT { + return @as(*const IMMDeviceCollection.VTable, @ptrCast(self.vtable)).Item(@as(*const IMMDeviceCollection, @ptrCast(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 @as(*const IMMDeviceEnumerator.VTable, @ptrCast(self.vtable)).EnumAudioEndpoints(@as(*const IMMDeviceEnumerator, @ptrCast(self)), dataFlow, dwStateMask, ppDevices); + } + pub inline fn GetDefaultAudioEndpoint(self: *const T, dataFlow: DataFlow, role: Role, ppEndpoint: ?*?*IMMDevice) HRESULT { + return @as(*const IMMDeviceEnumerator.VTable, @ptrCast(self.vtable)).GetDefaultAudioEndpoint(@as(*const IMMDeviceEnumerator, @ptrCast(self)), dataFlow, role, ppEndpoint); + } + pub inline fn GetDevice(self: *const T, pwstrId: ?[*:0]const u16, ppDevice: ?*?*IMMDevice) HRESULT { + return @as(*const IMMDeviceEnumerator.VTable, @ptrCast(self.vtable)).GetDevice(@as(*const IMMDeviceEnumerator, @ptrCast(self)), pwstrId, ppDevice); + } + pub inline fn RegisterEndpointNotificationCallback(self: *const T, pClient: ?*IMMNotificationClient) HRESULT { + return @as(*const IMMDeviceEnumerator.VTable, @ptrCast(self.vtable)).RegisterEndpointNotificationCallback(@as(*const IMMDeviceEnumerator, @ptrCast(self)), pClient); + } + pub inline fn UnregisterEndpointNotificationCallback(self: *const T, pClient: ?*IMMNotificationClient) HRESULT { + return @as(*const IMMDeviceEnumerator.VTable, @ptrCast(self.vtable)).UnregisterEndpointNotificationCallback(@as(*const IMMDeviceEnumerator, @ptrCast(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 @as(*const IMMEndpoint.VTable, @ptrCast(self.vtable)).GetDataFlow(@as(*const IMMEndpoint, @ptrCast(self)), pDataFlow); + } + }; + } + pub usingnamespace MethodMixin(@This()); +}; +pub const AUDCLNT_STREAMFLAGS_CROSSPROCESS = 65536; +pub const AUDCLNT_STREAMFLAGS_LOOPBACK = 131072; +pub const AUDCLNT_STREAMFLAGS_EVENTCALLBACK = 262144; +pub const AUDCLNT_STREAMFLAGS_NOPERSIST = 524288; +pub const AUDCLNT_STREAMFLAGS_RATEADJUST = 1048576; +pub const AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY = 134217728; +pub const AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM = 2147483648; +pub const AUDCLNT_SESSIONFLAGS_EXPIREWHENUNOWNED = 268435456; +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/src/sysaudio/webaudio.zig b/src/sysaudio/webaudio.zig new file mode 100644 index 00000000..c2666518 --- /dev/null +++ b/src/sysaudio/webaudio.zig @@ -0,0 +1,275 @@ +const std = @import("std"); +const js = @import("sysjs"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +const default_sample_rate = 44_100; // Hz +const channel_size = 1024; +const channel_size_bytes = channel_size * @sizeOf(f32); + +const default_playback = main.Device{ + .id = "default-playback", + .name = "Default Device", + .mode = .playback, + .channels = undefined, + .formats = &.{.f32}, + .sample_rate = .{ + .min = 8_000, + .max = 96_000, + }, +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context { + _ = options; + + const audio_context = js.global().get("AudioContext"); + if (audio_context.is(.undefined)) + return error.ConnectionRefused; + + const ctx = try allocator.create(Context); + errdefer allocator.destroy(ctx); + ctx.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + }; + + return .{ .webaudio = ctx }; + } + + pub fn deinit(ctx: *Context) void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.list.deinit(ctx.allocator); + ctx.allocator.destroy(ctx); + } + + pub fn refresh(ctx: *Context) !void { + for (ctx.devices_info.list.items) |d| + freeDevice(ctx.allocator, d); + ctx.devices_info.clear(ctx.allocator); + + try ctx.devices_info.list.append(ctx.allocator, default_playback); + ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 2); + ctx.devices_info.list.items[0].channels[0] = .front_left; + ctx.devices_info.list.items[0].channels[1] = .front_right; + ctx.devices_info.setDefault(.playback, 0); + } + + pub fn devices(ctx: Context) []const main.Device { + return ctx.devices_info.list.items; + } + + pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device { + return ctx.devices_info.default(mode); + } + + pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player { + const context_options = js.createMap(); + defer context_options.deinit(); + context_options.set("sampleRate", js.createNumber(options.sample_rate orelse default_sample_rate)); + + const audio_context = js.constructType("AudioContext", &.{context_options.toValue()}); + const gain_node = audio_context.call("createGain", &.{ + js.createNumber(1), + js.createNumber(0), + js.createNumber(device.channels.len), + }).view(.object); + const process_node = audio_context.call("createScriptProcessor", &.{ + js.createNumber(channel_size), + js.createNumber(device.channels.len), + }).view(.object); + + const player = try ctx.allocator.create(Player); + errdefer ctx.allocator.destroy(player); + + var captures = try ctx.allocator.alloc(js.Value, 1); + captures[0] = js.createNumber(@intFromPtr(player)); + + const document = js.global().get("document").view(.object); + defer document.deinit(); + const click_event_str = js.createString("click"); + defer click_event_str.deinit(); + const resume_on_click = js.createFunction(Player.resumeOnClick, captures); + _ = document.call("addEventListener", &.{ click_event_str.toValue(), resume_on_click.toValue() }); + + const audio_process_event = js.createFunction(Player.audioProcessEvent, captures); + defer audio_process_event.deinit(); + process_node.set("onaudioprocess", audio_process_event.toValue()); + + player.* = .{ + .allocator = ctx.allocator, + .audio_context = audio_context, + .process_node = process_node, + .gain_node = gain_node, + .process_captures = captures, + .resume_on_click = resume_on_click, + .buf = try ctx.allocator.alloc(u8, channel_size_bytes * device.channels.len), + .buf_js = js.constructType("Uint8Array", &.{js.createNumber(channel_size_bytes)}), + .is_paused = false, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + + return .{ .webaudio = player }; + } + + pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder { + _ = readFn; + const recorder = try ctx.allocator.create(Recorder); + recorder.* = .{ + .allocator = ctx.allocator, + .is_paused = false, + .vol = 1.0, + .channels = device.channels, + .format = options.format, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; + return .{ .webaudio = recorder }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + audio_context: js.Object, + process_node: js.Object, + gain_node: js.Object, + process_captures: []js.Value, + resume_on_click: js.Function, + buf: []u8, + buf_js: js.Object, + is_paused: bool, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(player: *Player) void { + player.resume_on_click.deinit(); + player.buf_js.deinit(); + player.gain_node.deinit(); + player.process_node.deinit(); + player.audio_context.deinit(); + player.allocator.free(player.process_captures); + player.allocator.free(player.buf); + player.allocator.destroy(player); + } + + pub fn start(player: *Player) !void { + const destination = player.audio_context.get("destination").view(.object); + defer destination.deinit(); + _ = player.gain_node.call("connect", &.{destination.toValue()}); + _ = player.process_node.call("connect", &.{player.gain_node.toValue()}); + } + + fn resumeOnClick(args: js.Object, _: usize, captures: []js.Value) js.Value { + const player = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num))))); + player.play() catch {}; + + const document = js.global().get("document").view(.object); + defer document.deinit(); + + const event = args.getIndex(0).view(.object); + defer event.deinit(); + _ = document.call("removeEventListener", &.{ event.toValue(), player.resume_on_click.toValue() }); + + return js.createUndefined(); + } + + fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value { + const player = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num))))); + + const event = args.getIndex(0).view(.object); + defer event.deinit(); + const output_buffer = event.get("outputBuffer").view(.object); + defer output_buffer.deinit(); + + player.writeFn(player.user_data, player.buf[0..channel_size]); + + for (player.channels, 0..) |_, i| { + player.buf_js.copyBytes(player.buf[i * channel_size_bytes .. (i + 1) * channel_size_bytes]); + const buf_f32_js = js.constructType("Float32Array", &.{ player.buf_js.get("buffer"), player.buf_js.get("byteOffset"), js.createNumber(channel_size) }); + defer buf_f32_js.deinit(); + _ = output_buffer.call("copyToChannel", &.{ buf_f32_js.toValue(), js.createNumber(i) }); + } + + return js.createUndefined(); + } + + pub fn play(player: *Player) !void { + _ = player.audio_context.call("resume", &.{js.createUndefined()}); + player.is_paused = false; + } + + pub fn pause(player: *Player) !void { + _ = player.audio_context.call("suspend", &.{js.createUndefined()}); + player.is_paused = true; + } + + pub fn paused(player: *Player) bool { + return player.is_paused; + } + + pub fn setVolume(player: *Player, vol: f32) !void { + const gain = player.gain_node.get("gain").view(.object); + defer gain.deinit(); + gain.set("value", js.createNumber(vol)); + } + + pub fn volume(player: *Player) !f32 { + const gain = player.gain_node.get("gain").view(.object); + defer gain.deinit(); + return @as(f32, @floatCast(gain.get("value").view(.num))); + } +}; + +pub const Recorder = struct { + allocator: std.mem.Allocator, + is_paused: bool, + vol: f32, + + channels: []main.ChannelPosition, + format: main.Format, + sample_rate: u24, + + pub fn deinit(recorder: *Recorder) void { + recorder.allocator.destroy(recorder); + } + + pub fn start(recorder: *Recorder) !void { + _ = recorder; + } + + pub fn record(recorder: *Recorder) !void { + recorder.is_paused = false; + } + + pub fn pause(recorder: *Recorder) !void { + recorder.is_paused = true; + } + + pub fn paused(recorder: *Recorder) bool { + return recorder.is_paused; + } + + pub fn setVolume(recorder: *Recorder, vol: f32) !void { + recorder.vol = vol; + } + + pub fn volume(recorder: *Recorder) !f32 { + return recorder.vol; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +}