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",
|
"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 });
|
||||||
|
|||||||
25
src/app.zig
25
src/app.zig
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
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 {
|
pub const nil = Id{ .index = std.math.maxInt(u16) };
|
||||||
logger: saudio.Logger = .{},
|
|
||||||
buffer_frames: u32 = 512,
|
|
||||||
max_sounds: usize = 128,
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
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 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);
|
const State = struct {
|
||||||
pub const Id = SlotMap.Id;
|
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,
|
||||||
|
.gpa = gpa,
|
||||||
|
.buffers = buffers,
|
||||||
|
.buses = buses,
|
||||||
|
.vorbis_alloc_buffer = vorbis_alloc_buffer,
|
||||||
|
.thread_state = .{
|
||||||
.mutex = .init,
|
.mutex = .init,
|
||||||
.sounds = .init(sounds_buffer),
|
.sounds = .init(sounds),
|
||||||
.temp_buffer = temp_buffer,
|
}
|
||||||
};
|
};
|
||||||
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;
|
|
||||||
|
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]);
|
if (!sound.loop) {
|
||||||
switch (sound.cb) {
|
break;
|
||||||
.block => |cb| {
|
} else {
|
||||||
cb(sound, &samples);
|
if (sound.cursor == buffer.samples.len) {
|
||||||
bumpSoundCursor(sound, @intCast(samples.items.len));
|
sound.cursor = 0;
|
||||||
},
|
|
||||||
.sample => |cb| {
|
|
||||||
samples.expandToCapacity();
|
|
||||||
for (samples.items) |*sample| {
|
|
||||||
sample.* = cb(sound);
|
|
||||||
bumpSoundCursor(sound, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user