diff --git a/libs/sysaudio/examples/sine-wave.zig b/libs/sysaudio/examples/sine-wave.zig index b15d49b4..445a8e7d 100644 --- a/libs/sysaudio/examples/sine-wave.zig +++ b/libs/sysaudio/examples/sine-wave.zig @@ -1,38 +1,40 @@ const std = @import("std"); const sysaudio = @import("sysaudio"); +var player: sysaudio.Player = undefined; + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var a = try sysaudio.Context.init(null, allocator, .{ .deviceChangeFn = deviceChange }); - defer a.deinit(); - try a.refresh(); + var ctx = try sysaudio.Context.init(null, allocator, .{ .deviceChangeFn = deviceChange }); + defer ctx.deinit(); + try ctx.refresh(); - const device = a.defaultDevice(.playback) orelse return error.NoDevice; + const device = ctx.defaultDevice(.playback) orelse return error.NoDevice; - var p = try a.createPlayer(device, writeCallback, .{}); - defer p.deinit(); - try p.start(); + player = try ctx.createPlayer(device, writeCallback, .{}); + defer player.deinit(); + try player.start(); - try p.setVolume(0.85); + try player.setVolume(0.85); var buf: [16]u8 = undefined; while (true) { - std.debug.print("( paused = {}, volume = {d} )\n> ", .{ p.paused(), try p.volume() }); + 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")) { var vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace)); - try p.setVolume(vol); + try player.setVolume(vol); } else if (std.mem.eql(u8, cmd, "pause")) { - try p.pause(); - try std.testing.expect(p.paused()); + try player.pause(); + try std.testing.expect(player.paused()); } else if (std.mem.eql(u8, cmd, "play")) { - try p.play(); - try std.testing.expect(!p.paused()); + try player.play(); + try std.testing.expect(!player.paused()); } else if (std.mem.eql(u8, cmd, "exit")) { break; } @@ -42,19 +44,16 @@ pub fn main() !void { const pitch = 440.0; const radians_per_second = pitch * 2.0 * std.math.pi; var seconds_offset: f32 = 0.0; -fn writeCallback(self_opaque: *anyopaque, n_frame: usize) void { - var self = @ptrCast(*sysaudio.Player, @alignCast(@alignOf(sysaudio.Player), self_opaque)); - - const seconds_per_frame = 1.0 / @intToFloat(f32, self.sampleRate()); +fn writeCallback(_: ?*anyopaque, n_frame: usize) void { + const seconds_per_frame = 1.0 / @intToFloat(f32, player.sampleRate()); var frame: usize = 0; while (frame < n_frame) : (frame += 1) { const sample = std.math.sin((seconds_offset + @intToFloat(f32, frame) * seconds_per_frame) * radians_per_second); - self.writeAll(frame, sample); + player.writeAll(frame, sample); } seconds_offset = @mod(seconds_offset + seconds_per_frame * @intToFloat(f32, n_frame), 1.0); } -fn deviceChange(self: ?*anyopaque) void { - _ = self; +fn deviceChange(_: ?*anyopaque) void { std.debug.print("Device change detected!\n", .{}); } diff --git a/libs/sysaudio/main.zig b/libs/sysaudio/main.zig deleted file mode 100644 index 8337712e..00000000 --- a/libs/sysaudio/main.zig +++ /dev/null @@ -1 +0,0 @@ -// diff --git a/libs/sysaudio/src/alsa.zig b/libs/sysaudio/src/alsa.zig index 5eae818e..43ea52a8 100644 --- a/libs/sysaudio/src/alsa.zig +++ b/libs/sysaudio/src/alsa.zig @@ -13,7 +13,7 @@ pub const Context = struct { const Watcher = struct { deviceChangeFn: main.DeviceChangeFn, - userdata: ?*anyopaque, + user_data: ?*anyopaque, thread: std.Thread, aborted: std.atomic.Atomic(bool), notify_fd: std.os.fd_t, @@ -71,7 +71,7 @@ pub const Context = struct { break :blk .{ .deviceChangeFn = deviceChangeFn, - .userdata = options.userdata, + .user_data = options.user_data, .aborted = .{ .value = false }, .notify_fd = notify_fd, .notify_wd = notify_wd, @@ -172,7 +172,7 @@ pub const Context = struct { } if (scan) { - watcher.deviceChangeFn(self.watcher.?.userdata); + watcher.deviceChangeFn(self.watcher.?.user_data); scan = false; } } @@ -410,24 +410,26 @@ pub const Context = struct { return error.IncompatibleDevice; } - return .{ - .alsa = .{ - .allocator = self.allocator, - .thread = undefined, - .mutex = .{}, - ._channels = device.channels, - ._format = format, - .sample_rate = sample_rate, - .writeFn = writeFn, - .aborted = .{ .value = false }, - .sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)), - .period_size = period_size, - .pcm = pcm.?, - .mixer = mixer.?, - .selem = selem.?, - .mixer_elm = mixer_elm.?, - }, + var player = try self.allocator.create(Player); + player.* = .{ + .allocator = self.allocator, + .thread = undefined, + .mutex = .{}, + .aborted = .{ .value = false }, + .sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)), + .period_size = period_size, + .pcm = pcm.?, + .mixer = mixer.?, + .selem = selem.?, + .mixer_elm = mixer_elm.?, + .sample_rate = sample_rate, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .write_step = format.frameSize(device.channels.len), }; + return .{ .alsa = player }; } }; @@ -435,10 +437,6 @@ pub const Player = struct { allocator: std.mem.Allocator, thread: std.Thread, mutex: std.Thread.Mutex, - _channels: []main.Channel, - _format: main.Format, - sample_rate: u24, - writeFn: main.WriteFn, aborted: std.atomic.Atomic(bool), sample_buffer: []u8, period_size: c_ulong, @@ -446,6 +444,13 @@ pub const Player = struct { 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, + sample_rate: u24, + + channels: []main.Channel, + format: main.Format, + write_step: u8, pub fn deinit(self: *Player) void { self.aborted.store(true, .Unordered); @@ -457,6 +462,7 @@ pub const Player = struct { _ = c.snd_pcm_hw_free(self.pcm); self.allocator.free(self.sample_buffer); + self.allocator.destroy(self); } pub fn start(self: *Player) !void { @@ -471,16 +477,14 @@ pub const Player = struct { } fn writeLoop(self: *Player) void { - var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); - - for (self.channels()) |*ch, i| { - ch.*.ptr = self.sample_buffer.ptr + self.format().frameSize(i); + for (self.channels) |*ch, i| { + ch.*.ptr = self.sample_buffer.ptr + self.format.frameSize(i); } while (!self.aborted.load(.Unordered)) { var frames_left = self.period_size; while (frames_left > 0) { - self.writeFn(parent, frames_left); + self.writeFn(self.user_data, frames_left); const n = c.snd_pcm_writei(self.pcm, self.sample_buffer.ptr, frames_left); if (n < 0) { if (c.snd_pcm_recover(self.pcm, @intCast(c_int, n), 1) < 0) { @@ -554,19 +558,6 @@ pub const Player = struct { return @intToFloat(f32, vol) / @intToFloat(f32, max_vol - min_vol); } - pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { - var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len); - std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; - } - - pub fn channels(self: Player) []main.Channel { - return self._channels; - } - - pub fn format(self: Player) main.Format { - return self._format; - } - pub fn sampleRate(self: Player) u24 { return self.sample_rate; } @@ -595,7 +586,6 @@ pub fn toAlsaFormat(format: main.Format) !c.snd_pcm_format_t { .i24_4b => if (is_little) c.SND_PCM_FORMAT_S24_LE else c.SND_PCM_FORMAT_S24_BE, .i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE, .f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE, - .f64 => if (is_little) c.SND_PCM_FORMAT_FLOAT64_LE else c.SND_PCM_FORMAT_FLOAT64_BE, }; } diff --git a/libs/sysaudio/src/backends.zig b/libs/sysaudio/src/backends.zig index 10508468..87035bec 100644 --- a/libs/sysaudio/src/backends.zig +++ b/libs/sysaudio/src/backends.zig @@ -20,29 +20,47 @@ pub const BackendContext = switch (builtin.os.tag) { 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 BackendPlayer = switch (builtin.os.tag) { .linux => union(enum) { - pulseaudio: @import("pulseaudio.zig").Player, - alsa: @import("alsa.zig").Player, - jack: @import("jack.zig").Player, - dummy: @import("dummy.zig").Player, + pulseaudio: *@import("pulseaudio.zig").Player, + alsa: *@import("alsa.zig").Player, + jack: *@import("jack.zig").Player, + dummy: *@import("dummy.zig").Player, }, .freebsd, .netbsd, .openbsd, .solaris => union(enum) { - pulseaudio: @import("pulseaudio.zig").Player, - dummy: @import("dummy.zig").Player, + pulseaudio: *@import("pulseaudio.zig").Player, + dummy: *@import("dummy.zig").Player, }, .macos, .ios, .watchos, .tvos => union(enum) { - dummy: @import("dummy.zig").Player, + dummy: *@import("dummy.zig").Player, }, .windows => union(enum) { - wasapi: @import("wasapi.zig").Player, - dummy: @import("dummy.zig").Player, + 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, + dummy: *@import("dummy.zig").Player, }, }; diff --git a/libs/sysaudio/src/dummy.zig b/libs/sysaudio/src/dummy.zig index 1328982b..7a5bdeea 100644 --- a/libs/sysaudio/src/dummy.zig +++ b/libs/sysaudio/src/dummy.zig @@ -44,19 +44,6 @@ pub const Context = struct { .devices_info = util.DevicesInfo.init(), }; - try self.devices_info.list.append(self.allocator, dummy_playback); - try self.devices_info.list.append(self.allocator, dummy_capture); - self.devices_info.list.items[0].channels = try allocator.alloc(main.Channel, 1); - self.devices_info.list.items[0].channels[0] = .{ - .id = .front_center, - }; - self.devices_info.list.items[1].channels = try allocator.alloc(main.Channel, 1); - self.devices_info.list.items[1].channels[0] = .{ - .id = .front_center, - }; - self.devices_info.setDefault(.playback, 0); - self.devices_info.setDefault(.capture, 1); - return .{ .dummy = self }; } @@ -68,7 +55,22 @@ pub const Context = struct { } pub fn refresh(self: *Context) !void { - _ = self; + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + try self.devices_info.list.append(self.allocator, dummy_playback); + try self.devices_info.list.append(self.allocator, dummy_capture); + self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 1); + self.devices_info.list.items[0].channels[0] = .{ + .id = .front_center, + }; + self.devices_info.list.items[1].channels = try self.allocator.alloc(main.Channel, 1); + self.devices_info.list.items[1].channels[0] = .{ + .id = .front_center, + }; + self.devices_info.setDefault(.playback, 0); + self.devices_info.setDefault(.capture, 1); } pub fn devices(self: Context) []const main.Device { @@ -80,29 +82,33 @@ pub const Context = struct { } pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { - _ = self; _ = writeFn; - return .{ - .dummy = .{ - ._channels = device.channels, - ._format = options.format, - .sample_rate = options.sample_rate, - .is_paused = false, - .vol = 1.0, - }, + var player = try self.allocator.create(Player); + player.* = .{ + .allocator = self.allocator, + .sample_rate = options.sample_rate, + .is_paused = false, + .vol = 1.0, + .channels = device.channels, + .format = options.format, + .write_step = 0, }; + return .{ .dummy = player }; } }; pub const Player = struct { - _channels: []main.Channel, - _format: main.Format, + allocator: std.mem.Allocator, sample_rate: u24, is_paused: bool, vol: f32, - pub fn deinit(self: Player) void { - _ = self; + channels: []main.Channel, + format: main.Format, + write_step: u8, + + pub fn deinit(self: *Player) void { + self.allocator.destroy(self); } pub fn start(self: Player) !void { @@ -129,21 +135,6 @@ pub const Player = struct { return self.vol; } - pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void { - _ = self; - _ = channel; - _ = frame; - _ = sample; - } - - pub fn channels(self: Player) []main.Channel { - return self._channels; - } - - pub fn format(self: Player) main.Format { - return self._format; - } - pub fn sampleRate(self: Player) u24 { return self.sample_rate; } diff --git a/libs/sysaudio/src/jack.zig b/libs/sysaudio/src/jack.zig index 98fa70aa..a14bfb99 100644 --- a/libs/sysaudio/src/jack.zig +++ b/libs/sysaudio/src/jack.zig @@ -12,7 +12,7 @@ pub const Context = struct { const Watcher = struct { deviceChangeFn: main.DeviceChangeFn, - userdata: ?*anyopaque, + user_data: ?*anyopaque, }; pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { @@ -34,7 +34,7 @@ pub const Context = struct { }, .watcher = if (options.deviceChangeFn) |deviceChangeFn| .{ .deviceChangeFn = deviceChangeFn, - .userdata = options.userdata, + .user_data = options.user_data, } else null, }; @@ -115,18 +115,18 @@ pub const Context = struct { fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int { var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); - self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + self.watcher.?.deviceChangeFn(self.watcher.?.user_data); return 0; } fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void { var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); - self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + self.watcher.?.deviceChangeFn(self.watcher.?.user_data); } fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void { var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?)); - self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + self.watcher.?.deviceChangeFn(self.watcher.?.user_data); } pub fn devices(self: *Context) []const main.Device { @@ -138,8 +138,6 @@ pub const Context = struct { } pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { - _ = options; - var ports = try self.allocator.alloc(*c.jack_port_t, device.channels.len); var dest_ports = try self.allocator.alloc([:0]const u8, ports.len); var buf: [64]u8 = undefined; @@ -151,36 +149,46 @@ pub const Context = struct { dest_ports[i] = dest_name; } - return .{ - .jack = .{ - .allocator = self.allocator, - .mutex = .{}, - .cond = .{}, - .device = device, - .writeFn = writeFn, - .client = self.client, - .ports = ports, - .dest_ports = dest_ports, - }, + var player = try self.allocator.create(Player); + player.* = .{ + .allocator = self.allocator, + .mutex = .{}, + .client = self.client, + .ports = ports, + .dest_ports = dest_ports, + .device = device, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + .write_step = main.Format.size(.f32), }; + return .{ .jack = player }; } }; pub const Player = struct { allocator: std.mem.Allocator, mutex: std.Thread.Mutex, - cond: std.Thread.Condition, - device: main.Device, - writeFn: main.WriteFn, client: *c.jack_client_t, ports: []const *c.jack_port_t, dest_ports: []const [:0]const u8, + device: main.Device, + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.Channel, + format: main.Format, + write_step: u8, pub fn deinit(self: *Player) void { self.allocator.free(self.ports); for (self.dest_ports) |d| self.allocator.free(d); self.allocator.free(self.dest_ports); + self.allocator.destroy(self); } pub fn start(self: *Player) !void { @@ -198,11 +206,10 @@ pub const Player = struct { fn processCallback(n_frames: c.jack_nframes_t, self_opaque: ?*anyopaque) callconv(.C) c_int { const self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?)); - var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); - for (self.channels()) |*ch, i| { + for (self.channels) |*ch, i| { ch.*.ptr = @ptrCast([*]u8, c.jack_port_get_buffer(self.ports[i], n_frames)); } - self.writeFn(parent, n_frames); + self.writeFn(self.user_data, n_frames); return 0; } @@ -235,28 +242,11 @@ pub const Player = struct { } pub fn setVolume(self: *Player, vol: f32) !void { - _ = self; - _ = vol; - @panic("incompatible backend"); + self.vol = vol; } pub fn volume(self: *Player) !f32 { - _ = self; - @panic("incompatible backend"); - } - - pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { - var ptr = channel.ptr + frame * self.format().size(); - std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; - } - - pub fn channels(self: Player) []main.Channel { - return self.device.channels; - } - - pub fn format(self: Player) main.Format { - _ = self; - return .f32; + return self.vol; } pub fn sampleRate(self: Player) u24 { @@ -271,4 +261,4 @@ pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { test { std.testing.refAllDeclsRecursive(@This()); -} \ No newline at end of file +} diff --git a/libs/sysaudio/src/main.zig b/libs/sysaudio/src/main.zig index 322d0901..c9b71393 100644 --- a/libs/sysaudio/src/main.zig +++ b/libs/sysaudio/src/main.zig @@ -19,7 +19,7 @@ pub const Context = struct { pub const Options = struct { app_name: [:0]const u8 = "Mach Game", deviceChangeFn: ?DeviceChangeFn = null, - userdata: ?*anyopaque = null, + user_data: ?*anyopaque = null, }; data: backends.BackendContext, @@ -89,7 +89,6 @@ pub const Context = struct { std.debug.assert(device.mode == .playback); return .{ - .userdata = options.userdata, .data = switch (self.data) { inline else => |b| try b.createPlayer(device, writeFn, options), }, @@ -99,21 +98,20 @@ pub const Context = struct { // TODO: `*Player` instead `*anyopaque` // https://github.com/ziglang/zig/issues/12325 -pub const WriteFn = *const fn (self: *anyopaque, frame_count_max: usize) void; +pub const WriteFn = *const fn (user_data: ?*anyopaque, frame_count_max: usize) void; pub const Player = struct { pub const Options = struct { format: Format = .f32, sample_rate: u24 = default_sample_rate, - userdata: ?*anyopaque = null, + user_data: ?*anyopaque = null, }; - userdata: ?*anyopaque, data: backends.BackendPlayer, - pub fn deinit(self: *Player) void { + pub fn deinit(self: Player) void { return switch (self.data) { - inline else => |*b| b.deinit(), + inline else => |b| b.deinit(), }; } @@ -123,9 +121,9 @@ pub const Player = struct { SystemResources, }; - pub fn start(self: *Player) StartError!void { + pub fn start(self: Player) StartError!void { return switch (self.data) { - inline else => |*b| b.start(), + inline else => |b| b.start(), }; } @@ -134,9 +132,9 @@ pub const Player = struct { OutOfMemory, }; - pub fn play(self: *Player) PlayError!void { + pub fn play(self: Player) PlayError!void { return switch (self.data) { - inline else => |*b| b.play(), + inline else => |b| b.play(), }; } @@ -145,15 +143,15 @@ pub const Player = struct { OutOfMemory, }; - pub fn pause(self: *Player) PauseError!void { + pub fn pause(self: Player) PauseError!void { return switch (self.data) { - inline else => |*b| b.pause(), + inline else => |b| b.pause(), }; } - pub fn paused(self: *Player) bool { + pub fn paused(self: Player) bool { return switch (self.data) { - inline else => |*b| b.paused(), + inline else => |b| b.paused(), }; } @@ -162,10 +160,10 @@ pub const Player = struct { }; // confidence interval (±) depends on the device - pub fn setVolume(self: *Player, vol: f32) SetVolumeError!void { + pub fn setVolume(self: Player, vol: f32) SetVolumeError!void { std.debug.assert(vol <= 1.0); return switch (self.data) { - inline else => |*b| b.setVolume(vol), + inline else => |b| b.setVolume(vol), }; } @@ -174,187 +172,133 @@ pub const Player = struct { }; // confidence interval (±) depends on the device - pub fn volume(self: *Player) GetVolumeError!f32 { + pub fn volume(self: Player) GetVolumeError!f32 { return switch (self.data) { - inline else => |*b| b.volume(), + inline else => |b| b.volume(), }; } - pub fn writeRaw(self: *Player, channel: Channel, frame: usize, sample: anytype) void { - return switch (self.data) { - inline else => |*b| b.writeRaw(channel, frame, sample), - }; - } - - pub fn writeAll(self: *Player, frame: usize, value: anytype) void { + pub fn writeAll(self: Player, frame: usize, value: anytype) void { for (self.channels()) |ch| self.write(ch, frame, value); } - pub fn write(self: *Player, channel: Channel, frame: usize, sample: anytype) void { + pub fn write(self: Player, channel: Channel, frame: usize, sample: anytype) void { switch (@TypeOf(sample)) { - u8 => self.writeU8(channel, frame, sample), - i16 => self.writeI16(channel, frame, sample), - i24 => self.writeI24(channel, frame, sample), - i32 => self.writeI32(channel, frame, sample), - f32 => self.writeF32(channel, frame, sample), - f64 => self.writeF64(channel, frame, sample), + f32, u8, i8, i16, i24, i32 => {}, else => @compileError( \\invalid sample type. supported types are: - \\u8, i8, i16, i24, i32, f32, f32, f64 + \\u8, i8, i16, i24, i32, f32 ), } - } - pub fn writeU8(self: *Player, channel: Channel, frame: usize, sample: u8) void { + var ptr = channel.ptr + frame * self.writeStep(); switch (self.format()) { - .u8 => self.writeRaw(channel, frame, sample), - .i8 => self.writeRaw(channel, frame, unsignedToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, unsignedToSigned(i16, sample)), - .i24 => self.writeRaw(channel, frame, unsignedToSigned(i24, sample)), + .u8 => std.mem.bytesAsValue(u8, ptr[0..@sizeOf(u8)]).* = switch (@TypeOf(sample)) { + u8 => sample, + i8, i16, i24, i32 => signedToUnsigned(u8, sample), + f32 => floatToUnsigned(u8, sample), + else => unreachable, + }, + .i8 => std.mem.bytesAsValue(i8, ptr[0..@sizeOf(i8)]).* = switch (@TypeOf(sample)) { + i8 => sample, + u8 => unsignedToSigned(i8, sample), + i16, i24, i32 => signedToSigned(i8, sample), + f32 => floatToSigned(i8, sample), + else => unreachable, + }, + .i16 => std.mem.bytesAsValue(i16, ptr[0..@sizeOf(i16)]).* = switch (@TypeOf(sample)) { + i16 => sample, + u8 => unsignedToSigned(i16, sample), + i8, i24, i32 => signedToSigned(i16, sample), + f32 => floatToSigned(i16, sample), + else => unreachable, + }, + .i24 => std.mem.bytesAsValue(i24, ptr[0..@sizeOf(i24)]).* = switch (@TypeOf(sample)) { + i24 => sample, + u8 => unsignedToSigned(i24, sample), + i8, i16, i32 => signedToSigned(i24, sample), + f32 => floatToSigned(i24, sample), + else => unreachable, + }, .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, unsignedToSigned(i32, sample)), - .f32 => self.writeRaw(channel, frame, unsignedToFloat(f32, sample)), - .f64 => self.writeRaw(channel, frame, unsignedToFloat(f64, sample)), + .i32 => std.mem.bytesAsValue(i32, ptr[0..@sizeOf(i32)]).* = switch (@TypeOf(sample)) { + i32 => sample, + u8 => unsignedToSigned(i32, sample), + i8, i16, i24 => signedToSigned(i32, sample), + f32 => floatToSigned(i32, sample), + else => unreachable, + }, + .f32 => std.mem.bytesAsValue(f32, ptr[0..@sizeOf(f32)]).* = switch (@TypeOf(sample)) { + f32 => sample, + u8 => unsignedToFloat(f32, sample), + i8, i16, i24, i32 => signedToFloat(f32, sample), + else => unreachable, + }, } } - pub fn writeI16(self: *Player, channel: Channel, frame: usize, sample: i16) void { - switch (self.format()) { - .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), - .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, sample), - .i24 => self.writeRaw(channel, frame, sample), - .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, sample), - .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), - .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), - } + pub fn sampleRate(self: Player) u24 { + return switch (self.data) { + inline else => |b| b.sampleRate(), + }; } - pub fn writeI24(self: *Player, channel: Channel, frame: usize, sample: i24) void { - switch (self.format()) { - .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), - .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)), - .i24 => self.writeRaw(channel, frame, sample), - .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, sample), - .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), - .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), - } - } - - pub fn writeI32(self: *Player, channel: Channel, frame: usize, sample: i32) void { - switch (self.format()) { - .u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)), - .i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)), - .i24 => self.writeRaw(channel, frame, signedToSigned(i24, sample)), - .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, sample), - .f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)), - .f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)), - } - } - - pub fn writeF32(self: *Player, channel: Channel, frame: usize, sample: f32) void { - switch (self.format()) { - .u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)), - .i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)), - .i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)), - .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)), - .f32 => self.writeRaw(channel, frame, sample), - .f64 => self.writeRaw(channel, frame, sample), - } - } - - pub fn writeF64(self: *Player, channel: Channel, frame: usize, sample: f64) void { - switch (self.format()) { - .u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)), - .i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)), - .i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)), - .i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)), - .i24_4b => @panic("TODO"), - .i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)), - .f32 => self.writeRaw(channel, frame, sample), - .f64 => self.writeRaw(channel, frame, sample), - } - } - - fn unsignedToSigned(comptime T: type, sample: anytype) T { - const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1); - const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample)); - return @intCast(T, sample -% half) << trunc; - } - - fn unsignedToFloat(comptime T: type, sample: anytype) T { - const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; - return (@intToFloat(T, sample) - max_int) * 1.0 / max_int; - } - - fn signedToSigned(comptime T: type, sample: anytype) T { - const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); - return @intCast(T, sample >> trunc); - } - - fn signedToUnsigned(comptime T: type, sample: anytype) T { - const half = 1 << (@bitSizeOf(T) - 1); - const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); - return @intCast(T, (sample >> trunc) + half); - } - - fn signedToFloat(comptime T: type, sample: anytype) T { - const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; - return @intToFloat(T, sample) * 1.0 / max_int; - } - - fn floatToSigned(comptime T: type, sample: f64) T { - return @floatToInt(T, sample * std.math.maxInt(T)); - } - - fn floatToUnsigned(comptime T: type, sample: f64) T { - const half = 1 << @bitSizeOf(T) - 1; - return @floatToInt(T, sample * (half - 1) + half); - } - - // TODO: needs test - // fn f32Toi24_4b(sample: f32) i32 { - // const scaled = sample * std.math.maxInt(i32); - // if (builtin.cpu.arch.endian() == .Little) { - // return @floatToInt(i32, scaled); - // } else { - // var res: [4]u8 = undefined; - // std.mem.writeIntSliceBig(i32, &res, @floatToInt(i32, res)); - // return @bitCast(i32, res); - // } - // } - pub fn channels(self: Player) []Channel { return switch (self.data) { - inline else => |*b| b.channels(), + inline else => |b| b.channels, }; } pub fn format(self: Player) Format { return switch (self.data) { - inline else => |*b| b.format(), + inline else => |b| b.format, }; } - pub fn sampleRate(self: Player) u24 { + pub fn writeStep(self: Player) u8 { return switch (self.data) { - inline else => |*b| b.sampleRate(), + inline else => |b| b.write_step, }; } - - pub fn frameSize(self: Player) u8 { - return self.format().frameSize(self.channels().len); - } }; +fn unsignedToSigned(comptime T: type, sample: anytype) T { + const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1); + const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample)); + return @intCast(T, sample -% half) << trunc; +} + +fn unsignedToFloat(comptime T: type, sample: anytype) T { + const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; + return (@intToFloat(T, sample) - max_int) * 1.0 / max_int; +} + +fn signedToSigned(comptime T: type, sample: anytype) T { + const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); + return @intCast(T, sample >> trunc); +} + +fn signedToUnsigned(comptime T: type, sample: anytype) T { + const half = 1 << (@bitSizeOf(T) - 1); + const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T); + return @intCast(T, (sample >> trunc) + half); +} + +fn signedToFloat(comptime T: type, sample: anytype) T { + const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0; + return @intToFloat(T, sample) * 1.0 / max_int; +} + +fn floatToSigned(comptime T: type, sample: f64) T { + return @floatToInt(T, sample * std.math.maxInt(T)); +} + +fn floatToUnsigned(comptime T: type, sample: f64) T { + const half = 1 << @bitSizeOf(T) - 1; + return @floatToInt(T, sample * (half - 1) + half); +} + pub const Device = struct { id: [:0]const u8, name: [:0]const u8, @@ -421,7 +365,6 @@ pub const Format = enum { i24_4b, i32, f32, - f64, pub fn size(self: Format) u8 { return switch (self) { @@ -429,7 +372,6 @@ pub const Format = enum { .i16 => 2, .i24 => 3, .i24_4b, .i32, .f32 => 4, - .f64 => 8, }; } @@ -439,7 +381,6 @@ pub const Format = enum { .i16 => 2, .i24, .i24_4b => 3, .i32, .f32 => 4, - .f64 => 8, }; } diff --git a/libs/sysaudio/src/pulseaudio.zig b/libs/sysaudio/src/pulseaudio.zig index bb61dd54..a809cb76 100644 --- a/libs/sysaudio/src/pulseaudio.zig +++ b/libs/sysaudio/src/pulseaudio.zig @@ -18,7 +18,7 @@ pub const Context = struct { const Watcher = struct { deviceChangeFn: main.DeviceChangeFn, - userdata: ?*anyopaque, + user_data: ?*anyopaque, }; pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { @@ -44,7 +44,7 @@ pub const Context = struct { .default_source = null, .watcher = if (options.deviceChangeFn) |dcf| .{ .deviceChangeFn = dcf, - .userdata = options.userdata, + .user_data = options.user_data, } else null, }; @@ -97,13 +97,13 @@ pub const Context = struct { return .{ .pulseaudio = self }; } - fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); - self.watcher.?.deviceChangeFn(self.watcher.?.userdata); + fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), user_data.?)); + self.watcher.?.deviceChangeFn(self.watcher.?.user_data); } - fn contextStateOp(ctx: ?*c.pa_context, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + fn contextStateOp(ctx: ?*c.pa_context, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), user_data.?)); self.ctx_state = c.pa_context_get_state(ctx); c.pa_threaded_mainloop_signal(self.main_loop, 0); @@ -159,8 +159,8 @@ pub const Context = struct { } } - fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), user_data.?)); defer c.pa_threaded_mainloop_signal(self.main_loop, 0); self.default_sink = self.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return; @@ -170,8 +170,8 @@ pub const Context = struct { }; } - fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), user_data.?)); if (eol != 0) { c.pa_threaded_mainloop_signal(self.main_loop, 0); return; @@ -180,8 +180,8 @@ pub const Context = struct { self.deviceInfoOp(info, .playback) catch return; } - fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?)); + fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), user_data.?)); if (eol != 0) { c.pa_threaded_mainloop_signal(self.main_loop, 0); return; @@ -274,19 +274,22 @@ pub const Context = struct { } } - return .{ - .pulseaudio = .{ - .main_loop = self.main_loop, - .ctx = self.ctx, - .stream = stream.?, - ._channels = device.channels, - ._format = format, - .sample_rate = sample_rate, - .writeFn = writeFn, - .write_ptr = undefined, - .vol = 1.0, - }, + var player = try self.allocator.create(Player); + player.* = .{ + .allocator = self.allocator, + .main_loop = self.main_loop, + .ctx = self.ctx, + .stream = stream.?, + .write_ptr = undefined, + .vol = 1.0, + .sample_rate = sample_rate, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .write_step = format.frameSize(device.channels.len), }; + return .{ .pulseaudio = player }; } const StreamStatus = struct { @@ -298,8 +301,8 @@ pub const Context = struct { }, }; - fn streamStateOp(stream: ?*c.pa_stream, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*StreamStatus, @alignCast(@alignOf(*StreamStatus), userdata.?)); + fn streamStateOp(stream: ?*c.pa_stream, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*StreamStatus, @alignCast(@alignOf(*StreamStatus), user_data.?)); switch (c.pa_stream_get_state(stream)) { c.PA_STREAM_UNCONNECTED, @@ -320,15 +323,19 @@ pub const Context = struct { }; pub const Player = struct { + allocator: std.mem.Allocator, main_loop: *c.pa_threaded_mainloop, ctx: *c.pa_context, stream: *c.pa_stream, - _channels: []main.Channel, - _format: main.Format, - sample_rate: u24, - writeFn: main.WriteFn, write_ptr: [*]u8, vol: f32, + sample_rate: u24, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.Channel, + format: main.Format, + write_step: u8, pub fn deinit(self: *Player) void { c.pa_threaded_mainloop_lock(self.main_loop); @@ -352,9 +359,8 @@ pub const Player = struct { c.pa_stream_set_write_callback(self.stream, playbackStreamWriteOp, self); } - fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); - var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); + fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), user_data.?)); var frames_left = nbytes; while (frames_left > 0) { @@ -371,12 +377,12 @@ pub const Player = struct { return; } - for (self.channels()) |*ch, i| { - ch.*.ptr = self.write_ptr + self.format().frameSize(i); + for (self.channels) |*ch, i| { + ch.*.ptr = self.write_ptr + self.format.frameSize(i); } - const frames = chunk_size / self.format().frameSize(self.channels().len); - self.writeFn(parent, frames); + const frames = chunk_size / self.format.frameSize(self.channels.len); + self.writeFn(self.user_data, frames); if (c.pa_stream_write(self.stream, self.write_ptr, chunk_size, null, 0, c.PA_SEEK_RELATIVE) != 0) { if (std.debug.runtime_safety) unreachable; @@ -422,7 +428,7 @@ pub const Player = struct { var cvolume: c.pa_cvolume = undefined; _ = c.pa_cvolume_init(&cvolume); - _ = c.pa_cvolume_set(&cvolume, @intCast(c_uint, self.channels().len), c.pa_sw_volume_from_linear(vol)); + _ = c.pa_cvolume_set(&cvolume, @intCast(c_uint, self.channels.len), c.pa_sw_volume_from_linear(vol)); performOperation( self.main_loop, @@ -436,8 +442,8 @@ pub const Player = struct { ); } - fn successOp(_: ?*c.pa_context, success: c_int, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); + fn successOp(_: ?*c.pa_context, success: c_int, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), user_data.?)); if (success == 1) c.pa_threaded_mainloop_signal(self.main_loop, 0); } @@ -459,8 +465,8 @@ pub const Player = struct { return self.vol; } - fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void { - var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?)); + fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), user_data.?)); if (eol != 0) { c.pa_threaded_mainloop_signal(self.main_loop, 0); @@ -470,19 +476,6 @@ pub const Player = struct { self.vol = @intToFloat(f32, info.*.volume.values[0]) / @intToFloat(f32, c.PA_VOLUME_NORM); } - pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void { - var ptr = channel.ptr + self.format().frameSize(self.channels().len) * frame; - std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; - } - - pub fn channels(self: Player) []main.Channel { - return self._channels; - } - - pub fn format(self: Player) main.Format { - return self._format; - } - pub fn sampleRate(self: Player) u24 { return self.sample_rate; } @@ -552,7 +545,7 @@ pub fn toPAFormat(format: main.Format) !c.pa_sample_format_t { .i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE, .f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE, - .f64, .i8 => error.Invalid, + .i8 => error.Invalid, }; } diff --git a/libs/sysaudio/src/wasapi.zig b/libs/sysaudio/src/wasapi.zig index 5c52b724..f171e2ac 100644 --- a/libs/sysaudio/src/wasapi.zig +++ b/libs/sysaudio/src/wasapi.zig @@ -12,7 +12,7 @@ pub const Context = struct { const Watcher = struct { deviceChangeFn: main.DeviceChangeFn, - userdata: ?*anyopaque, + user_data: ?*anyopaque, notif_client: win32.IMMNotificationClient, }; @@ -56,7 +56,7 @@ pub const Context = struct { }, .watcher = if (options.deviceChangeFn) |deviceChangeFn| .{ .deviceChangeFn = deviceChangeFn, - .userdata = options.userdata, + .user_data = options.user_data, .notif_client = win32.IMMNotificationClient{ .vtable = &.{ .base = .{ @@ -108,31 +108,31 @@ pub const Context = struct { fn onDeviceStateChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT { var watcher = @fieldParentPtr(Watcher, "notif_client", self); - watcher.deviceChangeFn(watcher.userdata); + watcher.deviceChangeFn(watcher.user_data); return win32.S_OK; } fn onDeviceAddedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { var watcher = @fieldParentPtr(Watcher, "notif_client", self); - watcher.deviceChangeFn(watcher.userdata); + watcher.deviceChangeFn(watcher.user_data); return win32.S_OK; } fn onDeviceRemovedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { var watcher = @fieldParentPtr(Watcher, "notif_client", self); - watcher.deviceChangeFn(watcher.userdata); + watcher.deviceChangeFn(watcher.user_data); return win32.S_OK; } fn onDefaultDeviceChangedCB(self: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT { var watcher = @fieldParentPtr(Watcher, "notif_client", self); - watcher.deviceChangeFn(watcher.userdata); + watcher.deviceChangeFn(watcher.user_data); return win32.S_OK; } fn onPropertyValueChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT { var watcher = @fieldParentPtr(Watcher, "notif_client", self); - watcher.deviceChangeFn(watcher.userdata); + watcher.deviceChangeFn(watcher.user_data); return win32.S_OK; } @@ -303,17 +303,28 @@ pub const Context = struct { }, .formats = blk: { var audio_client: ?*win32.IAudioClient = null; - hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client)); - switch (hr) { - win32.S_OK => {}, - win32.E_POINTER => unreachable, - win32.E_INVALIDARG => unreachable, - win32.E_NOINTERFACE => unreachable, - win32.E_OUTOFMEMORY => return error.OutOfMemory, - win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable, - else => return error.OpeningDevice, + var audio_client3: ?*win32.IAudioClient3 = null; + hr = imm_device.?.Activate(win32.IID_IAudioClient3, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client3)); + if (hr == win32.S_OK) { + hr = audio_client3.?.QueryInterface(win32.IID_IAudioClient, @ptrCast(?*?*anyopaque, &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, @ptrCast(?*?*anyopaque, &audio_client)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable, + else => return error.OpeningDevice, + } } - defer _ = audio_client.?.Release(); var fmt_arr = std.ArrayList(main.Format).init(self.allocator); var closest_match: ?*win32.WAVEFORMATEX = null; @@ -413,7 +424,7 @@ pub const Context = struct { .u8, .i16, .i24, .i24_4b, .i32 => { wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*; }, - .f32, .f64 => { + .f32 => { wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*; }, .i8 => return error.Invalid, @@ -439,19 +450,31 @@ pub const Context = struct { } var audio_client: ?*win32.IAudioClient = null; - hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client)); - switch (hr) { - win32.S_OK => {}, - win32.E_POINTER => unreachable, - win32.E_INVALIDARG => unreachable, - win32.E_NOINTERFACE => unreachable, - win32.E_OUTOFMEMORY => return error.OutOfMemory, - win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, - else => return error.OpeningDevice, + var audio_client3: ?*win32.IAudioClient3 = null; + hr = imm_device.?.Activate(win32.IID_IAudioClient3, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client3)); + if (hr == win32.S_OK) { + hr = audio_client3.?.QueryInterface(win32.IID_IAudioClient, @ptrCast(?*?*anyopaque, &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, @ptrCast(?*?*anyopaque, &audio_client)); + switch (hr) { + win32.S_OK => {}, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.E_NOINTERFACE => unreachable, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable, + else => return error.OpeningDevice, + } } const format = device.preferredFormat(options.format); - const sample_rate = device.sample_rate.clamp(options.sample_rate); + const sample_rate = device.sample_rate.min; const wave_format = win32.WAVEFORMATEXTENSIBLE{ .Format = .{ @@ -470,36 +493,61 @@ pub const Context = struct { .SubFormat = toSubFormat(format) catch return error.OpeningDevice, }; - hr = audio_client.?.Initialize( - .SHARED, - win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - win32.AUDCLNT_STREAMFLAGS_NOPERSIST, - 0, - 0, - @ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)), - null, - ); - switch (hr) { - win32.S_OK => {}, - win32.E_OUTOFMEMORY => return error.OutOfMemory, - win32.E_POINTER => unreachable, - win32.E_INVALIDARG => unreachable, - win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable, - win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, - win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better - win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice, - win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice, - win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, - win32.AUDCLNT_E_DEVICE_IN_USE => unreachable, - win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice, - win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice, - win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable, - win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable, - win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable, - win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, - else => return error.OpeningDevice, + if (audio_client3) |_| { + hr = audio_client3.?.InitializeSharedAudioStream( + win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + 0, + @ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)), + null, + ); + switch (hr) { + win32.S_OK => {}, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable, + win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, + win32.AUDCLNT_E_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, + @ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)), + null, + ); + switch (hr) { + win32.S_OK => {}, + win32.E_OUTOFMEMORY => return error.OutOfMemory, + win32.E_POINTER => unreachable, + win32.E_INVALIDARG => unreachable, + win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable, + win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable, + win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better + win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice, + win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice, + win32.AUDCLNT_E_DEVICE_IN_USE => unreachable, + win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice, + win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice, + win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable, + win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable, + win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable, + win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice, + else => return error.OpeningDevice, + } } - errdefer _ = audio_client.?.Release(); var render_client: ?*win32.IAudioRenderClient = null; hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @ptrCast(?*?*anyopaque, &render_client)); @@ -539,35 +587,38 @@ pub const Context = struct { else => return error.OpeningDevice, } - return .{ - .wasapi = .{ - .thread = undefined, - .mutex = .{}, - ._channels = device.channels, - ._format = format, - .sample_rate = sample_rate, - .writeFn = writeFn, - .audio_client = audio_client, - .simple_volume = simple_volume, - .imm_device = imm_device, - .render_client = render_client, - .ready_event = ready_event, - .is_paused = false, - .vol = 1.0, - .aborted = .{ .value = false }, - }, + var player = try self.allocator.create(Player); + player.* = .{ + .allocator = self.allocator, + .thread = undefined, + .mutex = .{}, + .audio_client = audio_client, + .audio_client3 = audio_client3, + .simple_volume = simple_volume, + .imm_device = imm_device, + .render_client = render_client, + .ready_event = ready_event, + .aborted = .{ .value = false }, + .is_paused = false, + .sample_rate = sample_rate, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = format, + .write_step = format.frameSize(device.channels.len), }; + return .{ .wasapi = player }; } fn toSubFormat(format: main.Format) !win32.Guid { return switch (format) { - .u8 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, - .i16 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, - .i24 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, - .i24_4b => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, - .i32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, + .u8, + .i16, + .i24, + .i24_4b, + .i32, + => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*, .f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*, - .f64 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*, else => error.Invalid, }; } @@ -599,20 +650,24 @@ pub const Context = struct { }; pub const Player = struct { + allocator: std.mem.Allocator, thread: std.Thread, mutex: std.Thread.Mutex, - _channels: []main.Channel, - _format: main.Format, - sample_rate: u24, - writeFn: main.WriteFn, simple_volume: ?*win32.ISimpleAudioVolume, imm_device: ?*win32.IMMDevice, audio_client: ?*win32.IAudioClient, + audio_client3: ?*win32.IAudioClient3, render_client: ?*win32.IAudioRenderClient, ready_event: win32.HANDLE, aborted: std.atomic.Atomic(bool), is_paused: bool, - vol: f32, + sample_rate: u24, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + write_step: u8, + channels: []main.Channel, + format: main.Format, pub fn deinit(self: *Player) void { self.aborted.store(true, .Unordered); @@ -620,7 +675,9 @@ pub const Player = struct { _ = self.simple_volume.?.Release(); _ = self.render_client.?.Release(); _ = self.audio_client.?.Release(); + _ = self.audio_client3.?.Release(); _ = self.imm_device.?.Release(); + self.allocator.destroy(self); } pub fn start(self: *Player) !void { @@ -635,8 +692,6 @@ pub const Player = struct { } fn writeLoop(self: *Player) void { - var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self)); - var hr = self.audio_client.?.Start(); switch (hr) { win32.S_OK => {}, @@ -689,11 +744,11 @@ pub const Player = struct { else => unreachable, } - for (self.channels()) |*ch, i| { - ch.*.ptr = data + self.format().frameSize(i); + for (self.channels) |*ch, i| { + ch.*.ptr = data + self.format.frameSize(i); } - self.writeFn(parent, frames); + self.writeFn(self.user_data, frames); hr = self.render_client.?.ReleaseBuffer(frames, 0); switch (hr) { @@ -766,20 +821,6 @@ pub const Player = struct { } return vol; } - - pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void { - var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len); - std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample; - } - - pub fn channels(self: Player) []main.Channel { - return self._channels; - } - - pub fn format(self: Player) main.Format { - return self._format; - } - pub fn sampleRate(self: Player) u24 { return self.sample_rate; } diff --git a/libs/sysaudio/src/wasapi/win32.zig b/libs/sysaudio/src/wasapi/win32.zig index 24720bea..59d52ca6 100644 --- a/libs/sysaudio/src/wasapi/win32.zig +++ b/libs/sysaudio/src/wasapi/win32.zig @@ -1272,6 +1272,162 @@ pub const IAudioClient = extern struct { } 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: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient2, + Category: AUDIO_STREAM_CATEGORY, + pbOffloadCapable: ?*BOOL, + ) callconv(WINAPI) HRESULT, + else => *const fn ( + self: *const IAudioClient2, + Category: AUDIO_STREAM_CATEGORY, + pbOffloadCapable: ?*BOOL, + ) callconv(WINAPI) HRESULT, + }, + SetClientProperties: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient2, + pProperties: ?*const AudioClientProperties, + ) callconv(WINAPI) HRESULT, + else => *const fn ( + self: *const IAudioClient2, + pProperties: ?*const AudioClientProperties, + ) callconv(WINAPI) HRESULT, + }, + GetBufferSizeLimits: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient2, + pFormat: ?*const WAVEFORMATEX, + bEventDriven: BOOL, + phnsMinBufferDuration: ?*i64, + phnsMaxBufferDuration: ?*i64, + ) callconv(WINAPI) HRESULT, + else => *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 @ptrCast(*const IAudioClient2.VTable, self.vtable).IsOffloadCapable(@ptrCast(*const IAudioClient2, self), Category, pbOffloadCapable); + } + pub inline fn SetClientProperties(self: *const T, pProperties: ?*const AudioClientProperties) HRESULT { + return @ptrCast(*const IAudioClient2.VTable, self.vtable).SetClientProperties(@ptrCast(*const IAudioClient2, self), pProperties); + } + pub inline fn GetBufferSizeLimits(self: *const T, pFormat: ?*const WAVEFORMATEX, bEventDriven: BOOL, phnsMinBufferDuration: ?*i64, phnsMaxBufferDuration: ?*i64) HRESULT { + return @ptrCast(*const IAudioClient2.VTable, self.vtable).GetBufferSizeLimits(@ptrCast(*const IAudioClient2, 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: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient3, + pFormat: ?*const WAVEFORMATEX, + pDefaultPeriodInFrames: ?*u32, + pFundamentalPeriodInFrames: ?*u32, + pMinPeriodInFrames: ?*u32, + pMaxPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + else => *const fn ( + self: *const IAudioClient3, + pFormat: ?*const WAVEFORMATEX, + pDefaultPeriodInFrames: ?*u32, + pFundamentalPeriodInFrames: ?*u32, + pMinPeriodInFrames: ?*u32, + pMaxPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + }, + GetCurrentSharedModeEnginePeriod: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient3, + ppFormat: ?*?*WAVEFORMATEX, + pCurrentPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + else => *const fn ( + self: *const IAudioClient3, + ppFormat: ?*?*WAVEFORMATEX, + pCurrentPeriodInFrames: ?*u32, + ) callconv(WINAPI) HRESULT, + }, + InitializeSharedAudioStream: switch (@import("builtin").zig_backend) { + .stage1 => fn ( + self: *const IAudioClient3, + StreamFlags: u32, + PeriodInFrames: u32, + pFormat: ?*const WAVEFORMATEX, + AudioSessionGuid: ?*const Guid, + ) callconv(WINAPI) HRESULT, + else => *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 @ptrCast(*const IAudioClient3.VTable, self.vtable).GetSharedModeEnginePeriod(@ptrCast(*const IAudioClient3, self), pFormat, pDefaultPeriodInFrames, pFundamentalPeriodInFrames, pMinPeriodInFrames, pMaxPeriodInFrames); + } + pub inline fn GetCurrentSharedModeEnginePeriod(self: *const T, ppFormat: ?*?*WAVEFORMATEX, pCurrentPeriodInFrames: ?*u32) HRESULT { + return @ptrCast(*const IAudioClient3.VTable, self.vtable).GetCurrentSharedModeEnginePeriod(@ptrCast(*const IAudioClient3, self), ppFormat, pCurrentPeriodInFrames); + } + pub inline fn InitializeSharedAudioStream(self: *const T, StreamFlags: u32, PeriodInFrames: u32, pFormat: ?*const WAVEFORMATEX, AudioSessionGuid: ?*const Guid) HRESULT { + return @ptrCast(*const IAudioClient3.VTable, self.vtable).InitializeSharedAudioStream(@ptrCast(*const IAudioClient3, 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 { diff --git a/libs/sysaudio/src/webaudio.zig b/libs/sysaudio/src/webaudio.zig new file mode 100644 index 00000000..15214cab --- /dev/null +++ b/libs/sysaudio/src/webaudio.zig @@ -0,0 +1,236 @@ +const std = @import("std"); +const js = @import("sysjs"); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +const channel_size = 1024; +const channel_size_bytes = channel_size * @sizeOf(f32); + +const dummy_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.BackendContext { + _ = options; + + const audio_context = js.global().get("AudioContext"); + if (audio_context.is(.undefined)) + return error.ConnectionRefused; + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + }; + + return .{ .webaudio = self }; + } + + pub fn deinit(self: *Context) void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + try self.devices_info.list.append(self.allocator, dummy_playback); + self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 2); + self.devices_info.list.items[0].channels[0] = .{ .id = .front_left }; + self.devices_info.list.items[0].channels[1] = .{ .id = .front_right }; + self.devices_info.setDefault(.playback, 0); + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + const context_options = js.createMap(); + defer context_options.deinit(); + context_options.set("sampleRate", js.createNumber(@intToFloat(f64, options.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(@intToFloat(f64, device.channels.len)), + }).view(.object); + const process_node = audio_context.call("createScriptProcessor", &.{ + js.createNumber(channel_size), + js.createNumber(@intToFloat(f64, device.channels.len)), + }).view(.object); + + var player = try self.allocator.create(Player); + errdefer self.allocator.destroy(player); + + var captures = try self.allocator.alloc(js.Value, 1); + captures[0] = js.createNumber(@intToFloat(f64, @ptrToInt(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 = self.allocator, + .audio_context = audio_context, + .process_node = process_node, + .gain_node = gain_node, + .process_captures = captures, + .resume_on_click = resume_on_click, + .buf = try self.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, + .sample_rate = options.sample_rate, + .channels = device.channels, + .format = .f32, + .write_step = @sizeOf(f32), + }; + + for (player.channels) |*ch, i| { + ch.*.ptr = player.buf.ptr + i * channel_size_bytes; + } + + return .{ .webaudio = player }; + } +}; + +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, + sample_rate: u24, + + channels: []main.Channel, + format: main.Format, + write_step: u8, + + pub fn deinit(self: *Player) void { + self.resume_on_click.deinit(); + self.buf_js.deinit(); + self.gain_node.deinit(); + self.process_node.deinit(); + self.audio_context.deinit(); + self.allocator.free(self.process_captures); + self.allocator.free(self.buf); + self.allocator.destroy(self); + } + + pub fn start(self: Player) !void { + const destination = self.audio_context.get("destination").view(.object); + defer destination.deinit(); + _ = self.gain_node.call("connect", &.{destination.toValue()}); + _ = self.process_node.call("connect", &.{self.gain_node.toValue()}); + } + + fn resumeOnClick(args: js.Object, _: usize, captures: []js.Value) js.Value { + const self = @intToPtr(*Player, @floatToInt(usize, captures[0].view(.num))); + self.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(), self.resume_on_click.toValue() }); + + return js.createUndefined(); + } + + fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value { + const self = @intToPtr(*Player, @floatToInt(usize, 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(); + + self.writeFn(self.user_data, channel_size); + + for (self.channels) |_, i| { + self.buf_js.copyBytes(self.buf[i * channel_size_bytes .. (i + 1) * channel_size_bytes]); + const buf_f32_js = js.constructType("Float32Array", &.{ self.buf_js.get("buffer"), self.buf_js.get("byteOffset"), js.createNumber(channel_size) }); + defer buf_f32_js.deinit(); + _ = output_buffer.call("copyToChannel", &.{ buf_f32_js.toValue(), js.createNumber(@intToFloat(f64, i)) }); + } + + return js.createUndefined(); + } + + pub fn play(self: *Player) !void { + _ = self.audio_context.call("resume", &.{js.createUndefined()}); + self.is_paused = false; + } + + pub fn pause(self: *Player) !void { + _ = self.audio_context.call("suspend", &.{js.createUndefined()}); + self.is_paused = true; + } + + pub fn paused(self: Player) bool { + return self.is_paused; + } + + pub fn setVolume(self: *Player, vol: f32) !void { + const gain = self.gain_node.get("gain").view(.object); + defer gain.deinit(); + gain.set("value", js.createNumber(vol)); + } + + pub fn volume(self: Player) !f32 { + const gain = self.gain_node.get("gain").view(.object); + defer gain.deinit(); + return @floatCast(f32, gain.get("value").view(.num)); + } + + pub fn sampleRate(self: Player) u24 { + return self.sample_rate; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +} + +test { + std.testing.refAllDeclsRecursive(@This()); +}