sysaudio: rewrite in zig
removes libsoundio dependency
This commit is contained in:
parent
8aa2c97079
commit
0f3e28bc2a
27 changed files with 4714 additions and 1344 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -15,9 +15,6 @@
|
|||
[submodule "glfw/upstream"]
|
||||
path = libs/glfw/upstream
|
||||
url = https://github.com/hexops/glfw
|
||||
[submodule "sysaudio/upstream"]
|
||||
path = libs/sysaudio/upstream
|
||||
url = https://github.com/hexops/soundio
|
||||
[submodule "basisu/upstream"]
|
||||
path = libs/basisu/upstream
|
||||
url = https://github.com/hexops/basisu
|
||||
|
|
|
|||
3
libs/sysaudio/.gitmodules
vendored
3
libs/sysaudio/.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "upstream"]
|
||||
path = upstream
|
||||
url = https://github.com/hexops/soundio
|
||||
|
|
@ -15,12 +15,12 @@ pub fn build(b: *std.build.Builder) void {
|
|||
test_step.dependOn(&sysaudio.testStep(b, mode, target).step);
|
||||
|
||||
inline for ([_][]const u8{
|
||||
"soundio-sine-wave",
|
||||
"sine-wave",
|
||||
}) |example| {
|
||||
const example_exe = b.addExecutable("example-" ++ example, "examples/" ++ example ++ ".zig");
|
||||
example_exe.setBuildMode(mode);
|
||||
example_exe.setTarget(target);
|
||||
example_exe.addPackage(sysaudio.soundio_pkg);
|
||||
example_exe.addPackage(sysaudio.pkg);
|
||||
sysaudio.link(b, example_exe, .{});
|
||||
example_exe.install();
|
||||
|
||||
|
|
|
|||
60
libs/sysaudio/examples/sine-wave.zig
Normal file
60
libs/sysaudio/examples/sine-wave.zig
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
const std = @import("std");
|
||||
const sysaudio = @import("sysaudio");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var a = try sysaudio.Context.init(null, allocator, .{ .deviceChangeFn = deviceChange });
|
||||
defer a.deinit();
|
||||
try a.refresh();
|
||||
|
||||
const device = a.defaultDevice(.playback) orelse return error.NoDevice;
|
||||
|
||||
var p = try a.createPlayer(device, writeCallback, .{});
|
||||
defer p.deinit();
|
||||
try p.start();
|
||||
|
||||
try p.setVolume(0.85);
|
||||
|
||||
var buf: [16]u8 = undefined;
|
||||
while (true) {
|
||||
std.debug.print("( paused = {}, volume = {d} )\n> ", .{ p.paused(), try p.volume() });
|
||||
const line = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')) orelse break;
|
||||
var iter = std.mem.split(u8, line, ":");
|
||||
const cmd = std.mem.trimRight(u8, iter.first(), &std.ascii.whitespace);
|
||||
if (std.mem.eql(u8, cmd, "vol")) {
|
||||
var vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace));
|
||||
try p.setVolume(vol);
|
||||
} else if (std.mem.eql(u8, cmd, "pause")) {
|
||||
try p.pause();
|
||||
try std.testing.expect(p.paused());
|
||||
} else if (std.mem.eql(u8, cmd, "play")) {
|
||||
try p.play();
|
||||
try std.testing.expect(!p.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(self_opaque: *anyopaque, n_frame: usize) void {
|
||||
var self = @ptrCast(*sysaudio.Player, @alignCast(@alignOf(sysaudio.Player), self_opaque));
|
||||
|
||||
const seconds_per_frame = 1.0 / @intToFloat(f32, self.sampleRate());
|
||||
var frame: usize = 0;
|
||||
while (frame < n_frame) : (frame += 1) {
|
||||
const sample = std.math.sin((seconds_offset + @intToFloat(f32, frame) * seconds_per_frame) * radians_per_second);
|
||||
self.writeAll(frame, sample);
|
||||
}
|
||||
seconds_offset = @mod(seconds_offset + seconds_per_frame * @intToFloat(f32, n_frame), 1.0);
|
||||
}
|
||||
|
||||
fn deviceChange(self: ?*anyopaque) void {
|
||||
_ = self;
|
||||
std.debug.print("Device change detected!\n", .{});
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
const std = @import("std");
|
||||
const soundio = @import("soundio");
|
||||
const c = soundio.c;
|
||||
const SoundIo = soundio.SoundIo;
|
||||
const OutStream = soundio.OutStream;
|
||||
|
||||
var seconds_offset: f32 = 0;
|
||||
|
||||
fn write_callback(
|
||||
maybe_outstream: ?[*]c.SoundIoOutStream,
|
||||
frame_count_min: c_int,
|
||||
frame_count_max: c_int,
|
||||
) callconv(.C) void {
|
||||
_ = frame_count_min;
|
||||
const outstream = OutStream{ .handle = @ptrCast(*c.SoundIoOutStream, maybe_outstream) };
|
||||
const layout = outstream.layout();
|
||||
const float_sample_rate = outstream.sampleRate();
|
||||
const seconds_per_frame = 1.0 / @intToFloat(f32, float_sample_rate);
|
||||
var frames_left = frame_count_max;
|
||||
|
||||
while (frames_left > 0) {
|
||||
var frame_count = frames_left;
|
||||
|
||||
var areas: [*]c.SoundIoChannelArea = undefined;
|
||||
outstream.beginWrite(
|
||||
@ptrCast([*]?[*]c.SoundIoChannelArea, &areas),
|
||||
&frame_count,
|
||||
) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});
|
||||
|
||||
if (frame_count == 0) break;
|
||||
|
||||
const pitch = 440.0;
|
||||
const radians_per_second = pitch * 2.0 * std.math.pi;
|
||||
var frame: c_int = 0;
|
||||
while (frame < frame_count) : (frame += 1) {
|
||||
const sample = std.math.sin((seconds_offset + @intToFloat(f32, frame) *
|
||||
seconds_per_frame) * radians_per_second);
|
||||
{
|
||||
var channel: usize = 0;
|
||||
while (channel < @intCast(usize, layout.channelCount())) : (channel += 1) {
|
||||
const channel_ptr = areas[channel].ptr;
|
||||
const sample_ptr = &channel_ptr[@intCast(usize, areas[channel].step * frame)];
|
||||
@ptrCast(*f32, @alignCast(@alignOf(f32), sample_ptr)).* = sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
seconds_offset += seconds_per_frame * @intToFloat(f32, frame_count);
|
||||
outstream.endWrite() catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)});
|
||||
frames_left -= frame_count;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
const sio = try SoundIo.init();
|
||||
defer sio.deinit();
|
||||
try sio.connect();
|
||||
sio.flushEvents();
|
||||
|
||||
const default_output_index = sio.defaultOutputDeviceIndex() orelse return error.NoOutputDeviceFound;
|
||||
|
||||
const device = sio.getOutputDevice(default_output_index) orelse return error.OutOfMemory;
|
||||
defer device.unref();
|
||||
|
||||
std.debug.print("Output device: {s}\n", .{device.name()});
|
||||
|
||||
const outstream = try device.createOutStream();
|
||||
defer outstream.deinit();
|
||||
|
||||
outstream.setFormat(.float32LE);
|
||||
outstream.setWriteCallback(write_callback);
|
||||
|
||||
try outstream.open();
|
||||
try outstream.start();
|
||||
|
||||
while (true) sio.waitEvents();
|
||||
}
|
||||
|
|
@ -2,17 +2,10 @@ const std = @import("std");
|
|||
|
||||
pub fn Sdk(comptime deps: anytype) type {
|
||||
return struct {
|
||||
const soundio_path = sdkPath("/upstream/soundio");
|
||||
|
||||
pub const pkg = std.build.Pkg{
|
||||
.name = "sysaudio",
|
||||
.source = .{ .path = sdkPath("/src/main.zig") },
|
||||
.dependencies = &.{ deps.sysjs.pkg, soundio_pkg },
|
||||
};
|
||||
|
||||
pub const soundio_pkg = std.build.Pkg{
|
||||
.name = "soundio",
|
||||
.source = .{ .path = sdkPath("/soundio/main.zig") },
|
||||
.dependencies = &.{deps.sysjs.pkg},
|
||||
};
|
||||
|
||||
pub const Options = struct {
|
||||
|
|
@ -23,82 +16,32 @@ pub fn Sdk(comptime deps: anytype) type {
|
|||
};
|
||||
|
||||
pub fn testStep(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget) *std.build.RunStep {
|
||||
const soundio_tests = b.addTestExe("soundio-tests", sdkPath("/soundio/main.zig"));
|
||||
soundio_tests.setBuildMode(mode);
|
||||
soundio_tests.setTarget(target);
|
||||
link(b, soundio_tests, .{});
|
||||
soundio_tests.install();
|
||||
|
||||
const main_tests = b.addTestExe("sysaudio-tests", sdkPath("/src/main.zig"));
|
||||
main_tests.setBuildMode(mode);
|
||||
main_tests.setTarget(target);
|
||||
main_tests.addPackage(soundio_pkg);
|
||||
link(b, main_tests, .{});
|
||||
main_tests.install();
|
||||
|
||||
const main_tests_run = main_tests.run();
|
||||
main_tests_run.step.dependOn(&soundio_tests.run().step);
|
||||
return main_tests_run;
|
||||
return main_tests.run();
|
||||
}
|
||||
|
||||
pub fn link(b: *std.build.Builder, step: *std.build.LibExeObjStep, options: Options) void {
|
||||
if (step.target.toTarget().cpu.arch != .wasm32) {
|
||||
// TODO(build-system): pass system SDK options through
|
||||
const soundio_lib = buildSoundIo(b, step.build_mode, step.target, options);
|
||||
step.linkLibrary(soundio_lib);
|
||||
step.addIncludePath(soundio_path);
|
||||
deps.system_sdk.include(b, step, options.system_sdk);
|
||||
deps.system_sdk.include(b, step, .{});
|
||||
if (step.target.toTarget().isDarwin()) {
|
||||
step.linkFramework("AudioToolbox");
|
||||
step.linkFramework("CoreFoundation");
|
||||
step.linkFramework("CoreAudio");
|
||||
} else if (step.target.toTarget().os.tag == .linux) {
|
||||
step.linkSystemLibrary("asound");
|
||||
step.linkSystemLibrary("pulse");
|
||||
step.linkSystemLibrary("jack");
|
||||
step.linkLibC();
|
||||
}
|
||||
}
|
||||
|
||||
fn buildSoundIo(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget, options: Options) *std.build.LibExeObjStep {
|
||||
// TODO(build-system): https://github.com/hexops/mach/issues/229#issuecomment-1100958939
|
||||
ensureDependencySubmodule(b.allocator, "upstream") catch unreachable;
|
||||
|
||||
const config_base =
|
||||
\\#ifndef SOUNDIO_CONFIG_H
|
||||
\\#define SOUNDIO_CONFIG_H
|
||||
\\#define SOUNDIO_VERSION_MAJOR 2
|
||||
\\#define SOUNDIO_VERSION_MINOR 0
|
||||
\\#define SOUNDIO_VERSION_PATCH 0
|
||||
\\#define SOUNDIO_VERSION_STRING "2.0.0"
|
||||
\\
|
||||
;
|
||||
|
||||
var config_file = std.fs.cwd().createFile(soundio_path ++ "/src/config.h", .{}) catch unreachable;
|
||||
defer config_file.close();
|
||||
config_file.writeAll(config_base) catch unreachable;
|
||||
|
||||
const lib = b.addStaticLibrary("soundio", null);
|
||||
lib.setBuildMode(mode);
|
||||
lib.setTarget(target);
|
||||
lib.linkLibC();
|
||||
lib.addIncludePath(soundio_path);
|
||||
lib.addCSourceFiles(soundio_sources, &.{});
|
||||
deps.system_sdk.include(b, lib, options.system_sdk);
|
||||
|
||||
const target_info = (std.zig.system.NativeTargetInfo.detect(target) catch unreachable).target;
|
||||
if (target_info.isDarwin()) {
|
||||
lib.addCSourceFile(soundio_path ++ "/src/coreaudio.c", &.{});
|
||||
lib.linkFramework("AudioToolbox");
|
||||
lib.linkFramework("CoreFoundation");
|
||||
lib.linkFramework("CoreAudio");
|
||||
config_file.writeAll("#define SOUNDIO_HAVE_COREAUDIO\n") catch unreachable;
|
||||
} else if (target_info.os.tag == .linux) {
|
||||
lib.addCSourceFile(soundio_path ++ "/src/alsa.c", &.{});
|
||||
lib.linkSystemLibrary("asound");
|
||||
config_file.writeAll("#define SOUNDIO_HAVE_ALSA\n") catch unreachable;
|
||||
} else if (target_info.os.tag == .windows) {
|
||||
lib.addCSourceFile(soundio_path ++ "/src/wasapi.c", &.{});
|
||||
lib.linkSystemLibrary("ole32");
|
||||
config_file.writeAll("#define SOUNDIO_HAVE_WASAPI\n") catch unreachable;
|
||||
if (options.install_libs) {
|
||||
step.install();
|
||||
}
|
||||
|
||||
config_file.writeAll("#endif\n") catch unreachable;
|
||||
|
||||
if (options.install_libs)
|
||||
lib.install();
|
||||
return lib;
|
||||
}
|
||||
|
||||
fn ensureDependencySubmodule(allocator: std.mem.Allocator, path: []const u8) !void {
|
||||
|
|
@ -121,14 +64,5 @@ pub fn Sdk(comptime deps: anytype) type {
|
|||
break :blk root_dir ++ suffix;
|
||||
};
|
||||
}
|
||||
|
||||
const soundio_sources = &[_][]const u8{
|
||||
soundio_path ++ "/src/soundio.c",
|
||||
soundio_path ++ "/src/util.c",
|
||||
soundio_path ++ "/src/os.c",
|
||||
soundio_path ++ "/src/dummy.c",
|
||||
soundio_path ++ "/src/channel_layout.c",
|
||||
soundio_path ++ "/src/ring_buffer.c",
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
const c = @import("c.zig");
|
||||
const intToError = @import("error.zig").intToError;
|
||||
const Error = @import("error.zig").Error;
|
||||
|
||||
const ChannelLayout = @This();
|
||||
|
||||
handle: c.SoundIoChannelLayout,
|
||||
|
||||
pub fn channelCount(self: ChannelLayout) i32 {
|
||||
return self.handle.channel_count;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const InStream = @import("InStream.zig");
|
||||
const OutStream = @import("OutStream.zig");
|
||||
const Format = @import("enums.zig").Format;
|
||||
|
||||
const Device = @This();
|
||||
|
||||
handle: *c.SoundIoDevice,
|
||||
|
||||
pub fn unref(self: Device) void {
|
||||
c.soundio_device_unref(self.handle);
|
||||
}
|
||||
|
||||
pub fn id(self: Device) [:0]const u8 {
|
||||
return std.mem.span(self.handle.*.id);
|
||||
}
|
||||
|
||||
pub fn name(self: Device) [:0]const u8 {
|
||||
return std.mem.span(self.handle.*.name);
|
||||
}
|
||||
|
||||
pub fn createInStream(self: Device) error{OutOfMemory}!InStream {
|
||||
return InStream{ .handle = c.soundio_instream_create(self.handle) orelse return error.OutOfMemory };
|
||||
}
|
||||
|
||||
pub fn createOutStream(self: Device) error{OutOfMemory}!OutStream {
|
||||
return OutStream{ .handle = c.soundio_outstream_create(self.handle) orelse return error.OutOfMemory };
|
||||
}
|
||||
|
||||
pub fn supportsFormat(self: Device, format: Format) bool {
|
||||
return c.soundio_device_supports_format(self.handle, @enumToInt(format));
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
const c = @import("c.zig");
|
||||
const intToError = @import("error.zig").intToError;
|
||||
const Error = @import("error.zig").Error;
|
||||
const Format = @import("enums.zig").Format;
|
||||
const ChannelLayout = @import("ChannelLayout.zig");
|
||||
|
||||
const InStream = @This();
|
||||
|
||||
pub const WriteCallback = *const fn (stream: ?[*]c.SoundIoInStream, frame_count_min: c_int, frame_count_max: c_int) callconv(.C) void;
|
||||
|
||||
handle: *c.SoundIoInStream,
|
||||
|
||||
pub fn deinit(self: InStream) void {
|
||||
c.soundio_instream_destroy(self.handle);
|
||||
}
|
||||
|
||||
pub fn open(self: InStream) Error!void {
|
||||
try intToError(c.soundio_instream_open(self.handle));
|
||||
}
|
||||
|
||||
pub fn start(self: InStream) Error!void {
|
||||
try intToError(c.soundio_instream_start(self.handle));
|
||||
}
|
||||
|
||||
pub fn pause(self: InStream, pause_state: bool) Error!void {
|
||||
try intToError(c.soundio_instream_pause(self.handle, pause_state));
|
||||
}
|
||||
|
||||
pub fn beginWrite(self: InStream, areas: [*]?[*]c.SoundIoChannelArea, frame_count: *i32) Error!void {
|
||||
try intToError(c.soundio_instream_begin_write(
|
||||
self.handle,
|
||||
areas,
|
||||
frame_count,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn endWrite(self: InStream) Error!void {
|
||||
try intToError(c.soundio_instream_end_write(self.handle));
|
||||
}
|
||||
|
||||
pub fn setFormat(self: InStream, format: Format) void {
|
||||
self.handle.*.format = @enumToInt(format);
|
||||
}
|
||||
|
||||
pub fn setWriteCallback(self: InStream, callback: WriteCallback) void {
|
||||
self.handle.*.write_callback = callback;
|
||||
}
|
||||
|
||||
pub fn layout(self: InStream) ChannelLayout {
|
||||
return ChannelLayout{ .handle = self.handle.*.layout };
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: InStream) i32 {
|
||||
return self.handle.*.sample_rate;
|
||||
}
|
||||
|
||||
pub fn layoutError(self: InStream) Error!void {
|
||||
try intToError(self.handle.*.layout_error);
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
const c = @import("c.zig");
|
||||
const intToError = @import("error.zig").intToError;
|
||||
const Error = @import("error.zig").Error;
|
||||
const Format = @import("enums.zig").Format;
|
||||
const ChannelLayout = @import("ChannelLayout.zig");
|
||||
|
||||
const OutStream = @This();
|
||||
|
||||
pub const WriteCallback = *const fn (stream: ?[*]c.SoundIoOutStream, frame_count_min: c_int, frame_count_max: c_int) callconv(.C) void;
|
||||
|
||||
handle: *c.SoundIoOutStream,
|
||||
|
||||
pub fn deinit(self: OutStream) void {
|
||||
c.soundio_outstream_destroy(self.handle);
|
||||
}
|
||||
|
||||
pub fn open(self: OutStream) Error!void {
|
||||
try intToError(c.soundio_outstream_open(self.handle));
|
||||
}
|
||||
|
||||
pub fn start(self: OutStream) Error!void {
|
||||
try intToError(c.soundio_outstream_start(self.handle));
|
||||
}
|
||||
|
||||
pub fn pause(self: OutStream, pause_state: bool) Error!void {
|
||||
try intToError(c.soundio_outstream_pause(self.handle, pause_state));
|
||||
}
|
||||
|
||||
pub fn beginWrite(self: OutStream, areas: [*]?[*]c.SoundIoChannelArea, frame_count: *i32) Error!void {
|
||||
try intToError(c.soundio_outstream_begin_write(
|
||||
self.handle,
|
||||
areas,
|
||||
frame_count,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn endWrite(self: OutStream) Error!void {
|
||||
try intToError(c.soundio_outstream_end_write(self.handle));
|
||||
}
|
||||
|
||||
pub fn setFormat(self: OutStream, format: Format) void {
|
||||
self.handle.*.format = @enumToInt(format);
|
||||
}
|
||||
|
||||
pub fn setWriteCallback(self: OutStream, callback: WriteCallback) void {
|
||||
self.handle.*.write_callback = callback;
|
||||
}
|
||||
|
||||
pub fn layout(self: OutStream) ChannelLayout {
|
||||
return ChannelLayout{ .handle = self.handle.*.layout };
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: OutStream) i32 {
|
||||
return self.handle.*.sample_rate;
|
||||
}
|
||||
|
||||
pub fn layoutError(self: OutStream) Error!void {
|
||||
try intToError(self.handle.*.layout_error);
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
const c = @import("c.zig");
|
||||
const intToError = @import("error.zig").intToError;
|
||||
const Error = @import("error.zig").Error;
|
||||
const Aim = @import("enums.zig").Aim;
|
||||
const Backend = @import("enums.zig").Backend;
|
||||
const Device = @import("Device.zig");
|
||||
|
||||
const SoundIo = @This();
|
||||
|
||||
handle: *c.SoundIo,
|
||||
|
||||
pub fn init() error{OutOfMemory}!SoundIo {
|
||||
return SoundIo{ .handle = c.soundio_create() orelse return error.OutOfMemory };
|
||||
}
|
||||
|
||||
pub fn deinit(self: SoundIo) void {
|
||||
c.soundio_destroy(self.handle);
|
||||
}
|
||||
|
||||
pub fn connect(self: SoundIo) Error!void {
|
||||
try intToError(c.soundio_connect(self.handle));
|
||||
}
|
||||
|
||||
pub fn connectBackend(self: SoundIo, backend: Backend) Error!void {
|
||||
try intToError(c.soundio_connect_backend(self.handle, @enumToInt(backend)));
|
||||
}
|
||||
|
||||
pub fn disconnect(self: SoundIo) void {
|
||||
c.soundio_disconnect(self.handle);
|
||||
}
|
||||
|
||||
pub fn flushEvents(self: SoundIo) void {
|
||||
c.soundio_flush_events(self.handle);
|
||||
}
|
||||
|
||||
pub fn waitEvents(self: SoundIo) void {
|
||||
c.soundio_wait_events(self.handle);
|
||||
}
|
||||
|
||||
pub fn wakeup(self: SoundIo) void {
|
||||
c.soundio_wakeup(self.handle);
|
||||
}
|
||||
|
||||
pub fn defaultInputDeviceIndex(self: SoundIo) ?u16 {
|
||||
const index = c.soundio_default_input_device_index(self.handle);
|
||||
return if (index < 0) null else @intCast(u16, index);
|
||||
}
|
||||
|
||||
pub fn defaultOutputDeviceIndex(self: SoundIo) ?u16 {
|
||||
const index = c.soundio_default_output_device_index(self.handle);
|
||||
return if (index < 0) null else @intCast(u16, index);
|
||||
}
|
||||
|
||||
pub fn inputDeviceCount(self: SoundIo) ?u16 {
|
||||
const count = c.soundio_input_device_count(self.handle);
|
||||
return if (count < 0) null else @intCast(u16, count);
|
||||
}
|
||||
|
||||
pub fn outputDeviceCount(self: SoundIo) ?u16 {
|
||||
const count = c.soundio_output_device_count(self.handle);
|
||||
return if (count < 0) null else @intCast(u16, count);
|
||||
}
|
||||
|
||||
pub fn getInputDevice(self: SoundIo, index: u16) ?Device {
|
||||
return Device{
|
||||
.handle = c.soundio_get_input_device(self.handle, index) orelse return null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getOutputDevice(self: SoundIo, index: u16) ?Device {
|
||||
return Device{
|
||||
.handle = c.soundio_get_output_device(self.handle, index) orelse return null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getInputDeviceFromID(self: SoundIo, id: [:0]const u8, is_raw: bool) ?Device {
|
||||
return Device{
|
||||
.handle = c.soundio_get_input_device_from_id(self.handle, id.ptr, is_raw) orelse return null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getOutputDeviceFromID(self: SoundIo, id: [:0]const u8, is_raw: bool) ?Device {
|
||||
return Device{
|
||||
.handle = c.soundio_get_output_device_from_id(self.handle, id.ptr, is_raw) orelse return null,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
pub usingnamespace @cImport({
|
||||
@cInclude("soundio/soundio.h");
|
||||
});
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
const c = @import("c.zig");
|
||||
|
||||
pub const ChannelId = enum(u7) {
|
||||
invalid = c.SoundIoChannelIdInvalid,
|
||||
|
||||
front_left = c.SoundIoChannelIdFrontLeft,
|
||||
front_right = c.SoundIoChannelIdFrontRight,
|
||||
front_center = c.SoundIoChannelIdFrontCenter,
|
||||
lfe = c.SoundIoChannelIdLfe,
|
||||
back_left = c.SoundIoChannelIdBackLeft,
|
||||
back_right = c.SoundIoChannelIdBackRight,
|
||||
front_left_center = c.SoundIoChannelIdFrontLeftCenter,
|
||||
front_right_center = c.SoundIoChannelIdFrontRightCenter,
|
||||
back_center = c.SoundIoChannelIdBackCenter,
|
||||
side_left = c.SoundIoChannelIdSideLeft,
|
||||
side_right = c.SoundIoChannelIdSideRight,
|
||||
top_center = c.SoundIoChannelIdTopCenter,
|
||||
top_front_left = c.SoundIoChannelIdTopFrontLeft,
|
||||
top_front_center = c.SoundIoChannelIdTopFrontCenter,
|
||||
top_front_right = c.SoundIoChannelIdTopFrontRight,
|
||||
top_back_left = c.SoundIoChannelIdTopBackLeft,
|
||||
top_back_center = c.SoundIoChannelIdTopBackCenter,
|
||||
top_back_right = c.SoundIoChannelIdTopBackRight,
|
||||
|
||||
back_left_center = c.SoundIoChannelIdBackLeftCenter,
|
||||
back_right_center = c.SoundIoChannelIdBackRightCenter,
|
||||
front_left_wide = c.SoundIoChannelIdFrontLeftWide,
|
||||
front_right_wide = c.SoundIoChannelIdFrontRightWide,
|
||||
front_left_high = c.SoundIoChannelIdFrontLeftHigh,
|
||||
front_center_high = c.SoundIoChannelIdFrontCenterHigh,
|
||||
front_right_high = c.SoundIoChannelIdFrontRightHigh,
|
||||
top_front_left_center = c.SoundIoChannelIdTopFrontLeftCenter,
|
||||
top_front_right_center = c.SoundIoChannelIdTopFrontRightCenter,
|
||||
top_side_left = c.SoundIoChannelIdTopSideLeft,
|
||||
top_side_right = c.SoundIoChannelIdTopSideRight,
|
||||
left_lfe = c.SoundIoChannelIdLeftLfe,
|
||||
right_lfe = c.SoundIoChannelIdRightLfe,
|
||||
lfe2 = c.SoundIoChannelIdLfe2,
|
||||
bottom_center = c.SoundIoChannelIdBottomCenter,
|
||||
bottom_left_center = c.SoundIoChannelIdBottomLeftCenter,
|
||||
bottom_right_center = c.SoundIoChannelIdBottomRightCenter,
|
||||
|
||||
// Mid/side recording
|
||||
ms_mid = c.SoundIoChannelIdMsMid,
|
||||
ms_side = c.SoundIoChannelIdMsSide,
|
||||
|
||||
// first order ambisonic channels
|
||||
ambisonic_w = c.SoundIoChannelIdAmbisonicW,
|
||||
ambisonic_x = c.SoundIoChannelIdAmbisonicX,
|
||||
ambisonic_y = c.SoundIoChannelIdAmbisonicY,
|
||||
ambisonic_z = c.SoundIoChannelIdAmbisonicZ,
|
||||
|
||||
// X-Y Recording
|
||||
x_y_x = c.SoundIoChannelIdXyX,
|
||||
x_y_y = c.SoundIoChannelIdXyY,
|
||||
|
||||
headphones_left = c.SoundIoChannelIdHeadphonesLeft,
|
||||
headphones_right = c.SoundIoChannelIdHeadphonesRight,
|
||||
click_track = c.SoundIoChannelIdClickTrack,
|
||||
foreign_language = c.SoundIoChannelIdForeignLanguage,
|
||||
hearing_impaired = c.SoundIoChannelIdHearingImpaired,
|
||||
narration = c.SoundIoChannelIdNarration,
|
||||
haptic = c.SoundIoChannelIdHaptic,
|
||||
dialog_centric_mix = c.SoundIoChannelIdDialogCentricMix,
|
||||
|
||||
aux = c.SoundIoChannelIdAux,
|
||||
aux0 = c.SoundIoChannelIdAux0,
|
||||
aux1 = c.SoundIoChannelIdAux1,
|
||||
aux2 = c.SoundIoChannelIdAux2,
|
||||
aux3 = c.SoundIoChannelIdAux3,
|
||||
aux4 = c.SoundIoChannelIdAux4,
|
||||
aux5 = c.SoundIoChannelIdAux5,
|
||||
aux6 = c.SoundIoChannelIdAux6,
|
||||
aux7 = c.SoundIoChannelIdAux7,
|
||||
aux8 = c.SoundIoChannelIdAux8,
|
||||
aux9 = c.SoundIoChannelIdAux9,
|
||||
aux10 = c.SoundIoChannelIdAux10,
|
||||
aux11 = c.SoundIoChannelIdAux11,
|
||||
aux12 = c.SoundIoChannelIdAux12,
|
||||
aux13 = c.SoundIoChannelIdAux13,
|
||||
aux14 = c.SoundIoChannelIdAux14,
|
||||
aux15 = c.SoundIoChannelIdAux15,
|
||||
};
|
||||
|
||||
pub const ChannelLayoutId = enum(u5) {
|
||||
mono = c.SoundIoChannelLayoutIdMono,
|
||||
stereo = c.SoundIoChannelLayoutIdStereo,
|
||||
_2point1 = c.SoundIoChannelLayoutId2Point1,
|
||||
_3point0 = c.SoundIoChannelLayoutId3Point0,
|
||||
_3point0_back = c.SoundIoChannelLayoutId3Point0Back,
|
||||
_3point1 = c.SoundIoChannelLayoutId3Point1,
|
||||
_4point0 = c.SoundIoChannelLayoutId4Point0,
|
||||
quad = c.SoundIoChannelLayoutIdQuad,
|
||||
quadSide = c.SoundIoChannelLayoutIdQuadSide,
|
||||
_4point1 = c.SoundIoChannelLayoutId4Point1,
|
||||
_5point0_back = c.SoundIoChannelLayoutId5Point0Back,
|
||||
_5point0_side = c.SoundIoChannelLayoutId5Point0Side,
|
||||
_5point1 = c.SoundIoChannelLayoutId5Point1,
|
||||
_5point1_back = c.SoundIoChannelLayoutId5Point1Back,
|
||||
_6point0_side = c.SoundIoChannelLayoutId6Point0Side,
|
||||
_6point0_front = c.SoundIoChannelLayoutId6Point0Front,
|
||||
hexagonal = c.SoundIoChannelLayoutIdHexagonal,
|
||||
_6point1 = c.SoundIoChannelLayoutId6Point1,
|
||||
_6point1_back = c.SoundIoChannelLayoutId6Point1Back,
|
||||
_6point1_front = c.SoundIoChannelLayoutId6Point1Front,
|
||||
_7point0 = c.SoundIoChannelLayoutId7Point0,
|
||||
_7point0_front = c.SoundIoChannelLayoutId7Point0Front,
|
||||
_7point1 = c.SoundIoChannelLayoutId7Point1,
|
||||
_7point1_wide = c.SoundIoChannelLayoutId7Point1Wide,
|
||||
_7point1_wide_back = c.SoundIoChannelLayoutId7Point1WideBack,
|
||||
octagonal = c.SoundIoChannelLayoutIdOctagonal,
|
||||
};
|
||||
|
||||
pub const Backend = enum(u3) {
|
||||
none = c.SoundIoBackendNone,
|
||||
jack = c.SoundIoBackendJack,
|
||||
pulseaudio = c.SoundIoBackendPulseAudio,
|
||||
alsa = c.SoundIoBackendAlsa,
|
||||
coreaudio = c.SoundIoBackendCoreAudio,
|
||||
wasapi = c.SoundIoBackendWasapi,
|
||||
dummy = c.SoundIoBackendDummy,
|
||||
};
|
||||
|
||||
pub const Aim = enum(u1) {
|
||||
input = c.SoundIoDeviceAimInput,
|
||||
output = c.SoundIoDeviceAimOutput,
|
||||
};
|
||||
|
||||
pub const Format = enum(u5) {
|
||||
invalid = c.SoundIoFormatInvalid,
|
||||
S8 = c.SoundIoFormatS8,
|
||||
U8 = c.SoundIoFormatU8,
|
||||
S16LE = c.SoundIoFormatS16LE,
|
||||
S16BE = c.SoundIoFormatS16BE,
|
||||
U16LE = c.SoundIoFormatU16LE,
|
||||
U16BE = c.SoundIoFormatU16BE,
|
||||
S24LE = c.SoundIoFormatS24LE,
|
||||
S24BE = c.SoundIoFormatS24BE,
|
||||
U24LE = c.SoundIoFormatU24LE,
|
||||
U24BE = c.SoundIoFormatU24BE,
|
||||
S32LE = c.SoundIoFormatS32LE,
|
||||
S32BE = c.SoundIoFormatS32BE,
|
||||
U32LE = c.SoundIoFormatU32LE,
|
||||
U32BE = c.SoundIoFormatU32BE,
|
||||
float32LE = c.SoundIoFormatFloat32LE,
|
||||
float32BE = c.SoundIoFormatFloat32BE,
|
||||
float64LE = c.SoundIoFormatFloat64LE,
|
||||
float64BE = c.SoundIoFormatFloat64BE,
|
||||
};
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
|
||||
pub const Error = error{
|
||||
OutOfMemory,
|
||||
/// The backend does not appear to be active or running.
|
||||
InitAudioBackend,
|
||||
/// A system resource other than memory was not available.
|
||||
SystemResources,
|
||||
/// Attempted to open a device and failed.
|
||||
OpeningDevice,
|
||||
NoSuchDevice,
|
||||
/// The programmer did not comply with the API.
|
||||
Invalid,
|
||||
/// libsoundio was compiled without support for that backend.
|
||||
BackendUnavailable,
|
||||
/// An open stream had an error that can only be recovered from by
|
||||
/// destroying the stream and creating it again.
|
||||
Streaming,
|
||||
/// Attempted to use a device with parameters it cannot support.
|
||||
IncompatibleDevice,
|
||||
/// When JACK returns `JackNoSuchClient`
|
||||
NoSuchClient,
|
||||
/// Attempted to use parameters that the backend cannot support.
|
||||
IncompatibleBackend,
|
||||
/// Backend server shutdown or became inactive.
|
||||
BackendDisconnected,
|
||||
Interrupted,
|
||||
/// Buffer underrun occurred.
|
||||
Underflow,
|
||||
/// Unable to convert to or from UTF-8 to the native string format.
|
||||
EncodingString,
|
||||
};
|
||||
|
||||
pub fn intToError(err: c_int) Error!void {
|
||||
return switch (err) {
|
||||
c.SoundIoErrorNone => {},
|
||||
c.SoundIoErrorNoMem => Error.OutOfMemory,
|
||||
c.SoundIoErrorInitAudioBackend => Error.InitAudioBackend,
|
||||
c.SoundIoErrorSystemResources => Error.SystemResources,
|
||||
c.SoundIoErrorOpeningDevice => Error.OpeningDevice,
|
||||
c.SoundIoErrorNoSuchDevice => Error.NoSuchDevice,
|
||||
c.SoundIoErrorInvalid => Error.Invalid,
|
||||
c.SoundIoErrorBackendUnavailable => Error.BackendUnavailable,
|
||||
c.SoundIoErrorStreaming => Error.Streaming,
|
||||
c.SoundIoErrorIncompatibleDevice => Error.IncompatibleDevice,
|
||||
c.SoundIoErrorNoSuchClient => Error.NoSuchClient,
|
||||
c.SoundIoErrorIncompatibleBackend => Error.IncompatibleBackend,
|
||||
c.SoundIoErrorBackendDisconnected => Error.BackendDisconnected,
|
||||
c.SoundIoErrorInterrupted => Error.Interrupted,
|
||||
c.SoundIoErrorUnderflow => Error.Underflow,
|
||||
c.SoundIoErrorEncodingString => Error.EncodingString,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
test "error convertion" {
|
||||
const expectError = @import("std").testing.expectError;
|
||||
|
||||
try intToError(c.SoundIoErrorNone);
|
||||
try expectError(Error.OutOfMemory, intToError(c.SoundIoErrorNoMem));
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
pub usingnamespace @import("enums.zig");
|
||||
pub const c = @import("c.zig");
|
||||
pub const SoundIo = @import("SoundIo.zig");
|
||||
pub const Device = @import("Device.zig");
|
||||
pub const InStream = @import("InStream.zig");
|
||||
pub const OutStream = @import("OutStream.zig");
|
||||
pub const Error = @import("error.zig").Error;
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@import("SoundIo.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("Device.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("OutStream.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("ChannelLayout.zig"));
|
||||
}
|
||||
649
libs/sysaudio/src/alsa.zig
Normal file
649
libs/sysaudio/src/alsa.zig
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("alsa/asoundlib.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
const inotify_event = std.os.linux.inotify_event;
|
||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
thread: std.Thread,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
notify_fd: std.os.fd_t,
|
||||
notify_wd: std.os.fd_t,
|
||||
notify_pipe_fd: [2]std.os.fd_t,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
_ = c.snd_lib_error_set_handler(@ptrCast(c.snd_lib_error_handler_t, &util.doNothing));
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.watcher = blk: {
|
||||
if (options.deviceChangeFn) |deviceChangeFn| {
|
||||
const notify_fd = std.os.inotify_init1(std.os.linux.IN.NONBLOCK) catch |err| switch (err) {
|
||||
error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded,
|
||||
error.SystemResources,
|
||||
=> return error.SystemResources,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
errdefer std.os.close(notify_fd);
|
||||
|
||||
const notify_wd = std.os.inotify_add_watch(
|
||||
notify_fd,
|
||||
"/dev/snd",
|
||||
std.os.linux.IN.CREATE | std.os.linux.IN.DELETE,
|
||||
) catch |err| switch (err) {
|
||||
error.AccessDenied => return error.AccessDenied,
|
||||
error.UserResourceLimitReached,
|
||||
error.NotDir,
|
||||
error.FileNotFound,
|
||||
error.SystemResources,
|
||||
=> return error.SystemResources,
|
||||
error.NameTooLong,
|
||||
error.WatchAlreadyExists,
|
||||
error.Unexpected,
|
||||
=> unreachable,
|
||||
};
|
||||
errdefer std.os.inotify_rm_watch(notify_fd, notify_wd);
|
||||
|
||||
const notify_pipe_fd = std.os.pipe2(std.os.O.NONBLOCK) catch |err| switch (err) {
|
||||
error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
errdefer {
|
||||
std.os.close(notify_pipe_fd[0]);
|
||||
std.os.close(notify_pipe_fd[1]);
|
||||
}
|
||||
|
||||
break :blk .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
.aborted = .{ .value = false },
|
||||
.notify_fd = notify_fd,
|
||||
.notify_wd = notify_wd,
|
||||
.notify_pipe_fd = notify_pipe_fd,
|
||||
.thread = std.Thread.spawn(.{}, deviceEventsLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
},
|
||||
};
|
||||
|
||||
return .{ .alsa = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (self.watcher) |*watcher| {
|
||||
watcher.aborted.store(true, .Unordered);
|
||||
_ = std.os.write(watcher.notify_pipe_fd[1], "a") catch {};
|
||||
watcher.thread.join();
|
||||
|
||||
std.os.close(watcher.notify_pipe_fd[0]);
|
||||
std.os.close(watcher.notify_pipe_fd[1]);
|
||||
std.os.inotify_rm_watch(watcher.notify_fd, watcher.notify_wd);
|
||||
std.os.close(watcher.notify_fd);
|
||||
}
|
||||
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
fn deviceEventsLoop(self: *Context) void {
|
||||
var watcher = self.watcher.?;
|
||||
var scan = false;
|
||||
var last_crash: ?i64 = null;
|
||||
var buf: [2048]u8 = undefined;
|
||||
var fds = [2]std.os.pollfd{
|
||||
.{
|
||||
.fd = watcher.notify_fd,
|
||||
.events = std.os.POLL.IN,
|
||||
.revents = 0,
|
||||
},
|
||||
.{
|
||||
.fd = watcher.notify_pipe_fd[0],
|
||||
.events = std.os.POLL.IN,
|
||||
.revents = 0,
|
||||
},
|
||||
};
|
||||
|
||||
while (!watcher.aborted.load(.Unordered)) {
|
||||
_ = std.os.poll(&fds, -1) catch |err| switch (err) {
|
||||
error.NetworkSubsystemFailed,
|
||||
error.SystemResources,
|
||||
=> {
|
||||
const ts = std.time.milliTimestamp();
|
||||
if (last_crash) |lc| {
|
||||
if (ts - lc < 500) return;
|
||||
}
|
||||
last_crash = ts;
|
||||
continue;
|
||||
},
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
if (watcher.notify_fd & std.os.POLL.IN != 0) {
|
||||
while (true) {
|
||||
const len = std.os.read(watcher.notify_fd, &buf) catch |err| {
|
||||
if (err == error.WouldBlock) break;
|
||||
const ts = std.time.milliTimestamp();
|
||||
if (last_crash) |lc| {
|
||||
if (ts - lc < 500) return;
|
||||
}
|
||||
last_crash = ts;
|
||||
break;
|
||||
};
|
||||
if (len == 0) break;
|
||||
|
||||
var i: usize = 0;
|
||||
var evt: *inotify_event = undefined;
|
||||
while (i < buf.len) : (i += @sizeOf(inotify_event) + evt.len) {
|
||||
evt = @ptrCast(*inotify_event, @alignCast(4, buf[i..]));
|
||||
const evt_name = @ptrCast([*]u8, buf[i..])[@sizeOf(inotify_event) .. @sizeOf(inotify_event) + 8];
|
||||
|
||||
if (evt.mask & std.os.linux.IN.ISDIR != 0 or !std.mem.startsWith(u8, evt_name, "pcm"))
|
||||
continue;
|
||||
|
||||
scan = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scan) {
|
||||
watcher.deviceChangeFn(self.watcher.?.userdata);
|
||||
scan = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
var pcm_info: ?*c.snd_pcm_info_t = null;
|
||||
_ = c.snd_pcm_info_malloc(&pcm_info);
|
||||
defer c.snd_pcm_info_free(pcm_info);
|
||||
|
||||
var card_idx: c_int = -1;
|
||||
if (c.snd_card_next(&card_idx) < 0)
|
||||
return error.SystemResources;
|
||||
|
||||
while (card_idx >= 0) {
|
||||
var card_id_buf: [8]u8 = undefined;
|
||||
const card_id = std.fmt.bufPrintZ(&card_id_buf, "hw:{d}", .{card_idx}) catch break;
|
||||
|
||||
var ctl: ?*c.snd_ctl_t = undefined;
|
||||
_ = switch (-c.snd_ctl_open(&ctl, card_id.ptr, 0)) {
|
||||
0 => {},
|
||||
@enumToInt(std.os.E.NOENT) => break,
|
||||
else => return error.OpeningDevice,
|
||||
};
|
||||
defer _ = c.snd_ctl_close(ctl);
|
||||
|
||||
var dev_idx: c_int = -1;
|
||||
if (c.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0)
|
||||
return error.SystemResources;
|
||||
|
||||
c.snd_pcm_info_set_device(pcm_info, @intCast(c_uint, dev_idx));
|
||||
c.snd_pcm_info_set_subdevice(pcm_info, 0);
|
||||
const name = std.mem.span(c.snd_pcm_info_get_name(pcm_info) orelse continue);
|
||||
|
||||
for (&[_]main.Device.Mode{ .playback, .capture }) |mode| {
|
||||
const snd_stream = modeToStream(mode);
|
||||
c.snd_pcm_info_set_stream(pcm_info, snd_stream);
|
||||
const err = c.snd_ctl_pcm_info(ctl, pcm_info);
|
||||
switch (@intToEnum(std.os.E, -err)) {
|
||||
.SUCCESS => {},
|
||||
.NOENT,
|
||||
.NXIO,
|
||||
.NODEV,
|
||||
=> break,
|
||||
else => return error.SystemResources,
|
||||
}
|
||||
|
||||
var buf: [9]u8 = undefined; // 'hw' + max(card|device) * 2 + ':' + \0
|
||||
const id = std.fmt.bufPrintZ(&buf, "hw:{d},{d}", .{ card_idx, dev_idx }) catch continue;
|
||||
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
if (c.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0)
|
||||
continue;
|
||||
defer _ = c.snd_pcm_close(pcm);
|
||||
|
||||
var params: ?*c.snd_pcm_hw_params_t = null;
|
||||
_ = c.snd_pcm_hw_params_malloc(¶ms);
|
||||
defer c.snd_pcm_hw_params_free(params);
|
||||
if (c.snd_pcm_hw_params_any(pcm, params) < 0)
|
||||
continue;
|
||||
|
||||
if (c.snd_pcm_hw_params_can_pause(params) == 0)
|
||||
continue;
|
||||
|
||||
const device = main.Device{
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
const chmap = c.snd_pcm_query_chmaps(pcm);
|
||||
if (chmap) |_| {
|
||||
defer c.snd_pcm_free_chmaps(chmap);
|
||||
|
||||
if (chmap[0] == null) continue;
|
||||
|
||||
var channels = try self.allocator.alloc(main.Channel, chmap.*.*.map.channels);
|
||||
for (channels) |*ch, i|
|
||||
ch.*.id = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice;
|
||||
break :blk channels;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.formats = blk: {
|
||||
var fmt_mask: ?*c.snd_pcm_format_mask_t = null;
|
||||
_ = c.snd_pcm_format_mask_malloc(&fmt_mask);
|
||||
defer c.snd_pcm_format_mask_free(fmt_mask);
|
||||
c.snd_pcm_format_mask_none(fmt_mask);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE);
|
||||
c.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE);
|
||||
c.snd_pcm_hw_params_get_format_mask(params, fmt_mask);
|
||||
|
||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
||||
inline for (std.meta.tags(main.Format)) |format| {
|
||||
if (c.snd_pcm_format_mask_test(
|
||||
fmt_mask,
|
||||
toAlsaFormat(format) catch unreachable,
|
||||
) != 0) {
|
||||
try fmt_arr.append(format);
|
||||
}
|
||||
}
|
||||
|
||||
break :blk try fmt_arr.toOwnedSlice();
|
||||
},
|
||||
.sample_rate = blk: {
|
||||
var rate_min: c_uint = 0;
|
||||
var rate_max: c_uint = 0;
|
||||
if (c.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0)
|
||||
continue;
|
||||
if (c.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0)
|
||||
continue;
|
||||
break :blk .{
|
||||
.min = @intCast(u24, rate_min),
|
||||
.max = @intCast(u24, rate_max),
|
||||
};
|
||||
},
|
||||
.id = try self.allocator.dupeZ(u8, id),
|
||||
.name = try self.allocator.dupeZ(u8, name),
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
|
||||
if (self.devices_info.default(mode) == null and dev_idx == 0) {
|
||||
self.devices_info.setDefault(mode, self.devices_info.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (c.snd_card_next(&card_idx) < 0)
|
||||
return error.SystemResources;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
var pcm: ?*c.snd_pcm_t = null;
|
||||
var mixer: ?*c.snd_mixer_t = null;
|
||||
var selem: ?*c.snd_mixer_selem_id_t = null;
|
||||
var mixer_elm: ?*c.snd_mixer_elem_t = null;
|
||||
var period_size: c_ulong = 0;
|
||||
|
||||
if (c.snd_pcm_open(&pcm, device.id.ptr, modeToStream(device.mode), 0) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = c.snd_pcm_close(pcm);
|
||||
{
|
||||
var hw_params: ?*c.snd_pcm_hw_params_t = null;
|
||||
|
||||
if ((c.snd_pcm_set_params(
|
||||
pcm,
|
||||
toAlsaFormat(format) catch unreachable,
|
||||
c.SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
@intCast(c_uint, device.channels.len),
|
||||
sample_rate,
|
||||
1,
|
||||
main.default_latency,
|
||||
)) < 0)
|
||||
return error.OpeningDevice;
|
||||
errdefer _ = c.snd_pcm_hw_free(pcm);
|
||||
|
||||
if (c.snd_pcm_hw_params_malloc(&hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
defer c.snd_pcm_hw_params_free(hw_params);
|
||||
|
||||
if (c.snd_pcm_hw_params_current(pcm, hw_params) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_pcm_hw_params_get_period_size(hw_params, &period_size, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
}
|
||||
|
||||
{
|
||||
var chmap: c.snd_pcm_chmap_t = .{ .channels = @intCast(c_uint, device.channels.len) };
|
||||
|
||||
for (device.channels) |ch, i|
|
||||
chmap.pos()[i] = toCHMAP(ch.id);
|
||||
|
||||
if (c.snd_pcm_set_chmap(pcm, &chmap) < 0)
|
||||
return error.IncompatibleDevice;
|
||||
}
|
||||
|
||||
{
|
||||
if (c.snd_mixer_open(&mixer, 0) < 0)
|
||||
return error.OutOfMemory;
|
||||
|
||||
const card_id = try self.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ','));
|
||||
defer self.allocator.free(card_id);
|
||||
|
||||
if (c.snd_mixer_attach(mixer, card_id.ptr) < 0)
|
||||
return error.IncompatibleDevice;
|
||||
|
||||
if (c.snd_mixer_selem_register(mixer, null, null) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_mixer_load(mixer) < 0)
|
||||
return error.OpeningDevice;
|
||||
|
||||
if (c.snd_mixer_selem_id_malloc(&selem) < 0)
|
||||
return error.OutOfMemory;
|
||||
errdefer c.snd_mixer_selem_id_free(selem);
|
||||
|
||||
c.snd_mixer_selem_id_set_index(selem, 0);
|
||||
c.snd_mixer_selem_id_set_name(selem, "Master");
|
||||
|
||||
mixer_elm = c.snd_mixer_find_selem(mixer, selem) orelse
|
||||
return error.IncompatibleDevice;
|
||||
}
|
||||
|
||||
return .{
|
||||
.alsa = .{
|
||||
.allocator = self.allocator,
|
||||
.thread = undefined,
|
||||
.mutex = .{},
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.aborted = .{ .value = false },
|
||||
.sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)),
|
||||
.period_size = period_size,
|
||||
.pcm = pcm.?,
|
||||
.mixer = mixer.?,
|
||||
.selem = selem.?,
|
||||
.mixer_elm = mixer_elm.?,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: std.Thread,
|
||||
mutex: std.Thread.Mutex,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
sample_buffer: []u8,
|
||||
period_size: c_ulong,
|
||||
pcm: *c.snd_pcm_t,
|
||||
mixer: *c.snd_mixer_t,
|
||||
selem: *c.snd_mixer_selem_id_t,
|
||||
mixer_elm: *c.snd_mixer_elem_t,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.aborted.store(true, .Unordered);
|
||||
self.thread.join();
|
||||
|
||||
_ = c.snd_mixer_close(self.mixer);
|
||||
c.snd_mixer_selem_id_free(self.selem);
|
||||
_ = c.snd_pcm_close(self.pcm);
|
||||
_ = c.snd_pcm_hw_free(self.pcm);
|
||||
|
||||
self.allocator.free(self.sample_buffer);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeLoop(self: *Player) void {
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = self.sample_buffer.ptr + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
while (!self.aborted.load(.Unordered)) {
|
||||
var frames_left = self.period_size;
|
||||
while (frames_left > 0) {
|
||||
self.writeFn(parent, frames_left);
|
||||
const n = c.snd_pcm_writei(self.pcm, self.sample_buffer.ptr, frames_left);
|
||||
if (n < 0) {
|
||||
if (c.snd_pcm_recover(self.pcm, @intCast(c_int, n), 1) < 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
frames_left -= @intCast(c_uint, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: Player) !void {
|
||||
if (c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED) {
|
||||
if (c.snd_pcm_pause(self.pcm, 0) < 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: Player) !void {
|
||||
if (c.snd_pcm_state(self.pcm) != c.SND_PCM_STATE_PAUSED) {
|
||||
if (c.snd_pcm_pause(self.pcm, 1) < 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return c.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotSetVolume;
|
||||
|
||||
const dist = @intToFloat(f32, max_vol - min_vol);
|
||||
if (c.snd_mixer_selem_set_playback_volume_all(
|
||||
self.mixer_elm,
|
||||
@floatToInt(c_long, dist * vol) + min_vol,
|
||||
) < 0)
|
||||
return error.CannotSetVolume;
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
var vol: c_long = 0;
|
||||
var channel: c_int = 0;
|
||||
|
||||
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
|
||||
if (c.snd_mixer_selem_has_playback_channel(self.mixer_elm, channel) == 1) {
|
||||
if (c.snd_mixer_selem_get_playback_volume(self.mixer_elm, channel, &vol) == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == c.SND_MIXER_SCHN_LAST)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
var min_vol: c_long = 0;
|
||||
var max_vol: c_long = 0;
|
||||
if (c.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
||||
return error.CannotGetVolume;
|
||||
|
||||
return @intToFloat(f32, vol) / @intToFloat(f32, max_vol - min_vol);
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len);
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.name);
|
||||
allocator.free(device.formats);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
pub fn modeToStream(mode: main.Device.Mode) c_uint {
|
||||
return switch (mode) {
|
||||
.playback => c.SND_PCM_STREAM_PLAYBACK,
|
||||
.capture => c.SND_PCM_STREAM_CAPTURE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toAlsaFormat(format: main.Format) !c.snd_pcm_format_t {
|
||||
return switch (format) {
|
||||
.u8 => c.SND_PCM_FORMAT_U8,
|
||||
.i8 => c.SND_PCM_FORMAT_S8,
|
||||
.i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE,
|
||||
.i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE,
|
||||
.i24_4b => if (is_little) c.SND_PCM_FORMAT_S24_LE else c.SND_PCM_FORMAT_S24_BE,
|
||||
.i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE,
|
||||
.f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE,
|
||||
.f64 => if (is_little) c.SND_PCM_FORMAT_FLOAT64_LE else c.SND_PCM_FORMAT_FLOAT64_BE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromAlsaChannel(pos: c_uint) !main.Channel.Id {
|
||||
return switch (pos) {
|
||||
c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid,
|
||||
c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center,
|
||||
c.SND_CHMAP_FL => .front_left,
|
||||
c.SND_CHMAP_FR => .front_right,
|
||||
c.SND_CHMAP_LFE => .lfe,
|
||||
c.SND_CHMAP_SL => .side_left,
|
||||
c.SND_CHMAP_SR => .side_right,
|
||||
c.SND_CHMAP_RC => .back_center,
|
||||
c.SND_CHMAP_FLC => .front_left_center,
|
||||
c.SND_CHMAP_FRC => .front_right_center,
|
||||
c.SND_CHMAP_TC => .top_center,
|
||||
c.SND_CHMAP_TFL => .top_front_left,
|
||||
c.SND_CHMAP_TFR => .top_front_right,
|
||||
c.SND_CHMAP_TFC => .top_front_center,
|
||||
c.SND_CHMAP_TRL => .top_back_left,
|
||||
c.SND_CHMAP_TRR => .top_back_right,
|
||||
c.SND_CHMAP_TRC => .top_back_center,
|
||||
|
||||
else => return error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toCHMAP(pos: main.Channel.Id) c_uint {
|
||||
return switch (pos) {
|
||||
.front_center => c.SND_CHMAP_FC,
|
||||
.front_left => c.SND_CHMAP_FL,
|
||||
.front_right => c.SND_CHMAP_FR,
|
||||
.lfe => c.SND_CHMAP_LFE,
|
||||
.side_left => c.SND_CHMAP_SL,
|
||||
.side_right => c.SND_CHMAP_SR,
|
||||
.back_center => c.SND_CHMAP_RC,
|
||||
.front_left_center => c.SND_CHMAP_FLC,
|
||||
.front_right_center => c.SND_CHMAP_FRC,
|
||||
.top_center => c.SND_CHMAP_TC,
|
||||
.top_front_left => c.SND_CHMAP_TFL,
|
||||
.top_front_right => c.SND_CHMAP_TFR,
|
||||
.top_front_center => c.SND_CHMAP_TFC,
|
||||
.top_back_left => c.SND_CHMAP_TRL,
|
||||
.top_back_right => c.SND_CHMAP_TRR,
|
||||
.top_back_center => c.SND_CHMAP_TRC,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
48
libs/sysaudio/src/backends.zig
Normal file
48
libs/sysaudio/src/backends.zig
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
|
||||
pub const Backend = std.meta.Tag(BackendContext);
|
||||
pub const BackendContext = switch (builtin.os.tag) {
|
||||
.linux => union(enum) {
|
||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
||||
alsa: *@import("alsa.zig").Context,
|
||||
jack: *@import("jack.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
.windows => union(enum) {
|
||||
wasapi: *@import("wasapi.zig").Context,
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
else => union(enum) {
|
||||
dummy: *@import("dummy.zig").Context,
|
||||
},
|
||||
};
|
||||
pub const BackendPlayer = switch (builtin.os.tag) {
|
||||
.linux => union(enum) {
|
||||
pulseaudio: @import("pulseaudio.zig").Player,
|
||||
alsa: @import("alsa.zig").Player,
|
||||
jack: @import("jack.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
||||
pulseaudio: @import("pulseaudio.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
.windows => union(enum) {
|
||||
wasapi: @import("wasapi.zig").Player,
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
else => union(enum) {
|
||||
dummy: @import("dummy.zig").Player,
|
||||
},
|
||||
};
|
||||
158
libs/sysaudio/src/dummy.zig
Normal file
158
libs/sysaudio/src/dummy.zig
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const min_sample_rate = 8_000; // Hz
|
||||
pub const max_sample_rate = 5_644_800; // Hz
|
||||
|
||||
const dummy_playback = main.Device{
|
||||
.id = "dummy-playback",
|
||||
.name = "Dummy Device",
|
||||
.mode = .playback,
|
||||
.channels = undefined,
|
||||
.formats = std.meta.tags(main.Format),
|
||||
.sample_rate = .{
|
||||
.min = min_sample_rate,
|
||||
.max = max_sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
const dummy_capture = main.Device{
|
||||
.id = "dummy-capture",
|
||||
.name = "Dummy Device",
|
||||
.mode = .capture,
|
||||
.channels = undefined,
|
||||
.formats = std.meta.tags(main.Format),
|
||||
.sample_rate = .{
|
||||
.min = min_sample_rate,
|
||||
.max = max_sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
_ = options;
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, dummy_playback);
|
||||
try self.devices_info.list.append(self.allocator, dummy_capture);
|
||||
self.devices_info.list.items[0].channels = try allocator.alloc(main.Channel, 1);
|
||||
self.devices_info.list.items[0].channels[0] = .{
|
||||
.id = .front_center,
|
||||
};
|
||||
self.devices_info.list.items[1].channels = try allocator.alloc(main.Channel, 1);
|
||||
self.devices_info.list.items[1].channels[0] = .{
|
||||
.id = .front_center,
|
||||
};
|
||||
self.devices_info.setDefault(.playback, 0);
|
||||
self.devices_info.setDefault(.capture, 1);
|
||||
|
||||
return .{ .dummy = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
_ = self;
|
||||
_ = writeFn;
|
||||
return .{
|
||||
.dummy = .{
|
||||
._channels = device.channels,
|
||||
._format = options.format,
|
||||
.sample_rate = options.sample_rate,
|
||||
.is_paused = false,
|
||||
.vol = 1.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
is_paused: bool,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: Player) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn start(self: Player) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
self.is_paused = false;
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
self.is_paused = true;
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return self.is_paused;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
self.vol = vol;
|
||||
}
|
||||
|
||||
pub fn volume(self: Player) !f32 {
|
||||
return self.vol;
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
_ = self;
|
||||
_ = channel;
|
||||
_ = frame;
|
||||
_ = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
274
libs/sysaudio/src/jack.zig
Normal file
274
libs/sysaudio/src/jack.zig
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("jack/jack.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
client: *c.jack_client_t,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
c.jack_set_error_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing));
|
||||
c.jack_set_info_function(@ptrCast(?*const fn ([*c]const u8) callconv(.C) void, &util.doNothing));
|
||||
|
||||
var status: c.jack_status_t = 0;
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.client = c.jack_client_open(options.app_name.ptr, c.JackNoStartServer, &status) orelse {
|
||||
std.debug.assert(status & c.JackInvalidOption == 0);
|
||||
return if (status & c.JackShmFailure != 0)
|
||||
error.SystemResources
|
||||
else
|
||||
error.ConnectionRefused;
|
||||
},
|
||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (options.deviceChangeFn) |_| {
|
||||
if (c.jack_set_sample_rate_callback(self.client, sampleRateCallback, self) != 0 or
|
||||
c.jack_set_port_registration_callback(self.client, portRegistrationCallback, self) != 0 or
|
||||
c.jack_set_port_rename_callback(self.client, portRenameCalllback, self) != 0)
|
||||
return error.ConnectionRefused;
|
||||
}
|
||||
|
||||
return .{ .jack = self };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
for (self.devices_info.list.items) |device|
|
||||
freeDevice(self.allocator, device);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
const sample_rate = @intCast(u24, c.jack_get_sample_rate(self.client));
|
||||
|
||||
const port_names = c.jack_get_ports(self.client, null, null, 0) orelse
|
||||
return error.OutOfMemory;
|
||||
defer c.jack_free(@ptrCast(?*anyopaque, port_names));
|
||||
|
||||
var i: usize = 0;
|
||||
outer: while (port_names[i] != null) : (i += 1) {
|
||||
const port = c.jack_port_by_name(self.client, port_names[i]) orelse break;
|
||||
const port_type = c.jack_port_type(port)[0..@intCast(usize, c.jack_port_type_size())];
|
||||
if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE))
|
||||
continue;
|
||||
|
||||
const flags = c.jack_port_flags(port);
|
||||
const mode: main.Device.Mode = if (flags & c.JackPortIsInput != 0) .capture else .playback;
|
||||
|
||||
const name = std.mem.span(port_names[i]);
|
||||
const id = std.mem.sliceTo(name, ':');
|
||||
|
||||
for (self.devices_info.list.items) |*dev| {
|
||||
if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) {
|
||||
const new_ch = main.Channel{
|
||||
.id = @intToEnum(main.Channel.Id, dev.channels.len),
|
||||
};
|
||||
dev.channels = try self.allocator.realloc(dev.channels, dev.channels.len + 1);
|
||||
dev.channels[dev.channels.len - 1] = new_ch;
|
||||
break :outer;
|
||||
}
|
||||
}
|
||||
|
||||
var device = main.Device{
|
||||
.id = try self.allocator.dupeZ(u8, id),
|
||||
.name = name,
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
var channels = try self.allocator.alloc(main.Channel, 1);
|
||||
channels[0] = .{ .id = @intToEnum(main.Channel.Id, 0) };
|
||||
break :blk channels;
|
||||
},
|
||||
.formats = &.{.f32},
|
||||
.sample_rate = .{
|
||||
.min = sample_rate,
|
||||
.max = sample_rate,
|
||||
},
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
if (std.mem.eql(u8, "system", id)) {
|
||||
self.devices_info.setDefault(device.mode, self.devices_info.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), arg.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
pub fn devices(self: *Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: *Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
_ = options;
|
||||
|
||||
var ports = try self.allocator.alloc(*c.jack_port_t, device.channels.len);
|
||||
var dest_ports = try self.allocator.alloc([:0]const u8, ports.len);
|
||||
var buf: [64]u8 = undefined;
|
||||
for (device.channels) |_, i| {
|
||||
const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable;
|
||||
const dest_name = try std.fmt.allocPrintZ(self.allocator, "{s}:{s}", .{ device.id, port_name });
|
||||
ports[i] = c.jack_port_register(self.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse
|
||||
return error.OpeningDevice;
|
||||
dest_ports[i] = dest_name;
|
||||
}
|
||||
|
||||
return .{
|
||||
.jack = .{
|
||||
.allocator = self.allocator,
|
||||
.mutex = .{},
|
||||
.cond = .{},
|
||||
.device = device,
|
||||
.writeFn = writeFn,
|
||||
.client = self.client,
|
||||
.ports = ports,
|
||||
.dest_ports = dest_ports,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
mutex: std.Thread.Mutex,
|
||||
cond: std.Thread.Condition,
|
||||
device: main.Device,
|
||||
writeFn: main.WriteFn,
|
||||
client: *c.jack_client_t,
|
||||
ports: []const *c.jack_port_t,
|
||||
dest_ports: []const [:0]const u8,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.allocator.free(self.ports);
|
||||
for (self.dest_ports) |d|
|
||||
self.allocator.free(d);
|
||||
self.allocator.free(self.dest_ports);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
if (c.jack_set_process_callback(self.client, processCallback, self) != 0)
|
||||
return error.CannotPlay;
|
||||
|
||||
if (c.jack_activate(self.client) != 0)
|
||||
return error.CannotPlay;
|
||||
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
fn processCallback(n_frames: c.jack_nframes_t, self_opaque: ?*anyopaque) callconv(.C) c_int {
|
||||
const self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?));
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = @ptrCast([*]u8, c.jack_port_get_buffer(self.ports[i], n_frames));
|
||||
}
|
||||
self.writeFn(parent, n_frames);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_connect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPlay;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_disconnect(self.client, c.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
||||
return error.CannotPause;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
for (self.ports) |port, i| {
|
||||
if (c.jack_port_connected_to(port, self.dest_ports[i].ptr) == 1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
_ = self;
|
||||
_ = vol;
|
||||
@panic("incompatible backend");
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
_ = self;
|
||||
@panic("incompatible backend");
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().size();
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self.device.channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
_ = self;
|
||||
return .f32;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return @intCast(u24, c.jack_get_sample_rate(self.client));
|
||||
}
|
||||
};
|
||||
|
||||
pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
@ -1,134 +1,461 @@
|
|||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const testing = std.testing;
|
||||
const builtin = @import("builtin");
|
||||
const Backend = if (builtin.cpu.arch == .wasm32) @import("webaudio.zig") else switch (builtin.os.tag) {
|
||||
.linux,
|
||||
.windows,
|
||||
.macos,
|
||||
.ios,
|
||||
=> @import("soundio.zig"),
|
||||
else => @compileError("unsupported os"),
|
||||
const std = @import("std");
|
||||
const util = @import("util.zig");
|
||||
const backends = @import("backends.zig");
|
||||
|
||||
pub const default_sample_rate = 44_100; // Hz
|
||||
pub const default_latency = 500 * std.time.us_per_ms; // μs
|
||||
|
||||
pub const Backend = backends.Backend;
|
||||
pub const DeviceChangeFn = *const fn (self: ?*anyopaque) void;
|
||||
pub const ConnectError = error{
|
||||
OutOfMemory,
|
||||
AccessDenied,
|
||||
SystemResources,
|
||||
ConnectionRefused,
|
||||
};
|
||||
pub const Error = Backend.Error;
|
||||
pub const Device = Backend.Device;
|
||||
pub const DeviceIterator = Backend.DeviceIterator;
|
||||
|
||||
pub const DataCallback = *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
|
||||
pub const Context = struct {
|
||||
pub const Options = struct {
|
||||
app_name: [:0]const u8 = "Mach Game",
|
||||
deviceChangeFn: ?DeviceChangeFn = null,
|
||||
userdata: ?*anyopaque = null,
|
||||
};
|
||||
|
||||
pub const Mode = enum {
|
||||
input,
|
||||
output,
|
||||
data: backends.BackendContext,
|
||||
|
||||
pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) ConnectError!Context {
|
||||
var data: backends.BackendContext = blk: {
|
||||
if (backend) |b| {
|
||||
break :blk try @typeInfo(
|
||||
std.meta.fieldInfo(backends.BackendContext, b).field_type,
|
||||
).Pointer.child.init(allocator, options);
|
||||
} else {
|
||||
inline for (std.meta.fields(Backend)) |b, i| {
|
||||
if (@typeInfo(
|
||||
std.meta.fieldInfo(backends.BackendContext, @intToEnum(Backend, b.value)).field_type,
|
||||
).Pointer.child.init(allocator, options)) |d| {
|
||||
break :blk d;
|
||||
} else |err| {
|
||||
if (i == std.meta.fields(Backend).len - 1)
|
||||
return err;
|
||||
}
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
return .{ .data = data };
|
||||
}
|
||||
|
||||
pub fn deinit(self: Context) void {
|
||||
switch (self.data) {
|
||||
inline else => |b| b.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const RefreshError = error{
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
OpeningDevice,
|
||||
};
|
||||
|
||||
pub fn refresh(self: Context) RefreshError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.refresh(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const Device {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.devices(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: Device.Mode) ?Device {
|
||||
return switch (self.data) {
|
||||
inline else => |b| b.defaultDevice(mode),
|
||||
};
|
||||
}
|
||||
|
||||
pub const CreateStreamError = error{
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
OpeningDevice,
|
||||
IncompatibleDevice,
|
||||
};
|
||||
|
||||
pub fn createPlayer(self: Context, device: Device, writeFn: WriteFn, options: Player.Options) CreateStreamError!Player {
|
||||
std.debug.assert(device.mode == .playback);
|
||||
|
||||
return .{
|
||||
.userdata = options.userdata,
|
||||
.data = switch (self.data) {
|
||||
inline else => |b| try b.createPlayer(device, writeFn, options),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: `*Player` instead `*anyopaque`
|
||||
// https://github.com/ziglang/zig/issues/12325
|
||||
pub const WriteFn = *const fn (self: *anyopaque, frame_count_max: usize) void;
|
||||
|
||||
pub const Player = struct {
|
||||
pub const Options = struct {
|
||||
format: Format = .f32,
|
||||
sample_rate: u24 = default_sample_rate,
|
||||
userdata: ?*anyopaque = null,
|
||||
};
|
||||
|
||||
userdata: ?*anyopaque,
|
||||
data: backends.BackendPlayer,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.deinit(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const StartError = error{
|
||||
CannotPlay,
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
};
|
||||
|
||||
pub fn start(self: *Player) StartError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.start(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const PlayError = error{
|
||||
CannotPlay,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn play(self: *Player) PlayError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.play(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const PauseError = error{
|
||||
CannotPause,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn pause(self: *Player) PauseError!void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.pause(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.paused(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const SetVolumeError = error{
|
||||
CannotSetVolume,
|
||||
};
|
||||
|
||||
// confidence interval (±) depends on the device
|
||||
pub fn setVolume(self: *Player, vol: f32) SetVolumeError!void {
|
||||
std.debug.assert(vol <= 1.0);
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.setVolume(vol),
|
||||
};
|
||||
}
|
||||
|
||||
pub const GetVolumeError = error{
|
||||
CannotGetVolume,
|
||||
};
|
||||
|
||||
// confidence interval (±) depends on the device
|
||||
pub fn volume(self: *Player) GetVolumeError!f32 {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.volume(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.writeRaw(channel, frame, sample),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeAll(self: *Player, frame: usize, value: anytype) void {
|
||||
for (self.channels()) |ch|
|
||||
self.write(ch, frame, value);
|
||||
}
|
||||
|
||||
pub fn write(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
|
||||
switch (@TypeOf(sample)) {
|
||||
u8 => self.writeU8(channel, frame, sample),
|
||||
i16 => self.writeI16(channel, frame, sample),
|
||||
i24 => self.writeI24(channel, frame, sample),
|
||||
i32 => self.writeI32(channel, frame, sample),
|
||||
f32 => self.writeF32(channel, frame, sample),
|
||||
f64 => self.writeF64(channel, frame, sample),
|
||||
else => @compileError(
|
||||
\\invalid sample type. supported types are:
|
||||
\\u8, i8, i16, i24, i32, f32, f32, f64
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeU8(self: *Player, channel: Channel, frame: usize, sample: u8) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, sample),
|
||||
.i8 => self.writeRaw(channel, frame, unsignedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, unsignedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, unsignedToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, unsignedToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, unsignedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, unsignedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI16(self: *Player, channel: Channel, frame: usize, sample: i16) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, sample),
|
||||
.i24 => self.writeRaw(channel, frame, sample),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI24(self: *Player, channel: Channel, frame: usize, sample: i24) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, sample),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeI32(self: *Player, channel: Channel, frame: usize, sample: i32) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, signedToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, sample),
|
||||
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
|
||||
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeF32(self: *Player, channel: Channel, frame: usize, sample: f32) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, sample),
|
||||
.f64 => self.writeRaw(channel, frame, sample),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeF64(self: *Player, channel: Channel, frame: usize, sample: f64) void {
|
||||
switch (self.format()) {
|
||||
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
|
||||
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
|
||||
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
|
||||
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
|
||||
.i24_4b => @panic("TODO"),
|
||||
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
|
||||
.f32 => self.writeRaw(channel, frame, sample),
|
||||
.f64 => self.writeRaw(channel, frame, sample),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsignedToSigned(comptime T: type, sample: anytype) T {
|
||||
const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1);
|
||||
const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample));
|
||||
return @intCast(T, sample -% half) << trunc;
|
||||
}
|
||||
|
||||
fn unsignedToFloat(comptime T: type, sample: anytype) T {
|
||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
||||
return (@intToFloat(T, sample) - max_int) * 1.0 / max_int;
|
||||
}
|
||||
|
||||
fn signedToSigned(comptime T: type, sample: anytype) T {
|
||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
||||
return @intCast(T, sample >> trunc);
|
||||
}
|
||||
|
||||
fn signedToUnsigned(comptime T: type, sample: anytype) T {
|
||||
const half = 1 << (@bitSizeOf(T) - 1);
|
||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
||||
return @intCast(T, (sample >> trunc) + half);
|
||||
}
|
||||
|
||||
fn signedToFloat(comptime T: type, sample: anytype) T {
|
||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
||||
return @intToFloat(T, sample) * 1.0 / max_int;
|
||||
}
|
||||
|
||||
fn floatToSigned(comptime T: type, sample: f64) T {
|
||||
return @floatToInt(T, sample * std.math.maxInt(T));
|
||||
}
|
||||
|
||||
fn floatToUnsigned(comptime T: type, sample: f64) T {
|
||||
const half = 1 << @bitSizeOf(T) - 1;
|
||||
return @floatToInt(T, sample * (half - 1) + half);
|
||||
}
|
||||
|
||||
// TODO: needs test
|
||||
// fn f32Toi24_4b(sample: f32) i32 {
|
||||
// const scaled = sample * std.math.maxInt(i32);
|
||||
// if (builtin.cpu.arch.endian() == .Little) {
|
||||
// return @floatToInt(i32, scaled);
|
||||
// } else {
|
||||
// var res: [4]u8 = undefined;
|
||||
// std.mem.writeIntSliceBig(i32, &res, @floatToInt(i32, res));
|
||||
// return @bitCast(i32, res);
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn channels(self: Player) []Channel {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.channels(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Player) Format {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.format(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return switch (self.data) {
|
||||
inline else => |*b| b.sampleRate(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn frameSize(self: Player) u8 {
|
||||
return self.format().frameSize(self.channels().len);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Device = struct {
|
||||
id: [:0]const u8,
|
||||
name: [:0]const u8,
|
||||
mode: Mode,
|
||||
channels: []Channel,
|
||||
formats: []const Format,
|
||||
sample_rate: util.Range(u24),
|
||||
|
||||
pub const Mode = enum {
|
||||
playback,
|
||||
capture,
|
||||
};
|
||||
|
||||
pub fn preferredFormat(self: Device, format: ?Format) Format {
|
||||
if (format) |f| {
|
||||
for (self.formats) |fmt| {
|
||||
if (f == fmt) {
|
||||
return fmt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var best: Format = self.formats[0];
|
||||
for (self.formats) |fmt| {
|
||||
if (fmt.size() >= best.size()) {
|
||||
if (fmt == .i24_4b and best == .i24)
|
||||
continue;
|
||||
best = fmt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Channel = struct {
|
||||
ptr: [*]u8 = undefined,
|
||||
id: Id,
|
||||
|
||||
pub const Id = enum {
|
||||
front_center,
|
||||
front_left,
|
||||
front_right,
|
||||
front_left_center,
|
||||
front_right_center,
|
||||
back_center,
|
||||
side_left,
|
||||
side_right,
|
||||
top_center,
|
||||
top_front_center,
|
||||
top_front_left,
|
||||
top_front_right,
|
||||
top_back_center,
|
||||
top_back_left,
|
||||
top_back_right,
|
||||
lfe,
|
||||
};
|
||||
};
|
||||
|
||||
pub const Format = enum {
|
||||
U8,
|
||||
S16,
|
||||
S24,
|
||||
S32,
|
||||
F32,
|
||||
u8,
|
||||
i8,
|
||||
i16,
|
||||
i24,
|
||||
i24_4b,
|
||||
i32,
|
||||
f32,
|
||||
f64,
|
||||
|
||||
pub fn size(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.u8, .i8 => 1,
|
||||
.i16 => 2,
|
||||
.i24 => 3,
|
||||
.i24_4b, .i32, .f32 => 4,
|
||||
.f64 => 8,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn validSize(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.u8, .i8 => 1,
|
||||
.i16 => 2,
|
||||
.i24, .i24_4b => 3,
|
||||
.i32, .f32 => 4,
|
||||
.f64 => 8,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn sizeBits(self: Format) u8 {
|
||||
return self.size() * 8;
|
||||
}
|
||||
|
||||
pub fn validSizeBits(self: Format) u8 {
|
||||
return self.validSize() * 8;
|
||||
}
|
||||
|
||||
pub fn frameSize(self: Format, ch_count: usize) u8 {
|
||||
return self.size() * @intCast(u5, ch_count);
|
||||
}
|
||||
};
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
backend: Backend,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
return Audio{
|
||||
.backend = try Backend.init(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Audio) void {
|
||||
self.backend.deinit();
|
||||
}
|
||||
|
||||
pub fn waitEvents(self: Audio) void {
|
||||
self.backend.waitEvents();
|
||||
}
|
||||
|
||||
pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: Device.Options) Error!*Device {
|
||||
return self.backend.requestDevice(allocator, config);
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return self.backend.inputDeviceIterator();
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return self.backend.outputDeviceIterator();
|
||||
}
|
||||
|
||||
test "list devices" {
|
||||
const a = try init();
|
||||
defer a.deinit();
|
||||
|
||||
var iter = a.inputDeviceIterator();
|
||||
while (try iter.next()) |_| {}
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "connect to device" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// const d = try a.requestDevice(std.testing.allocator, .{ .mode = .output });
|
||||
// defer d.deinit(std.testing.allocator);
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "connect to device from descriptor" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// while (try iter.next()) |desc| {
|
||||
// if (mem.eql(u8, desc.name orelse "", "default")) {
|
||||
// const d = try a.requestDevice(std.testing.allocator, desc);
|
||||
// defer d.deinit(std.testing.allocator);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return error.SkipZigTest;
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "requestDevice behavior: null is_raw" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
|
||||
|
||||
// const bad_conf = Device.Options{
|
||||
// .is_raw = null,
|
||||
// .mode = device_conf.mode,
|
||||
// .id = device_conf.id,
|
||||
// };
|
||||
// try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_conf));
|
||||
}
|
||||
|
||||
// TODO(sysaudio): get this test passing on CI
|
||||
test "requestDevice behavior: invalid id" {
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const a = try init();
|
||||
// defer a.deinit();
|
||||
|
||||
// var iter = a.outputDeviceIterator();
|
||||
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
|
||||
|
||||
// const bad_conf = Device.Options{
|
||||
// .is_raw = device_conf.is_raw,
|
||||
// .mode = device_conf.mode,
|
||||
// .id = "wrong-id",
|
||||
// };
|
||||
// try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf));
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
|
|||
590
libs/sysaudio/src/pulseaudio.zig
Normal file
590
libs/sysaudio/src/pulseaudio.zig
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
const std = @import("std");
|
||||
const c = @cImport(@cInclude("pulse/pulseaudio.h"));
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
app_name: [:0]const u8,
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
ctx: *c.pa_context,
|
||||
ctx_state: c.pa_context_state_t,
|
||||
default_sink: ?[:0]const u8,
|
||||
default_source: ?[:0]const u8,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
const main_loop = c.pa_threaded_mainloop_new() orelse
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_threaded_mainloop_free(main_loop);
|
||||
var main_loop_api = c.pa_threaded_mainloop_get_api(main_loop);
|
||||
|
||||
const ctx = c.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_context_unref(ctx);
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = Context{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.app_name = options.app_name,
|
||||
.main_loop = main_loop,
|
||||
.ctx = ctx,
|
||||
.ctx_state = c.PA_CONTEXT_UNCONNECTED,
|
||||
.default_sink = null,
|
||||
.default_source = null,
|
||||
.watcher = if (options.deviceChangeFn) |dcf| .{
|
||||
.deviceChangeFn = dcf,
|
||||
.userdata = options.userdata,
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (c.pa_context_connect(ctx, null, 0, null) != 0)
|
||||
return error.ConnectionRefused;
|
||||
errdefer c.pa_context_disconnect(ctx);
|
||||
c.pa_context_set_state_callback(ctx, contextStateOp, self);
|
||||
|
||||
if (c.pa_threaded_mainloop_start(main_loop) != 0)
|
||||
return error.SystemResources;
|
||||
errdefer c.pa_threaded_mainloop_stop(main_loop);
|
||||
|
||||
c.pa_threaded_mainloop_lock(main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(main_loop);
|
||||
|
||||
while (true) {
|
||||
switch (self.ctx_state) {
|
||||
// The context hasn't been connected yet.
|
||||
c.PA_CONTEXT_UNCONNECTED,
|
||||
// A connection is being established.
|
||||
c.PA_CONTEXT_CONNECTING,
|
||||
// The client is authorizing itself to the daemon.
|
||||
c.PA_CONTEXT_AUTHORIZING,
|
||||
// The client is passing its application name to the daemon.
|
||||
c.PA_CONTEXT_SETTING_NAME,
|
||||
=> c.pa_threaded_mainloop_wait(main_loop),
|
||||
|
||||
// The connection is established, the context is ready to execute operations.
|
||||
c.PA_CONTEXT_READY => break,
|
||||
|
||||
// The connection was terminated cleanly.
|
||||
c.PA_CONTEXT_TERMINATED,
|
||||
// The connection failed or was disconnected.
|
||||
c.PA_CONTEXT_FAILED,
|
||||
=> return error.ConnectionRefused,
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// subscribe to events
|
||||
if (options.deviceChangeFn != null) {
|
||||
c.pa_context_set_subscribe_callback(ctx, subscribeOp, self);
|
||||
const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE;
|
||||
const subscribe_op = c.pa_context_subscribe(ctx, events, null, self) orelse
|
||||
return error.OutOfMemory;
|
||||
c.pa_operation_unref(subscribe_op);
|
||||
}
|
||||
|
||||
return .{ .pulseaudio = self };
|
||||
}
|
||||
|
||||
fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
self.watcher.?.deviceChangeFn(self.watcher.?.userdata);
|
||||
}
|
||||
|
||||
fn contextStateOp(ctx: ?*c.pa_context, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
|
||||
self.ctx_state = c.pa_context_get_state(ctx);
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
c.pa_context_set_subscribe_callback(self.ctx, null, null);
|
||||
c.pa_context_set_state_callback(self.ctx, null, null);
|
||||
c.pa_context_disconnect(self.ctx);
|
||||
c.pa_context_unref(self.ctx);
|
||||
c.pa_threaded_mainloop_stop(self.main_loop);
|
||||
c.pa_threaded_mainloop_free(self.main_loop);
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.clear(self.allocator);
|
||||
|
||||
const list_sink_op = c.pa_context_get_sink_info_list(self.ctx, sinkInfoOp, self);
|
||||
const list_source_op = c.pa_context_get_source_info_list(self.ctx, sourceInfoOp, self);
|
||||
const server_info_op = c.pa_context_get_server_info(self.ctx, serverInfoOp, self);
|
||||
|
||||
performOperation(self.main_loop, list_sink_op);
|
||||
performOperation(self.main_loop, list_source_op);
|
||||
performOperation(self.main_loop, server_info_op);
|
||||
|
||||
defer {
|
||||
if (self.default_sink) |d|
|
||||
self.allocator.free(d);
|
||||
if (self.default_source) |d|
|
||||
self.allocator.free(d);
|
||||
}
|
||||
for (self.devices_info.list.items) |device, i| {
|
||||
if ((device.mode == .playback and
|
||||
self.default_sink != null and
|
||||
std.mem.eql(u8, device.id, self.default_sink.?)) or
|
||||
//
|
||||
(device.mode == .capture and
|
||||
self.default_source != null and
|
||||
std.mem.eql(u8, device.id, self.default_source.?)))
|
||||
{
|
||||
self.devices_info.setDefault(device.mode, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
|
||||
defer c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
self.default_sink = self.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return;
|
||||
self.default_source = self.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch {
|
||||
self.allocator.free(self.default_sink.?);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.deviceInfoOp(info, .playback) catch return;
|
||||
}
|
||||
|
||||
fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Context, @alignCast(@alignOf(*Context), userdata.?));
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.deviceInfoOp(info, .capture) catch return;
|
||||
}
|
||||
|
||||
fn deviceInfoOp(self: *Context, info: anytype, mode: main.Device.Mode) !void {
|
||||
var id = try self.allocator.dupeZ(u8, std.mem.span(info.*.name));
|
||||
errdefer self.allocator.free(id);
|
||||
var name = try self.allocator.dupeZ(u8, std.mem.span(info.*.description));
|
||||
errdefer self.allocator.free(name);
|
||||
|
||||
var device = main.Device{
|
||||
.mode = mode,
|
||||
.channels = blk: {
|
||||
var channels = try self.allocator.alloc(main.Channel, info.*.channel_map.channels);
|
||||
for (channels) |*ch, i|
|
||||
ch.*.id = fromPAChannelPos(info.*.channel_map.map[i]) catch unreachable;
|
||||
break :blk channels;
|
||||
},
|
||||
.formats = available_formats,
|
||||
.sample_rate = .{
|
||||
.min = @intCast(u24, info.*.sample_spec.rate),
|
||||
.max = @intCast(u24, info.*.sample_spec.rate),
|
||||
},
|
||||
.id = id,
|
||||
.name = name,
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
|
||||
const sample_spec = c.pa_sample_spec{
|
||||
.format = toPAFormat(format) catch unreachable,
|
||||
.rate = sample_rate,
|
||||
.channels = @intCast(u5, device.channels.len),
|
||||
};
|
||||
|
||||
const channel_map = try toPAChannelMap(device.channels);
|
||||
|
||||
var stream = c.pa_stream_new(self.ctx, self.app_name.ptr, &sample_spec, &channel_map);
|
||||
if (stream == null)
|
||||
return error.OutOfMemory;
|
||||
errdefer c.pa_stream_unref(stream);
|
||||
|
||||
var status: StreamStatus = .{ .main_loop = self.main_loop, .status = .unknown };
|
||||
c.pa_stream_set_state_callback(stream, streamStateOp, &status);
|
||||
|
||||
const buf_attr = c.pa_buffer_attr{
|
||||
.maxlength = std.math.maxInt(u32),
|
||||
.tlength = std.math.maxInt(u32),
|
||||
.prebuf = 0,
|
||||
.minreq = std.math.maxInt(u32),
|
||||
.fragsize = std.math.maxInt(u32),
|
||||
};
|
||||
|
||||
const flags =
|
||||
c.PA_STREAM_START_CORKED |
|
||||
c.PA_STREAM_AUTO_TIMING_UPDATE |
|
||||
c.PA_STREAM_INTERPOLATE_TIMING |
|
||||
c.PA_STREAM_ADJUST_LATENCY;
|
||||
|
||||
if (c.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) {
|
||||
return error.OpeningDevice;
|
||||
}
|
||||
errdefer _ = c.pa_stream_disconnect(stream);
|
||||
|
||||
while (true) {
|
||||
switch (status.status) {
|
||||
.unknown => c.pa_threaded_mainloop_wait(self.main_loop),
|
||||
.ready => break,
|
||||
.failure => return error.OpeningDevice,
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.pulseaudio = .{
|
||||
.main_loop = self.main_loop,
|
||||
.ctx = self.ctx,
|
||||
.stream = stream.?,
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.write_ptr = undefined,
|
||||
.vol = 1.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const StreamStatus = struct {
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
status: enum(u8) {
|
||||
unknown,
|
||||
ready,
|
||||
failure,
|
||||
},
|
||||
};
|
||||
|
||||
fn streamStateOp(stream: ?*c.pa_stream, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*StreamStatus, @alignCast(@alignOf(*StreamStatus), userdata.?));
|
||||
|
||||
switch (c.pa_stream_get_state(stream)) {
|
||||
c.PA_STREAM_UNCONNECTED,
|
||||
c.PA_STREAM_CREATING,
|
||||
c.PA_STREAM_TERMINATED,
|
||||
=> {},
|
||||
c.PA_STREAM_READY => {
|
||||
self.status = .ready;
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
},
|
||||
c.PA_STREAM_FAILED => {
|
||||
self.status = .failure;
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
main_loop: *c.pa_threaded_mainloop,
|
||||
ctx: *c.pa_context,
|
||||
stream: *c.pa_stream,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
write_ptr: [*]u8,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
c.pa_stream_set_write_callback(self.stream, null, null);
|
||||
c.pa_stream_set_state_callback(self.stream, null, null);
|
||||
c.pa_stream_set_underflow_callback(self.stream, null, null);
|
||||
c.pa_stream_set_overflow_callback(self.stream, null, null);
|
||||
_ = c.pa_stream_disconnect(self.stream);
|
||||
c.pa_stream_unref(self.stream);
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
const op = c.pa_stream_cork(self.stream, 0, null, null) orelse
|
||||
return error.CannotPlay;
|
||||
c.pa_operation_unref(op);
|
||||
c.pa_stream_set_write_callback(self.stream, playbackStreamWriteOp, self);
|
||||
}
|
||||
|
||||
fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
var frames_left = nbytes;
|
||||
while (frames_left > 0) {
|
||||
var chunk_size = frames_left;
|
||||
if (c.pa_stream_begin_write(
|
||||
self.stream,
|
||||
@ptrCast(
|
||||
[*c]?*anyopaque,
|
||||
@alignCast(@alignOf([*c]?*anyopaque), &self.write_ptr),
|
||||
),
|
||||
&chunk_size,
|
||||
) != 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = self.write_ptr + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
const frames = chunk_size / self.format().frameSize(self.channels().len);
|
||||
self.writeFn(parent, frames);
|
||||
|
||||
if (c.pa_stream_write(self.stream, self.write_ptr, chunk_size, null, 0, c.PA_SEEK_RELATIVE) != 0) {
|
||||
if (std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
}
|
||||
|
||||
frames_left -= chunk_size;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
if (c.pa_stream_is_corked(self.stream) > 0) {
|
||||
const op = c.pa_stream_cork(self.stream, 0, null, null) orelse
|
||||
return error.CannotPlay;
|
||||
c.pa_operation_unref(op);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
if (c.pa_stream_is_corked(self.stream) == 0) {
|
||||
const op = c.pa_stream_cork(self.stream, 1, null, null) orelse
|
||||
return error.CannotPause;
|
||||
c.pa_operation_unref(op);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: *Player) bool {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
return c.pa_stream_is_corked(self.stream) > 0;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
var cvolume: c.pa_cvolume = undefined;
|
||||
_ = c.pa_cvolume_init(&cvolume);
|
||||
_ = c.pa_cvolume_set(&cvolume, @intCast(c_uint, self.channels().len), c.pa_sw_volume_from_linear(vol));
|
||||
|
||||
performOperation(
|
||||
self.main_loop,
|
||||
c.pa_context_set_sink_input_volume(
|
||||
self.ctx,
|
||||
c.pa_stream_get_index(self.stream),
|
||||
&cvolume,
|
||||
successOp,
|
||||
self,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn successOp(_: ?*c.pa_context, success: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
if (success == 1)
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
}
|
||||
|
||||
pub fn volume(self: *Player) !f32 {
|
||||
c.pa_threaded_mainloop_lock(self.main_loop);
|
||||
defer c.pa_threaded_mainloop_unlock(self.main_loop);
|
||||
|
||||
performOperation(
|
||||
self.main_loop,
|
||||
c.pa_context_get_sink_input_info(
|
||||
self.ctx,
|
||||
c.pa_stream_get_index(self.stream),
|
||||
sinkInputInfoOp,
|
||||
self,
|
||||
),
|
||||
);
|
||||
|
||||
return self.vol;
|
||||
}
|
||||
|
||||
fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, userdata: ?*anyopaque) callconv(.C) void {
|
||||
var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), userdata.?));
|
||||
|
||||
if (eol != 0) {
|
||||
c.pa_threaded_mainloop_signal(self.main_loop, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
self.vol = @intToFloat(f32, info.*.volume.values[0]) / @intToFloat(f32, c.PA_VOLUME_NORM);
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: *Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + self.format().frameSize(self.channels().len) * frame;
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
||||
allocator.free(device.id);
|
||||
allocator.free(device.name);
|
||||
allocator.free(device.channels);
|
||||
}
|
||||
|
||||
fn performOperation(main_loop: *c.pa_threaded_mainloop, op: ?*c.pa_operation) void {
|
||||
while (true) {
|
||||
switch (c.pa_operation_get_state(op)) {
|
||||
c.PA_OPERATION_RUNNING => c.pa_threaded_mainloop_wait(main_loop),
|
||||
c.PA_OPERATION_DONE => return c.pa_operation_unref(op),
|
||||
c.PA_OPERATION_CANCELLED => {
|
||||
std.debug.assert(false);
|
||||
c.pa_operation_unref(op);
|
||||
return;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const available_formats = &[_]main.Format{
|
||||
.u8, .i16,
|
||||
.i24, .i24_4b,
|
||||
.i32, .f32,
|
||||
};
|
||||
|
||||
pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.Channel.Id {
|
||||
return switch (pos) {
|
||||
c.PA_CHANNEL_POSITION_MONO => .front_center,
|
||||
c.PA_CHANNEL_POSITION_FRONT_LEFT => .front_left, // PA_CHANNEL_POSITION_LEFT
|
||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT => .front_right, // PA_CHANNEL_POSITION_RIGHT
|
||||
c.PA_CHANNEL_POSITION_FRONT_CENTER => .front_center, // PA_CHANNEL_POSITION_CENTER
|
||||
c.PA_CHANNEL_POSITION_REAR_CENTER => .back_center,
|
||||
c.PA_CHANNEL_POSITION_LFE => .lfe, // PA_CHANNEL_POSITION_SUBWOOFER
|
||||
c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER => .front_left_center,
|
||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
||||
c.PA_CHANNEL_POSITION_SIDE_LEFT => .side_left,
|
||||
c.PA_CHANNEL_POSITION_SIDE_RIGHT => .side_right,
|
||||
|
||||
// TODO: .front_center?
|
||||
c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => error.Invalid,
|
||||
|
||||
c.PA_CHANNEL_POSITION_TOP_CENTER => .top_center,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT => .top_front_left,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT => .top_front_right,
|
||||
c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER => .top_front_center,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_LEFT => .top_back_left,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT => .top_back_right,
|
||||
c.PA_CHANNEL_POSITION_TOP_REAR_CENTER => .top_back_center,
|
||||
|
||||
else => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toPAFormat(format: main.Format) !c.pa_sample_format_t {
|
||||
return switch (format) {
|
||||
.u8 => c.PA_SAMPLE_U8,
|
||||
.i16 => if (is_little) c.PA_SAMPLE_S16LE else c.PA_SAMPLE_S16BE,
|
||||
.i24 => if (is_little) c.PA_SAMPLE_S24LE else c.PA_SAMPLE_S24LE,
|
||||
.i24_4b => if (is_little) c.PA_SAMPLE_S24_32LE else c.PA_SAMPLE_S24_32BE,
|
||||
.i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE,
|
||||
.f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE,
|
||||
|
||||
.f64, .i8 => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toPAChannelMap(channels: []const main.Channel) !c.pa_channel_map {
|
||||
var channel_map: c.pa_channel_map = undefined;
|
||||
channel_map.channels = @intCast(u5, channels.len);
|
||||
for (channels) |ch, i|
|
||||
channel_map.map[i] = try toPAChannelPos(ch.id);
|
||||
return channel_map;
|
||||
}
|
||||
|
||||
fn toPAChannelPos(channel_id: main.Channel.Id) !c.pa_channel_position_t {
|
||||
return switch (channel_id) {
|
||||
.front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT,
|
||||
.front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT,
|
||||
.front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER,
|
||||
.lfe => c.PA_CHANNEL_POSITION_LFE,
|
||||
.front_left_center => c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
|
||||
.front_right_center => c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
|
||||
.back_center => c.PA_CHANNEL_POSITION_REAR_CENTER,
|
||||
.side_left => c.PA_CHANNEL_POSITION_SIDE_LEFT,
|
||||
.side_right => c.PA_CHANNEL_POSITION_SIDE_RIGHT,
|
||||
.top_center => c.PA_CHANNEL_POSITION_TOP_CENTER,
|
||||
.top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT,
|
||||
.top_front_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER,
|
||||
.top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
|
||||
.top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT,
|
||||
.top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER,
|
||||
.top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Mode = @import("main.zig").Mode;
|
||||
const Format = @import("main.zig").Format;
|
||||
const DataCallback = @import("main.zig").DataCallback;
|
||||
const c = @import("soundio").c;
|
||||
const Aim = @import("soundio").Aim;
|
||||
const SoundIo = @import("soundio").SoundIo;
|
||||
const SoundIoFormat = @import("soundio").Format;
|
||||
const SoundIoDevice = @import("soundio").Device;
|
||||
const SoundIoInStream = @import("soundio").InStream;
|
||||
const SoundIoOutStream = @import("soundio").OutStream;
|
||||
|
||||
const SoundIoStream = union(Mode) {
|
||||
input: SoundIoInStream,
|
||||
output: SoundIoOutStream,
|
||||
};
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
const default_buffer_size_per_channel = 1024; // 21.33ms
|
||||
|
||||
pub const Device = struct {
|
||||
properties: Properties,
|
||||
|
||||
// Internal fields.
|
||||
handle: SoundIoStream,
|
||||
data_callback: ?DataCallback = null,
|
||||
user_data: ?*anyopaque = null,
|
||||
planar_buffer: [512000]u8 = undefined,
|
||||
started: bool = false,
|
||||
|
||||
pub const Options = struct {
|
||||
mode: Mode = .output,
|
||||
format: ?Format = null,
|
||||
is_raw: ?bool = null,
|
||||
channels: ?u8 = null,
|
||||
sample_rate: ?u32 = null,
|
||||
id: ?[:0]const u8 = null,
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Properties = struct {
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
is_raw: bool,
|
||||
channels: u8,
|
||||
sample_rate: u32,
|
||||
id: [:0]const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Device, allocator: std.mem.Allocator) void {
|
||||
switch (self.handle) {
|
||||
.input => |d| d.deinit(),
|
||||
.output => |d| d.deinit(),
|
||||
}
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn setCallback(self: *Device, callback: DataCallback, data: *anyopaque) void {
|
||||
self.data_callback = callback;
|
||||
self.user_data = data;
|
||||
switch (self.handle) {
|
||||
.input => |_| @panic("input not supported yet"),
|
||||
.output => |d| {
|
||||
// TODO(sysaudio): support other formats
|
||||
d.setFormat(.float32LE);
|
||||
|
||||
d.setWriteCallback((struct {
|
||||
fn cCallback(
|
||||
c_outstream: ?[*]c.SoundIoOutStream,
|
||||
frame_count_min: c_int,
|
||||
frame_count_max: c_int,
|
||||
) callconv(.C) void {
|
||||
const outstream = SoundIoOutStream{ .handle = @ptrCast(*c.SoundIoOutStream, c_outstream) };
|
||||
const device = @ptrCast(*Device, @alignCast(@alignOf(Device), outstream.handle.userdata));
|
||||
|
||||
// TODO(sysaudio): provide callback with outstream.sampleRate()
|
||||
|
||||
// TODO(sysaudio): according to issue tracker and PR from mason (did we include it?)
|
||||
// there may be issues with frame_count_max being way too large on Windows. May need
|
||||
// to artificially limit it or use Mason's PR.
|
||||
|
||||
// The data callback gives us planar data, e.g. in AAAABBBB format for channels
|
||||
// A and B. WebAudio similarly requires data in planar format. libsoundio however
|
||||
// does not guarantee planar data format, it may be in interleaved format ABABABAB.
|
||||
// Invoke our data callback with a temporary buffer, this involves one copy later
|
||||
// but it's such a small amount of memory it is entirely negligible.
|
||||
const layout = outstream.layout();
|
||||
|
||||
const desired_frame_count = default_buffer_size_per_channel * layout.channelCount();
|
||||
const total_frame_count = if (frame_count_max > desired_frame_count)
|
||||
if (frame_count_min <= desired_frame_count)
|
||||
@intCast(usize, desired_frame_count)
|
||||
else
|
||||
@intCast(usize, frame_count_min)
|
||||
else
|
||||
@intCast(usize, frame_count_max);
|
||||
|
||||
const buffer_size: usize = @sizeOf(f32) * total_frame_count * @intCast(usize, layout.channelCount());
|
||||
const addr = @ptrToInt(&device.planar_buffer);
|
||||
const aligned_addr = std.mem.alignForward(addr, @alignOf(f32));
|
||||
const padding = aligned_addr - addr;
|
||||
const planar_buffer = device.planar_buffer[padding .. padding + buffer_size];
|
||||
device.data_callback.?(device, device.user_data.?, planar_buffer);
|
||||
|
||||
var frames_left = total_frame_count;
|
||||
var frame_offset: usize = 0;
|
||||
while (frames_left > 0) {
|
||||
var frame_count: i32 = @intCast(i32, frames_left);
|
||||
|
||||
var areas: [*]c.SoundIoChannelArea = undefined;
|
||||
// TODO(sysaudio): improve error handling
|
||||
outstream.beginWrite(
|
||||
@ptrCast([*]?[*]c.SoundIoChannelArea, &areas),
|
||||
&frame_count,
|
||||
) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});
|
||||
|
||||
if (frame_count == 0) break;
|
||||
|
||||
var channel: usize = 0;
|
||||
while (channel < @intCast(usize, layout.channelCount())) : (channel += 1) {
|
||||
const channel_ptr = areas[channel].ptr;
|
||||
var frame: c_int = 0;
|
||||
while (frame < frame_count) : (frame += 1) {
|
||||
const sample_start = (channel * total_frame_count * @sizeOf(f32)) + ((frame_offset + @intCast(usize, frame)) * @sizeOf(f32));
|
||||
const src = @ptrCast(*f32, @alignCast(@alignOf(f32), &planar_buffer[sample_start]));
|
||||
const dst = &channel_ptr[@intCast(usize, areas[channel].step * frame)];
|
||||
@ptrCast(*f32, @alignCast(@alignOf(f32), dst)).* = src.*;
|
||||
}
|
||||
}
|
||||
// TODO(sysaudio): improve error handling
|
||||
outstream.endWrite() catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)});
|
||||
frames_left -= @intCast(usize, frame_count);
|
||||
frame_offset += @intCast(usize, frame_count);
|
||||
}
|
||||
}
|
||||
}).cCallback);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(device: *Device) Error!void {
|
||||
if (!device.started) return;
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.pause(true),
|
||||
.output => |d| d.pause(true),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start(device: *Device) Error!void {
|
||||
// TODO(sysaudio): after pause, may need to call d.pause(false) instead of d.start()?
|
||||
if (!device.started) {
|
||||
device.started = true;
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.start(),
|
||||
.output => |d| d.start(),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return (switch (device.handle) {
|
||||
.input => |d| d.pause(false),
|
||||
.output => |d| d.pause(false),
|
||||
}) catch |err| {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => @panic(@errorName(err)),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const DeviceIterator = struct {
|
||||
ctx: Audio,
|
||||
mode: Mode,
|
||||
device_len: u16,
|
||||
index: u16,
|
||||
|
||||
pub fn next(self: *DeviceIterator) IteratorError!?Device.Options {
|
||||
if (self.index < self.device_len) {
|
||||
const device_desc = switch (self.mode) {
|
||||
.input => self.ctx.handle.getInputDevice(self.index) orelse return null,
|
||||
.output => self.ctx.handle.getOutputDevice(self.index) orelse return null,
|
||||
};
|
||||
self.index += 1;
|
||||
return Device.Options{
|
||||
.mode = switch (@intToEnum(Aim, device_desc.handle.aim)) {
|
||||
.input => .input,
|
||||
.output => .output,
|
||||
},
|
||||
.is_raw = device_desc.handle.is_raw,
|
||||
.id = device_desc.id(),
|
||||
.name = device_desc.name(),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(sysaudio): standardize errors across backends
|
||||
pub const IteratorError = error{OutOfMemory};
|
||||
pub const Error = error{
|
||||
OutOfMemory,
|
||||
InvalidDeviceID,
|
||||
InvalidParameter,
|
||||
NoDeviceFound,
|
||||
AlreadyConnected,
|
||||
CannotConnect,
|
||||
UnsupportedOS,
|
||||
UnsupportedBackend,
|
||||
DeviceUnavailable,
|
||||
Invalid,
|
||||
OpeningDevice,
|
||||
BackendDisconnected,
|
||||
SystemResources,
|
||||
NoSuchClient,
|
||||
IncompatibleBackend,
|
||||
IncompatibleDevice,
|
||||
InitAudioBackend,
|
||||
NoSuchDevice,
|
||||
BackendUnavailable,
|
||||
Streaming,
|
||||
Interrupted,
|
||||
Underflow,
|
||||
EncodingString,
|
||||
};
|
||||
|
||||
handle: SoundIo,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
var self = Audio{
|
||||
.handle = try SoundIo.init(),
|
||||
};
|
||||
self.handle.connect() catch |err| {
|
||||
return switch (err) {
|
||||
error.SystemResources, error.NoSuchClient => error.CannotConnect,
|
||||
error.Invalid => error.AlreadyConnected,
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
};
|
||||
self.handle.flushEvents();
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: Audio) void {
|
||||
self.handle.deinit();
|
||||
}
|
||||
|
||||
pub fn waitEvents(self: Audio) void {
|
||||
self.handle.waitEvents();
|
||||
}
|
||||
|
||||
pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device {
|
||||
var sio_device: SoundIoDevice = undefined;
|
||||
|
||||
if (options.id) |id| {
|
||||
if (options.is_raw == null)
|
||||
return error.InvalidParameter;
|
||||
|
||||
sio_device = switch (options.mode) {
|
||||
.input => self.handle.getInputDeviceFromID(id, options.is_raw.?),
|
||||
.output => self.handle.getOutputDeviceFromID(id, options.is_raw.?),
|
||||
} orelse {
|
||||
return if (switch (options.mode) {
|
||||
.input => self.handle.inputDeviceCount().?,
|
||||
.output => self.handle.outputDeviceCount().?,
|
||||
} == 0)
|
||||
error.NoDeviceFound
|
||||
else
|
||||
error.DeviceUnavailable;
|
||||
};
|
||||
} else {
|
||||
const id = switch (options.mode) {
|
||||
.input => self.handle.defaultInputDeviceIndex(),
|
||||
.output => self.handle.defaultOutputDeviceIndex(),
|
||||
} orelse return error.NoDeviceFound;
|
||||
sio_device = switch (options.mode) {
|
||||
.input => self.handle.getInputDevice(id),
|
||||
.output => self.handle.getOutputDevice(id),
|
||||
} orelse return error.DeviceUnavailable;
|
||||
}
|
||||
|
||||
const handle = switch (options.mode) {
|
||||
.input => SoundIoStream{ .input = try sio_device.createInStream() },
|
||||
.output => SoundIoStream{ .output = try sio_device.createOutStream() },
|
||||
};
|
||||
|
||||
switch (handle) {
|
||||
.input => |d| try d.open(),
|
||||
.output => |d| try d.open(),
|
||||
}
|
||||
|
||||
const device = try allocator.create(Device);
|
||||
switch (handle) {
|
||||
.input => |d| d.handle.userdata = device,
|
||||
.output => |d| d.handle.userdata = device,
|
||||
}
|
||||
|
||||
// TODO(sysaudio): handle big endian architectures
|
||||
const format: Format = switch (handle) {
|
||||
.input => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) {
|
||||
.U8 => .U8,
|
||||
.S16LE => .S16,
|
||||
.S24LE => .S24,
|
||||
.S32LE => .S32,
|
||||
.float32LE => .F32,
|
||||
else => return error.InvalidParameter,
|
||||
},
|
||||
.output => |d| switch (@intToEnum(SoundIoFormat, d.handle.format)) {
|
||||
.U8 => .U8,
|
||||
.S16LE => .S16,
|
||||
.S24LE => .S24,
|
||||
.S32LE => .S32,
|
||||
.float32LE => .F32,
|
||||
else => return error.InvalidParameter,
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(sysaudio): Get the device name. Calling span or sliceTo on the name is causing segfaults on NixOS
|
||||
// const name_ptr = switch(handle) {
|
||||
// .input => |d| d.handle.name,
|
||||
// .output => |d| d.handle.name,
|
||||
// };
|
||||
// const name = std.mem.sliceTo(name_ptr, 0);
|
||||
|
||||
var properties = Device.Properties{
|
||||
.is_raw = options.is_raw orelse false,
|
||||
.format = format,
|
||||
.mode = options.mode,
|
||||
.id = std.mem.span(sio_device.handle.id),
|
||||
.name = "",
|
||||
.channels = @intCast(u8, switch (handle) {
|
||||
.input => |d| d.layout().channelCount(),
|
||||
.output => |d| d.layout().channelCount(),
|
||||
}),
|
||||
.sample_rate = @intCast(u32, switch (handle) {
|
||||
.input => |d| d.sampleRate(),
|
||||
.output => |d| d.sampleRate(),
|
||||
}),
|
||||
};
|
||||
|
||||
device.* = .{
|
||||
.properties = properties,
|
||||
.handle = handle,
|
||||
};
|
||||
return device;
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return .{
|
||||
.ctx = self,
|
||||
.mode = .output,
|
||||
.device_len = self.handle.outputDeviceCount() orelse 0,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(self: Audio) DeviceIterator {
|
||||
return .{
|
||||
.ctx = self,
|
||||
.mode = .input,
|
||||
.device_len = self.handle.inputDeviceCount() orelse 0,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
56
libs/sysaudio/src/util.zig
Normal file
56
libs/sysaudio/src/util.zig
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const std = @import("std");
|
||||
const main = @import("main.zig");
|
||||
|
||||
pub const DevicesInfo = struct {
|
||||
list: std.ArrayListUnmanaged(main.Device),
|
||||
default_output: ?usize,
|
||||
default_input: ?usize,
|
||||
|
||||
pub fn init() DevicesInfo {
|
||||
return .{
|
||||
.list = .{},
|
||||
.default_output = null,
|
||||
.default_input = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(self: *DevicesInfo, allocator: std.mem.Allocator) void {
|
||||
self.default_output = null;
|
||||
self.default_input = null;
|
||||
self.list.clearAndFree(allocator);
|
||||
}
|
||||
|
||||
pub fn get(self: DevicesInfo, i: usize) main.Device {
|
||||
return self.list.items[i];
|
||||
}
|
||||
|
||||
pub fn default(self: DevicesInfo, mode: main.Device.Mode) ?main.Device {
|
||||
const index = switch (mode) {
|
||||
.playback => self.default_output,
|
||||
.capture => self.default_input,
|
||||
} orelse return null;
|
||||
return self.get(index);
|
||||
}
|
||||
|
||||
pub fn setDefault(self: *DevicesInfo, mode: main.Device.Mode, i: usize) void {
|
||||
switch (mode) {
|
||||
.playback => self.default_output = i,
|
||||
.capture => self.default_input = i,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn Range(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
min: T,
|
||||
max: T,
|
||||
|
||||
pub fn clamp(self: Self, val: T) T {
|
||||
return std.math.clamp(val, self.min, self.max);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn doNothing() callconv(.C) void {}
|
||||
779
libs/sysaudio/src/wasapi.zig
Normal file
779
libs/sysaudio/src/wasapi.zig
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
const std = @import("std");
|
||||
const win32 = @import("wasapi/win32.zig");
|
||||
const main = @import("main.zig");
|
||||
const backends = @import("backends.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const Context = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
devices_info: util.DevicesInfo,
|
||||
enumerator: ?*win32.IMMDeviceEnumerator,
|
||||
watcher: ?Watcher,
|
||||
|
||||
const Watcher = struct {
|
||||
deviceChangeFn: main.DeviceChangeFn,
|
||||
userdata: ?*anyopaque,
|
||||
notif_client: win32.IMMNotificationClient,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
||||
const flags = win32.COINIT_APARTMENTTHREADED | win32.COINIT_DISABLE_OLE1DDE;
|
||||
var hr = win32.CoInitializeEx(null, flags);
|
||||
switch (hr) {
|
||||
win32.S_OK,
|
||||
win32.S_FALSE,
|
||||
win32.RPC_E_CHANGED_MODE,
|
||||
=> {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.E_UNEXPECTED => return error.SystemResources,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
var self = try allocator.create(Context);
|
||||
errdefer allocator.destroy(self);
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.devices_info = util.DevicesInfo.init(),
|
||||
.enumerator = blk: {
|
||||
var enumerator: ?*win32.IMMDeviceEnumerator = null;
|
||||
hr = win32.CoCreateInstance(
|
||||
win32.CLSID_MMDeviceEnumerator,
|
||||
null,
|
||||
win32.CLSCTX_ALL,
|
||||
win32.IID_IMMDeviceEnumerator,
|
||||
@ptrCast(*?*anyopaque, &enumerator),
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.CLASS_E_NOAGGREGATION => return error.SystemResources,
|
||||
win32.REGDB_E_CLASSNOTREG => unreachable,
|
||||
else => unreachable,
|
||||
}
|
||||
break :blk enumerator;
|
||||
},
|
||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
||||
.deviceChangeFn = deviceChangeFn,
|
||||
.userdata = options.userdata,
|
||||
.notif_client = win32.IMMNotificationClient{
|
||||
.vtable = &.{
|
||||
.base = .{
|
||||
.QueryInterface = queryInterfaceCB,
|
||||
.AddRef = addRefCB,
|
||||
.Release = releaseCB,
|
||||
},
|
||||
.OnDeviceStateChanged = onDeviceStateChangedCB,
|
||||
.OnDeviceAdded = onDeviceAddedCB,
|
||||
.OnDeviceRemoved = onDeviceRemovedCB,
|
||||
.OnDefaultDeviceChanged = onDefaultDeviceChangedCB,
|
||||
.OnPropertyValueChanged = onPropertyValueChangedCB,
|
||||
},
|
||||
},
|
||||
} else null,
|
||||
};
|
||||
|
||||
if (options.deviceChangeFn) |_| {
|
||||
hr = self.enumerator.?.RegisterEndpointNotificationCallback(&self.watcher.?.notif_client);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.SystemResources,
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .wasapi = self };
|
||||
}
|
||||
|
||||
fn queryInterfaceCB(self: *const win32.IUnknown, riid: ?*const win32.Guid, ppv: ?*?*anyopaque) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
if (riid.?.eql(win32.IID_IUnknown.*) or riid.?.eql(win32.IID_IMMNotificationClient.*)) {
|
||||
ppv.?.* = @intToPtr(?*anyopaque, @ptrToInt(self));
|
||||
_ = self.AddRef();
|
||||
return win32.S_OK;
|
||||
} else {
|
||||
ppv.?.* = null;
|
||||
return win32.E_NOINTERFACE;
|
||||
}
|
||||
}
|
||||
|
||||
fn addRefCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn releaseCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn onDeviceStateChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDeviceAddedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDeviceRemovedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onDefaultDeviceChangedCB(self: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
fn onPropertyValueChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
||||
watcher.deviceChangeFn(watcher.userdata);
|
||||
return win32.S_OK;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (self.watcher) |*watcher| {
|
||||
_ = self.enumerator.?.UnregisterEndpointNotificationCallback(&watcher.notif_client);
|
||||
}
|
||||
_ = self.enumerator.?.Release();
|
||||
for (self.devices_info.list.items) |d|
|
||||
freeDevice(self.allocator, d);
|
||||
self.devices_info.list.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn refresh(self: *Context) !void {
|
||||
// get default devices id
|
||||
var default_playback_device: ?*win32.IMMDevice = null;
|
||||
var hr = self.enumerator.?.GetDefaultAudioEndpoint(.render, .multimedia, &default_playback_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = default_playback_device.?.Release();
|
||||
|
||||
var default_capture_device: ?*win32.IMMDevice = null;
|
||||
hr = self.enumerator.?.GetDefaultAudioEndpoint(.capture, .multimedia, &default_capture_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = default_capture_device.?.Release();
|
||||
|
||||
var default_playback_id_u16: ?[*:0]u16 = undefined;
|
||||
hr = default_playback_device.?.GetId(&default_playback_id_u16);
|
||||
defer win32.CoTaskMemFree(default_playback_id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
const default_playback_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_playback_id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(default_playback_id);
|
||||
|
||||
var default_capture_id_u16: ?[*:0]u16 = undefined;
|
||||
hr = default_capture_device.?.GetId(&default_capture_id_u16);
|
||||
defer win32.CoTaskMemFree(default_capture_id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
const default_capture_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_capture_id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(default_capture_id);
|
||||
|
||||
// enumerate
|
||||
var collection: ?*win32.IMMDeviceCollection = null;
|
||||
hr = self.enumerator.?.EnumAudioEndpoints(
|
||||
win32.DataFlow.all,
|
||||
win32.DEVICE_STATE_ACTIVE,
|
||||
&collection,
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = collection.?.Release();
|
||||
|
||||
var device_count: u32 = 0;
|
||||
hr = collection.?.GetCount(&device_count);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < device_count) : (i += 1) {
|
||||
var imm_device: ?*win32.IMMDevice = null;
|
||||
hr = collection.?.Item(i, &imm_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = imm_device.?.Release();
|
||||
|
||||
var property_store: ?*win32.IPropertyStore = null;
|
||||
var variant: win32.PROPVARIANT = undefined;
|
||||
hr = imm_device.?.OpenPropertyStore(win32.STGM_READ, &property_store);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = property_store.?.Release();
|
||||
|
||||
hr = property_store.?.GetValue(&win32.PKEY_AudioEngine_DeviceFormat, &variant);
|
||||
switch (hr) {
|
||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
var wf = @ptrCast(
|
||||
*win32.WAVEFORMATEXTENSIBLE,
|
||||
variant.anon.anon.anon.blob.pBlobData,
|
||||
);
|
||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.blob.pBlobData);
|
||||
|
||||
var device = main.Device{
|
||||
.mode = blk: {
|
||||
var endpoint: ?*win32.IMMEndpoint = null;
|
||||
hr = imm_device.?.QueryInterface(win32.IID_IMMEndpoint, @ptrCast(?*?*anyopaque, &endpoint));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
else => unreachable,
|
||||
}
|
||||
defer _ = endpoint.?.Release();
|
||||
|
||||
var dataflow: win32.DataFlow = undefined;
|
||||
hr = endpoint.?.GetDataFlow(&dataflow);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
break :blk switch (dataflow) {
|
||||
.render => .playback,
|
||||
.capture => .capture,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
.channels = blk: {
|
||||
var chn_arr = std.ArrayList(main.Channel).init(self.allocator);
|
||||
var channel: u32 = win32.SPEAKER_FRONT_LEFT;
|
||||
while (channel < win32.SPEAKER_ALL) : (channel <<= 1) {
|
||||
if (wf.dwChannelMask & channel != 0)
|
||||
try chn_arr.append(.{ .id = fromWASApiChannel(channel) });
|
||||
}
|
||||
break :blk try chn_arr.toOwnedSlice();
|
||||
},
|
||||
.sample_rate = .{
|
||||
.min = @intCast(u24, wf.Format.nSamplesPerSec),
|
||||
.max = @intCast(u24, wf.Format.nSamplesPerSec),
|
||||
},
|
||||
.formats = blk: {
|
||||
var audio_client: ?*win32.IAudioClient = null;
|
||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer _ = audio_client.?.Release();
|
||||
|
||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
||||
var closest_match: ?*win32.WAVEFORMATEX = null;
|
||||
for (std.meta.tags(main.Format)) |format| {
|
||||
setWaveFormatFormat(wf, format) catch continue;
|
||||
if (audio_client.?.IsFormatSupported(
|
||||
.SHARED,
|
||||
@ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), wf)),
|
||||
&closest_match,
|
||||
) == win32.S_OK) {
|
||||
try fmt_arr.append(format);
|
||||
}
|
||||
}
|
||||
|
||||
break :blk try fmt_arr.toOwnedSlice();
|
||||
},
|
||||
.id = blk: {
|
||||
var id_u16: ?[*:0]u16 = undefined;
|
||||
hr = imm_device.?.GetId(&id_u16);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer win32.CoTaskMemFree(id_u16);
|
||||
|
||||
break :blk std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(id_u16.?)) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
.name = blk: {
|
||||
hr = property_store.?.GetValue(&win32.PKEY_Device_FriendlyName, &variant);
|
||||
switch (hr) {
|
||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.pwszVal);
|
||||
|
||||
break :blk std.unicode.utf16leToUtf8AllocZ(
|
||||
self.allocator,
|
||||
std.mem.span(variant.anon.anon.anon.pwszVal.?),
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
try self.devices_info.list.append(self.allocator, device);
|
||||
if (self.devices_info.default(device.mode) == null) {
|
||||
switch (device.mode) {
|
||||
.playback => if (std.mem.eql(u8, device.id, default_playback_id)) {
|
||||
self.devices_info.setDefault(.playback, self.devices_info.list.items.len - 1);
|
||||
},
|
||||
.capture => if (std.mem.eql(u8, device.id, default_capture_id)) {
|
||||
self.devices_info.setDefault(.capture, self.devices_info.list.items.len - 1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn devices(self: Context) []const main.Device {
|
||||
return self.devices_info.list.items;
|
||||
}
|
||||
|
||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
||||
return self.devices_info.default(mode);
|
||||
}
|
||||
|
||||
fn fromWASApiChannel(speaker: u32) main.Channel.Id {
|
||||
return switch (speaker) {
|
||||
win32.SPEAKER_FRONT_CENTER => .front_center,
|
||||
win32.SPEAKER_FRONT_LEFT => .front_left,
|
||||
win32.SPEAKER_FRONT_RIGHT => .front_right,
|
||||
win32.SPEAKER_FRONT_LEFT_OF_CENTER => .front_left_center,
|
||||
win32.SPEAKER_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
||||
win32.SPEAKER_BACK_CENTER => .back_center,
|
||||
win32.SPEAKER_SIDE_LEFT => .side_left,
|
||||
win32.SPEAKER_SIDE_RIGHT => .side_right,
|
||||
win32.SPEAKER_TOP_CENTER => .top_center,
|
||||
win32.SPEAKER_TOP_FRONT_CENTER => .top_front_center,
|
||||
win32.SPEAKER_TOP_FRONT_LEFT => .top_front_left,
|
||||
win32.SPEAKER_TOP_FRONT_RIGHT => .top_front_right,
|
||||
win32.SPEAKER_TOP_BACK_CENTER => .top_back_center,
|
||||
win32.SPEAKER_TOP_BACK_LEFT => .top_back_left,
|
||||
win32.SPEAKER_TOP_BACK_RIGHT => .top_back_right,
|
||||
win32.SPEAKER_LOW_FREQUENCY => .lfe,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn setWaveFormatFormat(wf: *win32.WAVEFORMATEXTENSIBLE, format: main.Format) !void {
|
||||
switch (format) {
|
||||
.u8, .i16, .i24, .i24_4b, .i32 => {
|
||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*;
|
||||
},
|
||||
.f32, .f64 => {
|
||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*;
|
||||
},
|
||||
.i8 => return error.Invalid,
|
||||
}
|
||||
wf.Format.wBitsPerSample = format.sizeBits();
|
||||
wf.Samples.wValidBitsPerSample = format.validSizeBits();
|
||||
}
|
||||
|
||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer {
|
||||
var imm_device: ?*win32.IMMDevice = null;
|
||||
var id_u16 = std.unicode.utf8ToUtf16LeWithNull(self.allocator, device.id) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => unreachable,
|
||||
};
|
||||
defer self.allocator.free(id_u16);
|
||||
var hr = self.enumerator.?.GetDevice(id_u16, &imm_device);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
// TODO: win32.E_NOTFOUND!?
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var audio_client: ?*win32.IAudioClient = null;
|
||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @ptrCast(?*?*anyopaque, &audio_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
const format = device.preferredFormat(options.format);
|
||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
||||
|
||||
const wave_format = win32.WAVEFORMATEXTENSIBLE{
|
||||
.Format = .{
|
||||
.wFormatTag = win32.WAVE_FORMAT_EXTENSIBLE,
|
||||
.nChannels = @intCast(u16, device.channels.len),
|
||||
.nSamplesPerSec = sample_rate,
|
||||
.nAvgBytesPerSec = sample_rate * format.frameSize(device.channels.len),
|
||||
.nBlockAlign = format.frameSize(device.channels.len),
|
||||
.wBitsPerSample = format.sizeBits(),
|
||||
.cbSize = 0x16,
|
||||
},
|
||||
.Samples = .{
|
||||
.wValidBitsPerSample = format.validSizeBits(),
|
||||
},
|
||||
.dwChannelMask = toChannelMask(device.channels),
|
||||
.SubFormat = toSubFormat(format) catch return error.OpeningDevice,
|
||||
};
|
||||
|
||||
hr = audio_client.?.Initialize(
|
||||
.SHARED,
|
||||
win32.AUDCLNT_STREAMFLAGS_NOPERSIST,
|
||||
0,
|
||||
0,
|
||||
@ptrCast(?*const win32.WAVEFORMATEX, @alignCast(@alignOf(*win32.WAVEFORMATEX), &wave_format)),
|
||||
null,
|
||||
);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_DEVICE_IN_USE => unreachable,
|
||||
win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable,
|
||||
win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable,
|
||||
win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
errdefer _ = audio_client.?.Release();
|
||||
|
||||
var render_client: ?*win32.IAudioRenderClient = null;
|
||||
hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @ptrCast(?*?*anyopaque, &render_client));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
var simple_volume: ?*win32.ISimpleAudioVolume = null;
|
||||
hr = audio_client.?.GetService(win32.IID_ISimpleAudioVolume, @ptrCast(?*?*anyopaque, &simple_volume));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.E_NOINTERFACE => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
||||
else => return error.OpeningDevice,
|
||||
}
|
||||
|
||||
return .{
|
||||
.wasapi = .{
|
||||
.thread = undefined,
|
||||
.mutex = .{},
|
||||
._channels = device.channels,
|
||||
._format = format,
|
||||
.sample_rate = sample_rate,
|
||||
.writeFn = writeFn,
|
||||
.audio_client = audio_client,
|
||||
.simple_volume = simple_volume,
|
||||
.imm_device = imm_device,
|
||||
.render_client = render_client,
|
||||
.is_paused = false,
|
||||
.vol = 1.0,
|
||||
.aborted = .{ .value = false },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn toSubFormat(format: main.Format) !win32.Guid {
|
||||
return switch (format) {
|
||||
.u8 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i16 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i24 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i24_4b => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.i32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
||||
.f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*,
|
||||
.f64 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*,
|
||||
else => error.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
fn toChannelMask(channels: []const main.Channel) u32 {
|
||||
var mask: u32 = 0;
|
||||
for (channels) |ch| {
|
||||
mask |= switch (ch.id) {
|
||||
.front_center => win32.SPEAKER_FRONT_CENTER,
|
||||
.front_left => win32.SPEAKER_FRONT_LEFT,
|
||||
.front_right => win32.SPEAKER_FRONT_RIGHT,
|
||||
.front_left_center => win32.SPEAKER_FRONT_LEFT_OF_CENTER,
|
||||
.front_right_center => win32.SPEAKER_FRONT_RIGHT_OF_CENTER,
|
||||
.back_center => win32.SPEAKER_BACK_CENTER,
|
||||
.side_left => win32.SPEAKER_SIDE_LEFT,
|
||||
.side_right => win32.SPEAKER_SIDE_RIGHT,
|
||||
.top_center => win32.SPEAKER_TOP_CENTER,
|
||||
.top_front_center => win32.SPEAKER_TOP_FRONT_CENTER,
|
||||
.top_front_left => win32.SPEAKER_TOP_FRONT_LEFT,
|
||||
.top_front_right => win32.SPEAKER_TOP_FRONT_RIGHT,
|
||||
.top_back_center => win32.SPEAKER_TOP_BACK_CENTER,
|
||||
.top_back_left => win32.SPEAKER_TOP_BACK_LEFT,
|
||||
.top_back_right => win32.SPEAKER_TOP_BACK_RIGHT,
|
||||
.lfe => win32.SPEAKER_LOW_FREQUENCY,
|
||||
};
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Player = struct {
|
||||
thread: std.Thread,
|
||||
mutex: std.Thread.Mutex,
|
||||
_channels: []main.Channel,
|
||||
_format: main.Format,
|
||||
sample_rate: u24,
|
||||
writeFn: main.WriteFn,
|
||||
simple_volume: ?*win32.ISimpleAudioVolume,
|
||||
imm_device: ?*win32.IMMDevice,
|
||||
audio_client: ?*win32.IAudioClient,
|
||||
render_client: ?*win32.IAudioRenderClient,
|
||||
aborted: std.atomic.Atomic(bool),
|
||||
is_paused: bool,
|
||||
vol: f32,
|
||||
|
||||
pub fn deinit(self: *Player) void {
|
||||
self.aborted.store(true, .Unordered);
|
||||
self.thread.join();
|
||||
_ = self.simple_volume.?.Release();
|
||||
_ = self.render_client.?.Release();
|
||||
_ = self.audio_client.?.Release();
|
||||
_ = self.imm_device.?.Release();
|
||||
}
|
||||
|
||||
pub fn start(self: *Player) !void {
|
||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
||||
error.ThreadQuotaExceeded,
|
||||
error.SystemResources,
|
||||
error.LockedMemoryLimitExceeded,
|
||||
=> return error.SystemResources,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Unexpected => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeLoop(self: *Player) void {
|
||||
var parent = @fieldParentPtr(main.Player, "data", @ptrCast(*backends.BackendPlayer, self));
|
||||
|
||||
var hr = self.audio_client.?.Start();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
while (!self.aborted.load(.Unordered)) {
|
||||
var frames_buf: u32 = 0;
|
||||
hr = self.audio_client.?.GetBufferSize(&frames_buf);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
var frames_used: u32 = 0;
|
||||
hr = self.audio_client.?.GetCurrentPadding(&frames_used);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
const writable_frame_count = frames_buf - frames_used;
|
||||
if (writable_frame_count > 0) {
|
||||
var data: [*]u8 = undefined;
|
||||
hr = self.render_client.?.GetBuffer(writable_frame_count, @ptrCast(?*?*u8, &data));
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_TOO_LARGE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_BUFFER_OPERATION_PENDING => continue,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
for (self.channels()) |*ch, i| {
|
||||
ch.*.ptr = data + self.format().frameSize(i);
|
||||
}
|
||||
|
||||
self.writeFn(parent, writable_frame_count);
|
||||
hr = self.render_client.?.ReleaseBuffer(writable_frame_count, 0);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_INVALID_SIZE => unreachable,
|
||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(self: *Player) !void {
|
||||
if (self.paused()) {
|
||||
const hr = self.audio_client.?.Start();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPlay,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPlay,
|
||||
else => unreachable,
|
||||
}
|
||||
self.is_paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(self: *Player) !void {
|
||||
if (!self.paused()) {
|
||||
const hr = self.audio_client.?.Stop();
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPause,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPause,
|
||||
else => unreachable,
|
||||
}
|
||||
self.is_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused(self: Player) bool {
|
||||
return self.is_paused;
|
||||
}
|
||||
|
||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
||||
const hr = self.simple_volume.?.SetMasterVolume(vol, null);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_INVALIDARG => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotSetVolume,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotSetVolume,
|
||||
else => return error.CannotSetVolume,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn volume(self: Player) !f32 {
|
||||
var vol: f32 = 0;
|
||||
const hr = self.simple_volume.?.GetMasterVolume(&vol);
|
||||
switch (hr) {
|
||||
win32.S_OK => {},
|
||||
win32.E_POINTER => unreachable,
|
||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotGetVolume,
|
||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotGetVolume,
|
||||
else => return error.CannotGetVolume,
|
||||
}
|
||||
return vol;
|
||||
}
|
||||
|
||||
pub fn writeRaw(self: Player, channel: main.Channel, frame: usize, sample: anytype) void {
|
||||
var ptr = channel.ptr + frame * self.format().frameSize(self.channels().len);
|
||||
std.mem.bytesAsValue(@TypeOf(sample), ptr[0..@sizeOf(@TypeOf(sample))]).* = sample;
|
||||
}
|
||||
|
||||
pub fn channels(self: Player) []main.Channel {
|
||||
return self._channels;
|
||||
}
|
||||
|
||||
pub fn format(self: Player) main.Format {
|
||||
return self._format;
|
||||
}
|
||||
|
||||
pub fn sampleRate(self: Player) u24 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn freeDevice(allocator: std.mem.Allocator, self: main.Device) void {
|
||||
allocator.free(self.id);
|
||||
allocator.free(self.name);
|
||||
allocator.free(self.formats);
|
||||
allocator.free(self.channels);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
1645
libs/sysaudio/src/wasapi/win32.zig
Normal file
1645
libs/sysaudio/src/wasapi/win32.zig
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,213 +0,0 @@
|
|||
const std = @import("std");
|
||||
const Mode = @import("main.zig").Mode;
|
||||
const Format = @import("main.zig").Format;
|
||||
const DataCallback = @import("main.zig").DataCallback;
|
||||
const js = @import("sysjs");
|
||||
|
||||
const Audio = @This();
|
||||
|
||||
pub const Device = struct {
|
||||
properties: Properties,
|
||||
|
||||
// Internal fields.
|
||||
context: js.Object,
|
||||
|
||||
pub const Options = struct {
|
||||
mode: Mode = .output,
|
||||
format: ?Format = null,
|
||||
is_raw: ?bool = null,
|
||||
channels: ?u8 = null,
|
||||
sample_rate: ?u32 = null,
|
||||
id: ?[:0]const u8 = null,
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Properties = struct {
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
is_raw: bool,
|
||||
channels: u8,
|
||||
sample_rate: u32,
|
||||
id: [:0]const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub fn deinit(device: *Device, allocator: std.mem.Allocator) void {
|
||||
device.context.deinit();
|
||||
allocator.destroy(device);
|
||||
}
|
||||
|
||||
pub fn setCallback(device: *Device, callback: DataCallback, user_data: ?*anyopaque) void {
|
||||
device.context.set("device", js.createNumber(@intToFloat(f64, @ptrToInt(device))));
|
||||
device.context.set("callback", js.createNumber(@intToFloat(f64, @ptrToInt(callback))));
|
||||
if (user_data) |ud|
|
||||
device.context.set("user_data", js.createNumber(@intToFloat(f64, @ptrToInt(ud))));
|
||||
}
|
||||
|
||||
pub fn pause(device: *Device) Error!void {
|
||||
_ = device.context.call("suspend", &.{});
|
||||
}
|
||||
|
||||
pub fn start(device: *Device) Error!void {
|
||||
_ = device.context.call("resume", &.{});
|
||||
}
|
||||
};
|
||||
|
||||
pub const DeviceIterator = struct {
|
||||
ctx: *Audio,
|
||||
mode: Mode,
|
||||
|
||||
pub fn next(_: DeviceIterator) IteratorError!?Device.Properties {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const IteratorError = error{};
|
||||
|
||||
pub const Error = error{
|
||||
OutOfMemory,
|
||||
AudioUnsupported,
|
||||
};
|
||||
|
||||
context_constructor: js.Function,
|
||||
|
||||
pub fn init() Error!Audio {
|
||||
const context = js.global().get("AudioContext");
|
||||
if (context.is(.undefined))
|
||||
return error.AudioUnsupported;
|
||||
|
||||
return Audio{ .context_constructor = context.view(.func) };
|
||||
}
|
||||
|
||||
pub fn deinit(audio: Audio) void {
|
||||
audio.context_constructor.deinit();
|
||||
}
|
||||
|
||||
// TODO(sysaudio): implement waitEvents for WebAudio, will a WASM process terminate without this?
|
||||
pub fn waitEvents(_: Audio) void {}
|
||||
|
||||
const default_channel_count = 2;
|
||||
const default_sample_rate = 48000;
|
||||
const default_buffer_size_per_channel = 1024; // 21.33ms
|
||||
|
||||
pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, options: Device.Options) Error!*Device {
|
||||
// NOTE: WebAudio only supports F32 audio format, so options.format is unused
|
||||
const mode = options.mode;
|
||||
const channels = options.channels orelse default_channel_count;
|
||||
const sample_rate = options.sample_rate orelse default_sample_rate;
|
||||
|
||||
const context_options = js.createMap();
|
||||
defer context_options.deinit();
|
||||
context_options.set("sampleRate", js.createNumber(@intToFloat(f64, sample_rate)));
|
||||
|
||||
const context = audio.context_constructor.construct(&.{context_options.toValue()});
|
||||
_ = context.call("suspend", &.{});
|
||||
|
||||
const input_channels = if (mode == .input) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined();
|
||||
const output_channels = if (mode == .output) js.createNumber(@intToFloat(f64, channels)) else js.createUndefined();
|
||||
|
||||
const node = context.call("createScriptProcessor", &.{ js.createNumber(default_buffer_size_per_channel), input_channels, output_channels }).view(.object);
|
||||
defer node.deinit();
|
||||
|
||||
context.set("node", node.toValue());
|
||||
|
||||
{
|
||||
// TODO(sysaudio): this capture leaks for now, we need a better way to pass captures via sysjs
|
||||
// that passes by value I think.
|
||||
const captures = std.heap.page_allocator.alloc(js.Value, 1) catch unreachable;
|
||||
captures[0] = context.toValue();
|
||||
const audio_process_event = js.createFunction(audioProcessEvent, captures);
|
||||
|
||||
// TODO(sysaudio): this leaks, we need a good place to clean this up.
|
||||
// defer audio_process_event.deinit();
|
||||
node.set("onaudioprocess", audio_process_event.toValue());
|
||||
}
|
||||
|
||||
{
|
||||
const destination = context.get("destination").view(.object);
|
||||
defer destination.deinit();
|
||||
_ = node.call("connect", &.{destination.toValue()});
|
||||
}
|
||||
|
||||
// TODO(sysaudio): Figure out ID/name or make optional again
|
||||
var properties = Device.Properties{
|
||||
.id = "0",
|
||||
.name = "WebAudio",
|
||||
.format = .F32,
|
||||
.mode = options.mode,
|
||||
.is_raw = false,
|
||||
.channels = options.channels orelse default_channel_count,
|
||||
.sample_rate = options.sample_rate orelse default_sample_rate,
|
||||
};
|
||||
|
||||
const device = try allocator.create(Device);
|
||||
device.* = .{
|
||||
.properties = properties,
|
||||
.context = context,
|
||||
};
|
||||
return device;
|
||||
}
|
||||
|
||||
fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value {
|
||||
const device_context = captures[0].view(.object);
|
||||
|
||||
const audio_event = args.getIndex(0).view(.object);
|
||||
defer audio_event.deinit();
|
||||
const output_buffer = audio_event.get("outputBuffer").view(.object);
|
||||
defer output_buffer.deinit();
|
||||
const num_channels = @floatToInt(usize, output_buffer.get("numberOfChannels").view(.num));
|
||||
|
||||
const buffer_length = default_buffer_size_per_channel * num_channels * @sizeOf(f32);
|
||||
// TODO(sysaudio): reuse buffer, do not allocate in this hot path
|
||||
const buffer = std.heap.page_allocator.alloc(u8, buffer_length) catch unreachable;
|
||||
defer std.heap.page_allocator.free(buffer);
|
||||
|
||||
const callback = device_context.get("callback");
|
||||
if (!callback.is(.undefined)) {
|
||||
var dev = @intToPtr(*Device, @floatToInt(usize, device_context.get("device").view(.num)));
|
||||
const cb = @intToPtr(DataCallback, @floatToInt(usize, callback.view(.num)));
|
||||
const user_data = device_context.get("user_data");
|
||||
const ud = if (user_data.is(.undefined)) null else @intToPtr(*anyopaque, @floatToInt(usize, user_data.view(.num)));
|
||||
|
||||
// TODO(sysaudio): do not reconstruct Uint8Array (expensive)
|
||||
const source = js.constructType("Uint8Array", &.{js.createNumber(@intToFloat(f64, buffer_length))});
|
||||
defer source.deinit();
|
||||
|
||||
cb(dev, ud, buffer[0..]);
|
||||
source.copyBytes(buffer[0..]);
|
||||
|
||||
const float_source = js.constructType("Float32Array", &.{
|
||||
source.get("buffer"),
|
||||
source.get("byteOffset"),
|
||||
js.createNumber(source.get("byteLength").view(.num) / 4),
|
||||
});
|
||||
defer float_source.deinit();
|
||||
|
||||
js.global().set("source", source.toValue());
|
||||
js.global().set("float_source", float_source.toValue());
|
||||
js.global().set("output_buffer", output_buffer.toValue());
|
||||
|
||||
var channel: usize = 0;
|
||||
while (channel < num_channels) : (channel += 1) {
|
||||
// TODO(sysaudio): investigate if using copyToChannel would be better?
|
||||
//_ = output_buffer.call("copyToChannel", &.{ float_source.toValue(), js.createNumber(@intToFloat(f64, channel)) });
|
||||
const output_data = output_buffer.call("getChannelData", &.{js.createNumber(@intToFloat(f64, channel))}).view(.object);
|
||||
defer output_data.deinit();
|
||||
const channel_slice = float_source.call("slice", &.{
|
||||
js.createNumber(@intToFloat(f64, channel * default_buffer_size_per_channel)),
|
||||
js.createNumber(@intToFloat(f64, (channel + 1) * default_buffer_size_per_channel)),
|
||||
});
|
||||
_ = output_data.call("set", &.{channel_slice});
|
||||
}
|
||||
}
|
||||
|
||||
return js.createUndefined();
|
||||
}
|
||||
|
||||
pub fn outputDeviceIterator(audio: Audio) DeviceIterator {
|
||||
return .{ .audio = audio, .mode = .output };
|
||||
}
|
||||
|
||||
pub fn inputDeviceIterator(audio: Audio) DeviceIterator {
|
||||
return .{ .audio = audio, .mode = .input };
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 56c1f298c25388e78bedd48085c385aed945d1e5
|
||||
Loading…
Add table
Add a link
Reference in a new issue