449 lines
14 KiB
Zig
449 lines
14 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const main = @import("main.zig");
|
|
const backends = @import("backends.zig");
|
|
const util = @import("util.zig");
|
|
// const c = @import("cimport.zig");
|
|
const c = @cImport({
|
|
@cInclude("CoreAudio/CoreAudio.h");
|
|
@cInclude("AudioUnit/AudioUnit.h");
|
|
});
|
|
const native_endian = builtin.cpu.arch.endian();
|
|
var is_darling = false;
|
|
|
|
pub const Context = struct {
|
|
allocator: std.mem.Allocator,
|
|
devices_info: util.DevicesInfo,
|
|
|
|
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
_ = options;
|
|
|
|
if (std.fs.accessAbsolute("/usr/lib/darling", .{})) {
|
|
is_darling = true;
|
|
} else |_| {}
|
|
|
|
var self = try allocator.create(Context);
|
|
errdefer allocator.destroy(self);
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.devices_info = util.DevicesInfo.init(),
|
|
};
|
|
|
|
return .{ .coreaudio = self };
|
|
}
|
|
|
|
pub fn deinit(self: *Context) void {
|
|
for (self.devices_info.list.items) |d|
|
|
freeDevice(self.allocator, d);
|
|
self.devices_info.list.deinit(self.allocator);
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
pub fn refresh(self: *Context) !void {
|
|
for (self.devices_info.list.items) |d|
|
|
freeDevice(self.allocator, d);
|
|
self.devices_info.clear(self.allocator);
|
|
|
|
var prop_address = c.AudioObjectPropertyAddress{
|
|
.mSelector = c.kAudioHardwarePropertyDevices,
|
|
.mScope = c.kAudioObjectPropertyScopeGlobal,
|
|
.mElement = c.kAudioObjectPropertyElementMaster,
|
|
};
|
|
|
|
var io_size: u32 = 0;
|
|
if (c.AudioObjectGetPropertyDataSize(
|
|
c.kAudioObjectSystemObject,
|
|
&prop_address,
|
|
0,
|
|
null,
|
|
&io_size,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
const devices_count = io_size / @sizeOf(c.AudioObjectID);
|
|
if (devices_count == 0) return;
|
|
|
|
var devs = try self.allocator.alloc(c.AudioObjectID, devices_count);
|
|
defer self.allocator.free(devs);
|
|
if (c.AudioObjectGetPropertyData(
|
|
c.kAudioObjectSystemObject,
|
|
&prop_address,
|
|
0,
|
|
null,
|
|
&io_size,
|
|
@ptrCast(*anyopaque, devs),
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
var default_input_id: c.AudioObjectID = undefined;
|
|
var default_output_id: c.AudioObjectID = undefined;
|
|
|
|
io_size = @sizeOf(c.AudioObjectID);
|
|
if (c.AudioHardwareGetProperty(
|
|
c.kAudioHardwarePropertyDefaultInputDevice,
|
|
&io_size,
|
|
&default_input_id,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
io_size = @sizeOf(c.AudioObjectID);
|
|
if (c.AudioHardwareGetProperty(
|
|
c.kAudioHardwarePropertyDefaultOutputDevice,
|
|
&io_size,
|
|
&default_output_id,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
for (devs) |id| {
|
|
var buf_list: *c.AudioBufferList = undefined;
|
|
defer self.allocator.destroy(buf_list);
|
|
var mode: main.Device.Mode = undefined;
|
|
for (std.meta.tags(main.Device.Mode)) |m| {
|
|
mode = m;
|
|
|
|
io_size = 0;
|
|
prop_address.mSelector = c.kAudioDevicePropertyStreamConfiguration;
|
|
prop_address.mScope = switch (mode) {
|
|
.playback => c.kAudioObjectPropertyScopeOutput,
|
|
.capture => c.kAudioObjectPropertyScopeInput,
|
|
};
|
|
if (c.AudioObjectGetPropertyDataSize(
|
|
id,
|
|
&prop_address,
|
|
0,
|
|
null,
|
|
&io_size,
|
|
) != c.noErr) {
|
|
continue;
|
|
}
|
|
|
|
buf_list = try self.allocator.create(c.AudioBufferList);
|
|
if (c.AudioObjectGetPropertyData(
|
|
id,
|
|
&prop_address,
|
|
0,
|
|
null,
|
|
&io_size,
|
|
@ptrCast(*anyopaque, buf_list),
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
if (buf_list.mBuffers[0].mNumberChannels > 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// for now, only set `channels` to the output channels
|
|
const audio_buffer_list_property_address = c.AudioObjectPropertyAddress{
|
|
.mSelector = c.kAudioDevicePropertyStreamConfiguration,
|
|
.mScope = c.kAudioDevicePropertyScopeOutput,
|
|
.mElement = c.kAudioObjectPropertyElementMain,
|
|
};
|
|
var output_audio_buffer_list: c.AudioBufferList = undefined;
|
|
var audio_buffer_list_size: c_uint = undefined;
|
|
|
|
if (c.AudioObjectGetPropertyDataSize(id, &audio_buffer_list_property_address, 0, null, &audio_buffer_list_size) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
if (c.AudioObjectGetPropertyData(id, &prop_address, 0, null, &audio_buffer_list_size, &output_audio_buffer_list) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
var output_channel_count: usize = 0;
|
|
for (0..output_audio_buffer_list.mNumberBuffers) |mBufferIndex| {
|
|
output_channel_count += output_audio_buffer_list.mBuffers[mBufferIndex].mNumberChannels;
|
|
}
|
|
|
|
var channels = try self.allocator.alloc(main.Channel, output_channel_count);
|
|
|
|
prop_address.mSelector = c.kAudioDevicePropertyNominalSampleRate;
|
|
io_size = @sizeOf(f64);
|
|
var sample_rate: f64 = undefined;
|
|
if (c.AudioObjectGetPropertyData(
|
|
id,
|
|
&prop_address,
|
|
0,
|
|
null,
|
|
&io_size,
|
|
&sample_rate,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
io_size = @sizeOf([*]const u8);
|
|
if (c.AudioDeviceGetPropertyInfo(
|
|
id,
|
|
0,
|
|
0,
|
|
c.kAudioDevicePropertyDeviceName,
|
|
&io_size,
|
|
null,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
const name = try self.allocator.allocSentinel(u8, io_size, 0);
|
|
errdefer self.allocator.free(name);
|
|
if (c.AudioDeviceGetProperty(
|
|
id,
|
|
0,
|
|
0,
|
|
c.kAudioDevicePropertyDeviceName,
|
|
&io_size,
|
|
name.ptr,
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
const id_str = try std.fmt.allocPrintZ(self.allocator, "{d}", .{id});
|
|
errdefer self.allocator.free(id_str);
|
|
|
|
var dev = main.Device{
|
|
.id = id_str,
|
|
.name = name,
|
|
.mode = mode,
|
|
.channels = channels,
|
|
.formats = &.{ .i16, .i32, .f32 },
|
|
.sample_rate = .{
|
|
.min = @floatToInt(u24, @floor(sample_rate)),
|
|
.max = @floatToInt(u24, @floor(sample_rate)),
|
|
},
|
|
};
|
|
|
|
try self.devices_info.list.append(self.allocator, dev);
|
|
if (id == default_output_id) {
|
|
self.devices_info.default_output = self.devices_info.list.items.len - 1;
|
|
}
|
|
|
|
if (id == default_input_id) {
|
|
self.devices_info.default_input = self.devices_info.list.items.len - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn devices(self: Context) []const main.Device {
|
|
return self.devices_info.list.items;
|
|
}
|
|
|
|
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
return self.devices_info.default(mode);
|
|
}
|
|
|
|
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
var player = try self.allocator.create(Player);
|
|
|
|
var component_desc = c.AudioComponentDescription{
|
|
.componentType = c.kAudioUnitType_Output,
|
|
.componentSubType = c.kAudioUnitSubType_HALOutput,
|
|
.componentManufacturer = c.kAudioUnitManufacturer_Apple,
|
|
.componentFlags = 0,
|
|
.componentFlagsMask = 0,
|
|
};
|
|
const component = c.AudioComponentFindNext(null, &component_desc);
|
|
if (component == null) return error.OpeningDevice;
|
|
|
|
var audio_unit: c.AudioComponentInstance = undefined;
|
|
if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice;
|
|
|
|
if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice;
|
|
errdefer _ = c.AudioUnitUninitialize(audio_unit);
|
|
|
|
const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch unreachable;
|
|
if (c.AudioUnitSetProperty(
|
|
audio_unit,
|
|
c.kAudioOutputUnitProperty_CurrentDevice,
|
|
c.kAudioUnitScope_Input,
|
|
0,
|
|
&device_id,
|
|
@sizeOf(c.AudioDeviceID),
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
const stream_desc = try createStreamDesc(options.format, options.sample_rate, device.channels.len);
|
|
if (c.AudioUnitSetProperty(
|
|
audio_unit,
|
|
c.kAudioUnitProperty_StreamFormat,
|
|
c.kAudioUnitScope_Input,
|
|
0,
|
|
&stream_desc,
|
|
@sizeOf(c.AudioStreamBasicDescription),
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
const render_callback = c.AURenderCallbackStruct{
|
|
.inputProc = Player.renderCallback,
|
|
.inputProcRefCon = player,
|
|
};
|
|
if (c.AudioUnitSetProperty(
|
|
audio_unit,
|
|
c.kAudioUnitProperty_SetRenderCallback,
|
|
c.kAudioUnitScope_Input,
|
|
0,
|
|
&render_callback,
|
|
@sizeOf(c.AURenderCallbackStruct),
|
|
) != c.noErr) {
|
|
return error.OpeningDevice;
|
|
}
|
|
|
|
player.* = .{
|
|
.allocator = self.allocator,
|
|
.audio_unit = audio_unit.?,
|
|
.is_paused = false,
|
|
.vol = 1.0,
|
|
.writeFn = writeFn,
|
|
.user_data = options.user_data,
|
|
.channels = device.channels,
|
|
.format = options.format,
|
|
.sample_rate = options.sample_rate,
|
|
.write_step = options.format.frameSize(device.channels.len),
|
|
};
|
|
return .{ .coreaudio = player };
|
|
}
|
|
};
|
|
|
|
pub const Player = struct {
|
|
allocator: std.mem.Allocator,
|
|
audio_unit: c.AudioUnit,
|
|
is_paused: bool,
|
|
vol: f32,
|
|
writeFn: main.WriteFn,
|
|
user_data: ?*anyopaque,
|
|
|
|
channels: []main.Channel,
|
|
format: main.Format,
|
|
sample_rate: u24,
|
|
write_step: u8,
|
|
|
|
pub fn renderCallback(
|
|
self_opaque: ?*anyopaque,
|
|
action_flags: [*c]c.AudioUnitRenderActionFlags,
|
|
time_stamp: [*c]const c.AudioTimeStamp,
|
|
bus_number: u32,
|
|
frames_left: u32,
|
|
buf: [*c]c.AudioBufferList,
|
|
) callconv(.C) c.OSStatus {
|
|
_ = action_flags;
|
|
_ = time_stamp;
|
|
_ = bus_number;
|
|
_ = frames_left;
|
|
|
|
const self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?));
|
|
|
|
for (self.channels, 0..) |*ch, i| {
|
|
ch.ptr = @ptrCast([*]u8, buf.*.mBuffers[0].mData.?) + self.format.frameSize(i);
|
|
}
|
|
const frames = buf.*.mBuffers[0].mDataByteSize / self.format.frameSize(self.channels.len);
|
|
self.writeFn(self.user_data, frames);
|
|
|
|
return c.noErr;
|
|
}
|
|
|
|
pub fn deinit(self: *Player) void {
|
|
_ = c.AudioOutputUnitStop(self.audio_unit);
|
|
_ = c.AudioUnitUninitialize(self.audio_unit);
|
|
_ = c.AudioComponentInstanceDispose(self.audio_unit);
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
pub fn start(self: *Player) !void {
|
|
return self.play();
|
|
}
|
|
|
|
pub fn play(self: *Player) !void {
|
|
if (c.AudioOutputUnitStart(self.audio_unit) != c.noErr) {
|
|
return error.CannotPlay;
|
|
}
|
|
self.is_paused = false;
|
|
}
|
|
|
|
pub fn pause(self: *Player) !void {
|
|
if (c.AudioOutputUnitStop(self.audio_unit) != c.noErr) {
|
|
return error.CannotPause;
|
|
}
|
|
self.is_paused = true;
|
|
}
|
|
|
|
pub fn paused(self: Player) bool {
|
|
return self.is_paused;
|
|
}
|
|
|
|
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
if (c.AudioUnitSetParameter(
|
|
self.audio_unit,
|
|
c.kHALOutputParam_Volume,
|
|
c.kAudioUnitScope_Global,
|
|
0,
|
|
vol,
|
|
0,
|
|
) != c.noErr) {
|
|
if (is_darling) return;
|
|
return error.CannotSetVolume;
|
|
}
|
|
}
|
|
|
|
pub fn volume(self: Player) !f32 {
|
|
var vol: f32 = 0;
|
|
if (c.AudioUnitGetParameter(
|
|
self.audio_unit,
|
|
c.kHALOutputParam_Volume,
|
|
c.kAudioUnitScope_Global,
|
|
0,
|
|
&vol,
|
|
) != c.noErr) {
|
|
if (is_darling) return 1;
|
|
return error.CannotGetVolume;
|
|
}
|
|
return vol;
|
|
}
|
|
};
|
|
|
|
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
allocator.free(device.id);
|
|
allocator.free(device.name);
|
|
allocator.free(device.channels);
|
|
}
|
|
|
|
fn createStreamDesc(format: main.Format, sample_rate: u24, ch_count: usize) !c.AudioStreamBasicDescription {
|
|
var desc = c.AudioStreamBasicDescription{
|
|
.mSampleRate = @intToFloat(f64, sample_rate),
|
|
.mFormatID = c.kAudioFormatLinearPCM,
|
|
.mFormatFlags = switch (format) {
|
|
.i16 => c.kAudioFormatFlagIsSignedInteger,
|
|
.i24 => c.kAudioFormatFlagIsSignedInteger,
|
|
.i32 => c.kAudioFormatFlagIsSignedInteger,
|
|
.f32 => c.kAudioFormatFlagIsFloat,
|
|
.u8 => return error.IncompatibleDevice,
|
|
.i24_4b => return error.IncompatibleDevice,
|
|
},
|
|
.mBytesPerPacket = format.frameSize(ch_count),
|
|
.mFramesPerPacket = 1,
|
|
.mBytesPerFrame = format.frameSize(ch_count),
|
|
.mChannelsPerFrame = @intCast(c_uint, ch_count),
|
|
.mBitsPerChannel = switch (format) {
|
|
.i16 => 16,
|
|
.i24 => 24,
|
|
.i32 => 32,
|
|
.f32 => 32,
|
|
.u8 => unreachable,
|
|
.i24_4b => unreachable,
|
|
},
|
|
.mReserved = 0,
|
|
};
|
|
|
|
if (native_endian == .Big) {
|
|
desc.mFormatFlags |= c.kAudioFormatFlagIsBigEndian;
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
|
|
test {
|
|
std.testing.refAllDeclsRecursive(@This());
|
|
}
|