add audio subsystem
This commit is contained in:
parent
12cf7201ac
commit
3c0c6d64ca
@ -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
|
||||
|
||||
11
build.zig
11
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(
|
||||
|
||||
@ -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",
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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");
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
11
src/game.zig
11
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
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user