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:
parent
d64d30c7db
commit
bca1543391
16 changed files with 7876 additions and 0 deletions
835
src/sysaudio/alsa.zig
Normal file
835
src/sysaudio/alsa.zig
Normal 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(¶ms);
|
||||
defer lib.snd_pcm_hw_params_free(params);
|
||||
if (lib.snd_pcm_hw_params_any(pcm, params) < 0)
|
||||
continue;
|
||||
|
||||
if (lib.snd_pcm_hw_params_can_pause(params) == 0)
|
||||
continue;
|
||||
|
||||
const device = main.Device{
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
const chmap = lib.snd_pcm_query_chmaps(pcm);
|
||||
if (chmap) |_| {
|
||||
defer lib.snd_pcm_free_chmaps(chmap);
|
||||
|
||||
if (chmap[0] == null) continue;
|
||||
|
||||
const channels = try ctx.allocator.alloc(main.ChannelPosition, chmap.*.*.map.channels);
|
||||
for (channels, 0..) |*ch, i|
|
||||
ch.* = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice;
|
||||
break :blk channels;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.formats = blk: {
|
||||
var fmt_mask: ?*c.snd_pcm_format_mask_t = null;
|
||||
_ = lib.snd_pcm_format_mask_malloc(&fmt_mask);
|
||||
defer lib.snd_pcm_format_mask_free(fmt_mask);
|
||||
lib.snd_pcm_format_mask_none(fmt_mask);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE);
|
||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE);
|
||||
lib.snd_pcm_hw_params_get_format_mask(params, fmt_mask);
|
||||
|
||||
var fmt_arr = std.ArrayList(main.Format).init(ctx.allocator);
|
||||
inline for (std.meta.tags(main.Format)) |format| {
|
||||
if (lib.snd_pcm_format_mask_test(fmt_mask, toAlsaFormat(format)) != 0) {
|
||||
try fmt_arr.append(format);
|
||||
}
|
||||
}
|
||||
|
||||
break :blk try fmt_arr.toOwnedSlice();
|
||||
},
|
||||
.sample_rate = blk: {
|
||||
var rate_min: c_uint = 0;
|
||||
var rate_max: c_uint = 0;
|
||||
if (lib.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0)
|
||||
continue;
|
||||
if (lib.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0)
|
||||
continue;
|
||||
break :blk .{
|
||||
.min = @as(u24, @intCast(rate_min)),
|
||||
.max = @as(u24, @intCast(rate_max)),
|
||||
};
|
||||
},
|
||||
.id = try ctx.allocator.dupeZ(u8, id),
|
||||
.name = try ctx.allocator.dupeZ(u8, name),
|
||||
};
|
||||
|
||||
try ctx.devices_info.list.append(ctx.allocator, device);
|
||||
|
||||
if (ctx.devices_info.default(mode) == null and dev_idx == 0) {
|
||||
ctx.devices_info.setDefault(mode, ctx.devices_info.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (lib.snd_card_next(&card_idx) < 0)
|
||||
return error.SystemResources;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devices(ctx: Context) []const main.Device {
|
||||
return ctx.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return ctx.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createStream(
|
||||
ctx: Context,
|
||||
device: main.Device,
|
||||
format: main.Format,
|
||||
sample_rate: u24,
|
||||
pcm: *?*c.snd_pcm_t,
|
||||
mixer: *?*c.snd_mixer_t,
|
||||
selem: *?*c.snd_mixer_selem_id_t,
|
||||
mixer_elm: *?*c.snd_mixer_elem_t,
|
||||
period_size: *c_ulong,
|
||||
) !void {
|
||||
if (lib.snd_pcm_open(pcm, device.id.ptr, modeToStream(device.mode), 0) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = lib.snd_pcm_close(pcm.*);
|
||||
{
|
||||
var hw_params: ?*c.snd_pcm_hw_params_t = null;
|
||||
|
||||
if ((lib.snd_pcm_set_params(
|
||||
pcm.*,
|
||||
toAlsaFormat(format),
|
||||
c.SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
@as(c_uint, @intCast(device.channels.len)),
|
||||
sample_rate,
|
||||
1,
|
||||
main.default_latency,
|
||||
)) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = lib.snd_pcm_hw_free(pcm.*);
|
||||
|
||||
if (lib.snd_pcm_hw_params_malloc(&hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
defer lib.snd_pcm_hw_params_free(hw_params);
|
||||
|
||||
if (lib.snd_pcm_hw_params_current(pcm.*, hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (lib.snd_pcm_hw_params_get_period_size(hw_params, period_size, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
}
|
||||
|
||||
{
|
||||
if (lib.snd_mixer_open(mixer, 0) < 0)
|
||||
return error.OutOfMemory;
|
||||
|
||||
const card_id = try ctx.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ','));
|
||||
defer ctx.allocator.free(card_id);
|
||||
|
||||
if (lib.snd_mixer_attach(mixer.*, card_id.ptr) < 0)
|
||||
return error.IncompatibleDevice;
|
||||
|
||||
if (lib.snd_mixer_selem_register(mixer.*, null, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (lib.snd_mixer_load(mixer.*) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (lib.snd_mixer_selem_id_malloc(selem) < 0)
|
||||
return error.OutOfMemory;
|
||||
errdefer lib.snd_mixer_selem_id_free(selem.*);
|
||||
|
||||
lib.snd_mixer_selem_id_set_index(selem.*, 0);
|
||||
lib.snd_mixer_selem_id_set_name(selem.*, "Master");
|
||||
|
||||
mixer_elm.* = lib.snd_mixer_find_selem(mixer.*, selem.*) orelse
|
||||
return error.IncompatibleDevice;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn createPlayer(ctx: Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
var mixer: ?*c.snd_mixer_t = null;
|
||||
var selem: ?*c.snd_mixer_selem_id_t = null;
|
||||
var mixer_elm: ?*c.snd_mixer_elem_t = null;
|
||||
var period_size: c_ulong = 0;
|
||||
try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size);
|
||||
|
||||
const player = try ctx.allocator.create(Player);
|
||||
player.* = .{
|
||||
.allocator = ctx.allocator,
|
||||
.thread = undefined,
|
||||
.aborted = .{ .raw = false },
|
||||
.sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))),
|
||||
.period_size = period_size,
|
||||
.pcm = pcm.?,
|
||||
.mixer = mixer.?,
|
||||
.selem = selem.?,
|
||||
.mixer_elm = mixer_elm.?,
|
||||
.writeFn = writeFn,
|
||||
.user_data = options.user_data,
|
||||
.channels = device.channels,
|
||||
.format = format,
|
||||
.sample_rate = sample_rate,
|
||||
};
|
||||
return .{ .alsa = player };
|
||||
}
|
||||
|
||||
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate orelse default_sample_rate);
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
var mixer: ?*c.snd_mixer_t = null;
|
||||
var selem: ?*c.snd_mixer_selem_id_t = null;
|
||||
var mixer_elm: ?*c.snd_mixer_elem_t = null;
|
||||
var period_size: c_ulong = 0;
|
||||
try ctx.createStream(device, format, sample_rate, &pcm, &mixer, &selem, &mixer_elm, &period_size);
|
||||
|
||||
const recorder = try ctx.allocator.create(Recorder);
|
||||
recorder.* = .{
|
||||
.allocator = ctx.allocator,
|
||||
.thread = undefined,
|
||||
.aborted = .{ .raw = false },
|
||||
.sample_buffer = try ctx.allocator.alloc(u8, period_size * format.frameSize(@intCast(device.channels.len))),
|
||||
.period_size = period_size,
|
||||
.pcm = pcm.?,
|
||||
.mixer = mixer.?,
|
||||
.selem = selem.?,
|
||||
.mixer_elm = mixer_elm.?,
|
||||
.readFn = readFn,
|
||||
.user_data = options.user_data,
|
||||
.channels = device.channels,
|
||||
.format = format,
|
||||
.sample_rate = sample_rate,
|
||||
};
|
||||
return .{ .alsa = recorder };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: std.Thread,
|
||||
aborted: std.atomic.Value(bool),
|
||||
sample_buffer: []u8,
|
||||
period_size: c_ulong,
|
||||
pcm: *c.snd_pcm_t,
|
||||
mixer: *c.snd_mixer_t,
|
||||
selem: *c.snd_mixer_selem_id_t,
|
||||
mixer_elm: *c.snd_mixer_elem_t,
|
||||
writeFn: main.WriteFn,
|
||||
user_data: ?*anyopaque,
|
||||
|
||||
channels: []main.ChannelPosition,
|
||||
format: main.Format,
|
||||
sample_rate: u24,
|
||||
|
||||
pub fn deinit(player: *Player) void {
|
||||
player.aborted.store(true, .Unordered);
|
||||
player.thread.join();
|
||||
|
||||
_ = lib.snd_mixer_close(player.mixer);
|
||||
lib.snd_mixer_selem_id_free(player.selem);
|
||||
_ = lib.snd_pcm_close(player.pcm);
|
||||
_ = lib.snd_pcm_hw_free(player.pcm);
|
||||
|
||||
player.allocator.free(player.sample_buffer);
|
||||
player.allocator.destroy(player);
|
||||
}
|
||||
|
||||
pub fn start(player: *Player) !void {
|
||||
player.thread = std.Thread.spawn(.{}, writeThread, .{player}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeThread(player: *Player) void {
|
||||
var underrun = false;
|
||||
while (!player.aborted.load(.Unordered)) {
|
||||
if (!underrun) {
|
||||
player.writeFn(
|
||||
player.user_data,
|
||||
player.sample_buffer[0 .. player.period_size * player.format.frameSize(@intCast(player.channels.len))],
|
||||
);
|
||||
}
|
||||
underrun = false;
|
||||
const n = lib.snd_pcm_writei(player.pcm, player.sample_buffer.ptr, player.period_size);
|
||||
if (n < 0) {
|
||||
_ = lib.snd_pcm_prepare(player.pcm);
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(player: *Player) !void {
|
||||
if (lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED) {
|
||||
if (lib.snd_pcm_pause(player.pcm, 0) < 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(player: *Player) !void {
|
||||
if (lib.snd_pcm_state(player.pcm) != c.SND_PCM_STATE_PAUSED) {
|
||||
if (lib.snd_pcm_pause(player.pcm, 1) < 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(player: *Player) bool {
|
||||
return lib.snd_pcm_state(player.pcm) == c.SND_PCM_STATE_PAUSED;
|
||||
}
|
||||
|
||||
pub fn setVolume(player: *Player, vol: f32) !void {
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotSetVolume;
|
||||
|
||||
const dist = @as(f32, @floatFromInt(max_vol - min_vol));
|
||||
if (lib.snd_mixer_selem_set_playback_volume_all(
|
||||
player.mixer_elm,
|
||||
@as(c_long, @intFromFloat(dist * vol)) + min_vol,
|
||||
) < 0)
|
||||
return error.CannotSetVolume;
|
||||
}
|
||||
|
||||
pub fn volume(player: *Player) !f32 {
|
||||
var vol: c_long = 0;
|
||||
var channel: c_int = 0;
|
||||
|
||||
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
|
||||
if (lib.snd_mixer_selem_has_playback_channel(player.mixer_elm, channel) == 1) {
|
||||
if (lib.snd_mixer_selem_get_playback_volume(player.mixer_elm, channel, &vol) == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == c.SND_MIXER_SCHN_LAST)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (lib.snd_mixer_selem_get_playback_volume_range(player.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol));
|
||||
}
|
||||
};
|
||||
|
||||
pub const Recorder = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: std.Thread,
|
||||
aborted: std.atomic.Value(bool),
|
||||
sample_buffer: []u8,
|
||||
period_size: c_ulong,
|
||||
pcm: *c.snd_pcm_t,
|
||||
mixer: *c.snd_mixer_t,
|
||||
selem: *c.snd_mixer_selem_id_t,
|
||||
mixer_elm: *c.snd_mixer_elem_t,
|
||||
readFn: main.ReadFn,
|
||||
user_data: ?*anyopaque,
|
||||
|
||||
channels: []main.ChannelPosition,
|
||||
format: main.Format,
|
||||
sample_rate: u24,
|
||||
|
||||
pub fn deinit(recorder: *Recorder) void {
|
||||
recorder.aborted.store(true, .Unordered);
|
||||
recorder.thread.join();
|
||||
|
||||
_ = lib.snd_mixer_close(recorder.mixer);
|
||||
lib.snd_mixer_selem_id_free(recorder.selem);
|
||||
_ = lib.snd_pcm_close(recorder.pcm);
|
||||
_ = lib.snd_pcm_hw_free(recorder.pcm);
|
||||
|
||||
recorder.allocator.free(recorder.sample_buffer);
|
||||
recorder.allocator.destroy(recorder);
|
||||
}
|
||||
|
||||
pub fn start(recorder: *Recorder) !void {
|
||||
recorder.thread = std.Thread.spawn(.{}, readThread, .{recorder}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn readThread(recorder: *Recorder) void {
|
||||
var underrun = false;
|
||||
while (!recorder.aborted.load(.Unordered)) {
|
||||
if (!underrun) {
|
||||
recorder.readFn(recorder.user_data, recorder.sample_buffer[0..recorder.period_size]);
|
||||
}
|
||||
underrun = false;
|
||||
const n = lib.snd_pcm_readi(recorder.pcm, recorder.sample_buffer.ptr, recorder.period_size);
|
||||
if (n < 0) {
|
||||
_ = lib.snd_pcm_prepare(recorder.pcm);
|
||||
underrun = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record(recorder: *Recorder) !void {
|
||||
if (lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED) {
|
||||
if (lib.snd_pcm_pause(recorder.pcm, 0) < 0)
|
||||
return error.CannotRecord;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(recorder: *Recorder) !void {
|
||||
if (lib.snd_pcm_state(recorder.pcm) != c.SND_PCM_STATE_PAUSED) {
|
||||
if (lib.snd_pcm_pause(recorder.pcm, 1) < 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(recorder: *Recorder) bool {
|
||||
return lib.snd_pcm_state(recorder.pcm) == c.SND_PCM_STATE_PAUSED;
|
||||
}
|
||||
|
||||
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotSetVolume;
|
||||
|
||||
const dist = @as(f32, @floatFromInt(max_vol - min_vol));
|
||||
if (lib.snd_mixer_selem_set_capture_volume_all(
|
||||
recorder.mixer_elm,
|
||||
@as(c_long, @intFromFloat(dist * vol)) + min_vol,
|
||||
) < 0)
|
||||
return error.CannotSetVolume;
|
||||
}
|
||||
|
||||
pub fn volume(recorder: *Recorder) !f32 {
|
||||
var vol: c_long = 0;
|
||||
var channel: c_int = 0;
|
||||
|
||||
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
|
||||
if (lib.snd_mixer_selem_has_capture_channel(recorder.mixer_elm, channel) == 1) {
|
||||
if (lib.snd_mixer_selem_get_capture_volume(recorder.mixer_elm, channel, &vol) == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == c.SND_MIXER_SCHN_LAST)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (lib.snd_mixer_selem_get_capture_volume_range(recorder.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol));
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.name);
|
||||
allocator.free(device.formats);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
pub fn modeToStream(mode: main.Device.Mode) c_uint {
|
||||
return switch (mode) {
|
||||
.playback => c.SND_PCM_STREAM_PLAYBACK,
|
||||
.capture => c.SND_PCM_STREAM_CAPTURE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toAlsaFormat(format: main.Format) c.snd_pcm_format_t {
|
||||
return switch (format) {
|
||||
.u8 => c.SND_PCM_FORMAT_U8,
|
||||
.i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE,
|
||||
.i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE,
|
||||
.i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE,
|
||||
.f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromAlsaChannel(pos: c_uint) !main.ChannelPosition {
|
||||
return switch (pos) {
|
||||
c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid,
|
||||
c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center,
|
||||
c.SND_CHMAP_FL => .front_left,
|
||||
c.SND_CHMAP_FR => .front_right,
|
||||
c.SND_CHMAP_LFE => .lfe,
|
||||
c.SND_CHMAP_SL => .side_left,
|
||||
c.SND_CHMAP_SR => .side_right,
|
||||
c.SND_CHMAP_RC => .back_center,
|
||||
c.SND_CHMAP_RLC => .back_left,
|
||||
c.SND_CHMAP_RRC => .back_right,
|
||||
c.SND_CHMAP_FLC => .front_left_center,
|
||||
c.SND_CHMAP_FRC => .front_right_center,
|
||||
c.SND_CHMAP_TC => .top_center,
|
||||
c.SND_CHMAP_TFL => .top_front_left,
|
||||
c.SND_CHMAP_TFR => .top_front_right,
|
||||
c.SND_CHMAP_TFC => .top_front_center,
|
||||
c.SND_CHMAP_TRL => .top_back_left,
|
||||
c.SND_CHMAP_TRR => .top_back_right,
|
||||
c.SND_CHMAP_TRC => .top_back_center,
|
||||
|
||||
else => return error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toCHMAP(pos: main.ChannelPosition) c_uint {
|
||||
return switch (pos) {
|
||||
.front_center => c.SND_CHMAP_FC,
|
||||
.front_left => c.SND_CHMAP_FL,
|
||||
.front_right => c.SND_CHMAP_FR,
|
||||
.lfe => c.SND_CHMAP_LFE,
|
||||
.side_left => c.SND_CHMAP_SL,
|
||||
.side_right => c.SND_CHMAP_SR,
|
||||
.back_center => c.SND_CHMAP_RC,
|
||||
.back_left => c.SND_CHMAP_RLC,
|
||||
.back_right => c.SND_CHMAP_RRC,
|
||||
.front_left_center => c.SND_CHMAP_FLC,
|
||||
.front_right_center => c.SND_CHMAP_FRC,
|
||||
.top_center => c.SND_CHMAP_TC,
|
||||
.top_front_left => c.SND_CHMAP_TFL,
|
||||
.top_front_right => c.SND_CHMAP_TFR,
|
||||
.top_front_center => c.SND_CHMAP_TFC,
|
||||
.top_back_left => c.SND_CHMAP_TRL,
|
||||
.top_back_right => c.SND_CHMAP_TRR,
|
||||
.top_back_center => c.SND_CHMAP_TRC,
|
||||
};
|
||||
}
|
||||
106
src/sysaudio/backends.zig
Normal file
106
src/sysaudio/backends.zig
Normal 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
349
src/sysaudio/conv.zig
Normal 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
769
src/sysaudio/coreaudio.zig
Normal 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
189
src/sysaudio/dummy.zig
Normal 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);
|
||||
}
|
||||
92
src/sysaudio/examples/record.zig
Normal file
92
src/sysaudio/examples/record.zig
Normal 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 {};
|
||||
}
|
||||
77
src/sysaudio/examples/sine.zig
Normal file
77
src/sysaudio/examples/sine.zig
Normal 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
404
src/sysaudio/jack.zig
Normal 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
496
src/sysaudio/main.zig
Normal 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
527
src/sysaudio/pipewire.zig
Normal 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,
|
||||
¶ms,
|
||||
params.len,
|
||||
) < 0) return error.OpeningDevice;
|
||||
|
||||
player.* = .{
|
||||
.allocator = ctx.allocator,
|
||||
.thread = thread,
|
||||
.stream = stream,
|
||||
.is_paused = .{ .raw = false },
|
||||
.vol = 1.0,
|
||||
.writeFn = writeFn,
|
||||
.user_data = options.user_data,
|
||||
.channels = device.channels,
|
||||
.format = .f32,
|
||||
.sample_rate = options.sample_rate orelse default_sample_rate,
|
||||
};
|
||||
return .{ .pipewire = player };
|
||||
}
|
||||
|
||||
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
|
||||
const media_role = switch (options.media_role) {
|
||||
.default => "Screen",
|
||||
.game => "Game",
|
||||
.music => "Music",
|
||||
.movie => "Movie",
|
||||
.communication => "Communication",
|
||||
};
|
||||
|
||||
var buf: [8]u8 = undefined;
|
||||
const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate orelse default_sample_rate}) catch unreachable;
|
||||
|
||||
const props = lib.pw_properties_new(
|
||||
c.PW_KEY_MEDIA_TYPE,
|
||||
"Audio",
|
||||
|
||||
c.PW_KEY_MEDIA_CATEGORY,
|
||||
"Capture",
|
||||
|
||||
c.PW_KEY_MEDIA_ROLE,
|
||||
media_role.ptr,
|
||||
|
||||
c.PW_KEY_MEDIA_NAME,
|
||||
ctx.app_name.ptr,
|
||||
|
||||
c.PW_KEY_AUDIO_RATE,
|
||||
audio_rate.ptr,
|
||||
|
||||
@as(*allowzero u0, @ptrFromInt(0)),
|
||||
);
|
||||
|
||||
const stream_events = c.pw_stream_events{
|
||||
.version = c.PW_VERSION_STREAM_EVENTS,
|
||||
.process = Recorder.processCb,
|
||||
.destroy = null,
|
||||
.state_changed = stateChangedCb,
|
||||
.control_info = null,
|
||||
.io_changed = null,
|
||||
.param_changed = null,
|
||||
.add_buffer = null,
|
||||
.remove_buffer = null,
|
||||
.drained = null,
|
||||
.command = null,
|
||||
.trigger_done = null,
|
||||
};
|
||||
|
||||
const recorder = try ctx.allocator.create(Recorder);
|
||||
errdefer ctx.allocator.destroy(recorder);
|
||||
|
||||
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
|
||||
const stream = lib.pw_stream_new_simple(
|
||||
lib.pw_thread_loop_get_loop(thread),
|
||||
"audio-capture",
|
||||
props,
|
||||
&stream_events,
|
||||
recorder,
|
||||
) orelse return error.OpeningDevice;
|
||||
|
||||
var builder_buf: [256]u8 = undefined;
|
||||
var pod_builder = c.spa_pod_builder{
|
||||
.data = &builder_buf,
|
||||
.size = builder_buf.len,
|
||||
._padding = 0,
|
||||
.state = .{
|
||||
.offset = 0,
|
||||
.flags = 0,
|
||||
.frame = null,
|
||||
},
|
||||
.callbacks = .{ .funcs = null, .data = null },
|
||||
};
|
||||
var info = c.spa_audio_info_raw{
|
||||
.format = c.SPA_AUDIO_FORMAT_F32,
|
||||
.channels = @as(u32, @intCast(device.channels.len)),
|
||||
.rate = options.sample_rate orelse default_sample_rate,
|
||||
.flags = 0,
|
||||
.position = undefined,
|
||||
};
|
||||
var params = [1][*c]c.spa_pod{
|
||||
sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info),
|
||||
};
|
||||
|
||||
if (lib.pw_stream_connect(
|
||||
stream,
|
||||
c.PW_DIRECTION_INPUT,
|
||||
c.PW_ID_ANY,
|
||||
c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS,
|
||||
¶ms,
|
||||
params.len,
|
||||
) < 0) return error.OpeningDevice;
|
||||
|
||||
recorder.* = .{
|
||||
.allocator = ctx.allocator,
|
||||
.thread = thread,
|
||||
.stream = stream,
|
||||
.is_paused = .{ .raw = false },
|
||||
.vol = 1.0,
|
||||
.readFn = readFn,
|
||||
.user_data = options.user_data,
|
||||
.channels = device.channels,
|
||||
.format = .f32,
|
||||
.sample_rate = options.sample_rate orelse default_sample_rate,
|
||||
};
|
||||
return .{ .pipewire = recorder };
|
||||
}
|
||||
};
|
||||
|
||||
fn stateChangedCb(player_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void {
|
||||
_ = old_state;
|
||||
_ = err;
|
||||
|
||||
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
|
||||
|
||||
if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) {
|
||||
lib.pw_thread_loop_signal(player.thread, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: *c.pw_thread_loop,
|
||||
stream: *c.pw_stream,
|
||||
is_paused: std.atomic.Value(bool),
|
||||
vol: f32,
|
||||
writeFn: main.WriteFn,
|
||||
user_data: ?*anyopaque,
|
||||
|
||||
channels: []main.ChannelPosition,
|
||||
format: main.Format,
|
||||
sample_rate: u24,
|
||||
|
||||
pub fn processCb(player_opaque: ?*anyopaque) callconv(.C) void {
|
||||
var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
|
||||
|
||||
const buf = lib.pw_stream_dequeue_buffer(player.stream) orelse return;
|
||||
if (buf.*.buffer.*.datas[0].data == null) return;
|
||||
defer _ = lib.pw_stream_queue_buffer(player.stream, buf);
|
||||
|
||||
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
|
||||
if (player.is_paused.load(.Unordered)) {
|
||||
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
|
||||
buf.*.buffer.*.datas[0].chunk.*.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const stride = player.format.frameSize(@intCast(player.channels.len));
|
||||
const frames = @min(buf.*.requested * stride, buf.*.buffer.*.datas[0].maxsize);
|
||||
buf.*.buffer.*.datas[0].chunk.*.stride = stride;
|
||||
buf.*.buffer.*.datas[0].chunk.*.size = @intCast(frames);
|
||||
|
||||
player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
|
||||
}
|
||||
|
||||
pub fn deinit(player: *Player) void {
|
||||
lib.pw_thread_loop_stop(player.thread);
|
||||
lib.pw_thread_loop_destroy(player.thread);
|
||||
lib.pw_stream_destroy(player.stream);
|
||||
player.allocator.destroy(player);
|
||||
}
|
||||
|
||||
pub fn start(player: *Player) !void {
|
||||
if (lib.pw_thread_loop_start(player.thread) < 0) return error.SystemResources;
|
||||
|
||||
lib.pw_thread_loop_lock(player.thread);
|
||||
lib.pw_thread_loop_wait(player.thread);
|
||||
lib.pw_thread_loop_unlock(player.thread);
|
||||
|
||||
if (lib.pw_stream_get_state(player.stream, null) == c.PW_STREAM_STATE_ERROR) {
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(player: *Player) !void {
|
||||
player.is_paused.store(false, .Unordered);
|
||||
}
|
||||
|
||||
pub fn pause(player: *Player) !void {
|
||||
player.is_paused.store(true, .Unordered);
|
||||
}
|
||||
|
||||
pub fn paused(player: *Player) bool {
|
||||
return player.is_paused.load(.Unordered);
|
||||
}
|
||||
|
||||
pub fn setVolume(player: *Player, vol: f32) !void {
|
||||
player.vol = vol;
|
||||
}
|
||||
|
||||
pub fn volume(player: *Player) !f32 {
|
||||
return player.vol;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Recorder = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: *c.pw_thread_loop,
|
||||
stream: *c.pw_stream,
|
||||
is_paused: std.atomic.Value(bool),
|
||||
vol: f32,
|
||||
readFn: main.ReadFn,
|
||||
user_data: ?*anyopaque,
|
||||
|
||||
channels: []main.ChannelPosition,
|
||||
format: main.Format,
|
||||
sample_rate: u24,
|
||||
|
||||
pub fn processCb(recorder_opaque: ?*anyopaque) callconv(.C) void {
|
||||
var recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?)));
|
||||
|
||||
const buf = lib.pw_stream_dequeue_buffer(recorder.stream) orelse return;
|
||||
if (buf.*.buffer.*.datas[0].data == null) return;
|
||||
defer _ = lib.pw_stream_queue_buffer(recorder.stream, buf);
|
||||
|
||||
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
|
||||
if (recorder.is_paused.load(.Unordered)) {
|
||||
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
|
||||
buf.*.buffer.*.datas[0].chunk.*.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const frames = buf.*.buffer.*.datas[0].chunk.*.size;
|
||||
recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
|
||||
}
|
||||
|
||||
pub fn deinit(recorder: *Recorder) void {
|
||||
lib.pw_thread_loop_stop(recorder.thread);
|
||||
lib.pw_thread_loop_destroy(recorder.thread);
|
||||
lib.pw_stream_destroy(recorder.stream);
|
||||
recorder.allocator.destroy(recorder);
|
||||
}
|
||||
|
||||
pub fn start(recorder: *Recorder) !void {
|
||||
if (lib.pw_thread_loop_start(recorder.thread) < 0) return error.SystemResources;
|
||||
|
||||
lib.pw_thread_loop_lock(recorder.thread);
|
||||
lib.pw_thread_loop_wait(recorder.thread);
|
||||
lib.pw_thread_loop_unlock(recorder.thread);
|
||||
|
||||
if (lib.pw_stream_get_state(recorder.stream, null) == c.PW_STREAM_STATE_ERROR) {
|
||||
return error.CannotRecord;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record(recorder: *Recorder) !void {
|
||||
recorder.is_paused.store(false, .Unordered);
|
||||
}
|
||||
|
||||
pub fn pause(recorder: *Recorder) !void {
|
||||
recorder.is_paused.store(true, .Unordered);
|
||||
}
|
||||
|
||||
pub fn paused(recorder: *Recorder) bool {
|
||||
return recorder.is_paused.load(.Unordered);
|
||||
}
|
||||
|
||||
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
|
||||
recorder.vol = vol;
|
||||
}
|
||||
|
||||
pub fn volume(recorder: *Recorder) !f32 {
|
||||
return recorder.vol;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
extern fn sysaudio_spa_format_audio_raw_build(builder: [*c]c.spa_pod_builder, id: u32, info: [*c]c.spa_audio_info_raw) callconv(.C) [*c]c.spa_pod;
|
||||
11
src/sysaudio/pipewire/sysaudio.c
Normal file
11
src/sysaudio/pipewire/sysaudio.c
Normal 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
811
src/sysaudio/pulseaudio.zig
Normal 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
61
src/sysaudio/util.zig
Normal 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
1053
src/sysaudio/wasapi.zig
Normal file
File diff suppressed because it is too large
Load diff
1821
src/sysaudio/wasapi/win32.zig
Normal file
1821
src/sysaudio/wasapi/win32.zig
Normal file
File diff suppressed because it is too large
Load diff
275
src/sysaudio/webaudio.zig
Normal file
275
src/sysaudio/webaudio.zig
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue