Working state. Split AudioManager into 3 different branches for easier testing and srp

This commit is contained in:
lieght
2025-10-11 04:50:49 +02:00
parent 7f3d858320
commit 123ecc7d3a
12 changed files with 426 additions and 256 deletions

View File

@@ -1,12 +1,20 @@
package org.toop;
import javafx.scene.media.MediaPlayer;
import org.toop.app.App;
import org.toop.framework.audio.SoundManager;
import org.toop.framework.audio.AudioEventListener;
import org.toop.framework.audio.AudioVolumeManager;
import org.toop.framework.audio.MusicManager;
import org.toop.framework.audio.SoundEffectManager;
import org.toop.framework.audio.interfaces.AudioManager;
import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException;
import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.ResourceManager;
import javax.sound.sampled.Clip;
public final class Main {
static void main(String[] args) {
initSystems();
@@ -16,6 +24,13 @@ public final class Main {
private static void initSystems() throws NetworkingInitializationException {
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
new Thread(NetworkingClientManager::new).start();
new Thread(SoundManager::new).start();
new Thread(() -> {
AudioEventListener a =
new AudioEventListener(
new MusicManager(),
new SoundEffectManager(),
new AudioVolumeManager()
); a.initListeners();
}).start();
}
}

View File

@@ -6,6 +6,7 @@ import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import jdk.jfr.Event;
import org.toop.app.layer.Layer;
import org.toop.app.layer.layers.MainLayer;
import org.toop.app.layer.layers.QuitPopup;
@@ -65,10 +66,11 @@ public final class App extends Application {
App.isQuitting = false;
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent();
final AppSettings settings = new AppSettings();
settings.applySettings();
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent();
activate(new MainLayer());
}

View File

@@ -54,9 +54,13 @@ public class AppSettings {
File settingsFile =
new File(basePath + File.separator + "ISY1" + File.separator + "settings.json");
// this.settingsAsset = new SettingsAsset(settingsFile);
ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile)));
return new SettingsAsset(settingsFile);
// this.settingsAsset = new SettingsAsset(settingsFile); // TODO
// ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile))); // TODO
}
return ResourceManager.get("settings.json");
return this.settingsAsset;
// return ResourceManager.get("settings.json"); // TODO
}
}

View File

@@ -0,0 +1,88 @@
package org.toop.framework.audio;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.audio.interfaces.MusicManager;
import org.toop.framework.audio.interfaces.SoundEffectManager;
import org.toop.framework.audio.interfaces.VolumeManager;
import org.toop.framework.eventbus.EventFlow;
public class AudioEventListener {
private final MusicManager<?> musicManager;
private final SoundEffectManager<?> soundEffectManager;
private final VolumeManager audioVolumeManager;
public AudioEventListener(
MusicManager<?> musicManager,
SoundEffectManager<?> soundEffectManager,
VolumeManager audioVolumeManager
) {
this.musicManager = musicManager;
this.soundEffectManager = soundEffectManager;
this.audioVolumeManager = audioVolumeManager;
}
public void initListeners() {
new EventFlow()
.listen(this::handlePlaySound)
.listen(this::handleStopSound)
.listen(this::handleMusicStart)
.listen(this::handleVolumeChange)
.listen(this::handleFxVolumeChange)
.listen(this::handleMusicVolumeChange)
.listen(this::handleGetVolume)
.listen(this::handleGetFxVolume)
.listen(this::handleGetMusicVolume);
}
private void handlePlaySound(AudioEvents.PlayEffect event) {
this.soundEffectManager.play(event.fileName(), event.loop());
}
private void handleStopSound(AudioEvents.StopEffect event) {
this.soundEffectManager.stop(event.clipId());
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic event) {
this.musicManager.play();
}
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
this.audioVolumeManager.setVolume(event.newVolume(), soundEffectManager, musicManager);
}
private void handleFxVolumeChange(AudioEvents.ChangeFxVolume event) {
this.audioVolumeManager.setFxVolume(event.newVolume(), soundEffectManager);
}
private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event) {
this.audioVolumeManager.setMusicVolume(event.newVolume(), musicManager);
}
private void handleGetVolume(AudioEvents.GetCurrentVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentVolumeResponse(
audioVolumeManager.getVolume(),
event.snowflakeId()))
.asyncPostEvent();
}
private void handleGetFxVolume(AudioEvents.GetCurrentFxVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentFxVolumeResponse(
audioVolumeManager.getFxVolume(),
event.snowflakeId()))
.asyncPostEvent();
}
private void handleGetMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentMusicVolumeResponse(
audioVolumeManager.getMusicVolume(),
event.snowflakeId()))
.asyncPostEvent();
}
}

View File

@@ -3,33 +3,21 @@ package org.toop.framework.audio;
import javafx.scene.media.MediaPlayer;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.audio.interfaces.AudioManager;
import org.toop.framework.audio.interfaces.VolumeManager;
public class AudioVolumeManager {
private final SoundManager sM;
public class AudioVolumeManager implements VolumeManager {
private double volume = 0.0;
private double fxVolume = 0.0;
private double musicVolume = 0.0;
private double volume = 1.0;
private double fxVolume = 1.0;
private double musicVolume = 1.0;
public AudioVolumeManager() {}
public AudioVolumeManager(SoundManager soundManager) {
this.sM = soundManager;
new EventFlow()
.listen(this::handleVolumeChange)
.listen(this::handleFxVolumeChange)
.listen(this::handleMusicVolumeChange)
.listen(this::handleGetCurrentVolume)
.listen(this::handleGetCurrentFxVolume)
.listen(this::handleGetCurrentMusicVolume);
}
public void updateMusicVolume(MediaPlayer mediaPlayer) {
private <T extends MediaPlayer> void updateMusicVolume(T mediaPlayer) {
mediaPlayer.setVolume(this.musicVolume * this.volume);
}
public void updateSoundEffectVolume(Clip clip) {
private <T extends Clip> void updateSoundEffectVolume(T clip) {
if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
FloatControl volumeControl =
(FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
@@ -49,50 +37,45 @@ public class AudioVolumeManager {
else return Math.max(volume, 0.0);
}
private void handleFxVolumeChange(AudioEvents.ChangeFxVolume event) {
this.fxVolume = limitVolume(event.newVolume() / 100);
for (Clip clip : sM.getActiveSoundEffects().values()) {
updateSoundEffectVolume(clip);
@Override
public void setVolume(double newVolume, AudioManager<?> sm, AudioManager<?> mm) {
this.volume = limitVolume(newVolume / 100);
for (var clip : sm.getActiveAudio()) {
this.updateSoundEffectVolume((Clip) clip);
}
for (var mediaPlayer : mm.getActiveAudio()) {
this.updateMusicVolume((MediaPlayer) mediaPlayer);
}
}
private void handleVolumeChange(AudioEvents.ChangeVolume event) {
this.volume = limitVolume(event.newVolume() / 100);
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
this.updateMusicVolume(mediaPlayer);
}
for (Clip clip : sM.getActiveSoundEffects().values()) {
updateSoundEffectVolume(clip);
@Override
public void setFxVolume(double newVolume, AudioManager<?> sm) {
this.fxVolume = limitVolume(newVolume / 100);
for (var clip : sm.getActiveAudio()) {
this.updateSoundEffectVolume((Clip) clip); // TODO: What if not clip
}
}
private void handleMusicVolumeChange(AudioEvents.ChangeMusicVolume event) {
this.musicVolume = limitVolume(event.newVolume() / 100);
for (MediaPlayer mediaPlayer : sM.getActiveMusic()) {
this.updateMusicVolume(mediaPlayer);
@Override
public void setMusicVolume(double newVolume, AudioManager<?> mm) {
this.musicVolume = limitVolume(newVolume / 100);
for (var mediaPlayer : mm.getActiveAudio()) {
this.updateMusicVolume((MediaPlayer) mediaPlayer); // TODO; What if not MediaPlayer
}
}
private void handleGetCurrentVolume(AudioEvents.GetCurrentVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentVolumeResponse(volume * 100, event.snowflakeId()))
.asyncPostEvent();
@Override
public double getVolume() {
return volume * 100;
}
private void handleGetCurrentFxVolume(AudioEvents.GetCurrentFxVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentFxVolumeResponse(
fxVolume * 100, event.snowflakeId()))
.asyncPostEvent();
@Override
public double getFxVolume() {
return fxVolume * 100;
}
private void handleGetCurrentMusicVolume(AudioEvents.GetCurrentMusicVolume event) {
new EventFlow()
.addPostEvent(
new AudioEvents.GetCurrentMusicVolumeResponse(
musicVolume * 100, event.snowflakeId()))
.asyncPostEvent();
@Override
public double getMusicVolume() {
return musicVolume * 100;
}
}

View File

@@ -0,0 +1,84 @@
package org.toop.framework.audio;
import javafx.scene.media.MediaPlayer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.resources.MusicAsset;
import java.util.*;
public class MusicManager implements org.toop.framework.audio.interfaces.MusicManager<MediaPlayer> {
private static final Logger logger = LogManager.getLogger(MusicManager.class);
// private final List<MusicAsset> musicAssets = new ArrayList<>(); // TODO
private final List<MediaPlayer> backgroundMusic = new LinkedList<>();
private int playingIndex = 0;
private boolean playing = false;
public MusicManager() {}
@Override
public Collection<MediaPlayer> getActiveAudio() {
return backgroundMusic;
}
private void addBackgroundMusic(MusicAsset musicAsset) {
backgroundMusic.add(new MediaPlayer(musicAsset.getMedia()));
}
private void addBackgroundMusic(MediaPlayer mediaPlayer) {
backgroundMusic.add(mediaPlayer);
}
public void play() { // TODO maybe remove VolumeManager from input
backgroundMusic.clear();
List<MediaPlayer> shuffledArray =
new ArrayList<>(
ResourceManager.getAllOfType(MusicAsset.class).stream()
.map(ma ->
initMediaPlayer(new MediaPlayer(ma.getResource().getMedia())))
.toList());
Collections.shuffle(shuffledArray);
backgroundMusic.addAll(shuffledArray);
backgroundMusicPlayer();
}
private void backgroundMusicPlayer() {
if (playingIndex >= backgroundMusic.size()) {
playingIndex = 0;
}
MediaPlayer ma = backgroundMusic.get(playingIndex);
if (ma == null) {
logger.error("Background music player is null. Queue: {}",
backgroundMusic.stream().map(e -> e.getMedia().getSource()));
return;
}
logger.info("Background music player is playing: {}", ma.getMedia().getSource()); //TODO shorten to name
ma.play();
this.playing = true;
}
private MediaPlayer initMediaPlayer(MediaPlayer mediaPlayer) {
mediaPlayer.setOnEndOfMedia(mediaPlayer::stop);
mediaPlayer.setOnError( () -> {
logger.error("Error playing music: {}", mediaPlayer.getMedia().getSource());
backgroundMusic.remove(mediaPlayer);
mediaPlayer.stop();
});
mediaPlayer.setOnStopped( () -> {
mediaPlayer.stop();
playingIndex++;
this.playing = false;
backgroundMusicPlayer();
});
return mediaPlayer;
}
}

View File

@@ -0,0 +1,28 @@
package org.toop.framework.audio;
import org.toop.framework.resource.resources.SoundEffectAsset;
import javax.sound.sampled.Clip;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class SoundEffectManager implements org.toop.framework.audio.interfaces.SoundEffectManager<Clip> {
private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
public Collection<Clip> getActiveAudio() {
return this.activeSoundEffects.values();
}
@Override
public void play(String name, boolean loop) {
}
@Override
public void stop(long clipId) {
}
}

View File

@@ -1,197 +1,135 @@
package org.toop.framework.audio;
import java.io.*;
import java.util.*;
import javafx.scene.media.MediaPlayer;
import javax.sound.sampled.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.ResourceMeta;
import org.toop.framework.resource.resources.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset;
public class SoundManager {
private static final Logger logger = LogManager.getLogger(SoundManager.class);
private final List<MediaPlayer> activeMusic = new ArrayList<>();
private final Queue<MusicAsset> backgroundMusicQueue = new LinkedList<>();
private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
private final AudioVolumeManager audioVolumeManager = new AudioVolumeManager(this);
public SoundManager() {
// Get all Audio Resources and add them to a list.
for (ResourceMeta<SoundEffectAsset> asset :
ResourceManager.getAllOfType(SoundEffectAsset.class)) {
try {
this.addAudioResource(asset);
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
throw new RuntimeException(e);
}
}
new EventFlow()
.listen(this::handlePlaySound)
.listen(this::handleStopSound)
.listen(this::handleMusicStart)
.listen(
AudioEvents.ClickButton.class,
_ -> {
try {
playSound("medium-button-click.wav", false);
} catch (UnsupportedAudioFileException
| LineUnavailableException
| IOException e) {
logger.error(e);
}
});
}
private void handlePlaySound(AudioEvents.PlayEffect event) {
try {
this.playSound(event.fileName(), event.loop());
} catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
throw new RuntimeException(e);
}
}
private void handleStopSound(AudioEvents.StopEffect event) {
this.stopSound(event.clipId());
}
private void addAudioResource(ResourceMeta<SoundEffectAsset> audioAsset)
throws IOException, UnsupportedAudioFileException, LineUnavailableException {
this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
}
private void handleMusicStart(AudioEvents.StartBackgroundMusic e) {
backgroundMusicQueue.clear();
List<MusicAsset> shuffledArray =
new ArrayList<>(
ResourceManager.getAllOfType(MusicAsset.class).stream()
.map(ResourceMeta::getResource)
.toList());
Collections.shuffle(shuffledArray);
backgroundMusicQueue.addAll(shuffledArray);
backgroundMusicPlayer();
}
private void addBackgroundMusic(MusicAsset musicAsset) {
backgroundMusicQueue.add(musicAsset);
}
private void backgroundMusicPlayer() {
MusicAsset ma = backgroundMusicQueue.poll();
if (ma == null) return;
MediaPlayer mediaPlayer = new MediaPlayer(ma.getMedia());
mediaPlayer.setOnEndOfMedia(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
mediaPlayer.dispose();
ma.unload();
backgroundMusicPlayer(); // play next
});
mediaPlayer.setOnStopped(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
});
mediaPlayer.setOnError(
() -> {
addBackgroundMusic(ma);
activeMusic.remove(mediaPlayer);
ma.unload();
});
audioVolumeManager.updateMusicVolume(mediaPlayer);
mediaPlayer.play();
activeMusic.add(mediaPlayer);
logger.info("Playing background music: {}", ma.getFile().getName());
logger.info(
"Background music next in line: {}",
backgroundMusicQueue.peek() != null
? backgroundMusicQueue.peek().getFile().getName()
: null);
}
private long playSound(String audioFileName, boolean loop)
throws UnsupportedAudioFileException, LineUnavailableException, IOException {
SoundEffectAsset asset = audioResources.get(audioFileName);
// Return -1 which indicates resource wasn't available
if (asset == null) {
logger.warn("Unable to load audio asset: {}", audioFileName);
return -1;
}
// Get a new clip from resource
Clip clip = asset.getNewClip();
// Set volume of clip
audioVolumeManager.updateSoundEffectVolume(clip);
// If supposed to loop make it loop, else just start it once
if (loop) {
clip.loop(Clip.LOOP_CONTINUOUSLY);
} else {
clip.start();
}
logger.debug("Playing sound: {}", asset.getFile().getName());
// Generate id for clip
long clipId = new SnowflakeGenerator().nextId();
// store it so we can stop it later
activeSoundEffects.put(clipId, clip);
// remove when finished (only for non-looping sounds)
clip.addLineListener(
event -> {
if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
activeSoundEffects.remove(clipId);
clip.close();
}
});
// Return id so it can be stopped
return clipId;
}
public void stopSound(long clipId) {
Clip clip = activeSoundEffects.get(clipId);
if (clip == null) {
return;
}
clip.stop();
clip.close();
activeSoundEffects.remove(clipId);
}
public void stopAllSounds() {
for (Clip clip : activeSoundEffects.values()) {
clip.stop();
clip.close();
}
activeSoundEffects.clear();
}
public Map<Long, Clip> getActiveSoundEffects() {
return this.activeSoundEffects;
}
public List<MediaPlayer> getActiveMusic() {
return activeMusic;
}
}
//package org.toop.framework.audio;
//
//import java.io.*;
//import java.util.*;
//import javafx.scene.media.MediaPlayer;
//import javax.sound.sampled.*;
//import org.apache.logging.log4j.LogManager;
//import org.apache.logging.log4j.Logger;
//import org.toop.framework.SnowflakeGenerator;
//import org.toop.framework.audio.events.AudioEvents;
//import org.toop.framework.eventbus.EventFlow;
//import org.toop.framework.resource.ResourceManager;
//import org.toop.framework.resource.ResourceMeta;
//import org.toop.framework.resource.resources.MusicAsset;
//import org.toop.framework.resource.resources.SoundEffectAsset;
//
//public class SoundManager {
// private static final Logger logger = LogManager.getLogger(SoundManager.class);
// private final Map<Long, Clip> activeSoundEffects = new HashMap<>();
// private final HashMap<String, SoundEffectAsset> audioResources = new HashMap<>();
//// private final AudioVolumeManager audioVolumeManager = new AudioVolumeManager(this);
//
// public SoundManager() {
// // Get all Audio Resources and add them to a list.
// for (ResourceMeta<SoundEffectAsset> asset :
// ResourceManager.getAllOfType(SoundEffectAsset.class)) {
// try {
// this.addAudioResource(asset);
// } catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
// throw new RuntimeException(e);
// }
// }
// new EventFlow()
// .listen(this::handlePlaySound)
// .listen(this::handleStopSound)
// .listen(
// AudioEvents.ClickButton.class,
// _ -> {
// try {
// playSound("medium-button-click.wav", false);
// } catch (UnsupportedAudioFileException
// | LineUnavailableException
// | IOException e) {
// logger.error(e);
// }
// });
// }
//
// private void handlePlaySound(AudioEvents.PlayEffect event) {
// try {
// this.playSound(event.fileName(), event.loop());
// } catch (UnsupportedAudioFileException | LineUnavailableException | IOException e) {
// throw new RuntimeException(e);
// }
// }
//
// private void handleStopSound(AudioEvents.StopEffect event) {
// this.stopSound(event.clipId());
// }
//
// private void addAudioResource(ResourceMeta<SoundEffectAsset> audioAsset)
// throws IOException, UnsupportedAudioFileException, LineUnavailableException {
//
// this.audioResources.put(audioAsset.getName(), audioAsset.getResource());
// }
//
// private long playSound(String audioFileName, boolean loop)
// throws UnsupportedAudioFileException, LineUnavailableException, IOException {
// SoundEffectAsset asset = audioResources.get(audioFileName);
//
// // Return -1 which indicates resource wasn't available
// if (asset == null) {
// logger.warn("Unable to load audio asset: {}", audioFileName);
// return -1;
// }
//
// // Get a new clip from resource
// Clip clip = asset.getNewClip();
//
// // Set volume of clip
//// audioVolumeManager.updateSoundEffectVolume(clip);
//
// // If supposed to loop make it loop, else just start it once
// if (loop) {
// clip.loop(Clip.LOOP_CONTINUOUSLY);
// } else {
// clip.start();
// }
//
// logger.debug("Playing sound: {}", asset.getFile().getName());
//
// // Generate id for clip
// long clipId = new SnowflakeGenerator().nextId();
//
// // store it so we can stop it later
// activeSoundEffects.put(clipId, clip);
//
// // remove when finished (only for non-looping sounds)
// clip.addLineListener(
// event -> {
// if (event.getType() == LineEvent.Type.STOP && !clip.isRunning()) {
// activeSoundEffects.remove(clipId);
// clip.close();
// }
// });
//
// // Return id so it can be stopped
// return clipId;
// }
//
// public void stopSound(long clipId) {
// Clip clip = activeSoundEffects.get(clipId);
//
// if (clip == null) {
// return;
// }
//
// clip.stop();
// clip.close();
// activeSoundEffects.remove(clipId);
// }
//
// public void stopAllSounds() {
// for (Clip clip : activeSoundEffects.values()) {
// clip.stop();
// clip.close();
// }
// activeSoundEffects.clear();
// }
//
// public Map<Long, Clip> getActiveSoundEffects() {
// return this.activeSoundEffects;
// }
//
//}

View File

@@ -0,0 +1,7 @@
package org.toop.framework.audio.interfaces;
import java.util.Collection;
public interface AudioManager<T> {
Collection<T> getActiveAudio();
}

View File

@@ -0,0 +1,5 @@
package org.toop.framework.audio.interfaces;
public interface MusicManager<T> extends AudioManager<T> {
void play();
}

View File

@@ -0,0 +1,6 @@
package org.toop.framework.audio.interfaces;
public interface SoundEffectManager<T> extends AudioManager<T> {
void play(String name, boolean loop);
void stop(long clipId);
}

View File

@@ -0,0 +1,10 @@
package org.toop.framework.audio.interfaces;
public interface VolumeManager {
void setVolume(double newVolume, AudioManager<?> sm, AudioManager<?> mm);
void setFxVolume(double newVolume, AudioManager<?> sm);
void setMusicVolume(double newVolume, AudioManager<?> mm);
double getVolume();
double getFxVolume();
double getMusicVolume();
}