sysaudio: rewrite in zig
removes libsoundio dependency
This commit is contained in:
parent
8aa2c97079
commit
0f3e28bc2a
27 changed files with 4714 additions and 1344 deletions
|
|
@ -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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue