refactor audio subsystem
This commit is contained in:
parent
a99bf9c17b
commit
045617b763
23
assets/impact-sounds/License.txt
Normal file
23
assets/impact-sounds/License.txt
Normal 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
|
||||
BIN
assets/impact-sounds/footstep_concrete_000.ogg
Normal file
BIN
assets/impact-sounds/footstep_concrete_000.ogg
Normal file
Binary file not shown.
@ -10,7 +10,8 @@ pub fn build(b: *std.Build) !void {
|
||||
"sokoban/sokoban_tilesheet.png",
|
||||
"roboto-font/Roboto-Regular.ttf",
|
||||
"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 });
|
||||
|
||||
25
src/app.zig
25
src/app.zig
@ -53,7 +53,10 @@ player_direction: Direction,
|
||||
player_frame_index: u32,
|
||||
animation_timer: Platform.Nanoseconds,
|
||||
|
||||
sound: Audio.Sound.Id,
|
||||
footstep_buffer: Audio.BufferId,
|
||||
footstep_sound: Audio.SoundId = .nil,
|
||||
|
||||
sfx_bus: Audio.BusId,
|
||||
|
||||
const Tilesheet = struct {
|
||||
image: Gfx.ImageData,
|
||||
@ -164,10 +167,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
}
|
||||
}
|
||||
|
||||
const sound = Audio.initSound(.{
|
||||
.cb = .{ .sample = sinSampleCallback },
|
||||
.playing = false,
|
||||
.frequency = 1
|
||||
const footstep_sound = Audio.addBuffer(.{
|
||||
.vorbis = plt.assets.readFile("impact-sounds/footstep_concrete_000.ogg")
|
||||
});
|
||||
|
||||
self.* = App{
|
||||
@ -181,16 +182,13 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
.player_frame_index = 0,
|
||||
.animation_timer = 0,
|
||||
.tilesheet = tilesheet,
|
||||
.sound = sound
|
||||
.footstep_buffer = footstep_sound,
|
||||
.sfx_bus = Audio.addBus(.{ .label = "sfx" })
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn sinSampleCallback(sound: *Audio.Sound) f32 {
|
||||
return @sin(440 * sound.phase);
|
||||
}
|
||||
|
||||
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
const input = plt.input;
|
||||
|
||||
@ -212,8 +210,13 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
dir = dir.normalized();
|
||||
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
|
||||
|
||||
if (input.isKeyPressed(.F)) {
|
||||
Audio.stop(self.footstep_sound);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -1,47 +1,65 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.audio);
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const sapp = sokol.app;
|
||||
const saudio = sokol.audio;
|
||||
|
||||
const ImGUI = @import("imgui.zig");
|
||||
const tracy = @import("tracy");
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
const Math = @import("math");
|
||||
|
||||
const SlotMapType = @import("./slot_map.zig").SlotMapType;
|
||||
|
||||
const State = struct {
|
||||
io: Io,
|
||||
mutex: std.Io.Mutex,
|
||||
sounds: Sound.SlotMap,
|
||||
volume: f32 = 0.1,
|
||||
running: std.atomic.Value(bool) = .init(false),
|
||||
const Bus = struct {
|
||||
label: ?[]const u8 = null,
|
||||
volume: f32 = 1,
|
||||
parent_id: ?Bus.Id = null,
|
||||
|
||||
temp_buffer: []f32
|
||||
pub const Id = packed struct {
|
||||
index: u16,
|
||||
|
||||
pub const nil = Id{ .index = std.math.maxInt(u16) };
|
||||
};
|
||||
};
|
||||
pub const BusId = Bus.Id;
|
||||
|
||||
const Buffer = struct {
|
||||
samples: Samples,
|
||||
|
||||
const Samples = struct {
|
||||
len: usize,
|
||||
left: [*]const f32,
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var g_state: State = undefined;
|
||||
pub const Id = packed struct {
|
||||
index: u16,
|
||||
|
||||
pub const InitOptions = struct {
|
||||
logger: saudio.Logger = .{},
|
||||
buffer_frames: u32 = 512,
|
||||
max_sounds: usize = 128,
|
||||
pub const nil = Id{ .index = std.math.maxInt(u16) };
|
||||
};
|
||||
};
|
||||
pub const BufferId = Buffer.Id;
|
||||
|
||||
pub const Sound = struct {
|
||||
cb: Callback,
|
||||
userdata: ?*anyopaque,
|
||||
volume: f32,
|
||||
looping: bool,
|
||||
adsr: ADSR,
|
||||
playing: bool,
|
||||
|
||||
phase_increment: f32 = 0,
|
||||
duration_frames: u32 = 0,
|
||||
|
||||
frame_index: u32 = 0,
|
||||
phase: f32 = 0,
|
||||
const Sound = struct {
|
||||
cursor: usize,
|
||||
buffer: BufferId,
|
||||
volume: f32 = 1,
|
||||
loop: bool = false,
|
||||
bus: BusId,
|
||||
|
||||
pub const ADSR = struct {
|
||||
attack: f32,
|
||||
@ -49,7 +67,7 @@ pub const Sound = struct {
|
||||
sustain: f32,
|
||||
release: f32,
|
||||
|
||||
pub const default = ADSR{
|
||||
pub const identity = ADSR{
|
||||
.attack = 0,
|
||||
.decay = 0,
|
||||
.sustain = 1,
|
||||
@ -57,38 +75,84 @@ pub const Sound = struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub const Callback = union(enum) {
|
||||
sample: *const fn(sound: *Sound) f32,
|
||||
block: *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void,
|
||||
const SlotMap = SlotMapType(u16, u8, Sound);
|
||||
const Id = SlotMap.Id;
|
||||
};
|
||||
pub const SoundId = Sound.Id;
|
||||
|
||||
const ThreadState = struct {
|
||||
// When modifying any fields in this struct, you must first acquire the mutex.
|
||||
mutex: std.Io.Mutex,
|
||||
|
||||
sounds: Sound.SlotMap,
|
||||
};
|
||||
|
||||
const SlotMap = SlotMapType(u8, u16, Sound);
|
||||
pub const Id = SlotMap.Id;
|
||||
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 {
|
||||
const channels = 1; // TODO: Stereo audio
|
||||
const self = &g_state;
|
||||
|
||||
const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds);
|
||||
errdefer gpa.free(sounds_buffer);
|
||||
var buses = try std.ArrayList(Bus).initCapacity(gpa, opts.max_buses);
|
||||
errdefer buses.deinit(gpa);
|
||||
|
||||
const temp_buffer = try gpa.alloc(f32, opts.buffer_frames);
|
||||
errdefer gpa.free(temp_buffer);
|
||||
var buffers = try std.ArrayList(Buffer).initCapacity(gpa, opts.max_buffers);
|
||||
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{
|
||||
.io = io,
|
||||
.gpa = gpa,
|
||||
.buffers = buffers,
|
||||
.buses = buses,
|
||||
.vorbis_alloc_buffer = vorbis_alloc_buffer,
|
||||
.thread_state = .{
|
||||
.mutex = .init,
|
||||
.sounds = .init(sounds_buffer),
|
||||
.temp_buffer = temp_buffer,
|
||||
.sounds = .init(sounds),
|
||||
}
|
||||
};
|
||||
self.running.store(true, .seq_cst);
|
||||
|
||||
main_bus = addBus(.{
|
||||
.label = "main"
|
||||
});
|
||||
|
||||
const channels = 2;
|
||||
saudio.setup(.{
|
||||
.stream_cb = sokolStreamCallback,
|
||||
.logger = opts.logger,
|
||||
.num_channels = channels,
|
||||
.buffer_frames = @intCast(opts.buffer_frames),
|
||||
.sample_rate = g_sample_rate
|
||||
});
|
||||
|
||||
log.debug("Init:", .{});
|
||||
@ -104,146 +168,359 @@ pub fn deinit(gpa: std.mem.Allocator) void {
|
||||
|
||||
saudio.shutdown();
|
||||
|
||||
gpa.free(self.sounds.slots.allocatedSlice());
|
||||
gpa.free(self.temp_buffer);
|
||||
for (self.buffers.items) |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 {
|
||||
return @floatFromInt(saudio.sampleRate());
|
||||
}
|
||||
|
||||
pub const SoundOptions = struct {
|
||||
cb: Sound.Callback,
|
||||
userdata: ?*anyopaque = null,
|
||||
adsr: Sound.ADSR = .default,
|
||||
volume: f32 = 1,
|
||||
playing: bool = true,
|
||||
looping: bool = false,
|
||||
|
||||
frequency: ?f32 = null,
|
||||
duration_ns: ?u64 = null
|
||||
const BufferData = union(enum) {
|
||||
raw: struct {
|
||||
samples_mono: []f32,
|
||||
sample_rate: u32
|
||||
},
|
||||
vorbis: []const u8
|
||||
};
|
||||
|
||||
pub fn initSound(opts: SoundOptions) Sound.Id {
|
||||
var self = &g_state;
|
||||
// Source: https://amini-allight.org/post/game-audio-programming-tutorial-part-6
|
||||
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;
|
||||
defer self.mutex.unlock(self.io);
|
||||
const b_index: isize = @intFromFloat(elapsed * @as(f32, @floatFromInt(input.len)));
|
||||
const a_index = b_index - 1;
|
||||
const c_index = b_index + 1;
|
||||
const d_index = b_index + 2;
|
||||
|
||||
const id = self.sounds.insertUndefined() catch {
|
||||
log.warn("Sound limit reached! limit: {}", .{self.sounds.slots.capacity});
|
||||
const a = if (a_index >= 0 and a_index < input.len) input[@intCast(a_index)] else input[@intCast(b_index)];
|
||||
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;
|
||||
};
|
||||
|
||||
var phase_increment: f32 = 0;
|
||||
if (opts.frequency) |frequency| {
|
||||
phase_increment = frequency * 2 * std.math.pi / getSampleRate();
|
||||
}
|
||||
|
||||
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
|
||||
const buffer_id = BufferId{ .index = @intCast(self.buffers.items.len) };
|
||||
const buffer = self.buffers.addOneAssumeCapacity();
|
||||
buffer.* = Buffer{
|
||||
.samples = samples
|
||||
};
|
||||
|
||||
return id;
|
||||
assert(buffer_id != BufferId.nil);
|
||||
|
||||
return buffer_id;
|
||||
}
|
||||
|
||||
pub fn deinitSound(id: Sound.Id) void {
|
||||
var self = &g_state;
|
||||
pub fn addBus(opts: Bus) BusId {
|
||||
const self = &g_state;
|
||||
|
||||
if (self.sounds.get(id)) |sound| {
|
||||
self.mutex.lock(self.io) catch return;
|
||||
defer self.mutex.unlock(self.io);
|
||||
if (self.buses.items.len == self.buses.capacity) {
|
||||
log.warn("Max audio buses reached, limit: {}", .{self.buses.capacity});
|
||||
return .nil;
|
||||
}
|
||||
|
||||
_ = sound; // autofix
|
||||
self.sounds.removeAssumeExists(id);
|
||||
const bus_id = BusId{ .index = @intCast(self.buses.items.len) };
|
||||
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 {
|
||||
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 {
|
||||
fn sokolStream(output_buffer: [*c]f32, num_frames: u32, num_channels: u32) !void {
|
||||
const self = &g_state;
|
||||
|
||||
if (!self.running.load(.seq_cst)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try self.mutex.lock(self.io);
|
||||
defer self.mutex.unlock(self.io);
|
||||
const mutex = &self.thread_state.mutex;
|
||||
const sounds = &self.thread_state.sounds;
|
||||
const buses = self.buses;
|
||||
|
||||
assert(num_channels == 1);
|
||||
assert(self.temp_buffer.len >= num_frames);
|
||||
try mutex.lock(self.io);
|
||||
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();
|
||||
while (iter.next()) |id| {
|
||||
const sound = self.sounds.getAssumeExists(id);
|
||||
if (!sound.playing) {
|
||||
continue;
|
||||
var sound_iter = sounds.iterator();
|
||||
while (sound_iter.next()) |sound_id| {
|
||||
const sound = sounds.getAssumeExists(sound_id);
|
||||
const buffer = &self.buffers.items[sound.buffer.index];
|
||||
|
||||
const bus_id = sound.bus;
|
||||
const bus = buses.items[bus_id.index];
|
||||
_ = bus; // autofix
|
||||
const bus_volume = busMultipliedVolume(bus_id);
|
||||
|
||||
const volume = sound.volume * bus_volume;
|
||||
|
||||
const left_channel = buffer.samples.left;
|
||||
const right_channel = buffer.samples.right orelse buffer.samples.left;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var samples = std.ArrayList(f32).initBuffer(self.temp_buffer[0..num_frames]);
|
||||
switch (sound.cb) {
|
||||
.block => |cb| {
|
||||
cb(sound, &samples);
|
||||
bumpSoundCursor(sound, @intCast(samples.items.len));
|
||||
},
|
||||
.sample => |cb| {
|
||||
samples.expandToCapacity();
|
||||
for (samples.items) |*sample| {
|
||||
sample.* = cb(sound);
|
||||
bumpSoundCursor(sound, 1);
|
||||
if (!sound.loop) {
|
||||
break;
|
||||
} else {
|
||||
if (sound.cursor == buffer.samples.len) {
|
||||
sound.cursor = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (0..samples.items.len) |i| {
|
||||
buffer[i] += samples.items[i] * sound.volume * self.volume;
|
||||
if (!sound.loop and sound.cursor == buffer.samples.len) {
|
||||
sounds.removeAssumeExists(sound_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -503,3 +503,59 @@ pub fn collapsingHeader(label: []const u8) bool {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -141,6 +141,7 @@ fn PlatformType(App: type) type {
|
||||
|
||||
Gfx.flush(.{});
|
||||
Gfx.showDebug();
|
||||
Audio.showDebug();
|
||||
|
||||
ImGUI.endFrame();
|
||||
Gfx.endFrame();
|
||||
|
||||
@ -129,7 +129,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
||||
|
||||
pub fn unusedCapacity(self: *Self) usize {
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user