sysaudio: rewrite in zig
removes libsoundio dependency
This commit is contained in:
parent
8aa2c97079
commit
0f3e28bc2a
27 changed files with 4714 additions and 1344 deletions
649
libs/sysaudio/src/alsa.zig
Normal file
649
libs/sysaudio/src/alsa.zig
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("alsa/asoundlib.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
const inotify_event = std.os.linux.inotify_event;
|
||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
thread: std.Thread,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
notify_fd: std.os.fd_t,
|
||||
notify_wd: std.os.fd_t,
|
||||
notify_pipe_fd: [2]std.os.fd_t,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
_ = c.snd_lib_error_set_handler(@ptrCast(c.snd_lib_error_handler_t, &util.doNothing));
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.watcher = blk: {
|
||||
if (options.deviceChangeFn) |deviceChangeFn| {
|
||||
const notify_fd = std.os.inotify_init1(std.os.linux.IN.NONBLOCK) catch |err| switch (err) {
|
||||
error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded,
|
||||
error.SystemResources,
|
||||
=> return error.SystemResources,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
errdefer std.os.close(notify_fd);
|
||||
|
||||
const notify_wd = std.os.inotify_add_watch(
|
||||
notify_fd,
|
||||
"/dev/snd",
|
||||
std.os.linux.IN.CREATE | std.os.linux.IN.DELETE,
|
||||
) catch |err| switch (err) {
|
||||
error.AccessDenied => return error.AccessDenied,
|
||||
error.UserResourceLimitReached,
|
||||
error.NotDir,
|
||||
error.FileNotFound,
|
||||
error.SystemResources,
|
||||
=> return error.SystemResources,
|
||||
error.NameTooLong,
|
||||
error.WatchAlreadyExists,
|
||||
error.Unexpected,
|
||||
=> unreachable,
|
||||
};
|
||||
errdefer std.os.inotify_rm_watch(notify_fd, notify_wd);
|
||||
|
||||
const notify_pipe_fd = std.os.pipe2(std.os.O.NONBLOCK) catch |err| switch (err) {
|
||||
error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
errdefer {
|
||||
std.os.close(notify_pipe_fd[0]);
|
||||
std.os.close(notify_pipe_fd[1]);
|
||||
}
|
||||
|
||||
break :blk .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
.aborted = .{ .value = false },
|
||||
.notify_fd = notify_fd,
|
||||
.notify_wd = notify_wd,
|
||||
.notify_pipe_fd = notify_pipe_fd,
|
||||
.thread = std.Thread.spawn(.{}, deviceEventsLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
},
|
||||
};
|
||||
|
||||
return .{ .alsa = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (self.watcher) |*watcher| {
|
||||
watcher.aborted.store(true, .Unordered);
|
||||
_ = std.os.write(watcher.notify_pipe_fd[1], "a") catch {};
|
||||
watcher.thread.join();
|
||||
|
||||
std.os.close(watcher.notify_pipe_fd[0]);
|
||||
std.os.close(watcher.notify_pipe_fd[1]);
|
||||
std.os.inotify_rm_watch(watcher.notify_fd, watcher.notify_wd);
|
||||
std.os.close(watcher.notify_fd);
|
||||
}
|
||||
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
fn deviceEventsLoop(self: *Context) void {
|
||||
var watcher = self.watcher.?;
|
||||
var scan = false;
|
||||
var last_crash: ?i64 = null;
|
||||
var buf: [2048]u8 = undefined;
|
||||
var fds = [2]std.os.pollfd{
|
||||
.{
|
||||
.fd = watcher.notify_fd,
|
||||
.events = std.os.POLL.IN,
|
||||
.revents = 0,
|
||||
},
|
||||
.{
|
||||
.fd = watcher.notify_pipe_fd[0],
|
||||
.events = std.os.POLL.IN,
|
||||
.revents = 0,
|
||||
},
|
||||
};
|
||||
|
||||
while (!watcher.aborted.load(.Unordered)) {
|
||||
_ = std.os.poll(&fds, -1) catch |err| switch (err) {
|
||||
error.NetworkSubsystemFailed,
|
||||
error.SystemResources,
|
||||
=> {
|
||||
const ts = std.time.milliTimestamp();
|
||||
if (last_crash) |lc| {
|
||||
if (ts - lc < 500) return;
|
||||
}
|
||||
last_crash = ts;
|
||||
continue;
|
||||
},
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
if (watcher.notify_fd & std.os.POLL.IN != 0) {
|
||||
while (true) {
|
||||
const len = std.os.read(watcher.notify_fd, &buf) catch |err| {
|
||||
if (err == error.WouldBlock) break;
|
||||
const ts = std.time.milliTimestamp();
|
||||
if (last_crash) |lc| {
|
||||
if (ts - lc < 500) return;
|
||||
}
|
||||
last_crash = ts;
|
||||
break;
|
||||
};
|
||||
if (len == 0) break;
|
||||
|
||||
var i: usize = 0;
|
||||
var evt: *inotify_event = undefined;
|
||||
while (i < buf.len) : (i += @sizeOf(inotify_event) + evt.len) {
|
||||
evt = @ptrCast(*inotify_event, @alignCast(4, buf[i..]));
|
||||
const evt_name = @ptrCast([*]u8, buf[i..])[@sizeOf(inotify_event) .. @sizeOf(inotify_event) + 8];
|
||||
|
||||
if (evt.mask & std.os.linux.IN.ISDIR != 0 or !std.mem.startsWith(u8, evt_name, "pcm"))
|
||||
continue;
|
||||
|
||||
scan = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scan) {
|
||||
watcher.deviceChangeFn(self.watcher.?.userdata);
|
||||
scan = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
var pcm_info: ?*c.snd_pcm_info_t = null;
|
||||
_ = c.snd_pcm_info_malloc(&pcm_info);
|
||||
defer c.snd_pcm_info_free(pcm_info);
|
||||
|
||||
var card_idx: c_int = -1;
|
||||
if (c.snd_card_next(&card_idx) < 0)
|
||||
return error.SystemResources;
|
||||
|
||||
while (card_idx >= 0) {
|
||||
var card_id_buf: [8]u8 = undefined;
|
||||
const card_id = std.fmt.bufPrintZ(&card_id_buf, "hw:{d}", .{card_idx}) catch break;
|
||||
|
||||
var ctl: ?*c.snd_ctl_t = undefined;
|
||||
_ = switch (-c.snd_ctl_open(&ctl, card_id.ptr, 0)) {
|
||||
0 => {},
|
||||
@enumToInt(std.os.E.NOENT) => break,
|
||||
else => return error.OpeningDevice,
|
||||
};
|
||||
defer _ = c.snd_ctl_close(ctl);
|
||||
|
||||
var dev_idx: c_int = -1;
|
||||
if (c.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0)
|
||||
return error.SystemResources;
|
||||
|
||||
c.snd_pcm_info_set_device(pcm_info, @intCast(c_uint, dev_idx));
|
||||
c.snd_pcm_info_set_subdevice(pcm_info, 0);
|
||||
const name = std.mem.span(c.snd_pcm_info_get_name(pcm_info) orelse continue);
|
||||
|
||||
for (&[_]main.Device.Mode{ .playback, .capture }) |mode| {
|
||||
const snd_stream = modeToStream(mode);
|
||||
c.snd_pcm_info_set_stream(pcm_info, snd_stream);
|
||||
const err = c.snd_ctl_pcm_info(ctl, pcm_info);
|
||||
switch (@intToEnum(std.os.E, -err)) {
|
||||
.SUCCESS => {},
|
||||
.NOENT,
|
||||
.NXIO,
|
||||
.NODEV,
|
||||
=> break,
|
||||
else => return error.SystemResources,
|
||||
}
|
||||
|
||||
var buf: [9]u8 = undefined; // 'hw' + max(card|device) * 2 + ':' + \0
|
||||
const id = std.fmt.bufPrintZ(&buf, "hw:{d},{d}", .{ card_idx, dev_idx }) catch continue;
|
||||
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
if (c.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0)
|
||||
continue;
|
||||
defer _ = c.snd_pcm_close(pcm);
|
||||
|
||||
var params: ?*c.snd_pcm_hw_params_t = null;
|
||||
_ = c.snd_pcm_hw_params_malloc(¶ms);
|
||||
defer c.snd_pcm_hw_params_free(params);
|
||||
if (c.snd_pcm_hw_params_any(pcm, params) < 0)
|
||||
continue;
|
||||
|
||||
if (c.snd_pcm_hw_params_can_pause(params) == 0)
|
||||
continue;
|
||||
|
||||
const device = main.Device{
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
const chmap = c.snd_pcm_query_chmaps(pcm);
|
||||
if (chmap) |_| {
|
||||
defer c.snd_pcm_free_chmaps(chmap);
|
||||
|
||||
if (chmap[0] == null) continue;
|
||||
|
||||
var channels = try self.allocator.alloc(main.Channel, chmap.*.*.map.channels);
|
||||
for (channels) |*ch, i|
|
||||
ch.*.id = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice;
|
||||
break :blk channels;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.formats = blk: {
|
||||
var fmt_mask: ?*c.snd_pcm_format_mask_t = null;
|
||||
_ = c.snd_pcm_format_mask_malloc(&fmt_mask);
|
||||
defer c.snd_pcm_format_mask_free(fmt_mask);
|
||||
c.snd_pcm_format_mask_none(fmt_mask);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE);
|
||||
c.snd_pcm_hw_params_get_format_mask(params, fmt_mask);
|
||||
|
||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
||||
inline for (std.meta.tags(main.Format)) |format| {
|
||||
if (c.snd_pcm_format_mask_test(
|
||||
fmt_mask,
|
||||
toAlsaFormat(format) catch unreachable,
|
||||
) != 0) {
|
||||
try fmt_arr.append(format);
|
||||
}
|
||||
}
|
||||
|
||||
break :blk try fmt_arr.toOwnedSlice();
|
||||
},
|
||||
.sample_rate = blk: {
|
||||
var rate_min: c_uint = 0;
|
||||
var rate_max: c_uint = 0;
|
||||
if (c.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0)
|
||||
continue;
|
||||
if (c.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0)
|
||||
continue;
|
||||
break :blk .{
|
||||
.min = @intCast(u24, rate_min),
|
||||
.max = @intCast(u24, rate_max),
|
||||
};
|
||||
},
|
||||
.id = try self.allocator.dupeZ(u8, id),
|
||||
.name = try self.allocator.dupeZ(u8, name),
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
|
||||
if (self.devices_info.default(mode) == null and dev_idx == 0) {
|
||||
self.devices_info.setDefault(mode, self.devices_info.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (c.snd_card_next(&card_idx) < 0)
|
||||
return error.SystemResources;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
var mixer: ?*c.snd_mixer_t = null;
|
||||
var selem: ?*c.snd_mixer_selem_id_t = null;
|
||||
var mixer_elm: ?*c.snd_mixer_elem_t = null;
|
||||
var period_size: c_ulong = 0;
|
||||
|
||||
if (c.snd_pcm_open(&pcm, device.id.ptr, modeToStream(device.mode), 0) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = c.snd_pcm_close(pcm);
|
||||
{
|
||||
var hw_params: ?*c.snd_pcm_hw_params_t = null;
|
||||
|
||||
if ((c.snd_pcm_set_params(
|
||||
pcm,
|
||||
toAlsaFormat(format) catch unreachable,
|
||||
c.SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
@intCast(c_uint, device.channels.len),
|
||||
sample_rate,
|
||||
1,
|
||||
main.default_latency,
|
||||
)) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = c.snd_pcm_hw_free(pcm);
|
||||
|
||||
if (c.snd_pcm_hw_params_malloc(&hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
defer c.snd_pcm_hw_params_free(hw_params);
|
||||
|
||||
if (c.snd_pcm_hw_params_current(pcm, hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_pcm_hw_params_get_period_size(hw_params, &period_size, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
}
|
||||
|
||||
{
|
||||
var chmap: c.snd_pcm_chmap_t = .{ .channels = @intCast(c_uint, device.channels.len) };
|
||||
|
||||
for (device.channels) |ch, i|
|
||||
chmap.pos()[i] = toCHMAP(ch.id);
|
||||
|
||||
if (c.snd_pcm_set_chmap(pcm, &chmap) < 0)
|
||||
return error.IncompatibleDevice;
|
||||
}
|
||||
|
||||
{
|
||||
if (c.snd_mixer_open(&mixer, 0) < 0)
|
||||
return error.OutOfMemory;
|
||||
|
||||
const card_id = try self.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ','));
|
||||
defer self.allocator.free(card_id);
|
||||
|
||||
if (c.snd_mixer_attach(mixer, card_id.ptr) < 0)
|
||||
return error.IncompatibleDevice;
|
||||
|
||||
if (c.snd_mixer_selem_register(mixer, null, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_mixer_load(mixer) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_mixer_selem_id_malloc(&selem) < 0)
|
||||
return error.OutOfMemory;
|
||||
errdefer c.snd_mixer_selem_id_free(selem);
|
||||
|
||||
c.snd_mixer_selem_id_set_index(selem, 0);
|
||||
c.snd_mixer_selem_id_set_name(selem, "Master");
|
||||
|
||||
mixer_elm = c.snd_mixer_find_selem(mixer, selem) orelse
|
||||
return error.IncompatibleDevice;
|
||||
}
|
||||
|
||||
return .{
|
||||
.alsa = .{
|
||||
.allocator = self.allocator,
|
||||
.thread = undefined,
|
||||
.mutex = .{},
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.aborted = .{ .value = false },
|
||||
.sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)),
|
||||
.period_size = period_size,
|
||||
.pcm = pcm.?,
|
||||
.mixer = mixer.?,
|
||||
.selem = selem.?,
|
||||
.mixer_elm = mixer_elm.?,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: std.Thread,
|
||||
mutex: std.Thread.Mutex,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
sample_buffer: []u8,
|
||||
period_size: c_ulong,
|
||||
pcm: *c.snd_pcm_t,
|
||||
mixer: *c.snd_mixer_t,
|
||||
selem: *c.snd_mixer_selem_id_t,
|
||||
mixer_elm: *c.snd_mixer_elem_t,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.aborted.store(true, .Unordered);
|
||||
self.thread.join();
|
||||
|
||||
_ = c.snd_mixer_close(self.mixer);
|
||||
c.snd_mixer_selem_id_free(self.selem);
|
||||
_ = c.snd_pcm_close(self.pcm);
|
||||
_ = c.snd_pcm_hw_free(self.pcm);
|
||||
|
||||
self.allocator.free(self.sample_buffer);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeLoop(self: *Player) void {
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = self.sample_buffer.ptr + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
while (!self.aborted.load(.Unordered)) {
|
||||
var frames_left = self.period_size;
|
||||
while (frames_left > 0) {
|
||||
self.writeFn(parent, frames_left);
|
||||
const n = c.snd_pcm_writei(self.pcm, self.sample_buffer.ptr, frames_left);
|
||||
if (n < 0) {
|
||||
if (c.snd_pcm_recover(self.pcm, @intCast(c_int, n), 1) < 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
frames_left -= @intCast(c_uint, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: Player) !void {
|
||||
if (c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED) {
|
||||
if (c.snd_pcm_pause(self.pcm, 0) < 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: Player) !void {
|
||||
if (c.snd_pcm_state(self.pcm) != c.SND_PCM_STATE_PAUSED) {
|
||||
if (c.snd_pcm_pause(self.pcm, 1) < 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotSetVolume;
|
||||
|
||||
const dist = @intToFloat(f32, max_vol - min_vol);
|
||||
if (c.snd_mixer_selem_set_playback_volume_all(
|
||||
self.mixer_elm,
|
||||
@floatToInt(c_long, dist * vol) + min_vol,
|
||||
) < 0)
|
||||
return error.CannotSetVolume;
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
var vol: c_long = 0;
|
||||
var channel: c_int = 0;
|
||||
|
||||
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
|
||||
if (c.snd_mixer_selem_has_playback_channel(self.mixer_elm, channel) == 1) {
|
||||
if (c.snd_mixer_selem_get_playback_volume(self.mixer_elm, channel, &vol) == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == c.SND_MIXER_SCHN_LAST)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
return @intToFloat(f32, vol) / @intToFloat(f32, max_vol - min_vol);
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len);
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.name);
|
||||
allocator.free(device.formats);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
pub fn modeToStream(mode: main.Device.Mode) c_uint {
|
||||
return switch (mode) {
|
||||
.playback => c.SND_PCM_STREAM_PLAYBACK,
|
||||
.capture => c.SND_PCM_STREAM_CAPTURE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toAlsaFormat(format: main.Format) !c.snd_pcm_format_t {
|
||||
return switch (format) {
|
||||
.u8 => c.SND_PCM_FORMAT_U8,
|
||||
.i8 => c.SND_PCM_FORMAT_S8,
|
||||
.i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE,
|
||||
.i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE,
|
||||
.i24_4b => if (is_little) c.SND_PCM_FORMAT_S24_LE else c.SND_PCM_FORMAT_S24_BE,
|
||||
.i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE,
|
||||
.f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE,
|
||||
.f64 => if (is_little) c.SND_PCM_FORMAT_FLOAT64_LE else c.SND_PCM_FORMAT_FLOAT64_BE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromAlsaChannel(pos: c_uint) !main.Channel.Id {
|
||||
return switch (pos) {
|
||||
c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid,
|
||||
c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center,
|
||||
c.SND_CHMAP_FL => .front_left,
|
||||
c.SND_CHMAP_FR => .front_right,
|
||||
c.SND_CHMAP_LFE => .lfe,
|
||||
c.SND_CHMAP_SL => .side_left,
|
||||
c.SND_CHMAP_SR => .side_right,
|
||||
c.SND_CHMAP_RC => .back_center,
|
||||
c.SND_CHMAP_FLC => .front_left_center,
|
||||
c.SND_CHMAP_FRC => .front_right_center,
|
||||
c.SND_CHMAP_TC => .top_center,
|
||||
c.SND_CHMAP_TFL => .top_front_left,
|
||||
c.SND_CHMAP_TFR => .top_front_right,
|
||||
c.SND_CHMAP_TFC => .top_front_center,
|
||||
c.SND_CHMAP_TRL => .top_back_left,
|
||||
c.SND_CHMAP_TRR => .top_back_right,
|
||||
c.SND_CHMAP_TRC => .top_back_center,
|
||||
|
||||
else => return error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toCHMAP(pos: main.Channel.Id) c_uint {
|
||||
return switch (pos) {
|
||||
.front_center => c.SND_CHMAP_FC,
|
||||
.front_left => c.SND_CHMAP_FL,
|
||||
.front_right => c.SND_CHMAP_FR,
|
||||
.lfe => c.SND_CHMAP_LFE,
|
||||
.side_left => c.SND_CHMAP_SL,
|
||||
.side_right => c.SND_CHMAP_SR,
|
||||
.back_center => c.SND_CHMAP_RC,
|
||||
.front_left_center => c.SND_CHMAP_FLC,
|
||||
.front_right_center => c.SND_CHMAP_FRC,
|
||||
.top_center => c.SND_CHMAP_TC,
|
||||
.top_front_left => c.SND_CHMAP_TFL,
|
||||
.top_front_right => c.SND_CHMAP_TFR,
|
||||
.top_front_center => c.SND_CHMAP_TFC,
|
||||
.top_back_left => c.SND_CHMAP_TRL,
|
||||
.top_back_right => c.SND_CHMAP_TRR,
|
||||
.top_back_center => c.SND_CHMAP_TRC,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
48
libs/sysaudio/src/backends.zig
Normal file
48
libs/sysaudio/src/backends.zig
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
|
||||
pub const Backend = std.meta.Tag(BackendContext);
|
||||
pub const BackendContext = switch (builtin.os.tag) {
|
||||
.linux => union(enum) {
|
||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
||||
alsa: *@import("alsa.zig").Context,
|
||||
jack: *@import("jack.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.windows => union(enum) {
|
||||
wasapi: *@import("wasapi.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
else => union(enum) {
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
};
|
||||
pub const BackendPlayer = switch (builtin.os.tag) {
|
||||
.linux => union(enum) {
|
||||
pulseaudio: @import("pulseaudio.zig").Player,
|
||||
alsa: @import("alsa.zig").Player,
|
||||
jack: @import("jack.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
||||
pulseaudio: @import("pulseaudio.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.windows => union(enum) {
|
||||
wasapi: @import("wasapi.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
else => union(enum) {
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
};
|
||||
158
libs/sysaudio/src/dummy.zig
Normal file
158
libs/sysaudio/src/dummy.zig
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const min_sample_rate = 8_000; // Hz
|
||||
pub const max_sample_rate = 5_644_800; // Hz
|
||||
|
||||
const dummy_playback = main.Device{
|
||||
.id = "dummy-playback",
|
||||
.name = "Dummy Device",
|
||||
.mode = .playback,
|
||||
.channels = undefined,
|
||||
.formats = std.meta.tags(main.Format),
|
||||
.sample_rate = .{
|
||||
.min = min_sample_rate,
|
||||
.max = max_sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
const dummy_capture = main.Device{
|
||||
.id = "dummy-capture",
|
||||
.name = "Dummy Device",
|
||||
.mode = .capture,
|
||||
.channels = undefined,
|
||||
.formats = std.meta.tags(main.Format),
|
||||
.sample_rate = .{
|
||||
.min = min_sample_rate,
|
||||
.max = max_sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
_ = options;
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, dummy_playback);
|
||||
try self.devices_info.list.append(self.allocator, dummy_capture);
|
||||
self.devices_info.list.items[0].channels = try allocator.alloc(main.Channel, 1);
|
||||
self.devices_info.list.items[0].channels[0] = .{
|
||||
.id = .front_center,
|
||||
};
|
||||
self.devices_info.list.items[1].channels = try allocator.alloc(main.Channel, 1);
|
||||
self.devices_info.list.items[1].channels[0] = .{
|
||||
.id = .front_center,
|
||||
};
|
||||
self.devices_info.setDefault(.playback, 0);
|
||||
self.devices_info.setDefault(.capture, 1);
|
||||
|
||||
return .{ .dummy = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
_ = self;
|
||||
_ = writeFn;
|
||||
return .{
|
||||
.dummy = .{
|
||||
._channels = device.channels,
|
||||
._format = options.format,
|
||||
.sample_rate = options.sample_rate,
|
||||
.is_paused = false,
|
||||
.vol = 1.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
is_paused: bool,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: Player) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn start(self: Player) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
self.is_paused = false;
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
self.is_paused = true;
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return self.is_paused;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
self.vol = vol;
|
||||
}
|
||||
|
||||
pub fn volume(self: Player) !f32 {
|
||||
return self.vol;
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
_ = self;
|
||||
_ = channel;
|
||||
_ = frame;
|
||||
_ = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
274
libs/sysaudio/src/jack.zig
Normal file
274
libs/sysaudio/src/jack.zig
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("jack/jack.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
client: *c.jack_client_t,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
c.jack_set_error_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing));
|
||||
c.jack_set_info_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing));
|
||||
|
||||
var status: c.jack_status_t = 0;
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.client = c.jack_client_open(options.app_name.ptr, c.JackNoStartServer, &status) orelse {
|
||||
std.debug.assert(status & c.JackInvalidOption == 0);
|
||||
return if (status & c.JackShmFailure != 0)
|
||||
error.SystemResources
|
||||
else
|
||||
error.ConnectionRefused;
|
||||
},
|
||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (options.deviceChangeFn) |_| {
|
||||
if (c.jack_set_sample_rate_callback(self.client, sampleRateCallback, self) != 0 or
|
||||
c.jack_set_port_registration_callback(self.client, portRegistrationCallback, self) != 0 or
|
||||
c.jack_set_port_rename_callback(self.client, portRenameCalllback, self) != 0)
|
||||
return error.ConnectionRefused;
|
||||
}
|
||||
|
||||
return .{ .jack = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
for (self.devices_info.list.items) |device|
|
||||
freeDevice(self.allocator, device);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
const sample_rate = @intCast(u24, c.jack_get_sample_rate(self.client));
|
||||
|
||||
const port_names = c.jack_get_ports(self.client, null, null, 0) orelse
|
||||
return error.OutOfMemory;
|
||||
defer c.jack_free(@ptrCast(?*anyopaque, port_names));
|
||||
|
||||
var i: usize = 0;
|
||||
outer: while (port_names[i] != null) : (i += 1) {
|
||||
const port = c.jack_port_by_name(self.client, port_names[i]) orelse break;
|
||||
const port_type = c.jack_port_type(port)[0..@intCast(usize, c.jack_port_type_size())];
|
||||
if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE))
|
||||
continue;
|
||||
|
||||
const flags = c.jack_port_flags(port);
|
||||
const mode: main.Device.Mode = if (flags & c.JackPortIsInput != 0) .capture else .playback;
|
||||
|
||||
const name = std.mem.span(port_names[i]);
|
||||
const id = std.mem.sliceTo(name, ':');
|
||||
|
||||
for (self.devices_info.list.items) |*dev| {
|
||||
if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) {
|
||||
const new_ch = main.Channel{
|
||||
.id = @intToEnum(main.Channel.Id, dev.channels.len),
|
||||
};
|
||||
dev.channels = try self.allocator.realloc(dev.channels, dev.channels.len + 1);
|
||||
dev.channels[dev.channels.len - 1] = new_ch;
|
||||
break :outer;
|
||||
}
|
||||
}
|
||||
|
||||
var device = main.Device{
|
||||
.id = try self.allocator.dupeZ(u8, id),
|
||||
.name = name,
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
var channels = try self.allocator.alloc(main.Channel, 1);
|
||||
channels[0] = .{ .id = @intToEnum(main.Channel.Id, 0) };
|
||||
break :blk channels;
|
||||
},
|
||||
.formats = &.{.f32},
|
||||
.sample_rate = .{
|
||||
.min = sample_rate,
|
||||
.max = sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
if (std.mem.eql(u8, "system", id)) {
|
||||
self.devices_info.setDefault(device.mode, self.devices_info.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
pub fn devices(self: *Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: *Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
_ = options;
|
||||
|
||||
var ports = try self.allocator.alloc(*c.jack_port_t, device.channels.len);
|
||||
var dest_ports = try self.allocator.alloc([:0]const u8, ports.len);
|
||||
var buf: [64]u8 = undefined;
|
||||
for (device.channels) |_, i| {
|
||||
const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable;
|
||||
const dest_name = try std.fmt.allocPrintZ(self.allocator, "{s}:{s}", .{ device.id, port_name });
|
||||
ports[i] = c.jack_port_register(self.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse
|
||||
return error.OpeningDevice;
|
||||
dest_ports[i] = dest_name;
|
||||
}
|
||||
|
||||
return .{
|
||||
.jack = .{
|
||||
.allocator = self.allocator,
|
||||
.mutex = .{},
|
||||
.cond = .{},
|
||||
.device = device,
|
||||
.writeFn = writeFn,
|
||||
.client = self.client,
|
||||
.ports = ports,
|
||||
.dest_ports = dest_ports,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
mutex: std.Thread.Mutex,
|
||||
cond: std.Thread.Condition,
|
||||
device: main.Device,
|
||||
writeFn: main.WriteFn,
|
||||
client: *c.jack_client_t,
|
||||
ports: []const *c.jack_port_t,
|
||||
dest_ports: []const [:0]const u8,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.allocator.free(self.ports);
|
||||
for (self.dest_ports) |d|
|
||||
self.allocator.free(d);
|
||||
self.allocator.free(self.dest_ports);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
if (c.jack_set_process_callback(self.client, processCallback, self) != 0)
|
||||
return error.CannotPlay;
|
||||
|
||||
if (c.jack_activate(self.client) != 0)
|
||||
return error.CannotPlay;
|
||||
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
fn processCallback(n_frames: c.jack_nframes_t, self_opaque: ?*anyopaque) callconv(.C) c_int {
|
||||
const self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?));
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = @ptrCast([*]u8, c.jack_port_get_buffer(self.ports[i], n_frames));
|
||||
}
|
||||
self.writeFn(parent, n_frames);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_disconnect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_port_connected_to(port, self.dest_ports[i].ptr) == 1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
_ = self;
|
||||
_ = vol;
|
||||
@panic("incompatible backend");
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
_ = self;
|
||||
@panic("incompatible backend");
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().size();
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self.device.channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
_ = self;
|
||||
return .f32;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return @intCast(u24, c.jack_get_sample_rate(self.client));
|
||||
}
|
||||
};
|
||||
|
||||
pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
@ -1,134 +1,461 @@
|
|||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const testing = std.testing;
|
||||
const builtin = @import("builtin");
|
||||
const Backend = if (builtin.cpu.arch == .wasm32) @import("webaudio.zig") else switch (builtin.os.tag) {
|
||||
.linux,
|
||||
.windows,
|
||||
.macos,
|
||||
.ios,
|
||||
=> @import("soundio.zig"),
|
||||
else => @compileError("unsupported os"),
|
||||
};
|
||||
pub const Error = Backend.Error;
|
||||
pub const Device = Backend.Device;
|
||||
pub const DeviceIterator = Backend.DeviceIterator;
|
||||
const std = @import("std");
|
||||
const util = @import("util.zig");
|
||||
const backends = @import("backends.zig");
|
||||
|
||||
pub const DataCallback = *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
|
||||
pub const default_sample_rate = 44_100; // Hz
|
||||
pub const default_latency = 500 * std.time.us_per_ms; // μs
|
||||
|
||||
pub const Mode = enum {
|
||||
input,
|
||||
output,
|
||||
pub const Backend = backends.Backend;
|
||||
pub const DeviceChangeFn = *const fn (self: ?*anyopaque) void;
|
||||
pub const ConnectError = error{
|
||||
OutOfMemory,
|
||||
AccessDenied,
|
||||
SystemResources,
|
||||
ConnectionRefused,
|
||||
};
|
||||
|
||||
pub const Format = enum {
|
||||
U8,
|
||||
S16,
|
||||
S24,
|
||||
S32,
|
||||
F32,
|
||||
};
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
backend: Backend,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
return Audio{
|
||||
.backend = try Backend.init(),
|
||||
pub const Context = struct {
|
||||
pub const Options = struct {
|
||||
app_name: [:0]const u8 = "Mach Game",
|
||||
deviceChangeFn: ?DeviceChangeFn = null,
|
||||
userdata: ?*anyopaque = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Audio) void {
|
||||
self.backend.deinit();
|
||||
}
|
||||
data: backends.BackendContext,
|
||||
|
||||
pub fn waitEvents(self: Audio) void {
|
||||
self.backend.waitEvents();
|
||||
}
|
||||
pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) ConnectError!Context {
|
||||
var data: backends.BackendContext = blk: {
|
||||
if (backend) |b| {
|
||||
break :blk try @typeInfo(
|
||||
std.meta.fieldInfo(backends.BackendContext, b).field_type,
|
||||
).Pointer.child.init(allocator, options);
|
||||
} else {
|
||||
inline for (std.meta.fields(Backend)) |b, i| {
|
||||
if (@typeInfo(
|
||||
std.meta.fieldInfo(backends.BackendContext, @intToEnum(Backend, b.value)).field_type,
|
||||
).Pointer.child.init(allocator, options)) |d| {
|
||||
break :blk d;
|
||||
} else |err| {
|
||||
if (i == std.meta.fields(Backend).len - 1)
|
||||
return err;
|
||||
}
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: Device.Options) Error!*Device {
|
||||
return self.backend.requestDevice(allocator, config);
|
||||
}
|
||||
return .{ .data = data };
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return self.backend.inputDeviceIterator();
|
||||
}
|
||||
pub fn deinit(self: Context) void {
|
||||
switch (self.data) {
|
||||
inline else => |b| b.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return self.backend.outputDeviceIterator();
|
||||
}
|
||||
pub const RefreshError = error{
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
OpeningDevice,
|
||||
};
|
||||
|
||||
test "list devices" {
|
||||
const a = try init();
|
||||
defer a.deinit();
|
||||
pub fn refresh(self: Context) RefreshError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.refresh(),
|
||||
};
|
||||
}
|
||||
|
||||
var iter = a.inputDeviceIterator();
|
||||
while (try iter.next()) |_| {}
|
||||
}
|
||||
pub fn devices(self: Context) []const Device {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.devices(),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "connect to device" {
|
||||
return error.SkipZigTest;
|
||||
pub fn defaultDevice(self: Context, mode: Device.Mode) ?Device {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.defaultDevice(mode),
|
||||
};
|
||||
}
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
pub const CreateStreamError = error{
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
OpeningDevice,
|
||||
IncompatibleDevice,
|
||||
};
|
||||
|
||||
// const d = try a.requestDevice(std.testing.allocator, .{ .mode = .output });
|
||||
// defer d.deinit(std.testing.allocator);
|
||||
}
|
||||
pub fn createPlayer(self: Context, device: Device, writeFn: WriteFn, options: Player.Options) CreateStreamError!Player {
|
||||
std.debug.assert(device.mode == .playback);
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "connect to device from descriptor" {
|
||||
return error.SkipZigTest;
|
||||
return .{
|
||||
.userdata = options.userdata,
|
||||
.data = switch (self.data) {
|
||||
inline else => |b| try b.createPlayer(device, writeFn, options),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
// TODO: `*Player` instead `*anyopaque`
|
||||
// https://github.com/ziglang/zig/issues/12325
|
||||
pub const WriteFn = *const fn (self: *anyopaque, frame_count_max: usize) void;
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// while (try iter.next()) |desc| {
|
||||
// if (mem.eql(u8, desc.name orelse "", "default")) {
|
||||
// const d = try a.requestDevice(std.testing.allocator, desc);
|
||||
// defer d.deinit(std.testing.allocator);
|
||||
// return;
|
||||
pub const Player = struct {
|
||||
pub const Options = struct {
|
||||
format: Format = .f32,
|
||||
sample_rate: u24 = default_sample_rate,
|
||||
userdata: ?*anyopaque = null,
|
||||
};
|
||||
|
||||
userdata: ?*anyopaque,
|
||||
data: backends.BackendPlayer,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.deinit(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const StartError = error{
|
||||
CannotPlay,
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
};
|
||||
|
||||
pub fn start(self: *Player) StartError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.start(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const PlayError = error{
|
||||
CannotPlay,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn play(self: *Player) PlayError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.play(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const PauseError = error{
|
||||
CannotPause,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn pause(self: *Player) PauseError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.pause(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.paused(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const SetVolumeError = error{
|
||||
CannotSetVolume,
|
||||
};
|
||||
|
||||
// confidence interval (±) depends on the device
|
||||
pub fn setVolume(self: *Player, vol: f32) SetVolumeError!void {
|
||||
std.debug.assert(vol <= 1.0);
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.setVolume(vol),
|
||||
};
|
||||
}
|
||||
|
||||
pub const GetVolumeError = error{
|
||||
CannotGetVolume,
|
||||
};
|
||||
|
||||
// confidence interval (±) depends on the device
|
||||
pub fn volume(self: *Player) GetVolumeError!f32 {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.volume(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.writeRaw(channel, frame, sample),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeAll(self: *Player, frame: usize, value: anytype) void {
|
||||
for (self.channels()) |ch|
|
||||
self.write(ch, frame, value);
|
||||
}
|
||||
|
||||
pub fn write(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
|
||||
switch (@TypeOf(sample)) {
|
||||
u8 => self.writeU8(channel, frame, sample),
|
||||
i16 => self.writeI16(channel, frame, sample),
|
||||
i24 => self.writeI24(channel, frame, sample),
|
||||
i32 => self.writeI32(channel, frame, sample),
|
||||
f32 => self.writeF32(channel, frame, sample),
|
||||
f64 => self.writeF64(channel, frame, sample),
|
||||
else => @compileError(
|
||||
\\invalid sample type. supported types are:
|
||||
\\u8, i8, i16, i24, i32, f32, f32, f64
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeU8(self: *Player, channel: Channel, frame: usize, sample: u8) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, sample),
|
||||
.i8 => self.writeRaw(channel, frame, unsignedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, unsignedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, unsignedToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, unsignedToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, unsignedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, unsignedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI16(self: *Player, channel: Channel, frame: usize, sample: i16) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, sample),
|
||||
.i24 => self.writeRaw(channel, frame, sample),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI24(self: *Player, channel: Channel, frame: usize, sample: i24) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, sample),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI32(self: *Player, channel: Channel, frame: usize, sample: i32) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, signedToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeF32(self: *Player, channel: Channel, frame: usize, sample: f32) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, sample),
|
||||
.f64 => self.writeRaw(channel, frame, sample),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeF64(self: *Player, channel: Channel, frame: usize, sample: f64) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, sample),
|
||||
.f64 => self.writeRaw(channel, frame, sample),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsignedToSigned(comptime T: type, sample: anytype) T {
|
||||
const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1);
|
||||
const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample));
|
||||
return @intCast(T, sample -% half) << trunc;
|
||||
}
|
||||
|
||||
fn unsignedToFloat(comptime T: type, sample: anytype) T {
|
||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
||||
return (@intToFloat(T, sample) - max_int) * 1.0 / max_int;
|
||||
}
|
||||
|
||||
fn signedToSigned(comptime T: type, sample: anytype) T {
|
||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
||||
return @intCast(T, sample >> trunc);
|
||||
}
|
||||
|
||||
fn signedToUnsigned(comptime T: type, sample: anytype) T {
|
||||
const half = 1 << (@bitSizeOf(T) - 1);
|
||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
||||
return @intCast(T, (sample >> trunc) + half);
|
||||
}
|
||||
|
||||
fn signedToFloat(comptime T: type, sample: anytype) T {
|
||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
||||
return @intToFloat(T, sample) * 1.0 / max_int;
|
||||
}
|
||||
|
||||
fn floatToSigned(comptime T: type, sample: f64) T {
|
||||
return @floatToInt(T, sample * std.math.maxInt(T));
|
||||
}
|
||||
|
||||
fn floatToUnsigned(comptime T: type, sample: f64) T {
|
||||
const half = 1 << @bitSizeOf(T) - 1;
|
||||
return @floatToInt(T, sample * (half - 1) + half);
|
||||
}
|
||||
|
||||
// TODO: needs test
|
||||
// fn f32Toi24_4b(sample: f32) i32 {
|
||||
// const scaled = sample * std.math.maxInt(i32);
|
||||
// if (builtin.cpu.arch.endian() == .Little) {
|
||||
// return @floatToInt(i32, scaled);
|
||||
// } else {
|
||||
// var res: [4]u8 = undefined;
|
||||
// std.mem.writeIntSliceBig(i32, &res, @floatToInt(i32, res));
|
||||
// return @bitCast(i32, res);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return error.SkipZigTest;
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "requestDevice behavior: null is_raw" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
|
||||
|
||||
// const bad_conf = Device.Options{
|
||||
// .is_raw = null,
|
||||
// .mode = device_conf.mode,
|
||||
// .id = device_conf.id,
|
||||
// };
|
||||
// try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_conf));
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "requestDevice behavior: invalid id" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
|
||||
|
||||
// const bad_conf = Device.Options{
|
||||
// .is_raw = device_conf.is_raw,
|
||||
// .mode = device_conf.mode,
|
||||
// .id = "wrong-id",
|
||||
// };
|
||||
// try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf));
|
||||
pub fn channels(self: Player) []Channel {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.channels(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Player) Format {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.format(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.sampleRate(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn frameSize(self: Player) u8 {
|
||||
return self.format().frameSize(self.channels().len);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Device = struct {
|
||||
id: [:0]const u8,
|
||||
name: [:0]const u8,
|
||||
mode: Mode,
|
||||
channels: []Channel,
|
||||
formats: []const Format,
|
||||
sample_rate: util.Range(u24),
|
||||
|
||||
pub const Mode = enum {
|
||||
playback,
|
||||
capture,
|
||||
};
|
||||
|
||||
pub fn preferredFormat(self: Device, format: ?Format) Format {
|
||||
if (format) |f| {
|
||||
for (self.formats) |fmt| {
|
||||
if (f == fmt) {
|
||||
return fmt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var best: Format = self.formats[0];
|
||||
for (self.formats) |fmt| {
|
||||
if (fmt.size() >= best.size()) {
|
||||
if (fmt == .i24_4b and best == .i24)
|
||||
continue;
|
||||
best = fmt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Channel = struct {
|
||||
ptr: [*]u8 = undefined,
|
||||
id: Id,
|
||||
|
||||
pub const Id = enum {
|
||||
front_center,
|
||||
front_left,
|
||||
front_right,
|
||||
front_left_center,
|
||||
front_right_center,
|
||||
back_center,
|
||||
side_left,
|
||||
side_right,
|
||||
top_center,
|
||||
top_front_center,
|
||||
top_front_left,
|
||||
top_front_right,
|
||||
top_back_center,
|
||||
top_back_left,
|
||||
top_back_right,
|
||||
lfe,
|
||||
};
|
||||
};
|
||||
|
||||
pub const Format = enum {
|
||||
u8,
|
||||
i8,
|
||||
i16,
|
||||
i24,
|
||||
i24_4b,
|
||||
i32,
|
||||
f32,
|
||||
f64,
|
||||
|
||||
pub fn size(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.u8, .i8 => 1,
|
||||
.i16 => 2,
|
||||
.i24 => 3,
|
||||
.i24_4b, .i32, .f32 => 4,
|
||||
.f64 => 8,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn validSize(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.u8, .i8 => 1,
|
||||
.i16 => 2,
|
||||
.i24, .i24_4b => 3,
|
||||
.i32, .f32 => 4,
|
||||
.f64 => 8,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn sizeBits(self: Format) u8 {
|
||||
return self.size() * 8;
|
||||
}
|
||||
|
||||
pub fn validSizeBits(self: Format) u8 {
|
||||
return self.validSize() * 8;
|
||||
}
|
||||
|
||||
pub fn frameSize(self: Format, ch_count: usize) u8 {
|
||||
return self.size() * @intCast(u5, ch_count);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
|
|||
590
libs/sysaudio/src/pulseaudio.zig
Normal file
590
libs/sysaudio/src/pulseaudio.zig
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("pulse/pulseaudio.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
app_name: [:0]const u8,
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
ctx: *c.pa_context,
|
||||
ctx_state: c.pa_context_state_t,
|
||||
default_sink: ?[:0]const u8,
|
||||
default_source: ?[:0]const u8,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
const main_loop = c.pa_threaded_mainloop_new() orelse
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_threaded_mainloop_free(main_loop);
|
||||
var main_loop_api = c.pa_threaded_mainloop_get_api(main_loop);
|
||||
|
||||
const ctx = c.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_context_unref(ctx);
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = Context{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.app_name = options.app_name,
|
||||
.main_loop = main_loop,
|
||||
.ctx = ctx,
|
||||
.ctx_state = c.PA_CONTEXT_UNCONNECTED,
|
||||
.default_sink = null,
|
||||
.default_source = null,
|
||||
.watcher = if (options.deviceChangeFn) |dcf| .{
|
||||
.deviceChangeFn = dcf,
|
||||
.userdata = options.userdata,
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (c.pa_context_connect(ctx, null, 0, null) != 0)
|
||||
return error.ConnectionRefused;
|
||||
errdefer c.pa_context_disconnect(ctx);
|
||||
c.pa_context_set_state_callback(ctx, contextStateOp, self);
|
||||
|
||||
if (c.pa_threaded_mainloop_start(main_loop) != 0)
|
||||
return error.SystemResources;
|
||||
errdefer c.pa_threaded_mainloop_stop(main_loop);
|
||||
|
||||
c.pa_threaded_mainloop_lock(main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(main_loop);
|
||||
|
||||
while (true) {
|
||||
switch (self.ctx_state) {
|
||||
// The context hasn't been connected yet.
|
||||
c.PA_CONTEXT_UNCONNECTED,
|
||||
// A connection is being established.
|
||||
c.PA_CONTEXT_CONNECTING,
|
||||
// The client is authorizing itself to the daemon.
|
||||
c.PA_CONTEXT_AUTHORIZING,
|
||||
// The client is passing its application name to the daemon.
|
||||
c.PA_CONTEXT_SETTING_NAME,
|
||||
=> c.pa_threaded_mainloop_wait(main_loop),
|
||||
|
||||
// The connection is established, the context is ready to execute operations.
|
||||
c.PA_CONTEXT_READY => break,
|
||||
|
||||
// The connection was terminated cleanly.
|
||||
c.PA_CONTEXT_TERMINATED,
|
||||
// The connection failed or was disconnected.
|
||||
c.PA_CONTEXT_FAILED,
|
||||
=> return error.ConnectionRefused,
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// subscribe to events
|
||||
if (options.deviceChangeFn != null) {
|
||||
c.pa_context_set_subscribe_callback(ctx, subscribeOp, self);
|
||||
const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE;
|
||||
const subscribe_op = c.pa_context_subscribe(ctx, events, null, self) orelse
|
||||
return error.OutOfMemory;
|
||||
c.pa_operation_unref(subscribe_op);
|
||||
}
|
||||
|
||||
return .{ .pulseaudio = self };
|
||||
}
|
||||
|
||||
fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
fn contextStateOp(ctx: ?*c.pa_context, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
|
||||
self.ctx_state = c.pa_context_get_state(ctx);
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
c.pa_context_set_subscribe_callback(self.ctx, null, null);
|
||||
c.pa_context_set_state_callback(self.ctx, null, null);
|
||||
c.pa_context_disconnect(self.ctx);
|
||||
c.pa_context_unref(self.ctx);
|
||||
c.pa_threaded_mainloop_stop(self.main_loop);
|
||||
c.pa_threaded_mainloop_free(self.main_loop);
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
const list_sink_op = c.pa_context_get_sink_info_list(self.ctx, sinkInfoOp, self);
|
||||
const list_source_op = c.pa_context_get_source_info_list(self.ctx, sourceInfoOp, self);
|
||||
const server_info_op = c.pa_context_get_server_info(self.ctx, serverInfoOp, self);
|
||||
|
||||
performOperation(self.main_loop, list_sink_op);
|
||||
performOperation(self.main_loop, list_source_op);
|
||||
performOperation(self.main_loop, server_info_op);
|
||||
|
||||
defer {
|
||||
if (self.default_sink) |d|
|
||||
self.allocator.free(d);
|
||||
if (self.default_source) |d|
|
||||
self.allocator.free(d);
|
||||
}
|
||||
for (self.devices_info.list.items) |device, i| {
|
||||
if ((device.mode == .playback and
|
||||
self.default_sink != null and
|
||||
std.mem.eql(u8, device.id, self.default_sink.?)) or
|
||||
//
|
||||
(device.mode == .capture and
|
||||
self.default_source != null and
|
||||
std.mem.eql(u8, device.id, self.default_source.?)))
|
||||
{
|
||||
self.devices_info.setDefault(device.mode, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
|
||||
defer c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
self.default_sink = self.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return;
|
||||
self.default_source = self.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch {
|
||||
self.allocator.free(self.default_sink.?);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.deviceInfoOp(info, .playback) catch return;
|
||||
}
|
||||
|
||||
fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.deviceInfoOp(info, .capture) catch return;
|
||||
}
|
||||
|
||||
fn deviceInfoOp(self: *Context, info: anytype, mode: main.Device.Mode) !void {
|
||||
var id = try self.allocator.dupeZ(u8, std.mem.span(info.*.name));
|
||||
errdefer self.allocator.free(id);
|
||||
var name = try self.allocator.dupeZ(u8, std.mem.span(info.*.description));
|
||||
errdefer self.allocator.free(name);
|
||||
|
||||
var device = main.Device{
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
var channels = try self.allocator.alloc(main.Channel, info.*.channel_map.channels);
|
||||
for (channels) |*ch, i|
|
||||
ch.*.id = fromPAChannelPos(info.*.channel_map.map[i]) catch unreachable;
|
||||
break :blk channels;
|
||||
},
|
||||
.formats = available_formats,
|
||||
.sample_rate = .{
|
||||
.min = @intCast(u24, info.*.sample_spec.rate),
|
||||
.max = @intCast(u24, info.*.sample_spec.rate),
|
||||
},
|
||||
.id = id,
|
||||
.name = name,
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
|
||||
const sample_spec = c.pa_sample_spec{
|
||||
.format = toPAFormat(format) catch unreachable,
|
||||
.rate = sample_rate,
|
||||
.channels = @intCast(u5, device.channels.len),
|
||||
};
|
||||
|
||||
const channel_map = try toPAChannelMap(device.channels);
|
||||
|
||||
var stream = c.pa_stream_new(self.ctx, self.app_name.ptr, &sample_spec, &channel_map);
|
||||
if (stream == null)
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_stream_unref(stream);
|
||||
|
||||
var status: StreamStatus = .{ .main_loop = self.main_loop, .status = .unknown };
|
||||
c.pa_stream_set_state_callback(stream, streamStateOp, &status);
|
||||
|
||||
const buf_attr = c.pa_buffer_attr{
|
||||
.maxlength = std.math.maxInt(u32),
|
||||
.tlength = std.math.maxInt(u32),
|
||||
.prebuf = 0,
|
||||
.minreq = std.math.maxInt(u32),
|
||||
.fragsize = std.math.maxInt(u32),
|
||||
};
|
||||
|
||||
const flags =
|
||||
c.PA_STREAM_START_CORKED |
|
||||
c.PA_STREAM_AUTO_TIMING_UPDATE |
|
||||
c.PA_STREAM_INTERPOLATE_TIMING |
|
||||
c.PA_STREAM_ADJUST_LATENCY;
|
||||
|
||||
if (c.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) {
|
||||
return error.OpeningDevice;
|
||||
}
|
||||
errdefer _ = c.pa_stream_disconnect(stream);
|
||||
|
||||
while (true) {
|
||||
switch (status.status) {
|
||||
.unknown => c.pa_threaded_mainloop_wait(self.main_loop),
|
||||
.ready => break,
|
||||
.failure => return error.OpeningDevice,
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.pulseaudio = .{
|
||||
.main_loop = self.main_loop,
|
||||
.ctx = self.ctx,
|
||||
.stream = stream.?,
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.write_ptr = undefined,
|
||||
.vol = 1.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const StreamStatus = struct {
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
status: enum(u8) {
|
||||
unknown,
|
||||
ready,
|
||||
failure,
|
||||
},
|
||||
};
|
||||
|
||||
fn streamStateOp(stream: ?*c.pa_stream, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*StreamStatus, @alignCast(@alignOf(*StreamStatus), userdata.?));
|
||||
|
||||
switch (c.pa_stream_get_state(stream)) {
|
||||
c.PA_STREAM_UNCONNECTED,
|
||||
c.PA_STREAM_CREATING,
|
||||
c.PA_STREAM_TERMINATED,
|
||||
=> {},
|
||||
c.PA_STREAM_READY => {
|
||||
self.status = .ready;
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
},
|
||||
c.PA_STREAM_FAILED => {
|
||||
self.status = .failure;
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
ctx: *c.pa_context,
|
||||
stream: *c.pa_stream,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
write_ptr: [*]u8,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
c.pa_stream_set_write_callback(self.stream, null, null);
|
||||
c.pa_stream_set_state_callback(self.stream, null, null);
|
||||
c.pa_stream_set_underflow_callback(self.stream, null, null);
|
||||
c.pa_stream_set_overflow_callback(self.stream, null, null);
|
||||
_ = c.pa_stream_disconnect(self.stream);
|
||||
c.pa_stream_unref(self.stream);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
const op = c.pa_stream_cork(self.stream, 0, null, null) orelse
|
||||
return error.CannotPlay;
|
||||
c.pa_operation_unref(op);
|
||||
c.pa_stream_set_write_callback(self.stream, playbackStreamWriteOp, self);
|
||||
}
|
||||
|
||||
fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
var frames_left = nbytes;
|
||||
while (frames_left > 0) {
|
||||
var chunk_size = frames_left;
|
||||
if (c.pa_stream_begin_write(
|
||||
self.stream,
|
||||
@ptrCast(
|
||||
[*c]?*anyopaque,
|
||||
@alignCast(@alignOf([*c]?*anyopaque), &self.write_ptr),
|
||||
),
|
||||
&chunk_size,
|
||||
) != 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = self.write_ptr + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
const frames = chunk_size / self.format().frameSize(self.channels().len);
|
||||
self.writeFn(parent, frames);
|
||||
|
||||
if (c.pa_stream_write(self.stream, self.write_ptr, chunk_size, null, 0, c.PA_SEEK_RELATIVE) != 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
|
||||
frames_left -= chunk_size;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
if (c.pa_stream_is_corked(self.stream) > 0) {
|
||||
const op = c.pa_stream_cork(self.stream, 0, null, null) orelse
|
||||
return error.CannotPlay;
|
||||
c.pa_operation_unref(op);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
if (c.pa_stream_is_corked(self.stream) == 0) {
|
||||
const op = c.pa_stream_cork(self.stream, 1, null, null) orelse
|
||||
return error.CannotPause;
|
||||
c.pa_operation_unref(op);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
return c.pa_stream_is_corked(self.stream) > 0;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
var cvolume: c.pa_cvolume = undefined;
|
||||
_ = c.pa_cvolume_init(&cvolume);
|
||||
_ = c.pa_cvolume_set(&cvolume, @intCast(c_uint, self.channels().len), c.pa_sw_volume_from_linear(vol));
|
||||
|
||||
performOperation(
|
||||
self.main_loop,
|
||||
c.pa_context_set_sink_input_volume(
|
||||
self.ctx,
|
||||
c.pa_stream_get_index(self.stream),
|
||||
&cvolume,
|
||||
successOp,
|
||||
self,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn successOp(_: ?*c.pa_context, success: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
if (success == 1)
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
performOperation(
|
||||
self.main_loop,
|
||||
c.pa_context_get_sink_input_info(
|
||||
self.ctx,
|
||||
c.pa_stream_get_index(self.stream),
|
||||
sinkInputInfoOp,
|
||||
self,
|
||||
),
|
||||
);
|
||||
|
||||
return self.vol;
|
||||
}
|
||||
|
||||
fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.vol = @intToFloat(f32, info.*.volume.values[0]) / @intToFloat(f32, c.PA_VOLUME_NORM);
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + self.format().frameSize(self.channels().len) * frame;
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.name);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
fn performOperation(main_loop: *c.pa_threaded_mainloop, op: ?*c.pa_operation) void {
|
||||
while (true) {
|
||||
switch (c.pa_operation_get_state(op)) {
|
||||
c.PA_OPERATION_RUNNING => c.pa_threaded_mainloop_wait(main_loop),
|
||||
c.PA_OPERATION_DONE => return c.pa_operation_unref(op),
|
||||
c.PA_OPERATION_CANCELLED => {
|
||||
std.debug.assert(false);
|
||||
c.pa_operation_unref(op);
|
||||
return;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const available_formats = &[_]main.Format{
|
||||
.u8, .i16,
|
||||
.i24, .i24_4b,
|
||||
.i32, .f32,
|
||||
};
|
||||
|
||||
pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.Channel.Id {
|
||||
return switch (pos) {
|
||||
c.PA_CHANNEL_POSITION_MONO => .front_center,
|
||||
c.PA_CHANNEL_POSITION_FRONT_LEFT => .front_left, // PA_CHANNEL_POSITION_LEFT
|
||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT => .front_right, // PA_CHANNEL_POSITION_RIGHT
|
||||
c.PA_CHANNEL_POSITION_FRONT_CENTER => .front_center, // PA_CHANNEL_POSITION_CENTER
|
||||
c.PA_CHANNEL_POSITION_REAR_CENTER => .back_center,
|
||||
c.PA_CHANNEL_POSITION_LFE => .lfe, // PA_CHANNEL_POSITION_SUBWOOFER
|
||||
c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER => .front_left_center,
|
||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
||||
c.PA_CHANNEL_POSITION_SIDE_LEFT => .side_left,
|
||||
c.PA_CHANNEL_POSITION_SIDE_RIGHT => .side_right,
|
||||
|
||||
// TODO: .front_center?
|
||||
c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => error.Invalid,
|
||||
|
||||
c.PA_CHANNEL_POSITION_TOP_CENTER => .top_center,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT => .top_front_left,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT => .top_front_right,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER => .top_front_center,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_LEFT => .top_back_left,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT => .top_back_right,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_CENTER => .top_back_center,
|
||||
|
||||
else => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toPAFormat(format: main.Format) !c.pa_sample_format_t {
|
||||
return switch (format) {
|
||||
.u8 => c.PA_SAMPLE_U8,
|
||||
.i16 => if (is_little) c.PA_SAMPLE_S16LE else c.PA_SAMPLE_S16BE,
|
||||
.i24 => if (is_little) c.PA_SAMPLE_S24LE else c.PA_SAMPLE_S24LE,
|
||||
.i24_4b => if (is_little) c.PA_SAMPLE_S24_32LE else c.PA_SAMPLE_S24_32BE,
|
||||
.i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE,
|
||||
.f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE,
|
||||
|
||||
.f64, .i8 => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toPAChannelMap(channels: []const main.Channel) !c.pa_channel_map {
|
||||
var channel_map: c.pa_channel_map = undefined;
|
||||
channel_map.channels = @intCast(u5, channels.len);
|
||||
for (channels) |ch, i|
|
||||
channel_map.map[i] = try toPAChannelPos(ch.id);
|
||||
return channel_map;
|
||||
}
|
||||
|
||||
fn toPAChannelPos(channel_id: main.Channel.Id) !c.pa_channel_position_t {
|
||||
return switch (channel_id) {
|
||||
.front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT,
|
||||
.front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT,
|
||||
.front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER,
|
||||
.lfe => c.PA_CHANNEL_POSITION_LFE,
|
||||
.front_left_center => c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
|
||||
.front_right_center => c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
|
||||
.back_center => c.PA_CHANNEL_POSITION_REAR_CENTER,
|
||||
.side_left => c.PA_CHANNEL_POSITION_SIDE_LEFT,
|
||||
.side_right => c.PA_CHANNEL_POSITION_SIDE_RIGHT,
|
||||
.top_center => c.PA_CHANNEL_POSITION_TOP_CENTER,
|
||||
.top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT,
|
||||
.top_front_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER,
|
||||
.top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
|
||||
.top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT,
|
||||
.top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER,
|
||||
.top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Mode = @import("main.zig").Mode;
|
||||
const Format = @import("main.zig").Format;
|
||||
const DataCallback = @import("main.zig").DataCallback;
|
||||
const c = @import("soundio").c;
|
||||
const Aim = @import("soundio").Aim;
|
||||
const SoundIo = @import("soundio").SoundIo;
|
||||
const SoundIoFormat = @import("soundio").Format;
|
||||
const SoundIoDevice = @import("soundio").Device;
|
||||
const SoundIoInStream = @import("soundio").InStream;
|
||||
const SoundIoOutStream = @import("soundio").OutStream;
|
||||
|
||||
const SoundIoStream = union(Mode) {
|
||||
input: SoundIoInStream,
|
||||
output: SoundIoOutStream,
|
||||
};
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
const default_buffer_size_per_channel = 1024; // 21.33ms
|
||||
|
||||
pub const Device = struct {
|
||||
properties: Properties,
|
||||
|
||||
// Internal fields.
|
||||
handle: SoundIoStream,
|
||||
data_callback: ?DataCallback = null,
|
||||
user_data: ?*anyopaque = null,
|
||||
planar_buffer: [512000]u8 = undefined,
|
||||
started: bool = false,
|
||||
|
||||
pub const Options = struct {
|
||||
mode: Mode = .output,
|
||||
format: ?Format = null,
|
||||
is_raw: ?bool = null,
|
||||
channels: ?u8 = null,
|
||||
sample_rate: ?u32 = null,
|
||||
id: ?[:0]const u8 = null,
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Properties = struct {
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
is_raw: bool,
|
||||
channels: u8,
|
||||
sample_rate: u32,
|
||||
id: [:0]const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Device, allocator: std.mem.Allocator) void {
|
||||
switch (self.handle) {
|
||||
.input => |d| d.deinit(),
|
||||
.output => |d| d.deinit(),
|
||||
}
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn setCallback(self: *Device, callback: DataCallback, data: *anyopaque) void {
|
||||
self.data_callback = callback;
|
||||
self.user_data = data;
|
||||
switch (self.handle) {
|
||||
.input => |_| @panic("input not supported yet"),
|
||||
.output => |d| {
|
||||
// TODO(sysaudio): support other formats
|
||||
d.setFormat(.float32LE);
|
||||
|
||||
d.setWriteCallback((struct {
|
||||
fn cCallback(
|
||||
c_outstream: ?[*]c.SoundIoOutStream,
|
||||
frame_count_min: c_int,
|
||||
frame_count_max: c_int,
|
||||
) callconv(.C) void {
|
||||
const outstream = SoundIoOutStream{ .handle = @ptrCast(*c.SoundIoOutStream, c_outstream) };
|
||||
const device = @ptrCast(*Device, @alignCast(@alignOf(Device), outstream.handle.userdata));
|
||||
|
||||
// TODO(sysaudio): provide callback with outstream.sampleRate()
|
||||
|
||||
// TODO(sysaudio): according to issue tracker and PR from mason (did we include it?)
|
||||
// there may be issues with frame_count_max being way too large on Windows. May need
|
||||
// to artificially limit it or use Mason's PR.
|
||||
|
||||
// The data callback gives us planar data, e.g. in AAAABBBB format for channels
|
||||
// A and B. WebAudio similarly requires data in planar format. libsoundio however
|
||||
// does not guarantee planar data format, it may be in interleaved format ABABABAB.
|
||||
// Invoke our data callback with a temporary buffer, this involves one copy later
|
||||
// but it's such a small amount of memory it is entirely negligible.
|
||||
const layout = outstream.layout();
|
||||
|
||||
const desired_frame_count = default_buffer_size_per_channel * layout.channelCount();
|
||||
const total_frame_count = if (frame_count_max > desired_frame_count)
|
||||
if (frame_count_min <= desired_frame_count)
|
||||
@intCast(usize, desired_frame_count)
|
||||
else
|
||||
@intCast(usize, frame_count_min)
|
||||
else
|
||||
@intCast(usize, frame_count_max);
|
||||
|
||||
const buffer_size: usize = @sizeOf(f32) * total_frame_count * @intCast(usize, layout.channelCount());
|
||||
const addr = @ptrToInt(&device.planar_buffer);
|
||||
const aligned_addr = std.mem.alignForward(addr, @alignOf(f32));
|
||||
const padding = aligned_addr - addr;
|
||||
const planar_buffer = device.planar_buffer[padding .. padding + buffer_size];
|
||||
device.data_callback.?(device, device.user_data.?, planar_buffer);
|
||||
|
||||
var frames_left = total_frame_count;
|
||||
var frame_offset: usize = 0;
|
||||
while (frames_left > 0) {
|
||||
var frame_count: i32 = @intCast(i32, frames_left);
|
||||
|
||||
var areas: [*]c.SoundIoChannelArea = undefined;
|
||||
// TODO(sysaudio): improve error handling
|
||||
outstream.beginWrite(
|
||||
@ptrCast([*]?[*]c.SoundIoChannelArea, &areas),
|
||||
&frame_count,
|
||||
) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});
|
||||
|
||||
if (frame_count == 0) break;
|
||||
|
||||
var channel: usize = 0;
|
||||
while (channel < @intCast(usize, layout.channelCount())) : (channel += 1) {
|
||||
const channel_ptr = areas[channel].ptr;
|
||||
var frame: c_int = 0;
|
||||
while (frame < frame_count) : (frame += 1) {
|
||||
const sample_start = (channel * total_frame_count * @sizeOf(f32)) + ((frame_offset + @intCast(usize, frame)) * @sizeOf(f32));
|
||||
const src = @ptrCast(*f32, @alignCast(@alignOf(f32), &planar_buffer[sample_start]));
|
||||
const dst = &channel_ptr[@intCast(usize, areas[channel].step * frame)];
|
||||
@ptrCast(*f32, @alignCast(@alignOf(f32), dst)).* = src.*;
|
||||
}
|
||||
}
|
||||
// TODO(sysaudio): improve error handling
|
||||
outstream.endWrite() catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)});
|
||||
frames_left -= @intCast(usize, frame_count);
|
||||
frame_offset += @intCast(usize, frame_count);
|
||||
}
|
||||
}
|
||||
}).cCallback);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(device: *Device) Error!void {
|
||||
if (!device.started) return;
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.pause(true),
|
||||
.output => |d| d.pause(true),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start(device: *Device) Error!void {
|
||||
// TODO(sysaudio): after pause, may need to call d.pause(false) instead of d.start()?
|
||||
if (!device.started) {
|
||||
device.started = true;
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.start(),
|
||||
.output => |d| d.start(),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.pause(false),
|
||||
.output => |d| d.pause(false),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const DeviceIterator = struct {
|
||||
ctx: Audio,
|
||||
mode: Mode,
|
||||
device_len: u16,
|
||||
index: u16,
|
||||
|
||||
pub fn next(self: *DeviceIterator) IteratorError!?Device.Options {
|
||||
if (self.index < self.device_len) {
|
||||
const device_desc = switch (self.mode) {
|
||||
.input => self.ctx.handle.getInputDevice(self.index) orelse return null,
|
||||
.output => self.ctx.handle.getOutputDevice(self.index) orelse return null,
|
||||
};
|
||||
self.index += 1;
|
||||
return Device.Options{
|
||||
.mode = switch (@intToEnum(Aim, device_desc.handle.aim)) {
|
||||
.input => .input,
|
||||
.output => .output,
|
||||
},
|
||||
.is_raw = device_desc.handle.is_raw,
|
||||
.id = device_desc.id(),
|
||||
.name = device_desc.name(),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(sysaudio): standardize errors across backends
|
||||
pub const IteratorError = error{OutOfMemory};
|
||||
pub const Error = error{
|
||||
OutOfMemory,
|
||||
InvalidDeviceID,
|
||||
InvalidParameter,
|
||||
NoDeviceFound,
|
||||
AlreadyConnected,
|
||||
CannotConnect,
|
||||
UnsupportedOS,
|
||||
UnsupportedBackend,
|
||||
DeviceUnavailable,
|
||||
Invalid,
|
||||
OpeningDevice,
|
||||
BackendDisconnected,
|
||||
SystemResources,
|
||||
NoSuchClient,
|
||||
IncompatibleBackend,
|
||||
IncompatibleDevice,
|
||||
InitAudioBackend,
|
||||
NoSuchDevice,
|
||||
BackendUnavailable,
|
||||
Streaming,
|
||||
Interrupted,
|
||||
Underflow,
|
||||
EncodingString,
|
||||
};
|
||||
|
||||
handle: SoundIo,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
var self = Audio{
|
||||
.handle = try SoundIo.init(),
|
||||
};
|
||||
self.handle.connect() catch |err| {
|
||||
return switch (err) {
|
||||
error.SystemResources, error.NoSuchClient => error.CannotConnect,
|
||||
error.Invalid => error.AlreadyConnected,
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
};
|
||||
self.handle.flushEvents();
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: Audio) void {
|
||||
self.handle.deinit();
|
||||
}
|
||||
|
||||
pub fn waitEvents(self: Audio) void {
|
||||
self.handle.waitEvents();
|
||||
}
|
||||
|
||||
pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device {
|
||||
var sio_device: SoundIoDevice = undefined;
|
||||
|
||||
if (options.id) |id| {
|
||||
if (options.is_raw == null)
|
||||
return error.InvalidParameter;
|
||||
|
||||
sio_device = switch (options.mode) {
|
||||
.input => self.handle.getInputDeviceFromID(id, options.is_raw.?),
|
||||
.output => self.handle.getOutputDeviceFromID(id, options.is_raw.?),
|
||||
} orelse {
|
||||
return if (switch (options.mode) {
|
||||
.input => self.handle.inputDeviceCount().?,
|
||||
.output => self.handle.outputDeviceCount().?,
|
||||
} == 0)
|
||||
error.NoDeviceFound
|
||||
else
|
||||
error.DeviceUnavailable;
|
||||
};
|
||||
} else {
|
||||
const id = switch (options.mode) {
|
||||
.input => self.handle.defaultInputDeviceIndex(),
|
||||
.output => self.handle.defaultOutputDeviceIndex(),
|
||||
} orelse return error.NoDeviceFound;
|
||||
sio_device = switch (options.mode) {
|
||||
.input => self.handle.getInputDevice(id),
|
||||
.output => self.handle.getOutputDevice(id),
|
||||
} orelse return error.DeviceUnavailable;
|
||||
}
|
||||
|
||||
const handle = switch (options.mode) {
|
||||
.input => SoundIoStream{ .input = try sio_device.createInStream() },
|
||||
.output => SoundIoStream{ .output = try sio_device.createOutStream() },
|
||||
};
|
||||
|
||||
switch (handle) {
|
||||
.input => |d| try d.open(),
|
||||
.output => |d| try d.open(),
|
||||
}
|
||||
|
||||
const device = try allocator.create(Device);
|
||||
switch (handle) {
|
||||
.input => |d| d.handle.userdata = device,
|
||||
.output => |d| d.handle.userdata = device,
|
||||
}
|
||||
|
||||
// TODO(sysaudio): handle big endian architectures
|
||||
const format: Format = switch (handle) {
|
||||
.input => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) {
|
||||
.U8 => .U8,
|
||||
.S16LE => .S16,
|
||||
.S24LE => .S24,
|
||||
.S32LE => .S32,
|
||||
.float32LE => .F32,
|
||||
else => return error.InvalidParameter,
|
||||
},
|
||||
.output => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) {
|
||||
.U8 => .U8,
|
||||
.S16LE => .S16,
|
||||
.S24LE => .S24,
|
||||
.S32LE => .S32,
|
||||
.float32LE => .F32,
|
||||
else => return error.InvalidParameter,
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(sysaudio): Get the device name. Calling span or sliceTo on the name is causing segfaults on NixOS
|
||||
// const name_ptr = switch(handle) {
|
||||
// .input => |d| d.handle.name,
|
||||
// .output => |d| d.handle.name,
|
||||
// };
|
||||
// const name = std.mem.sliceTo(name_ptr, 0);
|
||||
|
||||
var properties = Device.Properties{
|
||||
.is_raw = options.is_raw orelse false,
|
||||
.format = format,
|
||||
.mode = options.mode,
|
||||
.id = std.mem.span(sio_device.handle.id),
|
||||
.name = "",
|
||||
.channels = @intCast(u8, switch (handle) {
|
||||
.input => |d| d.layout().channelCount(),
|
||||
.output => |d| d.layout().channelCount(),
|
||||
}),
|
||||
.sample_rate = @intCast(u32, switch (handle) {
|
||||
.input => |d| d.sampleRate(),
|
||||
.output => |d| d.sampleRate(),
|
||||
}),
|
||||
};
|
||||
|
||||
device.* = .{
|
||||
.properties = properties,
|
||||
.handle = handle,
|
||||
};
|
||||
return device;
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return .{
|
||||
.ctx = self,
|
||||
.mode = .output,
|
||||
.device_len = self.handle.outputDeviceCount() orelse 0,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return .{
|
||||
.ctx = self,
|
||||
.mode = .input,
|
||||
.device_len = self.handle.inputDeviceCount() orelse 0,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
56
libs/sysaudio/src/util.zig
Normal file
56
libs/sysaudio/src/util.zig
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
|
||||
pub const DevicesInfo = struct {
|
||||
list: std.ArrayListUnmanaged(main.Device),
|
||||
default_output: ?usize,
|
||||
default_input: ?usize,
|
||||
|
||||
pub fn init() DevicesInfo {
|
||||
return .{
|
||||
.list = .{},
|
||||
.default_output = null,
|
||||
.default_input = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(self: *DevicesInfo, allocator: std.mem.Allocator) void {
|
||||
self.default_output = null;
|
||||
self.default_input = null;
|
||||
self.list.clearAndFree(allocator);
|
||||
}
|
||||
|
||||
pub fn get(self: DevicesInfo, i: usize) main.Device {
|
||||
return self.list.items[i];
|
||||
}
|
||||
|
||||
pub fn default(self: DevicesInfo, mode: main.Device.Mode) ?main.Device {
|
||||
const index = switch (mode) {
|
||||
.playback => self.default_output,
|
||||
.capture => self.default_input,
|
||||
} orelse return null;
|
||||
return self.get(index);
|
||||
}
|
||||
|
||||
pub fn setDefault(self: *DevicesInfo, mode: main.Device.Mode, i: usize) void {
|
||||
switch (mode) {
|
||||
.playback => self.default_output = i,
|
||||
.capture => self.default_input = i,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn Range(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
min: T,
|
||||
max: T,
|
||||
|
||||
pub fn clamp(self: Self, val: T) T {
|
||||
return std.math.clamp(val, self.min, self.max);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn doNothing() callconv(.C) void {}
|
||||
779
libs/sysaudio/src/wasapi.zig
Normal file
779
libs/sysaudio/src/wasapi.zig
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
const std = @import("std");
|
||||
const win32 = @import("wasapi/win32.zig");
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
enumerator: ?*win32.IMMDeviceEnumerator,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
notif_client: win32.IMMNotificationClient,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
const flags = win32.COINIT_APARTMENTTHREADED | win32.COINIT_DISABLE_OLE1DDE;
|
||||
var hr = win32.CoInitializeEx(null, flags);
|
||||
switch (hr) {
|
||||
win32.S_OK,
|
||||
win32.S_FALSE,
|
||||
win32.RPC_E_CHANGED_MODE,
|
||||
=> {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.E_UNEXPECTED => return error.SystemResources,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.enumerator = blk: {
|
||||
var enumerator: ?*win32.IMMDeviceEnumerator = null;
|
||||
hr = win32.CoCreateInstance(
|
||||
win32.CLSID_MMDeviceEnumerator,
|
||||
null,
|
||||
win32.CLSCTX_ALL,
|
||||
win32.IID_IMMDeviceEnumerator,
|
||||
@ptrCast(*?*anyopaque, &enumerator),
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.CLASS_E_NOAGGREGATION => return error.SystemResources,
|
||||
win32.REGDB_E_CLASSNOTREG => unreachable,
|
||||
else => unreachable,
|
||||
}
|
||||
break :blk enumerator;
|
||||
},
|
||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
.notif_client = win32.IMMNotificationClient{
|
||||
.vtable = &.{
|
||||
.base = .{
|
||||
.QueryInterface = queryInterfaceCB,
|
||||
.AddRef = addRefCB,
|
||||
.Release = releaseCB,
|
||||
},
|
||||
.OnDeviceStateChanged = onDeviceStateChangedCB,
|
||||
.OnDeviceAdded = onDeviceAddedCB,
|
||||
.OnDeviceRemoved = onDeviceRemovedCB,
|
||||
.OnDefaultDeviceChanged = onDefaultDeviceChangedCB,
|
||||
.OnPropertyValueChanged = onPropertyValueChangedCB,
|
||||
},
|
||||
},
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (options.deviceChangeFn) |_| {
|
||||
hr = self.enumerator.?.RegisterEndpointNotificationCallback(&self.watcher.?.notif_client);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.SystemResources,
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .wasapi = self };
|
||||
}
|
||||
|
||||
fn queryInterfaceCB(self: *const win32.IUnknown, riid: ?*const win32.Guid, ppv: ?*?*anyopaque) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
if (riid.?.eql(win32.IID_IUnknown.*) or riid.?.eql(win32.IID_IMMNotificationClient.*)) {
|
||||
ppv.?.* = @intToPtr(?*anyopaque, @ptrToInt(self));
|
||||
_ = self.AddRef();
|
||||
return win32.S_OK;
|
||||
} else {
|
||||
ppv.?.* = null;
|
||||
return win32.E_NOINTERFACE;
|
||||
}
|
||||
}
|
||||
|
||||
fn addRefCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn releaseCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn onDeviceStateChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDeviceAddedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDeviceRemovedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDefaultDeviceChangedCB(self: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onPropertyValueChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (self.watcher) |*watcher| {
|
||||
_ = self.enumerator.?.UnregisterEndpointNotificationCallback(&watcher.notif_client);
|
||||
}
|
||||
_ = self.enumerator.?.Release();
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
// get default devices id
|
||||
var default_playback_device: ?*win32.IMMDevice = null;
|
||||
var hr = self.enumerator.?.GetDefaultAudioEndpoint(.render, .multimedia, &default_playback_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = default_playback_device.?.Release();
|
||||
|
||||
var default_capture_device: ?*win32.IMMDevice = null;
|
||||
hr = self.enumerator.?.GetDefaultAudioEndpoint(.capture, .multimedia, &default_capture_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = default_capture_device.?.Release();
|
||||
|
||||
var default_playback_id_u16: ?[*:0]u16 = undefined;
|
||||
hr = default_playback_device.?.GetId(&default_playback_id_u16);
|
||||
defer win32.CoTaskMemFree(default_playback_id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
const default_playback_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_playback_id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(default_playback_id);
|
||||
|
||||
var default_capture_id_u16: ?[*:0]u16 = undefined;
|
||||
hr = default_capture_device.?.GetId(&default_capture_id_u16);
|
||||
defer win32.CoTaskMemFree(default_capture_id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
const default_capture_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_capture_id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(default_capture_id);
|
||||
|
||||
// enumerate
|
||||
var collection: ?*win32.IMMDeviceCollection = null;
|
||||
hr = self.enumerator.?.EnumAudioEndpoints(
|
||||
win32.DataFlow.all,
|
||||
win32.DEVICE_STATE_ACTIVE,
|
||||
&collection,
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = collection.?.Release();
|
||||
|
||||
var device_count: u32 = 0;
|
||||
hr = collection.?.GetCount(&device_count);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < device_count) : (i += 1) {
|
||||
var imm_device: ?*win32.IMMDevice = null;
|
||||
hr = collection.?.Item(i, &imm_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = imm_device.?.Release();
|
||||
|
||||
var property_store: ?*win32.IPropertyStore = null;
|
||||
var variant: win32.PROPVARIANT = undefined;
|
||||
hr = imm_device.?.OpenPropertyStore(win32.STGM_READ, &property_store);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = property_store.?.Release();
|
||||
|
||||
hr = property_store.?.GetValue(&win32.PKEY_AudioEngine_DeviceFormat, &variant);
|
||||
switch (hr) {
|
||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
var wf = @ptrCast(
|
||||
*win32.WAVEFORMATEXTENSIBLE,
|
||||
variant.anon.anon.anon.blob.pBlobData,
|
||||
);
|
||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.blob.pBlobData);
|
||||
|
||||
var device = main.Device{
|
||||
.mode = blk: {
|
||||
var endpoint: ?*win32.IMMEndpoint = null;
|
||||
hr = imm_device.?.QueryInterface(win32.IID_IMMEndpoint, @ptrCast(?*?*anyopaque, &endpoint));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
else => unreachable,
|
||||
}
|
||||
defer _ = endpoint.?.Release();
|
||||
|
||||
var dataflow: win32.DataFlow = undefined;
|
||||
hr = endpoint.?.GetDataFlow(&dataflow);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
break :blk switch (dataflow) {
|
||||
.render => .playback,
|
||||
.capture => .capture,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
.channels = blk: {
|
||||
var chn_arr = std.ArrayList(main.Channel).init(self.allocator);
|
||||
var channel: u32 = win32.SPEAKER_FRONT_LEFT;
|
||||
while (channel < win32.SPEAKER_ALL) : (channel <<= 1) {
|
||||
if (wf.dwChannelMask & channel != 0)
|
||||
try chn_arr.append(.{ .id = fromWASApiChannel(channel) });
|
||||
}
|
||||
break :blk try chn_arr.toOwnedSlice();
|
||||
},
|
||||
.sample_rate = .{
|
||||
.min = @intCast(u24, wf.Format.nSamplesPerSec),
|
||||
.max = @intCast(u24, wf.Format.nSamplesPerSec),
|
||||
},
|
||||
.formats = blk: {
|
||||
var audio_client: ?*win32.IAudioClient = null;
|
||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = audio_client.?.Release();
|
||||
|
||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
||||
var closest_match: ?*win32.WAVEFORMATEX = null;
|
||||
for (std.meta.tags(main.Format)) |format| {
|
||||
setWaveFormatFormat(wf, format) catch continue;
|
||||
if (audio_client.?.IsFormatSupported(
|
||||
.SHARED,
|
||||
@ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), wf)),
|
||||
&closest_match,
|
||||
) == win32.S_OK) {
|
||||
try fmt_arr.append(format);
|
||||
}
|
||||
}
|
||||
|
||||
break :blk try fmt_arr.toOwnedSlice();
|
||||
},
|
||||
.id = blk: {
|
||||
var id_u16: ?[*:0]u16 = undefined;
|
||||
hr = imm_device.?.GetId(&id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer win32.CoTaskMemFree(id_u16);
|
||||
|
||||
break :blk std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
.name = blk: {
|
||||
hr = property_store.?.GetValue(&win32.PKEY_Device_FriendlyName, &variant);
|
||||
switch (hr) {
|
||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.pwszVal);
|
||||
|
||||
break :blk std.unicode.utf16leToUtf8AllocZ(
|
||||
self.allocator,
|
||||
std.mem.span(variant.anon.anon.anon.pwszVal.?),
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
if (self.devices_info.default(device.mode) == null) {
|
||||
switch (device.mode) {
|
||||
.playback => if (std.mem.eql(u8, device.id, default_playback_id)) {
|
||||
self.devices_info.setDefault(.playback, self.devices_info.list.items.len - 1);
|
||||
},
|
||||
.capture => if (std.mem.eql(u8, device.id, default_capture_id)) {
|
||||
self.devices_info.setDefault(.capture, self.devices_info.list.items.len - 1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
fn fromWASApiChannel(speaker: u32) main.Channel.Id {
|
||||
return switch (speaker) {
|
||||
win32.SPEAKER_FRONT_CENTER => .front_center,
|
||||
win32.SPEAKER_FRONT_LEFT => .front_left,
|
||||
win32.SPEAKER_FRONT_RIGHT => .front_right,
|
||||
win32.SPEAKER_FRONT_LEFT_OF_CENTER => .front_left_center,
|
||||
win32.SPEAKER_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
||||
win32.SPEAKER_BACK_CENTER => .back_center,
|
||||
win32.SPEAKER_SIDE_LEFT => .side_left,
|
||||
win32.SPEAKER_SIDE_RIGHT => .side_right,
|
||||
win32.SPEAKER_TOP_CENTER => .top_center,
|
||||
win32.SPEAKER_TOP_FRONT_CENTER => .top_front_center,
|
||||
win32.SPEAKER_TOP_FRONT_LEFT => .top_front_left,
|
||||
win32.SPEAKER_TOP_FRONT_RIGHT => .top_front_right,
|
||||
win32.SPEAKER_TOP_BACK_CENTER => .top_back_center,
|
||||
win32.SPEAKER_TOP_BACK_LEFT => .top_back_left,
|
||||
win32.SPEAKER_TOP_BACK_RIGHT => .top_back_right,
|
||||
win32.SPEAKER_LOW_FREQUENCY => .lfe,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn setWaveFormatFormat(wf: *win32.WAVEFORMATEXTENSIBLE, format: main.Format) !void {
|
||||
switch (format) {
|
||||
.u8, .i16, .i24, .i24_4b, .i32 => {
|
||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*;
|
||||
},
|
||||
.f32, .f64 => {
|
||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*;
|
||||
},
|
||||
.i8 => return error.Invalid,
|
||||
}
|
||||
wf.Format.wBitsPerSample = format.sizeBits();
|
||||
wf.Samples.wValidBitsPerSample = format.validSizeBits();
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
var imm_device: ?*win32.IMMDevice = null;
|
||||
var id_u16 = std.unicode.utf8ToUtf16LeWithNull(self.allocator, device.id) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(id_u16);
|
||||
var hr = self.enumerator.?.GetDevice(id_u16, &imm_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var audio_client: ?*win32.IAudioClient = null;
|
||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
|
||||
const wave_format = win32.WAVEFORMATEXTENSIBLE{
|
||||
.Format = .{
|
||||
.wFormatTag = win32.WAVE_FORMAT_EXTENSIBLE,
|
||||
.nChannels = @intCast(u16, device.channels.len),
|
||||
.nSamplesPerSec = sample_rate,
|
||||
.nAvgBytesPerSec = sample_rate * format.frameSize(device.channels.len),
|
||||
.nBlockAlign = format.frameSize(device.channels.len),
|
||||
.wBitsPerSample = format.sizeBits(),
|
||||
.cbSize = 0x16,
|
||||
},
|
||||
.Samples = .{
|
||||
.wValidBitsPerSample = format.validSizeBits(),
|
||||
},
|
||||
.dwChannelMask = toChannelMask(device.channels),
|
||||
.SubFormat = toSubFormat(format) catch return error.OpeningDevice,
|
||||
};
|
||||
|
||||
hr = audio_client.?.Initialize(
|
||||
.SHARED,
|
||||
win32.AUDCLNT_STREAMFLAGS_NOPERSIST,
|
||||
0,
|
||||
0,
|
||||
@ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)),
|
||||
null,
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_DEVICE_IN_USE => unreachable,
|
||||
win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable,
|
||||
win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable,
|
||||
win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
errdefer _ = audio_client.?.Release();
|
||||
|
||||
var render_client: ?*win32.IAudioRenderClient = null;
|
||||
hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @ptrCast(?*?*anyopaque, &render_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var simple_volume: ?*win32.ISimpleAudioVolume = null;
|
||||
hr = audio_client.?.GetService(win32.IID_ISimpleAudioVolume, @ptrCast(?*?*anyopaque, &simple_volume));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
return .{
|
||||
.wasapi = .{
|
||||
.thread = undefined,
|
||||
.mutex = .{},
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.audio_client = audio_client,
|
||||
.simple_volume = simple_volume,
|
||||
.imm_device = imm_device,
|
||||
.render_client = render_client,
|
||||
.is_paused = false,
|
||||
.vol = 1.0,
|
||||
.aborted = .{ .value = false },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn toSubFormat(format: main.Format) !win32.Guid {
|
||||
return switch (format) {
|
||||
.u8 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i16 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i24 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i24_4b => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*,
|
||||
.f64 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*,
|
||||
else => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
fn toChannelMask(channels: []const main.Channel) u32 {
|
||||
var mask: u32 = 0;
|
||||
for (channels) |ch| {
|
||||
mask |= switch (ch.id) {
|
||||
.front_center => win32.SPEAKER_FRONT_CENTER,
|
||||
.front_left => win32.SPEAKER_FRONT_LEFT,
|
||||
.front_right => win32.SPEAKER_FRONT_RIGHT,
|
||||
.front_left_center => win32.SPEAKER_FRONT_LEFT_OF_CENTER,
|
||||
.front_right_center => win32.SPEAKER_FRONT_RIGHT_OF_CENTER,
|
||||
.back_center => win32.SPEAKER_BACK_CENTER,
|
||||
.side_left => win32.SPEAKER_SIDE_LEFT,
|
||||
.side_right => win32.SPEAKER_SIDE_RIGHT,
|
||||
.top_center => win32.SPEAKER_TOP_CENTER,
|
||||
.top_front_center => win32.SPEAKER_TOP_FRONT_CENTER,
|
||||
.top_front_left => win32.SPEAKER_TOP_FRONT_LEFT,
|
||||
.top_front_right => win32.SPEAKER_TOP_FRONT_RIGHT,
|
||||
.top_back_center => win32.SPEAKER_TOP_BACK_CENTER,
|
||||
.top_back_left => win32.SPEAKER_TOP_BACK_LEFT,
|
||||
.top_back_right => win32.SPEAKER_TOP_BACK_RIGHT,
|
||||
.lfe => win32.SPEAKER_LOW_FREQUENCY,
|
||||
};
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
thread: std.Thread,
|
||||
mutex: std.Thread.Mutex,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
simple_volume: ?*win32.ISimpleAudioVolume,
|
||||
imm_device: ?*win32.IMMDevice,
|
||||
audio_client: ?*win32.IAudioClient,
|
||||
render_client: ?*win32.IAudioRenderClient,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
is_paused: bool,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.aborted.store(true, .Unordered);
|
||||
self.thread.join();
|
||||
_ = self.simple_volume.?.Release();
|
||||
_ = self.render_client.?.Release();
|
||||
_ = self.audio_client.?.Release();
|
||||
_ = self.imm_device.?.Release();
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeLoop(self: *Player) void {
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
var hr = self.audio_client.?.Start();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
while (!self.aborted.load(.Unordered)) {
|
||||
var frames_buf: u32 = 0;
|
||||
hr = self.audio_client.?.GetBufferSize(&frames_buf);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
var frames_used: u32 = 0;
|
||||
hr = self.audio_client.?.GetCurrentPadding(&frames_used);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
const writable_frame_count = frames_buf - frames_used;
|
||||
if (writable_frame_count > 0) {
|
||||
var data: [*]u8 = undefined;
|
||||
hr = self.render_client.?.GetBuffer(writable_frame_count, @ptrCast(?*?*u8, &data));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_TOO_LARGE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_BUFFER_OPERATION_PENDING => continue,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = data + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
self.writeFn(parent, writable_frame_count);
|
||||
hr = self.render_client.?.ReleaseBuffer(writable_frame_count, 0);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_INVALID_SIZE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
if (self.paused()) {
|
||||
const hr = self.audio_client.?.Start();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPlay,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPlay,
|
||||
else => unreachable,
|
||||
}
|
||||
self.is_paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
if (!self.paused()) {
|
||||
const hr = self.audio_client.?.Stop();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPause,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPause,
|
||||
else => unreachable,
|
||||
}
|
||||
self.is_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return self.is_paused;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
const hr = self.simple_volume.?.SetMasterVolume(vol, null);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotSetVolume,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotSetVolume,
|
||||
else => return error.CannotSetVolume,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn volume(self: Player) !f32 {
|
||||
var vol: f32 = 0;
|
||||
const hr = self.simple_volume.?.GetMasterVolume(&vol);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotGetVolume,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotGetVolume,
|
||||
else => return error.CannotGetVolume,
|
||||
}
|
||||
return vol;
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len);
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn freeDevice(allocator: std.mem.Allocator, self: main.Device) void {
|
||||
allocator.free(self.id);
|
||||
allocator.free(self.name);
|
||||
allocator.free(self.formats);
|
||||
allocator.free(self.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
1645
libs/sysaudio/src/wasapi/win32.zig
Normal file
1645
libs/sysaudio/src/wasapi/win32.zig
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,213 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Mode = @import("main.zig").Mode;
|
||||
const Format = @import("main.zig").Format;
|
||||
const DataCallback = @import("main.zig").DataCallback;
|
||||
const js = @import("sysjs");
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
pub const Device = struct {
|
||||
properties: Properties,
|
||||
|
||||
// Internal fields.
|
||||
context: js.Object,
|
||||
|
||||
pub const Options = struct {
|
||||
mode: Mode = .output,
|
||||
format: ?Format = null,
|
||||
is_raw: ?bool = null,
|
||||
channels: ?u8 = null,
|
||||
sample_rate: ?u32 = null,
|
||||
id: ?[:0]const u8 = null,
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Properties = struct {
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
is_raw: bool,
|
||||
channels: u8,
|
||||
sample_rate: u32,
|
||||
id: [:0]const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub fn deinit(device: *Device, allocator: std.mem.Allocator) void {
|
||||
device.context.deinit();
|
||||
allocator.destroy(device);
|
||||
}
|
||||
|
||||
pub fn setCallback(device: *Device, callback: DataCallback, user_data: ?*anyopaque) void {
|
||||
device.context.set("device", js.createNumber(@intToFloat(f64, @ptrToInt(device))));
|
||||
device.context.set("callback", js.createNumber(@intToFloat(f64, @ptrToInt(callback))));
|
||||
if (user_data) |ud|
|
||||
device.context.set("user_data", js.createNumber(@intToFloat(f64, @ptrToInt(ud))));
|
||||
}
|
||||
|
||||
pub fn pause(device: *Device) Error!void {
|
||||
_ = device.context.call("suspend", &.{});
|
||||
}
|
||||
|
||||
pub fn start(device: *Device) Error!void {
|
||||
_ = device.context.call("resume", &.{});
|
||||
}
|
||||
};
|
||||
|
||||
pub const DeviceIterator = struct {
|
||||
ctx: *Audio,
|
||||
mode: Mode,
|
||||
|
||||
pub fn next(_: DeviceIterator) IteratorError!?Device.Properties {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const IteratorError = error{};
|
||||
|
||||
pub const Error = error{
|
||||
OutOfMemory,
|
||||
AudioUnsupported,
|
||||
};
|
||||
|
||||
context_constructor: js.Function,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
const context = js.global().get("AudioContext");
|
||||
if (context.is(.undefined))
|
||||
return error.AudioUnsupported;
|
||||
|
||||
return Audio{ .context_constructor = context.view(.func) };
|
||||
}
|
||||
|
||||
pub fn deinit(audio: Audio) void {
|
||||
audio.context_constructor.deinit();
|
||||
}
|
||||
|
||||
// TODO(sysaudio): implement waitEvents for WebAudio, will a WASM process terminate without this?
|
||||
pub fn waitEvents(_: Audio) void {}
|
||||
|
||||
const default_channel_count = 2;
|
||||
const default_sample_rate = 48000;
|
||||
const default_buffer_size_per_channel = 1024; // 21.33ms
|
||||
|
||||
pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device {
|
||||
// NOTE: WebAudio only supports F32 audio format, so options.format is unused
|
||||
const mode = options.mode;
|
||||
const channels = options.channels orelse default_channel_count;
|
||||
const sample_rate = options.sample_rate orelse default_sample_rate;
|
||||
|
||||
const context_options = js.createMap();
|
||||
defer context_options.deinit();
|
||||
context_options.set("sampleRate", js.createNumber(@intToFloat(f64, sample_rate)));
|
||||
|
||||
const context = audio.context_constructor.construct(&.{context_options.toValue()});
|
||||
_ = context.call("suspend", &.{});
|
||||
|
||||
const input_channels = if (mode == .input) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined();
|
||||
const output_channels = if (mode == .output) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined();
|
||||
|
||||
const node = context.call("createScriptProcessor", &.{ js.createNumber(default_buffer_size_per_channel), input_channels, output_channels }).view(.object);
|
||||
defer node.deinit();
|
||||
|
||||
context.set("node", node.toValue());
|
||||
|
||||
{
|
||||
// TODO(sysaudio): this capture leaks for now, we need a better way to pass captures via sysjs
|
||||
// that passes by value I think.
|
||||
const captures = std.heap.page_allocator.alloc(js.Value, 1) catch unreachable;
|
||||
captures[0] = context.toValue();
|
||||
const audio_process_event = js.createFunction(audioProcessEvent, captures);
|
||||
|
||||
// TODO(sysaudio): this leaks, we need a good place to clean this up.
|
||||
// defer audio_process_event.deinit();
|
||||
node.set("onaudioprocess", audio_process_event.toValue());
|
||||
}
|
||||
|
||||
{
|
||||
const destination = context.get("destination").view(.object);
|
||||
defer destination.deinit();
|
||||
_ = node.call("connect", &.{destination.toValue()});
|
||||
}
|
||||
|
||||
// TODO(sysaudio): Figure out ID/name or make optional again
|
||||
var properties = Device.Properties{
|
||||
.id = "0",
|
||||
.name = "WebAudio",
|
||||
.format = .F32,
|
||||
.mode = options.mode,
|
||||
.is_raw = false,
|
||||
.channels = options.channels orelse default_channel_count,
|
||||
.sample_rate = options.sample_rate orelse default_sample_rate,
|
||||
};
|
||||
|
||||
const device = try allocator.create(Device);
|
||||
device.* = .{
|
||||
.properties = properties,
|
||||
.context = context,
|
||||
};
|
||||
return device;
|
||||
}
|
||||
|
||||
fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value {
|
||||
const device_context = captures[0].view(.object);
|
||||
|
||||
const audio_event = args.getIndex(0).view(.object);
|
||||
defer audio_event.deinit();
|
||||
const output_buffer = audio_event.get("outputBuffer").view(.object);
|
||||
defer output_buffer.deinit();
|
||||
const num_channels = @floatToInt(usize, output_buffer.get("numberOfChannels").view(.num));
|
||||
|
||||
const buffer_length = default_buffer_size_per_channel * num_channels * @sizeOf(f32);
|
||||
// TODO(sysaudio): reuse buffer, do not allocate in this hot path
|
||||
const buffer = std.heap.page_allocator.alloc(u8, buffer_length) catch unreachable;
|
||||
defer std.heap.page_allocator.free(buffer);
|
||||
|
||||
const callback = device_context.get("callback");
|
||||
if (!callback.is(.undefined)) {
|
||||
var dev = @intToPtr(*Device, @floatToInt(usize, device_context.get("device").view(.num)));
|
||||
const cb = @intToPtr(DataCallback, @floatToInt(usize, callback.view(.num)));
|
||||
const user_data = device_context.get("user_data");
|
||||
const ud = if (user_data.is(.undefined)) null else @intToPtr(*anyopaque, @floatToInt(usize, user_data.view(.num)));
|
||||
|
||||
// TODO(sysaudio): do not reconstruct Uint8Array (expensive)
|
||||
const source = js.constructType("Uint8Array", &.{js.createNumber(@intToFloat(f64, buffer_length))});
|
||||
defer source.deinit();
|
||||
|
||||
cb(dev, ud, buffer[0..]);
|
||||
source.copyBytes(buffer[0..]);
|
||||
|
||||
const float_source = js.constructType("Float32Array", &.{
|
||||
source.get("buffer"),
|
||||
source.get("byteOffset"),
|
||||
js.createNumber(source.get("byteLength").view(.num) / 4),
|
||||
});
|
||||
defer float_source.deinit();
|
||||
|
||||
js.global().set("source", source.toValue());
|
||||
js.global().set("float_source", float_source.toValue());
|
||||
js.global().set("output_buffer", output_buffer.toValue());
|
||||
|
||||
var channel: usize = 0;
|
||||
while (channel < num_channels) : (channel += 1) {
|
||||
// TODO(sysaudio): investigate if using copyToChannel would be better?
|
||||
//_ = output_buffer.call("copyToChannel", &.{ float_source.toValue(), js.createNumber(@intToFloat(f64, channel)) });
|
||||
const output_data = output_buffer.call("getChannelData", &.{js.createNumber(@intToFloat(f64, channel))}).view(.object);
|
||||
defer output_data.deinit();
|
||||
const channel_slice = float_source.call("slice", &.{
|
||||
js.createNumber(@intToFloat(f64, channel * default_buffer_size_per_channel)),
|
||||
js.createNumber(@intToFloat(f64, (channel + 1) * default_buffer_size_per_channel)),
|
||||
});
|
||||
_ = output_data.call("set", &.{channel_slice});
|
||||
}
|
||||
}
|
||||
|
||||
return js.createUndefined();
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(audio: Audio) DeviceIterator {
|
||||
return .{ .audio = audio, .mode = .output };
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(audio: Audio) DeviceIterator {
|
||||
return .{ .audio = audio, .mode = .input };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue