add audio volume

This commit is contained in:
Rokas Puzonas 2026-01-28 07:24:10 +02:00
parent e8b565508f
commit 2a55252942
8 changed files with 165 additions and 32 deletions

View File

@ -18,6 +18,8 @@ const FontName = enum {
const EnumArray = std.EnumArray(FontName, Gfx.Font.Id); const EnumArray = std.EnumArray(FontName, Gfx.Font.Id);
}; };
arena: std.heap.ArenaAllocator,
font_id: FontName.EnumArray, font_id: FontName.EnumArray,
wood01: Audio.Data.Id, wood01: Audio.Data.Id,
map: tiled.Tilemap, map: tiled.Tilemap,
@ -26,8 +28,12 @@ tileset_texture: Gfx.TextureId,
players_texture: Gfx.TextureId, players_texture: Gfx.TextureId,
tile_size: Engine.Vec2, tile_size: Engine.Vec2,
player_size: Engine.Vec2, player_size: Engine.Vec2,
move_sound: []Audio.Data.Id,
pub fn init(gpa: std.mem.Allocator) !Assets { pub fn init(gpa: std.mem.Allocator) !Assets {
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const font_id_array: FontName.EnumArray = .init(.{ const font_id_array: FontName.EnumArray = .init(.{
.regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")), .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
.bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")), .bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")),
@ -82,7 +88,18 @@ pub fn init(gpa: std.mem.Allocator) !Assets {
} }
}); });
const move_c = try Audio.load(.{
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg"),
.format = .vorbis,
});
const move_d = try Audio.load(.{
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-d.ogg"),
.format = .vorbis,
});
const move_sound = try arena.allocator().dupe(Audio.Data.Id, &.{ move_c, move_d });
return Assets{ return Assets{
.arena = arena,
.font_id = font_id_array, .font_id = font_id_array,
.wood01 = wood01, .wood01 = wood01,
.map = map, .map = map,
@ -90,11 +107,13 @@ pub fn init(gpa: std.mem.Allocator) !Assets {
.tileset_texture = tileset_texture, .tileset_texture = tileset_texture,
.tile_size = .init(16, 16), .tile_size = .init(16, 16),
.players_texture = players_texture, .players_texture = players_texture,
.player_size = .init(24, 24) .player_size = .init(24, 24),
.move_sound = move_sound
}; };
} }
pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void { pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void {
self.map.deinit(); self.map.deinit();
self.tilesets.deinit(gpa); self.tilesets.deinit(gpa);
self.arena.deinit();
} }

View File

@ -60,5 +60,15 @@ pub const Data = union(enum) {
}; };
} }
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 = enum (u16) { _ }; pub const Id = enum (u16) { _ };
}; };

View File

