From d958b9730abd335d55604ff73bc50aa26c99067f Mon Sep 17 00:00:00 2001 From: Stef <48526421+StefBuwalda@users.noreply.github.com> Date: Sat, 11 Oct 2025 23:00:06 +0200 Subject: [PATCH] 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. --- .../framework/audio/AudioEventListener.java | 11 ++- .../framework/audio/SoundEffectManager.java | 49 ++++++++++++-- .../framework/audio/events/AudioEvents.java | 2 +- .../audio/interfaces/SoundEffectManager.java | 6 +- .../resource/resources/SoundEffectAsset.java | 67 +++++++++---------- 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java b/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java index 3326d28..5389ea6 100644 --- a/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java +++ b/framework/src/main/java/org/toop/framework/audio/AudioEventListener.java @@ -7,6 +7,10 @@ import org.toop.framework.audio.interfaces.VolumeManager; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.types.AudioResource; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.IOException; + public class AudioEventListener { private final MusicManager musicManager; private final SoundEffectManager soundEffectManager; @@ -33,7 +37,10 @@ public class AudioEventListener { + soundEffectManager.play("medium-button-click.wav", false); + }); } private void handleStopMusicManager(AudioEvents.StopAudioManager event) { @@ -45,7 +52,7 @@ public class AudioEventListener { - private final Map activeSoundEffects = new HashMap<>(); - private final HashMap audioResources = new HashMap<>(); + private static final Logger logger = LogManager.getLogger(SoundEffectManager.class); + private final HashMap 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 public Collection getActiveAudio() { - return this.audioResources.values(); + return this.soundEffectResources.values(); } @Override 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 - public void stop(long clipId) { - //TODO + public void stop(String name){ + 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()); } } diff --git a/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java b/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java index 1ef84f4..5998d06 100644 --- a/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java +++ b/framework/src/main/java/org/toop/framework/audio/events/AudioEvents.java @@ -11,7 +11,7 @@ public class AudioEvents extends EventsBase { /** Starts playing a sound. */ 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 {} diff --git a/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java b/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java index 3beddc3..2a72297 100644 --- a/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java +++ b/framework/src/main/java/org/toop/framework/audio/interfaces/SoundEffectManager.java @@ -3,7 +3,11 @@ package org.toop.framework.audio.interfaces; import org.toop.framework.resource.resources.SoundEffectAsset; import org.toop.framework.resource.types.AudioResource; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.IOException; + public interface SoundEffectManager extends AudioManager { void play(String name, boolean loop); - void stop(long clipId); + void stop(String name); } diff --git a/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java b/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java index f55238b..a96e3f4 100644 --- a/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java +++ b/framework/src/main/java/org/toop/framework/resource/resources/SoundEffectAsset.java @@ -8,45 +8,23 @@ import org.toop.framework.resource.types.AudioResource; import org.toop.framework.resource.types.FileExtension; 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"}) public class SoundEffectAsset extends BaseResource implements LoadableResource, AudioResource { - private byte[] rawData; - private Clip clip = null; + private final Clip clip = AudioSystem.getClip(); - public SoundEffectAsset(final File audioFile) { + public SoundEffectAsset(final File audioFile) throws LineUnavailableException { super(audioFile); } // Gets a new clip to play - public Clip getNewClip() - throws LineUnavailableException, UnsupportedAudioFileException, IOException { - // 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; + public Clip getClip() { + if (!this.isLoaded()) {this.load();} return this.clip; } - // Generates a new audio stream from byte array - 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) { + private AudioInputStream downSampleAudio(AudioInputStream audioInputStream, AudioFormat baseFormat) { AudioFormat decodedFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, @@ -61,19 +39,35 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource, return AudioSystem.getAudioInputStream(decodedFormat, audioInputStream); } + + @Override public void load() { 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; - } catch (IOException e) { + } catch (LineUnavailableException | UnsupportedAudioFileException | IOException e) { throw new RuntimeException(e); } } @Override 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; } @@ -112,12 +106,15 @@ public class SoundEffectAsset extends BaseResource implements LoadableResource, @Override public void play() { - // TODO + if (!isLoaded()) load(); + + this.clip.setFramePosition(0); // rewind to the start + this.clip.start(); } @Override public void stop() { - // TODO + if (this.clip.isRunning()) this.clip.stop(); } }