src/sysaudio: move mach-sysaudio@ce8ab30dd300b822224d14997c58c06520b642c9 package to here

Helps hexops/mach#1165

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-04 18:44:39 -07:00 committed by Stephen Gutekanst
parent d64d30c7db
commit bca1543391
16 changed files with 7876 additions and 0 deletions

835
src/sysaudio/alsa.zig Normal file
View file

@ -0,0 +1,835 @@
const std = @import("std");
const c = @cImport(@cInclude("alsa/asoundlib.h"));
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const inotify_event = std.os.linux.inotify_event;
const is_little = @import("builtin").cpu.arch.endian() == .little;
const default_sample_rate = 44_100; // Hz
var lib: Lib = undefined;
const Lib = struct {
handle: std.DynLib,
snd_lib_error_set_handler: *const fn (c.snd_lib_error_handler_t) callconv(.C) c_int,
snd_pcm_info_malloc: *const fn ([*c]?*c.snd_pcm_info_t) callconv(.C) c_int,
snd_pcm_info_free: *const fn (?*c.snd_pcm_info_t) callconv(.C) void,
snd_pcm_open: *const fn ([*c]?*c.snd_pcm_t, [*c]const u8, c.snd_pcm_stream_t, c_int) callconv(.C) c_int,
snd_pcm_close: *const fn (?*c.snd_pcm_t) callconv(.C) c_int,
snd_pcm_state: *const fn (?*c.snd_pcm_t) callconv(.C) c.snd_pcm_state_t,
snd_pcm_pause: *const fn (?*c.snd_pcm_t, c_int) callconv(.C) c_int,
snd_pcm_writei: *const fn (?*c.snd_pcm_t, ?*const anyopaque, c.snd_pcm_uframes_t) callconv(.C) c.snd_pcm_sframes_t,
snd_pcm_readi: *const fn (?*c.snd_pcm_t, ?*const anyopaque, c.snd_pcm_uframes_t) callconv(.C) c.snd_pcm_sframes_t,
snd_pcm_prepare: *const fn (?*c.snd_pcm_t) callconv(.C) c_int,
snd_pcm_info_set_device: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void,
snd_pcm_info_set_subdevice: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void,
snd_pcm_info_get_name: *const fn (?*const c.snd_pcm_info_t) callconv(.C) [*c]const u8,
snd_pcm_info_set_stream: *const fn (?*c.snd_pcm_info_t, c.snd_pcm_stream_t) callconv(.C) void,
snd_pcm_hw_free: *const fn (?*c.snd_pcm_t) callconv(.C) c_int,
snd_pcm_hw_params_malloc: *const fn ([*c]?*c.snd_pcm_hw_params_t) callconv(.C) c_int,
snd_pcm_hw_params_free: *const fn (?*c.snd_pcm_hw_params_t) callconv(.C) void,
snd_pcm_set_params: *const fn (?*c.snd_pcm_t, c.snd_pcm_format_t, c.snd_pcm_access_t, c_uint, c_uint, c_int, c_uint) callconv(.C) c_int,
snd_pcm_hw_params_any: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int,
snd_pcm_hw_params_can_pause: *const fn (?*const c.snd_pcm_hw_params_t) callconv(.C) c_int,
snd_pcm_hw_params_current: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int,
snd_pcm_hw_params_get_format_mask: *const fn (?*c.snd_pcm_hw_params_t, ?*c.snd_pcm_format_mask_t) callconv(.C) void,
snd_pcm_hw_params_get_rate_min: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int,
snd_pcm_hw_params_get_rate_max: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int,
snd_pcm_hw_params_get_period_size: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c.snd_pcm_uframes_t, [*c]c_int) callconv(.C) c_int,
snd_pcm_query_chmaps: *const fn (?*c.snd_pcm_t) callconv(.C) [*c][*c]c.snd_pcm_chmap_query_t,
snd_pcm_free_chmaps: *const fn ([*c][*c]c.snd_pcm_chmap_query_t) callconv(.C) void,
snd_pcm_format_mask_malloc: *const fn ([*c]?*c.snd_pcm_format_mask_t) callconv(.C) c_int,
snd_pcm_format_mask_free: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void,
snd_pcm_format_mask_none: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void,
snd_pcm_format_mask_set: *const fn (?*c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) void,
snd_pcm_format_mask_test: *const fn (?*const c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) c_int,
snd_card_next: *const fn ([*c]c_int) callconv(.C) c_int,
snd_ctl_open: *const fn ([*c]?*c.snd_ctl_t, [*c]const u8, c_int) callconv(.C) c_int,
snd_ctl_close: *const fn (?*c.snd_ctl_t) callconv(.C) c_int,
snd_ctl_pcm_next_device: *const fn (?*c.snd_ctl_t, [*c]c_int) callconv(.C) c_int,
snd_ctl_pcm_info: *const fn (?*c.snd_ctl_t, ?*c.snd_pcm_info_t) callconv(.C) c_int,
snd_mixer_open: *const fn ([*c]?*c.snd_mixer_t, c_int) callconv(.C) c_int,
snd_mixer_close: *const fn (?*c.snd_mixer_t) callconv(.C) c_int,
snd_mixer_load: *const fn (?*c.snd_mixer_t) callconv(.C) c_int,
snd_mixer_attach: *const fn (?*c.snd_mixer_t, [*c]const u8) callconv(.C) c_int,
snd_mixer_find_selem: *const fn (?*c.snd_mixer_t, ?*const c.snd_mixer_selem_id_t) callconv(.C) ?*c.snd_mixer_elem_t,
snd_mixer_selem_register: *const fn (?*c.snd_mixer_t, [*c]c.struct_snd_mixer_selem_regopt, [*c]?*c.snd_mixer_class_t) callconv(.C) c_int,
snd_mixer_selem_id_malloc: *const fn ([*c]?*c.snd_mixer_selem_id_t) callconv(.C) c_int,
snd_mixer_selem_id_free: *const fn (?*c.snd_mixer_selem_id_t) callconv(.C) void,
snd_mixer_selem_id_set_index: *const fn (?*c.snd_mixer_selem_id_t, c_uint) callconv(.C) void,
snd_mixer_selem_id_set_name: *const fn (?*c.snd_mixer_selem_id_t, [*c]const u8) callconv(.C) void,
snd_mixer_selem_set_playback_volume_all: *const fn (?*c.snd_mixer_elem_t, c_long) callconv(.C) c_int,
snd_mixer_selem_get_playback_volume: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t, [*c]c_long) callconv(.C) c_int,
snd_mixer_selem_get_playback_volume_range: *const fn (?*c.snd_mixer_elem_t, [*c]c_long, [*c]c_long) callconv(.C) c_int,
snd_mixer_selem_has_playback_channel: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t) callconv(.C) c_int,
snd_mixer_selem_set_capture_volume_all: *const fn (?*c.snd_mixer_elem_t, c_long) callconv(.C) c_int,
snd_mixer_selem_get_capture_volume: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t, [*c]c_long) callconv(.C) c_int,
snd_mixer_selem_get_capture_volume_range: *const fn (?*c.snd_mixer_elem_t, [*c]c_long, [*c]c_long) callconv(.C) c_int,
snd_mixer_selem_has_capture_channel: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t) callconv(.C) c_int,
pub fn load() !void {
lib.handle = std.DynLib.openZ("libasound.so") catch return error.LibraryNotFound;
inline for (@typeInfo(Lib).Struct.fields[1..]) |field| {
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]);
@field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup;
}
}
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
watcher: ?Watcher,
const Watcher = struct {
deviceChangeFn: main.Context.DeviceChangeFn,
user_data: ?*anyopaque,
thread: std.Thread,
aborted: std.atomic.Value(bool),
notify_fd: std.os.fd_t,
notify_wd: std.os.fd_t,
notify_pipe_fd: [2]std.os.fd_t,
};
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
try Lib.load();
_ = lib.snd_lib_error_set_handler(@as(c.snd_lib_error_handler_t, @ptrCast(&util.doNothing)));
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
.watcher = blk: {
if (options.deviceChangeFn) |deviceChangeFn| {
const notify_fd = std.os.inotify_init1(std.os.linux.IN.NONBLOCK) catch |err| switch (err) {
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
error.SystemResources,
=> return error.SystemResources,
error.Unexpected => unreachable,
};
errdefer std.os.close(notify_fd);
const notify_wd = std.os.inotify_add_watch(
notify_fd,
"/dev/snd",
std.os.linux.IN.CREATE | std.os.linux.IN.DELETE,
) catch |err| switch (err) {
error.AccessDenied => return error.AccessDenied,
error.UserResourceLimitReached,
error.NotDir,
error.FileNotFound,
error.SystemResources,
=> return error.SystemResources,
error.NameTooLong,
error.WatchAlreadyExists,
error.Unexpected,
=> unreachable,
};
errdefer std.os.inotify_rm_watch(notify_fd, notify_wd);
const notify_pipe_fd = std.os.pipe2(std.os.O.NONBLOCK) catch |err| switch (err) {
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
=> return error.SystemResources,
error.Unexpected => unreachable,
};
errdefer {
std.os.close(notify_pipe_fd[0]);
std.os.close(notify_pipe_fd[1]);
}
break :blk .{
.deviceChangeFn = deviceChangeFn,
.user_data = options.user_data,
.aborted = .{ .raw = false },
.notify_fd = notify_fd,
.notify_wd = notify_wd,
.notify_pipe_fd = notify_pipe_fd,
.thread = std.Thread.spawn(.{}, deviceEventsLoop, .{ctx}) catch |err| switch (err) {
error.ThreadQuotaExceeded,
error.SystemResources,
error.LockedMemoryLimitExceeded,
=> return error.SystemResources,
error.OutOfMemory => return error.OutOfMemory,
error.Unexpected => unreachable,
},
};
}
break :blk null;
},
};
return .{ .alsa = ctx };
}
pub fn deinit(ctx: *Context) void {
if (ctx.watcher) |*watcher| {
watcher.aborted.store(true, .Unordered);
_ = std.os.write(watcher.notify_pipe_fd[1], "a") catch {};
watcher.thread.join();
std.os.close(watcher.notify_pipe_fd[0]);
std.os.close(watcher.notify_pipe_fd[1]);
std.os.inotify_rm_watch(watcher.notify_fd, watcher.notify_wd);
std.os.close(watcher.notify_fd);
}
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
ctx.allocator.destroy(ctx);
lib.handle.close();
}
fn deviceEventsLoop(ctx: *Context) void {
var watcher = ctx.watcher.?;
var scan = false;
var last_crash: ?i64 = null;
var buf: [2048]u8 = undefined;
var fds = [2]std.os.pollfd{
.{
.fd = watcher.notify_fd,
.events = std.os.POLL.IN,
.revents = 0,
},
.{
.fd = watcher.notify_pipe_fd[0],
.events = std.os.POLL.IN,
.revents = 0,
},
};
while (!watcher.aborted.load(.Unordered)) {
_ = std.os.poll(&fds, -1) catch |err| switch (err) {
error.NetworkSubsystemFailed,
error.SystemResources,
=> {
const ts = std.time.milliTimestamp();
if (last_crash) |lc| {
if (ts - lc < 500) return;
}
last_crash = ts;
continue;
},
error.Unexpected => unreachable,
};
if (watcher.notify_fd & std.os.POLL.IN != 0) {
while (true) {
const len = std.os.read(watcher.notify_fd, &buf) catch |err| {
if (err == error.WouldBlock) break;
const ts = std.time.milliTimestamp();
if (last_crash) |lc| {
if (ts - lc < 500) return;
}
last_crash = ts;
break;
};
if (len == 0) break;
var i: usize = 0;
var evt: *inotify_event = undefined;
while (i < buf.len) : (i += @sizeOf(inotify_event) + evt.len) {
evt = @as(*inotify_event, @ptrCast(@alignCast(buf[i..])));
const evt_name = @as([*]u8, @ptrCast(buf[i..]))[@sizeOf(inotify_event) .. @sizeOf(inotify_event) + 8];
if (evt.mask & std.os.linux.IN.ISDIR != 0 or !std.mem.startsWith(u8, evt_name, "pcm"))
continue;
scan = true;
}
}
}
if (scan) {
watcher.deviceChangeFn(ctx.watcher.?.user_data);
scan = false;
}
}
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
var pcm_info: ?*c.snd_pcm_info_t = null;
_ = lib.snd_pcm_info_malloc(&pcm_info);
defer lib.snd_pcm_info_free(pcm_info);
var card_idx: c_int = -1;
if (lib.snd_card_next(&card_idx) < 0)
return error.SystemResources;
while (card_idx >= 0) {
var card_id_buf: [8]u8 = undefined;
const card_id = std.fmt.bufPrintZ(&card_id_buf, "hw:{d}", .{card_idx}) catch break;
var ctl: ?*c.snd_ctl_t = undefined;
_ = switch (-lib.snd_ctl_open(&ctl, card_id.ptr, 0)) {
0 => {},
@intFromEnum(std.os.E.NOENT) => break,
else => return error.OpeningDevice,
};
defer _ = lib.snd_ctl_close(ctl);
var dev_idx: c_int = -1;
if (lib.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0)
return error.SystemResources;
lib.snd_pcm_info_set_device(pcm_info, @as(c_uint, @intCast(dev_idx)));
lib.snd_pcm_info_set_subdevice(pcm_info, 0);
const name = std.mem.span(lib.snd_pcm_info_get_name(pcm_info) orelse continue);
for (&[_]main.Device.Mode{ .playback, .capture }) |mode| {
const snd_stream = modeToStream(mode);
lib.snd_pcm_info_set_stream(pcm_info, snd_stream);
const err = lib.snd_ctl_pcm_info(ctl, pcm_info);
switch (@as(std.os.E, @enumFromInt(-err))) {
.SUCCESS => {},
.NOENT,
.NXIO,
.NODEV,
=> break,
else => return error.SystemResources,
}
var buf: [9]u8 = undefined; // 'hw' + max(card|device) * 2 + ':' + \0
const id = std.fmt.bufPrintZ(&buf, "hw:{d},{d}", .{ card_idx, dev_idx }) catch continue;
var pcm: ?*c.snd_pcm_t = null;
if (lib.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0)
continue;
defer _ = lib.snd_pcm_close(pcm);
var params: ?*c.snd_pcm_hw_params_t = null;
_ = lib.snd_pcm_hw_params_malloc(&params);
defer lib.snd_pcm_hw_params_free(params);
if (lib.snd_pcm_hw_params_any(pcm, params) < 0)
continue;
if (lib.snd_pcm_hw_params_can_pause(params) == 0)
continue;
const device = main.Device{
.mode = mode,
.channels = blk: {
const chmap = lib.snd_pcm_query_chmaps(pcm);
if (chmap) |_| {
defer lib.snd_pcm_free_chmaps(chmap);
if (chmap[0] == null) continue;
const channels = try ctx.allocator.alloc(main.ChannelPosition, chmap.*.*.map.channels);
for (channels, 0..) |*ch, i|
ch.* = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice;
break :blk channels;
} else {
continue;
}
},
.formats = blk: {
var fmt_mask: ?*c.snd_pcm_format_mask_t = null;
_ = lib.snd_pcm_format_mask_malloc(&fmt_mask);
defer lib.snd_pcm_format_mask_free(fmt_mask);
lib.snd_pcm_format_mask_none(fmt_mask);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE);
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE);
lib.snd_pcm_hw_params_get_format_mask(params, fmt_mask);
var fmt_arr = std.ArrayList(main.Format).init(ctx.allocator);
inline for (std.meta.tags(main.Format)) |format| {
if (lib.snd_pcm_format_mask_test(fmt_mask, toAlsaFormat(format)) != 0) {
try fmt_arr.append(format);
}
}
break :blk try fmt_arr.toOwnedSlice();
},
.sample_rate = blk: {
var rate_min: c_uint = 0;
var rate_max: c_uint = 0;
if (lib.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0)
continue;
if (lib.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0)
continue;
break :blk .{
.min = @as(u24, @intCast(rate_min)),
.max = @as(u24, @intCast(rate_max)),
};
},
.id = try ctx.allocator.dupeZ(u8, id),
.name = try ctx.allocator.dupeZ(u8, name),
};
try ctx.devices_info.list.append(ctx.allocator, device);
if (ctx.devices_info.default(mode) == null and dev_idx == 0) {
ctx.devices_info.setDefault(mode, ctx.devices_info.list.items.len - 1);
}
}
if (lib.snd_card_next(&card_idx) < 0)
return error.SystemResources;
}
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createStream(
ctx: Context,
device: main.Device,
format: main.Format,
sample_rate: u24,
pcm: *?*c.snd_pcm_t,
mixer: *?*c.snd_mixer_t,
selem: *?*c.snd_mixer_selem_id_t,
mixer_elm: *?*c.snd_mixer_elem_t,
period_size: *c_ulong,
) !void {
if (lib.snd_pcm_open(pcm, device.id.ptr, modeToStream(device.mode), 0) < 0)
return error.OpeningDevice;
errdefer _ = lib.snd_pcm_close(pcm.*);
{
var hw_params: ?*c.snd_pcm_hw_params_t = null;
if ((lib.snd_pcm_set_params(
pcm.*,
toAlsaFormat(format),
c.SND_PCM_ACCESS_RW_INTERLEAVED,
@as(c_uint, @intCast(device.channels.len)),
sample_rate,
1,
main.default_latency,
)) < 0)
return error.OpeningDevice;
errdefer _ = lib.snd_pcm_hw_free(pcm.*);
if (lib.snd_pcm_hw_params_malloc(&hw_params) < 0)
return error.OpeningDevice;
defer lib.snd_pcm_hw_params_free(hw_params);
if (lib.snd_pcm_hw_params_current(pcm.*, hw_params) < 0)
return error.OpeningDevice;
if (lib.snd_pcm_hw_params_get_period_size(hw_params, period_size, null) < 0)
return error.OpeningDevice;
}
{
if (lib.snd_mixer_open(mixer, 0) < 0)
return error.OutOfMemory;
const card_id = try ctx.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ','));
defer ctx.allocator.free(card_id);
if (lib.snd_mixer_attach(mixer.*, card_id.ptr) < 0)
return error.IncompatibleDevice;
if (lib.snd_mixer_selem_register(mixer.*, null, null) < 0)
return error.OpeningDevice;
if (lib.snd_mixer_load(mixer.*) < 0)
return error.OpeningDevice;
if (lib.snd_mixer_selem_id_malloc(selem) < 0)
return error.OutOfMemory;
errdefer lib.snd_mixer_selem_id_free(selem.*);
lib.snd_mixer_selem_id_set_index(selem.*, 0);
lib.snd_mixer_selem_id_set_name(selem.*, "Master");
mixer_elm.* = lib.snd_mixer_find_selem(mixer.*, selem.*) orelse
return error.IncompatibleDevice;
}
}
pub fn createPlayer(ctx: Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
const format = device.preferredFormat(options.format);
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
var pcm: ?*c.snd_pcm_t = null;
var mixer: ?*c.snd_mixer_t = null;
var selem: ?*c.snd_mixer_selem_id_t = null;
var mixer_elm: ?*c.snd_mixer_elem_t = null;
var period_size: c_ulong = 0;
try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size);
const player = try ctx.allocator.create(Player);
player.* = .{
.allocator = ctx.allocator,
.thread = undefined,
.aborted = .{ .raw = false },
.sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))),
.period_size = period_size,
.pcm = pcm.?,
.mixer = mixer.?,
.selem = selem.?,
.mixer_elm = mixer_elm.?,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = format,
.sample_rate = sample_rate,
};
return .{ .alsa = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
const format = device.preferredFormat(options.format);
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
var pcm: ?*c.snd_pcm_t = null;
var mixer: ?*c.snd_mixer_t = null;
var selem: ?*c.snd_mixer_selem_id_t = null;
var mixer_elm: ?*c.snd_mixer_elem_t = null;
var period_size: c_ulong = 0;
try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size);
const recorder = try ctx.allocator.create(Recorder);
recorder.* = .{
.allocator = ctx.allocator,
.thread = undefined,
.aborted = .{ .raw = false },
.sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))),
.period_size = period_size,
.pcm = pcm.?,
.mixer = mixer.?,
.selem = selem.?,
.mixer_elm = mixer_elm.?,
.readFn = readFn,
.user_data = options.user_data,
.channels = device.channels,
.format = format,
.sample_rate = sample_rate,
};
return .{ .alsa = recorder };
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
thread: std.Thread,
aborted: std.atomic.Value(bool),
sample_buffer: []u8,
period_size: c_ulong,
pcm: *c.snd_pcm_t,
mixer: *c.snd_mixer_t,
selem: *c.snd_mixer_selem_id_t,
mixer_elm: *c.snd_mixer_elem_t,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(player: *Player) void {
player.aborted.store(true, .Unordered);
player.thread.join();
_ = lib.snd_mixer_close(player.mixer);
lib.snd_mixer_selem_id_free(player.selem);
_ = lib.snd_pcm_close(player.pcm);
_ = lib.snd_pcm_hw_free(player.pcm);
player.allocator.free(player.sample_buffer);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
player.thread = std.Thread.spawn(.{}, writeThread, .{player}) catch |err| switch (err) {
error.ThreadQuotaExceeded,
error.SystemResources,
error.LockedMemoryLimitExceeded,
=> return error.SystemResources,
error.OutOfMemory => return error.OutOfMemory,
error.Unexpected => unreachable,
};
}
fn writeThread(player: *Player) void {
var underrun = false;
while (!player.aborted.load(.Unordered)) {
if (!underrun) {
player.writeFn(
player.user_data,
player.sample_buffer[0 .. player.period_size * player.format.frameSize(@intCast(player.channels.len))],
);
}
underrun = false;
const n = lib.snd_pcm_writei(player.pcm, player.sample_buffer.ptr, player.period_size);
if (n < 0) {
_ = lib.snd_pcm_prepare(player.pcm);
underrun = true;
}
}
}
pub fn play(player: *Player) !void {
if (lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED) {
if (lib.snd_pcm_pause(player.pcm, 0) < 0)
return error.CannotPlay;
}
}
pub fn pause(player: *Player) !void {
if (lib.snd_pcm_state(player.pcm) != c.SND_PCM_STATE_PAUSED) {
if (lib.snd_pcm_pause(player.pcm, 1) < 0)
return error.CannotPause;
}
}
pub fn paused(player: *Player) bool {
return lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED;
}
pub fn setVolume(player: *Player, vol: f32) !void {
var min_vol: c_long = 0;
var max_vol: c_long = 0;
if (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0)
return error.CannotSetVolume;
const dist = @as(f32, @floatFromInt(max_vol - min_vol));
if (lib.snd_mixer_selem_set_playback_volume_all(
player.mixer_elm,
@as(c_long, @intFromFloat(dist * vol)) + min_vol,
) < 0)
return error.CannotSetVolume;
}
pub fn volume(player: *Player) !f32 {
var vol: c_long = 0;
var channel: c_int = 0;
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
if (lib.snd_mixer_selem_has_playback_channel(player.mixer_elm, channel) == 1) {
if (lib.snd_mixer_selem_get_playback_volume(player.mixer_elm, channel, &vol) == 0)
break;
}
}
if (channel == c.SND_MIXER_SCHN_LAST)
return error.CannotGetVolume;
var min_vol: c_long = 0;
var max_vol: c_long = 0;
if (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0)
return error.CannotGetVolume;
return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol));
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
thread: std.Thread,
aborted: std.atomic.Value(bool),
sample_buffer: []u8,
period_size: c_ulong,
pcm: *c.snd_pcm_t,
mixer: *c.snd_mixer_t,
selem: *c.snd_mixer_selem_id_t,
mixer_elm: *c.snd_mixer_elem_t,
readFn: main.ReadFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(recorder: *Recorder) void {
recorder.aborted.store(true, .Unordered);
recorder.thread.join();
_ = lib.snd_mixer_close(recorder.mixer);
lib.snd_mixer_selem_id_free(recorder.selem);
_ = lib.snd_pcm_close(recorder.pcm);
_ = lib.snd_pcm_hw_free(recorder.pcm);
recorder.allocator.free(recorder.sample_buffer);
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
recorder.thread = std.Thread.spawn(.{}, readThread, .{recorder}) catch |err| switch (err) {
error.ThreadQuotaExceeded,
error.SystemResources,
error.LockedMemoryLimitExceeded,
=> return error.SystemResources,
error.OutOfMemory => return error.OutOfMemory,
error.Unexpected => unreachable,
};
}
fn readThread(recorder: *Recorder) void {
var underrun = false;
while (!recorder.aborted.load(.Unordered)) {
if (!underrun) {
recorder.readFn(recorder.user_data, recorder.sample_buffer[0..recorder.period_size]);
}
underrun = false;
const n = lib.snd_pcm_readi(recorder.pcm, recorder.sample_buffer.ptr, recorder.period_size);
if (n < 0) {
_ = lib.snd_pcm_prepare(recorder.pcm);
underrun = true;
}
}
}
pub fn record(recorder: *Recorder) !void {
if (lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED) {
if (lib.snd_pcm_pause(recorder.pcm, 0) < 0)
return error.CannotRecord;
}
}
pub fn pause(recorder: *Recorder) !void {
if (lib.snd_pcm_state(recorder.pcm) != c.SND_PCM_STATE_PAUSED) {
if (lib.snd_pcm_pause(recorder.pcm, 1) < 0)
return error.CannotPause;
}
}
pub fn paused(recorder: *Recorder) bool {
return lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
var min_vol: c_long = 0;
var max_vol: c_long = 0;
if (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0)
return error.CannotSetVolume;
const dist = @as(f32, @floatFromInt(max_vol - min_vol));
if (lib.snd_mixer_selem_set_capture_volume_all(
recorder.mixer_elm,
@as(c_long, @intFromFloat(dist * vol)) + min_vol,
) < 0)
return error.CannotSetVolume;
}
pub fn volume(recorder: *Recorder) !f32 {
var vol: c_long = 0;
var channel: c_int = 0;
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
if (lib.snd_mixer_selem_has_capture_channel(recorder.mixer_elm, channel) == 1) {
if (lib.snd_mixer_selem_get_capture_volume(recorder.mixer_elm, channel, &vol) == 0)
break;
}
}
if (channel == c.SND_MIXER_SCHN_LAST)
return error.CannotGetVolume;
var min_vol: c_long = 0;
var max_vol: c_long = 0;
if (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0)
return error.CannotGetVolume;
return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol));
}
};
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.id);
allocator.free(device.name);
allocator.free(device.formats);
allocator.free(device.channels);
}
pub fn modeToStream(mode: main.Device.Mode) c_uint {
return switch (mode) {
.playback => c.SND_PCM_STREAM_PLAYBACK,
.capture => c.SND_PCM_STREAM_CAPTURE,
};
}
pub fn toAlsaFormat(format: main.Format) c.snd_pcm_format_t {
return switch (format) {
.u8 => c.SND_PCM_FORMAT_U8,
.i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE,
.i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE,
.i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE,
.f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE,
};
}
pub fn fromAlsaChannel(pos: c_uint) !main.ChannelPosition {
return switch (pos) {
c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid,
c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center,
c.SND_CHMAP_FL => .front_left,
c.SND_CHMAP_FR => .front_right,
c.SND_CHMAP_LFE => .lfe,
c.SND_CHMAP_SL => .side_left,
c.SND_CHMAP_SR => .side_right,
c.SND_CHMAP_RC => .back_center,
c.SND_CHMAP_RLC => .back_left,
c.SND_CHMAP_RRC => .back_right,
c.SND_CHMAP_FLC => .front_left_center,
c.SND_CHMAP_FRC => .front_right_center,
c.SND_CHMAP_TC => .top_center,
c.SND_CHMAP_TFL => .top_front_left,
c.SND_CHMAP_TFR => .top_front_right,
c.SND_CHMAP_TFC => .top_front_center,
c.SND_CHMAP_TRL => .top_back_left,
c.SND_CHMAP_TRR => .top_back_right,
c.SND_CHMAP_TRC => .top_back_center,
else => return error.Invalid,
};
}
pub fn toCHMAP(pos: main.ChannelPosition) c_uint {
return switch (pos) {
.front_center => c.SND_CHMAP_FC,
.front_left => c.SND_CHMAP_FL,
.front_right => c.SND_CHMAP_FR,
.lfe => c.SND_CHMAP_LFE,
.side_left => c.SND_CHMAP_SL,
.side_right => c.SND_CHMAP_SR,
.back_center => c.SND_CHMAP_RC,
.back_left => c.SND_CHMAP_RLC,
.back_right => c.SND_CHMAP_RRC,
.front_left_center => c.SND_CHMAP_FLC,
.front_right_center => c.SND_CHMAP_FRC,
.top_center => c.SND_CHMAP_TC,
.top_front_left => c.SND_CHMAP_TFL,
.top_front_right => c.SND_CHMAP_TFR,
.top_front_center => c.SND_CHMAP_TFC,
.top_back_left => c.SND_CHMAP_TRL,
.top_back_right => c.SND_CHMAP_TRR,
.top_back_center => c.SND_CHMAP_TRC,
};
}

106
src/sysaudio/backends.zig Normal file
View file

@ -0,0 +1,106 @@
const builtin = @import("builtin");
const std = @import("std");
pub const Backend = std.meta.Tag(Context);
pub const Context = switch (builtin.os.tag) {
.linux => union(enum) {
pulseaudio: *@import("pulseaudio.zig").Context,
pipewire: *@import("pipewire.zig").Context,
jack: *@import("jack.zig").Context,
alsa: *@import("alsa.zig").Context,
dummy: *@import("dummy.zig").Context,
},
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
pipewire: *@import("pipewire.zig").Context,
pulseaudio: *@import("pulseaudio.zig").Context,
jack: *@import("jack.zig").Context,
dummy: *@import("dummy.zig").Context,
},
.macos, .ios, .watchos, .tvos => union(enum) {
coreaudio: *@import("coreaudio.zig").Context,
dummy: *@import("dummy.zig").Context,
},
.windows => union(enum) {
wasapi: *@import("wasapi.zig").Context,
dummy: *@import("dummy.zig").Context,
},
.freestanding => switch (builtin.cpu.arch) {
.wasm32 => union(enum) {
webaudio: *@import("webaudio.zig").Context,
dummy: *@import("dummy.zig").Context,
},
else => union(enum) {
dummy: *@import("dummy.zig").Context,
},
},
else => union(enum) { dummy: *@import("dummy.zig").Context },
};
pub const Player = switch (builtin.os.tag) {
.linux => union(enum) {
pulseaudio: *@import("pulseaudio.zig").Player,
pipewire: *@import("pipewire.zig").Player,
jack: *@import("jack.zig").Player,
alsa: *@import("alsa.zig").Player,
dummy: *@import("dummy.zig").Player,
},
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
pipewire: *@import("pipewire.zig").Player,
pulseaudio: *@import("pulseaudio.zig").Player,
jack: *@import("jack.zig").Player,
dummy: *@import("dummy.zig").Player,
},
.macos, .ios, .watchos, .tvos => union(enum) {
coreaudio: *@import("coreaudio.zig").Player,
dummy: *@import("dummy.zig").Player,
},
.windows => union(enum) {
wasapi: *@import("wasapi.zig").Player,
dummy: *@import("dummy.zig").Player,
},
.freestanding => switch (builtin.cpu.arch) {
.wasm32 => union(enum) {
webaudio: *@import("webaudio.zig").Player,
dummy: *@import("dummy.zig").Player,
},
else => union(enum) {
dummy: *@import("dummy.zig").Player,
},
},
else => union(enum) { dummy: *@import("dummy.zig").Player },
};
pub const Recorder = switch (builtin.os.tag) {
.linux => union(enum) {
pulseaudio: *@import("pulseaudio.zig").Recorder,
pipewire: *@import("pipewire.zig").Recorder,
jack: *@import("jack.zig").Recorder,
alsa: *@import("alsa.zig").Recorder,
dummy: *@import("dummy.zig").Recorder,
},
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
pipewire: *@import("pipewire.zig").Recorder,
pulseaudio: *@import("pulseaudio.zig").Recorder,
jack: *@import("jack.zig").Recorder,
dummy: *@import("dummy.zig").Recorder,
},
.macos, .ios, .watchos, .tvos => union(enum) {
coreaudio: *@import("coreaudio.zig").Recorder,
dummy: *@import("dummy.zig").Recorder,
},
.windows => union(enum) {
wasapi: *@import("wasapi.zig").Recorder,
dummy: *@import("dummy.zig").Recorder,
},
.freestanding => switch (builtin.cpu.arch) {
.wasm32 => union(enum) {
webaudio: *@import("webaudio.zig").Recorder,
dummy: *@import("dummy.zig").Recorder,
},
else => union(enum) {
dummy: *@import("dummy.zig").Recorder,
},
},
else => union(enum) { dummy: *@import("dummy.zig").Recorder },
};

349
src/sysaudio/conv.zig Normal file
View file

