Improved sound management.

This commit is contained in:
Andrew Lalis 2021-07-09 23:59:26 +02:00
parent b99047d696
commit 719f2a8edf
9 changed files with 104 additions and 48 deletions

View File

@ -89,6 +89,9 @@ public class Client {
player.setOrientation(p.getOrientation()); player.setOrientation(p.getOrientation());
player.setVelocity(p.getVelocity()); player.setVelocity(p.getVelocity());
player.setGun(new Gun(this.world.getGunTypes().get(p.getGunTypeName()))); 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()) { for (var t : update.getTeamUpdates()) {

View File

@ -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.ChatMessage;
import nl.andrewlalis.aos_core.net.chat.ChatType; import nl.andrewlalis.aos_core.net.chat.ChatType;
import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; 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 nl.andrewlalis.aos_core.net.data.SoundType;
import java.util.LinkedList; import java.util.LinkedList;
@ -46,7 +46,7 @@ public class ChatManager {
public synchronized void addChatMessage(ChatMessage message) { public synchronized void addChatMessage(ChatMessage message) {
this.chatMessages.add(message); this.chatMessages.add(message);
if (message instanceof PlayerChatMessage) { 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) { while (this.chatMessages.size() > MAX_CHAT_MESSAGES) {
this.chatMessages.remove(0); this.chatMessages.remove(0);

View File

@ -1,7 +1,7 @@
package nl.andrewlalis.aos_client.sound; package nl.andrewlalis.aos_client.sound;
import nl.andrewlalis.aos_core.model.Player; 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.AudioFormat;
import javax.sound.sampled.AudioInputStream; 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 * Simple container object for an in-memory audio clip. The contents of this
* clip are played using a {@link javax.sound.sampled.SourceDataLine} during * clip are played using a {@link javax.sound.sampled.SourceDataLine} during
* runtime. * runtime.
* @see SoundManager#play(Sound, Player) * @see SoundManager#play(SoundData, Player)
*/ */
public class AudioClip { public class AudioClip {
private final AudioFormat format; private final AudioFormat format;

View File

@ -2,7 +2,8 @@ package nl.andrewlalis.aos_client.sound;
import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.Player; 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.AudioSystem;
import javax.sound.sampled.FloatControl; import javax.sound.sampled.FloatControl;
@ -15,6 +16,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
/** /**
* The sound manager is responsible for playing game sounds, using a cached set * 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 ExecutorService soundPlayerThreadPool = Executors.newCachedThreadPool();
private final Map<String, AudioClip> audioClips = new HashMap<>(); private final Map<String, AudioClip> audioClips = new HashMap<>();
private final Map<Player, TimedCompletableFuture<Void>> footstepAudioFutures = new HashMap<>();
/** /**
* Plays the given list of sounds from the player's perspective. * Plays the given list of sounds from the player's perspective.
* @param sounds The list of sounds to play. * @param sounds The list of sounds to play.
* @param player The player that's hearing the sounds. * @param player The player that's hearing the sounds.
*/ */
public void play(List<Sound> sounds, Player player) { public void play(List<SoundData> sounds, Player player) {
for (Sound sound : sounds) { for (SoundData sound : sounds) {
this.play(sound, player); this.play(sound.getType().getSoundName(), sound.getPosition(), sound.getVolume(), player);
} }
} }
@ -52,20 +55,31 @@ public class SoundManager {
* where the player is. * where the player is.
* @param sound The sound to play. * @param sound The sound to play.
*/ */
public void play(Sound sound) { public void play(SoundData sound) {
this.play(sound, null); this.play(sound.getType().getSoundName(), sound.getPosition(), sound.getVolume(), null);
} }
/** /**
* Plays the given sound from the player's perspective. * 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. * @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) { public TimedCompletableFuture<Void> play(String soundName, Vec2 soundOrigin, float originalVolume, Player player) {
final float volume = this.computeVolume(sound, player); TimedCompletableFuture<Void> cf = new TimedCompletableFuture<>();
if (volume <= 0.0f) return; // Don't play the sound at all, if its volume is nothing. final float volume = this.computeVolume(originalVolume, soundOrigin, player);
final float pan = this.computePan(sound, player); if (volume <= 0.0f) {
this.soundPlayerThreadPool.submit(() -> this.play(sound, pan, volume)); 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 * This method is blocking, and should ideally be called in a separate
* thread or submitted as a lambda expression to a thread pool. * thread or submitted as a lambda expression to a thread pool.
* </p> * </p>
* @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 pan The pan setting, from -1.0 (left) to 1.0 (right).
* @param volume The volume, from 0.0 to 1.0. * @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 { 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); final int bufferSize = clip.getFormat().getFrameSize() * Math.round(clip.getFormat().getSampleRate() * CLIP_BUFFER_SIZE);
byte[] buffer = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
SourceDataLine line = AudioSystem.getSourceDataLine(clip.getFormat()); 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 * Computes the volume that a sound should be played at, from the given
* player's perspective. * 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. * @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. * @return The volume the sound should be played at, from 0.0 to 1.0.
*/ */
private float computeVolume(Sound sound, Player player) { private float computeVolume(float originalVolume, Vec2 soundOrigin, Player player) {
float v = sound.getVolume(); if (player != null && soundOrigin != null) {
if (player != null && sound.getPosition() != null) { float dist = player.getPosition().dist(soundOrigin);
float dist = player.getPosition().dist(sound.getPosition()); originalVolume *= (Math.max(HEARING_RANGE - dist, 0) / HEARING_RANGE);
v *= (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; float pan = 0.0f;
if (player != null && player.getTeam() != null && sound.getPosition() != null) { if (player != null && player.getTeam() != null && soundOrigin != null) {
Vec2 soundDir = sound.getPosition() Vec2 soundDir = soundOrigin
.sub(player.getPosition()) .sub(player.getPosition())
.rotate(player.getTeam().getOrientation().perp().angle()) .rotate(player.getTeam().getOrientation().perp().angle())
.unit(); .unit();
@ -139,8 +168,7 @@ public class SoundManager {
return pan; return pan;
} }
private AudioClip getAudioClip(Sound sound) throws IOException { private AudioClip getAudioClip(String soundName) throws IOException {
String soundName = sound.getType().getSoundName();
AudioClip clip = this.audioClips.get(soundName); AudioClip clip = this.audioClips.get(soundName);
if (clip == null) { if (clip == null) {
clip = new AudioClip("/nl/andrewlalis/aos_client/sound/" + soundName); clip = new AudioClip("/nl/andrewlalis/aos_client/sound/" + soundName);

View File

@ -6,14 +6,14 @@ import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
public class Sound { public class SoundData {
public static final int BYTES = 3 * Float.BYTES + 1; public static final int BYTES = 3 * Float.BYTES + 1;
private final Vec2 position; private final Vec2 position;
private final float volume; private final float volume;
private final SoundType type; private final SoundType type;
public Sound(Vec2 position, float volume, SoundType type) { public SoundData(Vec2 position, float volume, SoundType type) {
this.position = position; this.position = position;
this.volume = volume; this.volume = volume;
this.type = type; this.type = type;
@ -38,8 +38,8 @@ public class Sound {
out.writeByte(this.type.getCode()); out.writeByte(this.type.getCode());
} }
public static Sound read(DataInputStream in) throws IOException { public static SoundData read(DataInputStream in) throws IOException {
return new Sound( return new SoundData(
Vec2.read(in), Vec2.read(in),
in.readFloat(), in.readFloat(),
SoundType.get(in.readByte()) SoundType.get(in.readByte())

View File

@ -20,7 +20,8 @@ public enum SoundType {
BULLET_IMPACT_2(7, "bullet_impact_2.wav"), BULLET_IMPACT_2(7, "bullet_impact_2.wav"),
BULLET_IMPACT_3(8, "bullet_impact_3.wav"), BULLET_IMPACT_3(8, "bullet_impact_3.wav"),
BULLET_IMPACT_4(9, "bullet_impact_4.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 byte code;
private final String soundName; private final String soundName;

View File

@ -22,13 +22,13 @@ public class WorldUpdate {
private final List<PlayerUpdate> playerUpdates; private final List<PlayerUpdate> playerUpdates;
private final List<BulletUpdate> bulletUpdates; private final List<BulletUpdate> bulletUpdates;
private final List<TeamUpdate> teamUpdates; private final List<TeamUpdate> teamUpdates;
private final List<Sound> soundsToPlay; private final List<SoundData> soundsToPlay;
public WorldUpdate() { public WorldUpdate() {
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
} }
private WorldUpdate(List<PlayerUpdate> playerUpdates, List<BulletUpdate> bulletUpdates, List<TeamUpdate> teamUpdates, List<Sound> soundsToPlay) { private WorldUpdate(List<PlayerUpdate> playerUpdates, List<BulletUpdate> bulletUpdates, List<TeamUpdate> teamUpdates, List<SoundData> soundsToPlay) {
this.playerUpdates = playerUpdates; this.playerUpdates = playerUpdates;
this.bulletUpdates = bulletUpdates; this.bulletUpdates = bulletUpdates;
this.teamUpdates = teamUpdates; this.teamUpdates = teamUpdates;
@ -54,7 +54,7 @@ public class WorldUpdate {
this.teamUpdates.add(new TeamUpdate(team)); this.teamUpdates.add(new TeamUpdate(team));
} }
public void addSound(Sound sound) { public void addSound(SoundData sound) {
this.soundsToPlay.add(sound); this.soundsToPlay.add(sound);
} }
@ -70,7 +70,7 @@ public class WorldUpdate {
return teamUpdates; return teamUpdates;
} }
public List<Sound> getSoundsToPlay() { public List<SoundData> getSoundsToPlay() {
return soundsToPlay; return soundsToPlay;
} }
@ -79,7 +79,7 @@ public class WorldUpdate {
this.playerUpdates.size() * PlayerUpdate.BYTES + this.playerUpdates.size() * PlayerUpdate.BYTES +
this.bulletUpdates.size() * BulletUpdate.BYTES + this.bulletUpdates.size() * BulletUpdate.BYTES +
this.teamUpdates.size() * TeamUpdate.BYTES + this.teamUpdates.size() * TeamUpdate.BYTES +
this.soundsToPlay.size() * Sound.BYTES; this.soundsToPlay.size() * SoundData.BYTES;
ByteArrayOutputStream out = new ByteArrayOutputStream(size); ByteArrayOutputStream out = new ByteArrayOutputStream(size);
DataOutputStream dataOut = new DataOutputStream(out); DataOutputStream dataOut = new DataOutputStream(out);
dataOut.writeInt(this.playerUpdates.size()); dataOut.writeInt(this.playerUpdates.size());
@ -123,9 +123,9 @@ public class WorldUpdate {
teamUpdates.add(TeamUpdate.read(dataIn)); teamUpdates.add(TeamUpdate.read(dataIn));
} }
int sounds = dataIn.readInt(); int sounds = dataIn.readInt();
List<Sound> soundsToPlay = new ArrayList<>(sounds); List<SoundData> soundsToPlay = new ArrayList<>(sounds);
for (int i = 0; i < sounds; i++) { 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); var obj = new WorldUpdate(playerUpdates, bulletUpdates, teamUpdates, soundsToPlay);
dataIn.close(); dataIn.close();

View File

@ -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 <T> The type which is produced when this completes.
*/
public class TimedCompletableFuture<T> extends CompletableFuture<T> {
private final long creationTimestamp;
public TimedCompletableFuture(long creationTimestamp) {
this.creationTimestamp = creationTimestamp;
}
public TimedCompletableFuture() {
this(System.currentTimeMillis());
}
public long getElapsedMillis() {
return System.currentTimeMillis() - this.creationTimestamp;
}
}

View File

@ -4,7 +4,7 @@ import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.*;
import nl.andrewlalis.aos_core.model.tools.GunCategory; import nl.andrewlalis.aos_core.model.tools.GunCategory;
import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; 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.SoundType;
import nl.andrewlalis.aos_core.net.data.WorldUpdate; 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) { } else if (p.getGun().getType().getCategory() == GunCategory.MACHINE) {
soundType = ThreadLocalRandom.current().nextFloat() < 0.8f ? SoundType.SHOT_MACHINE_GUN_1 : SoundType.SHOT_MACHINE_GUN_2; 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.useWeapon();
p.setVelocity(p.getVelocity().add(p.getOrientation().mul(-1 * p.getGun().getType().getRecoil()))); 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()) { if (p.isReloading() && p.isReloadingComplete()) {
p.finishReloading(); 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() 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); 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); bulletsToRemove.add(b);
removed = true; removed = true;
break; break;
@ -260,7 +260,7 @@ public class WorldUpdater extends Thread {
if (p.getHealth() == 0.0f) { if (p.getHealth() == 0.0f) {
Player shooter = this.world.getPlayers().get(b.getPlayerId()); Player shooter = this.world.getPlayers().get(b.getPlayerId());
this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + shooter.getName() + ".")); 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(); shooter.incrementKillCount();
if (shooter.getTeam() != null) { if (shooter.getTeam() != null) {
shooter.getTeam().incrementScore(); shooter.getTeam().incrementScore();