diff --git a/libs/sysaudio/src/soundio.zig b/libs/sysaudio/src/soundio.zig index 3bf3e84e..a409c872 100644 --- a/libs/sysaudio/src/soundio.zig +++ b/libs/sysaudio/src/soundio.zig @@ -7,6 +7,7 @@ const SoundIo = @import("soundio").SoundIo; const SoundIoDevice = @import("soundio").Device; const SoundIoInStream = @import("soundio").InStream; const SoundIoOutStream = @import("soundio").OutStream; + const SoundIoStream = union(Mode) { input: SoundIoInStream, output: SoundIoOutStream, @@ -15,27 +16,125 @@ const SoundIoStream = union(Mode) { const Audio = @This(); pub const DataCallback = if (@import("builtin").zig_backend == .stage1) - fn (device: Device, frame_count: u32) void + fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void else - *const fn (device: Device, frame_count: u32) void; + *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; pub const Device = struct { + descriptor: DeviceDescriptor, + + // Internal fields. handle: SoundIoStream, data_callback: ?DataCallback = null, user_data: ?*anyopaque = null, + planar_buffer: [512000]u8 = undefined, - pub fn setCallback(self: Device, callback: DataCallback, data: *anyopaque) void { - self.data_callback = callback; - self.user_data = data; - } - - pub fn deinit(self: Device) void { - return switch (self.handle) { + 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 { + _ = frame_count_min; + 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 total_frame_count = @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..buffer_size]; + device.data_callback.?(device, device.user_data.?, planar_buffer); + + var frames_left = total_frame_count; + 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)) + (@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); + } + } + }).cCallback); + }, + } + } + + pub fn pause(device: *Device) Error!void { + return (switch (device.handle) { + .input => |d| d.pause(), + .output => |d| d.pause(), + }) catch |err| { + return switch (err) { + error.OutOfMemory => error.OutOfMemory, + else => @panic(@errorName(err)), + }; + }; + } + + pub fn start(device: *Device) Error!void { + 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)), + }; }; } }; + pub const DeviceIterator = struct { ctx: Audio, mode: Mode, @@ -63,6 +162,7 @@ pub const DeviceIterator = struct { } }; +// TODO(sysaudio): standardize errors across backends pub const IteratorError = error{OutOfMemory}; pub const Error = error{ OutOfMemory, @@ -74,6 +174,20 @@ pub const Error = error{ UnsupportedOS, UnsupportedBackend, DeviceUnavailable, + Invalid, + OpeningDevice, + BackendDisconnected, + SystemResources, + NoSuchClient, + IncompatibleBackend, + IncompatibleDevice, + InitAudioBackend, + NoSuchDevice, + BackendUnavailable, + Streaming, + Interrupted, + Underflow, + EncodingString, }; handle: SoundIo, @@ -102,46 +216,71 @@ pub fn waitEvents(self: Audio) void { self.handle.waitEvents(); } -pub fn requestDevice(self: Audio, config: DeviceDescriptor) Error!Device { - return Device{ - .handle = blk: { - var sio_device: SoundIoDevice = undefined; +pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDescriptor) Error!*Device { + var sio_device: SoundIoDevice = undefined; - if (config.id) |id| { - if (config.mode == null or config.is_raw == null) - return error.InvalidParameter; + if (config.id) |id| { + if (config.mode == null or config.is_raw == null) + return error.InvalidParameter; - sio_device = switch (config.mode.?) { - .input => self.handle.getInputDeviceFromID(id, config.is_raw.?), - .output => self.handle.getOutputDeviceFromID(id, config.is_raw.?), - } orelse { - return if (switch (config.mode.?) { - .input => self.handle.inputDeviceCount().?, - .output => self.handle.outputDeviceCount().?, - } == 0) - error.NoDeviceFound - else - error.DeviceUnavailable; - }; - } else { - if (config.mode == null) return error.InvalidParameter; + sio_device = switch (config.mode.?) { + .input => self.handle.getInputDeviceFromID(id, config.is_raw.?), + .output => self.handle.getOutputDeviceFromID(id, config.is_raw.?), + } orelse { + return if (switch (config.mode.?) { + .input => self.handle.inputDeviceCount().?, + .output => self.handle.outputDeviceCount().?, + } == 0) + error.NoDeviceFound + else + error.DeviceUnavailable; + }; + } else { + if (config.mode == null) return error.InvalidParameter; - const id = switch (config.mode.?) { - .input => self.handle.defaultInputDeviceIndex(), - .output => self.handle.defaultOutputDeviceIndex(), - } orelse return error.NoDeviceFound; - sio_device = switch (config.mode.?) { - .input => self.handle.getInputDevice(id), - .output => self.handle.getOutputDevice(id), - } orelse return error.DeviceUnavailable; - } + const id = switch (config.mode.?) { + .input => self.handle.defaultInputDeviceIndex(), + .output => self.handle.defaultOutputDeviceIndex(), + } orelse return error.NoDeviceFound; + sio_device = switch (config.mode.?) { + .input => self.handle.getInputDevice(id), + .output => self.handle.getOutputDevice(id), + } orelse return error.DeviceUnavailable; + } - break :blk switch (config.mode.?) { - .input => SoundIoStream{ .input = try sio_device.createInStream() }, - .output => SoundIoStream{ .output = try sio_device.createOutStream() }, - }; - }, + const handle = switch (config.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, + } + var descriptor = config; + descriptor.mode = descriptor.mode orelse .output; + descriptor.channels = @intCast(u8, switch (handle) { + .input => |d| d.layout().channelCount(), + .output => |d| d.layout().channelCount(), + }); + descriptor.sample_rate = @intCast(u32, switch (handle) { + .input => |d| d.sampleRate(), + .output => |d| d.sampleRate(), + }); + std.log.info("channels {}", .{descriptor.channels.?}); + std.log.info("sample_rate {}\n", .{descriptor.sample_rate.?}); + + device.* = .{ + .descriptor = descriptor, + .handle = handle, + }; + return device; } pub fn outputDeviceIterator(self: Audio) DeviceIterator {