refactor audio subsystem

This commit is contained in:
Rokas Puzonas 2026-08-16 19:27:39 +03:00
parent a99bf9c17b
commit 045617b763
8 changed files with 526 additions and 161 deletions

View File

@ -0,0 +1,23 @@
Impact Sounds (1.0)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 19-12-2019
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Request: http://request.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

Binary file not shown.

View File

@ -10,7 +10,8 @@ pub fn build(b: *std.Build) !void {
"sokoban/sokoban_tilesheet.png", "sokoban/sokoban_tilesheet.png",
"roboto-font/Roboto-Regular.ttf", "roboto-font/Roboto-Regular.ttf",
"tiled/map.tmx", "tiled/map.tmx",
"tiled/tileset.tsx" "tiled/tileset.tsx",
"impact-sounds/footstep_concrete_000.ogg"
}; };
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });

View File

@ -53,7 +53,10 @@ player_direction: Direction,
player_frame_index: u32, player_frame_index: u32,
animation_timer: Platform.Nanoseconds, animation_timer: Platform.Nanoseconds,
sound: Audio.Sound.Id, footstep_buffer: Audio.BufferId,
footstep_sound: Audio.SoundId = .nil,
sfx_bus: Audio.BusId,
const Tilesheet = struct { const Tilesheet = struct {
image: Gfx.ImageData, image: Gfx.ImageData,
@ -164,10 +167,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
} }
} }
const sound = Audio.initSound(.{ const footstep_sound = Audio.addBuffer(.{
.cb = .{ .sample = sinSampleCallback }, .vorbis = plt.assets.readFile("impact-sounds/footstep_concrete_000.ogg")
.playing = false,
.frequency = 1
}); });
self.* = App{ self.* = App{
@ -181,16 +182,13 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.player_frame_index = 0, .player_frame_index = 0,
.animation_timer = 0, .animation_timer = 0,
.tilesheet = tilesheet, .tilesheet = tilesheet,
.sound = sound .footstep_buffer = footstep_sound,
.sfx_bus = Audio.addBus(.{ .label = "sfx" })
}; };
return null; return null;
} }
fn sinSampleCallback(sound: *Audio.Sound) f32 {
return @sin(440 * sound.phase);
}
pub fn frame(self: *App, plt: Platform.Frame) !void { pub fn frame(self: *App, plt: Platform.Frame) !void {
const input = plt.input; const input = plt.input;
@ -212,8 +210,13 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
dir = dir.normalized(); dir = dir.normalized();
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
if (input.isKeyPressed(.F)) {
Audio.stop(self.footstep_sound);
}
if (input.isKeyPressed(.E)) { if (input.isKeyPressed(.E)) {
Audio.setPlaying(self.sound, !Audio.getPlaying(self.sound)); Audio.stop(self.footstep_sound);
self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .loop = true, .bus = self.sfx_bus });
} }
var new_direction = self.player_direction; var new_direction = self.player_direction;

View File

@ -1,47 +1,65 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.audio); const log = std.log.scoped(.audio);
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const sokol = @import("sokol"); const sokol = @import("sokol");
const sapp = sokol.app; const sapp = sokol.app;
const saudio = sokol.audio; const saudio = sokol.audio;
const ImGUI = @import("imgui.zig");
const tracy = @import("tracy"); const tracy = @import("tracy");
const STBVorbis = @import("stb_vorbis");
const Math = @import("math");
const SlotMapType = @import("./slot_map.zig").SlotMapType; const SlotMapType = @import("./slot_map.zig").SlotMapType;
const State = struct { const Bus = struct {
io: Io, label: ?[]const u8 = null,
mutex: std.Io.Mutex, volume: f32 = 1,
sounds: Sound.SlotMap, parent_id: ?Bus.Id = null,
volume: f32 = 0.1,
running: std.atomic.Value(bool) = .init(false),
temp_buffer: []f32 pub const Id = packed struct {
index: u16,
pub const nil = Id{ .index = std.math.maxInt(u16) };
};
}; };
pub const BusId = Bus.Id;
var g_state: State = undefined; const Buffer = struct {
samples: Samples,
pub const InitOptions = struct { const Samples = struct {
logger: saudio.Logger = .{}, len: usize,
buffer_frames: u32 = 512, left: [*]const f32,
max_sounds: usize = 128,
// If `right` is is null, then this is mono
right: ?[*]const f32,
pub fn deinit(self: Samples, gpa: Allocator) void {
gpa.free(self.left[0..self.len]);
if (self.right) |right| {
gpa.free(right[0..self.len]);
}
}
};
pub const Id = packed struct {
index: u16,
pub const nil = Id{ .index = std.math.maxInt(u16) };
};
}; };
pub const BufferId = Buffer.Id;
pub const Sound = struct { const Sound = struct {
cb: Callback, cursor: usize,
userdata: ?*anyopaque, buffer: BufferId,
volume: f32, volume: f32 = 1,
looping: bool, loop: bool = false,
adsr: ADSR, bus: BusId,
playing: bool,
phase_increment: f32 = 0,
duration_frames: u32 = 0,
frame_index: u32 = 0,
phase: f32 = 0,
pub const ADSR = struct { pub const ADSR = struct {
attack: f32, attack: f32,
@ -49,7 +67,7 @@ pub const Sound = struct {
sustain: f32, sustain: f32,
release: f32, release: f32,
pub const default = ADSR{ pub const identity = ADSR{
.attack = 0, .attack = 0,
.decay = 0, .decay = 0,
.sustain = 1, .sustain = 1,
@ -57,38 +75,84 @@ pub const Sound = struct {
}; };
}; };
pub const Callback = union(enum) { const SlotMap = SlotMapType(u16, u8, Sound);
sample: *const fn(sound: *Sound) f32, const Id = SlotMap.Id;
block: *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void, };
}; pub const SoundId = Sound.Id;
const SlotMap = SlotMapType(u8, u16, Sound); const ThreadState = struct {
pub const Id = SlotMap.Id; // When modifying any fields in this struct, you must first acquire the mutex.
mutex: std.Io.Mutex,
sounds: Sound.SlotMap,
};
const State = struct {
io: Io,
gpa: Allocator,
running: std.atomic.Value(bool) = .init(false),
vorbis_alloc_buffer: []u8,
buses: std.ArrayList(Bus),
buffers: std.ArrayList(Buffer),
thread_state: ThreadState
};
var g_state: State = undefined;
pub var main_bus: BusId = .nil;
const g_sample_rate = 44100;
pub const InitOptions = struct {
logger: saudio.Logger = .{},
buffer_frames: u32 = 512,
max_buffers: usize = 128,
max_sounds: usize = 256,
max_buses: usize = 16,
max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib,
}; };
pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void { pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void {
const channels = 1; // TODO: Stereo audio
const self = &g_state; const self = &g_state;
const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds); var buses = try std.ArrayList(Bus).initCapacity(gpa, opts.max_buses);
errdefer gpa.free(sounds_buffer); errdefer buses.deinit(gpa);
const temp_buffer = try gpa.alloc(f32, opts.buffer_frames); var buffers = try std.ArrayList(Buffer).initCapacity(gpa, opts.max_buffers);
errdefer gpa.free(temp_buffer); errdefer buffers.deinit(gpa);
const sounds = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds);
errdefer gpa.free(sounds);
const vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
errdefer gpa.free(vorbis_alloc_buffer);
self.* = State{ self.* = State{
.io = io, .io = io,
.mutex = .init, .gpa = gpa,
.sounds = .init(sounds_buffer), .buffers = buffers,
.temp_buffer = temp_buffer, .buses = buses,
.vorbis_alloc_buffer = vorbis_alloc_buffer,
.thread_state = .{
.mutex = .init,
.sounds = .init(sounds),
}
}; };
self.running.store(true, .seq_cst); self.running.store(true, .seq_cst);
main_bus = addBus(.{
.label = "main"
});
const channels = 2;
saudio.setup(.{ saudio.setup(.{
.stream_cb = sokolStreamCallback, .stream_cb = sokolStreamCallback,
.logger = opts.logger, .logger = opts.logger,
.num_channels = channels, .num_channels = channels,
.buffer_frames = @intCast(opts.buffer_frames), .buffer_frames = @intCast(opts.buffer_frames),
.sample_rate = g_sample_rate
}); });
log.debug("Init:", .{}); log.debug("Init:", .{});
@ -104,146 +168,359 @@ pub fn deinit(gpa: std.mem.Allocator) void {
saudio.shutdown(); saudio.shutdown();
gpa.free(self.sounds.slots.allocatedSlice()); for (self.buffers.items) |buffer| {
gpa.free(self.temp_buffer); buffer.samples.deinit(gpa);
}
self.buffers.deinit(gpa);
self.buses.deinit(gpa);
gpa.free(self.thread_state.sounds.slots.allocatedSlice());
gpa.free(self.vorbis_alloc_buffer);
} }
pub fn getSampleRate() f32 { const BufferData = union(enum) {
return @floatFromInt(saudio.sampleRate()); raw: struct {
} samples_mono: []f32,
sample_rate: u32
pub const SoundOptions = struct { },
cb: Sound.Callback, vorbis: []const u8
userdata: ?*anyopaque = null,
adsr: Sound.ADSR = .default,
volume: f32 = 1,
playing: bool = true,
looping: bool = false,
frequency: ?f32 = null,
duration_ns: ?u64 = null
}; };
pub fn initSound(opts: SoundOptions) Sound.Id { // Source: https://amini-allight.org/post/game-audio-programming-tutorial-part-6
var self = &g_state; fn hermiteResample(input: []f32, output: []f32) void {
for (0..output.len) |i| {
const elapsed = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(output.len));
self.mutex.lock(self.io) catch return .nil; const b_index: isize = @intFromFloat(elapsed * @as(f32, @floatFromInt(input.len)));
defer self.mutex.unlock(self.io); const a_index = b_index - 1;
const c_index = b_index + 1;
const d_index = b_index + 2;
const id = self.sounds.insertUndefined() catch { const a = if (a_index >= 0 and a_index < input.len) input[@intCast(a_index)] else input[@intCast(b_index)];
log.warn("Sound limit reached! limit: {}", .{self.sounds.slots.capacity}); const b = input[@intCast(b_index)];
const c = if (c_index >= 0 and c_index < input.len) input[@intCast(c_index)] else input[@intCast(b_index)];
const d = if (d_index >= 0 and d_index < input.len) input[@intCast(d_index)] else input[@intCast(b_index)];
const t = (elapsed * @as(f32, @floatFromInt(input.len))) - @as(f32, @floatFromInt(b_index));
const c0 = b;
const c1 = (c - a) * 0.5;
const c2 = a - (b * 2.5) + (c * 2.0) - (d * 0.5);
const c3 = ((b - c) * 1.5) + ((d - a) * 0.5);
output[i] = (((((c3 * t) + c2) * t) + c1) * t) + c0;
}
}
fn resampleIfNeeded(gpa: Allocator, samples: *[]f32, sample_rate: u32) !void {
const new_sample_count = samples.len * g_sample_rate / sample_rate;
if (new_sample_count != samples.len) {
const new_samples = try gpa.alloc(f32, new_sample_count);
hermiteResample(samples.*, new_samples);
gpa.free(samples.*);
samples.* = new_samples;
}
}
fn bufferDataToSamples(data: BufferData) !Buffer.Samples {
const self = &g_state;
return switch (data) {
.raw => |raw| {
var samples = try self.gpa.dupe(f32, raw.samples_mono);
errdefer self.gpa.free(samples);
try resampleIfNeeded(self.gpa, &samples, raw.sample_rate);
return Buffer.Samples{
.len = samples.len,
.left = samples.ptr,
.right = null
};
},
.vorbis => |vorbis_data| {
const vorbis = try STBVorbis.init(vorbis_data, self.vorbis_alloc_buffer);
const sample_count = vorbis.getStreamLengthInSamples();
const info = vorbis.getInfo();
if (info.channels == 1) {
var samples = try self.gpa.alloc(f32, sample_count);
errdefer self.gpa.free(samples);
const result = vorbis.getSamples(&.{ samples.ptr }, sample_count);
assert(result == sample_count);
try resampleIfNeeded(self.gpa, &samples, info.sample_rate);
return .{
.len = samples.len,
.left = samples.ptr,
.right = null
};
} else if (info.channels >= 2) {
var left_samples = try self.gpa.alloc(f32, sample_count);
errdefer self.gpa.free(left_samples);
var right_samples = try self.gpa.alloc(f32, sample_count);
errdefer self.gpa.free(right_samples);
const result = vorbis.getSamples(&.{ left_samples.ptr, right_samples.ptr }, sample_count);
assert(result == sample_count);
try resampleIfNeeded(self.gpa, &left_samples, info.sample_rate);
try resampleIfNeeded(self.gpa, &right_samples, info.sample_rate);
return Buffer.Samples{
.len = left_samples.len,
.left = left_samples.ptr,
.right = right_samples.ptr
};
} else {
return Buffer.Samples{
.len = 0,
.left = &.{},
.right = null
};
}
}
};
}
pub fn addBuffer(data: BufferData) BufferId {
const self = &g_state;
if (self.buffers.items.len == self.buffers.capacity) {
log.warn("Max audio buffers reached, limit: {}", .{self.buffers.capacity});
return .nil;
}
const samples = bufferDataToSamples(data) catch |e| {
log.err("Failed to add buffer: {}", .{e});
return .nil; return .nil;
}; };
var phase_increment: f32 = 0; const buffer_id = BufferId{ .index = @intCast(self.buffers.items.len) };
if (opts.frequency) |frequency| { const buffer = self.buffers.addOneAssumeCapacity();
phase_increment = frequency * 2 * std.math.pi / getSampleRate(); buffer.* = Buffer{
} .samples = samples
var duration_frames: u32 = 0;
if (opts.duration_ns) |duration_ns| {
duration_frames = @intCast(duration_ns * @as(u64, @intCast(saudio.sampleRate())) / std.time.ns_per_s);
}
const sound = self.sounds.getAssumeExists(id);
sound.* = Sound{
.cb = opts.cb,
.userdata = opts.userdata,
.adsr = opts.adsr,
.looping = opts.looping,
.volume = opts.volume,
.playing = opts.playing,
.duration_frames = duration_frames,
.phase_increment = phase_increment
}; };
return id; assert(buffer_id != BufferId.nil);
return buffer_id;
} }
pub fn deinitSound(id: Sound.Id) void { pub fn addBus(opts: Bus) BusId {
var self = &g_state; const self = &g_state;
if (self.sounds.get(id)) |sound| { if (self.buses.items.len == self.buses.capacity) {
self.mutex.lock(self.io) catch return; log.warn("Max audio buses reached, limit: {}", .{self.buses.capacity});
defer self.mutex.unlock(self.io); return .nil;
}
_ = sound; // autofix const bus_id = BusId{ .index = @intCast(self.buses.items.len) };
self.sounds.removeAssumeExists(id); const bus = self.buses.addOneAssumeCapacity();
bus.* = opts;
if (bus.parent_id != null and bus.parent_id == BusId.nil) {
bus.parent_id = null;
}
if (bus.parent_id == null and main_bus != BusId.nil) {
bus.parent_id = main_bus;
}
assert(bus_id != BusId.nil);
return bus_id;
}
fn busMultipliedVolume(id: BusId) f32 {
const self = &g_state;
var result: f32 = 1;
var iter: ?BusId = id;
while (iter) |current| {
const bus = &self.buses.items[current.index];
result *= bus.volume;
iter = bus.parent_id;
}
return result;
}
const PlayOptions = struct {
buffer: BufferId,
// TODO: adsr: ?ADSR = null,
volume: f32 = 1,
loop: bool = false,
bus: ?BusId = null
};
pub fn play(opts: PlayOptions) SoundId {
const self = &g_state;
var mutex = &self.thread_state.mutex;
var sounds = &self.thread_state.sounds;
mutex.lock(self.io) catch |e| {
log.warn("Failed to lock mutex: {}", .{e});
return .nil;
};
defer mutex.unlock(self.io);
const bus = opts.bus orelse main_bus;
if (bus == BusId.nil) {
return .nil;
}
if (opts.buffer == BufferId.nil) {
return .nil;
}
if (sounds.unusedCapacity() == 0) {
log.warn("Max sounds reached, limit: {}", .{sounds.slots.capacity});
return .nil;
}
const sound_id = sounds.insertAssumeCapacity();
const sound = sounds.getAssumeExists(sound_id);
sound.* = Sound{
.cursor = 0,
.volume = opts.volume,
.loop = opts.loop,
.buffer = opts.buffer,
.bus = bus,
};
return sound_id;
}
pub fn stop(id: SoundId) void {
const self = &g_state;
var mutex = &self.thread_state.mutex;
var sounds = &self.thread_state.sounds;
mutex.lock(self.io) catch |e| {
log.warn("Failed to lock mutex: {}", .{e});
return;
};
defer mutex.unlock(self.io);
_ = sounds.remove(id);
}
pub fn getBus(id: BusId) ?*Bus {
const self = &g_state;
if (id == .nil) {
return null;
}
return &self.buses.items[id.index];
}
pub fn showDebug() void {
const self = &g_state;
if (ImGUI.beginWindow(.{
.name = "audio",
.size = .init(200, 200)
})) {
defer ImGUI.endWindow();
const sounds = &self.thread_state.sounds;
const buses = &self.buses;
ImGUI.text("Sounds: {}/{}", .{sounds.count(), sounds.slots.capacity});
if(ImGUI.beginTable("buses", 3, 0)) {
defer ImGUI.endTable();
ImGUI.tableSetupColumn("ID (Label)", 0);
ImGUI.tableSetupColumn("Volume", 0);
ImGUI.tableSetupColumn("Multiplied volume", 0);
ImGUI.tableHeadersRow();
for (0.., buses.items) |i, *bus| {
ImGUI.pushID(.{ .int = @intCast(i) });
defer ImGUI.popID();
ImGUI.tableNextRow();
ImGUI.tableNextColumn();
ImGUI.text("{} ({s})", .{i, bus.label orelse "-"});
ImGUI.tableNextColumn();
_ = ImGUI.inputF32Drag(.{
.label = "",
.value = &bus.volume,
.ex = .{
.min = 0,
.max = 1,
.speed = 0.01
}
});
ImGUI.tableNextColumn();
ImGUI.text("{}", .{busMultipliedVolume(.{ .index = @intCast(i) })});
}
}
} }
} }
pub fn setPlaying(id: Sound.Id, playing: bool) void { fn sokolStream(output_buffer: [*c]f32, num_frames: u32, num_channels: u32) !void {
var self = &g_state;
if (self.sounds.get(id)) |sound| {
self.mutex.lock(self.io) catch return;
defer self.mutex.unlock(self.io);
sound.playing = playing;
}
}
pub fn getPlaying(id: Sound.Id) bool {
var self = &g_state;
if (self.sounds.get(id)) |sound| {
return sound.playing;
} else {
return false;
}
}
fn bumpSoundCursor(sound: *Sound, count: u32) void {
if (sound.duration_frames > 0) {
sound.frame_index += count;
sound.frame_index = @mod(sound.frame_index, sound.duration_frames);
}
if (sound.phase_increment > 0) {
sound.phase += sound.phase_increment * @as(f32, @floatFromInt(count));
sound.phase = @mod(sound.phase, 2 * std.math.pi);
}
}
fn sokolStream(buffer: [*c]f32, num_frames: u32, num_channels: u32) !void {
const self = &g_state; const self = &g_state;
if (!self.running.load(.seq_cst)) { if (!self.running.load(.seq_cst)) {
return; return;
} }
try self.mutex.lock(self.io); const mutex = &self.thread_state.mutex;
defer self.mutex.unlock(self.io); const sounds = &self.thread_state.sounds;
const buses = self.buses;
assert(num_channels == 1); try mutex.lock(self.io);
assert(self.temp_buffer.len >= num_frames); defer mutex.unlock(self.io);
@memset(buffer[0..num_frames], 0); assert(num_channels == 2);
@memset(output_buffer[0..(num_frames*2)], 0);
var iter = self.sounds.iterator(); var sound_iter = sounds.iterator();
while (iter.next()) |id| { while (sound_iter.next()) |sound_id| {
const sound = self.sounds.getAssumeExists(id); const sound = sounds.getAssumeExists(sound_id);
if (!sound.playing) { const buffer = &self.buffers.items[sound.buffer.index];
continue;
}
var samples = std.ArrayList(f32).initBuffer(self.temp_buffer[0..num_frames]); const bus_id = sound.bus;
switch (sound.cb) { const bus = buses.items[bus_id.index];
.block => |cb| { _ = bus; // autofix
cb(sound, &samples); const bus_volume = busMultipliedVolume(bus_id);
bumpSoundCursor(sound, @intCast(samples.items.len));
}, const volume = sound.volume * bus_volume;
.sample => |cb| {
samples.expandToCapacity(); const left_channel = buffer.samples.left;
for (samples.items) |*sample| { const right_channel = buffer.samples.right orelse buffer.samples.left;
sample.* = cb(sound);
bumpSoundCursor(sound, 1); var output_index: usize = 0;
while (output_index < num_frames) {
while (sound.cursor < buffer.samples.len and output_index < num_frames) {
const left = left_channel[sound.cursor];
const right = right_channel[sound.cursor];
sound.cursor += 1;
output_buffer[2*output_index+0] += left * volume;
output_buffer[2*output_index+1] += right * volume;
output_index += 1;
}
if (!sound.loop) {
break;
} else {
if (sound.cursor == buffer.samples.len) {
sound.cursor = 0;
} }
} }
} }
for (0..samples.items.len) |i| { if (!sound.loop and sound.cursor == buffer.samples.len) {
buffer[i] += samples.items[i] * sound.volume * self.volume; sounds.removeAssumeExists(sound_id);
} }
} }
} }

View File

@ -503,3 +503,59 @@ pub fn collapsingHeader(label: []const u8) bool {
ig.ImGuiTreeNodeFlags_None ig.ImGuiTreeNodeFlags_None
); );
} }
pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool {
if (isDisabled()) {
return false;
}
return ig.igBeginTable(id, @intCast(columns), flags);
}
pub fn endTable() void {
if (isDisabled()) {
return;
}
ig.igEndTable();
}
pub fn tableNextColumn() void {
if (isDisabled()) {
return;
}
_ = ig.igTableNextColumn();
}
pub fn tableNextRow() void {
if (isDisabled()) {
return;
}
_ = ig.igTableNextRow();
}
pub fn tableSetColumnIndex(index: usize) void {
if (isDisabled()) {
return;
}
_ = ig.igTableSetColumnIndex(@intCast(index));
}
pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void {
if (isDisabled()) {
return;
}
ig.igTableSetupColumn(label, flags);
}
pub fn tableHeadersRow() void {
if (isDisabled()) {
return;
}
ig.igTableHeadersRow();
}

View File

@ -141,6 +141,7 @@ fn PlatformType(App: type) type {
Gfx.flush(.{}); Gfx.flush(.{});
Gfx.showDebug(); Gfx.showDebug();
Audio.showDebug();
ImGUI.endFrame(); ImGUI.endFrame();
Gfx.endFrame(); Gfx.endFrame();

View File

@ -129,7 +129,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
pub fn unusedCapacity(self: *Self) usize { pub fn unusedCapacity(self: *Self) usize {
const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
return capacity - self.slots.items.len + self.hole_count; return capacity - self.count();
}
pub fn count(self: *Self) usize {
return self.slots.items.len - self.hole_count;
} }
pub fn insertAssumeCapacity(self: *Self) Id { pub fn insertAssumeCapacity(self: *Self) Id {