diff --git a/README.md b/README.md index 54b01d0..51fb2d6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ zig build -Dtarget=x86_64-windows ## TODO * Use [Skribidi](https://github.com/memononen/Skribidi) instead of fontstash for text rendering +* Support for audio formats (Might not need all of them, haven't decided): + * QOA, maybe [qoa.h](https://github.com/phoboslab/qoa/blob/master/qoa.h)? + * Flac, maybe [dr_flac.h](https://github.com/mackron/dr_libs/blob/master/dr_flac.h)? + * Wav, maybe [dr_wav.h](https://github.com/mackron/dr_libs/blob/master/dr_wav.h)? + * Mp3, maybe [dr_mp3.h](https://github.com/mackron/dr_libs/blob/master/dr_mp3.h)? * Gamepad support. * WASM Support. Currently a build config isn't provided for this target. * Update build config for other platforms to reduce binary size. All of the video and audio drivers aren't needed, only gamepads diff --git a/build.zig b/build.zig index 3693756..004e8b7 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const cimgui = @import("cimgui"); const sokol = @import("sokol"); const builtin = @import("builtin"); @@ -24,7 +23,6 @@ pub fn build(b: *std.Build) !void { .link_libc = true }); - const cimgui_conf = cimgui.getConfig(false); const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, @@ -38,6 +36,8 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, })) |dep_cimgui| { + const cimgui = b.lazyImport(@This(), "cimgui").?; + const cimgui_conf = cimgui.getConfig(false); mod_main.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name)); dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path(cimgui_conf.include_dir)); } @@ -57,8 +57,9 @@ pub fn build(b: *std.Build) !void { const dep_tiled = b.dependency("tiled", .{}); mod_main.addImport("tiled", dep_tiled.module("tiled")); - const dep_stb_image = b.dependency("stb_image", .{}); - mod_main.addImport("stb_image", dep_stb_image.module("stb_image")); + const dep_stb = b.dependency("stb", .{}); + mod_main.addImport("stb_image", dep_stb.module("stb_image")); + mod_main.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); const dep_fontstash_c = b.dependency("fontstash_c", .{}); mod_main.addIncludePath(dep_fontstash_c.path("src")); @@ -106,7 +107,6 @@ pub fn build(b: *std.Build) !void { .name = "sokol_template", .mod_main = mod_main, .dep_sokol = dep_sokol, - .cimgui_clib_name = cimgui_conf.clib_name, }); } else { try buildNative(b, "sokol_template", mod_main, has_console); @@ -169,7 +169,6 @@ const BuildWasmOptions = struct { name: []const u8, mod_main: *std.Build.Module, dep_sokol: *std.Build.Dependency, - cimgui_clib_name: []const u8, }; fn patchWasmIncludeDirs( diff --git a/build.zig.zon b/build.zig.zon index 24484e4..0aee37a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -28,8 +28,8 @@ .tiled = .{ .path = "./libs/tiled", }, - .stb_image = .{ - .path = "./libs/stb_image", + .stb = .{ + .path = "./libs/stb", }, // .sdl = .{ // .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8", diff --git a/libs/stb/build.zig b/libs/stb/build.zig new file mode 100644 index 0000000..963b76f --- /dev/null +++ b/libs/stb/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const stb_dependency = b.dependency("stb", .{}); + + { + const mod_stb_image = b.addModule("stb_image", .{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/stb_image.zig"), + .link_libc = true, + }); + + mod_stb_image.addIncludePath(stb_dependency.path(".")); + mod_stb_image.addCSourceFile(.{ + .file = b.path("src/stb_image_impl.c"), + .flags = &.{} + }); + } + + { + const mod_stb_image = b.addModule("stb_vorbis", .{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/stb_vorbis.zig"), + .link_libc = true, + }); + + mod_stb_image.addIncludePath(stb_dependency.path(".")); + mod_stb_image.addCSourceFile(.{ + .file = b.path("src/stb_vorbis_impl.c"), + .flags = &.{} + }); + } +} diff --git a/libs/stb_image/build.zig.zon b/libs/stb/build.zig.zon similarity index 100% rename from libs/stb_image/build.zig.zon rename to libs/stb/build.zig.zon diff --git a/libs/stb_image/src/root.zig b/libs/stb/src/stb_image.zig similarity index 100% rename from libs/stb_image/src/root.zig rename to libs/stb/src/stb_image.zig diff --git a/libs/stb_image/src/stb_image_impl.c b/libs/stb/src/stb_image_impl.c similarity index 100% rename from libs/stb_image/src/stb_image_impl.c rename to libs/stb/src/stb_image_impl.c diff --git a/libs/stb/src/stb_vorbis.zig b/libs/stb/src/stb_vorbis.zig new file mode 100644 index 0000000..ea25583 --- /dev/null +++ b/libs/stb/src/stb_vorbis.zig @@ -0,0 +1,170 @@ +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.stb_vorbis); + +const c = @cImport({ + @cDefine("STB_VORBIS_NO_INTEGER_CONVERSION", {}); + @cDefine("STB_VORBIS_NO_STDIO", {}); + @cDefine("STB_VORBIS_HEADER_ONLY", {}); + @cInclude("stb_vorbis.c"); +}); + +const STBVorbis = @This(); + +pub const Error = error { + NeedMoreData, + InvalidApiMixing, + OutOfMemory, + FeatureNotSupported, + TooManyChannels, + FileOpenFailure, + SeekWithoutLength, + UnexpectedEof, + SeekInvalid, + InvalidSetup, + InvalidStream, + MissingCapturePattern, + InvalidStreamStructureVersion, + ContinuedPacketFlagInvalid, + IncorrectStreamSerialNumber, + InvalidFirstPage, + BadPacketType, + CantFindLastPage, + SeekFailed, + OggSkeletonNotSupported, + + Unknown +}; + +fn errorFromInt(err: c_int) ?Error { + return switch (err) { + c.VORBIS__no_error => null, + c.VORBIS_need_more_data => Error.NeedMoreData, + c.VORBIS_invalid_api_mixing => Error.InvalidApiMixing, + c.VORBIS_outofmem => Error.OutOfMemory, + c.VORBIS_feature_not_supported => Error.FeatureNotSupported, + c.VORBIS_too_many_channels => Error.TooManyChannels, + c.VORBIS_file_open_failure => Error.FileOpenFailure, + c.VORBIS_seek_without_length => Error.SeekWithoutLength, + c.VORBIS_unexpected_eof => Error.UnexpectedEof, + c.VORBIS_seek_invalid => Error.SeekInvalid, + c.VORBIS_invalid_setup => Error.InvalidSetup, + c.VORBIS_invalid_stream => Error.InvalidStream, + c.VORBIS_missing_capture_pattern => Error.MissingCapturePattern, + c.VORBIS_invalid_stream_structure_version => Error.InvalidStreamStructureVersion, + c.VORBIS_continued_packet_flag_invalid => Error.ContinuedPacketFlagInvalid, + c.VORBIS_incorrect_stream_serial_number => Error.IncorrectStreamSerialNumber, + c.VORBIS_invalid_first_page => Error.InvalidFirstPage, + c.VORBIS_bad_packet_type => Error.BadPacketType, + c.VORBIS_cant_find_last_page => Error.CantFindLastPage, + c.VORBIS_seek_failed => Error.SeekFailed, + c.VORBIS_ogg_skeleton_not_supported => Error.OggSkeletonNotSupported, + else => Error.Unknown + }; +} + +handle: *c.stb_vorbis, + +pub fn init(data: []const u8, alloc_buffer: []u8) Error!STBVorbis { + const stb_vorbis_alloc: c.stb_vorbis_alloc = .{ + .alloc_buffer = alloc_buffer.ptr, + .alloc_buffer_length_in_bytes = @intCast(alloc_buffer.len) + }; + var error_code: c_int = -1; + const handle = c.stb_vorbis_open_memory( + data.ptr, + @intCast(data.len), + &error_code, + &stb_vorbis_alloc + ); + if (handle == null) { + return errorFromInt(error_code) orelse Error.Unknown; + } + + return STBVorbis{ + .handle = handle.? + }; +} + +pub fn getMinimumAllocBufferSize(self: STBVorbis) u32 { + const info = self.getInfo(); + return info.setup_memory_required + @max(info.setup_temp_memory_required, info.temp_memory_required); +} + +fn getLastError(self: STBVorbis) ?Error { + const error_code = c.stb_vorbis_get_error(self.handle); + return errorFromInt(error_code); +} + +pub fn seek(self: STBVorbis, sample_number: u32) !void { + const success = c.stb_vorbis_seek(self.handle, sample_number); + if (success != 1) { + return self.getLastError() orelse Error.Unknown; + } +} + +pub fn getStreamLengthInSamples(self: STBVorbis) u32 { + return c.stb_vorbis_stream_length_in_samples(self.handle); +} + +pub fn getStreamLengthInSeconds(self: STBVorbis) f32 { + return c.stb_vorbis_stream_length_in_seconds(self.handle); +} + +pub fn getSamples( + self: STBVorbis, + channels: []const [*]f32, + max_samples_per_channel: u32 +) u32 { + const samples_per_channel = c.stb_vorbis_get_samples_float( + self.handle, + @intCast(channels.len), + @constCast(@ptrCast(channels.ptr)), + @intCast(max_samples_per_channel) + ); + return @intCast(samples_per_channel); +} + +const Frame = struct { + channels: []const [*c]const f32, + samples_per_channel: u32 +}; + +pub fn getFrame(self: STBVorbis) Frame { + var output: [*c][*c]f32 = null; + var channels: c_int = undefined; + const samples_per_channel = c.stb_vorbis_get_frame_float( + self.handle, + &channels, + &output + ); + return Frame{ + .channels = output[0..@intCast(channels)], + .samples_per_channel = @intCast(samples_per_channel) + }; +} + +pub const Info = struct { + sample_rate: u32, + channels: u32, + + setup_memory_required: u32, + setup_temp_memory_required: u32, + temp_memory_required: u32, + + max_frame_size: u32 +}; + +pub fn getInfo(self: STBVorbis) Info { + const info = c.stb_vorbis_get_info(self.handle); + return Info{ + .sample_rate = info.sample_rate, + .channels = @intCast(info.channels), + + .setup_memory_required = info.setup_memory_required, + .setup_temp_memory_required = info.setup_temp_memory_required, + .temp_memory_required = info.temp_memory_required, + + .max_frame_size = @intCast(info.max_frame_size), + }; +} diff --git a/libs/stb/src/stb_vorbis_impl.c b/libs/stb/src/stb_vorbis_impl.c new file mode 100644 index 0000000..4a53456 --- /dev/null +++ b/libs/stb/src/stb_vorbis_impl.c @@ -0,0 +1,3 @@ +#define STB_VORBIS_NO_INTEGER_CONVERSION +#define STB_VORBIS_NO_STDIO +#include "stb_vorbis.c" diff --git a/libs/stb_image/build.zig b/libs/stb_image/build.zig deleted file mode 100644 index 970e6ca..0000000 --- a/libs/stb_image/build.zig +++ /dev/null @@ -1,31 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const mod = b.addModule("stb_image", .{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/root.zig"), - .link_libc = true, - }); - - const stb_dependency = b.dependency("stb", .{}); - mod.addIncludePath(stb_dependency.path(".")); - mod.addCSourceFile(.{ - .file = b.path("src/stb_image_impl.c"), - .flags = &.{} - }); - - { - const tests = b.addTest(.{ - .root_module = mod - }); - - const run_tests = b.addRunArtifact(tests); - - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&run_tests.step); - } -} diff --git a/src/assets.zig b/src/assets.zig index 010e749..8ea3e31 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -1,5 +1,9 @@ const std = @import("std"); -const Gfx = @import("./engine/graphics.zig"); + +const Math = @import("./engine/math.zig"); +const Engine = @import("./engine/root.zig"); +const Gfx = Engine.Graphics; +const Audio = Engine.Audio; const Assets = @This(); @@ -13,18 +17,28 @@ const FontName = enum { font_id: FontName.EnumArray, -pub fn init() !Assets { +wood01: Audio.Data.Id, + +pub fn init(gpa: std.mem.Allocator) !Assets { + _ = gpa; // autofix const font_id_array: FontName.EnumArray = .init(.{ .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")), .bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")), .italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")), }); + const wood01 = try Audio.load(.{ + .format = .vorbis, + .data = @embedFile("assets/wood01.ogg"), + }); + return Assets{ - .font_id = font_id_array + .font_id = font_id_array, + .wood01 = wood01 }; } -pub fn deinit(self: *Assets) void { +pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void { _ = self; // autofix + _ = gpa; // autofix } diff --git a/src/assets/wood01.ogg b/src/assets/wood01.ogg new file mode 100644 index 0000000..be5a409 Binary files /dev/null and b/src/assets/wood01.ogg differ diff --git a/src/assets/wood02.ogg b/src/assets/wood02.ogg new file mode 100644 index 0000000..7132356 Binary files /dev/null and b/src/assets/wood02.ogg differ diff --git a/src/assets/wood03.ogg b/src/assets/wood03.ogg new file mode 100644 index 0000000..070b762 Binary files /dev/null and b/src/assets/wood03.ogg differ diff --git a/src/engine/audio/data.zig b/src/engine/audio/data.zig new file mode 100644 index 0000000..36c9d67 --- /dev/null +++ b/src/engine/audio/data.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const STBVorbis = @import("stb_vorbis"); + +pub const Data = union(enum) { + raw: struct { + channels: [][*]f32, + sample_count: u32, + sample_rate: u32 + }, + vorbis: struct { + alloc_buffer: []u8, + stb_vorbis: STBVorbis, + }, + + pub fn streamChannel( + self: Data, + buffer: []f32, + cursor: u32, + channel_index: u32, + sample_rate: u32 + ) []f32 { + // var result: std.ArrayList(f32) = .initBuffer(buffer); + switch (self) { + .raw => |opts| { + if (opts.sample_rate == sample_rate) { + assert(channel_index < opts.channels.len); // TODO: + const channel = opts.channels[channel_index]; + + var memcpy_len: usize = 0; + if (cursor + buffer.len <= opts.sample_count) { + memcpy_len = buffer.len; + } else if (cursor < opts.sample_count) { + memcpy_len = opts.sample_count - cursor; + } + + @memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]); + return buffer[0..memcpy_len]; + } else { + // const in_sample_rate: f32 = @floatFromInt(opts.sample_rate); + // const out_sample_rate: f32 = @floatFromInt(sample_rate); + // const increment = in_sample_rate / out_sample_rate; + // _ = increment; // autofix + unreachable; + } + }, + .vorbis => |opts| { + _ = opts; // autofix + unreachable; + }, + } + // return result.items; + } + + pub fn getSampleCount(self: Data) u32 { + return switch (self) { + .raw => |opts| opts.sample_count, + .vorbis => |opts| opts.stb_vorbis.getStreamLengthInSamples() + }; + } + + pub const Id = enum (u16) { _ }; +}; diff --git a/src/engine/audio/mixer.zig b/src/engine/audio/mixer.zig new file mode 100644 index 0000000..31b0f89 --- /dev/null +++ b/src/engine/audio/mixer.zig @@ -0,0 +1,131 @@ +const std = @import("std"); +const log = std.log.scoped(.audio); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const sokol = @import("sokol"); +const saudio = sokol.audio; + +const AudioData = @import("./data.zig").Data; +pub const Store = @import("./store.zig"); + +const Mixer = @This(); + +pub const Instance = struct { + data_id: AudioData.Id, + cursor: u32 = 0, +}; + +pub const Command = union(enum) { + play: struct { + data_id: AudioData.Id, + }, + + pub const RingBuffer = struct { + // TODO: This ring buffer will work in a single producer single consumer configuration + // For my game this will be good enough + + items: []Command, + head: std.atomic.Value(usize) = .init(0), + tail: std.atomic.Value(usize) = .init(0), + + pub fn push(self: *RingBuffer, command: Command) error{OutOfMemory}!void { + const head = self.head.load(.monotonic); + const tail = self.tail.load(.monotonic); + const next_head = @mod(head + 1, self.items.len); + + // A single slot in the .items array will always not be used. + if (next_head == tail) { + return error.OutOfMemory; + } + + self.items[head] = command; + self.head.store(next_head, .monotonic); + } + + pub fn pop(self: *RingBuffer) ?Command { + const head = self.head.load(.monotonic); + const tail = self.tail.load(.monotonic); + + if (head == tail) { + return null; + } + + const result = self.items[tail]; + self.tail.store(@mod(tail + 1, self.items.len), .monotonic); + return result; + } + }; +}; + +// TODO: Tracks + +instances: std.ArrayList(Instance), +commands: Command.RingBuffer, + +pub fn init( + gpa: Allocator, + max_instances: u32, + max_commands: u32 +) !Mixer { + var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances); + errdefer instances.deinit(gpa); + + const commands = try gpa.alloc(Command, max_commands); + errdefer gpa.free(commands); + + return Mixer{ + .instances = instances, + .commands = .{ + .items = commands + } + }; +} + +pub fn deinit(self: *Mixer, gpa: Allocator) void { + self.instances.deinit(gpa); + gpa.free(self.commands.items); +} + +pub fn queue(self: *Mixer, command: Command) void { + self.commands.push(command) catch log.warn("Maximum number of audio commands reached!", .{}); +} + +pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_channels: u32) !void { + while (self.commands.pop()) |command| { + switch (command) { + .play => |opts| { + self.instances.appendBounded(.{ + .data_id = opts.data_id + }) catch log.warn("Maximum number of audio instances reached!", .{}); + } + } + } + + assert(num_channels == 1); // TODO: + const sample_rate: u32 = @intCast(saudio.sampleRate()); + + @memset(buffer, 0); + + // var written: u32 = 0; + for (self.instances.items) |*instance| { + const audio_data = store.get(instance.data_id); + const samples = audio_data.streamChannel(buffer[0..num_frames], instance.cursor, 0, sample_rate); + instance.cursor += @intCast(samples.len); + } + + { + var i: usize = 0; + while (i < self.instances.items.len) { + const instance = self.instances.items[i]; + const audio_data = store.get(instance.data_id); + const is_complete = instance.cursor == audio_data.getSampleCount(); + + if (is_complete) { + _ = self.instances.swapRemove(i); + } else { + i += 1; + } + } + } +} diff --git a/src/engine/audio/root.zig b/src/engine/audio/root.zig new file mode 100644 index 0000000..c384a4d --- /dev/null +++ b/src/engine/audio/root.zig @@ -0,0 +1,100 @@ +const std = @import("std"); +const log = std.log.scoped(.audio); +const assert = std.debug.assert; + +const tracy = @import("tracy"); + +const Math = @import("../math.zig"); +const STBVorbis = @import("stb_vorbis"); + +pub const Data = @import("./data.zig").Data; +pub const Store = @import("./store.zig"); +pub const Mixer = @import("./mixer.zig"); + +const sokol = @import("sokol"); +const saudio = sokol.audio; + +var stopped: bool = true; +var gpa: std.mem.Allocator = undefined; +var store: Store = undefined; +pub var mixer: Mixer = undefined; + +const Options = struct { + allocator: std.mem.Allocator, + logger: saudio.Logger = .{}, + channels: u32 = 1, + max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib, + buffer_frames: u32 = 2048, + max_instances: u32 = 64 +}; + +pub fn init(opts: Options) !void { + gpa = opts.allocator; + + store = try Store.init(.{ + .allocator = opts.allocator, + .max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size, + }); + + mixer = try Mixer.init(gpa, opts.max_instances, opts.max_instances); + + saudio.setup(.{ + .logger = opts.logger, + .stream_cb = sokolStreamCallback, + .num_channels = @intCast(opts.channels), + .buffer_frames = @intCast(opts.buffer_frames) + }); + stopped = false; + + const sample_rate: f32 = @floatFromInt(saudio.sampleRate()); + const audio_latency: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate; + log.debug("Audio latency = {D}", .{@as(u64, @intFromFloat(audio_latency * std.time.ns_per_s))}); +} + +pub fn deinit() void { + stopped = true; + saudio.shutdown(); + mixer.deinit(gpa); + store.deinit(); +} + +pub fn load(opts: Store.LoadOptions) !Data.Id { + return try store.load(opts); +} + +pub const PlayOptions = struct { + id: Data.Id, + delay: f32 = 0 +}; + +pub fn play(opts: PlayOptions) void { + mixer.queue(.{ + .play = .{ + .data_id = opts.id + } + }); +} + +fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void { + if (stopped) { + return; + } + + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + const num_frames_u32: u32 = @intCast(num_frames); + const num_channels_u32: u32 = @intCast(num_channels); + + mixer.stream( + store, + buffer[0..(num_frames_u32 * num_channels_u32)], + num_frames_u32, + num_channels_u32 + ) catch |e| { + log.err("mixer.stream() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + }; +} diff --git a/src/engine/audio/store.zig b/src/engine/audio/store.zig new file mode 100644 index 0000000..10e3b13 --- /dev/null +++ b/src/engine/audio/store.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const Math = @import("../math.zig"); +const STBVorbis = @import("stb_vorbis"); + +const AudioData = @import("./data.zig").Data; + +const Store = @This(); + +arena: std.heap.ArenaAllocator, +list: std.ArrayList(AudioData), +temp_vorbis_alloc_buffer: []u8, + +const Options = struct { + allocator: std.mem.Allocator, + max_vorbis_alloc_buffer_size: u32, +}; + +pub fn init(opts: Options) !Store { + const gpa = opts.allocator; + + const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size); + errdefer gpa.free(temp_vorbis_alloc_buffer); + + return Store{ + .arena = std.heap.ArenaAllocator.init(gpa), + .list = .empty, + .temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer + }; +} + +pub fn deinit(self: *Store) void { + const gpa = self.arena.child_allocator; + gpa.free(self.temp_vorbis_alloc_buffer); + self.list.deinit(gpa); + self.arena.deinit(); +} + +pub const LoadOptions = struct { + const PlaybackStyle = enum { + stream, + decode_once, + + // If the decoded size is less than `stream_threshold`, then .decode_once will by default be used. + const stream_threshold = 10 * Math.bytes_per_mib; + }; + + const Format = enum { + vorbis + }; + + format: Format, + data: []const u8, + playback_style: ?PlaybackStyle = null, +}; + +pub fn load(self: *Store, opts: LoadOptions) !AudioData.Id { + const gpa = self.arena.child_allocator; + + const id = self.list.items.len; + try self.list.ensureUnusedCapacity(gpa, 1); + + const PlaybackStyle = LoadOptions.PlaybackStyle; + const temp_stb_vorbis = try STBVorbis.init(opts.data, self.temp_vorbis_alloc_buffer); + const info = temp_stb_vorbis.getInfo(); + const duration_in_samples = temp_stb_vorbis.getStreamLengthInSamples(); + const decoded_size = info.channels * duration_in_samples * @sizeOf(f32); + const stream_threshold = PlaybackStyle.stream_threshold; + const default_playback_style: PlaybackStyle = if (decoded_size < stream_threshold) .decode_once else .stream; + + const arena_allocator = self.arena.allocator(); + const playback_style = opts.playback_style orelse default_playback_style; + if (playback_style == .decode_once) { + const channels = try arena_allocator.alloc([*]f32, info.channels); + for (channels) |*channel| { + channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr; + } + const samples_decoded = temp_stb_vorbis.getSamples(channels, duration_in_samples); + assert(samples_decoded == duration_in_samples); + + self.list.appendAssumeCapacity(AudioData{ + .raw = .{ + .channels = channels, + .sample_count = duration_in_samples, + .sample_rate = info.sample_rate + } + }); + } else { + const alloc_buffer = try arena_allocator.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize()); + const stb_vorbis = STBVorbis.init(opts.data, alloc_buffer) catch unreachable; + + self.list.appendAssumeCapacity(AudioData{ + .vorbis = .{ + .alloc_buffer = alloc_buffer, + .stb_vorbis = stb_vorbis + } + }); + } + + return @enumFromInt(id); +} + +pub fn get(self: Store, id: AudioData.Id) AudioData { + return self.list.items[@intFromEnum(id)]; +} diff --git a/src/engine/root.zig b/src/engine/root.zig index e479b60..1a7bfe2 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -13,6 +13,7 @@ pub const Input = @import("./input.zig"); const ScreenScalar = @import("./screen_scaler.zig"); pub const imgui = @import("./imgui.zig"); pub const Graphics = @import("./graphics.zig"); +pub const Audio = @import("./audio/root.zig"); const tracy = @import("tracy"); const builtin = @import("builtin"); const STBImage = @import("stb_image"); @@ -148,7 +149,12 @@ fn sokolInit(self: *Engine) !void { } }); - self.assets = try Assets.init(); + try Audio.init(.{ + .allocator = self.allocator, + .logger = .{ .func = sokolLogCallback }, + }); + + self.assets = try Assets.init(self.allocator); self.game = try Game.init(self.allocator, &self.assets); } @@ -156,8 +162,9 @@ fn sokolCleanup(self: *Engine) void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); + Audio.deinit(); self.game.deinit(); - self.assets.deinit(); + self.assets.deinit(self.allocator); Gfx.deinit(); } diff --git a/src/game.zig b/src/game.zig index 02636a3..a3a1836 100644 --- a/src/game.zig +++ b/src/game.zig @@ -9,6 +9,7 @@ const imgui = Engine.imgui; const Vec2 = Engine.Vec2; const rgb = Engine.Math.rgb; const Gfx = Engine.Graphics; +const Audio = Engine.Audio; const Game = @This(); @@ -48,6 +49,12 @@ pub fn tick(self: *Game, frame: Engine.Frame) !void { } dir = dir.normalized(); + if (dir.x != 0 or dir.y != 0) { + Audio.play(.{ + .id = self.assets.wood01 + }); + } + self.player = self.player.add(dir.multiplyScalar(50 * frame.dt)); const regular_font = self.assets.font_id.get(.regular); @@ -77,4 +84,8 @@ pub fn debug(self: *Game) !void { defer imgui.endWindow(); imgui.text("Hello World!\n"); + imgui.textFmt("Audio: {}/{}\n", .{ + Audio.mixer.instances.items.len, + Audio.mixer.instances.capacity + }); }