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.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> {
private final MusicManager<T> musicManager;
private final SoundEffectManager<K> soundEffectManager;
@@ -33,7 +37,10 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
.listen(this::handleMusicVolumeChange)
.listen(this::handleGetVolume)
.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) {
@@ -45,7 +52,7 @@ public class AudioEventListener<T extends AudioResource, K extends AudioResource
}
private void handleStopSound(AudioEvents.StopEffect event) {
this.soundEffectManager.stop(event.clipId());
this.soundEffectManager.stop(event.fileName());
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {

View File

@@ -1,27 +1,64 @@
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 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.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class SoundEffectManager implements org.toop.framework.audio.interfaces.SoundEffectManager<SoundEffectAsset> {
private final Map<Long, SoundEffectAsset> activeSoundEffects = new HashMap<>();
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
private static final Logger logger = LogManager.getLogger(SoundEffectManager.class);
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
public Collection<SoundEffectAsset> 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());
}
}

View File

@@ -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 {}

View File

@@ -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<T extends AudioResource> extends AudioManager<T> {
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.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();
}
}