From 2a552529427e87cd6f3c06acb0bfb112e6dc8ce2 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 28 Jan 2026 07:24:10 +0200 Subject: [PATCH] add audio volume --- src/assets.zig | 21 ++++++++++- src/engine/audio/data.zig | 10 ++++++ src/engine/audio/mixer.zig | 35 +++++++++++++++---- src/engine/audio/root.zig | 24 ++++++++++++- src/engine/frame.zig | 10 ++---- src/engine/math.zig | 22 ++++++++++++ src/engine/root.zig | 4 ++- src/game.zig | 71 ++++++++++++++++++++++++++++++-------- 8 files changed, 165 insertions(+), 32 deletions(-) diff --git a/src/assets.zig b/src/assets.zig index 7f493b8..9d07621 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -18,6 +18,8 @@ const FontName = enum { const EnumArray = std.EnumArray(FontName, Gfx.Font.Id); }; +arena: std.heap.ArenaAllocator, + font_id: FontName.EnumArray, wood01: Audio.Data.Id, map: tiled.Tilemap, @@ -26,8 +28,12 @@ tileset_texture: Gfx.TextureId, players_texture: Gfx.TextureId, tile_size: Engine.Vec2, player_size: Engine.Vec2, +move_sound: []Audio.Data.Id, 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(.{ .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.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{ + .arena = arena, .font_id = font_id_array, .wood01 = wood01, .map = map, @@ -90,11 +107,13 @@ pub fn init(gpa: std.mem.Allocator) !Assets { .tileset_texture = tileset_texture, .tile_size = .init(16, 16), .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 { self.map.deinit(); self.tilesets.deinit(gpa); + self.arena.deinit(); } diff --git a/src/engine/audio/data.zig b/src/engine/audio/data.zig index 36c9d67..0fd8bfe 100644 --- a/src/engine/audio/data.zig +++ b/src/engine/audio/data.zig @@ -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) { _ }; }; diff --git a/src/engine/audio/mixer.zig b/src/engine/audio/mixer.zig index 31b0f89..2cc6daf 100644 --- a/src/engine/audio/mixer.zig +++ b/src/engine/audio/mixer.zig @@ -13,13 +13,17 @@ const Mixer = @This(); pub const Instance = struct { data_id: AudioData.Id, + volume: f32 = 0, cursor: u32 = 0, }; pub const Command = union(enum) { - play: struct { - data_id: AudioData.Id, - }, + pub const Play = struct { + id: AudioData.Id, + volume: f32 = 1 + }; + + play: Play, pub const RingBuffer = struct { // 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), commands: Command.RingBuffer, +working_buffer: []f32, pub fn init( gpa: Allocator, max_instances: u32, - max_commands: u32 + max_commands: u32, + working_buffer_size: u32 ) !Mixer { var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances); errdefer instances.deinit(gpa); @@ -74,7 +80,11 @@ pub fn init( 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 @@ -85,6 +95,7 @@ pub fn init( 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 { @@ -95,8 +106,15 @@ pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_ch 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.data_id + .data_id = opts.id, + .volume = volume, }) 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()); @memset(buffer, 0); + assert(self.working_buffer.len >= num_frames); - // var written: u32 = 0; for (self.instances.items) |*instance| { 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); } diff --git a/src/engine/audio/root.zig b/src/engine/audio/root.zig index 6c18a1c..a1d9365 100644 --- a/src/engine/audio/root.zig +++ b/src/engine/audio/root.zig @@ -13,6 +13,7 @@ pub const Mixer = @import("./mixer.zig"); pub const Command = Mixer.Command; +const Nanoseconds = @import("../root.zig").Nanoseconds; const sokol = @import("sokol"); 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, }); - 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(.{ .logger = opts.logger, @@ -64,6 +69,23 @@ 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; diff --git a/src/engine/frame.zig b/src/engine/frame.zig index 0d879ef..7b2e0bc 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -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 { 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(.{ - .play = .{ - .data_id = options.id - } + .play = options, }); } diff --git a/src/engine/math.zig b/src/engine/math.zig index e38d759..10b1116 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -9,6 +9,28 @@ pub const bytes_per_kb = 1000; pub const bytes_per_mb = bytes_per_kb * 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 { x: f32, y: f32, diff --git a/src/engine/root.zig b/src/engine/root.zig index faacfcc..b944641 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -130,8 +130,10 @@ fn sokolInit(self: *Engine) !void { .logger = .{ .func = sokolLogCallback }, }); + const seed: u64 = @bitCast(std.time.milliTimestamp()); + 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 { diff --git a/src/game.zig b/src/game.zig index aec3b9e..c169ac9 100644 --- a/src/game.zig +++ b/src/game.zig @@ -5,14 +5,19 @@ const assert = std.debug.assert; const Assets = @import("./assets.zig"); const Engine = @import("./engine/root.zig"); +const Nanoseconds = Engine.Nanoseconds; const imgui = Engine.imgui; const Vec2 = Engine.Vec2; const Rect = Engine.Math.Rect; const rgb = Engine.Math.rgb; +const Range = Engine.Math.Range; const TextureId = Engine.Graphics.TextureId; +const AudioId = Engine.Audio.Data.Id; const Game = @This(); +const RNGState = std.Random.DefaultPrng; + const Animation = struct { texture: TextureId, 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, gpa: Allocator, +rng: RNGState, assets: *Assets, player: Vec2, player_anim_state: Animation.State = .default, last_faced_left: bool = false, +player_walk_sound: AudioBundle = .empty, 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); errdefer arena.deinit(); @@ -72,11 +118,7 @@ pub fn init(gpa: Allocator, assets: *Assets) !Game { }, .{ .uv = getUVFromTilemap(tilemap_size, tile_size, 1, 0), - .duration = 0.1, - }, - .{ - .uv = getUVFromTilemap(tilemap_size, tile_size, 2, 0), - .duration = 0.15, + .duration = 0.2, } }), }; @@ -86,7 +128,8 @@ pub fn init(gpa: Allocator, assets: *Assets) !Game { .gpa = gpa, .assets = assets, .player = findSpawnpoint(assets) orelse .init(0, 0), - .player_anim = player_anim + .player_anim = player_anim, + .rng = RNGState.init(seed) }; } @@ -161,7 +204,7 @@ fn drawTilemap(self: *Game, frame: *Engine.Frame) void { .color = rgb(255, 255, 255), .texture = .{ .id = self.assets.tileset_texture, - .uv = getUVFromTilemapByID(tilemap_size, tile_size,tile.id) + .uv = getUVFromTilemapByID(tilemap_size, tile_size, tile.id) } }); } @@ -202,16 +245,16 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { if (dir.x != 0 or dir.y != 0) { 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 { 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)); const regular_font = self.assets.font_id.get(.regular);