{sysaudio,examples}: get sysaudio running on linux, separate audio configuration and descriptor (#518)

* Get sysaudio working on linux
* Separate audio configuration and descriptor
* Config/Descriptor -> Options/Properties
- Rename sysaudio DeviceConfig and DeviceDescriptor to Device.Options and Device.Properties
- example: Convert buffer before passing to renderWithType
* make Device.start() idempotent
This commit is contained in:
Louis Pearson 2022-09-09 09:58:03 -06:00 committed by GitHub
parent f807c85232
commit 0e71daf504
Failed to generate hash of commit
4 changed files with 193 additions and 98 deletions

View file

@ -2,6 +2,7 @@ const std = @import("std");
const mach = @import("mach"); const mach = @import("mach");
const sysaudio = mach.sysaudio; const sysaudio = mach.sysaudio;
const js = mach.sysjs; const js = mach.sysjs;
const builtin = @import("builtin");
pub const App = @This(); 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)); const app: *App = @ptrCast(*App, @alignCast(@alignOf(App), user_data));
// Where the magic happens: fill our audio buffer with PCM dat. // 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 { pub fn deinit(app: *App, core: *mach.Core) void {
@ -41,11 +42,16 @@ pub fn update(app: *App, engine: *mach.Core) !void {
switch (event) { switch (event) {
.key_press => |ev| { .key_press => |ev| {
try app.device.start(); try app.device.start();
app.tone_engine.play(ToneEngine.keyToFrequency(ev.key)); app.tone_engine.play(app.device.properties, ToneEngine.keyToFrequency(ev.key));
}, },
else => {}, else => {},
} }
} }
const back_buffer_view = engine.swap_chain.?.getCurrentTextureView();
engine.swap_chain.?.present();
back_buffer_view.release();
} }
// A simple tone engine. // A simple tone engine.
@ -69,12 +75,31 @@ pub const ToneEngine = struct {
duration: usize, duration: usize,
}; };
pub fn render(engine: *ToneEngine, descriptor: sysaudio.DeviceDescriptor, buffer: []u8) void { pub fn render(engine: *ToneEngine, properties: sysaudio.Device.Properties, buffer: []u8) void {
// TODO(sysaudio): demonstrate how to properly handle format of the buffer here. switch (properties.format) {
// Right now we blindly assume f32 format, which is wrong (but always right in WASM.) .U8 => renderWithType(u8, engine, properties, buffer),
const sample_rate = @intToFloat(f32, descriptor.sample_rate.?); .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)]; const buf = @ptrCast([*]f32, @alignCast(@alignOf(f32), buffer.ptr))[0 .. buffer.len / @sizeOf(f32)];
const frames = buf.len / descriptor.channels.?; 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; var frame: usize = 0;
while (frame < frames) : (frame += 1) { while (frame < frames) : (frame += 1) {
@ -89,7 +114,8 @@ pub const ToneEngine = struct {
const duration = @intToFloat(f32, tone.duration); const duration = @intToFloat(f32, tone.duration);
// The sine wave that plays the frequency. // 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. // 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); 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; 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. // Emit the sample on all channels.
var channel: usize = 0; var channel: usize = 0;
while (channel < descriptor.channels.?) : (channel += 1) { while (channel < properties.channels) : (channel += 1) {
var channel_buf = buf[channel * frames .. (channel + 1) * frames]; var channel_buffer = buffer[channel * frames .. (channel + 1) * frames];
channel_buf[frame] = sample; channel_buffer[frame] = sample_t;
} }
} }
} }
pub fn play(engine: *ToneEngine, frequency: f32) void { pub fn play(engine: *ToneEngine, properties: sysaudio.Device.Properties, frequency: f32) void {
// TODO(sysaudio): get from device const sample_rate = @intToFloat(f32, properties.sample_rate);
const sample_rate = 44100.0;
for (engine.playing) |*tone| { for (engine.playing) |*tone| {
if (tone.sample_counter >= tone.duration) { if (tone.sample_counter >= tone.duration) {
tone.* = Tone{ tone.* = Tone{
.frequency = frequency, .frequency = frequency,
.sample_counter = 0, .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; return;
} }

View file

@ -27,8 +27,8 @@ pub const Format = enum {
F32, F32,
}; };
pub const DeviceDescriptor = struct { pub const DeviceOptions = struct {
mode: ?Mode = null, mode: Mode = .output,
format: ?Format = null, format: ?Format = null,
is_raw: ?bool = null, is_raw: ?bool = null,
channels: ?u8 = null, channels: ?u8 = null,
@ -37,6 +37,28 @@ pub const DeviceDescriptor = struct {
name: ?[]const u8 = null, 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(); const Audio = @This();
backend: Backend, backend: Backend,
@ -55,7 +77,7 @@ pub fn waitEvents(self: Audio) void {
self.backend.waitEvents(); 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); return self.backend.requestDevice(allocator, config);
} }
@ -88,9 +110,9 @@ test "connect to device from descriptor" {
defer a.deinit(); defer a.deinit();
var iter = a.outputDeviceIterator(); 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); defer d.deinit(std.testing.allocator);
} }
@ -99,29 +121,14 @@ test "requestDevice behavior: null is_raw" {
defer a.deinit(); defer a.deinit();
var iter = a.outputDeviceIterator(); 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, .is_raw = null,
.mode = device_desc.mode, .mode = device_conf.mode,
.id = device_desc.id, .id = device_conf.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: 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));
} }
test "requestDevice behavior: invalid id" { test "requestDevice behavior: invalid id" {
@ -130,12 +137,12 @@ test "requestDevice behavior: invalid id" {
// defer a.deinit(); // defer a.deinit();
// var iter = a.outputDeviceIterator(); // 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 = device_desc.is_raw, // .is_raw = device_conf.is_raw,
// .mode = device_desc.mode, // .mode = device_conf.mode,
// .id = "wrong-id", // .id = "wrong-id",
// }; // };
// try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_desc)); // try testing.expectError(error.DeviceUnavailable, a.requestDevice(bad_conf));
} }

View file

@ -1,9 +1,12 @@
const std = @import("std"); const std = @import("std");
const Mode = @import("main.zig").Mode; 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 c = @import("soundio").c;
const Aim = @import("soundio").Aim; const Aim = @import("soundio").Aim;
const SoundIo = @import("soundio").SoundIo; const SoundIo = @import("soundio").SoundIo;
const SoundIoFormat = @import("soundio").Format;
const SoundIoDevice = @import("soundio").Device; const SoundIoDevice = @import("soundio").Device;
const SoundIoInStream = @import("soundio").InStream; const SoundIoInStream = @import("soundio").InStream;
const SoundIoOutStream = @import("soundio").OutStream; const SoundIoOutStream = @import("soundio").OutStream;
@ -21,13 +24,17 @@ else
*const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
pub const Device = struct { pub const Device = struct {
descriptor: DeviceDescriptor, properties: Properties,
// Internal fields. // Internal fields.
handle: SoundIoStream, handle: SoundIoStream,
data_callback: ?DataCallback = null, data_callback: ?DataCallback = null,
user_data: ?*anyopaque = null, user_data: ?*anyopaque = null,
planar_buffer: [512000]u8 = undefined, 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 { pub fn deinit(self: *Device, allocator: std.mem.Allocator) void {
switch (self.handle) { switch (self.handle) {
@ -111,6 +118,7 @@ pub const Device = struct {
} }
pub fn pause(device: *Device) Error!void { pub fn pause(device: *Device) Error!void {
if (!device.started) return;
return (switch (device.handle) { return (switch (device.handle) {
.input => |d| d.pause(true), .input => |d| d.pause(true),
.output => |d| d.pause(true), .output => |d| d.pause(true),
@ -124,6 +132,8 @@ pub const Device = struct {
pub fn start(device: *Device) Error!void { pub fn start(device: *Device) Error!void {
// TODO(sysaudio): after pause, may need to call d.pause(false) instead of d.start()? // 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) { return (switch (device.handle) {
.input => |d| d.start(), .input => |d| d.start(),
.output => |d| d.start(), .output => |d| d.start(),
@ -133,6 +143,17 @@ pub const Device = struct {
else => @panic(@errorName(err)), 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, device_len: u16,
index: u16, index: u16,
pub fn next(self: *DeviceIterator) IteratorError!?DeviceDescriptor { pub fn next(self: *DeviceIterator) IteratorError!?DeviceOptions {
if (self.index < self.device_len) { if (self.index < self.device_len) {
const device_desc = switch (self.mode) { const device_desc = switch (self.mode) {
.input => self.ctx.handle.getInputDevice(self.index) orelse return null, .input => self.ctx.handle.getInputDevice(self.index) orelse return null,
.output => self.ctx.handle.getOutputDevice(self.index) orelse return null, .output => self.ctx.handle.getOutputDevice(self.index) orelse return null,
}; };
self.index += 1; self.index += 1;
return DeviceDescriptor{ return DeviceOptions{
.mode = switch (@intToEnum(Aim, device_desc.handle.aim)) { .mode = switch (@intToEnum(Aim, device_desc.handle.aim)) {
.input => .input, .input => .input,
.output => .output, .output => .output,
@ -217,18 +238,18 @@ pub fn waitEvents(self: Audio) void {
self.handle.waitEvents(); 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; var sio_device: SoundIoDevice = undefined;
if (config.id) |id| { if (options.id) |id| {
if (config.mode == null or config.is_raw == null) if (options.is_raw == null)
return error.InvalidParameter; return error.InvalidParameter;
sio_device = switch (config.mode.?) { sio_device = switch (options.mode) {
.input => self.handle.getInputDeviceFromID(id, config.is_raw.?), .input => self.handle.getInputDeviceFromID(id, options.is_raw.?),
.output => self.handle.getOutputDeviceFromID(id, config.is_raw.?), .output => self.handle.getOutputDeviceFromID(id, options.is_raw.?),
} orelse { } orelse {
return if (switch (config.mode.?) { return if (switch (options.mode) {
.input => self.handle.inputDeviceCount().?, .input => self.handle.inputDeviceCount().?,
.output => self.handle.outputDeviceCount().?, .output => self.handle.outputDeviceCount().?,
} == 0) } == 0)
@ -237,19 +258,17 @@ pub fn requestDevice(self: Audio, allocator: std.mem.Allocator, config: DeviceDe
error.DeviceUnavailable; error.DeviceUnavailable;
}; };
} else { } else {
if (config.mode == null) return error.InvalidParameter; const id = switch (options.mode) {
const id = switch (config.mode.?) {
.input => self.handle.defaultInputDeviceIndex(), .input => self.handle.defaultInputDeviceIndex(),
.output => self.handle.defaultOutputDeviceIndex(), .output => self.handle.defaultOutputDeviceIndex(),
} orelse return error.NoDeviceFound; } orelse return error.NoDeviceFound;
sio_device = switch (config.mode.?) { sio_device = switch (options.mode) {
.input => self.handle.getInputDevice(id), .input => self.handle.getInputDevice(id),
.output => self.handle.getOutputDevice(id), .output => self.handle.getOutputDevice(id),
} orelse return error.DeviceUnavailable; } orelse return error.DeviceUnavailable;
} }
const handle = switch (config.mode.?) { const handle = switch (options.mode) {
.input => SoundIoStream{ .input = try sio_device.createInStream() }, .input => SoundIoStream{ .input = try sio_device.createInStream() },
.output => SoundIoStream{ .output = try sio_device.createOutStream() }, .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, .input => |d| d.handle.userdata = device,
.output => |d| d.handle.userdata = device, .output => |d| d.handle.userdata = device,
} }
var descriptor = config;
descriptor.mode = descriptor.mode orelse .output; // TODO(sysaudio): handle big endian architectures
descriptor.channels = @intCast(u8, switch (handle) { 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(), .input => |d| d.layout().channelCount(),
.output => |d| d.layout().channelCount(), .output => |d| d.layout().channelCount(),
}); }),
descriptor.sample_rate = @intCast(u32, switch (handle) { .sample_rate = @intCast(u32, switch (handle) {
.input => |d| d.sampleRate(), .input => |d| d.sampleRate(),
.output => |d| d.sampleRate(), .output => |d| d.sampleRate(),
}); }),
std.log.info("channels {}", .{descriptor.channels.?}); };
std.log.info("sample_rate {}\n", .{descriptor.sample_rate.?});
device.* = .{ device.* = .{
.descriptor = descriptor, .properties = properties,
.handle = handle, .handle = handle,
}; };
return device; return device;

View file

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const Mode = @import("main.zig").Mode; 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 js = @import("sysjs");
const Audio = @This(); const Audio = @This();
@ -11,11 +12,14 @@ else
*const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void; *const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
pub const Device = struct { pub const Device = struct {
descriptor: DeviceDescriptor, properties: DeviceProperties,
// Internal fields. // Internal fields.
context: js.Object, context: js.Object,
pub const Options = DeviceOptions;
pub const Properties = DeviceProperties;
pub fn deinit(device: *Device, allocator: std.mem.Allocator) void { pub fn deinit(device: *Device, allocator: std.mem.Allocator) void {
device.context.deinit(); device.context.deinit();
allocator.destroy(device); allocator.destroy(device);
@ -41,7 +45,7 @@ pub const DeviceIterator = struct {
ctx: *Audio, ctx: *Audio,
mode: Mode, mode: Mode,
pub fn next(_: DeviceIterator) IteratorError!?DeviceDescriptor { pub fn next(_: DeviceIterator) IteratorError!?DeviceProperties {
return null; return null;
} }
}; };
@ -67,18 +71,18 @@ pub fn deinit(audio: Audio) void {
audio.context_constructor.deinit(); 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 {} pub fn waitEvents(_: Audio) void {}
const default_channel_count = 2; const default_channel_count = 2;
const default_sample_rate = 48000; const default_sample_rate = 48000;
const default_buffer_size_per_channel = 1024; // 21.33ms const default_buffer_size_per_channel = 1024; // 21.33ms
pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, config: DeviceDescriptor) Error!*Device { pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, options: DeviceOptions) Error!*Device {
// NOTE: WebAudio only supports F32 audio format, so config.format is unused // NOTE: WebAudio only supports F32 audio format, so options.format is unused
const mode = config.mode orelse .output; const mode = options.mode;
const channels = config.channels orelse default_channel_count; const channels = options.channels orelse default_channel_count;
const sample_rate = config.sample_rate orelse default_sample_rate; const sample_rate = options.sample_rate orelse default_sample_rate;
const context_options = js.createMap(); const context_options = js.createMap();
defer context_options.deinit(); defer context_options.deinit();
@ -113,15 +117,16 @@ pub fn requestDevice(audio: Audio, allocator: std.mem.Allocator, config: DeviceD
_ = node.call("connect", &.{destination.toValue()}); _ = node.call("connect", &.{destination.toValue()});
} }
// TODO(sysaudio): introduce a descriptor type that has non-optional fields. var properties = DeviceProperties {
var descriptor = config; .format = .F32,
descriptor.mode = descriptor.mode orelse .output; .mode = options.mode orelse .output,
descriptor.channels = descriptor.channels orelse default_channel_count; .channels = options.channels orelse default_channel_count,
descriptor.sample_rate = descriptor.sample_rate orelse default_sample_rate; .sample_rate = options.sample_rate orelse default_sample_rate,
};
const device = try allocator.create(Device); const device = try allocator.create(Device);
device.* = .{ device.* = .{
.descriptor = descriptor, .properties = properties,
.context = context, .context = context,
}; };
return device; return device;