make audio system not global
This commit is contained in:
parent
83ce69a794
commit
f80859877c
489
engine/src/lib/generational_array_list.zig
Normal file
489
engine/src/lib/generational_array_list.zig
Normal file
@ -0,0 +1,489 @@
|
||||
const std = @import("std");
|
||||
const tracy = @import("tracy");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// This array provides:
|
||||
/// - O(1) insertion
|
||||
/// - O(1) deletion
|
||||
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
||||
pub fn GenerationalArrayList(
|
||||
Index: type,
|
||||
Generation: type,
|
||||
Item: type
|
||||
) type {
|
||||
assert(@bitSizeOf(Generation) % 8 == 0);
|
||||
assert(@bitSizeOf(Index) % 8 == 0);
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
items: [*]Item,
|
||||
generations: [*]Generation,
|
||||
unused: [*]u8,
|
||||
|
||||
len: u32,
|
||||
capacity: u32,
|
||||
|
||||
pub const empty = Self{
|
||||
.items = &[_]Item{},
|
||||
.generations = &[_]Generation{},
|
||||
.unused = &[_]u8{},
|
||||
.capacity = 0,
|
||||
.len = 0,
|
||||
};
|
||||
|
||||
pub const Id = packed struct {
|
||||
pub const Int = @Type(.{
|
||||
.int = .{
|
||||
.bits = @bitSizeOf(Index) + @bitSizeOf(Generation),
|
||||
.signedness = .unsigned
|
||||
}
|
||||
});
|
||||
|
||||
generation: Generation,
|
||||
index: Index,
|
||||
|
||||
// TODO: Maybe `Id.Optional` type should be created to ensure .wrap() and .toOptional()
|
||||
pub const none = Id{
|
||||
.generation = std.math.maxInt(Generation),
|
||||
.index = std.math.maxInt(Index),
|
||||
};
|
||||
|
||||
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
if (self == Id.none) {
|
||||
try writer.print("Id({s}){{ .none }}", .{ @typeName(Item) });
|
||||
} else {
|
||||
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Item), self.index, self.generation });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn asInt(self: Id) Int {
|
||||
return @bitCast(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ItemWithId = struct {
|
||||
id: Id,
|
||||
item: *Item,
|
||||
};
|
||||
|
||||
pub const Iterator = struct {
|
||||
array_list: *Self,
|
||||
index: Index,
|
||||
|
||||
pub fn nextId(self: *Iterator) ?Id {
|
||||
while (self.index < self.array_list.len) {
|
||||
const index = self.index;
|
||||
self.index += 1;
|
||||
|
||||
// TODO: Inline the `byte_index` calculate for better speed.
|
||||
// Probably not needed. Idk
|
||||
if (self.array_list.isUnused(index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Id{
|
||||
.index = @intCast(index),
|
||||
.generation = self.array_list.generations[index]
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn nextItem(self: *Iterator) ?*Item {
|
||||
if (self.nextId()) |id| {
|
||||
return &self.array_list.items[id.index];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn next(self: *Iterator) ?ItemWithId {
|
||||
if (self.nextId()) |id| {
|
||||
return ItemWithId{
|
||||
.id = id,
|
||||
.item = &self.array_list.items[id.index]
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Metadata = extern struct {
|
||||
len: u32,
|
||||
count: u32
|
||||
};
|
||||
|
||||
fn divCeilGeneration(num: u32) u32 {
|
||||
return std.math.divCeil(u32, num, @bitSizeOf(Generation)) catch unreachable;
|
||||
}
|
||||
|
||||
fn divFloorGeneration(num: u32) u32 {
|
||||
return @divFloor(num, @bitSizeOf(Generation));
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacityPrecise(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
||||
if (new_capacity > std.math.maxInt(Index)) {
|
||||
return error.OutOfIndexSpace;
|
||||
}
|
||||
|
||||
// TODO: Shrinking is not supported
|
||||
assert(new_capacity >= self.capacity);
|
||||
|
||||
const unused_bit_array_len = divCeilGeneration(self.capacity);
|
||||
const new_unused_bit_array_len = divCeilGeneration(new_capacity);
|
||||
|
||||
// TODO: Handle allocation failure case
|
||||
const new_unused = try allocator.realloc(self.unused[0..unused_bit_array_len], new_unused_bit_array_len);
|
||||
const new_items = try allocator.realloc(self.items[0..self.capacity], new_capacity);
|
||||
const new_generations = try allocator.realloc(self.generations[0..self.capacity], new_capacity);
|
||||
|
||||
self.unused = new_unused.ptr;
|
||||
self.items = new_items.ptr;
|
||||
self.generations = new_generations.ptr;
|
||||
self.capacity = new_capacity;
|
||||
}
|
||||
|
||||
fn growCapacity(current: u32, minimum: u32) u32 {
|
||||
const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(Item)));
|
||||
|
||||
var new = current;
|
||||
while (true) {
|
||||
new +|= new / 2 + init_capacity;
|
||||
if (new >= minimum) {
|
||||
return new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: u32) !void {
|
||||
if (self.capacity >= new_capacity) return;
|
||||
|
||||
const better_capacity = Self.growCapacity(self.capacity, new_capacity);
|
||||
try self.ensureTotalCapacityPrecise(allocator, better_capacity);
|
||||
}
|
||||
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
self.count = 0;
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
pub fn ensureUnusedCapacity(self: *Self, allocator: Allocator, unused_capacity: u32) !void {
|
||||
try self.ensureTotalCapacity(allocator, self.len + unused_capacity);
|
||||
}
|
||||
|
||||
fn findFirstUnused(self: *Self) ?Index {
|
||||
for (0..divCeilGeneration(self.len)) |byte_index| {
|
||||
if (self.unused[byte_index] != 0) {
|
||||
const found = @ctz(self.unused[byte_index]) + byte_index * @bitSizeOf(Generation);
|
||||
if (found < self.len) {
|
||||
return @intCast(found);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn markUnused(self: *Self, index: Index, unused: bool) void {
|
||||
assert(index < self.len);
|
||||
|
||||
const byte_index = divFloorGeneration(index);
|
||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
||||
if (unused) {
|
||||
self.unused[byte_index] |= bit_flag;
|
||||
} else {
|
||||
self.unused[byte_index] &= ~bit_flag;
|
||||
}
|
||||
}
|
||||
|
||||
fn isUnused(self: *Self, index: Index) bool {
|
||||
assert(index < self.len);
|
||||
|
||||
const byte_index = divFloorGeneration(index);
|
||||
const bit_index = @mod(index, @bitSizeOf(Generation));
|
||||
const bit_flag = @as(u8, 1) << @intCast(bit_index);
|
||||
return (self.unused[byte_index] & bit_flag) != 0;
|
||||
}
|
||||
|
||||
pub fn insertUndefined(self: *Self, allocator: Allocator) !Id {
|
||||
var unused_index: Index = undefined;
|
||||
|
||||
if (self.findFirstUnused()) |index| {
|
||||
unused_index = index;
|
||||
} else {
|
||||
try self.ensureUnusedCapacity(allocator, 1);
|
||||
|
||||
unused_index = @intCast(self.len);
|
||||
self.len += 1;
|
||||
self.generations[unused_index] = 0;
|
||||
}
|
||||
|
||||
self.markUnused(unused_index, false);
|
||||
self.count += 1;
|
||||
|
||||
const id = Id{
|
||||
.index = @intCast(unused_index),
|
||||
.generation = self.generations[unused_index]
|
||||
};
|
||||
|
||||
assert(id != Id.none);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn insert(self: *Self, allocator: Allocator, item: Item) !Id {
|
||||
const id = try self.insertUndefined(allocator);
|
||||
|
||||
const new_item_ptr = self.getAssumeExists(id);
|
||||
new_item_ptr.* = item;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn exists(self: *Self, id: Id) bool {
|
||||
if (id.index >= self.len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.isUnused(id.index)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.generations[id.index] != id.generation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeAssumeExists(self: *Self, id: Id) void {
|
||||
assert(self.exists(id));
|
||||
|
||||
self.markUnused(id.index, true);
|
||||
// TODO: Maybe a log should be shown when a wrap-around occurs?
|
||||
self.generations[id.index] +%= 1;
|
||||
self.count -= 1;
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, id: Id) bool {
|
||||
if (!self.exists(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.removeAssumeExists(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getAssumeExists(self: *Self, id: Id) *Item {
|
||||
assert(self.exists(id));
|
||||
|
||||
return &self.items[id.index];
|
||||
}
|
||||
|
||||
pub fn get(self: *Self, id: Id) ?*Item {
|
||||
if (self.exists(id)) {
|
||||
return self.getAssumeExists(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{
|
||||
.array_list = self,
|
||||
.index = 0
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
allocator.free(self.unused[0..divCeilGeneration(self.capacity)]);
|
||||
allocator.free(self.generations[0..self.capacity]);
|
||||
allocator.free(self.items[0..self.capacity]);
|
||||
}
|
||||
|
||||
pub fn clone(self: *Self, allocator: Allocator) !Self {
|
||||
const items = try allocator.dupe(Item, self.items[0..self.capacity]);
|
||||
errdefer allocator.free(items);
|
||||
|
||||
const generations = try allocator.dupe(Generation, self.generations[0..self.capacity]);
|
||||
errdefer allocator.free(generations);
|
||||
|
||||
const unused = try allocator.dupe(u8, self.unused[0..divCeilGeneration(self.capacity)]);
|
||||
errdefer allocator.free(unused);
|
||||
|
||||
return Self{
|
||||
.items = items.ptr,
|
||||
.generations = generations.ptr,
|
||||
.unused = unused.ptr,
|
||||
.len = self.len,
|
||||
.count = self.count,
|
||||
.capacity = self.capacity
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getMetadata(self: *Self) Metadata {
|
||||
return Metadata{
|
||||
.len = self.len,
|
||||
.count = self.count
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *Self, writer: *std.Io.Writer, endian: std.builtin.Endian) !void {
|
||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list write" });
|
||||
defer zone.end();
|
||||
|
||||
try writer.writeSliceEndian(Item, self.items[0..self.len], endian);
|
||||
try writer.writeSliceEndian(Generation, self.generations[0..self.len], endian);
|
||||
try writer.writeAll(self.unused[0..divCeilGeneration(self.len)]);
|
||||
}
|
||||
|
||||
pub fn read(
|
||||
self: *Self,
|
||||
allocator: Allocator,
|
||||
reader: *std.Io.Reader,
|
||||
endian: std.builtin.Endian,
|
||||
metadata: Metadata
|
||||
) !void {
|
||||
const zone = tracy.beginZone(@src(), .{ .name = "gen array list read" });
|
||||
defer zone.end();
|
||||
|
||||
try self.ensureTotalCapacity(allocator, metadata.len);
|
||||
|
||||
try reader.readSliceEndian(Item, self.items[0..metadata.len], endian);
|
||||
try reader.readSliceEndian(Generation, self.generations[0..metadata.len], endian);
|
||||
try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]);
|
||||
|
||||
self.len = metadata.len;
|
||||
self.count = metadata.count;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const TestArray = GenerationalArrayList(u24, u8, u32);
|
||||
|
||||
test "insert & remove" {
|
||||
const expect = std.testing.expect;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id1));
|
||||
try expect(array_list.remove(id1));
|
||||
try expect(!array_list.exists(id1));
|
||||
try expect(!array_list.remove(id1));
|
||||
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id2));
|
||||
try expect(!array_list.exists(id1));
|
||||
try expect(id1.index == id2.index);
|
||||
}
|
||||
|
||||
test "generation wrap around" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
// Grow array list so that at least 1 slot exists
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
array_list.removeAssumeExists(id1);
|
||||
|
||||
// Artificially increase generation count
|
||||
array_list.generations[id1.index] = std.math.maxInt(@FieldType(TestArray.Id, "generation"));
|
||||
|
||||
// Check if generation wraps around
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
array_list.removeAssumeExists(id2);
|
||||
try expectEqual(id1.index, id2.index);
|
||||
try expectEqual(0, array_list.generations[id1.index]);
|
||||
}
|
||||
|
||||
test "iterator" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
// Create array which has a hole
|
||||
const id1 = try array_list.insert(gpa, 1);
|
||||
const id2 = try array_list.insert(gpa, 2);
|
||||
const id3 = try array_list.insert(gpa, 3);
|
||||
|
||||
array_list.removeAssumeExists(id2);
|
||||
|
||||
var iter = array_list.iterator();
|
||||
try expectEqual(
|
||||
TestArray.ItemWithId{
|
||||
.id = id1,
|
||||
.item = array_list.getAssumeExists(id1)
|
||||
},
|
||||
iter.next().?
|
||||
);
|
||||
try expectEqual(
|
||||
TestArray.ItemWithId{
|
||||
.id = id3,
|
||||
.item = array_list.getAssumeExists(id3)
|
||||
},
|
||||
iter.next().?
|
||||
);
|
||||
try expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test "read & write" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list1: TestArray = .empty;
|
||||
defer array_list1.deinit(gpa);
|
||||
|
||||
var array_list2: TestArray = .empty;
|
||||
defer array_list2.deinit(gpa);
|
||||
|
||||
const id1 = try array_list1.insert(gpa, 1);
|
||||
const id2 = try array_list1.insert(gpa, 2);
|
||||
const id3 = try array_list1.insert(gpa, 3);
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&buffer);
|
||||
const native_endian = builtin.cpu.arch.endian();
|
||||
|
||||
try array_list1.write(&writer, native_endian);
|
||||
|
||||
var reader = std.Io.Reader.fixed(writer.buffered());
|
||||
try array_list2.read(gpa, &reader, native_endian, array_list1.getMetadata());
|
||||
|
||||
try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*);
|
||||
try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*);
|
||||
try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*);
|
||||
try expectEqual(array_list1.count, array_list2.count);
|
||||
}
|
||||
|
||||
test "clear retaining capacity" {
|
||||
const expect = std.testing.expect;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var array_list: TestArray = .empty;
|
||||
defer array_list.deinit(gpa);
|
||||
|
||||
const id1 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id1));
|
||||
array_list.clearRetainingCapacity();
|
||||
|
||||
const id2 = try array_list.insert(gpa, 10);
|
||||
try expect(array_list.exists(id2));
|
||||
|
||||
try expectEqual(id1, id2);
|
||||
}
|
||||
@ -4,6 +4,7 @@ const log = std.log.scoped(.engine);
|
||||
pub const build_options = @import("build_options");
|
||||
pub const ImGui = @import("./imgui.zig");
|
||||
pub const Math = @import("./math.zig");
|
||||
pub const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
|
||||
|
||||
const rgb = Math.rgb;
|
||||
const Rect = Math.Rect;
|
||||
@ -143,7 +144,7 @@ pub const MouseButton = enum {
|
||||
|
||||
pub const TextureId = enum(u16) { _ };
|
||||
pub const FontId = enum(u16) { _ };
|
||||
pub const AudioId = enum(u16) { _ };
|
||||
pub const SoundId = enum(u16) { _ };
|
||||
|
||||
pub const Sprite = struct {
|
||||
texture: TextureId,
|
||||
@ -227,7 +228,7 @@ pub const GraphicsCommand = union(enum) {
|
||||
|
||||
pub const AudioCommand = union(enum) {
|
||||
pub const Play = struct {
|
||||
id: AudioId,
|
||||
id: SoundId,
|
||||
volume: f32 = 1
|
||||
};
|
||||
|
||||
|
||||
429
engine/src/runtime/audio.zig
Normal file
429
engine/src/runtime/audio.zig
Normal file
@ -0,0 +1,429 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.engine);
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const tracy = @import("tracy");
|
||||
|
||||
const Lib = @import("lib");
|
||||
const Math = Lib.Math;
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
|
||||
const AudioSystem = @This();
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const saudio = sokol.audio;
|
||||
|
||||
const Sound = union(enum) {
|
||||
nil,
|
||||
raw: Raw,
|
||||
vorbis: Vorbis,
|
||||
|
||||
const Raw = struct {
|
||||
channels: [][*]f32,
|
||||
sample_count: u32,
|
||||
sample_rate: u32,
|
||||
|
||||
fn getSampleCount(self: Raw) u32 {
|
||||
return self.sample_count;
|
||||
}
|
||||
|
||||
fn getSampleRate(self: Raw) u32 {
|
||||
return self.sample_rate;
|
||||
}
|
||||
|
||||
pub fn streamChannel(self: Raw, opts: StreamOptions) []f32 {
|
||||
assert(opts.sample_rate == self.sample_rate); // TODO:
|
||||
assert(opts.channel_index < self.channels.len); // TODO:
|
||||
|
||||
const channel = self.channels[opts.channel_index];
|
||||
|
||||
var memcpy_len: usize = 0;
|
||||
if (opts.cursor + opts.buffer.len <= self.sample_count) {
|
||||
memcpy_len = opts.buffer.len;
|
||||
} else if (opts.cursor < opts.sample_count) {
|
||||
memcpy_len = opts.sample_count - opts.cursor;
|
||||
}
|
||||
|
||||
@memcpy(opts.buffer[0..memcpy_len], channel[opts.cursor..][0..memcpy_len]);
|
||||
return opts.buffer[0..memcpy_len];
|
||||
}
|
||||
};
|
||||
|
||||
const Vorbis = struct {
|
||||
alloc_buffer: []u8,
|
||||
stb_vorbis: STBVorbis,
|
||||
|
||||
fn init(arena: std.mem.Allocator, data: []const u8, temp_vorbis_alloc_buffer: []u8) Vorbis {
|
||||
const temp_stb_vorbis = try STBVorbis.init(data, temp_vorbis_alloc_buffer);
|
||||
const alloc_buffer = try arena.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize());
|
||||
|
||||
// This can't fail because `alloc_buffer` is guarenteed to be big enough
|
||||
// And there can't be a decode error because `temp_stb_vorbis` was successfully initialized
|
||||
const stb_vorbis = STBVorbis.init(data, alloc_buffer) catch unreachable;
|
||||
|
||||
return Vorbis{
|
||||
.alloc_buffer = alloc_buffer,
|
||||
.stb_vorbis = stb_vorbis
|
||||
};
|
||||
}
|
||||
|
||||
fn getSampleCount(self: Vorbis) u32 {
|
||||
return self.stb_vorbis.getStreamLengthInSamples();
|
||||
}
|
||||
|
||||
fn getSampleRate(self: Vorbis) u32 {
|
||||
const info = self.stb_vorbis.getInfo();
|
||||
return info.sample_rate;
|
||||
}
|
||||
|
||||
pub fn streamChannel(self: Vorbis, opts: StreamOptions) []f32 {
|
||||
_ = self; // autofix
|
||||
_ = opts; // autofix
|
||||
@panic("TODO");
|
||||
}
|
||||
};
|
||||
|
||||
const StreamOptions = struct {
|
||||
buffer: []f32,
|
||||
cursor: u32,
|
||||
channel_index: u32,
|
||||
sample_rate: u32
|
||||
};
|
||||
|
||||
pub fn streamChannel(self: Sound, opts: StreamOptions) []f32 {
|
||||
return switch (self) {
|
||||
.nil => opts.buffer[0..0],
|
||||
.raw => |raw| raw.streamChannel(opts),
|
||||
.vorbis => |vorbis| vorbis.streamChannel(opts),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSampleCount(self: Sound) u32 {
|
||||
return switch (self) {
|
||||
.nil => 0,
|
||||
.raw => |raw| raw.getSampleCount(),
|
||||
.vorbis => |vorbis| vorbis.getSampleCount()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSampleRate(self: Sound) u32 {
|
||||
return switch (self) {
|
||||
.nil => 0,
|
||||
.raw => |raw| raw.getSampleRate(),
|
||||
.vorbis => |vorbis| vorbis.getSampleRate()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getDuration(self: Sound) Lib.Nanoseconds {
|
||||
const sample_count = self.getSampleCount();
|
||||
const sample_rate = self.getSampleRate();
|
||||
if (sample_rate == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return @as(Lib.Nanoseconds, sample_count) * std.time.ns_per_s / sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
const CommandRingBuffer = struct {
|
||||
// TODO: This ring buffer will work in a single producer single consumer configuration
|
||||
// For my game this will be good enough
|
||||
|
||||
items: []Lib.AudioCommand,
|
||||
head: std.atomic.Value(usize),
|
||||
tail: std.atomic.Value(usize),
|
||||
|
||||
fn init(buffer: []Lib.AudioCommand) CommandRingBuffer {
|
||||
return CommandRingBuffer{
|
||||
.items = buffer,
|
||||
.head = .init(0),
|
||||
.tail = .init(0),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn push(self: *CommandRingBuffer, command: Lib.AudioCommand) 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: *CommandRingBuffer) ?Lib.AudioCommand {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/// Be mindful when accessing fields on this struct!
|
||||
/// This data will be accessed by the audio thread which runs
|
||||
/// independentaly of the main thread
|
||||
const ThreadState = struct {
|
||||
instances: std.ArrayList(SoundInstance),
|
||||
commands: CommandRingBuffer,
|
||||
temp_channel_buffer: []f32,
|
||||
|
||||
pub const SoundInstance = struct {
|
||||
sound_id: Lib.SoundId,
|
||||
volume: f32 = 0,
|
||||
cursor: u32 = 0,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
gpa: std.mem.Allocator,
|
||||
max_instances: usize,
|
||||
max_commands: usize,
|
||||
max_frames_per_channel: usize
|
||||
) !ThreadState {
|
||||
var instances: std.ArrayList(SoundInstance) = try .initCapacity(gpa, max_instances);
|
||||
errdefer instances.deinit(gpa);
|
||||
|
||||
const command_buffer = try gpa.alloc(Lib.AudioCommand, max_commands);
|
||||
errdefer gpa.free(command_buffer);
|
||||
|
||||
const temp_channel_buffer = try gpa.alloc(f32, max_frames_per_channel);
|
||||
errdefer gpa.free(temp_channel_buffer);
|
||||
|
||||
return ThreadState{
|
||||
.instances = instances,
|
||||
.commands = .init(command_buffer),
|
||||
.temp_channel_buffer = temp_channel_buffer
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ThreadState, gpa: std.mem.Allocator) void {
|
||||
self.instances.deinit(gpa);
|
||||
gpa.free(self.commands.items);
|
||||
gpa.free(self.temp_channel_buffer);
|
||||
}
|
||||
};
|
||||
|
||||
const SoundsList = Lib.GenerationalArrayList(u8, u8, Sound);
|
||||
|
||||
gpa: std.mem.Allocator,
|
||||
running: std.atomic.Value(bool),
|
||||
temp_vorbis_alloc_buffer: []u8,
|
||||
|
||||
sounds_arena: std.heap.ArenaAllocator,
|
||||
sounds: SoundsList,
|
||||
|
||||
thread_state: ThreadState,
|
||||
|
||||
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,
|
||||
max_commands: u32 = 64,
|
||||
};
|
||||
|
||||
pub fn init(self: *AudioSystem, opts: Options) !void {
|
||||
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);
|
||||
|
||||
const thread_state = try ThreadState.init(gpa,
|
||||
opts.max_instances,
|
||||
opts.max_instances,
|
||||
opts.buffer_frames
|
||||
);
|
||||
errdefer thread_state.deinit(gpa);
|
||||
|
||||
self.* = AudioSystem{
|
||||
.gpa = gpa,
|
||||
.running = .init(false),
|
||||
.temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer,
|
||||
.sounds = .empty,
|
||||
.sounds_arena = std.heap.ArenaAllocator.init(gpa),
|
||||
.thread_state = thread_state,
|
||||
};
|
||||
|
||||
saudio.setup(.{
|
||||
.stream_userdata_cb = sokolStreamCallback,
|
||||
.user_data = self,
|
||||
.logger = opts.logger,
|
||||
.num_channels = @intCast(opts.channels),
|
||||
.buffer_frames = @intCast(opts.buffer_frames),
|
||||
});
|
||||
self.running.store(true, .seq_cst);
|
||||
|
||||
const sample_rate: f32 = @floatFromInt(saudio.sampleRate());
|
||||
const max_latency: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate;
|
||||
|
||||
log.debug("Audio:", .{});
|
||||
log.debug("- sample_rate: {}", .{saudio.sampleRate()});
|
||||
log.debug("- channels: {}", .{saudio.channels()});
|
||||
log.debug("- buffer_frames: {}", .{saudio.bufferFrames()});
|
||||
log.debug("- max_latency: {D}", .{@as(u64, @intFromFloat(max_latency * std.time.ns_per_s))});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AudioSystem) void {
|
||||
self.running.store(false, .seq_cst);
|
||||
|
||||
saudio.shutdown();
|
||||
|
||||
self.thread_state.deinit(self.gpa);
|
||||
self.gpa.free(self.temp_vorbis_alloc_buffer);
|
||||
self.sounds_arena.deinit();
|
||||
self.sounds.deinit(self.gpa);
|
||||
}
|
||||
|
||||
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: *AudioSystem, opts: LoadOptions) !Lib.SoundId {
|
||||
_ = self; // autofix
|
||||
_ = opts; // autofix
|
||||
@panic("TODO");
|
||||
|
||||
// const id = self.sounds.items.len;
|
||||
// try self.sounds.ensureUnusedCapacity(self.gpa, 1);
|
||||
//
|
||||
// // const list = std.ArrayList(u32).initBuffer(&.{});
|
||||
// // list.unused
|
||||
//
|
||||
// const arena_allocator = self.sounds_arena.allocator();
|
||||
//
|
||||
// assert(opts.format == .vorbis);
|
||||
// // const vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer);
|
||||
//
|
||||
// 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 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.sounds.appendAssumeCapacity(Sound{
|
||||
// .raw = .{
|
||||
// .channels = channels,
|
||||
// .sample_count = duration_in_samples,
|
||||
// .sample_rate = info.sample_rate
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// self.sounds.appendAssumeCapacity(Sound{
|
||||
// .vorbis = try Sound.Vorbis.init(arena_allocator, opts.data, self.temp_vorbis_alloc_buffer)
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return @enumFromInt(id);
|
||||
}
|
||||
|
||||
pub fn unload(self: *AudioSystem, id: Lib.SoundId) void {
|
||||
_ = self; // autofix
|
||||
_ = id; // autofix
|
||||
@panic("TODO");
|
||||
}
|
||||
|
||||
pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound {
|
||||
return self.sounds.items[@intFromEnum(id)];
|
||||
}
|
||||
|
||||
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: i32, num_channels: i32) void {
|
||||
if (!self.running.load(.seq_cst)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zone = tracy.initZone(@src(), .{ });
|
||||
defer zone.deinit();
|
||||
|
||||
const thread_state = &self.thread_state;
|
||||
while (thread_state.commands.pop()) |command| {
|
||||
switch (command) {
|
||||
.play => |opts| {
|
||||
const volume = @max(opts.volume, 0);
|
||||
if (volume == 0) {
|
||||
log.warn("Attempt to play audio with 0 volume", .{});
|
||||
continue;
|
||||
}
|
||||
|
||||
thread_state.instances.appendBounded(.{
|
||||
.sound_id = opts.id,
|
||||
.volume = volume,
|
||||
}) catch log.warn("Maximum number of audio instances reached!", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(num_channels == 1); // TODO:
|
||||
const sample_rate: u32 = @intCast(saudio.sampleRate());
|
||||
|
||||
assert(thread_state.temp_channel_buffer.len >= num_frames);
|
||||
|
||||
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
|
||||
_ = sample_rate; // autofix
|
||||
|
||||
for (thread_state.instances.items) |*instance| {
|
||||
_ = instance; // autofix
|
||||
// const sound = self.sounds
|
||||
// const audio_data = store.get(instance.data_id);
|
||||
// const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate);
|
||||
// for (0.., samples) |i, sample| {
|
||||
// buffer[i] += sample * instance.volume;
|
||||
// }
|
||||
// instance.cursor += @intCast(samples.len);
|
||||
}
|
||||
|
||||
{
|
||||
// var i: usize = 0;
|
||||
// while (i < thread_state.instances.items.len) {
|
||||
// const instance = thread_state.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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32, user_data: ?*anyopaque) callconv(.c) void {
|
||||
const audio: *AudioSystem = @alignCast(@ptrCast(user_data));
|
||||
audio.sokolStream(buffer, num_frames, num_channels);
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
const Lib = @import("lib");
|
||||
|
||||
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 fn getSampleRate(self: Data) u32 {
|
||||
return switch (self) {
|
||||
.raw => |opts| opts.sample_rate,
|
||||
.vorbis => |opts| blk: {
|
||||
const info = opts.stb_vorbis.getInfo();
|
||||
break :blk info.sample_rate;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Id = Lib.AudioId;
|
||||
};
|
||||
@ -1,146 +0,0 @@
|
||||
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 Lib = @import("lib");
|
||||
const AudioData = @import("./data.zig").Data;
|
||||
pub const Store = @import("./store.zig");
|
||||
|
||||
const Mixer = @This();
|
||||
|
||||
pub const Instance = struct {
|
||||
data_id: Lib.AudioId,
|
||||
volume: f32 = 0,
|
||||
cursor: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Command = Lib.AudioCommand;
|
||||
|
||||
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: RingBuffer,
|
||||
working_buffer: []f32,
|
||||
|
||||
pub fn init(
|
||||
gpa: Allocator,
|
||||
max_instances: u32,
|
||||
max_commands: u32,
|
||||
working_buffer_size: 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);
|
||||
|
||||
const working_buffer = try gpa.alloc(f32, working_buffer_size);
|
||||
errdefer gpa.free(working_buffer);
|
||||
|
||||
return Mixer{
|
||||
.working_buffer = working_buffer,
|
||||
.instances = instances,
|
||||
.commands = .{
|
||||
.items = commands
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Mixer, gpa: Allocator) void {
|
||||
self.instances.deinit(gpa);
|
||||
gpa.free(self.commands.items);
|
||||
gpa.free(self.working_buffer);
|
||||
}
|
||||
|
||||
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| {
|
||||
const volume = @max(opts.volume, 0);
|
||||
if (volume == 0) {
|
||||
log.warn("Attempt to play audio with 0 volume", .{});
|
||||
continue;
|
||||
}
|
||||
|
||||
self.instances.appendBounded(.{
|
||||
.data_id = opts.id,
|
||||
.volume = volume,
|
||||
}) catch log.warn("Maximum number of audio instances reached!", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(num_channels == 1); // TODO:
|
||||
const sample_rate: u32 = @intCast(saudio.sampleRate());
|
||||
|
||||
@memset(buffer, 0);
|
||||
assert(self.working_buffer.len >= num_frames);
|
||||
|
||||
for (self.instances.items) |*instance| {
|
||||
const audio_data = store.get(instance.data_id);
|
||||
const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate);
|
||||
for (0.., samples) |i, sample| {
|
||||
buffer[i] += sample * instance.volume;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.audio);
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const tracy = @import("tracy");
|
||||
|
||||
const Math = @import("lib").Math;
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
|
||||
pub const Data = @import("./data.zig").Data;
|
||||
pub const Store = @import("./store.zig");
|
||||
pub const Mixer = @import("./mixer.zig");
|
||||
|
||||
pub const Command = Mixer.Command;
|
||||
|
||||
const Nanoseconds = @import("lib").Nanoseconds;
|
||||
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,
|
||||
opts.buffer_frames
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const Info = struct {
|
||||
sample_count: u32,
|
||||
sample_rate: u32,
|
||||
|
||||
pub fn getDuration(self: Info) Nanoseconds {
|
||||
return @as(Nanoseconds, self.sample_count) * std.time.ns_per_s / self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn getInfo(id: Data.Id) Info {
|
||||
const data = store.get(id);
|
||||
return Info{
|
||||
.sample_count = data.getSampleCount(),
|
||||
.sample_rate = data.getSampleRate(),
|
||||
};
|
||||
}
|
||||
|
||||
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.*);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Math = @import("lib").Math;
|
||||
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)];
|
||||
}
|
||||
@ -10,7 +10,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 Audio = @import("./audio.zig");
|
||||
const tracy = @import("tracy");
|
||||
const builtin = @import("builtin");
|
||||
const STBImage = @import("stb_image");
|
||||
@ -464,6 +464,7 @@ const Engine = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
graphics: Graphics,
|
||||
audio: Audio,
|
||||
|
||||
game_linking: GameLinking,
|
||||
game_state: GameState,
|
||||
@ -506,6 +507,7 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
|
||||
self.* = Engine{
|
||||
.allocator = opts.allocator,
|
||||
.graphics = undefined,
|
||||
.audio = undefined,
|
||||
.game_state = game_state,
|
||||
.game_linking = game_linking,
|
||||
.show_debug = false,
|
||||
@ -584,7 +586,7 @@ fn sokolInit(self: *Engine) !void {
|
||||
// }
|
||||
});
|
||||
|
||||
try Audio.init(.{
|
||||
try self.audio.init(.{
|
||||
.allocator = self.allocator,
|
||||
.logger = .{ .func = sokolLogCallback },
|
||||
});
|
||||
@ -601,7 +603,7 @@ fn sokolCleanup(self: *Engine) void {
|
||||
self.queued_events.deinit(self.allocator);
|
||||
self.game_state.runGameDeinit(self.game_linking.callbacks.deinit);
|
||||
self.game_state.deinit();
|
||||
Audio.deinit();
|
||||
self.audio.deinit();
|
||||
self.graphics.deinit(self.allocator);
|
||||
self.game_linking.deinit(self.allocator);
|
||||
}
|
||||
@ -661,9 +663,10 @@ fn sokolFrame(self: *Engine) !void {
|
||||
}
|
||||
}
|
||||
|
||||
for (tick_result.audio) |command| {
|
||||
try Audio.mixer.commands.push(command);
|
||||
}
|
||||
// TODO:
|
||||
// for (tick_result.audio) |command| {
|
||||
// try self.audio.mixer.commands.push(command);
|
||||
// }
|
||||
}
|
||||
|
||||
if (self.restart_game) {
|
||||
@ -749,10 +752,11 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
|
||||
imgui.textFmt("Draw commands: {}\n", .{
|
||||
frame.graphics_commands.items.len,
|
||||
});
|
||||
imgui.textFmt("Audio instances: {}/{}\n", .{
|
||||
Audio.mixer.instances.items.len,
|
||||
Audio.mixer.instances.capacity
|
||||
});
|
||||
// TODO:
|
||||
// imgui.textFmt("Audio instances: {}/{}\n", .{
|
||||
// Audio.mixer.instances.items.len,
|
||||
// Audio.mixer.instances.capacity
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@ -924,14 +928,14 @@ fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callco
|
||||
}
|
||||
}
|
||||
|
||||
fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||
fn sokolCleanupCallback(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
||||
|
||||
engine.sokolCleanup();
|
||||
}
|
||||
|
||||
fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||
fn sokolInitCallback(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
||||
|
||||
engine.sokolInit() catch |e| {
|
||||
log.err("sokolInit() failed: {}", .{e});
|
||||
@ -942,8 +946,8 @@ fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||
};
|
||||
}
|
||||
|
||||
fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(userdata));
|
||||
fn sokolFrameCallback(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const engine: *Engine = @alignCast(@ptrCast(user_data));
|
||||
|
||||
engine.sokolFrame() catch |e| {
|
||||
log.err("sokolFrame() failed: {}", .{e});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user