sysaudio: add webaudio backend

This commit is contained in:
Ali Chraghi 2022-12-25 13:13:00 +03:30 committed by Stephen Gutekanst
parent 052d3a7da8
commit f9b3ac2106
11 changed files with 841 additions and 487 deletions

View file

@ -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", .{});
}

View file

@ -1 +0,0 @@
//

View file

@ -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,15 +410,11 @@ pub const Context = struct {
return error.IncompatibleDevice;
}
return .{
.alsa = .{
var player = try self.allocator.create(Player);
player.* = .{
.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,
@ -426,8 +422,14 @@ pub const Context = struct {
.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,
};
}

View file

@ -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,
dummy: *@import("dummy.zig").Player,
},
},
else => union(enum) {
dummy: *@import("dummy.zig").Player,
},
};

View file

@ -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,
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;
}

View file

@ -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 = .{
var player = try self.allocator.create(Player);
player.* = .{
.allocator = self.allocator,
.mutex = .{},
.cond = .{},
.device = device,
.writeFn = writeFn,
.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 {

View file

@ -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,
};
}

View file

@ -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 = .{
var player = try self.allocator.create(Player);
player.* = .{
.allocator = self.allocator,
.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,
},
.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,
};
}

View file

@ -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,6 +303,17 @@ pub const Context = struct {
},
.formats = blk: {
var audio_client: ?*win32.IAudioClient = null;
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 => {},
@ -313,7 +324,7 @@ pub const Context = struct {
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,6 +450,17 @@ pub const Context = struct {
}
var audio_client: ?*win32.IAudioClient = null;
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 => {},
@ -446,12 +468,13 @@ pub const Context = struct {
win32.E_INVALIDARG => unreachable,
win32.E_NOINTERFACE => unreachable,
win32.E_OUTOFMEMORY => return error.OutOfMemory,
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
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,10 +493,35 @@ pub const Context = struct {
.SubFormat = toSubFormat(format) catch 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 |
win32.AUDCLNT_STREAMFLAGS_NOPERSIST,
win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
0,
0,
@ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)),
@ -499,7 +547,7 @@ pub const Context = struct {
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 = .{
var player = try self.allocator.create(Player);
player.* = .{
.allocator = self.allocator,
.thread = undefined,
.mutex = .{},
._channels = device.channels,
._format = format,
.sample_rate = sample_rate,
.writeFn = writeFn,
.audio_client = audio_client,
.audio_client3 = audio_client3,
.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 },
},
.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;
}

View file

@ -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 {

View file

@ -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());
}