From 719f2a8edf3708771a843cdcb600e0a12f276271 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 9 Jul 2021 23:59:26 +0200 Subject: [PATCH] Improved sound management. --- .../nl/andrewlalis/aos_client/Client.java | 3 + .../aos_client/net/ChatManager.java | 4 +- .../aos_client/sound/AudioClip.java | 4 +- .../aos_client/sound/SoundManager.java | 82 +++++++++++++------ .../net/data/{Sound.java => SoundData.java} | 8 +- .../aos_core/net/data/SoundType.java | 3 +- .../aos_core/net/data/WorldUpdate.java | 14 ++-- .../aos_core/util/TimedCompletableFuture.java | 24 ++++++ .../andrewlalis/aos_server/WorldUpdater.java | 10 +-- 9 files changed, 104 insertions(+), 48 deletions(-) rename core/src/main/java/nl/andrewlalis/aos_core/net/data/{Sound.java => SoundData.java} (82%) create mode 100644 core/src/main/java/nl/andrewlalis/aos_core/util/TimedCompletableFuture.java diff --git a/client/src/main/java/nl/andrewlalis/aos_client/Client.java b/client/src/main/java/nl/andrewlalis/aos_client/Client.java index 954f91a..5d678fa 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Client.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Client.java @@ -89,6 +89,9 @@ public class Client { player.setOrientation(p.getOrientation()); player.setVelocity(p.getVelocity()); player.setGun(new Gun(this.world.getGunTypes().get(p.getGunTypeName()))); + if (player.getVelocity().mag() > 0) { + this.soundManager.playWalking(player, myPlayer); + } } } for (var t : update.getTeamUpdates()) { diff --git a/client/src/main/java/nl/andrewlalis/aos_client/net/ChatManager.java b/client/src/main/java/nl/andrewlalis/aos_client/net/ChatManager.java index 357f051..c3fada7 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/net/ChatManager.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/net/ChatManager.java @@ -4,7 +4,7 @@ import nl.andrewlalis.aos_client.sound.SoundManager; import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.chat.ChatType; import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; -import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundData; import nl.andrewlalis.aos_core.net.data.SoundType; import java.util.LinkedList; @@ -46,7 +46,7 @@ public class ChatManager { public synchronized void addChatMessage(ChatMessage message) { this.chatMessages.add(message); if (message instanceof PlayerChatMessage) { - this.soundManager.play(new Sound(null, 1.0f, SoundType.CHAT)); + this.soundManager.play(new SoundData(null, 1.0f, SoundType.CHAT)); } while (this.chatMessages.size() > MAX_CHAT_MESSAGES) { this.chatMessages.remove(0); diff --git a/client/src/main/java/nl/andrewlalis/aos_client/sound/AudioClip.java b/client/src/main/java/nl/andrewlalis/aos_client/sound/AudioClip.java index 5d2b384..b2a1af8 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/sound/AudioClip.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/sound/AudioClip.java @@ -1,7 +1,7 @@ package nl.andrewlalis.aos_client.sound; import nl.andrewlalis.aos_core.model.Player; -import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundData; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; @@ -15,7 +15,7 @@ import java.io.InputStream; * Simple container object for an in-memory audio clip. The contents of this * clip are played using a {@link javax.sound.sampled.SourceDataLine} during * runtime. - * @see SoundManager#play(Sound, Player) + * @see SoundManager#play(SoundData, Player) */ public class AudioClip { private final AudioFormat format; diff --git a/client/src/main/java/nl/andrewlalis/aos_client/sound/SoundManager.java b/client/src/main/java/nl/andrewlalis/aos_client/sound/SoundManager.java index 0f1f723..28e6358 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/sound/SoundManager.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/sound/SoundManager.java @@ -2,7 +2,8 @@ package nl.andrewlalis.aos_client.sound; import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.Player; -import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundData; +import nl.andrewlalis.aos_core.util.TimedCompletableFuture; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.FloatControl; @@ -15,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; /** * The sound manager is responsible for playing game sounds, using a cached set @@ -35,15 +37,16 @@ public class SoundManager { private final ExecutorService soundPlayerThreadPool = Executors.newCachedThreadPool(); private final Map audioClips = new HashMap<>(); + private final Map> footstepAudioFutures = new HashMap<>(); /** * Plays the given list of sounds from the player's perspective. * @param sounds The list of sounds to play. * @param player The player that's hearing the sounds. */ - public void play(List sounds, Player player) { - for (Sound sound : sounds) { - this.play(sound, player); + public void play(List sounds, Player player) { + for (SoundData sound : sounds) { + this.play(sound.getType().getSoundName(), sound.getPosition(), sound.getVolume(), player); } } @@ -52,20 +55,31 @@ public class SoundManager { * where the player is. * @param sound The sound to play. */ - public void play(Sound sound) { - this.play(sound, null); + public void play(SoundData sound) { + this.play(sound.getType().getSoundName(), sound.getPosition(), sound.getVolume(), null); } /** * Plays the given sound from the player's perspective. - * @param sound The sound to play. + * @param soundName The name of the sound. + * @param soundOrigin The origin point of the sound. + * @param originalVolume The original volume of the sound. * @param player The player that's hearing the sounds. + * @return A future that completes when the sound is done playing. */ - public void play(Sound sound, Player player) { - final float volume = this.computeVolume(sound, player); - if (volume <= 0.0f) return; // Don't play the sound at all, if its volume is nothing. - final float pan = this.computePan(sound, player); - this.soundPlayerThreadPool.submit(() -> this.play(sound, pan, volume)); + public TimedCompletableFuture play(String soundName, Vec2 soundOrigin, float originalVolume, Player player) { + TimedCompletableFuture cf = new TimedCompletableFuture<>(); + final float volume = this.computeVolume(originalVolume, soundOrigin, player); + if (volume <= 0.0f) { + cf.complete(null); // Don't play the sound at all, if its volume is nothing. + return cf; + } + final float pan = this.computePan(soundOrigin, player); + this.soundPlayerThreadPool.submit(() -> { + this.play(soundName, pan, volume); + cf.complete(null); + }); + return cf; } /** @@ -74,13 +88,13 @@ public class SoundManager { * This method is blocking, and should ideally be called in a separate * thread or submitted as a lambda expression to a thread pool. *

- * @param sound The sound to play. + * @param soundName The sound to play. * @param pan The pan setting, from -1.0 (left) to 1.0 (right). * @param volume The volume, from 0.0 to 1.0. */ - private void play(Sound sound, float pan, float volume) { + private void play(String soundName, float pan, float volume) { try { - AudioClip clip = this.getAudioClip(sound); + AudioClip clip = this.getAudioClip(soundName); final int bufferSize = clip.getFormat().getFrameSize() * Math.round(clip.getFormat().getSampleRate() * CLIP_BUFFER_SIZE); byte[] buffer = new byte[bufferSize]; SourceDataLine line = AudioSystem.getSourceDataLine(clip.getFormat()); @@ -110,26 +124,41 @@ public class SoundManager { } } + public void playWalking(Player emitter, Player listener) { + var f = this.footstepAudioFutures.get(emitter); + long delay = 500; + if (emitter.isSprinting()) { + delay -= 150; + } else if (emitter.isSneaking()) { + delay += 150; + } + if (f == null || f.getElapsedMillis() > delay) { + int choice = ThreadLocalRandom.current().nextInt(1, 5); + var cf = this.play("footsteps" + choice + ".wav", emitter.getPosition(), 0.1f, listener); + this.footstepAudioFutures.put(emitter, cf); + } + } + /** * Computes the volume that a sound should be played at, from the given * player's perspective. - * @param sound The sound to play. + * @param originalVolume The original volume of the sound. + * @param soundOrigin The origin point of the sound. * @param player The player that will be hearing the sound. * @return The volume the sound should be played at, from 0.0 to 1.0. */ - private float computeVolume(Sound sound, Player player) { - float v = sound.getVolume(); - if (player != null && sound.getPosition() != null) { - float dist = player.getPosition().dist(sound.getPosition()); - v *= (Math.max(HEARING_RANGE - dist, 0) / HEARING_RANGE); + private float computeVolume(float originalVolume, Vec2 soundOrigin, Player player) { + if (player != null && soundOrigin != null) { + float dist = player.getPosition().dist(soundOrigin); + originalVolume *= (Math.max(HEARING_RANGE - dist, 0) / HEARING_RANGE); } - return v; + return originalVolume; } - private float computePan(Sound sound, Player player) { + private float computePan(Vec2 soundOrigin, Player player) { float pan = 0.0f; - if (player != null && player.getTeam() != null && sound.getPosition() != null) { - Vec2 soundDir = sound.getPosition() + if (player != null && player.getTeam() != null && soundOrigin != null) { + Vec2 soundDir = soundOrigin .sub(player.getPosition()) .rotate(player.getTeam().getOrientation().perp().angle()) .unit(); @@ -139,8 +168,7 @@ public class SoundManager { return pan; } - private AudioClip getAudioClip(Sound sound) throws IOException { - String soundName = sound.getType().getSoundName(); + private AudioClip getAudioClip(String soundName) throws IOException { AudioClip clip = this.audioClips.get(soundName); if (clip == null) { clip = new AudioClip("/nl/andrewlalis/aos_client/sound/" + soundName); diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundData.java similarity index 82% rename from core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java rename to core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundData.java index 4913d9d..f453000 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/data/Sound.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundData.java @@ -6,14 +6,14 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -public class Sound { +public class SoundData { public static final int BYTES = 3 * Float.BYTES + 1; private final Vec2 position; private final float volume; private final SoundType type; - public Sound(Vec2 position, float volume, SoundType type) { + public SoundData(Vec2 position, float volume, SoundType type) { this.position = position; this.volume = volume; this.type = type; @@ -38,8 +38,8 @@ public class Sound { out.writeByte(this.type.getCode()); } - public static Sound read(DataInputStream in) throws IOException { - return new Sound( + public static SoundData read(DataInputStream in) throws IOException { + return new SoundData( Vec2.read(in), in.readFloat(), SoundType.get(in.readByte()) diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java index 57a4780..9b0dc40 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/SoundType.java @@ -20,7 +20,8 @@ public enum SoundType { BULLET_IMPACT_2(7, "bullet_impact_2.wav"), BULLET_IMPACT_3(8, "bullet_impact_3.wav"), BULLET_IMPACT_4(9, "bullet_impact_4.wav"), - BULLET_IMPACT_5(10, "bullet_impact_5.wav"); + BULLET_IMPACT_5(10, "bullet_impact_5.wav"), + FOOTSTEPS_1(13, "footsteps1.wav"); private final byte code; private final String soundName; diff --git a/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java b/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java index 41153fe..d5f0f52 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/net/data/WorldUpdate.java @@ -22,13 +22,13 @@ public class WorldUpdate { private final List playerUpdates; private final List bulletUpdates; private final List teamUpdates; - private final List soundsToPlay; + private final List soundsToPlay; public WorldUpdate() { this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } - private WorldUpdate(List playerUpdates, List bulletUpdates, List teamUpdates, List soundsToPlay) { + private WorldUpdate(List playerUpdates, List bulletUpdates, List teamUpdates, List soundsToPlay) { this.playerUpdates = playerUpdates; this.bulletUpdates = bulletUpdates; this.teamUpdates = teamUpdates; @@ -54,7 +54,7 @@ public class WorldUpdate { this.teamUpdates.add(new TeamUpdate(team)); } - public void addSound(Sound sound) { + public void addSound(SoundData sound) { this.soundsToPlay.add(sound); } @@ -70,7 +70,7 @@ public class WorldUpdate { return teamUpdates; } - public List getSoundsToPlay() { + public List getSoundsToPlay() { return soundsToPlay; } @@ -79,7 +79,7 @@ public class WorldUpdate { this.playerUpdates.size() * PlayerUpdate.BYTES + this.bulletUpdates.size() * BulletUpdate.BYTES + this.teamUpdates.size() * TeamUpdate.BYTES + - this.soundsToPlay.size() * Sound.BYTES; + this.soundsToPlay.size() * SoundData.BYTES; ByteArrayOutputStream out = new ByteArrayOutputStream(size); DataOutputStream dataOut = new DataOutputStream(out); dataOut.writeInt(this.playerUpdates.size()); @@ -123,9 +123,9 @@ public class WorldUpdate { teamUpdates.add(TeamUpdate.read(dataIn)); } int sounds = dataIn.readInt(); - List soundsToPlay = new ArrayList<>(sounds); + List soundsToPlay = new ArrayList<>(sounds); for (int i = 0; i < sounds; i++) { - soundsToPlay.add(Sound.read(dataIn)); + soundsToPlay.add(SoundData.read(dataIn)); } var obj = new WorldUpdate(playerUpdates, bulletUpdates, teamUpdates, soundsToPlay); dataIn.close(); diff --git a/core/src/main/java/nl/andrewlalis/aos_core/util/TimedCompletableFuture.java b/core/src/main/java/nl/andrewlalis/aos_core/util/TimedCompletableFuture.java new file mode 100644 index 0000000..6125f93 --- /dev/null +++ b/core/src/main/java/nl/andrewlalis/aos_core/util/TimedCompletableFuture.java @@ -0,0 +1,24 @@ +package nl.andrewlalis.aos_core.util; + +import java.util.concurrent.CompletableFuture; + +/** + * A completable future which keeps track of how long it's been since it was + * initialized. + * @param The type which is produced when this completes. + */ +public class TimedCompletableFuture extends CompletableFuture { + private final long creationTimestamp; + + public TimedCompletableFuture(long creationTimestamp) { + this.creationTimestamp = creationTimestamp; + } + + public TimedCompletableFuture() { + this(System.currentTimeMillis()); + } + + public long getElapsedMillis() { + return System.currentTimeMillis() - this.creationTimestamp; + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java index dce84cf..b8651ff 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java @@ -4,7 +4,7 @@ import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.tools.GunCategory; import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; -import nl.andrewlalis.aos_core.net.data.Sound; +import nl.andrewlalis.aos_core.net.data.SoundData; import nl.andrewlalis.aos_core.net.data.SoundType; import nl.andrewlalis.aos_core.net.data.WorldUpdate; @@ -198,7 +198,7 @@ public class WorldUpdater extends Thread { } else if (p.getGun().getType().getCategory() == GunCategory.MACHINE) { soundType = ThreadLocalRandom.current().nextFloat() < 0.8f ? SoundType.SHOT_MACHINE_GUN_1 : SoundType.SHOT_MACHINE_GUN_2; } - this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, soundType)); + this.worldUpdate.addSound(new SoundData(p.getPosition(), 1.0f, soundType)); p.useWeapon(); p.setVelocity(p.getVelocity().add(p.getOrientation().mul(-1 * p.getGun().getType().getRecoil()))); } @@ -207,7 +207,7 @@ public class WorldUpdater extends Thread { } if (p.isReloading() && p.isReloadingComplete()) { p.finishReloading(); - this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, SoundType.RELOAD)); + this.worldUpdate.addSound(new SoundData(p.getPosition(), 1.0f, SoundType.RELOAD)); } } @@ -228,7 +228,7 @@ public class WorldUpdater extends Thread { pos.y() > bar.getPosition().y() && pos.y() < bar.getPosition().y() + bar.getSize().y() ) { int code = ThreadLocalRandom.current().nextInt(SoundType.BULLET_IMPACT_1.getCode(), SoundType.BULLET_IMPACT_5.getCode() + 1); - this.worldUpdate.addSound(new Sound(b.getPosition(), 1.0f, SoundType.get((byte) code))); + this.worldUpdate.addSound(new SoundData(b.getPosition(), 1.0f, SoundType.get((byte) code))); bulletsToRemove.add(b); removed = true; break; @@ -260,7 +260,7 @@ public class WorldUpdater extends Thread { if (p.getHealth() == 0.0f) { Player shooter = this.world.getPlayers().get(b.getPlayerId()); this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + shooter.getName() + ".")); - this.worldUpdate.addSound(new Sound(p.getPosition(), 1.0f, SoundType.DEATH)); + this.worldUpdate.addSound(new SoundData(p.getPosition(), 1.0f, SoundType.DEATH)); shooter.incrementKillCount(); if (shooter.getTeam() != null) { shooter.getTeam().incrementScore();