@ -13,13 +13,17 @@ const Mixer = @This();
pub const Instance = struct { pub const Instance = struct {
data_id: AudioData.Id, data_id: AudioData.Id,
volume: f32 = 0,
cursor: u32 = 0, cursor: u32 = 0,
}; };
pub const Command = union(enum) { pub const Command = union(enum) {
play: struct { pub const Play = struct {
data_id: AudioData.Id, id: AudioData.Id,
}, volume: f32 = 1
};
play: Play,
pub const RingBuffer = struct { pub const RingBuffer = struct {
// TODO: This ring buffer will work in a single producer single consumer configuration // TODO: This ring buffer will work in a single producer single consumer configuration
@ -62,11 +66,13 @@ pub const Command = union(enum) {
instances: std.ArrayList(Instance), instances: std.ArrayList(Instance),
commands: Command.RingBuffer, commands: Command.RingBuffer,
working_buffer: []f32,
pub fn init( pub fn init(
gpa: Allocator, gpa: Allocator,
max_instances: u32, max_instances: u32,
max_commands: u32 max_commands: u32,
working_buffer_size: u32
) !Mixer { ) !Mixer {
var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances); var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances);
errdefer instances.deinit(gpa); errdefer instances.deinit(gpa);
@ -74,7 +80,11 @@ pub fn init(
const commands = try gpa.alloc(Command, max_commands); const commands = try gpa.alloc(Command, max_commands);
errdefer gpa.free(commands); errdefer gpa.free(commands);
const working_buffer = try gpa.alloc(f32, working_buffer_size);
errdefer gpa.free(working_buffer);
return Mixer{ return Mixer{
.working_buffer = working_buffer,
.instances = instances, .instances = instances,
.commands = .{ .commands = .{
.items = commands .items = commands
@ -85,6 +95,7 @@ pub fn init(
pub fn deinit(self: *Mixer, gpa: Allocator) void { pub fn deinit(self: *Mixer, gpa: Allocator) void {
self.instances.deinit(gpa); self.instances.deinit(gpa);
gpa.free(self.commands.items); gpa.free(self.commands.items);
gpa.free(self.working_buffer);
} }
pub fn queue(self: *Mixer, command: Command) void { pub fn queue(self: *Mixer, command: Command) void {
@ -95,8 +106,15 @@ pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_ch
while (self.commands.pop()) |command| { while (self.commands.pop()) |command| {
switch (command) { switch (command) {
.play => |opts| { .play => |opts| {
const volume = @max(opts.volume, 0);
if (volume == 0) {
log.warn("Attempt to play audio with 0 volume", .{});
continue;
}
self.instances.appendBounded(.{ self.instances.appendBounded(.{
.data_id = opts.data_id .data_id = opts.id,
.volume = volume,
}) catch log.warn("Maximum number of audio instances reached!", .{}); }) catch log.warn("Maximum number of audio instances reached!", .{});
} }
} }
@ -106,11 +124,14 @@ pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_ch
const sample_rate: u32 = @intCast(saudio.sampleRate()); const sample_rate: u32 = @intCast(saudio.sampleRate());
@memset(buffer, 0); @memset(buffer, 0);
assert(self.working_buffer.len >= num_frames);
// var written: u32 = 0;
for (self.instances.items) |*instance| { for (self.instances.items) |*instance| {
const audio_data = store.get(instance.data_id); const audio_data = store.get(instance.data_id);
const samples = audio_data.streamChannel(buffer[0..num_frames], instance.cursor, 0, sample_rate); 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); instance.cursor += @intCast(samples.len);
} }

View File

@ -13,6 +13,7 @@ pub const Mixer = @import("./mixer.zig");
pub const Command = Mixer.Command; pub const Command = Mixer.Command;
const Nanoseconds = @import("../root.zig").Nanoseconds;
const sokol = @import("sokol"); const sokol = @import("sokol");
const saudio = sokol.audio; const saudio = sokol.audio;
@ -38,7 +39,11 @@ pub fn init(opts: Options) !void {
.max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size, .max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size,
}); });
mixer = try Mixer.init(gpa, opts.max_instances, opts.max_instances); mixer = try Mixer.init(gpa,
opts.max_instances,
opts.max_instances,
opts.buffer_frames
);
saudio.setup(.{ saudio.setup(.{
.logger = opts.logger, .logger = opts.logger,
@ -64,6 +69,23 @@ pub fn load(opts: Store.LoadOptions) !Data.Id {
return try store.load(opts); 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 { fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void {
if (stopped) { if (stopped) {
return; return;

View File

@ -163,10 +163,6 @@ pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState {
}; };
} }
pub const PlayAudioOptions = struct {
id: AudioData.Id,
};
fn pushAudioCommand(self: *Frame, command: AudioCommand) void { fn pushAudioCommand(self: *Frame, command: AudioCommand) void {
const arena = self.arena.allocator(); const arena = self.arena.allocator();
@ -191,11 +187,9 @@ pub fn prependGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
}; };
} }
pub fn playAudio(self: *Frame, options: PlayAudioOptions) !void { pub fn playAudio(self: *Frame, options: AudioCommand.Play) void {
self.pushAudioCommand(.{ self.pushAudioCommand(.{
.play = .{ .play = options,
.data_id = options.id
}
}); });
} }

View File

@ -9,6 +9,28 @@ pub const bytes_per_kb = 1000;
pub const bytes_per_mb = bytes_per_kb * 1000; pub const bytes_per_mb = bytes_per_kb * 1000;
pub const bytes_per_gb = bytes_per_mb * 1000; pub const bytes_per_gb = bytes_per_mb * 1000;
pub const Range = struct {
from: f32,
to: f32,
pub const zero = init(0, 0);
pub fn init(from: f32, to: f32) Range {
return Range{
.from = from,
.to = to
};
}
pub fn getSize(self: Range) f32 {
return @abs(self.from - self.to);
}
pub fn random(self: Range, rng: std.Random) f32 {
return self.from + rng.float(f32) * (self.to - self.from);
}
};
pub const Vec2 = extern struct { pub const Vec2 = extern struct {
x: f32, x: f32,
y: f32, y: f32,

View File

@ -130,8 +130,10 @@ fn sokolInit(self: *Engine) !void {
.logger = .{ .func = sokolLogCallback }, .logger = .{ .func = sokolLogCallback },
}); });
const seed: u64 = @bitCast(std.time.milliTimestamp());
self.assets = try Assets.init(self.allocator); self.assets = try Assets.init(self.allocator);
self.game = try Game.init(self.allocator, &self.assets); self.game = try Game.init(self.allocator, seed, &self.assets);
} }
fn sokolCleanup(self: *Engine) void { fn sokolCleanup(self: *Engine) void {

View File

@ -5,14 +5,19 @@ const assert = std.debug.assert;
const Assets = @import("./assets.zig"); const Assets = @import("./assets.zig");
const Engine = @import("./engine/root.zig"); const Engine = @import("./engine/root.zig");
const Nanoseconds = Engine.Nanoseconds;
const imgui = Engine.imgui; const imgui = Engine.imgui;
const Vec2 = Engine.Vec2; const Vec2 = Engine.Vec2;
const Rect = Engine.Math.Rect; const Rect = Engine.Math.Rect;
const rgb = Engine.Math.rgb; const rgb = Engine.Math.rgb;
const Range = Engine.Math.Range;
const TextureId = Engine.Graphics.TextureId; const TextureId = Engine.Graphics.TextureId;
const AudioId = Engine.Audio.Data.Id;
const Game = @This(); const Game = @This();
const RNGState = std.Random.DefaultPrng;
const Animation = struct { const Animation = struct {
texture: TextureId, texture: TextureId,
frames: []Frame, frames: []Frame,
@ -46,17 +51,58 @@ const Animation = struct {
}; };
}; };
const AudioBundle = struct {
cooldown_until: ?Engine.Nanoseconds,
const empty = AudioBundle{
.cooldown_until = null,
};
const PlayOptions = struct {
sounds: []const AudioId,
volume: Range = .init(1, 1),
fixed_delay: Range = .zero,
rng: std.Random
};
pub fn play(self: *AudioBundle, frame: *Engine.Frame, opts: PlayOptions) void {
if (opts.sounds.len == 0) {
return;
}
if (self.cooldown_until) |cooldown_until| {
if (cooldown_until > frame.time_ns) {
return;
}
}
const sound_index = opts.rng.uintLessThan(usize, opts.sounds.len);
const sound = opts.sounds[sound_index];
frame.playAudio(.{
.id = sound,
.volume = opts.volume.random(opts.rng)
});
const sound_info = Engine.Audio.getInfo(sound);
var duration = sound_info.getDuration();
duration += @intFromFloat(opts.fixed_delay.random(opts.rng) * std.time.ns_per_s);
self.cooldown_until = frame.time_ns + duration;
}
};
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
gpa: Allocator, gpa: Allocator,
rng: RNGState,
assets: *Assets, assets: *Assets,
player: Vec2, player: Vec2,
player_anim_state: Animation.State = .default, player_anim_state: Animation.State = .default,
last_faced_left: bool = false, last_faced_left: bool = false,
player_walk_sound: AudioBundle = .empty,
player_anim: Animation, player_anim: Animation,
pub fn init(gpa: Allocator, assets: *Assets) !Game { pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
var arena = std.heap.ArenaAllocator.init(gpa); var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit(); errdefer arena.deinit();
@ -72,11 +118,7 @@ pub fn init(gpa: Allocator, assets: *Assets) !Game {
}, },
.{ .{
.uv = getUVFromTilemap(tilemap_size, tile_size, 1, 0), .uv = getUVFromTilemap(tilemap_size, tile_size, 1, 0),
.duration = 0.1, .duration = 0.2,
},
.{
.uv = getUVFromTilemap(tilemap_size, tile_size, 2, 0),
.duration = 0.15,
} }
}), }),
}; };
@ -86,7 +128,8 @@ pub fn init(gpa: Allocator, assets: *Assets) !Game {
.gpa = gpa, .gpa = gpa,
.assets = assets, .assets = assets,
.player = findSpawnpoint(assets) orelse .init(0, 0), .player = findSpawnpoint(assets) orelse .init(0, 0),
.player_anim = player_anim .player_anim = player_anim,
.rng = RNGState.init(seed)
}; };
} }
@ -202,16 +245,16 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void {
if (dir.x != 0 or dir.y != 0) { if (dir.x != 0 or dir.y != 0) {
self.player_anim_state.update(self.player_anim, dt); self.player_anim_state.update(self.player_anim, dt);
self.player_walk_sound.play(frame, .{
.sounds = self.assets.move_sound,
.fixed_delay = .init(0.1, 0.15),
.volume = .init(0.025, 0.03),
.rng = self.rng.random()
});
} else { } else {
self.player_anim_state.frame_index = 0; self.player_anim_state.frame_index = 0;
} }
// if (dir.x != 0 or dir.y != 0) {
// try frame.playAudio(.{
// .id = self.assets.wood01
// });
// }
self.player = self.player.add(dir.multiplyScalar(50 * dt)); self.player = self.player.add(dir.multiplyScalar(50 * dt));
const regular_font = self.assets.font_id.get(.regular); const regular_font = self.assets.font_id.get(.regular);