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.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()) {

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

View File

@ -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;

View File

@ -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<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.
* @param sounds The list of sounds to play.
* @param player The player that's hearing the sounds.
*/
public void play(List<Sound> sounds, Player player) {
for (Sound sound : sounds) {
this.play(sound, player);
public void play(List<SoundData> 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<Void> play(String soundName, Vec2 soundOrigin, float originalVolume, Player player) {
TimedCompletableFuture<Void> 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.
* </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 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);

View File

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

View File

@ -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;

View File

@ -22,13 +22,13 @@ public class WorldUpdate {
private final List<PlayerUpdate> playerUpdates;
private final List<BulletUpdate> bulletUpdates;
private final List<TeamUpdate> teamUpdates;
private final List<Sound> soundsToPlay;
private final List<SoundData> soundsToPlay;
public WorldUpdate() {
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.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<Sound> getSoundsToPlay() {
public List<SoundData> 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<Sound> soundsToPlay = new ArrayList<>(sounds);
List<SoundData> 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();

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