@ -0,0 +1,349 @@
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const shl = std.math.shl;
const shr = std.math.shr;
const maxInt = std.math.maxInt;
pub fn unsignedToSigned(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const half = (maxInt(SrcType) + 1) / 2;
const trunc = @bitSizeOf(DestType) - @bitSizeOf(SrcType);
dst_sample.* = shl(DestType, @intCast(src_sample.* -% half), trunc);
}
}
test unsignedToSigned {
var u8_to_i16: [1]i16 = undefined;
var u8_to_i24: [1]i24 = undefined;
var u8_to_i32: [1]i32 = undefined;
unsignedToSigned(u8, &.{5}, i16, &u8_to_i16);
unsignedToSigned(u8, &.{5}, i24, &u8_to_i24);
unsignedToSigned(u8, &.{5}, i32, &u8_to_i32);
try expectEqual(@as(i16, -31488), u8_to_i16[0]);
try expectEqual(@as(i24, -8060928), u8_to_i24[0]);
try expectEqual(@as(i32, -2063597568), u8_to_i32[0]);
}
pub fn unsignedToFloat(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const half = (maxInt(SrcType) + 1) / 2;
dst_sample.* = (@as(DestType, @floatFromInt(src_sample.*)) - half) * 1.0 / half;
}
}
test unsignedToFloat {
var u8_to_f32: [1]f32 = undefined;
unsignedToFloat(u8, &.{5}, f32, &u8_to_f32);
try expectEqual(@as(f32, -0.9609375), u8_to_f32[0]);
}
pub fn signedToUnsigned(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const half = (maxInt(DestType) + 1) / 2;
const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType);
dst_sample.* = shr(DestType, @intCast(src_sample.*), trunc) + half;
}
}
test signedToUnsigned {
var i16_to_u8: [1]u8 = undefined;
var i24_to_u8: [1]u8 = undefined;
var i32_to_u8: [1]u8 = undefined;
signedToUnsigned(i16, &.{5}, u8, &i16_to_u8);
signedToUnsigned(i24, &.{5}, u8, &i24_to_u8);
signedToUnsigned(i32, &.{5}, u8, &i32_to_u8);
try expectEqual(@as(u8, 128), i16_to_u8[0]);
try expectEqual(@as(u8, 128), i24_to_u8[0]);
try expectEqual(@as(u8, 128), i32_to_u8[0]);
}
pub fn signedToSigned(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
// TODO: Uncomment this (zig crashes)
// if (std.simd.suggestVectorSize(SrcType)) |_| {
// signedToSignedSIMD(SrcType, src, DestType, dst);
// } else {
signedToSignedScalar(SrcType, src, DestType, dst);
// }
}
pub fn signedToSignedScalar(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType);
dst_sample.* = shr(DestType, @intCast(src_sample.*), trunc);
}
}
test signedToSignedScalar {
var i16_to_i24: [1]i24 = undefined;
var i16_to_i32: [1]i32 = undefined;
var i24_to_i16: [1]i16 = undefined;
var i24_to_i32: [1]i32 = undefined;
var i32_to_i16: [1]i16 = undefined;
var i32_to_i24: [1]i24 = undefined;
signedToSignedScalar(i24, &.{5}, i16, &i24_to_i16);
signedToSignedScalar(i32, &.{5}, i16, &i32_to_i16);
signedToSignedScalar(i16, &.{5}, i24, &i16_to_i24);
signedToSignedScalar(i32, &.{5}, i24, &i32_to_i24);
signedToSignedScalar(i16, &.{5}, i32, &i16_to_i32);
signedToSignedScalar(i24, &.{5}, i32, &i24_to_i32);
try expectEqual(@as(i24, 1280), i16_to_i24[0]);
try expectEqual(@as(i32, 327680), i16_to_i32[0]);
try expectEqual(@as(i16, 0), i24_to_i16[0]);
try expectEqual(@as(i32, 1280), i24_to_i32[0]);
try expectEqual(@as(i16, 0), i32_to_i16[0]);
try expectEqual(@as(i24, 0), i32_to_i24[0]);
}
pub fn signedToSignedSIMD(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
const vec_size = std.simd.suggestVectorSize(SrcType).?;
const VecSrc = @Vector(vec_size, SrcType);
const VecDst = @Vector(vec_size, DestType);
const trunc = @bitSizeOf(SrcType) - @bitSizeOf(DestType);
const vec_blocks_len = src.len - (src.len % vec_size);
var i: usize = 0;
while (i < vec_blocks_len) : (i += vec_size) {
const src_vec: VecSrc = src[i..][0..vec_size].*;
dst[i..][0..vec_size].* = shr(VecDst, @intCast(src_vec), trunc);
}
if (i != src.len) signedToSignedScalar(SrcType, src[i..], DestType, dst[i..]);
}
test signedToSignedSIMD {
var i16_to_i32: [32 + 7]i32 = undefined;
const items = [1]i16{5} ** (32 + 7);
signedToSignedSIMD(i16, &items, i32, &i16_to_i32);
try expectEqual(@as(i32, 327680), i16_to_i32[0]);
try expectEqual(i16_to_i32[0], i16_to_i32[i16_to_i32.len - 1]);
}
pub fn signedToFloat(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
if (std.simd.suggestVectorSize(SrcType)) |_| {
signedToFloatSIMD(SrcType, src, DestType, dst);
} else {
signedToFloatScalar(SrcType, src, DestType, dst);
}
}
pub fn signedToFloatScalar(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
const max: comptime_float = maxInt(SrcType) + 1;
const div_by_max = 1.0 / max;
for (src, dst) |*src_sample, *dst_sample| {
dst_sample.* = @as(DestType, @floatFromInt(src_sample.*)) * div_by_max;
}
}
test signedToFloatScalar {
var i16_to_f32: [1]f32 = undefined;
var i24_to_f32: [1]f32 = undefined;
var i32_to_f32: [1]f32 = undefined;
signedToFloatScalar(i16, &.{5}, f32, &i16_to_f32);
signedToFloatScalar(i24, &.{5}, f32, &i24_to_f32);
signedToFloatScalar(i32, &.{5}, f32, &i32_to_f32);
try expectEqual(@as(f32, 1.52587890625e-4), i16_to_f32[0]);
try expectEqual(@as(f32, 5.9604644775391e-7), i24_to_f32[0]);
try expectEqual(@as(f32, 2.32830643e-09), i32_to_f32[0]);
}
pub fn signedToFloatSIMD(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
const vec_size = std.simd.suggestVectorSize(SrcType).?;
const VecSrc = @Vector(vec_size, SrcType);
const VecDst = @Vector(vec_size, DestType);
const div_by_max: VecDst = @splat(1.0 / @as(comptime_float, maxInt(SrcType) + 1));
const vec_blocks_len = src.len - (src.len % vec_size);
var i: usize = 0;
while (i < vec_blocks_len) : (i += vec_size) {
const src_vec: VecSrc = src[i..][0..vec_size].*;
dst[i..][0..vec_size].* = @as(VecDst, @floatFromInt(src_vec)) * div_by_max;
}
if (i != src.len) signedToFloatScalar(SrcType, src[i..], DestType, dst[i..]);
}
test signedToFloatSIMD {
var i32_to_f32: [32 + 7]f32 = undefined;
const items = [1]i32{5} ** (32 + 7);
signedToFloatSIMD(i32, &items, f32, &i32_to_f32);
try expectEqual(@as(f32, 2.32830643e-09), i32_to_f32[0]);
try expectEqual(i32_to_f32[0], i32_to_f32[i32_to_f32.len - 1]);
}
pub fn floatToUnsigned(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
if (std.simd.suggestVectorSize(SrcType)) |_| {
floatToUnsignedSIMD(SrcType, src, DestType, dst);
} else {
floatToUnsignedScalar(SrcType, src, DestType, dst);
}
}
pub fn floatToUnsignedScalar(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const half = maxInt(DestType) / 2;
dst_sample.* = @intFromFloat(src_sample.* * half + (half + 1));
}
}
test floatToUnsignedScalar {
var f32_to_u8: [1]u8 = undefined;
floatToUnsignedScalar(f32, &.{0.5}, u8, &f32_to_u8);
try expectEqual(@as(u8, 191), f32_to_u8[0]);
}
pub fn floatToUnsignedSIMD(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
const vec_size = std.simd.suggestVectorSize(SrcType).?;
const VecSrc = @Vector(vec_size, SrcType);
const VecDst = @Vector(vec_size, DestType);
const half: VecSrc = @splat(maxInt(DestType) / 2);
const half_plus_one: VecSrc = @splat(maxInt(DestType) / 2 + 1);
const vec_blocks_len = src.len - (src.len % vec_size);
var i: usize = 0;
while (i < vec_blocks_len) : (i += vec_size) {
const src_vec: VecSrc = src[i..][0..vec_size].*;
dst[i..][0..vec_size].* = @as(VecDst, @intFromFloat(src_vec * half + half_plus_one));
}
if (i != src.len) floatToUnsignedScalar(SrcType, src[i..], DestType, dst[i..]);
}
test floatToUnsignedSIMD {
var f32_to_u8: [32 + 7]u8 = undefined;
const items = [1]f32{0.5} ** (32 + 7);
floatToUnsignedSIMD(f32, &items, u8, &f32_to_u8);
try expectEqual(@as(u8, 191), f32_to_u8[0]);
try expectEqual(f32_to_u8[0], f32_to_u8[f32_to_u8.len - 1]);
}
pub fn floatToSigned(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
if (std.simd.suggestVectorSize(SrcType)) |_| {
floatToSignedSIMD(SrcType, src, DestType, dst);
} else {
floatToSignedScalar(SrcType, src, DestType, dst);
}
}
pub fn floatToSignedScalar(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
for (src, dst) |*src_sample, *dst_sample| {
const max = maxInt(DestType) + 1;
dst_sample.* = @truncate(@as(i32, @intFromFloat(src_sample.* * max)));
}
}
test floatToSignedScalar {
var f32_to_i16: [1]i16 = undefined;
var f32_to_i24: [1]i24 = undefined;
var f32_to_i32: [1]i32 = undefined;
floatToSignedScalar(f32, &.{0.5}, i16, &f32_to_i16);
floatToSignedScalar(f32, &.{0.5}, i24, &f32_to_i24);
floatToSignedScalar(f32, &.{0.5}, i32, &f32_to_i32);
try expectEqual(@as(i16, 16384), f32_to_i16[0]);
try expectEqual(@as(i24, 4194304), f32_to_i24[0]);
try expectEqual(@as(i32, 1073741824), f32_to_i32[0]);
}
pub fn floatToSignedSIMD(
comptime SrcType: type,
src: []const SrcType,
comptime DestType: type,
dst: []DestType,
) void {
const vec_size = std.simd.suggestVectorSize(SrcType).?;
const VecSrc = @Vector(vec_size, SrcType);
const VecDst = @Vector(vec_size, DestType);
const max: VecSrc = @splat(maxInt(DestType) + 1);
const vec_blocks_len = src.len - (src.len % vec_size);
var i: usize = 0;
while (i < vec_blocks_len) : (i += vec_size) {
const src_vec: VecSrc = src[i..][0..vec_size].*;
dst[i..][0..vec_size].* = @as(VecDst, @intFromFloat(src_vec * max));
}
if (i != src.len) floatToSignedScalar(SrcType, src[i..], DestType, dst[i..]);
}
test floatToSignedSIMD {
var f32_to_i16: [32 + 7]i16 = undefined;
const items = [1]f32{0.5} ** (32 + 7);
floatToSignedSIMD(f32, &items, i16, &f32_to_i16);
try expectEqual(@as(i16, 16384), f32_to_i16[0]);
try expectEqual(f32_to_i16[0], f32_to_i16[f32_to_i16.len - 1]);
}

769
src/sysaudio/coreaudio.zig Normal file
View file

@ -0,0 +1,769 @@
const std = @import("std");
const builtin = @import("builtin");
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const c = @cImport({
@cInclude("CoreAudio/CoreAudio.h");
@cInclude("AudioUnit/AudioUnit.h");
});
const avaudio = @import("objc").avf_audio.avaudio;
const native_endian = builtin.cpu.arch.endian();
var is_darling = false;
const default_sample_rate = 44_100; // Hz
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
_ = options;
if (std.fs.accessAbsolute("/usr/lib/darling", .{})) {
is_darling = true;
} else |_| {}
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
};
return .{ .coreaudio = ctx };
}
pub fn deinit(ctx: *Context) void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
ctx.allocator.destroy(ctx);
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
var prop_address = c.AudioObjectPropertyAddress{
.mSelector = c.kAudioHardwarePropertyDevices,
.mScope = c.kAudioObjectPropertyScopeGlobal,
.mElement = c.kAudioObjectPropertyElementMain,
};
var io_size: u32 = 0;
if (c.AudioObjectGetPropertyDataSize(
c.kAudioObjectSystemObject,
&prop_address,
0,
null,
&io_size,
) != c.noErr) {
return error.OpeningDevice;
}
const devices_count = io_size / @sizeOf(c.AudioObjectID);
if (devices_count == 0) return;
const devs = try ctx.allocator.alloc(c.AudioObjectID, devices_count);
defer ctx.allocator.free(devs);
if (c.AudioObjectGetPropertyData(
c.kAudioObjectSystemObject,
&prop_address,
0,
null,
&io_size,
@as(*anyopaque, @ptrCast(devs)),
) != c.noErr) {
return error.OpeningDevice;
}
var default_input_id: c.AudioObjectID = undefined;
var default_output_id: c.AudioObjectID = undefined;
io_size = @sizeOf(c.AudioObjectID);
if (c.AudioHardwareGetProperty(
c.kAudioHardwarePropertyDefaultInputDevice,
&io_size,
&default_input_id,
) != c.noErr) {
return error.OpeningDevice;
}
io_size = @sizeOf(c.AudioObjectID);
if (c.AudioHardwareGetProperty(
c.kAudioHardwarePropertyDefaultOutputDevice,
&io_size,
&default_output_id,
) != c.noErr) {
return error.OpeningDevice;
}
for (devs) |id| {
const buf_list = try ctx.allocator.create(c.AudioBufferList);
defer ctx.allocator.destroy(buf_list);
for (std.meta.tags(main.Device.Mode)) |mode| {
io_size = 0;
prop_address.mSelector = c.kAudioDevicePropertyStreamConfiguration;
prop_address.mScope = switch (mode) {
.playback => c.kAudioObjectPropertyScopeOutput,
.capture => c.kAudioObjectPropertyScopeInput,
};
if (c.AudioObjectGetPropertyDataSize(
id,
&prop_address,
0,
null,
&io_size,
) != c.noErr) {
continue;
}
if (c.AudioObjectGetPropertyData(
id,
&prop_address,
0,
null,
&io_size,
buf_list,
) != c.noErr) {
return error.OpeningDevice;
}
if (buf_list.mBuffers[0].mNumberChannels == 0) break;
const audio_buffer_list_property_address = c.AudioObjectPropertyAddress{
.mSelector = c.kAudioDevicePropertyStreamConfiguration,
.mScope = switch (mode) {
.playback => c.kAudioDevicePropertyScopeOutput,
.capture => c.kAudioDevicePropertyScopeInput,
},
.mElement = c.kAudioObjectPropertyElementMain,
};
var output_audio_buffer_list: c.AudioBufferList = undefined;
var audio_buffer_list_size: c_uint = undefined;
if (c.AudioObjectGetPropertyDataSize(
id,
&audio_buffer_list_property_address,
0,
null,
&audio_buffer_list_size,
) != c.noErr) {
return error.OpeningDevice;
}
if (c.AudioObjectGetPropertyData(
id,
&prop_address,
0,
null,
&audio_buffer_list_size,
&output_audio_buffer_list,
) != c.noErr) {
return error.OpeningDevice;
}
var output_channel_count: usize = 0;
for (0..output_audio_buffer_list.mNumberBuffers) |mBufferIndex| {
output_channel_count += output_audio_buffer_list.mBuffers[mBufferIndex].mNumberChannels;
}
const channels = try ctx.allocator.alloc(main.ChannelPosition, output_channel_count);
prop_address.mSelector = c.kAudioDevicePropertyNominalSampleRate;
io_size = @sizeOf(f64);
var sample_rate: f64 = undefined;
if (c.AudioObjectGetPropertyData(
id,
&prop_address,
0,
null,
&io_size,
&sample_rate,
) != c.noErr) {
return error.OpeningDevice;
}
io_size = @sizeOf([*]const u8);
if (c.AudioDeviceGetPropertyInfo(
id,
0,
0,
c.kAudioDevicePropertyDeviceName,
&io_size,
null,
) != c.noErr) {
return error.OpeningDevice;
}
const name = try ctx.allocator.allocSentinel(u8, io_size, 0);
errdefer ctx.allocator.free(name);
if (c.AudioDeviceGetProperty(
id,
0,
0,
c.kAudioDevicePropertyDeviceName,
&io_size,
name.ptr,
) != c.noErr) {
return error.OpeningDevice;
}
const id_str = try std.fmt.allocPrintZ(ctx.allocator, "{d}", .{id});
errdefer ctx.allocator.free(id_str);
const dev = main.Device{
.id = id_str,
.name = name,
.mode = mode,
.channels = channels,
.formats = &.{ .i16, .i32, .f32 },
.sample_rate = .{
.min = @as(u24, @intFromFloat(@floor(sample_rate))),
.max = @as(u24, @intFromFloat(@floor(sample_rate))),
},
};
try ctx.devices_info.list.append(ctx.allocator, dev);
if (id == default_output_id and mode == .playback) {
ctx.devices_info.default_output = ctx.devices_info.list.items.len - 1;
}
if (id == default_input_id and mode == .capture) {
ctx.devices_info.default_input = ctx.devices_info.list.items.len - 1;
}
}
}
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
const player = try ctx.allocator.create(Player);
errdefer ctx.allocator.destroy(player);
// obtain an AudioOutputUnit using an AUHAL component description
var component_desc = c.AudioComponentDescription{
.componentType = c.kAudioUnitType_Output,
.componentSubType = c.kAudioUnitSubType_HALOutput,
.componentManufacturer = c.kAudioUnitManufacturer_Apple,
.componentFlags = 0,
.componentFlagsMask = 0,
};
const component = c.AudioComponentFindNext(null, &component_desc);
if (component == null) return error.OpeningDevice;
// instantiate the audio unit
var audio_unit: c.AudioComponentInstance = undefined;
if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice;
// Initialize the AUHAL before making any changes or using it. Note that an AUHAL may need
// to be initialized twice, e.g. before and after making changes to it, as an AUHAL needs to
// be initialized *before* anything is done to it.
if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice;
errdefer _ = c.AudioUnitUninitialize(audio_unit);
const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch return error.OpeningDevice;
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioOutputUnitProperty_CurrentDevice,
c.kAudioUnitScope_Input,
0,
&device_id,
@sizeOf(c.AudioDeviceID),
) != c.noErr) {
return error.OpeningDevice;
}
const stream_desc = try createStreamDesc(options.format, options.sample_rate orelse default_sample_rate, device.channels.len);
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioUnitProperty_StreamFormat,
c.kAudioUnitScope_Input,
0,
&stream_desc,
@sizeOf(c.AudioStreamBasicDescription),
) != c.noErr) {
return error.OpeningDevice;
}
const render_callback = c.AURenderCallbackStruct{
.inputProc = Player.renderCallback,
.inputProcRefCon = player,
};
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioUnitProperty_SetRenderCallback,
c.kAudioUnitScope_Input,
0,
&render_callback,
@sizeOf(c.AURenderCallbackStruct),
) != c.noErr) {
return error.OpeningDevice;
}
player.* = .{
.allocator = ctx.allocator,
.audio_unit = audio_unit.?,
.is_paused = false,
.vol = 1.0,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = options.format,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .coreaudio = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
// Request permission to record via requestRecordPermission. If permission was previously
// granted, it will immediately return. Otherwise the function will block, the OS will display
// an "<App> wants to access the Microphone. [Allow] [Deny]" menu, then the callback will be
// invoked and requestRecordPermission will return.
const audio_session = avaudio.AVAudioSession.sharedInstance();
const PermissionContext = void;
const perm_ctx: PermissionContext = {};
audio_session.requestRecordPermission(perm_ctx, (struct {
pub fn callback(perm_ctx_2: PermissionContext, permission_granted: bool) void {
_ = permission_granted;
_ = perm_ctx_2;
// Note: in the event permission was NOT granted by the user, we could capture that here
// and surface it as an error.
//
// However, in this situation the OS will simply replace all audio samples with zero-value
// (silence) ones - so there's no harm for us in doing nothing in that case: the user would
// find out we're recording only silence, and they would need to correct it in their System
// Preferences by granting the app permission to record.
}
}).callback);
const recorder = try ctx.allocator.create(Recorder);
errdefer ctx.allocator.destroy(recorder);
const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch return error.OpeningDevice;
var io_size: u32 = 0;
var prop_address = c.AudioObjectPropertyAddress{
.mSelector = c.kAudioDevicePropertyStreamConfiguration,
.mScope = c.kAudioObjectPropertyScopeInput,
.mElement = c.kAudioObjectPropertyElementMain,
};
if (c.AudioObjectGetPropertyDataSize(
device_id,
&prop_address,
0,
null,
&io_size,
) != c.noErr) {
return error.OpeningDevice;
}
std.debug.assert(io_size == @sizeOf(c.AudioBufferList));
const buf_list = try ctx.allocator.create(c.AudioBufferList);
errdefer ctx.allocator.destroy(buf_list);
if (c.AudioObjectGetPropertyData(
device_id,
&prop_address,
0,
null,
&io_size,
@as(*anyopaque, @ptrCast(buf_list)),
) != c.noErr) {
return error.OpeningDevice;
}
// obtain an AudioOutputUnit using an AUHAL component description
var component_desc = c.AudioComponentDescription{
.componentType = c.kAudioUnitType_Output,
.componentSubType = c.kAudioUnitSubType_HALOutput,
.componentManufacturer = c.kAudioUnitManufacturer_Apple,
.componentFlags = 0,
.componentFlagsMask = 0,
};
const component = c.AudioComponentFindNext(null, &component_desc);
if (component == null) return error.OpeningDevice;
// instantiate the audio unit
var audio_unit: c.AudioComponentInstance = undefined;
if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice;
// Initialize the AUHAL before making any changes or using it. Note that an AUHAL may need
// to be initialized twice, e.g. before and after making changes to it, as an AUHAL needs to
// be initialized *before* anything is done to it.
if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice;
errdefer _ = c.AudioUnitUninitialize(audio_unit);
// To obtain the device input, we must enable IO on the input scope of the Audio Unit. Input
// must be explicitly enabled with kAudioOutputUnitProperty_EnableIO on Element 1 of the
// AUHAL, and this *must be done before* setting the AUHAL's current device. We must also
// disable IO on the output scope of the AUHAL, since it can be used for both.
// Enable AUHAL input
const enable_io: u32 = 1;
const au_element_input: u32 = 1;
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioOutputUnitProperty_EnableIO,
c.kAudioUnitScope_Input,
au_element_input,
&enable_io,
@sizeOf(u32),
) != c.noErr) {
return error.OpeningDevice;
}
// Disable AUHAL output
const disable_io: u32 = 0;
const au_element_output: u32 = 0;
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioOutputUnitProperty_EnableIO,
c.kAudioUnitScope_Output,
au_element_output,
&disable_io,
@sizeOf(u32),
) != c.noErr) {
return error.OpeningDevice;
}
// Set the audio device to be Audio Unit's current device. A device can only be associated
// with an AUHAL after enabling IO.
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioOutputUnitProperty_CurrentDevice,
c.kAudioUnitScope_Global,
au_element_output,
&device_id,
@sizeOf(c.AudioDeviceID),
) != c.noErr) {
return error.OpeningDevice;
}
// Register the capture callback for the AUHAL; this will be called when the AUHAL has
// received new data from the input device.
const capture_callback = c.AURenderCallbackStruct{
.inputProc = Recorder.captureCallback,
.inputProcRefCon = recorder,
};
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioOutputUnitProperty_SetInputCallback,
c.kAudioUnitScope_Global,
au_element_output,
&capture_callback,
@sizeOf(c.AURenderCallbackStruct),
) != c.noErr) {
return error.OpeningDevice;
}
// Set the desired output format.
const sample_rate = blk: {
if (options.sample_rate) |rate| {
if (rate < device.sample_rate.min or rate > device.sample_rate.max) return error.OpeningDevice;
break :blk rate;
}
break :blk device.sample_rate.max;
};
const stream_desc = try createStreamDesc(options.format, sample_rate, device.channels.len);
if (c.AudioUnitSetProperty(
audio_unit,
c.kAudioUnitProperty_StreamFormat,
c.kAudioUnitScope_Output,
au_element_input,
&stream_desc,
@sizeOf(c.AudioStreamBasicDescription),
) != c.noErr) {
return error.OpeningDevice;
}
// Now that we are done with modifying the AUHAL, initialize it once more to ensure that it
// is ready to use.
if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice;
errdefer _ = c.AudioUnitUninitialize(audio_unit);
recorder.* = .{
.allocator = ctx.allocator,
.audio_unit = audio_unit.?,
.is_paused = false,
.vol = 1.0,
.buf_list = buf_list,
.readFn = readFn,
.user_data = options.user_data,
.channels = device.channels,
.format = options.format,
.sample_rate = sample_rate,
};
return .{ .coreaudio = recorder };
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
audio_unit: c.AudioUnit,
is_paused: bool,
vol: f32,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn renderCallback(
player_opaque: ?*anyopaque,
action_flags: [*c]c.AudioUnitRenderActionFlags,
time_stamp: [*c]const c.AudioTimeStamp,
bus_number: u32,
frames_left: u32,
buf: [*c]c.AudioBufferList,
) callconv(.C) c.OSStatus {
_ = action_flags;
_ = time_stamp;
_ = bus_number;
_ = frames_left;
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
const frames = buf.*.mBuffers[0].mDataByteSize;
player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.mBuffers[0].mData.?))[0..frames]);
return c.noErr;
}
pub fn deinit(player: *Player) void {
_ = c.AudioOutputUnitStop(player.audio_unit);
_ = c.AudioUnitUninitialize(player.audio_unit);
_ = c.AudioComponentInstanceDispose(player.audio_unit);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
try player.play();
}
pub fn play(player: *Player) !void {
if (c.AudioOutputUnitStart(player.audio_unit) != c.noErr) {
return error.CannotPlay;
}
player.is_paused = false;
}
pub fn pause(player: *Player) !void {
if (c.AudioOutputUnitStop(player.audio_unit) != c.noErr) {
return error.CannotPause;
}
player.is_paused = true;
}
pub fn paused(player: *Player) bool {
return player.is_paused;
}
pub fn setVolume(player: *Player, vol: f32) !void {
if (c.AudioUnitSetParameter(
player.audio_unit,
c.kHALOutputParam_Volume,
c.kAudioUnitScope_Global,
0,
vol,
0,
) != c.noErr) {
if (is_darling) return;
return error.CannotSetVolume;
}
}
pub fn volume(player: *Player) !f32 {
var vol: f32 = 0;
if (c.AudioUnitGetParameter(
player.audio_unit,
c.kHALOutputParam_Volume,
c.kAudioUnitScope_Global,
0,
&vol,
) != c.noErr) {
if (is_darling) return 1;
return error.CannotGetVolume;
}
return vol;
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
audio_unit: c.AudioUnit,
is_paused: bool,
vol: f32,
buf_list: *c.AudioBufferList,
m_data: ?[]u8 = null,
readFn: main.ReadFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn captureCallback(
recorder_opaque: ?*anyopaque,
action_flags: [*c]c.AudioUnitRenderActionFlags,
time_stamp: [*c]const c.AudioTimeStamp,
bus_number: u32,
num_frames: u32,
buffer_list: [*c]c.AudioBufferList,
) callconv(.C) c.OSStatus {
_ = buffer_list;
const recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?)));
// We want interleaved multi-channel audio, when multiple channels are available-so we'll
// only use a single buffer. If we wanted non-interleaved audio we would use multiple
// buffers.
var m_buffer = &recorder.buf_list.*.mBuffers[0];
// Ensure our buffer matches the size needed for the render operation. Note that the buffer
// may grow (in the case of multi-channel audio during the first render callback) or shrink
// in e.g. the event of the device being unplugged and the default input device switching.
const new_len = num_frames * recorder.format.frameSize(@intCast(recorder.channels.len));
if (recorder.m_data == null or recorder.m_data.?.len != new_len) {
if (recorder.m_data) |old| recorder.allocator.free(old);
recorder.m_data = recorder.allocator.alloc(u8, new_len) catch return c.noErr;
}
recorder.buf_list.*.mNumberBuffers = 1;
m_buffer.mData = recorder.m_data.?.ptr;
m_buffer.mDataByteSize = @intCast(recorder.m_data.?.len);
m_buffer.mNumberChannels = @intCast(recorder.channels.len);
const err_no = c.AudioUnitRender(
recorder.audio_unit,
action_flags,
time_stamp,
bus_number,
num_frames,
recorder.buf_list,
);
if (err_no != c.noErr) {
// TODO: err_no here is rather helpful, we should indicate what it is back to the user
// in this event probably?
return c.noErr;
}
if (recorder.buf_list.*.mNumberBuffers == 1) {
recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(recorder.buf_list.*.mBuffers[0].mData.?))[0..new_len]);
} else {
@panic("TODO: convert planar to interleaved");
// for (recorder.channels, 0..) |*ch, i| {
// ch.ptr = @as([*]u8, @ptrCast(recorder.buf_list.*.mBuffers[i].mData.?));
// }
}
return c.noErr;
}
pub fn deinit(recorder: *Recorder) void {
_ = c.AudioOutputUnitStop(recorder.audio_unit);
_ = c.AudioUnitUninitialize(recorder.audio_unit);
_ = c.AudioComponentInstanceDispose(recorder.audio_unit);
recorder.allocator.destroy(recorder.buf_list);
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
try recorder.record();
}
pub fn record(recorder: *Recorder) !void {
if (c.AudioOutputUnitStart(recorder.audio_unit) != c.noErr) {
return error.CannotRecord;
}
recorder.is_paused = false;
}
pub fn pause(recorder: *Recorder) !void {
if (c.AudioOutputUnitStop(recorder.audio_unit) != c.noErr) {
return error.CannotPause;
}
recorder.is_paused = true;
}
pub fn paused(recorder: *Recorder) bool {
return recorder.is_paused;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
if (c.AudioUnitSetParameter(
recorder.audio_unit,
c.kHALOutputParam_Volume,
c.kAudioUnitScope_Global,
0,
vol,
0,
) != c.noErr) {
if (is_darling) return;
return error.CannotSetVolume;
}
}
pub fn volume(recorder: *Recorder) !f32 {
var vol: f32 = 0;
if (c.AudioUnitGetParameter(
recorder.audio_unit,
c.kHALOutputParam_Volume,
c.kAudioUnitScope_Global,
0,
&vol,
) != c.noErr) {
if (is_darling) return 1;
return error.CannotGetVolume;
}
return vol;
}
};
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.id);
allocator.free(device.name);
allocator.free(device.channels);
}
fn createStreamDesc(format: main.Format, sample_rate: u24, ch_count: usize) !c.AudioStreamBasicDescription {
var desc = c.AudioStreamBasicDescription{
.mSampleRate = @as(f64, @floatFromInt(sample_rate)),
.mFormatID = c.kAudioFormatLinearPCM,
.mFormatFlags = switch (format) {
.i16 => c.kAudioFormatFlagIsSignedInteger,
.i24 => c.kAudioFormatFlagIsSignedInteger,
.i32 => c.kAudioFormatFlagIsSignedInteger,
.f32 => c.kAudioFormatFlagIsFloat,
.u8 => return error.IncompatibleDevice,
},
.mBytesPerPacket = format.frameSize(@intCast(ch_count)),
.mFramesPerPacket = 1,
.mBytesPerFrame = format.frameSize(@intCast(ch_count)),
.mChannelsPerFrame = @intCast(ch_count),
.mBitsPerChannel = switch (format) {
.i16 => 16,
.i24 => 24,
.i32 => 32,
.f32 => 32,
.u8 => return error.IncompatibleDevice,
},
.mReserved = 0,
};
if (native_endian == .big) {
desc.mFormatFlags |= c.kAudioFormatFlagIsBigEndian;
}
return desc;
}

