Split SoundEffectManager from AudioManager. (#171)

Clips no longer create a new clip instance each time they are played.  A singular clip is made for each resource and is opened/closed when loaded/unloaded. When a clip is played that is already playing it'll stop playback and start again. Clip volume handling isn't done very well.
This commit is contained in:
Stef
2025-10-11 23:00:06 +02:00
committed by GitHub
parent 9749d3eee8
commit d958b9730a
5 changed files with 90 additions and 45 deletions

View File

@@ -7,6 +7,10 @@ import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
public class AudioEventListener<T extends AudioResource, K extends AudioResource> { public class AudioEventListener<T extends AudioResource, K extends AudioResource> {
private final MusicManager<T> musicManager; private final MusicManager<T> musicManager;
private final SoundEffectManager<K> soundEffectManager; private final SoundEffectManager<K> soundEffectManager;
@@ -33,7 +37,10 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
.listen(this::handleMusicVolumeChange) .listen(this::handleMusicVolumeChange)
.listen(this::handleGetVolume) .listen(this::handleGetVolume)
.listen(this::handleGetFxVolume) .listen(this::handleGetFxVolume)
.listen(this::handleGetMusicVolume); .listen(this::handleGetMusicVolume)
.listen(AudioEvents.ClickButton.class, _ -> {
soundEffectManager.play("medium-button-click.wav", false);
});
} }
private void handleStopMusicManager(AudioEvents.StopAudioManager event) { private void handleStopMusicManager(AudioEvents.StopAudioManager event) {
@@ -45,7 +52,7 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
} }
private void handleStopSound(AudioEvents.StopEffect event) { private void handleStopSound(AudioEvents.StopEffect event) {
this.soundEffectManager.stop(event.clipId()); this.soundEffectManager.stop(event.fileName());
} }
private void handleMusicStart(AudioEvents.StartBackgroundMusic event) { private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {

View File

@@ -1,27 +1,64 @@
package org.toop.framework.audio; package org.toop.framework.audio;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.BaseResource;
import org.toop.framework.resource.resources.SoundEffectAsset; import org.toop.framework.resource.resources.SoundEffectAsset;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
public class SoundEffectManager implements org.toop.framework.audio.interfaces.SoundEffectManager<SoundEffectAsset> { public class SoundEffectManager implements org.toop.framework.audio.interfaces.SoundEffectManager<SoundEffectAsset> {
private final Map<Long, SoundEffectAsset> activeSoundEffects = new HashMap<>(); private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>(); private final HashMap<String, SoundEffectAsset> soundEffectResources;
public SoundEffectManager(){
// If there are duplicates, takes discards the first
soundEffectResources = ResourceManager.getAllOfType(SoundEffectAsset.class).stream()
.collect(Collectors.toMap(ResourceMeta::getName, ResourceMeta::getResource, (a, b) -> b, HashMap::new));
}
@Override @Override
public Collection<SoundEffectAsset> getActiveAudio() { public Collection<SoundEffectAsset> getActiveAudio() {
return this.audioResources.values(); return this.soundEffectResources.values();
} }
@Override @Override
public void play(String name, boolean loop) { public void play(String name, boolean loop) {
//TODO SoundEffectAsset asset = soundEffectResources.get(name);
if (asset == null) {
logger.warn("Unable to load audio asset: {}", name);
return;
}
asset.play();
// TODO: Volume of Sound Effect isn't set when loading. When loading an effect it will be full volume.
logger.debug("Playing sound: {}", asset.getFile().getName());
} }
@Override @Override
public void stop(long clipId) { public void stop(String name){
//TODO SoundEffectAsset asset = soundEffectResources.get(name);
if (asset == null) {
logger.warn("Unable to load audio asset: {}", name);
return;
}
asset.stop();
logger.debug("Stopped sound: {}", asset.getFile().getName());
} }
} }

View File

@@ -11,7 +11,7 @@ public class AudioEvents extends EventsBase {
/** Starts playing a sound. */ /** Starts playing a sound. */
public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {} public record PlayEffect(String fileName, boolean loop) implements EventWithoutSnowflake {}
public record StopEffect(long clipId) implements EventWithoutSnowflake {} public record StopEffect(String fileName) implements EventWithoutSnowflake {}
public record StartBackgroundMusic() implements EventWithoutSnowflake {} public record StartBackgroundMusic() implements EventWithoutSnowflake {}

View File

@@ -3,7 +3,11 @@ package org.toop.framework.audio.interfaces;
import org.toop.framework.resource.resources.SoundEffectAsset; import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.AudioResource;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> { public interface SoundEffectManager<T extends AudioResource> extends AudioManager<T> {
void play(String name, boolean loop); void play(String name, boolean loop);
void stop(long clipId); void stop(String name);
} }

View File

@@ -8,45 +8,23 @@ import org.toop.framework.resource.types.AudioResource;
import org.toop.framework.resource.types.FileExtension; import org.toop.framework.resource.types.FileExtension;
import org.toop.framework.resource.types.LoadableResource; import org.toop.framework.resource.types.LoadableResource;
import static javax.sound.sampled.LineEvent.Type.CLOSE;
import static javax.sound.sampled.LineEvent.Type.STOP;
@FileExtension({"wav"}) @FileExtension({"wav"})
public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource { public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource {
private byte[] rawData; private final Clip clip = AudioSystem.getClip();
private Clip clip = null;
public SoundEffectAsset(final File audioFile) { public SoundEffectAsset(final File audioFile) throws LineUnavailableException {
super(audioFile); super(audioFile);
} }
// Gets a new clip to play // Gets a new clip to play
public Clip getNewClip() public Clip getClip() {
throws LineUnavailableException, UnsupportedAudioFileException, IOException { if (!this.isLoaded()) {this.load();} return this.clip;
// Get a new clip from audio system
Clip clip = AudioSystem.getClip();
// Insert a new audio stream into the clip
AudioInputStream inputStream = this.getAudioStream();
AudioFormat baseFormat = inputStream.getFormat();
if (baseFormat.getSampleSizeInBits() > 16)
inputStream = downSampleAudio(inputStream, baseFormat);
clip.open(
inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
this.clip = clip;
return clip;
} }
// Generates a new audio stream from byte array private AudioInputStream downSampleAudio(AudioInputStream audioInputStream, AudioFormat baseFormat) {
private AudioInputStream getAudioStream() throws UnsupportedAudioFileException, IOException {
// Check if raw data is loaded into memory
if (!this.isLoaded()) {
this.load();
}
// Turn rawData into an input stream and turn that into an audio input stream;
return AudioSystem.getAudioInputStream(new ByteArrayInputStream(this.rawData));
}
private AudioInputStream downSampleAudio(
AudioInputStream audioInputStream, AudioFormat baseFormat) {
AudioFormat decodedFormat = AudioFormat decodedFormat =
new AudioFormat( new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED, AudioFormat.Encoding.PCM_SIGNED,
@@ -61,19 +39,35 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource,
return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream); return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream);
} }
@Override @Override
public void load() { public void load() {
try { try {
this.rawData = Files.readAllBytes(file.toPath()); if (this.isLoaded){
return; // Return if it is already loaded
}
// Insert a new audio stream into the clip
AudioInputStream inputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(this.getFile())));
AudioFormat baseFormat = inputStream.getFormat();
if (baseFormat.getSampleSizeInBits() > 16)
inputStream = downSampleAudio(inputStream, baseFormat);
this.clip.open(inputStream); // ^ Clip can only run 16 bit and lower, thus downsampling necessary.
this.isLoaded = true; this.isLoaded = true;
} catch (IOException e) { } catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override @Override
public void unload() { public void unload() {
this.rawData = null; if (!this.isLoaded) return; // Return if already unloaded
if (clip.isRunning()) clip.stop(); // Stops playback of the clip
clip.close(); // Releases native resources (empties buffer)
this.isLoaded = false; this.isLoaded = false;
} }
@@ -112,12 +106,15 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource,
@Override @Override
public void play() { public void play() {
// TODO if (!isLoaded()) load();
this.clip.setFramePosition(0); // rewind to the start
this.clip.start();
} }
@Override @Override
public void stop() { public void stop() {
// TODO if (this.clip.isRunning()) this.clip.stop();
} }
} }