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

Helps hexops/mach#1165

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

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

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