189
src/sysaudio/dummy.zig Normal file
View file

@ -0,0 +1,189 @@
const std = @import("std");
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const default_sample_rate = 44_100; // Hz
const dummy_playback = main.Device{
.id = "dummy-playback",
.name = "Dummy Device",
.mode = .playback,
.channels = undefined,
.formats = std.meta.tags(main.Format),
.sample_rate = .{
.min = main.min_sample_rate,
.max = main.max_sample_rate,
},
};
const dummy_capture = main.Device{
.id = "dummy-capture",
.name = "Dummy Device",
.mode = .capture,
.channels = undefined,
.formats = std.meta.tags(main.Format),
.sample_rate = .{
.min = main.min_sample_rate,
.max = main.max_sample_rate,
},
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
_ = options;
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
};
return .{ .dummy = ctx };
}
pub fn deinit(ctx: *Context) void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
ctx.allocator.destroy(ctx);
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
try ctx.devices_info.list.append(ctx.allocator, dummy_playback);
try ctx.devices_info.list.append(ctx.allocator, dummy_capture);
ctx.devices_info.setDefault(.playback, 0);
ctx.devices_info.setDefault(.capture, 1);
ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 1);
ctx.devices_info.list.items[1].channels = try ctx.allocator.alloc(main.ChannelPosition, 1);
ctx.devices_info.list.items[0].channels[0] = .front_center;
ctx.devices_info.list.items[1].channels[0] = .front_center;
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
_ = writeFn;
const player = try ctx.allocator.create(Player);
player.* = .{
.allocator = ctx.allocator,
.is_paused = false,
.vol = 1.0,
.channels = device.channels,
.format = options.format,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .dummy = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
_ = readFn;
const recorder = try ctx.allocator.create(Recorder);
recorder.* = .{
.allocator = ctx.allocator,
.is_paused = false,
.vol = 1.0,
.channels = device.channels,
.format = options.format,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .dummy = recorder };
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
is_paused: bool,
vol: f32,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(player: *Player) void {
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
_ = player;
}
pub fn play(player: *Player) !void {
player.is_paused = false;
}
pub fn pause(player: *Player) !void {
player.is_paused = true;
}
pub fn paused(player: *Player) bool {
return player.is_paused;
}
pub fn setVolume(player: *Player, vol: f32) !void {
player.vol = vol;
}
pub fn volume(player: *Player) !f32 {
return player.vol;
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
is_paused: bool,
vol: f32,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(recorder: *Recorder) void {
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
_ = recorder;
}
pub fn record(recorder: *Recorder) !void {
recorder.is_paused = false;
}
pub fn pause(recorder: *Recorder) !void {
recorder.is_paused = true;
}
pub fn paused(recorder: *Recorder) bool {
return recorder.is_paused;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
recorder.vol = vol;
}
pub fn volume(recorder: *Recorder) !f32 {
return recorder.vol;
}
};
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.channels);
}

View file

@ -0,0 +1,92 @@
//! Redirects input device into zig-out/raw_audio file.
const std = @import("std");
const sysaudio = @import("mach").sysaudio;
var recorder: sysaudio.Recorder = undefined;
var file: std.fs.File = undefined;
// Note: this Info.plist file gets embedded into the final binary __TEXT,__info_plist
// linker section. On macOS this means that NSMicrophoneUsageDescription is set. Without
// that being set, the application would be denied access to the microphone (the prompt
// for microphone access would not even appear.)
//
// The linker is just a convenient way to specify this without building a .app bundle with
// a separate Info.plist file.
export var __info_plist: [663:0]u8 linksection("__TEXT,__info_plist") =
(
\\ <?xml version="1.0" encoding="UTF-8"?>
\\ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
\\ <plist version="1.0">
\\ <dict>
\\ <key>CFBundleDevelopmentRegion</key>
\\ <string>English</string>
\\ <key>CFBundleIdentifier</key>
\\ <string>com.my.app</string>
\\ <key>CFBundleInfoDictionaryVersion</key>
\\ <string>6.0</string>
\\ <key>CFBundleName</key>
\\ <string>myapp</string>
\\ <key>CFBundleDisplayName</key>
\\ <string>My App</string>
\\ <key>CFBundleVersion</key>
\\ <string>1.0.0</string>
\\ <key>NSMicrophoneUsageDescription</key>
\\ <string>To record audio from your microphone</string>
\\ </dict>
\\ </plist>
).*;
pub fn main() !void {
_ = __info_plist;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var ctx = try sysaudio.Context.init(null, gpa.allocator(), .{});
defer ctx.deinit();
try ctx.refresh();
const device = ctx.defaultDevice(.capture) orelse return error.NoDevice;
recorder = try ctx.createRecorder(device, readCallback, .{});
defer recorder.deinit();
try recorder.start();
const zig_out = try std.fs.cwd().makeOpenPath("zig-out", .{});
file = try zig_out.createFile("raw_audio", .{});
std.debug.print(
\\Recording to zig-out/raw_audio using:
\\
\\ device: {s}
\\ channels: {}
\\ sample_rate: {}
\\
\\You can play this recording back using e.g.:
\\ $ ffplay -f f32le -ar {} -ac {} zig-out/raw_audio
\\
, .{
device.name,
device.channels.len,
recorder.sampleRate(),
recorder.sampleRate(),
device.channels.len,
});
// Note: you may also use e.g.:
//
// ```
// paplay -p --format=FLOAT32LE --rate 48000 --raw zig-out/raw_audio
// aplay -f FLOAT_LE -r 48000 zig-out/raw_audio
// ```
while (true) {}
}
fn readCallback(_: ?*anyopaque, input: []const u8) void {
const format_size = recorder.format().size();
const samples = input.len / format_size;
var buffer: [16 * 1024]f32 = undefined;
sysaudio.convertFrom(f32, buffer[0..samples], recorder.format(), input);
_ = file.write(std.mem.sliceAsBytes(buffer[0 .. input.len / format_size])) catch {};
}

View file

@ -0,0 +1,77 @@
const std = @import("std");
const sysaudio = @import("mach").sysaudio;
var player: sysaudio.Player = undefined;
pub fn main() !void {
var timer = try std.time.Timer.start();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var ctx = try sysaudio.Context.init(null, gpa.allocator(), .{ .deviceChangeFn = deviceChange });
std.log.info("Took {} to initialize the context...", .{std.fmt.fmtDuration(timer.lap())});
defer ctx.deinit();
try ctx.refresh();
std.log.info("Took {} to refresh the context...", .{std.fmt.fmtDuration(timer.lap())});
const device = ctx.defaultDevice(.playback) orelse return error.NoDevice;
std.log.info("Took {} to get the default playback device...", .{std.fmt.fmtDuration(timer.lap())});
player = try ctx.createPlayer(device, writeCallback, .{});
std.log.info("Took {} to create a player...", .{std.fmt.fmtDuration(timer.lap())});
defer player.deinit();
try player.start();
std.log.info("Took {} to start the player...", .{std.fmt.fmtDuration(timer.lap())});
try player.setVolume(0.85);
std.log.info("Took {} to set the volume...", .{std.fmt.fmtDuration(timer.lap())});
var buf: [16]u8 = undefined;
std.log.info("player created & entering i/o loop...", .{});
while (true) {
std.debug.print("( paused = {}, volume = {d} )\n> ", .{ player.paused(), try player.volume() });
const line = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')) orelse break;
var iter = std.mem.split(u8, line, ":");
const cmd = std.mem.trimRight(u8, iter.first(), &std.ascii.whitespace);
if (std.mem.eql(u8, cmd, "vol")) {
const vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace));
try player.setVolume(vol);
} else if (std.mem.eql(u8, cmd, "pause")) {
try player.pause();
try std.testing.expect(player.paused());
} else if (std.mem.eql(u8, cmd, "play")) {
try player.play();
try std.testing.expect(!player.paused());
} else if (std.mem.eql(u8, cmd, "exit")) {
break;
}
}
}
const pitch = 440.0;
const radians_per_second = pitch * 2.0 * std.math.pi;
var seconds_offset: f32 = 0.0;
fn writeCallback(_: ?*anyopaque, output: []u8) void {
const seconds_per_frame = 1.0 / @as(f32, @floatFromInt(player.sampleRate()));
const frame_size = player.format().frameSize(@intCast(player.channels().len));
const frames = output.len / frame_size;
var i: usize = 0;
while (i < output.len) : (i += frame_size) {
const frame_index: f32 = @floatFromInt(i / frame_size);
const sample = @sin((seconds_offset + frame_index * seconds_per_frame) * radians_per_second);
sysaudio.convertTo(
f32,
&.{ sample, sample },
player.format(),
output[i..][0..frame_size],
);
}
seconds_offset = @mod(seconds_offset + seconds_per_frame * @as(f32, @floatFromInt(frames)), 1.0);
}
fn deviceChange(_: ?*anyopaque) void {
std.log.info("device change detected!", .{});
}

404
src/sysaudio/jack.zig Normal file
View file

