add audio volume
This commit is contained in:
parent
e8b565508f
commit
2a55252942
@ -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();
|
||||
}
|
||||
|
||||
@ -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) { _ };
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
71
src/game.zig
71
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user