summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.build.yml1
-rw-r--r--build.zig9
-rw-r--r--examples/play.zig53
-rw-r--r--src/al.zig182
-rw-r--r--src/alc.zig2
-rw-r--r--src/main.zig76
-rw-r--r--src/sf.zig94
7 files changed, 365 insertions, 52 deletions
diff --git a/.build.yml b/.build.yml
index 5b1d52e..d0f26a2 100644
--- a/.build.yml
+++ b/.build.yml
@@ -1,5 +1,6 @@
 image: archlinux
 packages:
+  - libsndfile
   - openal
   - pulseaudio
   - zig
diff --git a/build.zig b/build.zig
index 6a92ebe..472a5cb 100644
--- a/build.zig
+++ b/build.zig
@@ -29,9 +29,18 @@ pub fn build(b: *std.build.Builder) void {
 
     var main_tests = b.addTest("src/main.zig");
     main_tests.linkSystemLibrary("openal");
+    main_tests.linkSystemLibrary("sndfile");
     main_tests.linkSystemLibrary("c");
     main_tests.setBuildMode(mode);
 
     const test_step = b.step("test", "Run library tests");
     test_step.dependOn(&main_tests.step);
+
+    const example_play = b.addExecutable("zeal-play", "examples/play.zig");
+    example_play.addPackagePath("zeal", "src/main.zig");
+    example_play.linkSystemLibrary("openal");
+    example_play.linkSystemLibrary("sndfile");
+    example_play.linkSystemLibrary("c");
+    example_play.setBuildMode(mode);
+    example_play.install();
 }
diff --git a/examples/play.zig b/examples/play.zig
new file mode 100644
index 0000000..81403a0
--- /dev/null
+++ b/examples/play.zig
@@ -0,0 +1,53 @@
+// Playing audio example
+// Copyright (C) 2021  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 <https://www.gnu.org/licenses/>.
+
+const std = @import("std");
+const allocator = std.heap.c_allocator;
+const args = std.process.args;
+const sleep = std.time.sleep;
+
+const zeal = @import("zeal");
+const alc = zeal.alc;
+const Device = zeal.Device;
+const Context = zeal.Context;
+const useContext = zeal.useContext;
+const Buffer = zeal.Buffer;
+const Source = zeal.Source;
+
+pub fn main() !void {
+    const device = try Device.init(null);
+    defer device.deinit() catch unreachable;
+    const context = try Context.init(device, &.{});
+    defer context.deinit() catch unreachable;
+    try useContext(context);
+
+    var argv = args();
+    _ = argv.next(allocator);
+    const filename = try argv.next(allocator).?;
+    const buffer = try Buffer.init(allocator, context, filename);
+    defer buffer.deinit() catch unreachable;
+
+    const source = try Source.init(context);
+    defer source.deinit() catch unreachable;
+    try source.play(buffer);
+    defer std.debug.print("\n", .{});
+    while (try source.isPlaying()) {
+        sleep(10_000_000);
+        defer std.debug.print("\r{d:.1} s", .{ source.getSecOffset() });
+    }
+}
diff --git a/src/al.zig b/src/al.zig
index bfb93b6..7b2e6db 100644
--- a/src/al.zig
+++ b/src/al.zig
@@ -33,12 +33,16 @@ pub const Error = error {
     OutOfMemory,
 };
 
+pub const BUFFER = AL_BUFFER;
 pub const POSITION = AL_POSITION;
 pub const ORIENTATION = AL_ORIENTATION;
