Cleaned up the sound system.
This commit is contained in:
parent
6a6d367054
commit
b3483ade7e
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewlalis.aos_client;
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewlalis.aos_client;
|
package nl.andrewlalis.aos_client;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_client.sound.SoundManager;
|
||||||
import nl.andrewlalis.aos_client.view.GameFrame;
|
import nl.andrewlalis.aos_client.view.GameFrame;
|
||||||
import nl.andrewlalis.aos_client.view.GamePanel;
|
import nl.andrewlalis.aos_client.view.GamePanel;
|
||||||
import nl.andrewlalis.aos_core.model.Player;
|
import nl.andrewlalis.aos_core.model.Player;
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
package nl.andrewlalis.aos_client;
|
|
||||||
|
|
||||||
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.SoundType;
|
|
||||||
|
|
||||||
import javax.sound.sampled.*;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class SoundManager {
|
|
||||||
private static final float HEARING_RANGE = 50.0f;
|
|
||||||
private final Map<String, List<Clip>> soundData = new HashMap<>();
|
|
||||||
private final Map<String, Integer> clipIndexes = new HashMap<>();
|
|
||||||
|
|
||||||
public void play(List<Sound> sounds, Player player) {
|
|
||||||
for (Sound sound : sounds) {
|
|
||||||
this.play(sound, player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void play(List<Sound> sounds) {
|
|
||||||
for (Sound sound : sounds) {
|
|
||||||
this.play(sound, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void play(Sound sound) {
|
|
||||||
this.play(sound, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void play(Sound sound, Player player) {
|
|
||||||
var clip = this.getClip(sound.getType());
|
|
||||||
if (clip == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clip.setFramePosition(0);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (v <= 0.0f) return;
|
|
||||||
if (player != null && player.getTeam() != null && sound.getPosition() != null) {
|
|
||||||
setPan(clip, player.getPosition(), sound.getPosition(), player.getTeam().getOrientation());
|
|
||||||
}
|
|
||||||
setVolume(clip, v);
|
|
||||||
clip.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setVolume(Clip clip, float volume) {
|
|
||||||
volume = Math.max(Math.min(volume, 1.0f), 0.0f);
|
|
||||||
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
|
|
||||||
gainControl.setValue(20f * (float) Math.log10(volume));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPan(Clip clip, Vec2 playerPos, Vec2 soundPos, Vec2 playerOrientation) {
|
|
||||||
Vec2 soundDir = soundPos
|
|
||||||
.sub(playerPos)
|
|
||||||
.rotate(playerOrientation.perp().angle())
|
|
||||||
.unit();
|
|
||||||
float pan = Math.max(Math.min(soundDir.dot(Vec2.RIGHT), 1.0f), -1.0f);
|
|
||||||
if (Float.isNaN(pan)) pan = 0f;
|
|
||||||
FloatControl panControl = (FloatControl) clip.getControl(FloatControl.Type.PAN);
|
|
||||||
panControl.setValue(pan);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Clip getClip(SoundType soundType) {
|
|
||||||
String sound = soundType.getSoundName();
|
|
||||||
var clips = this.soundData.get(sound);
|
|
||||||
if (clips == null) {
|
|
||||||
InputStream is = Client.class.getResourceAsStream("/nl/andrewlalis/aos_client/sound/" + sound);
|
|
||||||
if (is == null) {
|
|
||||||
System.err.println("Could not load sound: " + sound);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
||||||
is.transferTo(bos);
|
|
||||||
byte[] data = bos.toByteArray();
|
|
||||||
clips = new ArrayList<>(soundType.getClipBufferCount());
|
|
||||||
for (int i = 0; i < soundType.getClipBufferCount(); i++) {
|
|
||||||
var ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(data));
|
|
||||||
var clip = AudioSystem.getClip();
|
|
||||||
clip.open(ais);
|
|
||||||
ais.close();
|
|
||||||
clips.add(clip);
|
|
||||||
}
|
|
||||||
this.soundData.put(sound, clips);
|
|
||||||
this.clipIndexes.put(sound, 0);
|
|
||||||
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int index = this.clipIndexes.get(sound);
|
|
||||||
if (index >= soundType.getClipBufferCount()) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
Clip clip = clips.get(index);
|
|
||||||
this.clipIndexes.put(sound, index + 1);
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
for (var c : this.soundData.values()) {
|
|
||||||
for (var clip : c) {
|
|
||||||
clip.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package nl.andrewlalis.aos_client.sound;
|
||||||
|
|
||||||
|
import nl.andrewlalis.aos_core.model.Player;
|
||||||
|
import nl.andrewlalis.aos_core.net.data.Sound;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.AudioInputStream;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
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)
|
||||||
|
*/
|
||||||
|
public class AudioClip {
|
||||||
|
private final AudioFormat format;
|
||||||
|
private final byte[] samples;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new audio clip, using the given resource name to load audio
|
||||||
|
* data from a classpath resource.
|
||||||
|
* @param resource The name of the classpath resource to load.
|
||||||
|
* @throws IOException If the clip could not be loaded.
|
||||||
|
*/
|
||||||
|
public AudioClip(String resource) throws IOException {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = AudioClip.class.getResourceAsStream(resource);
|
||||||
|
if (inputStream == null) throw new IOException("Could not get resource as stream: " + resource);
|
||||||
|
AudioInputStream in = AudioSystem.getAudioInputStream(new BufferedInputStream(inputStream));
|
||||||
|
this.format = in.getFormat();
|
||||||
|
this.samples = in.readAllBytes();
|
||||||
|
} catch (UnsupportedAudioFileException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioFormat getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSamples() {
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
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 javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.FloatControl;
|
||||||
|
import javax.sound.sampled.SourceDataLine;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sound manager is responsible for playing game sounds, using a cached set
|
||||||
|
* of audio clips that are loaded from sound resource files. Sounds are played
|
||||||
|
* using a thread pool.
|
||||||
|
*/
|
||||||
|
public class SoundManager {
|
||||||
|
/**
|
||||||
|
* The range in which a player can hear a sound. If it's further than this
|
||||||
|
* distance, the sound is not played.
|
||||||
|
*/
|
||||||
|
private static final float HEARING_RANGE = 50.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the clip buffer used during audio playback, in seconds.
|
||||||
|
*/
|
||||||
|
private static final float CLIP_BUFFER_SIZE = 1.0f / 10.0f;
|
||||||
|
|
||||||
|
private final ExecutorService soundPlayerThreadPool = Executors.newCachedThreadPool();
|
||||||
|
private final Map<String, AudioClip> audioClips = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a sound without any perspective, i.e. constant volume no matter
|
||||||
|
* where the player is.
|
||||||
|
* @param sound The sound to play.
|
||||||
|
*/
|
||||||
|
public void play(Sound sound) {
|
||||||
|
this.play(sound, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the given sound from the player's perspective.
|
||||||
|
* @param sound The sound to play.
|
||||||
|
* @param player The player that's hearing the sounds.
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the audio clip for a sound, using the given pan and volume settings.
|
||||||
|
* <p>
|
||||||
|
* 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 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) {
|
||||||
|
try {
|
||||||
|
AudioClip clip = this.getAudioClip(sound);
|
||||||
|
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());
|
||||||
|
line.open(clip.getFormat(), bufferSize);
|
||||||
|
line.start();
|
||||||
|
|
||||||
|
// Set pan.
|
||||||
|
FloatControl panControl = (FloatControl) line.getControl(FloatControl.Type.PAN);
|
||||||
|
panControl.setValue(pan);
|
||||||
|
// Set volume.
|
||||||
|
volume = Math.max(Math.min(volume, 1.0f), 0.0f);
|
||||||
|
FloatControl gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
||||||
|
gainControl.setValue(20f * (float) Math.log10(volume));
|
||||||
|
|
||||||
|
InputStream source = new ByteArrayInputStream(clip.getSamples());
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (bytesRead != -1) {
|
||||||
|
bytesRead = source.read(buffer, 0, bufferSize);
|
||||||
|
if (bytesRead != -1) {
|
||||||
|
line.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.drain();
|
||||||
|
line.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the volume that a sound should be played at, from the given
|
||||||
|
* player's perspective.
|
||||||
|
* @param sound The sound to play.
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float computePan(Sound sound, Player player) {
|
||||||
|
float pan = 0.0f;
|
||||||
|
if (player != null && player.getTeam() != null && sound.getPosition() != null) {
|
||||||
|
Vec2 soundDir = sound.getPosition()
|
||||||
|
.sub(player.getPosition())
|
||||||
|
.rotate(player.getTeam().getOrientation().perp().angle())
|
||||||
|
.unit();
|
||||||
|
pan = Math.max(Math.min(soundDir.dot(Vec2.RIGHT), 1.0f), -1.0f);
|
||||||
|
if (Float.isNaN(pan)) pan = 0f;
|
||||||
|
}
|
||||||
|
return pan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioClip getAudioClip(Sound sound) throws IOException {
|
||||||
|
String soundName = sound.getType().getSoundName();
|
||||||
|
AudioClip clip = this.audioClips.get(soundName);
|
||||||
|
if (clip == null) {
|
||||||
|
clip = new AudioClip("/nl/andrewlalis/aos_client/sound/" + soundName);
|
||||||
|
this.audioClips.put(soundName, clip);
|
||||||
|
}
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
this.soundPlayerThreadPool.shutdown();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue