diff --git a/examples/sysaudio/main.zig b/examples/sysaudio/main.zig index 471a4b0f..af3fedef 100644 --- a/examples/sysaudio/main.zig +++ b/examples/sysaudio/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const mach = @import("mach"); const sysaudio = mach.sysaudio; const js = mach.sysjs; +const builtin = @import("builtin"); pub const App = @This(); @@ -28,7 +29,7 @@ fn callback(device: *sysaudio.Device, user_data: ?*anyopaque, buffer: []u8) void const app: *App = @ptrCast(*App, @alignCast(@alignOf(App), user_data)); // Where the magic happens: fill our audio buffer with PCM dat. - app.tone_engine.render(device.descriptor, buffer); + app.tone_engine.render(device.properties, buffer); } pub fn deinit(app: *App, core: *mach.Core) void { @@ -41,11 +42,16 @@ pub fn update(app: *App, engine: *mach.Core) !void { switch (event) { .key_press => |ev| { try app.device.start(); - app.tone_engine.play(ToneEngine.keyToFrequency(ev.key)); + app.tone_engine.play(app.device.properties, ToneEngine.keyToFrequency(ev.key)); }, else => {}, } } + + const back_buffer_view = engine.swap_chain.?.getCurrentTextureView(); + + engine.swap_chain.?.present(); + back_buffer_view.release(); } // A simple tone engine. @@ -69,12 +75,31 @@ pub const ToneEngine = struct { duration: usize, }; - pub fn render(engine: *ToneEngine, descriptor: sysaudio.DeviceDescriptor, buffer: []u8) void { - // TODO(sysaudio): demonstrate how to properly handle format of the buffer here. - // Right now we blindly assume f32 format, which is wrong (but always right in WASM.) - const sample_rate = @intToFloat(f32, descriptor.sample_rate.?); - const buf = @ptrCast([*]f32, @alignCast(@alignOf(f32), buffer.ptr))[0 .. buffer.len / @sizeOf(f32)]; - const frames = buf.len / descriptor.channels.?; + pub fn render(engine: *ToneEngine, properties: sysaudio.Device.Properties, buffer: []u8) void { + switch (properties.format) { + .U8 => renderWithType(u8, engine, properties, buffer), + .S16 => { + const buf = @ptrCast([*]i16, @alignCast(@alignOf(i16), buffer.ptr))[0 .. buffer.len / @sizeOf(i16)]; + renderWithType(i16, engine, properties, buf); + }, + .S24 => { + const buf = @ptrCast([*]i24, @alignCast(@alignOf(i24), buffer.ptr))[0 .. buffer.len / @sizeOf(i24)]; + renderWithType(i24, engine, properties, buf); + }, + .S32 => { + const buf = @ptrCast([*]i32, @alignCast(@alignOf(i32), buffer.ptr))[0 .. buffer.len / @sizeOf(i32)]; + renderWithType(i32, engine, properties, buf); + }, + .F32 => { + const buf = @ptrCast([*]f32, @alignCast(@alignOf(f32), buffer.ptr))[0 .. buffer.len / @sizeOf(f32)]; + renderWithType(f32, engine, properties, buf); + }, + } + } + + pub fn renderWithType(comptime T: type, engine: *ToneEngine, properties: sysaudio.Device.Properties, buffer: []T) void { + const sample_rate = @intToFloat(f32, properties.sample_rate); + const frames = buffer.len / properties.channels; var frame: usize = 0; while (frame < frames) : (frame += 1) { @@ -89,7 +114,8 @@ pub const ToneEngine = struct { const duration = @intToFloat(f32, tone.duration); // The sine wave that plays the frequency. - const sine_wave = std.math.sin(tone.frequency * 2.0 * std.math.pi * sample_counter / sample_rate); + const gain = 0.1; + const sine_wave = std.math.sin(tone.frequency * 2.0 * std.math.pi * sample_counter / sample_rate) * gain; // A number ranging from 0.0 to 1.0 in the first 1/64th of the duration of the tone. const fade_in = std.math.min(sample_counter / (duration / 64.0), 1.0); @@ -104,25 +130,32 @@ pub const ToneEngine = struct { sample += sine_wave * fade_in * fade_out; } + const sample_t: T = sample: { + switch (T) { + f32 => break :sample sample, + u8 => break :sample @floatToInt(u8, (sample + 1.0) * 255), + else => break :sample @floatToInt(T, sample * std.math.maxInt(T)), + } + }; + // Emit the sample on all channels. var channel: usize = 0; - while (channel < descriptor.channels.?) : (channel += 1) { - var channel_buf = buf[channel * frames .. (channel + 1) * frames]; - channel_buf[frame] = sample; + while (channel < properties.channels) : (channel += 1) { + var channel_buffer = buffer[channel * frames .. (channel + 1) * frames]; + channel_buffer[frame] = sample_t; } } } - pub fn play(engine: *ToneEngine, frequency: f32) void { - // TODO(sysaudio): get from device - const sample_rate = 44100.0; + pub fn play(engine: *ToneEngine, properties: sysaudio.Device.Properties, frequency: f32) void { + const sample_rate = @intToFloat(f32, properties.sample_rate); for (engine.playing) |*tone| { if (tone.sample_counter >= tone.duration) { tone.* = Tone{ .frequency = frequency, .sample_counter = 0, - .duration = 1.5 * sample_rate, // play the tone for 1.5s + .duration = @floatToInt(usize, 1.5 * sample_rate), // play the tone for 1.5s }; return; } diff --git a/libs/sysaudio/src/main.zig b/libs/sysaudio/src/main.zig index 913792e5..85578500 100644 --- a/libs/sysaudio/src/main.zig +++ b/libs/sysaudio/src/main.zig @@ -27,8 +27,8 @@ pub const Format = enum { F32, }; -pub const DeviceDescriptor = struct { - mode: ?Mode = null, +pub const DeviceOptions = struct { + mode: Mode = .output, format: ?Format = null, is_raw: ?bool = null, channels: ?u8 = null, @@ -37,6 +37,28 @@ pub const DeviceDescriptor = struct { name: ?[]const u8 = null, }; +pub const DeviceProperties = struct { + mode: Mode, + format: Format, + is_raw: bool, + channels: u8, + sample_rate: u32, + id: [:0]const u8, + name: []const u8, + + pub fn intoConfig(properties: DeviceProperties) DeviceOptions { + return .{ + .mode = properties.mode, + .format = properties.format, + .is_raw = properties.is_raw, + .channels = properties.channels, + .sample_rate = properties.sample_rate, + .id = properties.id, + .name = properties.name, + }; + } +}; + const Audio = @This(); backend: Backend, @@ -55,7 +77,7 @@ pub fn waitEvents(self: Audio) void { self.backend.waitEvents(); } -pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDescriptor) Error!*Device { +pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: Device.Options) Error!*Device { return self.backend.requestDevice(allocator, config); } @@ -88,9 +110,9 @@ test "connect to device from descriptor" { defer a.deinit(); var iter = a.outputDeviceIterator(); - var device_desc = (try iter.next()) orelse return error.NoDeviceFound; + var device_conf = (try iter.next()) orelse return error.NoDeviceFound; - const d = try a.requestDevice(std.testing.allocator, device_desc); + const d = try a.requestDevice(std.testing.allocator, device_conf); defer d.deinit(std.testing.allocator); } @@ -99,29 +121,14 @@ test "requestDevice behavior: null is_raw" { defer a.deinit(); var iter = a.outputDeviceIterator(); - var device_desc = (try iter.next()) orelse return error.NoDeviceFound; + var device_conf = (try iter.next()) orelse return error.NoDeviceFound; - const bad_desc = DeviceDescriptor{ + const bad_conf = Device.Options{ .is_raw = null, - .mode = device_desc.mode, - .id = device_desc.id, + .mode = device_conf.mode, + .id = device_conf.id, }; - try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_desc)); -} - -test "requestDevice behavior: null mode" { - const a = try init(); - defer a.deinit(); - - var iter = a.outputDeviceIterator(); - var device_desc = (try iter.next()) orelse return error.NoDeviceFound; - - const bad_desc = DeviceDescriptor{ - .is_raw = device_desc.is_raw, - .mode = null, - .id = device_desc.id, - }; - try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_desc)); + try testing.expectError(error.InvalidParameter, a.requestDevice(std.testing.allocator, bad_conf)); } test "requestDevice behavior: invalid id" { @@ -130,12 +137,12 @@ test "requestDevice behavior: invalid id" { // defer a.deinit(); // var iter = a.outputDeviceIterator(); - // var device_desc = (try iter.next()) orelse return error.NoDeviceFound; + // var device_conf = (try iter.next()) orelse return error.NoDeviceFound; - // const bad_desc = DeviceDescriptor{ - // .is_raw = device_desc.is_raw, - // .mode = device_desc.mode, + // 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_desc)); + // try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf)); } diff --git a/libs/sysaudio/src/soundio.zig b/libs/sysaudio/src/soundio.zig index c61fea6d..5dedd2be 100644 --- a/libs/sysaudio/src/soundio.zig +++ b/libs/sysaudio/src/soundio.zig @@ -1,9 +1,12 @@ const std = @import("std"); const Mode = @import("main.zig").Mode; -const DeviceDescriptor = @import("main.zig").DeviceDescriptor; +const DeviceOptions = @import("main.zig").DeviceOptions; +const DeviceProperties = @import("main.zig").DeviceProperties; +const Format = @import("main.zig").Format; 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; @@ -21,13 +24,17 @@ else *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; pub const Device = struct { - descriptor: DeviceDescriptor, + 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 = DeviceOptions; + pub const Properties = DeviceProperties; pub fn deinit(self: *Device, allocator: std.mem.Allocator) void { switch (self.handle) { @@ -111,6 +118,7 @@ pub const Device = struct { } 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), @@ -124,15 +132,28 @@ pub const Device = struct { pub fn start(device: *Device) Error!void { // TODO(sysaudio): after pause, may need to call d.pause(false) instead of d.start()? - 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)), + 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)), + }; + }; + } } }; @@ -142,14 +163,14 @@ pub const DeviceIterator = struct { device_len: u16, index: u16, - pub fn next(self: *DeviceIterator) IteratorError!?DeviceDescriptor { + pub fn next(self: *DeviceIterator) IteratorError!?DeviceOptions { 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 DeviceDescriptor{ + return DeviceOptions{ .mode = switch (@intToEnum(Aim, device_desc.handle.aim)) { .input => .input, .output => .output, @@ -217,18 +238,18 @@ pub fn waitEvents(self: Audio) void { self.handle.waitEvents(); } -pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDescriptor) Error!*Device { +pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, options: DeviceOptions) Error!*Device { var sio_device: SoundIoDevice = undefined; - if (config.id) |id| { - if (config.mode == null or config.is_raw == null) + if (options.id) |id| { + if (options.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.?), + 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 (config.mode.?) { + return if (switch (options.mode) { .input => self.handle.inputDeviceCount().?, .output => self.handle.outputDeviceCount().?, } == 0) @@ -237,19 +258,17 @@ pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDe error.DeviceUnavailable; }; } else { - if (config.mode == null) return error.InvalidParameter; - - const id = switch (config.mode.?) { + const id = switch (options.mode) { .input => self.handle.defaultInputDeviceIndex(), .output => self.handle.defaultOutputDeviceIndex(), } orelse return error.NoDeviceFound; - sio_device = switch (config.mode.?) { + sio_device = switch (options.mode) { .input => self.handle.getInputDevice(id), .output => self.handle.getOutputDevice(id), } orelse return error.DeviceUnavailable; } - const handle = switch (config.mode.?) { + const handle = switch (options.mode) { .input => SoundIoStream{ .input = try sio_device.createInStream() }, .output => SoundIoStream{ .output = try sio_device.createOutStream() }, }; @@ -264,21 +283,52 @@ pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDe .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.?}); + + // 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 = DeviceProperties{ + .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.* = .{ - .descriptor = descriptor, + .properties = properties, .handle = handle, }; return device; diff --git a/libs/sysaudio/src/webaudio.zig b/libs/sysaudio/src/webaudio.zig index 27979a86..956c928d 100644 --- a/libs/sysaudio/src/webaudio.zig +++ b/libs/sysaudio/src/webaudio.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Mode = @import("main.zig").Mode; -const DeviceDescriptor = @import("main.zig").DeviceDescriptor; +const DeviceOptions = @import("main.zig").DeviceOptions; +const DeviceProperties = @import("main.zig").DeviceProperties; const js = @import("sysjs"); const Audio = @This(); @@ -11,11 +12,14 @@ else *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; pub const Device = struct { - descriptor: DeviceDescriptor, + properties: DeviceProperties, // Internal fields. context: js.Object, + pub const Options = DeviceOptions; + pub const Properties = DeviceProperties; + pub fn deinit(device: *Device, allocator: std.mem.Allocator) void { device.context.deinit(); allocator.destroy(device); @@ -41,7 +45,7 @@ pub const DeviceIterator = struct { ctx: *Audio, mode: Mode, - pub fn next(_: DeviceIterator) IteratorError!?DeviceDescriptor { + pub fn next(_: DeviceIterator) IteratorError!?DeviceProperties { return null; } }; @@ -67,18 +71,18 @@ pub fn deinit(audio: Audio) void { audio.context_constructor.deinit(); } -// TODO)sysaudio): implement waitEvents for WebAudio, will a WASM process terminate without this? +// 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, config: DeviceDescriptor) Error!*Device { - // NOTE: WebAudio only supports F32 audio format, so config.format is unused - const mode = config.mode orelse .output; - const channels = config.channels orelse default_channel_count; - const sample_rate = config.sample_rate orelse default_sample_rate; +pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, options: DeviceOptions) 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(); @@ -113,15 +117,16 @@ pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, config: DeviceD _ = node.call("connect", &.{destination.toValue()}); } - // TODO(sysaudio): introduce a descriptor type that has non-optional fields. - var descriptor = config; - descriptor.mode = descriptor.mode orelse .output; - descriptor.channels = descriptor.channels orelse default_channel_count; - descriptor.sample_rate = descriptor.sample_rate orelse default_sample_rate; + var properties = DeviceProperties { + .format = .F32, + .mode = options.mode orelse .output, + .channels = options.channels orelse default_channel_count, + .sample_rate = options.sample_rate orelse default_sample_rate, + }; const device = try allocator.create(Device); device.* = .{ - .descriptor = descriptor, + .properties = properties, .context = context, }; return device;