diff --git a/.gitignore b/.gitignore index 58b48e3480..97ee3ccc41 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ test.png !/assets/cubyz !/assets/backgrounds/default_background.png /assets/cubyz/music +/assets/cubyz/sounds/audio /assets/cubyz/fonts 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/build.zig b/build.zig index d09ad38eba..166a695518 100644 --- a/build.zig +++ b/build.zig @@ -179,6 +179,11 @@ pub fn build(b: *std.Build) !void { .install_subdir = "assets/cubyz/music/", .install_dir = .{.custom = ".."}, }); + b.installDirectory(.{ + .source_dir = largeAssets.path("sounds"), + .install_subdir = "assets/cubyz/sounds/audio/", + .install_dir = .{.custom = ".."}, + }); b.installDirectory(.{ .source_dir = largeAssets.path("fonts"), .install_subdir = "assets/cubyz/fonts/", diff --git a/src/assets.zig b/src/assets.zig index 989ac911f2..b2e82b60bd 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,7 @@ pub const Assets = struct { // MARK: Assets worldPresets: ZonHashMap, entityModelDescriptions: ZonHashMap, entityModelMigrations: ZonHashMap, + sounds: ZonHashMap, fn init() Assets { return .{ @@ -65,6 +67,7 @@ pub const Assets = struct { // MARK: Assets .worldPresets = .{}, .entityModelDescriptions = .{}, .entityModelMigrations = .{}, + .sounds = .{}, }; } fn deinit(self: *Assets, allocator: NeverFailingAllocator) void { @@ -88,6 +91,7 @@ pub const Assets = struct { // MARK: Assets self.worldPresets.deinit(allocator.allocator); self.entityModelDescriptions.deinit(allocator.allocator); self.entityModelMigrations.deinit(allocator.allocator); + self.sounds.deinit(allocator.allocator); } fn clone(self: Assets, allocator: NeverFailingAllocator) Assets { return .{ @@ -111,6 +115,7 @@ pub const Assets = struct { // MARK: Assets .worldPresets = .{}, // Not accessible inside the world .entityModelDescriptions = self.entityModelDescriptions.clone(allocator.allocator) catch unreachable, .entityModelMigrations = self.entityModelMigrations.clone(allocator.allocator) catch unreachable, + .sounds = self.sounds.clone(allocator.allocator) catch unreachable, }; } fn read(self: *Assets, allocator: NeverFailingAllocator, assetDir: main.files.Dir, assetPath: []const u8) void { @@ -133,12 +138,13 @@ pub const Assets = struct { // MARK: Assets 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.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, 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.sounds.count()}, ); } @@ -724,6 +730,11 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale particles.ParticleManager.register(assetFolder, entry.key_ptr.*, entry.value_ptr.*); } + 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| { @@ -806,6 +817,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 d834df5e76..0ea7c13169 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"); @@ -138,6 +141,7 @@ var activeTasks: main.List([]const u8) = .empty; // MARK: Music var taskMutex: main.utils.Mutex = .{}; var musicCache: utils.Cache(AudioData, 4, 4, AudioData.deinit) = .{}; +var audioCache: utils.Cache(AudioData, 4, 4, AudioData.deinit) = .{}; fn findMusic(musicId: []const u8) ?[]f32 { { @@ -156,6 +160,28 @@ fn findMusic(musicId: []const u8) ?[]f32 { return null; } +fn findSound(musicId: []const u8) ?*AudioData { + { + taskMutex.lock(); + defer taskMutex.unlock(); + if (audioCache.find(AudioData{.audioId = musicId}, null)) |musicData| { + return musicData; + } + const data = AudioData.init(musicId, "sounds/audio"); + // we convert to mono for easier audio processing + if (data.channelType == .stereo) { + std.log.err("Sound effects must be mono. Error on {s}", .{musicId}); + return null; + } + const hasOld = audioCache.addToCache(data, data.hashCode()); + if (hasOld) |old| { + old.deinit(); + } + return data; + } + return null; +} + const MusicLoadTask = struct { musicId: []const u8, @@ -236,11 +262,22 @@ pub fn deinit() void { defer mutex.unlock(); main.threadPool.closeAllTasksOfType(&MusicLoadTask.vtable); musicCache.clear(); + audioCache.clear(); activeTasks.deinit(main.globalAllocator); main.globalAllocator.free(preferredMusic); preferredMusic.len = 0; main.globalAllocator.free(activeMusicId); activeMusicId.len = 0; + + soundDataIdMap.deinit(main.globalAllocator.allocator); + soundDatas.deinit(main.globalAllocator); + activeSounds.deinit(main.globalAllocator); +} + +pub fn reset() void { + activeSounds.clearRetainingCapacity(); + soundDataIdMap.clearRetainingCapacity(); + soundDatas.clearRetainingCapacity(); } const currentMusic = struct { @@ -286,6 +323,77 @@ pub fn setMusic(music: []const u8) void { preferredMusic = main.globalAllocator.dupe(u8, music); } +const SoundData = struct { // MARK: Sounds + audioName: []const u8, + volume: f32 = 1, +}; + +const PlayingSound = struct { + pos: Vec3f = @splat(0), + audioName: []const u8, + bufPos: u32 = 0, + + volume: f32 = 1, + maxDistance: f32 = 10, + isSpatial: bool = false, +}; + +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 registerSound(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const audioId = zon.get([]const u8, "audio") orelse { + std.log.err("Error: Sound Data audio was not specified: {s} ({s})", .{id, assetsFolder}); + return; + }; + if (findSound(audioId) == null) { + std.log.err("Error: Sound Data audio was not found: {s} ({s})", .{id, assetsFolder}); + return; + } + + soundDataIdMap.put(main.globalAllocator.allocator, id, @intCast(soundDatas.items.len)) catch unreachable; + soundDatas.append(main.globalAllocator, SoundData{ + .audioName = audioId, + .volume = zon.get(f32, "volume") orelse 1.0, + }); + + std.log.debug("Registered sound data: {s}", .{id}); +} + +const AddSoundParameters = struct { // MARK: Sounds + isSpatial: bool, + maxDistance: f32, + pos: Vec3f, +}; + +pub fn addSound(id: []const u8, soundParameters: AddSoundParameters) void { + mutex.lock(); + defer mutex.unlock(); + + const idx = soundDataIdMap.get(id) orelse return; + const soundData = soundDatas.items[idx]; + activeSounds.append(main.globalAllocator, PlayingSound{ + .audioName = soundData.audioName, + .volume = soundData.volume, + .pos = soundParameters.pos, + .isSpatial = soundParameters.isSpatial, + .maxDistance = soundParameters.maxDistance, + }); +} + +pub fn playSound(id: []const u8) void { + addSound(id, .{.pos = @splat(0), .maxDistance = 0, .isSpatial = false}); +} + +pub fn playSpatialSound(id: []const u8, pos: Vec3f, maxDistance: f32) void { + addSound(id, .{.pos = pos, .maxDistance = maxDistance, .isSpatial = true}); +} + fn mixMusic(buffer: []f32) void { mutex.lock(); defer mutex.unlock(); @@ -333,6 +441,68 @@ 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; + main: while (i < activeSounds.items.len) { + var sound = activeSounds.items[i]; + const audioData = findSound(sound.audioName) orelse continue :main; + const soundBuffer = audioData.data; + + 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) { + _ = activeSounds.swapRemove(i); + 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]*amplitude*rightVol; + sound.bufPos += 1; + + if (sound.bufPos >= soundBuffer.len) { + _ = activeSounds.swapRemove(i); + continue :main; + } + } + activeSounds.items[i] = sound; + i += 1; + } +} + fn miniaudioCallback( maDevice: ?*anyopaque, output: ?*anyopaque, @@ -345,4 +515,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 cb7b157be7..73e1293da9 100644 --- a/src/game.zig +++ b/src/game.zig @@ -618,6 +618,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(); @@ -650,6 +652,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{ @@ -707,6 +715,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 bb53739a90..b08e965e03 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 allocator.print("Music volume:", .{}); } +fn soundFormatter(allocator: NeverFailingAllocator, value: f32) []const u8 { + const percentage = 100*deziBelToLinear(value); + if (percentage == 0) return allocator.dupe(u8, "Sound volume: Off"); + return allocator.dupe(u8, "Sound volume:"); +} + 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 c53aef5b8a..12317bc54f 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 02a9ad8db2..cdecba318f 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1138,6 +1138,7 @@ pub const MeshSelection = struct { // MARK: MeshSelection while (swinging.currentSwingProgress > swinging.currentSwingTime) { swinging.currentSwingProgress -= swinging.currentSwingTime; breaking.progress += damage*swinging.currentSwingTime/swingTime/block.blockHealth(); + main.audio.playSound("cubyz:block_hit"); if (breaking.progress > 0.9999) break; const swings = @ceil(block.blockHealth()/damage); const damagePerSwing = block.blockHealth()/swings; @@ -1170,6 +1171,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;