+pub const PLAYING = AL_PLAYING;
+pub const SEC_OFFSET = AL_SEC_OFFSET;
+pub const SOURCE_STATE = AL_SOURCE_STATE;
 
 pub const listener = struct {
     /// Set a property for the listener.
-    pub fn set(param: ALenum, value: anytype) !void {
+    pub fn set(param: ALenum, value: anytype) Error!void {
         const T = @TypeOf(value);
         switch (T) {
             f32 => alListenerf(param, value),
@@ -60,42 +64,154 @@ pub const listener = struct {
     }
 };
 
-/// Generate one buffer for audio data and return its reference.
-pub fn genBuffer() !u32 {
-    var reference: u32 = undefined;
-    alGenBuffers(1, &reference);
-    return switch (alGetError()) {
-        AL_NO_ERROR => reference,
-        AL_INVALID_VALUE => Error.InvalidValue,
-        AL_OUT_OF_MEMORY => Error.OutOfMemory,
-        else => unreachable,
-    };
-}
-
-// TODO: genBuffers
-
-/// Free resources used by one buffer.
-/// Buffers attached to a source cannot be deleted.
-pub fn deleteBuffer(reference: *const u32) !void {
-    alDeleteBuffers(1, reference);
-    switch (alGetError()) {
-        AL_NO_ERROR => {},
-        AL_INVALID_OPERATION => return Error.InvalidOperation,
-        AL_INVALID_NAME => return Error.InvalidName,
-        AL_INVALID_VALUE => return Error.InvalidValue,
-        else => unreachable,
+pub const Data = union(enum) {
+    /// Unsigned 8-bit mono.
+    mono8: []const u8,
+    /// Signed 16-bit mono.
+    mono16: []const i16,
+    /// Unsigned 8-bit stereo.
+    stereo8: []const u8,
+    /// Signed 16-bit stereo.
+    stereo16: []const i16,
+};
+
+pub const buffer = struct {
+    /// Generate one buffer for audio data and return its reference.
+    pub fn create() Error!u32 {
+        var reference: u32 = undefined;
+        alGenBuffers(1, &reference);
+        return switch (alGetError()) {
+            AL_NO_ERROR => reference,
+            AL_INVALID_VALUE => Error.InvalidValue,
+            AL_OUT_OF_MEMORY => Error.OutOfMemory,
+            else => unreachable,
+        };
+    }
+
+    /// Free resources used by one buffer.
+    /// Buffers attached to a source cannot be deleted.
+    pub fn destroy(reference: *const u32) Error!void {
+        alDeleteBuffers(1, reference);
+        switch (alGetError()) {
+            AL_NO_ERROR => {},
+            AL_INVALID_OPERATION => return Error.InvalidOperation,
+            AL_INVALID_NAME => return Error.InvalidName,
+            AL_INVALID_VALUE => return Error.InvalidValue,
+            else => unreachable,
+        }
+    }
+
+    /// Fill a buffer with audio data.
+    pub fn fill(reference: u32, data: Data, freq: i32) Error!void {
+        const format = switch (data) {
+            .mono8 => AL_FORMAT_MONO8,
+            .mono16 => AL_FORMAT_MONO16,
+            .stereo8 => AL_FORMAT_STEREO8,
+            .stereo16 => AL_FORMAT_STEREO16,
+        };
+
+        switch (data) {
+            .mono8, .stereo8 => |slice| {
+                const size = @intCast(c_int, slice.len);
+                alBufferData(reference, format, slice.ptr, size, freq);
+            },
+            .mono16, .stereo16 => |slice| {
+                const size = @intCast(c_int, slice.len) * 2;
+                alBufferData(reference, format, slice.ptr, size, freq);
+            },
+        }
+
+        switch (alGetError()) {
+            AL_NO_ERROR => {},
+            AL_OUT_OF_MEMORY => return Error.OutOfMemory,
+            AL_INVALID_VALUE => return Error.InvalidValue,
+            AL_INVALID_ENUM => return Error.InvalidEnum,
+            else => unreachable,
+        }
+    }
+};
+
+pub const source = struct {
+    /// Generate one source for audio data and return its reference.
+    pub fn create() Error!u32 {
+        var reference: u32 = undefined;
+        alGenSources(1, &reference);
+        return switch (alGetError()) {
+            AL_NO_ERROR => reference,
+            AL_INVALID_VALUE => Error.InvalidValue,
+            AL_OUT_OF_MEMORY => Error.OutOfMemory,
+            else => unreachable,
+        };
+    }
+
+    /// Free resources used by one source.
+    /// Sources attached to a source cannot be deleted.
+    pub fn destroy(reference: *const u32) Error!void {
+        alDeleteSources(1, reference);
+        switch (alGetError()) {
+            AL_NO_ERROR => {},
+            AL_INVALID_OPERATION => return Error.InvalidOperation,
+            AL_INVALID_NAME => return Error.InvalidName,
+            AL_INVALID_VALUE => return Error.InvalidValue,
+            else => unreachable,
+        }
     }
-}
 
-// TODO: deleteBuffers
+    /// Set a property for the source.
+    pub fn set(reference: u32, param: ALenum, value: anytype) Error!void {
+        const T = @TypeOf(value);
+        switch (T) {
+            f32 => alSourcef(reference, param, value),
+            i32 => alSourcei(reference, param, value),
+            else => switch (Child(T)) {
+                f32 => alSourcefv(reference, param, value[0..]),
+                i32 => alSourceiv(reference, param, value[0..]),
+                else => unreachable,
+            }
+        }
+
+        switch (alGetError()) {
+            AL_NO_ERROR => {},
+            AL_INVALID_VALUE => return Error.InvalidValue,
+            AL_INVALID_ENUM => return Error.InvalidEnum,
+            AL_INVALID_NAME => return Error.InvalidName,
+            AL_INVALID_OPERATION => return Error.InvalidOperation,
+            else => unreachable,
+        }
+    }
+
+    /// Get a scalar property from the source.
+    pub fn get(comptime T: type, reference: u32, param: ALenum) Error!T {
+        var value: T = undefined;
+        switch (T) {
+            f32 => alGetSourcef(reference, param, &value),
+            i32 => alGetSourcei(reference, param, &value),
+            else => unreachable,
+        }
+
+        return switch (alGetError()) {
+            AL_NO_ERROR => value,
+            AL_INVALID_VALUE => return Error.InvalidValue,
+            AL_INVALID_ENUM => return Error.InvalidEnum,
+            AL_INVALID_NAME => return Error.InvalidName,
+            AL_INVALID_OPERATION => return Error.InvalidOperation,
+            else => unreachable,
+        };
+    }
+
+    /// Play the source.
+    pub fn play(reference: u32) Error!void {
+        alSourcePlay(reference);
+        switch (alGetError()) {
+            AL_NO_ERROR => {},
+            AL_INVALID_NAME => return Error.InvalidName,
+            AL_INVALID_OPERATION => return Error.InvalidOperation,
+            else => unreachable,
+        }
+    }
+};
 
-// alBufferData
-// alDeleteSources
-// alGenSources
 // alGetSourcei
-// alSourcef
-// alSourcefv
-// alSourcei
 // alSourcePause
 // alSourcePlay
 // alSourceStop
diff --git a/src/alc.zig b/src/alc.zig
index 405d8bc..8e478e8 100644
--- a/src/alc.zig
+++ b/src/alc.zig
@@ -90,7 +90,7 @@ pub fn getContextsDevice(context: *Context) !*Device {
 }
 
 /// Open the named playback device.
-pub fn openDevice(name: ?[]const u8) !*Device {
+pub fn openDevice(name: ?[:0]const u8) !*Device {
     const name_ptr = if (name == null) null else name.?.ptr;
     if (alcOpenDevice(name_ptr)) |device|
         return device;
diff --git a/src/main.zig b/src/main.zig
index 21e4f3a..7e8d8c5 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -16,16 +16,17 @@
 // You should have received a copy of the GNU Lesser General Public License
 // along with zeal.  If not, see <https://www.gnu.org/licenses/>.
 
-const al = @import("al.zig");
-const alc = @import("alc.zig");
-const expectEqual = std.testing.expectEqual;
-const eql = std.meta.eql;
+const Allocator = std.mem.Allocator;
+const SndFile = sf.SndFile;
+pub const al = @import("al.zig");
+pub const alc = @import("alc.zig");
+const sf = @import("sf.zig");
 const std = @import("std");
 
 pub const Device = struct {
     pimpl: *alc.Device,
 
-    pub fn init(name: ?[]const u8) alc.Error!Device {
+    pub fn init(name: ?[:0]const u8) alc.Error!Device {
         return Device{ .pimpl = try alc.openDevice(name) };
     }
 
@@ -87,15 +88,61 @@ pub const Buffer = struct {
     context: Context,
     reference: u32,
 
-    pub fn init(context: Context, name: []const u8) !Buffer {
+    pub fn init(allocator: *Allocator, context: Context,
+                name: [:0]const u8) !Buffer {
         try checkContext(context);
-        const reference = try al.genBuffer();
+        const snd_file = try SndFile.open(name, sf.Mode.read);
+        defer snd_file.close();
+        const data = try snd_file.readAll(allocator);
+        defer allocator.free(data);
+        const al_data = switch (snd_file.channels) {
+            1 => al.Data{ .mono16 = data },
+            2 => al.Data{ .stereo16 = data },
+            else => unreachable,
+        };
+
+        const reference = try al.buffer.create();
+        try al.buffer.fill(reference, al_data, snd_file.sample_rate);
         return Buffer{ .context = context, .reference = reference };
     }
 
     pub fn deinit(self: Buffer) !void {
         try checkContext(self.context);
-        try al.deleteBuffer(&self.reference);
+        try al.buffer.destroy(&self.reference);
+    }
+};
+
+pub const Source = struct {
+    context: Context,
+    reference: u32,
+
+    pub fn init(context: Context) !Source {
+        try checkContext(context);
+        const reference = try al.source.create();
+        return Source{ .context = context, .reference = reference };
+    }
+
+    pub fn deinit(self: Source) !void {
+        try checkContext(self.context);
+        try al.source.destroy(&self.reference);
+    }
+
+    pub fn isPlaying(self: Source) !bool {
+        try checkContext(self.context);
+        const state = try al.source.get(i32, self.reference, al.SOURCE_STATE);
+        return state == al.PLAYING;
+    }
+
+    pub fn getSecOffset(self: Source) !f32 {
+        try checkContext(self.context);
+        return try al.source.get(f32, self.reference, al.SEC_OFFSET);
+    }
+
+    pub fn play(self: Source, buffer: Buffer) !void {
+        try checkContext(self.context);
+        try al.source.set(self.reference, al.BUFFER,
+                          @intCast(i32, buffer.reference));
+        try al.source.play(self.reference);
     }
 };
 
@@ -104,6 +151,9 @@ test "Device" {
     try device.deinit();
 }
 
+const testing = std.testing;
+const expectEqual = testing.expectEqual;
+
 test "Context" {
     const device = try Device.init(null);
     defer device.deinit() catch unreachable;
@@ -132,13 +182,3 @@ test "Listener" {
     try listener.setPosition(.{ 1, 2, 3 });
     try listener.setOrientation(.{ 4, 5, 6 }, .{ 7, 8, 9 });
 }
-
-test "Buffer" {
-    const context = try Context.init(try Device.init(null), &.{});
-    defer context.device.deinit() catch unreachable;
-    defer context.deinit() catch unreachable;
-
-    try useContext(context);
-    const buffer = try Buffer.init(context, "");
-    defer buffer.deinit() catch unreachable;
-}
diff --git a/src/sf.zig b/src/sf.zig
new file mode 100644
index 0000000..d38446c
--- /dev/null
+++ b/src/sf.zig
@@ -0,0 +1,94 @@
+// libsndfile wrapper
+// Copyright (C) 2021  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 <https://www.gnu.org/licenses/>.
+
+const Allocator = @import("std").mem.Allocator;
+
+usingnamespace @cImport(@cInclude("sndfile.h"));
+
+pub const Mode = enum {
+    read,
+    write,
+    read_write,
+};
+
+pub const Error = error {
+    UnrecognizedFormat,
+    SystemError,
+    MalformedFile,
+    UnsupportedEncoding,
+};
+
+pub const SndFile = struct {
+    pimpl: *SNDFILE,
+    frames: usize,
+    sample_rate: c_int,
+    channels: c_int,
+    format: c_int,
+    sections: c_int,
+    seekable: bool,
+
+    /// Open the sound file at the specified path.
+    pub fn open(path: [:0]const u8, mode: Mode) Error!SndFile {
+        const c_mode = switch (mode) {
+            .read => SFM_READ,
+            .write => SFM_WRITE,
+            .read_write => SFM_RDWR,
+        };
+
+        var info: SF_INFO = undefined;
+        const pimpl = sf_open(path.ptr, c_mode, &info);
+        return SndFile {
+            .pimpl = pimpl orelse return switch (sf_error(null)) {
+                SF_ERR_UNRECOGNISED_FORMAT => Error.UnrecognizedFormat,
+                SF_ERR_SYSTEM => Error.SystemError,
+                SF_ERR_MALFORMED_FILE => Error.MalformedFile,
+                SF_ERR_UNSUPPORTED_ENCODING => Error.UnsupportedEncoding,
+                else => unreachable,
+            },
+            .frames = @intCast(usize, info.frames),
+            .sample_rate = info.samplerate,
+            .channels = info.channels,
+            .format = info.format,
+            .sections = info.sections,
+            .seekable = if (info.seekable == 0) false else true,
+        };
+    }
+
+    /// Read the requested number of frames.
+    /// The returned memory is managed by the caller.
+    pub fn read(self: SndFile, allocator: *Allocator,
+                frames: usize) Allocator.Error![]const i16 {
+        const items = frames * @intCast(usize, self.channels);
+        const memory = try allocator.alloc(i16, items);
+        const n = sf_read_short(self.pimpl, memory.ptr, @intCast(i64, items));
+        return try allocator.realloc(memory, @intCast(usize, n));
+    }
+
+    /// Read the entire file.  The returned memory is managed by the caller.
+    pub fn readAll(self: SndFile,
+                   allocator: *Allocator) Allocator.Error![]const i16 {
+        return self.read(allocator, self.frames);
+    }
+
+    /// Close the file and deallocate its internal buffers.
+    /// Like std.os.close, this function does not return
+    /// any indication of failure.
+    pub fn close(self: SndFile) void {
+        _ = sf_close(self.pimpl);
+    }
+};