add audio subsystem
This commit is contained in:
parent
12cf7201ac
commit
3c0c6d64ca
@ -20,6 +20,11 @@ zig build -Dtarget=x86_64-windows
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* Use [Skribidi](https://github.com/memononen/Skribidi) instead of fontstash for text rendering
|
* 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.
|
* Gamepad support.
|
||||||
* WASM Support. Currently a build config isn't provided for this target.
|
* 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
|
* Update build config for other platforms to reduce binary size. All of the video and audio drivers aren't needed, only gamepads
|
||||||
|
|||||||
11
build.zig
11
build.zig
@ -1,5 +1,4 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const cimgui = @import("cimgui");
|
|
||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.link_libc = true
|
.link_libc = true
|
||||||
});
|
});
|
||||||
|
|
||||||
const cimgui_conf = cimgui.getConfig(false);
|
|
||||||
const dep_sokol = b.dependency("sokol", .{
|
const dep_sokol = b.dependency("sokol", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
@ -38,6 +36,8 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
})) |dep_cimgui| {
|
})) |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));
|
mod_main.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
|
||||||
dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
|
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", .{});
|
const dep_tiled = b.dependency("tiled", .{});
|
||||||
mod_main.addImport("tiled", dep_tiled.module("tiled"));
|
mod_main.addImport("tiled", dep_tiled.module("tiled"));
|
||||||
|
|
||||||
const dep_stb_image = b.dependency("stb_image", .{});
|
const dep_stb = b.dependency("stb", .{});
|
||||||
mod_main.addImport("stb_image", dep_stb_image.module("stb_image"));
|
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", .{});
|
const dep_fontstash_c = b.dependency("fontstash_c", .{});
|
||||||
mod_main.addIncludePath(dep_fontstash_c.path("src"));
|
mod_main.addIncludePath(dep_fontstash_c.path("src"));
|
||||||
@ -106,7 +107,6 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.name = "sokol_template",
|
.name = "sokol_template",
|
||||||
.mod_main = mod_main,
|
.mod_main = mod_main,
|
||||||
.dep_sokol = dep_sokol,
|
.dep_sokol = dep_sokol,
|
||||||
.cimgui_clib_name = cimgui_conf.clib_name,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try buildNative(b, "sokol_template", mod_main, has_console);
|
try buildNative(b, "sokol_template", mod_main, has_console);
|
||||||
@ -169,7 +169,6 @@ const BuildWasmOptions = struct {
|
|||||||
name: []const u8,
|
name: []const u8,
|
||||||
mod_main: *std.Build.Module,
|
mod_main: *std.Build.Module,
|
||||||
dep_sokol: *std.Build.Dependency,
|
dep_sokol: *std.Build.Dependency,
|
||||||
cimgui_clib_name: []const u8,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn patchWasmIncludeDirs(
|
fn patchWasmIncludeDirs(
|
||||||
|
|||||||
@ -28,8 +28,8 @@
|
|||||||
.tiled = .{
|
.tiled = .{
|
||||||
.path = "./libs/tiled",
|
.path = "./libs/tiled",
|
||||||
},
|
},
|
||||||
.stb_image = .{
|
.stb = .{
|
||||||
.path = "./libs/stb_image",
|
.path = "./libs/stb",
|
||||||
},
|
},
|
||||||
// .sdl = .{
|
// .sdl = .{
|
||||||
// .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8",
|
// .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8",
|
||||||
|
|||||||
38
libs/stb/build.zig
Normal file
38
libs/stb/build.zig
Normal file
@ -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 = &.{}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
170
libs/stb/src/stb_vorbis.zig
Normal file
170
libs/stb/src/stb_vorbis.zig
Normal file
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
3
libs/stb/src/stb_vorbis_impl.c
Normal file
3
libs/stb/src/stb_vorbis_impl.c
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#define STB_VORBIS_NO_INTEGER_CONVERSION
|
||||||
|
#define STB_VORBIS_NO_STDIO
|
||||||
|
#include "stb_vorbis.c"
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,9 @@
|
|||||||
const std = @import("std");
|
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();
|
const Assets = @This();
|
||||||
|
|
||||||
@ -13,18 +17,28 @@ const FontName = enum {
|
|||||||
|
|
||||||
font_id: FontName.EnumArray,
|
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(.{
|
const font_id_array: FontName.EnumArray = .init(.{
|
||||||
.regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
|
.regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
|
||||||
.bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.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")),
|
.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{
|
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
|
_ = self; // autofix
|
||||||
|
_ = gpa; // autofix
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/wood01.ogg
Normal file
BIN
src/assets/wood01.ogg
Normal file
Binary file not shown.
BIN
src/assets/wood02.ogg
Normal file
BIN
src/assets/wood02.ogg
Normal file
Binary file not shown.
BIN
src/assets/wood03.ogg
Normal file
BIN
src/assets/wood03.ogg
Normal file
Binary file not shown.
64
src/engine/audio/data.zig
Normal file
64
src/engine/audio/data.zig
Normal file
@ -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) { _ };
|
||||||
|
};
|
||||||
131
src/engine/audio/mixer.zig
Normal file
131
src/engine/audio/mixer.zig
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/engine/audio/root.zig
Normal file
100
src/engine/audio/root.zig
Normal file
@ -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.*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
106
src/engine/audio/store.zig
Normal file
106
src/engine/audio/store.zig
Normal file
@ -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)];
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ pub const Input = @import("./input.zig");
|
|||||||
const ScreenScalar = @import("./screen_scaler.zig");
|
const ScreenScalar = @import("./screen_scaler.zig");
|
||||||
pub const imgui = @import("./imgui.zig");
|
pub const imgui = @import("./imgui.zig");
|
||||||
pub const Graphics = @import("./graphics.zig");
|
pub const Graphics = @import("./graphics.zig");
|
||||||
|
pub const Audio = @import("./audio/root.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const STBImage = @import("stb_image");
|
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);
|
self.game = try Game.init(self.allocator, &self.assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +162,9 @@ fn sokolCleanup(self: *Engine) void {
|
|||||||
const zone = tracy.initZone(@src(), .{ });
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
defer zone.deinit();
|
defer zone.deinit();
|
||||||
|
|
||||||
|
Audio.deinit();
|
||||||
self.game.deinit();
|
self.game.deinit();
|
||||||
self.assets.deinit();
|
self.assets.deinit(self.allocator);
|
||||||
Gfx.deinit();
|
Gfx.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/game.zig
11
src/game.zig
@ -9,6 +9,7 @@ const imgui = Engine.imgui;
|
|||||||
const Vec2 = Engine.Vec2;
|
const Vec2 = Engine.Vec2;
|
||||||
const rgb = Engine.Math.rgb;
|
const rgb = Engine.Math.rgb;
|
||||||
const Gfx = Engine.Graphics;
|
const Gfx = Engine.Graphics;
|
||||||
|
const Audio = Engine.Audio;
|
||||||
|
|
||||||
const Game = @This();
|
const Game = @This();
|
||||||
|
|
||||||
@ -48,6 +49,12 @@ pub fn tick(self: *Game, frame: Engine.Frame) !void {
|
|||||||
}
|
}
|
||||||
dir = dir.normalized();
|
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));
|
self.player = self.player.add(dir.multiplyScalar(50 * frame.dt));
|
||||||
|
|
||||||
const regular_font = self.assets.font_id.get(.regular);
|
const regular_font = self.assets.font_id.get(.regular);
|
||||||
@ -77,4 +84,8 @@ pub fn debug(self: *Game) !void {
|
|||||||
defer imgui.endWindow();
|
defer imgui.endWindow();
|
||||||
|
|
||||||
imgui.text("Hello World!\n");
|
imgui.text("Hello World!\n");
|
||||||
|
imgui.textFmt("Audio: {}/{}\n", .{
|
||||||
|
Audio.mixer.instances.items.len,
|
||||||
|
Audio.mixer.instances.capacity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user