The offset into the planar_buffer was not being stored, so any time multiple iterations occured the beginning of the wave was copied again, instead of correctly copying the next part of the planar_buffer.
355 lines
13 KiB
Zig
355 lines
13 KiB
Zig
const std = @import("std");
|
|
const Mode = @import("main.zig").Mode;
|
|
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;
|
|
|
|
const SoundIoStream = union(Mode) {
|
|
input: SoundIoInStream,
|
|
output: SoundIoOutStream,
|
|
};
|
|
|
|
const Audio = @This();
|
|
|
|
pub const DataCallback = if (@import("builtin").zig_backend == .stage1)
|
|
fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void
|
|
else
|
|
*const fn (device: *Device, user_data: ?*anyopaque, buffer: []u8) void;
|
|
|
|
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 = DeviceOptions;
|
|
pub const Properties = DeviceProperties;
|
|
|
|
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..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!?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 DeviceOptions{
|
|
.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: DeviceOptions) 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 = 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.* = .{
|
|
.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,
|
|
};
|
|
}
|