@ -0,0 +1,404 @@
const std = @import("std");
const c = @cImport(@cInclude("jack/jack.h"));
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
var lib: Lib = undefined;
const Lib = struct {
handle: std.DynLib,
jack_free: *const fn (ptr: ?*anyopaque) callconv(.C) void,
jack_set_error_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void,
jack_set_info_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void,
jack_client_open: *const fn ([*c]const u8, c.jack_options_t, [*c]c.jack_status_t, ...) callconv(.C) ?*c.jack_client_t,
jack_client_close: *const fn (?*c.jack_client_t) callconv(.C) c_int,
jack_connect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int,
jack_disconnect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int,
jack_activate: *const fn (?*c.jack_client_t) callconv(.C) c_int,
jack_deactivate: *const fn (?*c.jack_client_t) callconv(.C) c_int,
jack_port_by_name: *const fn (?*c.jack_client_t, [*c]const u8) callconv(.C) ?*c.jack_port_t,
jack_port_register: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong, c_ulong) callconv(.C) ?*c.jack_port_t,
jack_set_sample_rate_callback: *const fn (?*c.jack_client_t, c.JackSampleRateCallback, ?*anyopaque) callconv(.C) c_int,
jack_set_port_registration_callback: *const fn (?*c.jack_client_t, c.JackPortRegistrationCallback, ?*anyopaque) callconv(.C) c_int,
jack_set_process_callback: *const fn (?*c.jack_client_t, c.JackProcessCallback, ?*anyopaque) callconv(.C) c_int,
jack_set_port_rename_callback: *const fn (?*c.jack_client_t, c.JackPortRenameCallback, ?*anyopaque) callconv(.C) c_int,
jack_get_sample_rate: *const fn (?*c.jack_client_t) callconv(.C) c.jack_nframes_t,
jack_get_ports: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong) callconv(.C) [*c][*c]const u8,
jack_port_type: *const fn (port: ?*const c.jack_port_t) callconv(.C) [*c]const u8,
jack_port_flags: *const fn (port: ?*const c.jack_port_t) callconv(.C) c_int,
jack_port_name: *const fn (?*const c.jack_port_t) callconv(.C) [*c]const u8,
jack_port_get_buffer: *const fn (?*c.jack_port_t, c.jack_nframes_t) callconv(.C) ?*anyopaque,
jack_port_connected_to: *const fn (?*const c.jack_port_t, [*c]const u8) callconv(.C) c_int,
jack_port_type_size: *const fn () c_int,
pub fn load() !void {
lib.handle = std.DynLib.openZ("libjack.so") catch return error.LibraryNotFound;
inline for (@typeInfo(Lib).Struct.fields[1..]) |field| {
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]);
@field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup;
}
}
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
client: *c.jack_client_t,
watcher: ?Watcher,
const Watcher = struct {
deviceChangeFn: main.Context.DeviceChangeFn,
user_data: ?*anyopaque,
};
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
try Lib.load();
lib.jack_set_error_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing)));
lib.jack_set_info_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing)));
var status: c.jack_status_t = 0;
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
.client = lib.jack_client_open(options.app_name.ptr, c.JackNoStartServer, &status) orelse {
std.debug.assert(status & c.JackInvalidOption == 0);
return if (status & c.JackShmFailure != 0)
error.SystemResources
else
error.ConnectionRefused;
},
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
.deviceChangeFn = deviceChangeFn,
.user_data = options.user_data,
} else null,
};
if (options.deviceChangeFn) |_| {
if (lib.jack_set_sample_rate_callback(ctx.client, sampleRateCallback, ctx) != 0 or
lib.jack_set_port_registration_callback(ctx.client, portRegistrationCallback, ctx) != 0 or
lib.jack_set_port_rename_callback(ctx.client, portRenameCalllback, ctx) != 0)
return error.ConnectionRefused;
}
return .{ .jack = ctx };
}
pub fn deinit(ctx: *Context) void {
for (ctx.devices_info.list.items) |device|
freeDevice(ctx.allocator, device);
ctx.devices_info.list.deinit(ctx.allocator);
_ = lib.jack_client_close(ctx.client);
ctx.allocator.destroy(ctx);
lib.handle.close();
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
const sample_rate = @as(u24, @intCast(lib.jack_get_sample_rate(ctx.client)));
const port_names = lib.jack_get_ports(ctx.client, null, null, 0) orelse
return error.OutOfMemory;
defer lib.jack_free(@as(?*anyopaque, @ptrCast(port_names)));
var i: usize = 0;
outer: while (port_names[i] != null) : (i += 1) {
const port = lib.jack_port_by_name(ctx.client, port_names[i]) orelse break;
const port_type = lib.jack_port_type(port)[0..@as(usize, @intCast(lib.jack_port_type_size()))];
if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE))
continue;
const flags = lib.jack_port_flags(port);
const mode: main.Device.Mode = if (flags & c.JackPortIsInput != 0) .capture else .playback;
const name = std.mem.span(port_names[i]);
const id = std.mem.sliceTo(name, ':');
for (ctx.devices_info.list.items) |*dev| {
if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) {
const new_ch: main.ChannelPosition = @enumFromInt(dev.channels.len);
dev.channels = try ctx.allocator.realloc(dev.channels, dev.channels.len + 1);
dev.channels[dev.channels.len - 1] = new_ch;
break :outer;
}
}
const device = main.Device{
.id = try ctx.allocator.dupeZ(u8, id),
.name = name,
.mode = mode,
.channels = blk: {
var channels = try ctx.allocator.alloc(main.ChannelPosition, 1);
channels[0] = .front_center;
break :blk channels;
},
.formats = &.{.f32},
.sample_rate = .{
.min = sample_rate,
.max = sample_rate,
},
};
try ctx.devices_info.list.append(ctx.allocator, device);
if (ctx.devices_info.default(mode) == null) {
ctx.devices_info.setDefault(mode, ctx.devices_info.list.items.len - 1);
}
}
}
fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
var ctx = @as(*Context, @ptrCast(@alignCast(arg.?)));
ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data);
return 0;
}
fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(arg.?)));
ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data);
}
fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(arg.?)));
ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data);
}
pub fn devices(ctx: *Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: *Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
var ports = try ctx.allocator.alloc(*c.jack_port_t, device.channels.len);
var dest_ports = try ctx.allocator.alloc([:0]const u8, ports.len);
var buf: [64]u8 = undefined;
for (device.channels, 0..) |_, i| {
const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable;
const dest_name = try std.fmt.allocPrintZ(ctx.allocator, "{s}:{s}", .{ device.id, port_name });
ports[i] = lib.jack_port_register(ctx.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse
return error.OpeningDevice;
dest_ports[i] = dest_name;
}
const player = try ctx.allocator.create(Player);
player.* = .{
.allocator = ctx.allocator,
.client = ctx.client,
.ports = ports,
.dest_ports = dest_ports,
.device = device,
.vol = 1.0,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = .f32,
};
return .{ .jack = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
var ports = try ctx.allocator.alloc(*c.jack_port_t, device.channels.len);
var dest_ports = try ctx.allocator.alloc([:0]const u8, ports.len);
var buf: [64]u8 = undefined;
for (device.channels, 0..) |_, i| {
const port_name = std.fmt.bufPrintZ(&buf, "capture_{d}", .{i + 1}) catch unreachable;
const dest_name = try std.fmt.allocPrintZ(ctx.allocator, "{s}:{s}", .{ device.id, port_name });
ports[i] = lib.jack_port_register(ctx.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsInput, 0) orelse
return error.OpeningDevice;
dest_ports[i] = dest_name;
}
const recorder = try ctx.allocator.create(Recorder);
recorder.* = .{
.allocator = ctx.allocator,
.client = ctx.client,
.ports = ports,
.dest_ports = dest_ports,
.device = device,
.vol = 1.0,
.readFn = readFn,
.user_data = options.user_data,
.channels = device.channels,
.format = .f32,
};
return .{ .jack = recorder };
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
client: *c.jack_client_t,
ports: []const *c.jack_port_t,
dest_ports: []const [:0]const u8,
device: main.Device,
vol: f32,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
pub fn deinit(player: *Player) void {
player.allocator.free(player.ports);
for (player.dest_ports) |d|
player.allocator.free(d);
player.allocator.free(player.dest_ports);
_ = lib.jack_deactivate(player.client);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
if (lib.jack_set_process_callback(player.client, processCallback, player) != 0)
return error.CannotPlay;
if (lib.jack_activate(player.client) != 0)
return error.CannotPlay;
for (player.ports, 0..) |port, i| {
if (lib.jack_connect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0)
return error.CannotPlay;
}
}
fn processCallback(n_frames: c.jack_nframes_t, player_opaque: ?*anyopaque) callconv(.C) c_int {
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
if (true) @panic("TODO: convert planar to interleaved");
// for (player.channels, 0..) |*ch, i| {
// ch.*.ptr = @as([*]u8, @ptrCast(lib.jack_port_get_buffer(player.ports[i], n_frames)));
// }
player.writeFn(player.user_data, undefined, n_frames);
return 0;
}
pub fn play(player: *Player) !void {
for (player.ports, 0..) |port, i| {
if (lib.jack_connect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0)
return error.CannotPlay;
}
}
pub fn pause(player: *Player) !void {
for (player.ports, 0..) |port, i| {
if (lib.jack_disconnect(player.client, lib.jack_port_name(port), player.dest_ports[i].ptr) != 0)
return error.CannotPause;
}
}
pub fn paused(player: *Player) bool {
for (player.ports, 0..) |port, i| {
if (lib.jack_port_connected_to(port, player.dest_ports[i].ptr) == 1)
return false;
}
return true;
}
pub fn setVolume(player: *Player, vol: f32) !void {
player.vol = vol;
}
pub fn volume(player: *Player) !f32 {
return player.vol;
}
pub fn sampleRate(player: *Player) u24 {
return @as(u24, @intCast(lib.jack_get_sample_rate(player.client)));
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
client: *c.jack_client_t,
ports: []const *c.jack_port_t,
dest_ports: []const [:0]const u8,
device: main.Device,
vol: f32,
readFn: main.ReadFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
pub fn deinit(recorder: *Recorder) void {
recorder.allocator.free(recorder.ports);
for (recorder.dest_ports) |d|
recorder.allocator.free(d);
recorder.allocator.free(recorder.dest_ports);
_ = lib.jack_deactivate(recorder.client);
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
if (lib.jack_set_process_callback(recorder.client, processCallback, recorder) != 0)
return error.CannotRecord;
if (lib.jack_activate(recorder.client) != 0)
return error.CannotRecord;
for (recorder.ports, 0..) |port, i| {
if (lib.jack_connect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0)
return error.CannotRecord;
}
}
fn processCallback(n_frames: c.jack_nframes_t, recorder_opaque: ?*anyopaque) callconv(.C) c_int {
const recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?)));
if (true) @panic("TODO: convert planar to interleaved");
// for (recorder.channels, 0..) |*ch, i| {
// ch.*.ptr = @as([*]u8, @ptrCast(lib.jack_port_get_buffer(recorder.ports[i], n_frames)));
// }
recorder.readFn(recorder.user_data, n_frames);
return 0;
}
pub fn record(recorder: *Recorder) !void {
for (recorder.ports, 0..) |port, i| {
if (lib.jack_connect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0)
return error.CannotRecord;
}
}
pub fn pause(recorder: *Recorder) !void {
for (recorder.ports, 0..) |port, i| {
if (lib.jack_disconnect(recorder.client, lib.jack_port_name(port), recorder.dest_ports[i].ptr) != 0)
return error.CannotPause;
}
}
pub fn paused(recorder: *Recorder) bool {
for (recorder.ports, 0..) |port, i| {
if (lib.jack_port_connected_to(port, recorder.dest_ports[i].ptr) == 1)
return false;
}
return true;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
recorder.vol = vol;
}
pub fn volume(recorder: *Recorder) !f32 {
return recorder.vol;
}
pub fn sampleRate(recorder: *Recorder) u24 {
return @as(u24, @intCast(lib.jack_get_sample_rate(recorder.client)));
}
};
pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.id);
allocator.free(device.channels);
}

496
src/sysaudio/main.zig Normal file
View file

@ -0,0 +1,496 @@
const builtin = @import("builtin");
const std = @import("std");
const util = @import("util.zig");
const backends = @import("backends.zig");
const conv = @import("conv.zig");
pub const Backend = backends.Backend;
pub const Range = util.Range;
pub const default_latency = 500 * std.time.us_per_ms; // μs
pub const min_sample_rate = 8_000; // Hz
pub const max_sample_rate = 5_644_800; // Hz
pub const Context = struct {
pub const DeviceChangeFn = *const fn (userdata: ?*anyopaque) void;
pub const Options = struct {
app_name: [:0]const u8 = "Mach Game",
deviceChangeFn: ?DeviceChangeFn = null,
user_data: ?*anyopaque = null,
};
data: backends.Context,
pub const InitError = error{
OutOfMemory,
AccessDenied,
LibraryNotFound,
SymbolLookup,
SystemResources,
ConnectionRefused,
};
pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) InitError!Context {
const data: backends.Context = blk: {
if (backend) |b| {
break :blk try @typeInfo(
std.meta.fieldInfo(backends.Context, b).type,
).Pointer.child.init(allocator, options);
} else {
inline for (std.meta.fields(Backend), 0..) |b, i| {
if (@typeInfo(
std.meta.fieldInfo(backends.Context, @as(Backend, @enumFromInt(b.value))).type,
).Pointer.child.init(allocator, options)) |d| {
break :blk d;
} else |err| {
if (i == std.meta.fields(Backend).len - 1)
return err;
}
}
unreachable;
}
};
return .{ .data = data };
}
pub inline fn deinit(ctx: Context) void {
switch (ctx.data) {
inline else => |b| b.deinit(),
}
}
pub const RefreshError = error{
OutOfMemory,
SystemResources,
OpeningDevice,
};
pub inline fn refresh(ctx: Context) RefreshError!void {
return switch (ctx.data) {
inline else => |b| b.refresh(),
};
}
pub inline fn devices(ctx: Context) []const Device {
return switch (ctx.data) {
inline else => |b| b.devices(),
};
}
pub inline fn defaultDevice(ctx: Context, mode: Device.Mode) ?Device {
return switch (ctx.data) {
inline else => |b| b.defaultDevice(mode),
};
}
pub const CreateStreamError = error{
OutOfMemory,
SystemResources,
OpeningDevice,
IncompatibleDevice,
};
pub inline fn createPlayer(ctx: Context, device: Device, writeFn: WriteFn, options: StreamOptions) CreateStreamError!Player {
std.debug.assert(device.mode == .playback);
return .{
.data = switch (ctx.data) {
inline else => |b| try b.createPlayer(device, writeFn, options),
},
};
}
pub inline fn createRecorder(ctx: Context, device: Device, readFn: ReadFn, options: StreamOptions) CreateStreamError!Recorder {
std.debug.assert(device.mode == .capture);
return .{
.data = switch (ctx.data) {
inline else => |b| try b.createRecorder(device, readFn, options),
},
};
}
};
pub const StreamOptions = struct {
format: Format = .f32,
sample_rate: ?u24 = null,
media_role: MediaRole = .default,
user_data: ?*anyopaque = null,
};
pub const MediaRole = enum {
default,
game,
music,
movie,
communication,
};
// TODO: `*Player` instead `*anyopaque`
// https://github.com/ziglang/zig/issues/12325
pub const WriteFn = *const fn (user_data: ?*anyopaque, output: []u8) void;
// TODO: `*Recorder` instead `*anyopaque`
pub const ReadFn = *const fn (user_data: ?*anyopaque, input: []const u8) void;
pub const Player = struct {
data: backends.Player,
pub inline fn deinit(player: *Player) void {
return switch (player.data) {
inline else => |b| b.deinit(),
};
}
pub const StartError = error{
CannotPlay,
OutOfMemory,
SystemResources,
};
pub inline fn start(player: *Player) StartError!void {
return switch (player.data) {
inline else => |b| b.start(),
};
}
pub const PlayError = error{
CannotPlay,
OutOfMemory,
};
pub inline fn play(player: *Player) PlayError!void {
return switch (player.data) {
inline else => |b| b.play(),
};
}
pub const PauseError = error{
CannotPause,
OutOfMemory,
};
pub inline fn pause(player: *Player) PauseError!void {
return switch (player.data) {
inline else => |b| b.pause(),
};
}
pub inline fn paused(player: *Player) bool {
return switch (player.data) {
inline else => |b| b.paused(),
};
}
pub const SetVolumeError = error{
CannotSetVolume,
};
// confidence interval (±) depends on the device
pub inline fn setVolume(player: *Player, vol: f32) SetVolumeError!void {
std.debug.assert(vol <= 1.0);
return switch (player.data) {
inline else => |b| b.setVolume(vol),
};
}
pub const GetVolumeError = error{
CannotGetVolume,
};
// confidence interval (±) depends on the device
pub inline fn volume(player: *Player) GetVolumeError!f32 {
return switch (player.data) {
inline else => |b| b.volume(),
};
}
pub inline fn sampleRate(player: *Player) u24 {
return if (@hasField(Backend, "jack")) switch (player.data) {
.jack => |b| b.sampleRate(),
inline else => |b| b.sample_rate,
} else switch (player.data) {
inline else => |b| b.sample_rate,
};
}
pub inline fn channels(player: *Player) []ChannelPosition {
return switch (player.data) {
inline else => |b| b.channels,
};
}
pub inline fn format(player: *Player) Format {
return switch (player.data) {
inline else => |b| b.format,
};
}
};
pub const Recorder = struct {
data: backends.Recorder,
pub inline fn deinit(recorder: *Recorder) void {
return switch (recorder.data) {
inline else => |b| b.deinit(),
};
}
pub const StartError = error{
CannotRecord,
OutOfMemory,
SystemResources,
};
pub inline fn start(recorder: *Recorder) StartError!void {
return switch (recorder.data) {
inline else => |b| b.start(),
};
}
pub const RecordError = error{
CannotRecord,
OutOfMemory,
};
pub inline fn record(recorder: *Recorder) RecordError!void {
return switch (recorder.data) {
inline else => |b| b.record(),
};
}
pub const PauseError = error{
CannotPause,
OutOfMemory,
};
pub inline fn pause(recorder: *Recorder) PauseError!void {
return switch (recorder.data) {
inline else => |b| b.pause(),
};
}
pub inline fn paused(recorder: *Recorder) bool {
return switch (recorder.data) {
inline else => |b| b.paused(),
};
}
pub const SetVolumeError = error{
CannotSetVolume,
};
// confidence interval (±) depends on the device
pub inline fn setVolume(recorder: *Recorder, vol: f32) SetVolumeError!void {
std.debug.assert(vol <= 1.0);
return switch (recorder.data) {
inline else => |b| b.setVolume(vol),
};
}
pub const GetVolumeError = error{
CannotGetVolume,
};
// confidence interval (±) depends on the device
pub inline fn volume(recorder: *Recorder) GetVolumeError!f32 {
return switch (recorder.data) {
inline else => |b| b.volume(),
};
}
pub inline fn sampleRate(recorder: *Recorder) u24 {
return if (@hasField(Backend, "jack")) switch (recorder.data) {
.jack => |b| b.sampleRate(),
inline else => |b| b.sample_rate,
} else switch (recorder.data) {
inline else => |b| b.sample_rate,
};
}
pub inline fn channels(recorder: *Recorder) []ChannelPosition {
return switch (recorder.data) {
inline else => |b| b.channels,
};
}
pub inline fn format(recorder: *Recorder) Format {
return switch (recorder.data) {
inline else => |b| b.format,
};
}
};
pub fn convertTo(comptime SrcType: type, src: []const SrcType, dst_format: Format, dst: []u8) void {
const dst_len = dst.len / dst_format.size();
std.debug.assert(dst_len == src.len);
return switch (dst_format) {
.u8 => switch (SrcType) {
u8 => @memcpy(@as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len], src),
i8, i16, i24, i32 => conv.signedToUnsigned(SrcType, src, u8, @as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len]),
f32 => conv.floatToUnsigned(SrcType, src, u8, @as([*]u8, @ptrCast(@alignCast(dst)))[0..dst_len]),
else => unreachable,
},
.i16 => switch (SrcType) {
i16 => @memcpy(@as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len], src),
u8 => conv.unsignedToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]),
i8, i24, i32 => conv.signedToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]),
f32 => conv.floatToSigned(SrcType, src, i16, @as([*]i16, @ptrCast(@alignCast(dst)))[0..dst_len]),
else => unreachable,
},
.i24 => switch (SrcType) {
i24 => @memcpy(@as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len], src),
u8 => conv.unsignedToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]),
i8, i16, i32 => conv.signedToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]),
f32 => conv.floatToSigned(SrcType, src, i24, @as([*]i24, @ptrCast(@alignCast(dst)))[0..dst_len]),
else => unreachable,
},
.i32 => switch (SrcType) {
i32 => @memcpy(@as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len], src),
u8 => conv.unsignedToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]),
i8, i16, i24 => conv.signedToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]),
f32 => conv.floatToSigned(SrcType, src, i32, @as([*]i32, @ptrCast(@alignCast(dst)))[0..dst_len]),
else => unreachable,
},
.f32 => switch (SrcType) {
f32 => @memcpy(@as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len], src),
u8 => conv.unsignedToFloat(SrcType, src, f32, @as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len]),
i8, i16, i24, i32 => conv.signedToFloat(SrcType, src, f32, @as([*]f32, @ptrCast(@alignCast(dst)))[0..dst_len]),
else => unreachable,
},
};
}
pub fn convertFrom(comptime DestType: type, dst: []DestType, src_format: Format, src: []const u8) void {
const src_len = src.len / src_format.size();
std.debug.assert(src_len == dst.len);
return switch (src_format) {
.u8 => switch (DestType) {
u8 => @memcpy(dst, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len]),
i8, i16, i24, i32 => conv.unsignedToSigned(u8, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
f32 => conv.unsignedToFloat(u8, @as([*]const u8, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
else => unreachable,
},
.i16 => switch (DestType) {
i16 => @memcpy(dst, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len]),
u8 => conv.signedToUnsigned(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
i8, i24, i32 => conv.signedToSigned(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
f32 => conv.signedToFloat(i16, @as([*]const i16, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
else => unreachable,
},
.i24 => switch (DestType) {
i24 => @memcpy(dst, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len]),
u8 => conv.signedToUnsigned(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
i8, i16, i32 => conv.signedToSigned(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
f32 => conv.signedToFloat(i24, @as([*]const i24, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
else => unreachable,
},
.i32 => switch (DestType) {
i32 => @memcpy(dst, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len]),
u8 => conv.signedToUnsigned(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
i8, i16, i24 => conv.signedToSigned(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
f32 => conv.signedToFloat(i32, @as([*]const i32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
else => unreachable,
},
.f32 => switch (DestType) {
f32 => @memcpy(dst, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len]),
u8 => conv.floatToUnsigned(f32, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
i8, i16, i24, i32 => conv.floatToSigned(f32, @as([*]const f32, @ptrCast(@alignCast(src)))[0..src_len], DestType, dst),
else => unreachable,
},
};
}
pub const Device = struct {
id: [:0]const u8,
name: [:0]const u8,
mode: Mode,
channels: []ChannelPosition,
formats: []const Format,
sample_rate: util.Range(u24),
pub const Mode = enum {
playback,
capture,
};
pub fn preferredFormat(device: Device, format: ?Format) Format {
if (format) |f| {
for (device.formats) |fmt| if (f == fmt) return fmt;
}
var best: Format = device.formats[0];
for (device.formats) |fmt| {
if (@intFromEnum(fmt) > @intFromEnum(best)) best = fmt;
}
return best;
}
};
pub const ChannelPosition = enum {
front_center,
front_left,
front_right,
front_left_center,
front_right_center,
back_center,
back_left,
back_right,
side_left,
side_right,
top_center,
top_front_center,
top_front_left,
top_front_right,
top_back_center,
top_back_left,
top_back_right,
lfe,
};
pub const Format = enum(u3) {
u8 = 0,
i16 = 1,
i24 = 2,
i32 = 3,
f32 = 4,
pub inline fn size(format: Format) u8 {
return switch (format) {
.u8 => 1,
.i16 => 2,
.i24 => 3,
.i32, .f32 => 4,
};
}
pub inline fn validSize(format: Format) u8 {
return switch (format) {
.u8 => 1,
.i16 => 2,
.i24 => 3,
.i32, .f32 => 4,
};
}
pub inline fn sizeBits(format: Format) u8 {
return format.size() * 8;
}
pub inline fn validSizeBits(format: Format) u8 {
return format.validSize() * 8;
}
pub inline fn frameSize(format: Format, channels: u8) u8 {
return format.size() * channels;
}
};
test "reference declarations" {
_ = conv;
_ = backends.Context;
_ = backends.Player;
_ = backends.Recorder;
}

527
src/sysaudio/pipewire.zig Normal file
View file

@ -0,0 +1,527 @@
const std = @import("std");
const c = @cImport({
@cInclude("pipewire/pipewire.h");
@cInclude("spa/param/audio/format-utils.h");
});
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const default_sample_rate = 44_100; // Hz
var lib: Lib = undefined;
const Lib = struct {
handle: std.DynLib,
pw_init: *const fn ([*c]c_int, [*c][*c][*c]u8) callconv(.C) void,
pw_deinit: *const fn () callconv(.C) void,
pw_thread_loop_new: *const fn ([*c]const u8, [*c]const c.spa_dict) callconv(.C) ?*c.pw_thread_loop,
pw_thread_loop_destroy: *const fn (?*c.pw_thread_loop) callconv(.C) void,
pw_thread_loop_start: *const fn (?*c.pw_thread_loop) callconv(.C) c_int,
pw_thread_loop_stop: *const fn (?*c.pw_thread_loop) callconv(.C) void,
pw_thread_loop_signal: *const fn (?*c.pw_thread_loop, bool) callconv(.C) void,
pw_thread_loop_wait: *const fn (?*c.pw_thread_loop) callconv(.C) void,
pw_thread_loop_lock: *const fn (?*c.pw_thread_loop) callconv(.C) void,
pw_thread_loop_unlock: *const fn (?*c.pw_thread_loop) callconv(.C) void,
pw_thread_loop_get_loop: *const fn (?*c.pw_thread_loop) callconv(.C) [*c]c.pw_loop,
pw_properties_new: *const fn ([*c]const u8, ...) callconv(.C) [*c]c.pw_properties,
pw_stream_new_simple: *const fn ([*c]c.pw_loop, [*c]const u8, [*c]c.pw_properties, [*c]const c.pw_stream_events, ?*anyopaque) callconv(.C) ?*c.pw_stream,
pw_stream_destroy: *const fn (?*c.pw_stream) callconv(.C) void,
pw_stream_connect: *const fn (?*c.pw_stream, c.spa_direction, u32, c.pw_stream_flags, [*c][*c]const c.spa_pod, u32) callconv(.C) c_int,
pw_stream_queue_buffer: *const fn (?*c.pw_stream, [*c]c.pw_buffer) callconv(.C) c_int,
pw_stream_dequeue_buffer: *const fn (?*c.pw_stream) callconv(.C) [*c]c.pw_buffer,
pw_stream_get_state: *const fn (?*c.pw_stream, [*c][*c]const u8) callconv(.C) c.pw_stream_state,
pub fn load() !void {
lib.handle = std.DynLib.openZ("libpipewire-0.3.so") catch return error.LibraryNotFound;
inline for (@typeInfo(Lib).Struct.fields[1..]) |field| {
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]);
@field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup;
}
}
};
const default_playback = main.Device{
.id = "default-playback",
.name = "Default Device",
.mode = .playback,
.channels = undefined,
.formats = std.meta.tags(main.Format),
.sample_rate = .{
.min = main.min_sample_rate,
.max = main.max_sample_rate,
},
};
const default_capture = main.Device{
.id = "default-capture",
.name = "Default Device",
.mode = .capture,
.channels = undefined,
.formats = std.meta.tags(main.Format),
.sample_rate = .{
.min = main.min_sample_rate,
.max = main.max_sample_rate,
},
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
app_name: [:0]const u8,
// watcher: ?Watcher,
const Watcher = struct {
deviceChangeFn: main.Context.DeviceChangeFn,
user_data: ?*anyopaque,
thread: *c.pw_thread_loop,
aborted: std.atomic.Value(bool),
};
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
try Lib.load();
lib.pw_init(null, null);
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
.app_name = options.app_name,
// TODO: device change watcher
// .watcher = blk: {
// if (options.deviceChangeFn != null) {
// const thread = c.pw_thread_loop_new("device-change-watcher", null) orelse return error.SystemResources;
// const context = c.pw_context_new(c.pw_thread_loop_get_loop(thread), null, 0);
// const core = c.pw_context_connect(context, null, 0);
// const registry = c.pw_core_get_registry(core, c.PW_VERSION_REGISTRY, 0);
// _ = c.spa_zero(registry);
// var registry_listener: c.spa_hook = undefined;
// _ = c.pw_registry_add_listener(registry, registry_listener);
// break :blk .{
// .deviceChangeFn = options.deviceChangeFn.?,
// .user_data = options.user_data,
// .thread = thread,
// .aborted = .{ .raw = false },
// };
// } else break :blk null;
// },
};
return .{ .pipewire = ctx };
}
pub fn deinit(ctx: *Context) void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
lib.pw_deinit();
ctx.allocator.destroy(ctx);
lib.handle.close();
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
try ctx.devices_info.list.append(ctx.allocator, default_playback);
try ctx.devices_info.list.append(ctx.allocator, default_capture);
ctx.devices_info.setDefault(.playback, 0);
ctx.devices_info.setDefault(.capture, 1);
ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 2);
ctx.devices_info.list.items[1].channels = try ctx.allocator.alloc(main.ChannelPosition, 2);
ctx.devices_info.list.items[0].channels[0] = .front_right;
ctx.devices_info.list.items[0].channels[1] = .front_left;
ctx.devices_info.list.items[1].channels[0] = .front_right;
ctx.devices_info.list.items[1].channels[1] = .front_left;
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
const media_role = switch (options.media_role) {
.default => "Screen",
.game => "Game",
.music => "Music",
.movie => "Movie",
.communication => "Communication",
};
var buf: [8]u8 = undefined;
const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate orelse default_sample_rate}) catch unreachable;
const props = lib.pw_properties_new(
c.PW_KEY_MEDIA_TYPE,
"Audio",
c.PW_KEY_MEDIA_CATEGORY,
"Playback",
c.PW_KEY_MEDIA_ROLE,
media_role.ptr,
c.PW_KEY_MEDIA_NAME,
ctx.app_name.ptr,
c.PW_KEY_AUDIO_RATE,
audio_rate.ptr,
@as(*allowzero u0, @ptrFromInt(0)),
);
const stream_events = c.pw_stream_events{
.version = c.PW_VERSION_STREAM_EVENTS,
.process = Player.processCb,
.destroy = null,
.state_changed = stateChangedCb,
.control_info = null,
.io_changed = null,
.param_changed = null,
.add_buffer = null,
.remove_buffer = null,
.drained = null,
.command = null,
.trigger_done = null,
};
const player = try ctx.allocator.create(Player);
errdefer ctx.allocator.destroy(player);
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
const stream = lib.pw_stream_new_simple(
lib.pw_thread_loop_get_loop(thread),
"audio-src",
props,
&stream_events,
player,
) orelse return error.OpeningDevice;
var builder_buf: [256]u8 = undefined;
var pod_builder = c.spa_pod_builder{
.data = &builder_buf,
.size = builder_buf.len,
._padding = 0,
.state = .{
.offset = 0,
.flags = 0,
.frame = null,
},
.callbacks = .{ .funcs = null, .data = null },
};
var info = c.spa_audio_info_raw{
.format = c.SPA_AUDIO_FORMAT_F32,
.channels = @as(u32, @intCast(device.channels.len)),
.rate = options.sample_rate orelse default_sample_rate,
.flags = 0,
.position = undefined,
};
var params = [1][*c]c.spa_pod{
sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info),
};
if (lib.pw_stream_connect(
stream,
c.PW_DIRECTION_OUTPUT,
c.PW_ID_ANY,
c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS,
&params,
params.len,
) < 0) return error.OpeningDevice;
player.* = .{
.allocator = ctx.allocator,
.thread = thread,
.stream = stream,
.is_paused = .{ .raw = false },
.vol = 1.0,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = .f32,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .pipewire = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
const media_role = switch (options.media_role) {
.default => "Screen",
.game => "Game",
.music => "Music",
.movie => "Movie",
.communication => "Communication",
};
var buf: [8]u8 = undefined;
const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate orelse default_sample_rate}) catch unreachable;
const props = lib.pw_properties_new(
c.PW_KEY_MEDIA_TYPE,
"Audio",
c.PW_KEY_MEDIA_CATEGORY,
"Capture",
c.PW_KEY_MEDIA_ROLE,
media_role.ptr,
c.PW_KEY_MEDIA_NAME,
ctx.app_name.ptr,
c.PW_KEY_AUDIO_RATE,
audio_rate.ptr,
@as(*allowzero u0, @ptrFromInt(0)),
);
const stream_events = c.pw_stream_events{
.version = c.PW_VERSION_STREAM_EVENTS,
.process = Recorder.processCb,
.destroy = null,
.state_changed = stateChangedCb,
.control_info = null,
.io_changed = null,
.param_changed = null,
.add_buffer = null,
.remove_buffer = null,
.drained = null,
.command = null,
.trigger_done = null,
};
const recorder = try ctx.allocator.create(Recorder);
errdefer ctx.allocator.destroy(recorder);
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
const stream = lib.pw_stream_new_simple(
lib.pw_thread_loop_get_loop(thread),
"audio-capture",
props,
&stream_events,
recorder,
) orelse return error.OpeningDevice;
var builder_buf: [256]u8 = undefined;
var pod_builder = c.spa_pod_builder{
.data = &builder_buf,
.size = builder_buf.len,
._padding = 0,
.state = .{
.offset = 0,
.flags = 0,
.frame = null,
},
.callbacks = .{ .funcs = null, .data = null },
};
var info = c.spa_audio_info_raw{
.format = c.SPA_AUDIO_FORMAT_F32,
.channels = @as(u32, @intCast(device.channels.len)),
.rate = options.sample_rate orelse default_sample_rate,
.flags = 0,
.position = undefined,
};
var params = [1][*c]c.spa_pod{
sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info),
};
if (lib.pw_stream_connect(
stream,
c.PW_DIRECTION_INPUT,
c.PW_ID_ANY,
c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS,
&params,
params.len,
) < 0) return error.OpeningDevice;
recorder.* = .{
.allocator = ctx.allocator,
.thread = thread,
.stream = stream,
.is_paused = .{ .raw = false },
.vol = 1.0,
.readFn = readFn,
.user_data = options.user_data,
.channels = device.channels,
.format = .f32,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .pipewire = recorder };
}
};
fn stateChangedCb(player_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void {
_ = old_state;
_ = err;
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) {
lib.pw_thread_loop_signal(player.thread, false);
}
}
pub const Player = struct {
allocator: std.mem.Allocator,
thread: *c.pw_thread_loop,
stream: *c.pw_stream,
is_paused: std.atomic.Value(bool),
vol: f32,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn processCb(player_opaque: ?*anyopaque) callconv(.C) void {
var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
const buf = lib.pw_stream_dequeue_buffer(player.stream) orelse return;
if (buf.*.buffer.*.datas[0].data == null) return;
defer _ = lib.pw_stream_queue_buffer(player.stream, buf);
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
if (player.is_paused.load(.Unordered)) {
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
buf.*.buffer.*.datas[0].chunk.*.size = 0;
return;
}
const stride = player.format.frameSize(@intCast(player.channels.len));
const frames = @min(buf.*.requested * stride, buf.*.buffer.*.datas[0].maxsize);
buf.*.buffer.*.datas[0].chunk.*.stride = stride;
buf.*.buffer.*.datas[0].chunk.*.size = @intCast(frames);
player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
}
pub fn deinit(player: *Player) void {
lib.pw_thread_loop_stop(player.thread);
lib.pw_thread_loop_destroy(player.thread);
lib.pw_stream_destroy(player.stream);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
if (lib.pw_thread_loop_start(player.thread) < 0) return error.SystemResources;
lib.pw_thread_loop_lock(player.thread);
lib.pw_thread_loop_wait(player.thread);
lib.pw_thread_loop_unlock(player.thread);
if (lib.pw_stream_get_state(player.stream, null) == c.PW_STREAM_STATE_ERROR) {
return error.CannotPlay;
}
}
pub fn play(player: *Player) !void {
player.is_paused.store(false, .Unordered);
}
pub fn pause(player: *Player) !void {
player.is_paused.store(true, .Unordered);
}
pub fn paused(player: *Player) bool {
return player.is_paused.load(.Unordered);
}
pub fn setVolume(player: *Player, vol: f32) !void {
player.vol = vol;
}
pub fn volume(player: *Player) !f32 {
return player.vol;
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
thread: *c.pw_thread_loop,
stream: *c.pw_stream,
is_paused: std.atomic.Value(bool),
vol: f32,
readFn: main.ReadFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn processCb(recorder_opaque: ?*anyopaque) callconv(.C) void {
var recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?)));
const buf = lib.pw_stream_dequeue_buffer(recorder.stream) orelse return;
if (buf.*.buffer.*.datas[0].data == null) return;
defer _ = lib.pw_stream_queue_buffer(recorder.stream, buf);
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
if (recorder.is_paused.load(.Unordered)) {
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
buf.*.buffer.*.datas[0].chunk.*.size = 0;
return;
}
const frames = buf.*.buffer.*.datas[0].chunk.*.size;
recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
}
pub fn deinit(recorder: *Recorder) void {
lib.pw_thread_loop_stop(recorder.thread);
lib.pw_thread_loop_destroy(recorder.thread);
lib.pw_stream_destroy(recorder.stream);
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
if (lib.pw_thread_loop_start(recorder.thread) < 0) return error.SystemResources;
lib.pw_thread_loop_lock(recorder.thread);
lib.pw_thread_loop_wait(recorder.thread);
lib.pw_thread_loop_unlock(recorder.thread);
if (lib.pw_stream_get_state(recorder.stream, null) == c.PW_STREAM_STATE_ERROR) {
return error.CannotRecord;
}
}
pub fn record(recorder: *Recorder) !void {
recorder.is_paused.store(false, .Unordered);
}
pub fn pause(recorder: *Recorder) !void {
recorder.is_paused.store(true, .Unordered);
}
pub fn paused(recorder: *Recorder) bool {
return recorder.is_paused.load(.Unordered);
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
recorder.vol = vol;
}
pub fn volume(recorder: *Recorder) !f32 {
return recorder.vol;
}
};
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.channels);
}
extern fn sysaudio_spa_format_audio_raw_build(builder: [*c]c.spa_pod_builder, id: u32, info: [*c]c.spa_audio_info_raw) callconv(.C) [*c]c.spa_pod;

View file

@ -0,0 +1,11 @@
#include <pipewire/core.h>
#include <spa/param/audio/format-utils.h>
struct spa_pod *sysaudio_spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info)
{
return spa_format_audio_raw_build(builder, id, info);
}
void sysaudio_pw_registry_add_listener(struct pw_registry *reg, struct spa_hook *reg_listener, struct pw_registry_events *events) {
pw_registry_add_listener(reg, reg_listener, events, NULL);
}

811
src/sysaudio/pulseaudio.zig Normal file
View file

@ -0,0 +1,811 @@
const std = @import("std");
const c = @cImport(@cInclude("pulse/pulseaudio.h"));
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const is_little = @import("builtin").cpu.arch.endian() == .little;
const default_sample_rate = 44_100; // Hz
var lib: Lib = undefined;
const Lib = struct {
handle: std.DynLib,
pa_threaded_mainloop_new: *const fn () callconv(.C) ?*c.pa_threaded_mainloop,
pa_threaded_mainloop_free: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void,
pa_threaded_mainloop_start: *const fn (?*c.pa_threaded_mainloop) callconv(.C) c_int,
pa_threaded_mainloop_stop: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void,
pa_threaded_mainloop_signal: *const fn (?*c.pa_threaded_mainloop, c_int) callconv(.C) void,
pa_threaded_mainloop_wait: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void,
pa_threaded_mainloop_lock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void,
pa_threaded_mainloop_unlock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void,
pa_threaded_mainloop_get_api: *const fn (?*c.pa_threaded_mainloop) callconv(.C) [*c]c.pa_mainloop_api,
pa_operation_unref: *const fn (?*c.pa_operation) callconv(.C) void,
pa_operation_get_state: *const fn (?*const c.pa_operation) callconv(.C) c.pa_operation_state_t,
pa_context_new_with_proplist: *const fn ([*c]c.pa_mainloop_api, [*c]const u8, ?*const c.pa_proplist) callconv(.C) ?*c.pa_context,
pa_context_unref: *const fn (?*c.pa_context) callconv(.C) void,
pa_context_connect: *const fn (?*c.pa_context, [*c]const u8, c.pa_context_flags_t, [*c]const c.pa_spawn_api) callconv(.C) c_int,
pa_context_disconnect: *const fn (?*c.pa_context) callconv(.C) void,
pa_context_subscribe: *const fn (?*c.pa_context, c.pa_subscription_mask_t, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_context_get_state: *const fn (?*const c.pa_context) callconv(.C) c.pa_context_state_t,
pa_context_set_state_callback: *const fn (?*c.pa_context, c.pa_context_notify_cb_t, ?*anyopaque) callconv(.C) void,
pa_context_set_subscribe_callback: *const fn (?*c.pa_context, c.pa_context_subscribe_cb_t, ?*anyopaque) callconv(.C) void,
pa_context_get_sink_input_info: *const fn (?*c.pa_context, u32, c.pa_sink_input_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_context_get_sink_info_list: *const fn (?*c.pa_context, c.pa_sink_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_context_set_sink_input_volume: *const fn (?*c.pa_context, u32, [*c]const c.pa_cvolume, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_context_get_source_info_list: *const fn (?*c.pa_context, c.pa_source_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_context_get_server_info: *const fn (?*c.pa_context, c.pa_server_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_stream_new: *const fn (?*c.pa_context, [*c]const u8, [*c]const c.pa_sample_spec, [*c]const c.pa_channel_map) callconv(.C) ?*c.pa_stream,
pa_stream_unref: *const fn (?*c.pa_stream) callconv(.C) void,
pa_stream_connect_playback: *const fn (?*c.pa_stream, [*c]const u8, [*c]const c.pa_buffer_attr, c.pa_stream_flags_t, [*c]const c.pa_cvolume, ?*c.pa_stream) callconv(.C) c_int,
pa_stream_connect_record: *const fn (?*c.pa_stream, [*c]const u8, [*c]const c.pa_buffer_attr, c.pa_stream_flags_t) callconv(.C) c_int,
pa_stream_disconnect: *const fn (?*c.pa_stream) callconv(.C) c_int,
pa_stream_cork: *const fn (?*c.pa_stream, c_int, c.pa_stream_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation,
pa_stream_is_corked: *const fn (?*const c.pa_stream) callconv(.C) c_int,
pa_stream_begin_write: *const fn (?*c.pa_stream, [*c]?*anyopaque, [*c]usize) callconv(.C) c_int,
pa_stream_peek: *const fn (?*c.pa_stream, [*c]?*anyopaque, [*c]usize) callconv(.C) c_int,
pa_stream_drop: *const fn (?*c.pa_stream) callconv(.C) c_int,
pa_stream_write: *const fn (?*c.pa_stream, ?*const anyopaque, usize, c.pa_free_cb_t, i64, c.pa_seek_mode_t) callconv(.C) c_int,
pa_stream_get_state: *const fn (?*const c.pa_stream) callconv(.C) c.pa_stream_state_t,
pa_stream_get_index: *const fn (?*const c.pa_stream) callconv(.C) u32,
pa_stream_set_state_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void,
pa_stream_set_read_callback: *const fn (?*c.pa_stream, c.pa_stream_request_cb_t, ?*anyopaque) callconv(.C) void,
pa_stream_set_write_callback: *const fn (?*c.pa_stream, c.pa_stream_request_cb_t, ?*anyopaque) callconv(.C) void,
pa_stream_set_underflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void,
pa_stream_set_overflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void,
pa_cvolume_init: *const fn ([*c]c.pa_cvolume) callconv(.C) [*c]c.pa_cvolume,
pa_cvolume_set: *const fn ([*c]c.pa_cvolume, c_uint, c.pa_volume_t) callconv(.C) [*c]c.pa_cvolume,
pa_sw_volume_from_linear: *const fn (f64) callconv(.C) c.pa_volume_t,
pub fn load() !void {
lib.handle = std.DynLib.openZ("libpulse.so") catch return error.LibraryNotFound;
inline for (@typeInfo(Lib).Struct.fields[1..]) |field| {
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]);
@field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup;
}
}
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
app_name: [:0]const u8,
main_loop: *c.pa_threaded_mainloop,
pulse_ctx: *c.pa_context,
pulse_ctx_state: c.pa_context_state_t,
default_sink: ?[:0]const u8,
default_source: ?[:0]const u8,
watcher: ?Watcher,
const Watcher = struct {
deviceChangeFn: main.Context.DeviceChangeFn,
user_data: ?*anyopaque,
};
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
try Lib.load();
const main_loop = lib.pa_threaded_mainloop_new() orelse
return error.OutOfMemory;
errdefer lib.pa_threaded_mainloop_free(main_loop);
const main_loop_api = lib.pa_threaded_mainloop_get_api(main_loop);
const pulse_ctx = lib.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse
return error.OutOfMemory;
errdefer lib.pa_context_unref(pulse_ctx);
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = Context{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
.app_name = options.app_name,
.main_loop = main_loop,
.pulse_ctx = pulse_ctx,
.pulse_ctx_state = c.PA_CONTEXT_UNCONNECTED,
.default_sink = null,
.default_source = null,
.watcher = if (options.deviceChangeFn) |dcf| .{
.deviceChangeFn = dcf,
.user_data = options.user_data,
} else null,
};
if (lib.pa_context_connect(pulse_ctx, null, 0, null) != 0)
return error.ConnectionRefused;
errdefer lib.pa_context_disconnect(pulse_ctx);
lib.pa_context_set_state_callback(pulse_ctx, contextStateOp, ctx);
if (lib.pa_threaded_mainloop_start(main_loop) != 0)
return error.SystemResources;
errdefer lib.pa_threaded_mainloop_stop(main_loop);
lib.pa_threaded_mainloop_lock(main_loop);
defer lib.pa_threaded_mainloop_unlock(main_loop);
while (true) {
switch (ctx.pulse_ctx_state) {
// The context hasn't been connected yet.
c.PA_CONTEXT_UNCONNECTED,
// A connection is being established.
c.PA_CONTEXT_CONNECTING,
// The client is authorizing itself to the daemon.
c.PA_CONTEXT_AUTHORIZING,
// The client is passing its application name to the daemon.
c.PA_CONTEXT_SETTING_NAME,
=> lib.pa_threaded_mainloop_wait(main_loop),
// The connection is established, the context is ready to execute operations.
c.PA_CONTEXT_READY => break,
// The connection was terminated cleanly.
c.PA_CONTEXT_TERMINATED,
// The connection failed or was disconnected.
c.PA_CONTEXT_FAILED,
=> return error.ConnectionRefused,
else => unreachable,
}
}
// subscribe to events
if (options.deviceChangeFn != null) {
lib.pa_context_set_subscribe_callback(pulse_ctx, subscribeOp, ctx);
const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE;
const subscribe_op = lib.pa_context_subscribe(pulse_ctx, events, null, ctx) orelse
return error.OutOfMemory;
lib.pa_operation_unref(subscribe_op);
}
return .{ .pulseaudio = ctx };
}
fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, ctx_opaque: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(ctx_opaque.?)));
ctx.watcher.?.deviceChangeFn(ctx.watcher.?.user_data);
}
fn contextStateOp(pulse_ctx: ?*c.pa_context, ctx_opaque: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(ctx_opaque.?)));
ctx.pulse_ctx_state = lib.pa_context_get_state(pulse_ctx);
lib.pa_threaded_mainloop_signal(ctx.main_loop, 0);
}
pub fn deinit(ctx: *Context) void {
lib.pa_context_set_subscribe_callback(ctx.pulse_ctx, null, null);
lib.pa_context_set_state_callback(ctx.pulse_ctx, null, null);
lib.pa_context_disconnect(ctx.pulse_ctx);
lib.pa_context_unref(ctx.pulse_ctx);
lib.pa_threaded_mainloop_stop(ctx.main_loop);
lib.pa_threaded_mainloop_free(ctx.main_loop);
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
ctx.allocator.destroy(ctx);
lib.handle.close();
}
pub fn refresh(ctx: *Context) !void {
lib.pa_threaded_mainloop_lock(ctx.main_loop);
defer lib.pa_threaded_mainloop_unlock(ctx.main_loop);
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear();
const list_sink_op = lib.pa_context_get_sink_info_list(ctx.pulse_ctx, sinkInfoOp, ctx);
const list_source_op = lib.pa_context_get_source_info_list(ctx.pulse_ctx, sourceInfoOp, ctx);
const server_info_op = lib.pa_context_get_server_info(ctx.pulse_ctx, serverInfoOp, ctx);
performOperation(ctx.main_loop, list_sink_op);
performOperation(ctx.main_loop, list_source_op);
performOperation(ctx.main_loop, server_info_op);
defer {
if (ctx.default_sink) |d|
ctx.allocator.free(d);
if (ctx.default_source) |d|
ctx.allocator.free(d);
}
for (ctx.devices_info.list.items, 0..) |device, i| {
if ((device.mode == .playback and
ctx.default_sink != null and
std.mem.eql(u8, device.id, ctx.default_sink.?)) or
//
(device.mode == .capture and
ctx.default_source != null and
std.mem.eql(u8, device.id, ctx.default_source.?)))
{
ctx.devices_info.setDefault(device.mode, i);
break;
}
}
}
fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, user_data: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?)));
defer lib.pa_threaded_mainloop_signal(ctx.main_loop, 0);
ctx.default_sink = ctx.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return;
ctx.default_source = ctx.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch {
ctx.allocator.free(ctx.default_sink.?);
return;
};
}
fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?)));
if (eol != 0) {
lib.pa_threaded_mainloop_signal(ctx.main_loop, 0);
return;
}
ctx.deviceInfoOp(info, .playback) catch return;
}
fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void {
var ctx = @as(*Context, @ptrCast(@alignCast(user_data.?)));
if (eol != 0) {
lib.pa_threaded_mainloop_signal(ctx.main_loop, 0);
return;
}
ctx.deviceInfoOp(info, .capture) catch return;
}
fn deviceInfoOp(ctx: *Context, info: anytype, mode: main.Device.Mode) !void {
const id = try ctx.allocator.dupeZ(u8, std.mem.span(info.*.name));
errdefer ctx.allocator.free(id);
const name = try ctx.allocator.dupeZ(u8, std.mem.span(info.*.description));
errdefer ctx.allocator.free(name);
const device = main.Device{
.mode = mode,
.channels = blk: {
const channels = try ctx.allocator.alloc(main.ChannelPosition, info.*.channel_map.channels);
for (channels, 0..) |*ch, i| ch.* = try fromPAChannelPos(info.*.channel_map.map[i]);
break :blk channels;
},
.formats = available_formats,
.sample_rate = .{
.min = @as(u24, @intCast(info.*.sample_spec.rate)),
.max = @as(u24, @intCast(info.*.sample_spec.rate)),
},
.id = id,
.name = name,
};
try ctx.devices_info.list.append(ctx.allocator, device);
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
lib.pa_threaded_mainloop_lock(ctx.main_loop);
defer lib.pa_threaded_mainloop_unlock(ctx.main_loop);
const format = device.preferredFormat(options.format);
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
const sample_spec = c.pa_sample_spec{
.format = toPAFormat(format),
.rate = sample_rate,
.channels = @as(u5, @intCast(device.channels.len)),
};
const channel_map = try toPAChannelMap(device.channels);
const stream = lib.pa_stream_new(ctx.pulse_ctx, ctx.app_name.ptr, &sample_spec, &channel_map);
if (stream == null)
return error.OutOfMemory;
errdefer lib.pa_stream_unref(stream);
var status: StreamStatus = .{ .main_loop = ctx.main_loop, .status = .unknown };
lib.pa_stream_set_state_callback(stream, streamStateOp, &status);
const buf_attr = c.pa_buffer_attr{
.maxlength = std.math.maxInt(u32),
.tlength = std.math.maxInt(u32),
.prebuf = 0,
.minreq = std.math.maxInt(u32),
.fragsize = std.math.maxInt(u32),
};
const flags =
c.PA_STREAM_START_CORKED |
c.PA_STREAM_AUTO_TIMING_UPDATE |
c.PA_STREAM_INTERPOLATE_TIMING |
c.PA_STREAM_ADJUST_LATENCY;
if (lib.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) {
return error.OpeningDevice;
}
errdefer _ = lib.pa_stream_disconnect(stream);
while (true) {
switch (status.status) {
.unknown => lib.pa_threaded_mainloop_wait(ctx.main_loop),
.ready => break,
.failure => return error.OpeningDevice,
}
}
const player = try ctx.allocator.create(Player);
player.* = .{
.allocator = ctx.allocator,
.main_loop = ctx.main_loop,
.pulse_ctx = ctx.pulse_ctx,
.stream = stream.?,
.write_ptr = undefined,
.vol = 1.0,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = format,
.sample_rate = sample_rate,
};
return .{ .pulseaudio = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
lib.pa_threaded_mainloop_lock(ctx.main_loop);
defer lib.pa_threaded_mainloop_unlock(ctx.main_loop);
const format = device.preferredFormat(options.format);
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
const sample_spec = c.pa_sample_spec{
.format = toPAFormat(format),
.rate = sample_rate,
.channels = @as(u5, @intCast(device.channels.len)),
};
const channel_map = try toPAChannelMap(device.channels);
const stream = lib.pa_stream_new(ctx.pulse_ctx, ctx.app_name.ptr, &sample_spec, &channel_map);
if (stream == null)
return error.OutOfMemory;
errdefer lib.pa_stream_unref(stream);
var status: StreamStatus = .{ .main_loop = ctx.main_loop, .status = .unknown };
lib.pa_stream_set_state_callback(stream, streamStateOp, &status);
const buf_attr = c.pa_buffer_attr{
.maxlength = std.math.maxInt(u32),
.tlength = std.math.maxInt(u32),
.prebuf = 0,
.minreq = std.math.maxInt(u32),
.fragsize = std.math.maxInt(u32),
};
const flags =
c.PA_STREAM_START_CORKED |
c.PA_STREAM_AUTO_TIMING_UPDATE |
c.PA_STREAM_INTERPOLATE_TIMING |
c.PA_STREAM_ADJUST_LATENCY;
if (lib.pa_stream_connect_record(stream, device.id.ptr, &buf_attr, flags) != 0) {
return error.OpeningDevice;
}
errdefer _ = lib.pa_stream_disconnect(stream);
while (true) {
switch (status.status) {
.unknown => lib.pa_threaded_mainloop_wait(ctx.main_loop),
.ready => break,
.failure => return error.OpeningDevice,
}
}
const recorder = try ctx.allocator.create(Recorder);
recorder.* = .{
.allocator = ctx.allocator,
.main_loop = ctx.main_loop,
.pulse_ctx = ctx.pulse_ctx,
.stream = stream.?,
.peek_ptr = undefined,
.peek_index = 0,
.vol = 1.0,
.readFn = readFn,
.user_data = options.user_data,
.channels = device.channels,
.format = format,
.sample_rate = sample_rate,
};
return .{ .pulseaudio = recorder };
}
const StreamStatus = struct {
main_loop: *c.pa_threaded_mainloop,
status: enum(u8) {
unknown,
ready,
failure,
},
};
fn streamStateOp(stream: ?*c.pa_stream, stream_status_opaque: ?*anyopaque) callconv(.C) void {
const stream_status = @as(*StreamStatus, @ptrCast(@alignCast(stream_status_opaque.?)));
switch (lib.pa_stream_get_state(stream)) {
c.PA_STREAM_UNCONNECTED,
c.PA_STREAM_CREATING,
c.PA_STREAM_TERMINATED,
=> {},
c.PA_STREAM_READY => {
stream_status.status = .ready;
lib.pa_threaded_mainloop_signal(stream_status.main_loop, 0);
},
c.PA_STREAM_FAILED => {
stream_status.status = .failure;
lib.pa_threaded_mainloop_signal(stream_status.main_loop, 0);
},
else => unreachable,
}
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
main_loop: *c.pa_threaded_mainloop,
pulse_ctx: *c.pa_context,
stream: *c.pa_stream,
write_ptr: [*]u8,
vol: f32,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(player: *Player) void {
lib.pa_threaded_mainloop_lock(player.main_loop);
lib.pa_stream_set_write_callback(player.stream, null, null);
lib.pa_stream_set_state_callback(player.stream, null, null);
lib.pa_stream_set_underflow_callback(player.stream, null, null);
lib.pa_stream_set_overflow_callback(player.stream, null, null);
_ = lib.pa_stream_disconnect(player.stream);
lib.pa_stream_unref(player.stream);
lib.pa_threaded_mainloop_unlock(player.main_loop);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
const op = lib.pa_stream_cork(player.stream, 0, null, null) orelse
return error.CannotPlay;
lib.pa_operation_unref(op);
lib.pa_stream_set_write_callback(player.stream, playbackStreamWriteOp, player);
}
fn playbackStreamWriteOp(stream: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void {
var player = @as(*Player, @ptrCast(@alignCast(user_data.?)));
var frames_left = nbytes;
if (lib.pa_stream_begin_write(
stream,
@as(
[*c]?*anyopaque,
@ptrCast(@alignCast(&player.write_ptr)),
),
&frames_left,
) != 0) return;
player.writeFn(player.user_data, player.write_ptr[0..frames_left]);
if (lib.pa_stream_write(
stream,
player.write_ptr,
frames_left,
null,
0,
c.PA_SEEK_RELATIVE,
) != 0) return;
}
pub fn play(player: *Player) !void {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
if (lib.pa_stream_is_corked(player.stream) > 0) {
const op = lib.pa_stream_cork(player.stream, 0, null, null) orelse
return error.CannotPlay;
lib.pa_operation_unref(op);
}
}
pub fn pause(player: *Player) !void {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
if (lib.pa_stream_is_corked(player.stream) == 0) {
const op = lib.pa_stream_cork(player.stream, 1, null, null) orelse
return error.CannotPause;
lib.pa_operation_unref(op);
}
}
pub fn paused(player: *Player) bool {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
return lib.pa_stream_is_corked(player.stream) > 0;
}
pub fn setVolume(player: *Player, vol: f32) !void {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
var cvolume: c.pa_cvolume = undefined;
_ = lib.pa_cvolume_init(&cvolume);
_ = lib.pa_cvolume_set(&cvolume, @as(c_uint, @intCast(player.channels.len)), lib.pa_sw_volume_from_linear(vol));
performOperation(
player.main_loop,
lib.pa_context_set_sink_input_volume(
player.pulse_ctx,
lib.pa_stream_get_index(player.stream),
&cvolume,
successOp,
player,
),
);
}
pub fn volume(player: *Player) !f32 {
lib.pa_threaded_mainloop_lock(player.main_loop);
defer lib.pa_threaded_mainloop_unlock(player.main_loop);
performOperation(
player.main_loop,
lib.pa_context_get_sink_input_info(
player.pulse_ctx,
lib.pa_stream_get_index(player.stream),
sinkInputInfoOp,
player,
),
);
return player.vol;
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
main_loop: *c.pa_threaded_mainloop,
pulse_ctx: *c.pa_context,
stream: *c.pa_stream,
peek_ptr: [*]u8,
peek_index: usize,
vol: f32,
readFn: main.ReadFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(recorder: *Recorder) void {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
lib.pa_stream_set_write_callback(recorder.stream, null, null);
lib.pa_stream_set_state_callback(recorder.stream, null, null);
lib.pa_stream_set_underflow_callback(recorder.stream, null, null);
lib.pa_stream_set_overflow_callback(recorder.stream, null, null);
_ = lib.pa_stream_disconnect(recorder.stream);
lib.pa_stream_unref(recorder.stream);
lib.pa_threaded_mainloop_unlock(recorder.main_loop);
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
const op = lib.pa_stream_cork(recorder.stream, 0, null, null) orelse
return error.CannotRecord;
lib.pa_operation_unref(op);
lib.pa_stream_set_read_callback(recorder.stream, playbackStreamReadOp, recorder);
}
fn playbackStreamReadOp(stream: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void {
var recorder = @as(*Recorder, @ptrCast(@alignCast(user_data.?)));
var frames_left = nbytes;
var peek_ptr: ?*anyopaque = undefined;
if (lib.pa_stream_peek(stream, &peek_ptr, &frames_left) != 0) {
if (std.debug.runtime_safety) unreachable;
return;
}
if (peek_ptr) |ptr| {
recorder.peek_ptr = @ptrCast(ptr);
recorder.readFn(recorder.user_data, (recorder.peek_ptr + recorder.peek_index)[0..frames_left]);
recorder.peek_index += frames_left;
} else {
_ = lib.pa_stream_drop(stream);
}
}
pub fn record(recorder: *Recorder) !void {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
if (lib.pa_stream_is_corked(recorder.stream) > 0) {
const op = lib.pa_stream_cork(recorder.stream, 0, null, null) orelse
return error.CannotRecord;
lib.pa_operation_unref(op);
}
}
pub fn pause(recorder: *Recorder) !void {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
if (lib.pa_stream_is_corked(recorder.stream) == 0) {
const op = lib.pa_stream_cork(recorder.stream, 1, null, null) orelse
return error.CannotPause;
lib.pa_operation_unref(op);
}
}
pub fn paused(recorder: *Recorder) bool {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
return lib.pa_stream_is_corked(recorder.stream) > 0;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
var cvolume: c.pa_cvolume = undefined;
_ = lib.pa_cvolume_init(&cvolume);
_ = lib.pa_cvolume_set(&cvolume, @as(c_uint, @intCast(recorder.channels.len)), lib.pa_sw_volume_from_linear(vol));
performOperation(
recorder.main_loop,
lib.pa_context_set_sink_input_volume(
recorder.pulse_ctx,
lib.pa_stream_get_index(recorder.stream),
&cvolume,
successOp,
recorder,
),
);
}
pub fn volume(recorder: *Recorder) !f32 {
lib.pa_threaded_mainloop_lock(recorder.main_loop);
defer lib.pa_threaded_mainloop_unlock(recorder.main_loop);
performOperation(
recorder.main_loop,
lib.pa_context_get_sink_input_info(
recorder.pulse_ctx,
lib.pa_stream_get_index(recorder.stream),
sinkInputInfoOp,
recorder,
),
);
return recorder.vol;
}
};
fn successOp(_: ?*c.pa_context, success: c_int, player_opaque: ?*anyopaque) callconv(.C) void {
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
if (success == 1)
lib.pa_threaded_mainloop_signal(player.main_loop, 0);
}
fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, player_opaque: ?*anyopaque) callconv(.C) void {
var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
if (eol != 0) {
lib.pa_threaded_mainloop_signal(player.main_loop, 0);
return;
}
player.vol = @as(f32, @floatFromInt(info.*.volume.values[0])) / @as(f32, @floatFromInt(c.PA_VOLUME_NORM));
}
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.id);
allocator.free(device.name);
allocator.free(device.channels);
}
fn performOperation(main_loop: *c.pa_threaded_mainloop, op: ?*c.pa_operation) void {
while (true) {
switch (lib.pa_operation_get_state(op)) {
c.PA_OPERATION_RUNNING => lib.pa_threaded_mainloop_wait(main_loop),
c.PA_OPERATION_DONE => return lib.pa_operation_unref(op),
c.PA_OPERATION_CANCELLED => return lib.pa_operation_unref(op),
else => unreachable,
}
}
}
pub const available_formats = &[_]main.Format{ .u8, .i16, .i24, .i32, .f32 };
pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.ChannelPosition {
return switch (pos) {
c.PA_CHANNEL_POSITION_MONO => .front_center,
c.PA_CHANNEL_POSITION_FRONT_LEFT => .front_left, // PA_CHANNEL_POSITION_LEFT
c.PA_CHANNEL_POSITION_FRONT_RIGHT => .front_right, // PA_CHANNEL_POSITION_RIGHT
c.PA_CHANNEL_POSITION_FRONT_CENTER => .front_center, // PA_CHANNEL_POSITION_CENTER
c.PA_CHANNEL_POSITION_REAR_CENTER => .back_center,
c.PA_CHANNEL_POSITION_REAR_LEFT => .back_left,
c.PA_CHANNEL_POSITION_REAR_RIGHT => .back_right,
c.PA_CHANNEL_POSITION_LFE => .lfe, // PA_CHANNEL_POSITION_SUBWOOFER
c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER => .front_left_center,
c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER => .front_right_center,
c.PA_CHANNEL_POSITION_SIDE_LEFT => .side_left,
c.PA_CHANNEL_POSITION_SIDE_RIGHT => .side_right,
c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => .front_center,
c.PA_CHANNEL_POSITION_TOP_CENTER => .top_center,
c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT => .top_front_left,
c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT => .top_front_right,
c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER => .top_front_center,
c.PA_CHANNEL_POSITION_TOP_REAR_LEFT => .top_back_left,
c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT => .top_back_right,
c.PA_CHANNEL_POSITION_TOP_REAR_CENTER => .top_back_center,
else => error.Invalid,
};
}
pub fn toPAFormat(format: main.Format) c.pa_sample_format_t {
return switch (format) {
.u8 => c.PA_SAMPLE_U8,
.i16 => if (is_little) c.PA_SAMPLE_S16LE else c.PA_SAMPLE_S16BE,
.i24 => if (is_little) c.PA_SAMPLE_S24LE else c.PA_SAMPLE_S24LE,
.i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE,
.f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE,
};
}
pub fn toPAChannelMap(channels: []const main.ChannelPosition) !c.pa_channel_map {
var channel_map: c.pa_channel_map = undefined;
channel_map.channels = @as(u5, @intCast(channels.len));
for (channels, 0..) |ch, i|
channel_map.map[i] = try toPAChannelPos(ch);
return channel_map;
}
fn toPAChannelPos(channel_id: main.ChannelPosition) !c.pa_channel_position_t {
return switch (channel_id) {
.lfe => c.PA_CHANNEL_POSITION_LFE,
.front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER,
.front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT,
.front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT,
.front_left_center => c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
.front_right_center => c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
.back_center => c.PA_CHANNEL_POSITION_REAR_CENTER,
.back_left => c.PA_CHANNEL_POSITION_REAR_LEFT,
.back_right => c.PA_CHANNEL_POSITION_REAR_RIGHT,
.side_left => c.PA_CHANNEL_POSITION_SIDE_LEFT,
.side_right => c.PA_CHANNEL_POSITION_SIDE_RIGHT,
.top_center => c.PA_CHANNEL_POSITION_TOP_CENTER,
.top_front_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER,
.top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT,
.top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
.top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER,
.top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT,
.top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT,
};
}

61
src/sysaudio/util.zig Normal file
View file

@ -0,0 +1,61 @@
const std = @import("std");
const main = @import("main.zig");
pub const DevicesInfo = struct {
list: std.ArrayListUnmanaged(main.Device),
default_output: ?usize,
default_input: ?usize,
pub fn init() DevicesInfo {
return .{
.list = .{},
.default_output = null,
.default_input = null,
};
}
pub fn clear(device_info: *DevicesInfo) void {
device_info.default_output = null;
device_info.default_input = null;
device_info.list.clearRetainingCapacity();
}
pub fn get(device_info: DevicesInfo, i: usize) main.Device {
return device_info.list.items[i];
}
pub fn default(device_info: DevicesInfo, mode: main.Device.Mode) ?main.Device {
const index = switch (mode) {
.playback => device_info.default_output,
.capture => device_info.default_input,
} orelse {
for (device_info.list.items) |device| {
if (device.mode == mode) {
return device;
}
}
return null;
};
return device_info.get(index);
}
pub fn setDefault(device_info: *DevicesInfo, mode: main.Device.Mode, i: usize) void {
switch (mode) {
.playback => device_info.default_output = i,
.capture => device_info.default_input = i,
}
}
};
pub fn Range(comptime T: type) type {
return struct {
min: T,
max: T,
pub fn clamp(range: @This(), val: T) T {
return std.math.clamp(val, range.min, range.max);
}
};
}
pub fn doNothing() callconv(.C) void {}

1053
src/sysaudio/wasapi.zig Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

275
src/sysaudio/webaudio.zig Normal file
View file

@ -0,0 +1,275 @@
const std = @import("std");
const js = @import("sysjs");
const main = @import("main.zig");
const backends = @import("backends.zig");
const util = @import("util.zig");
const default_sample_rate = 44_100; // Hz
const channel_size = 1024;
const channel_size_bytes = channel_size * @sizeOf(f32);
const default_playback = main.Device{
.id = "default-playback",
.name = "Default Device",
.mode = .playback,
.channels = undefined,
.formats = &.{.f32},
.sample_rate = .{
.min = 8_000,
.max = 96_000,
},
};
pub const Context = struct {
allocator: std.mem.Allocator,
devices_info: util.DevicesInfo,
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
_ = options;
const audio_context = js.global().get("AudioContext");
if (audio_context.is(.undefined))
return error.ConnectionRefused;
const ctx = try allocator.create(Context);
errdefer allocator.destroy(ctx);
ctx.* = .{
.allocator = allocator,
.devices_info = util.DevicesInfo.init(),
};
return .{ .webaudio = ctx };
}
pub fn deinit(ctx: *Context) void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.list.deinit(ctx.allocator);
ctx.allocator.destroy(ctx);
}
pub fn refresh(ctx: *Context) !void {
for (ctx.devices_info.list.items) |d|
freeDevice(ctx.allocator, d);
ctx.devices_info.clear(ctx.allocator);
try ctx.devices_info.list.append(ctx.allocator, default_playback);
ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 2);
ctx.devices_info.list.items[0].channels[0] = .front_left;
ctx.devices_info.list.items[0].channels[1] = .front_right;
ctx.devices_info.setDefault(.playback, 0);
}
pub fn devices(ctx: Context) []const main.Device {
return ctx.devices_info.list.items;
}
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
return ctx.devices_info.default(mode);
}
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
const context_options = js.createMap();
defer context_options.deinit();
context_options.set("sampleRate", js.createNumber(options.sample_rate orelse default_sample_rate));
const audio_context = js.constructType("AudioContext", &.{context_options.toValue()});
const gain_node = audio_context.call("createGain", &.{
js.createNumber(1),
js.createNumber(0),
js.createNumber(device.channels.len),
}).view(.object);
const process_node = audio_context.call("createScriptProcessor", &.{
js.createNumber(channel_size),
js.createNumber(device.channels.len),
}).view(.object);
const player = try ctx.allocator.create(Player);
errdefer ctx.allocator.destroy(player);
var captures = try ctx.allocator.alloc(js.Value, 1);
captures[0] = js.createNumber(@intFromPtr(player));
const document = js.global().get("document").view(.object);
defer document.deinit();
const click_event_str = js.createString("click");
defer click_event_str.deinit();
const resume_on_click = js.createFunction(Player.resumeOnClick, captures);
_ = document.call("addEventListener", &.{ click_event_str.toValue(), resume_on_click.toValue() });
const audio_process_event = js.createFunction(Player.audioProcessEvent, captures);
defer audio_process_event.deinit();
process_node.set("onaudioprocess", audio_process_event.toValue());
player.* = .{
.allocator = ctx.allocator,
.audio_context = audio_context,
.process_node = process_node,
.gain_node = gain_node,
.process_captures = captures,
.resume_on_click = resume_on_click,
.buf = try ctx.allocator.alloc(u8, channel_size_bytes * device.channels.len),
.buf_js = js.constructType("Uint8Array", &.{js.createNumber(channel_size_bytes)}),
.is_paused = false,
.writeFn = writeFn,
.user_data = options.user_data,
.channels = device.channels,
.format = .f32,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .webaudio = player };
}
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
_ = readFn;
const recorder = try ctx.allocator.create(Recorder);
recorder.* = .{
.allocator = ctx.allocator,
.is_paused = false,
.vol = 1.0,
.channels = device.channels,
.format = options.format,
.sample_rate = options.sample_rate orelse default_sample_rate,
};
return .{ .webaudio = recorder };
}
};
pub const Player = struct {
allocator: std.mem.Allocator,
audio_context: js.Object,
process_node: js.Object,
gain_node: js.Object,
process_captures: []js.Value,
resume_on_click: js.Function,
buf: []u8,
buf_js: js.Object,
is_paused: bool,
writeFn: main.WriteFn,
user_data: ?*anyopaque,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(player: *Player) void {
player.resume_on_click.deinit();
player.buf_js.deinit();
player.gain_node.deinit();
player.process_node.deinit();
player.audio_context.deinit();
player.allocator.free(player.process_captures);
player.allocator.free(player.buf);
player.allocator.destroy(player);
}
pub fn start(player: *Player) !void {
const destination = player.audio_context.get("destination").view(.object);
defer destination.deinit();
_ = player.gain_node.call("connect", &.{destination.toValue()});
_ = player.process_node.call("connect", &.{player.gain_node.toValue()});
}
fn resumeOnClick(args: js.Object, _: usize, captures: []js.Value) js.Value {
const player = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num)))));
player.play() catch {};
const document = js.global().get("document").view(.object);
defer document.deinit();
const event = args.getIndex(0).view(.object);
defer event.deinit();
_ = document.call("removeEventListener", &.{ event.toValue(), player.resume_on_click.toValue() });
return js.createUndefined();
}
fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value {
const player = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num)))));
const event = args.getIndex(0).view(.object);
defer event.deinit();
const output_buffer = event.get("outputBuffer").view(.object);
defer output_buffer.deinit();
player.writeFn(player.user_data, player.buf[0..channel_size]);
for (player.channels, 0..) |_, i| {
player.buf_js.copyBytes(player.buf[i * channel_size_bytes .. (i + 1) * channel_size_bytes]);
const buf_f32_js = js.constructType("Float32Array", &.{ player.buf_js.get("buffer"), player.buf_js.get("byteOffset"), js.createNumber(channel_size) });
defer buf_f32_js.deinit();
_ = output_buffer.call("copyToChannel", &.{ buf_f32_js.toValue(), js.createNumber(i) });
}
return js.createUndefined();
}
pub fn play(player: *Player) !void {
_ = player.audio_context.call("resume", &.{js.createUndefined()});
player.is_paused = false;
}
pub fn pause(player: *Player) !void {
_ = player.audio_context.call("suspend", &.{js.createUndefined()});
player.is_paused = true;
}
pub fn paused(player: *Player) bool {
return player.is_paused;
}
pub fn setVolume(player: *Player, vol: f32) !void {
const gain = player.gain_node.get("gain").view(.object);
defer gain.deinit();
gain.set("value", js.createNumber(vol));
}
pub fn volume(player: *Player) !f32 {
const gain = player.gain_node.get("gain").view(.object);
defer gain.deinit();
return @as(f32, @floatCast(gain.get("value").view(.num)));
}
};
pub const Recorder = struct {
allocator: std.mem.Allocator,
is_paused: bool,
vol: f32,
channels: []main.ChannelPosition,
format: main.Format,
sample_rate: u24,
pub fn deinit(recorder: *Recorder) void {
recorder.allocator.destroy(recorder);
}
pub fn start(recorder: *Recorder) !void {
_ = recorder;
}
pub fn record(recorder: *Recorder) !void {
recorder.is_paused = false;
}
pub fn pause(recorder: *Recorder) !void {
recorder.is_paused = true;
}
pub fn paused(recorder: *Recorder) bool {
return recorder.is_paused;
}
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
recorder.vol = vol;
}
pub fn volume(recorder: *Recorder) !f32 {
return recorder.vol;
}
};
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
allocator.free(device.channels);
}