diff --git a/assets/cubyz/sounds/audio/block_break.ogg b/assets/cubyz/sounds/audio/block_break.ogg new file mode 100644 index 0000000000..ee9ce48c19 Binary files /dev/null and b/assets/cubyz/sounds/audio/block_break.ogg differ diff --git a/assets/cubyz/sounds/audio/block_hit.ogg b/assets/cubyz/sounds/audio/block_hit.ogg new file mode 100644 index 0000000000..884e628513 Binary files /dev/null and b/assets/cubyz/sounds/audio/block_hit.ogg differ diff --git a/assets/cubyz/sounds/audio/jump.ogg b/assets/cubyz/sounds/audio/jump.ogg new file mode 100644 index 0000000000..884e628513 Binary files /dev/null and b/assets/cubyz/sounds/audio/jump.ogg differ diff --git a/assets/cubyz/sounds/block_break.zig.zon b/assets/cubyz/sounds/block_break.zig.zon new file mode 100644 index 0000000000..a3fb5c5ad7 --- /dev/null +++ b/assets/cubyz/sounds/block_break.zig.zon @@ -0,0 +1,4 @@ +.{ + .audio = "cubyz:block_break", + .volume = 1.25, +} diff --git a/assets/cubyz/sounds/block_hit.zig.zon b/assets/cubyz/sounds/block_hit.zig.zon new file mode 100644 index 0000000000..640239eed0 --- /dev/null +++ b/assets/cubyz/sounds/block_hit.zig.zon @@ -0,0 +1,4 @@ +.{ + .audio = "cubyz:block_hit", + .volume = 0.25, +} diff --git a/assets/cubyz/sounds/jump.zig.zon b/assets/cubyz/sounds/jump.zig.zon new file mode 100644 index 0000000000..049c3d34ac --- /dev/null +++ b/assets/cubyz/sounds/jump.zig.zon @@ -0,0 +1,4 @@ +.{ + .audio = "cubyz:jump", + .volume = 0.35, +} diff --git a/src/assets.zig b/src/assets.zig index 8ab0a792cb..7537683007 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -7,6 +7,7 @@ const migrations = @import("migrations.zig"); const blueprint = @import("blueprint.zig"); const Blueprint = blueprint.Blueprint; const particles = @import("particles.zig"); +const audio = @import("audio.zig"); const ZonElement = main.ZonElement; const biomes = main.server.terrain.biomes; const sbb = main.server.terrain.sbb; @@ -42,6 +43,8 @@ pub const Assets = struct { worldPresets: ZonHashMap, entityModelDescriptions: ZonHashMap, entityModelMigrations: ZonHashMap, + audioDatas: main.List([]const u8), + sounds: ZonHashMap, fn init() Assets { return .{ @@ -65,6 +68,8 @@ pub const Assets = struct { .worldPresets = .{}, .entityModelDescriptions = .{}, .entityModelMigrations = .{}, + .audioDatas = .empty, + .sounds = .{}, }; } fn deinit(self: *Assets, allocator: NeverFailingAllocator) void { @@ -88,6 +93,8 @@ pub const Assets = struct { self.worldPresets.deinit(allocator.allocator); self.entityModelDescriptions.deinit(allocator.allocator); self.entityModelMigrations.deinit(allocator.allocator); + self.audioDatas.deinit(allocator); + self.sounds.deinit(allocator.allocator); } fn clone(self: Assets, allocator: NeverFailingAllocator) Assets { return .{ @@ -111,6 +118,8 @@ pub const Assets = struct { .worldPresets = .{}, // Not accessible inside the world .entityModelDescriptions = self.entityModelDescriptions.clone(allocator.allocator) catch unreachable, .entityModelMigrations = self.entityModelMigrations.clone(allocator.allocator) catch unreachable, + .audioDatas = self.audioDatas.clone(allocator), + .sounds = self.sounds.clone(allocator.allocator) catch unreachable, }; } fn read(self: *Assets, allocator: NeverFailingAllocator, assetDir: main.files.Dir, assetPath: []const u8) void { @@ -133,12 +142,14 @@ pub const Assets = struct { addon.readAllZon(allocator, "particles", true, &self.particles, null); addon.readAllZon(allocator, "world_presets", true, &self.worldPresets, null); addon.readAllZon(allocator, "entity_models", true, &self.entityModelDescriptions, &self.entityModelMigrations); + addon.readAllAudio(allocator, "sounds/audio", ".ogg", &self.audioDatas); + addon.readAllZon(allocator, "sounds", true, &self.sounds, null); } } fn log(self: *Assets, typ: enum { common, world }) void { std.log.info( - "Finished {s} assets reading with {} blocks, {} items, {} procedural items, {} biomes, {} cave layers, {} structure tables, {} recipes, {} structure building blocks, {} blueprints, {} particles, {} world presets, block models {}, and {} block model ZONs", - .{@tagName(typ), self.blocks.count(), self.items.count(), self.proceduralItems.count(), self.biomes.count(), self.caveLayers.count(), self.structureTables.count(), self.recipes.count(), self.structureBuildingBlocks.count(), self.blueprints.count(), self.particles.count(), self.worldPresets.count(), self.blockModels.count(), self.blockModelsZon.count()}, + "Finished {s} assets reading with {} blocks, {} items, {} procedural items, {} biomes, {} cave layers, {} structure tables, {} recipes, {} structure building blocks, {} blueprints, {} particles, {} world presets, block models {}, {} block model ZONs, {} audio datas, and {} sounds", + .{@tagName(typ), self.blocks.count(), self.items.count(), self.proceduralItems.count(), self.biomes.count(), self.caveLayers.count(), self.structureTables.count(), self.recipes.count(), self.structureBuildingBlocks.count(), self.blueprints.count(), self.particles.count(), self.worldPresets.count(), self.blockModels.count(), self.blockModelsZon.count(), self.audioDatas.items.len, self.sounds.count()}, ); } @@ -337,6 +348,30 @@ pub const Assets = struct { output.put(allocator.allocator, id, string) catch unreachable; } } + + pub fn readAllAudio(addon: Addon, allocator: NeverFailingAllocator, subPath: []const u8, fileEnding: []const u8, output: *main.List([]const u8)) void { + var assetsDirectory = addon.dir.openIterableDir(subPath) catch |err| { + if (err != error.FileNotFound) { + std.log.err("Could not open addon directory {s}: {s}", .{subPath, @errorName(err)}); + } + return; + }; + defer assetsDirectory.close(); + var walker = assetsDirectory.walk(main.stackAllocator); + defer walker.deinit(); + + while (walker.next(main.io) catch |err| blk: { + std.log.err("Got error while iterating addon directory {s}: {s}", .{subPath, @errorName(err)}); + break :blk null; + }) |entry| { + if (entry.kind != .file) continue; + if (!std.ascii.endsWithIgnoreCase(entry.basename, fileEnding)) continue; + + const id = createAssetStringID(allocator, addon.name, "audio", entry.path) catch continue; + + output.append(allocator, id); + } + } }; }; @@ -709,6 +744,15 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale particles.ParticleManager.register(assetFolder, entry.key_ptr.*, entry.value_ptr.*); } + for (worldAssets.audioDatas.items) |entry| { + audio.registerAudioData(assetFolder, entry); + } + + iterator = worldAssets.sounds.iterator(); + while (iterator.next()) |entry| { + audio.registerSound(assetFolder, entry.key_ptr.*, entry.value_ptr.*); + } + // Biomes: var nextBiomeNumericId: u32 = 0; for (biomePalette.palette.items) |id| { @@ -791,6 +835,7 @@ pub fn unloadAssets() void { // MARK: unloadAssets() main.server.terrain.structures.reset(); main.models.reset(); main.particles.ParticleManager.reset(); + main.audio.reset(); main.rotation.reset(); main.Tag.resetTags(); main.entityModel.reset(); diff --git a/src/audio.zig b/src/audio.zig index 5aaff2dc2b..62a668622c 100644 --- a/src/audio.zig +++ b/src/audio.zig @@ -1,7 +1,10 @@ const std = @import("std"); const main = @import("main"); +const vec = main.vec; +const Vec3f = vec.Vec3f; const utils = main.utils; +const ZonElement = @import("zon.zig").ZonElement; const c = @import("c"); @@ -241,6 +244,23 @@ pub fn deinit() void { preferredMusic.len = 0; main.globalAllocator.free(activeMusicId); activeMusicId.len = 0; + + audioIdMap.deinit(main.globalAllocator.allocator); + audios.deinit(main.globalAllocator); + soundDataIdMap.deinit(main.globalAllocator.allocator); + soundDatas.deinit(main.globalAllocator); + activeSounds.deinit(main.globalAllocator); +} + +pub fn reset() void { + audioIdMap.clearRetainingCapacity(); + for (audios.items) |s| { + s.deinit(); + } + audios.clearRetainingCapacity(); + activeSounds.clearRetainingCapacity(); + soundDataIdMap.clearRetainingCapacity(); + soundDatas.clearRetainingCapacity(); } const currentMusic = struct { @@ -286,6 +306,76 @@ pub fn setMusic(music: []const u8) void { preferredMusic = main.globalAllocator.dupe(u8, music); } +const SoundData = struct { // MARK: Sounds + audioIndex: u32, + volume: f32 = 1, +}; + +const PlayingSound = struct { + pos: Vec3f = @splat(0), + audioIndex: u32, + bufPos: u32 = 0, + + volume: f32 = 1, + maxDistance: f32 = 10, + isSpatial: bool = false, +}; + +var audioIdMap: std.StringHashMapUnmanaged(u32) = .{}; +var audios: main.List(*AudioData) = .empty; +var soundDataIdMap: std.StringHashMapUnmanaged(u32) = .{}; +var soundDatas: main.List(SoundData) = .empty; +var activeSounds: main.List(PlayingSound) = .empty; + +pub fn getActiveSoundCount() u32 { + return @intCast(activeSounds.items.len); +} + +pub fn registerAudioData(_: []const u8, id: []const u8) void { + const audio = AudioData.init(id, "sounds/audio"); + audioIdMap.put(main.globalAllocator.allocator, id, @intCast(audios.items.len)) catch unreachable; + audios.append(main.globalAllocator, audio); + std.log.debug("Registered sound audio: {s}", .{id}); +} + +pub fn registerSound(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const audioId = zon.get([]const u8, "audio") orelse { + std.log.err("Sound Data audio was not specified: {s} ({s})", .{id, assetsFolder}); + return; + }; + + soundDataIdMap.put(main.globalAllocator.allocator, id, @intCast(soundDatas.items.len)) catch unreachable; + soundDatas.append(main.globalAllocator, SoundData{ + .audioIndex = audioIdMap.get(audioId) orelse { + std.log.err("Sound Data audio could not be found: {s} ({s}) audio ID: {s}", .{id, assetsFolder, audioId}); + return; + }, + .volume = zon.get(f32, "volume") orelse 1.0, + }); + + std.log.debug("Registered sound data: {s}", .{id}); +} + +fn addSound(id: []const u8, pos: Vec3f, maxDistance: f32, isSpatial: bool) void { + const idx = soundDataIdMap.get(id) orelse return; + const soundData = soundDatas.items[idx]; + activeSounds.append(main.globalAllocator, PlayingSound{ + .audioIndex = soundData.audioIndex, + .volume = soundData.volume, + .pos = pos, + .isSpatial = isSpatial, + .maxDistance = maxDistance, + }); +} + +pub fn playSound(id: []const u8) void { + addSound(id, @splat(0), 0, false); +} + +pub fn playSpatialSound(id: []const u8, pos: Vec3f, maxDistance: f32) void { + addSound(id, pos, maxDistance, true); +} + fn mixMusic(buffer: []f32) void { mutex.lock(); defer mutex.unlock(); @@ -333,6 +423,76 @@ fn mixMusic(buffer: []f32) void { } } +fn mixSound(buffer: []f32) void { + mutex.lock(); + defer mutex.unlock(); + + if (activeSounds.items.len == 0) return; + + const playerPos: Vec3f = @floatCast(main.game.Player.getPosBlocking()); + const playerForward = main.game.camera.direction; + const playerRight = vec.normalize(vec.cross(playerForward, Vec3f{0, 0, 1})); + + var i: u32 = 0; + var soundCount = activeSounds.items.len; + main: while (i < soundCount) { + var sound = activeSounds.items[i]; + const audioData = audios.items[sound.audioIndex]; + const soundBuffer = audioData.data; + + const notMonoInt: u32 = @intFromBool(audioData.channelType != .mono); + const bufferStep: u32 = 1 + notMonoInt; + + var leftVol: f32 = 1; + var rightVol: f32 = 1; + + if (sound.isSpatial) { + const toSound = sound.pos - playerPos; + const distance: f32 = vec.length(toSound); + + if (distance > sound.maxDistance) { + sound.bufPos += @intCast(if (audioData.channelType == .mono) @divFloor(buffer.len, 2) else buffer.len); + if (sound.bufPos >= soundBuffer.len) { + soundCount -= 1; + activeSounds.items[i] = activeSounds.items[soundCount]; + continue :main; + } + } + + const pan: f32 = vec.dot(toSound/@as(Vec3f, @splat(distance)), playerRight); + + const angle = (pan + 1)*0.25*std.math.pi; + leftVol = @cos(angle); + rightVol = @sin(angle); + + var volume: f32 = 1 - distance/sound.maxDistance; + + volume = if (volume < 0) 0 else volume; + leftVol *= volume; + rightVol *= volume; + } + + var j: usize = 0; + while (j < buffer.len) : (j += 2) { + const amplitude: f32 = main.settings.soundVolume*sound.volume; + + buffer[j] += soundBuffer[sound.bufPos]*amplitude*leftVol; + buffer[j + 1] += soundBuffer[sound.bufPos + notMonoInt]*amplitude*rightVol; + sound.bufPos += bufferStep; + + if (sound.bufPos >= soundBuffer.len) { + soundCount -= 1; + activeSounds.items[i] = activeSounds.items[soundCount]; + continue :main; + } + } + activeSounds.items[i] = sound; + i += 1; + } + + activeSounds.items.len = soundCount; +} + fn miniaudioCallback( maDevice: ?*anyopaque, output: ?*anyopaque, @@ -345,4 +505,5 @@ fn miniaudioCallback( const buffer = @as([*]f32, @ptrCast(@alignCast(output)))[0..valuesPerBuffer]; @memset(buffer, 0); mixMusic(buffer); + mixSound(buffer); } diff --git a/src/game.zig b/src/game.zig index 97c06a49b2..af1ea37f3a 100644 --- a/src/game.zig +++ b/src/game.zig @@ -579,6 +579,8 @@ pub fn getBlockWithSide(comptime side: main.sync.Side, x: i32, y: i32, z: i32) ? } } +var timeing: f64 = 0; + pub fn update(deltaTime: f64) void { // MARK: update() if (world.?.shouldRestart.load(.acquire)) { restart(); @@ -606,6 +608,12 @@ pub fn update(deltaTime: f64) void { // MARK: update() const right = Vec3d{-horizontalForward[1], horizontalForward[0], 0}; var movementDir: Vec3d = .{0, 0, 0}; + timeing += deltaTime; + if (timeing > 0.5) { + main.audio.playSpatialSound("cubyz:block_break", Vec3f{0, 0, 0}, 20); + timeing = 0; + } + if (main.Window.grabbed) { const walkingSpeed: f64 = if (Player.crouching) 2.5 else 4.5; var movementSpeed: f64 = walkingSpeed*@min(1, vec.length(Vec2f{ @@ -652,6 +660,7 @@ pub fn update(deltaTime: f64) void { // MARK: update() movementDir[2] += 5.5; } } else if ((Player.onGround or Player.jumpCoyote > 0.0) and Player.jumpCooldown <= 0) { + main.audio.playSound("cubyz:jump"); jumping = true; Player.jumpCooldown = Player.jumpCooldownConstant; if (!Player.onGround) { diff --git a/src/gui/windows/audio.zig b/src/gui/windows/audio.zig index 3cb1ac358d..3055633087 100644 --- a/src/gui/windows/audio.zig +++ b/src/gui/windows/audio.zig @@ -22,6 +22,11 @@ fn musicCallback(newValue: f32) void { settings.save(); } +fn soundCallback(newValue: f32) void { + settings.soundVolume = deziBelToLinear(newValue); + settings.save(); +} + fn deziBelToLinear(x: f32) f32 { if (x < -59.95) return 0; return std.math.pow(f32, 10, x/20); @@ -39,11 +44,18 @@ fn musicFormatter(allocator: NeverFailingAllocator, value: f32) []const u8 { return std.fmt.allocPrint(allocator.allocator, "Music volume:", .{}) catch unreachable; } +fn soundFormatter(allocator: NeverFailingAllocator, value: f32) []const u8 { + const percentage = 100*deziBelToLinear(value); + if (percentage == 0) return allocator.dupe(u8, "Sound volume: Off"); + return std.fmt.allocPrint(allocator.allocator, "Sound volume:", .{}) catch unreachable; +} + const padding: f32 = 8; pub fn onOpen() void { const list = VerticalList.init(.{padding, 16 + padding}, 300, 16); list.add(ContinuousSlider.init(.{0, 0}, 128, -60, 0, linearToDezibel(settings.musicVolume), &musicCallback, &musicFormatter)); + list.add(ContinuousSlider.init(.{0, 0}, 128, -60, 0, linearToDezibel(settings.soundVolume), &soundCallback, &soundFormatter)); list.finish(.center); window.rootComponent = list.toComponent(); window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); diff --git a/src/gui/windows/debug.zig b/src/gui/windows/debug.zig index ed1eb92bdc..7748c25a92 100644 --- a/src/gui/windows/debug.zig +++ b/src/gui/windows/debug.zig @@ -118,5 +118,7 @@ pub fn render() void { y += 8; draw.print("items: {} entities: {}", .{main.game.world.?.itemDrops.super.size, main.client.entity_manager.entities.len}, 0, y, 8); y += 8; + draw.print("Active sound count: {}", .{main.audio.getActiveSoundCount()}, 0, y, 8); + y += 8; } } diff --git a/src/renderer.zig b/src/renderer.zig index d9e33f3b80..423cb56693 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1092,7 +1092,9 @@ pub const MeshSelection = struct { // MARK: MeshSelection currentSwingTime = damagePerSwing/damage*swingTime; } currentSwingProgress += @floatCast(deltaTime); + var swung = false; while (currentSwingProgress > currentSwingTime) { + swung = true; currentSwingProgress -= currentSwingTime; currentBlockProgress += damage*currentSwingTime/swingTime/block.blockHealth(); if (currentBlockProgress > 0.9999) break; @@ -1103,6 +1105,9 @@ pub const MeshSelection = struct { // MARK: MeshSelection if (currentBlockProgress < 0.9999) { mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); if (currentBlockProgress != 0) { + if (swung) { + main.audio.playSound("cubyz:block_hit"); + } mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); } main.sync.client.mutex.unlock(); @@ -1127,6 +1132,7 @@ pub const MeshSelection = struct { // MARK: MeshSelection main.sync.client.mutex.unlock(); if (newBlock != block) { + main.audio.playSpatialSound("cubyz:block_break", @floatFromInt(selectedPos), 13); updateBlockAndSendUpdate(inventory, slot, selectedPos, block, newBlock); } } diff --git a/src/settings.zig b/src/settings.zig index 2cf4ddde27..aa7ff037af 100644 --- a/src/settings.zig +++ b/src/settings.zig @@ -55,6 +55,8 @@ pub var guiScale: ?f32 = null; pub var musicVolume: f32 = 1; +pub var soundVolume: f32 = 1; + pub var leavesQuality: u16 = 2; pub var @"lod0.5Distance": f32 = 200; diff --git a/src/utils/list.zig b/src/utils/list.zig index bed404252b..55566e61e6 100644 --- a/src/utils/list.zig +++ b/src/utils/list.zig @@ -208,6 +208,15 @@ pub fn ListManaged(comptime T: type) type { } } + pub fn clone(self: @This(), allocator: NeverFailingAllocator) @This() { + const newList: @This() = .{ + .capacity = self.capacity, + .items = allocator.dupe(self.items), + .allocator = self.allocator, + }; + return newList; + } + pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) void { var writer = std.Io.Writer.Allocating.init(main.stackAllocator.allocator); // TODO: Is there no easier way to make this without an extra copy? defer writer.deinit(); @@ -411,6 +420,14 @@ pub fn List(comptime T: type) type { } } + pub fn clone(self: @This(), allocator: NeverFailingAllocator) @This() { + const newList: @This() = .{ + .capacity = self.capacity, + .items = allocator.dupe([]const u8, self.items), + }; + return newList; + } + pub fn print(self: *@This(), allocator: NeverFailingAllocator, comptime fmt: []const u8, args: anytype) void { var buffer: std.ArrayList(u8) = .{.items = self.items, .capacity = self.capacity}; var writer = std.Io.Writer.Allocating.fromArrayList(allocator.allocator, &buffer);