// Zig easy audio library // Copyright (C) 2021-2023, 2025 Nguyễn Gia Phong // // This file is part of zeal. // // Zeal is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Zeal is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with zeal. If not, see . const Allocator = std.mem.Allocator; const SndFile = sf.SndFile; const al = @import("al.zig"); const alc = @import("alc.zig"); const sf = @import("sf.zig"); const std = @import("std"); pub const Error = al.Error || error{ UncurrentContext, }; /// Return version information and error strings. pub fn get(comptime attr: alc.Enum) !attr.getType() { return alc.get(null, attr); } pub const Device = struct { pimpl: *alc.Device, pub fn init(name: ?[:0]const u8) alc.Error!Device { return Device{ .pimpl = try alc.openDevice(name) }; } /// Return infomation about the device. pub fn get(self: Device, comptime attr: alc.Enum) !attr.getType() { return alc.get(self.pimpl, attr); } pub fn deinit(self: Device) alc.Error!void { try alc.closeDevice(self.pimpl); } }; pub const Context = struct { pimpl: *alc.Context, device: Device, pub const Attributes = struct { /// Channel configuration. channels_format: ?alc.ChannelsFormat = null, /// Sample type. sample_type: ?alc.SampleType = null, /// Frequency in hertz. frequency: ?i32 = null, /// Ambisonic format layout. ambisonic_layout: ?alc.AmbisonicLayout = null, /// Ambisonic normalization method. ambisonic_scaling: ?alc.AmbisonicScaling = null, /// Ambisonic order. ambisonic_order: ?i32 = null, /// Number of mono (3D) sources. mono_sources: ?i32 = null, /// Number of stereo sources. stereo_sources: ?i32 = null, /// Maximum number of auxiliary source sends. max_auxiliary_sends: ?i32 = null, /// Enabling HRTF. hrtf: ?alc.Logical = null, /// The HRTF to be used. hrtf_id: ?i32 = null, /// Enabling gain limiter. output_limiter: ?alc.Logical = null, /// Output mode. output_mode: ?i32 = null, }; pub fn init(device: Device, attributes: Attributes) alc.Error!Context { const fields = @typeInfo(Attributes).@"struct".fields; var attr_list = [_]i32{ 0 } ** (fields.len * 2 + 1); var i: u8 = 0; inline for (fields) |f| if (@field(attributes, f.name)) |v| { attr_list[i] = @intFromEnum(@field(alc.Enum, f.name)); attr_list[i + 1] = if (@TypeOf(v) == i32) v else @intFromEnum(v); i += 2; }; return Context{ .pimpl = try alc.createContext(device.pimpl, attr_list[0..i:0]), .device = device, }; } pub fn deinit(self: Context) alc.Error!void { try alc.makeContextCurrent(null); try alc.destroyContext(self.pimpl); } pub fn makeCurrent(self: ?Context) alc.Error!void { try alc.makeContextCurrent(if (self) |context| context.pimpl else null); } pub fn getCurrent() ?alc.Error!Context { if (alc.getCurrentContext()) |pimpl| return Context{ .pimpl = pimpl, .device = Device{ .pimpl = try alc.getContextsDevice(pimpl) }, }; return null; } pub fn checkCurrent(self: Context) !void { if (self.pimpl != alc.getCurrentContext()) return error.UncurrentContext; } }; pub const listener = struct { pub fn setMetersPerUnit(factor: f32) Error!void { try al.listener.set(.meters_per_unit, factor); } pub fn setPosition(position: [3]f32) Error!void { try al.listener.set(.position, position); } pub fn setVelocity(velocity: [3]f32) Error!void { try al.listener.set(.velocity, velocity); } pub fn setOrientation(at: [3]f32, up: [3]f32) Error!void { const orientation = [_]f32{ at[0], at[1], at[2], up[0], up[1], up[2] }; try al.listener.set(.orientation, orientation); } }; pub const Audio = struct { allocator: Allocator, data: al.Data, frequency: i32, /// Read audio from file. pub fn read(allocator: Allocator, path: [:0]const u8) sf.Error!Audio { const sound = try SndFile.open(path, sf.Mode.read); defer sound.close(); const data = try sound.readAll(allocator); return Audio{ .allocator = allocator, .data = switch (sound.channels) { 1 => al.Data{ .mono16 = data }, 2 => al.Data{ .stereo16 = data }, else => unreachable, }, .frequency = sound.sample_rate, }; } /// Free allocated memory. pub fn free(self: Audio) void { switch (self.data) { .mono8, .stereo8 => |data| self.allocator.free(data), .mono16, .stereo16 => |data| self.allocator.free(data), } } }; pub const Buffer = struct { reference: u32, pub fn init(audio: Audio) Error!Buffer { const reference = try al.buffer.create(); errdefer al.buffer.destroy(&reference) catch unreachable; try al.buffer.fill(reference, audio.data, audio.frequency); return Buffer{ .reference = reference }; } pub fn deinit(self: Buffer) Error!void { try al.buffer.destroy(&self.reference); } }; pub const Source = struct { reference: u32, pub fn init() Error!Source { const reference = try al.source.create(); return Source{ .reference = reference }; } pub fn deinit(self: Source) Error!void { try al.source.destroy(&self.reference); } pub fn bind(self: Source, buffer: Buffer) Error!void { const reference: i32 = @intCast(buffer.reference); try al.source.set(self.reference, .buffer, reference); } /// Specify if the source always has 3D spatialization features (true), /// never has 3D spatialization features (false), or if spatialization /// is enabled based on playing a mono sound or not (null, default). pub fn setSpatialize(self: Source, enable: ?bool) Error!void { const value: i32 = if (enable) |b| al.boolToEnum(b) else al.AUTO; try al.source.set(self.reference, .spatialize, value); } fn getState(self: Source) Error!al.source.State { return try al.source.get(al.source.State, self.reference, .state); } pub fn isPlaying(self: Source) Error!bool { return (try self.getState()) == .playing; } pub fn play(self: Source) Error!void { try al.source.play(self.reference); } pub fn setPosition(self: Source, position: [3]f32) Error!void { try al.source.set(self.reference, .position, position); } pub fn getSecOffset(self: Source) Error!f32 { return try al.source.get(f32, self.reference, .sec_offset); } }; test "Device" { const device = try Device.init("OpenAL Soft"); try device.deinit(); } const expectEqual = std.testing.expectEqual; test "Context" { const device = try Device.init(null); defer device.deinit() catch unreachable; const context = try Context.init(device, .{ .hrtf = .true }); defer context.deinit() catch unreachable; try expectEqual(Context.getCurrent(), null); if (context.checkCurrent()) unreachable else |err| switch (err) { error.UncurrentContext => {}, } try context.makeCurrent(); const current_context = try Context.getCurrent().?; try expectEqual(current_context, context); try expectEqual(current_context.device, device); try context.checkCurrent(); } test "listener" { const context = try Context.init(try Device.init(null), .{}); defer context.device.deinit() catch unreachable; defer context.deinit() catch unreachable; try context.makeCurrent(); try listener.setPosition(.{ 1, 2, 3 }); try listener.setOrientation(.{ 4, 5, 6 }, .{ 7, 8, 9 }); }