sysaudio: rewrite in zig

removes libsoundio dependency
This commit is contained in:
Ali Chraghi 2022-12-16 14:55:46 +03:30 committed by Stephen Gutekanst
parent 8aa2c97079
commit 0f3e28bc2a
27 changed files with 4714 additions and 1344 deletions

View file

@ -1,3 +0,0 @@
[submodule "upstream"]
path = upstream
url = https://github.com/hexops/soundio

View file

@ -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();

View 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", .{});
}

View file

@ -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();
}

View file

@ -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",
};
};
}

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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,
};
}

View file

@ -1,3 +0,0 @@
pub usingnamespace @cImport({
@cInclude("soundio/soundio.h");
});

View file

@ -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,
};

View file

@ -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));
}

View file

@ -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
View 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(&params);
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());
}

View 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
View 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
View 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());
}

View file

@ -1,134 +1,461 @@
const std = @import("std");
const mem = std.mem;
const testing = std.testing;
const builtin = @import("builtin");
const Backend = if (builtin.cpu.arch == .wasm32) @import("webaudio.zig") else switch (builtin.os.tag) {
.linux,
.windows,
.macos,
.ios,
=> @import("soundio.zig"),
else => @compileError("unsupported os"),
};
pub const Error = Backend.Error;
pub const Device = Backend.Device;
pub const DeviceIterator = Backend.DeviceIterator;
const std = @import("std");
const util = @import("util.zig");
const backends = @import("backends.zig");
pub const DataCallback = *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
pub const default_sample_rate = 44_100; // Hz
pub const default_latency = 500 * std.time.us_per_ms; // μs
pub const Mode = enum {
input,
output,
pub const Backend = backends.Backend;
pub const DeviceChangeFn = *const fn (self: ?*anyopaque) void;
pub const ConnectError = error{
OutOfMemory,
AccessDenied,
SystemResources,
ConnectionRefused,
};
pub const Format = enum {
U8,
S16,
S24,
S32,
F32,
};
const Audio = @This();
backend: Backend,
pub fn init() Error!Audio {
return Audio{
.backend = try Backend.init(),
pub const Context = struct {
pub const Options = struct {
app_name: [:0]const u8 = "Mach Game",
deviceChangeFn: ?DeviceChangeFn = null,
userdata: ?*anyopaque = null,
};
}
pub fn deinit(self: Audio) void {
self.backend.deinit();
}
data: backends.BackendContext,
pub fn waitEvents(self: Audio) void {
self.backend.waitEvents();
}
pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) ConnectError!Context {
var data: backends.BackendContext = blk: {
if (backend) |b| {
break :blk try @typeInfo(
std.meta.fieldInfo(backends.BackendContext, b).field_type,
).Pointer.child.init(allocator, options);
} else {
inline for (std.meta.fields(Backend)) |b, i| {
if (@typeInfo(
std.meta.fieldInfo(backends.BackendContext, @intToEnum(Backend, b.value)).field_type,
).Pointer.child.init(allocator, options)) |d| {
break :blk d;
} else |err| {
if (i == std.meta.fields(Backend).len - 1)
return err;
}
}
unreachable;
}
};
pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: Device.Options) Error!*Device {
return self.backend.requestDevice(allocator, config);
}
return .{ .data = data };
}
pub fn inputDeviceIterator(self: Audio) DeviceIterator {
return self.backend.inputDeviceIterator();
}
pub fn deinit(self: Context) void {
switch (self.data) {
inline else => |b| b.deinit(),
}
}
pub fn outputDeviceIterator(self: Audio) DeviceIterator {
return self.backend.outputDeviceIterator();
}
pub const RefreshError = error{
OutOfMemory,
SystemResources,
OpeningDevice,
};
test "list devices" {
const a = try init();
defer a.deinit();
pub fn refresh(self: Context) RefreshError!void {
return switch (self.data) {
inline else => |b| b.refresh(),
};
}
var iter = a.inputDeviceIterator();
while (try iter.next()) |_| {}
}
pub fn devices(self: Context) []const Device {
return switch (self.data) {
inline else => |b| b.devices(),
};
}
// TODO(sysaudio): get this test passing on CI
test "connect to device" {
return error.SkipZigTest;
pub fn defaultDevice(self: Context, mode: Device.Mode) ?Device {
return switch (self.data) {
inline else => |b| b.defaultDevice(mode),
};
}
// const a = try init();
// defer a.deinit();
pub const CreateStreamError = error{
OutOfMemory,
SystemResources,
OpeningDevice,
IncompatibleDevice,
};
// const d = try a.requestDevice(std.testing.allocator, .{ .mode = .output });
// defer d.deinit(std.testing.allocator);
}
pub fn createPlayer(self: Context, device: Device, writeFn: WriteFn, options: Player.Options) CreateStreamError!Player {
std.debug.assert(device.mode == .playback);
// TODO(sysaudio): get this test passing on CI
test "connect to device from descriptor" {
return error.SkipZigTest;
return .{
.userdata = options.userdata,
.data = switch (self.data) {
inline else => |b| try b.createPlayer(device, writeFn, options),
},
};
}
};
// const a = try init();
// defer a.deinit();
// TODO: `*Player` instead `*anyopaque`
// https://github.com/ziglang/zig/issues/12325
pub const WriteFn = *const fn (self: *anyopaque, frame_count_max: usize) void;
// var iter = a.outputDeviceIterator();
// while (try iter.next()) |desc| {
// if (mem.eql(u8, desc.name orelse "", "default")) {
// const d = try a.requestDevice(std.testing.allocator, desc);
// defer d.deinit(std.testing.allocator);
// return;
pub const Player = struct {
pub const Options = struct {
format: Format = .f32,
sample_rate: u24 = default_sample_rate,
userdata: ?*anyopaque = null,
};
userdata: ?*anyopaque,
data: backends.BackendPlayer,
pub fn deinit(self: *Player) void {
return switch (self.data) {
inline else => |*b| b.deinit(),
};
}
pub const StartError = error{
CannotPlay,
OutOfMemory,
SystemResources,
};
pub fn start(self: *Player) StartError!void {
return switch (self.data) {
inline else => |*b| b.start(),
};
}
pub const PlayError = error{
CannotPlay,
OutOfMemory,
};
pub fn play(self: *Player) PlayError!void {
return switch (self.data) {
inline else => |*b| b.play(),
};
}
pub const PauseError = error{
CannotPause,
OutOfMemory,
};
pub fn pause(self: *Player) PauseError!void {
return switch (self.data) {
inline else => |*b| b.pause(),
};
}
pub fn paused(self: *Player) bool {
return switch (self.data) {
inline else => |*b| b.paused(),
};
}
pub const SetVolumeError = error{
CannotSetVolume,
};
// confidence interval (±) depends on the device
pub fn setVolume(self: *Player, vol: f32) SetVolumeError!void {
std.debug.assert(vol <= 1.0);
return switch (self.data) {
inline else => |*b| b.setVolume(vol),
};
}
pub const GetVolumeError = error{
CannotGetVolume,
};
// confidence interval (±) depends on the device
pub fn volume(self: *Player) GetVolumeError!f32 {
return switch (self.data) {
inline else => |*b| b.volume(),
};
}
pub fn writeRaw(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
return switch (self.data) {
inline else => |*b| b.writeRaw(channel, frame, sample),
};
}
pub fn writeAll(self: *Player, frame: usize, value: anytype) void {
for (self.channels()) |ch|
self.write(ch, frame, value);
}
pub fn write(self: *Player, channel: Channel, frame: usize, sample: anytype) void {
switch (@TypeOf(sample)) {
u8 => self.writeU8(channel, frame, sample),
i16 => self.writeI16(channel, frame, sample),
i24 => self.writeI24(channel, frame, sample),
i32 => self.writeI32(channel, frame, sample),
f32 => self.writeF32(channel, frame, sample),
f64 => self.writeF64(channel, frame, sample),
else => @compileError(
\\invalid sample type. supported types are:
\\u8, i8, i16, i24, i32, f32, f32, f64
),
}
}
pub fn writeU8(self: *Player, channel: Channel, frame: usize, sample: u8) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, sample),
.i8 => self.writeRaw(channel, frame, unsignedToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, unsignedToSigned(i16, sample)),
.i24 => self.writeRaw(channel, frame, unsignedToSigned(i24, sample)),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, unsignedToSigned(i32, sample)),
.f32 => self.writeRaw(channel, frame, unsignedToFloat(f32, sample)),
.f64 => self.writeRaw(channel, frame, unsignedToFloat(f64, sample)),
}
}
pub fn writeI16(self: *Player, channel: Channel, frame: usize, sample: i16) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, sample),
.i24 => self.writeRaw(channel, frame, sample),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, sample),
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
}
}
pub fn writeI24(self: *Player, channel: Channel, frame: usize, sample: i24) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
.i24 => self.writeRaw(channel, frame, sample),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, sample),
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
}
}
pub fn writeI32(self: *Player, channel: Channel, frame: usize, sample: i32) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, signedToUnsigned(u8, sample)),
.i8 => self.writeRaw(channel, frame, signedToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, signedToSigned(i16, sample)),
.i24 => self.writeRaw(channel, frame, signedToSigned(i24, sample)),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, sample),
.f32 => self.writeRaw(channel, frame, signedToFloat(f32, sample)),
.f64 => self.writeRaw(channel, frame, signedToFloat(f64, sample)),
}
}
pub fn writeF32(self: *Player, channel: Channel, frame: usize, sample: f32) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
.f32 => self.writeRaw(channel, frame, sample),
.f64 => self.writeRaw(channel, frame, sample),
}
}
pub fn writeF64(self: *Player, channel: Channel, frame: usize, sample: f64) void {
switch (self.format()) {
.u8 => self.writeRaw(channel, frame, floatToUnsigned(u8, sample)),
.i8 => self.writeRaw(channel, frame, floatToSigned(i8, sample)),
.i16 => self.writeRaw(channel, frame, floatToSigned(i16, sample)),
.i24 => self.writeRaw(channel, frame, floatToSigned(i24, sample)),
.i24_4b => @panic("TODO"),
.i32 => self.writeRaw(channel, frame, floatToSigned(i32, sample)),
.f32 => self.writeRaw(channel, frame, sample),
.f64 => self.writeRaw(channel, frame, sample),
}
}
fn unsignedToSigned(comptime T: type, sample: anytype) T {
const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1);
const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample));
return @intCast(T, sample -% half) << trunc;
}
fn unsignedToFloat(comptime T: type, sample: anytype) T {
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
return (@intToFloat(T, sample) - max_int) * 1.0 / max_int;
}
fn signedToSigned(comptime T: type, sample: anytype) T {
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
return @intCast(T, sample >> trunc);
}
fn signedToUnsigned(comptime T: type, sample: anytype) T {
const half = 1 << (@bitSizeOf(T) - 1);
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
return @intCast(T, (sample >> trunc) + half);
}
fn signedToFloat(comptime T: type, sample: anytype) T {
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
return @intToFloat(T, sample) * 1.0 / max_int;
}
fn floatToSigned(comptime T: type, sample: f64) T {
return @floatToInt(T, sample * std.math.maxInt(T));
}
fn floatToUnsigned(comptime T: type, sample: f64) T {
const half = 1 << @bitSizeOf(T) - 1;
return @floatToInt(T, sample * (half - 1) + half);
}
// TODO: needs test
// fn f32Toi24_4b(sample: f32) i32 {
// const scaled = sample * std.math.maxInt(i32);
// if (builtin.cpu.arch.endian() == .Little) {
// return @floatToInt(i32, scaled);
// } else {
// var res: [4]u8 = undefined;
// std.mem.writeIntSliceBig(i32, &res, @floatToInt(i32, res));
// return @bitCast(i32, res);
// }
// }
// return error.SkipZigTest;
}
// TODO(sysaudio): get this test passing on CI
test "requestDevice behavior: null is_raw" {
return error.SkipZigTest;
// const a = try init();
// defer a.deinit();
// var iter = a.outputDeviceIterator();
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
// const bad_conf = Device.Options{
// .is_raw = null,
// .mode = device_conf.mode,
// .id = device_conf.id,
// };
// try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_conf));
}
// TODO(sysaudio): get this test passing on CI
test "requestDevice behavior: invalid id" {
return error.SkipZigTest;
// const a = try init();
// defer a.deinit();
// var iter = a.outputDeviceIterator();
// var device_conf = (try iter.next()) orelse return error.NoDeviceFound;
// const bad_conf = Device.Options{
// .is_raw = device_conf.is_raw,
// .mode = device_conf.mode,
// .id = "wrong-id",
// };
// try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf));
pub fn channels(self: Player) []Channel {
return switch (self.data) {
inline else => |*b| b.channels(),
};
}
pub fn format(self: Player) Format {
return switch (self.data) {
inline else => |*b| b.format(),
};
}
pub fn sampleRate(self: Player) u24 {
return switch (self.data) {
inline else => |*b| b.sampleRate(),
};
}
pub fn frameSize(self: Player) u8 {
return self.format().frameSize(self.channels().len);
}
};
pub const Device = struct {
id: [:0]const u8,
name: [:0]const u8,
mode: Mode,
channels: []Channel,
formats: []const Format,
sample_rate: util.Range(u24),
pub const Mode = enum {
playback,
capture,
};
pub fn preferredFormat(self: Device, format: ?Format) Format {
if (format) |f| {
for (self.formats) |fmt| {
if (f == fmt) {
return fmt;
}
}
}
var best: Format = self.formats[0];
for (self.formats) |fmt| {
if (fmt.size() >= best.size()) {
if (fmt == .i24_4b and best == .i24)
continue;
best = fmt;
}
}
return best;
}
};
pub const Channel = struct {
ptr: [*]u8 = undefined,
id: Id,
pub const Id = enum {
front_center,
front_left,
front_right,
front_left_center,
front_right_center,
back_center,
side_left,
side_right,
top_center,
top_front_center,
top_front_left,
top_front_right,
top_back_center,
top_back_left,
top_back_right,
lfe,
};
};
pub const Format = enum {
u8,
i8,
i16,
i24,
i24_4b,
i32,
f32,
f64,
pub fn size(self: Format) u8 {
return switch (self) {
.u8, .i8 => 1,
.i16 => 2,
.i24 => 3,
.i24_4b, .i32, .f32 => 4,
.f64 => 8,
};
}
pub fn validSize(self: Format) u8 {
return switch (self) {
.u8, .i8 => 1,
.i16 => 2,
.i24, .i24_4b => 3,
.i32, .f32 => 4,
.f64 => 8,
};
}
pub fn sizeBits(self: Format) u8 {
return self.size() * 8;
}
pub fn validSizeBits(self: Format) u8 {
return self.validSize() * 8;
}
pub fn frameSize(self: Format, ch_count: usize) u8 {
return self.size() * @intCast(u5, ch_count);
}
};
test {
std.testing.refAllDeclsRecursive(@This());
}

View 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());
}

View file

@ -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,
};
}

View 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 {}

View 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());
}

File diff suppressed because it is too large Load diff

View file

@ -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