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();