feat(audio): Implement audio management system with multi-stream and single-stream players
- Added AudioData class to encapsulate audio name and input stream. - Introduced AudioLoader for loading audio files with caching mechanism. - Created AudioLoaderRequest for handling audio loading requests. - Developed AudioOptions class to manage audio settings for sound effects and music. - Implemented AudioPlayer interface and its concrete classes: AudioPlayerMultiStream and AudioPlayerSingleStream. - Established AudioStrategy interface with implementations for Java Mixer and Software Mixing strategies. - Enhanced StaticResource class with a custom toString method for better resource path representation. - Updated Spring configuration to include AudioAutoConfiguration for audio management.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResourceService;
|
||||
import de.extio.game_engine.storage.StorageService;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(name = "game-engine.audio.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class AudioAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
AudioController audioController(final StaticResourceService resourceService, final StorageService storageService) {
|
||||
return new AudioController(resourceService, storageService);
|
||||
}
|
||||
}
|
||||
45
src/main/java/de/extio/game_engine/audio/AudioControl.java
Normal file
45
src/main/java/de/extio/game_engine/audio/AudioControl.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
|
||||
/**
|
||||
* Controls the audio system
|
||||
*/
|
||||
public interface AudioControl {
|
||||
|
||||
/**
|
||||
* Plays a collection of music tracks, music playback will be shuffled and repeat
|
||||
* @param opener Optional opener track to be played first, will not repeat, can be null
|
||||
* @param audioResources Collection of music tracks to be played
|
||||
* @param standard Defines the given collection of music tracks as standard collection, see playStandardMusic()
|
||||
*/
|
||||
void playMusic(StaticResource opener, List<StaticResource> audioResources, boolean standard);
|
||||
|
||||
/**
|
||||
* Plays the standard collection of music tracks, music playback will be shuffled and repeat
|
||||
*/
|
||||
void playStandardMusic();
|
||||
|
||||
/**
|
||||
* Stops playing music
|
||||
*/
|
||||
void stopMusic();
|
||||
|
||||
/**
|
||||
* Plays a sound effect once
|
||||
*/
|
||||
void play(StaticResource audioResource);
|
||||
|
||||
/**
|
||||
* @return Copy of audio options to configure the audio system
|
||||
*/
|
||||
AudioOptions getAudioOptions();
|
||||
|
||||
/**
|
||||
* Applies the given audio options to the audio system
|
||||
*/
|
||||
void applyAudioOptions(AudioOptions audioOptions);
|
||||
|
||||
}
|
||||
184
src/main/java/de/extio/game_engine/audio/AudioController.java
Normal file
184
src/main/java/de/extio/game_engine/audio/AudioController.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
import de.extio.game_engine.resource.StaticResourceService;
|
||||
import de.extio.game_engine.storage.StorageService;
|
||||
|
||||
public final class AudioController implements AudioControl {
|
||||
|
||||
private final StorageService storageService;
|
||||
|
||||
private AudioOptions audioOptions;
|
||||
|
||||
private AudioStrategy audioStrategy;
|
||||
|
||||
private final AudioLoader audioLoader;
|
||||
|
||||
private Thread audioLoaderThread;
|
||||
|
||||
private final Thread audioControllerThread;
|
||||
|
||||
private final List<StaticResource> standardMusic = new ArrayList<>();
|
||||
|
||||
private List<StaticResource> lastMusicQueued = null;
|
||||
|
||||
private boolean started;
|
||||
|
||||
private boolean useSoftwareMixing;
|
||||
|
||||
public AudioController(final StaticResourceService resourceService, final StorageService storageService) {
|
||||
this.storageService = storageService;
|
||||
storageService.loadByPath(AudioOptions.class, List.of("audio"), "audioOptions").ifPresentOrElse(
|
||||
loadedOptions -> {
|
||||
this.audioOptions = loadedOptions;
|
||||
},
|
||||
() -> {
|
||||
this.audioOptions = new AudioOptions();
|
||||
this.storeAudioOptions();
|
||||
});
|
||||
|
||||
this.audioLoader = new AudioLoader(resourceService);
|
||||
|
||||
this.startStrategy();
|
||||
|
||||
this.audioControllerThread = Thread.ofPlatform()
|
||||
.name("Audio Controller")
|
||||
.daemon(true)
|
||||
.start(() -> {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
this.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeAudioOptions() {
|
||||
this.storageService.store(List.of("audio"), "audioOptions", this.audioOptions);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (!this.started || this.audioOptions.getSfxOptions().isSoftwareMixing() != this.useSoftwareMixing) {
|
||||
this.stopStrategy();
|
||||
this.startStrategy();
|
||||
}
|
||||
|
||||
if (this.audioStrategy != null) {
|
||||
this.audioStrategy.run();
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.audioControllerThread.interrupt();
|
||||
this.stopStrategy();
|
||||
}
|
||||
|
||||
private void startStrategy() {
|
||||
if (!this.started) {
|
||||
this.audioLoader.getQueue().clear();
|
||||
this.audioLoaderThread = Thread.ofPlatform()
|
||||
.name("Audio Loader")
|
||||
.daemon(true)
|
||||
.start(this.audioLoader);
|
||||
|
||||
if (this.audioOptions.getSfxOptions().isSoftwareMixing()) {
|
||||
this.audioStrategy = new AudioStrategySoftwareMixing(this.audioOptions, this.audioLoader);
|
||||
this.useSoftwareMixing = true;
|
||||
}
|
||||
else {
|
||||
this.audioStrategy = new AudioStrategyJavaMixer(this.audioOptions, this.audioLoader);
|
||||
this.useSoftwareMixing = false;
|
||||
}
|
||||
|
||||
this.audioStrategy.start();
|
||||
if (this.lastMusicQueued != null) {
|
||||
this.audioStrategy.queueMusic(this.lastMusicQueued);
|
||||
}
|
||||
|
||||
this.started = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopStrategy() {
|
||||
if (this.started) {
|
||||
if (this.audioStrategy != null) {
|
||||
this.audioStrategy.stop();
|
||||
this.audioStrategy = null;
|
||||
}
|
||||
if (this.audioLoaderThread != null) {
|
||||
this.audioLoaderThread.interrupt();
|
||||
this.audioLoaderThread = null;
|
||||
}
|
||||
this.audioLoader.getQueue().clear();
|
||||
this.started = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playMusic(final StaticResource opener, final List<StaticResource> audioResources, final boolean standard) {
|
||||
if (audioResources == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (standard) {
|
||||
this.standardMusic.clear();
|
||||
this.standardMusic.addAll(audioResources);
|
||||
}
|
||||
|
||||
if (this.audioStrategy != null) {
|
||||
this.lastMusicQueued = this.wrapOpener(opener, audioResources);
|
||||
this.audioStrategy.queueMusic(this.lastMusicQueued);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playStandardMusic() {
|
||||
if (this.audioStrategy != null) {
|
||||
this.lastMusicQueued = this.wrapOpener(null, this.standardMusic);
|
||||
this.audioStrategy.queueMusic(this.lastMusicQueued);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopMusic() {
|
||||
if (this.audioStrategy != null) {
|
||||
this.audioStrategy.clearMusic();
|
||||
}
|
||||
this.lastMusicQueued = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(final StaticResource audioResource) {
|
||||
if (this.audioStrategy != null) {
|
||||
this.audioStrategy.queueSfx(List.of(audioResource));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioOptions getAudioOptions() {
|
||||
return new AudioOptions(this.audioOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyAudioOptions(final AudioOptions audioOptions) {
|
||||
this.audioOptions.apply(audioOptions);
|
||||
this.storeAudioOptions();
|
||||
}
|
||||
|
||||
private List<StaticResource> wrapOpener(final StaticResource opener, final List<StaticResource> audioFileNames) {
|
||||
final List<StaticResource> result = new ArrayList<>(audioFileNames.size() + 1);
|
||||
result.add(opener);
|
||||
result.addAll(audioFileNames);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
7
src/main/java/de/extio/game_engine/audio/AudioData.java
Normal file
7
src/main/java/de/extio/game_engine/audio/AudioData.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public record AudioData(String name, InputStream inputStream) {
|
||||
|
||||
}
|
||||
110
src/main/java/de/extio/game_engine/audio/AudioLoader.java
Normal file
110
src/main/java/de/extio/game_engine/audio/AudioLoader.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResourceService;
|
||||
|
||||
final class AudioLoader implements Runnable {
|
||||
|
||||
private final static int CACHE_SIZE_LIMIT = 128;
|
||||
|
||||
private final static int CACHE_ELEMENT_BYTES_LIMIT = 262144;
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(AudioLoader.class);
|
||||
|
||||
private final BlockingQueue<AudioLoaderRequest> queue = new LinkedBlockingQueue<>(50);
|
||||
|
||||
private final Map<String, CacheEntry> lruCache = new LRUCache<>();
|
||||
|
||||
private final StaticResourceService resourceService;
|
||||
|
||||
public AudioLoader(final StaticResourceService resourceService) {
|
||||
this.resourceService = resourceService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("start");
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
final AudioLoaderRequest request = this.queue.take();
|
||||
|
||||
if (request.isUseCache()) {
|
||||
final String cacheKey = request.getStaticResource().toString();
|
||||
|
||||
final CacheEntry cacheEntry = this.lruCache.get(cacheKey);
|
||||
if (cacheEntry != null) {
|
||||
LOGGER.debug("Loading audio file from cache: {}", cacheKey);
|
||||
|
||||
cacheEntry.lastAccessTime().set(System.currentTimeMillis());
|
||||
request.getConsumer().accept(new ByteArrayInputStream(cacheEntry.data()));
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Loading audio file from disk: {}", cacheKey);
|
||||
|
||||
this.resourceService.loadStreamByPath(request.getStaticResource()).ifPresent(resourceStream -> {
|
||||
try (InputStream stream = resourceStream) {
|
||||
final byte[] bytes = stream.readAllBytes();
|
||||
LOGGER.debug("Loaded {} bytes", bytes.length);
|
||||
|
||||
if (bytes.length <= CACHE_ELEMENT_BYTES_LIMIT) {
|
||||
this.lruCache.put(cacheKey, new CacheEntry(bytes, new AtomicLong(System.currentTimeMillis())));
|
||||
LOGGER.debug("Added to cache: {}", cacheKey);
|
||||
}
|
||||
|
||||
request.getConsumer().accept(new ByteArrayInputStream(bytes));
|
||||
}
|
||||
catch (final IOException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Loading audio file from disk (no cache): {}", request.getStaticResource());
|
||||
|
||||
// Stream will be closed by audio player async, don't close it here
|
||||
this.resourceService.loadStreamByPath(request.getStaticResource()).ifPresent(request.getConsumer()::accept);
|
||||
}
|
||||
}
|
||||
catch (final Exception e) {
|
||||
if (e instanceof InterruptedException) {
|
||||
LOGGER.info(e.getClass().getName());
|
||||
break;
|
||||
}
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("end");
|
||||
}
|
||||
|
||||
public BlockingQueue<AudioLoaderRequest> getQueue() {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||||
|
||||
public LRUCache() {
|
||||
super((int) (CACHE_SIZE_LIMIT * 1.5), 0.75f, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
|
||||
return this.size() >= CACHE_SIZE_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
private static record CacheEntry(byte[] data, AtomicLong lastAccessTime) {}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
|
||||
final class AudioLoaderRequest {
|
||||
|
||||
private StaticResource StaticResource;
|
||||
|
||||
private Consumer<InputStream> consumer;
|
||||
|
||||
private boolean useCache;
|
||||
|
||||
public AudioLoaderRequest(final StaticResource StaticResource, final boolean useCache, final Consumer<InputStream> consumer) {
|
||||
this.StaticResource = StaticResource;
|
||||
this.useCache = useCache;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public Consumer<InputStream> getConsumer() {
|
||||
return this.consumer;
|
||||
}
|
||||
|
||||
public void setConsumer(final Consumer<InputStream> consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public StaticResource getStaticResource() {
|
||||
return this.StaticResource;
|
||||
}
|
||||
|
||||
public void setStaticResource(final StaticResource StaticResource) {
|
||||
this.StaticResource = StaticResource;
|
||||
}
|
||||
|
||||
public boolean isUseCache() {
|
||||
return this.useCache;
|
||||
}
|
||||
|
||||
public void setUseCache(final boolean useCache) {
|
||||
this.useCache = useCache;
|
||||
}
|
||||
|
||||
}
|
||||
83
src/main/java/de/extio/game_engine/audio/AudioOptions.java
Normal file
83
src/main/java/de/extio/game_engine/audio/AudioOptions.java
Normal file
@@ -0,0 +1,83 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
public class AudioOptions {
|
||||
|
||||
private AudioStreamOptions sfxOptions;
|
||||
|
||||
private AudioStreamOptions musicOptions;
|
||||
|
||||
public AudioOptions() {
|
||||
this.sfxOptions = new AudioStreamOptions(true, 1.0, true);
|
||||
this.musicOptions = new AudioStreamOptions(true, 0.8, false);
|
||||
}
|
||||
|
||||
public AudioOptions(final AudioOptions other) {
|
||||
this.sfxOptions = new AudioStreamOptions(other.sfxOptions.enable, other.sfxOptions.volume, other.sfxOptions.softwareMixing);
|
||||
this.musicOptions = new AudioStreamOptions(other.musicOptions.enable, other.musicOptions.volume, other.musicOptions.softwareMixing);
|
||||
}
|
||||
|
||||
public void apply(final AudioOptions other) {
|
||||
this.sfxOptions = new AudioStreamOptions(other.sfxOptions.enable, other.sfxOptions.volume, other.sfxOptions.softwareMixing);
|
||||
this.musicOptions = new AudioStreamOptions(other.musicOptions.enable, other.musicOptions.volume, other.musicOptions.softwareMixing);
|
||||
}
|
||||
|
||||
public AudioStreamOptions getSfxOptions() {
|
||||
return sfxOptions;
|
||||
}
|
||||
|
||||
public void setSfxOptions(final AudioStreamOptions sfxOptions) {
|
||||
this.sfxOptions = sfxOptions;
|
||||
}
|
||||
|
||||
public AudioStreamOptions getMusicOptions() {
|
||||
return musicOptions;
|
||||
}
|
||||
|
||||
public void setMusicOptions(final AudioStreamOptions musicOptions) {
|
||||
this.musicOptions = musicOptions;
|
||||
}
|
||||
|
||||
public class AudioStreamOptions {
|
||||
|
||||
boolean enable;
|
||||
|
||||
double volume;
|
||||
|
||||
private boolean softwareMixing;
|
||||
|
||||
public AudioStreamOptions() {
|
||||
|
||||
}
|
||||
|
||||
public AudioStreamOptions(final boolean enable, final double volume, final boolean softwareMixing) {
|
||||
this.enable = enable;
|
||||
this.volume = volume;
|
||||
this.softwareMixing = softwareMixing;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return this.enable;
|
||||
}
|
||||
|
||||
public void setEnable(final boolean enable) {
|
||||
this.enable = enable;
|
||||
}
|
||||
|
||||
public double getVolume() {
|
||||
return this.volume;
|
||||
}
|
||||
|
||||
public void setVolume(final double volume) {
|
||||
this.volume = volume;
|
||||
}
|
||||
|
||||
public boolean isSoftwareMixing() {
|
||||
return this.softwareMixing;
|
||||
}
|
||||
|
||||
public void setSoftwareMixing(final boolean softwareMixing) {
|
||||
this.softwareMixing = softwareMixing;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
src/main/java/de/extio/game_engine/audio/AudioPlayer.java
Normal file
19
src/main/java/de/extio/game_engine/audio/AudioPlayer.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import de.extio.game_engine.audio.AudioOptions.AudioStreamOptions;
|
||||
|
||||
interface AudioPlayer extends Runnable {
|
||||
|
||||
void exit();
|
||||
|
||||
void stopAudio();
|
||||
|
||||
void updateAudioOptions(AudioStreamOptions options);
|
||||
|
||||
boolean isPlaying();
|
||||
|
||||
BlockingQueue<AudioData> getQueue();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.BooleanControl;
|
||||
import javax.sound.sampled.DataLine.Info;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.extio.game_engine.audio.AudioOptions.AudioStreamOptions;
|
||||
|
||||
final class AudioPlayerMultiStream implements AudioPlayer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AudioPlayerMultiStream.class);
|
||||
|
||||
private static final int STREAMS = 4;
|
||||
|
||||
private static final int MUSIC_FADING_MS = 1000;
|
||||
|
||||
private final BlockingQueue<AudioData> queue = new LinkedBlockingQueue<>(20);
|
||||
|
||||
private volatile AudioData musicQueue;
|
||||
|
||||
private volatile boolean musicPlaying;
|
||||
|
||||
private volatile boolean stopAudio;
|
||||
|
||||
private volatile boolean stopMusic;
|
||||
|
||||
private volatile boolean playing;
|
||||
|
||||
private volatile double sfxVolume;
|
||||
|
||||
private volatile boolean sfxMute;
|
||||
|
||||
private volatile double musicVolume;
|
||||
|
||||
private volatile boolean musicMute;
|
||||
|
||||
private volatile boolean exit;
|
||||
|
||||
private boolean curMute;
|
||||
|
||||
private long notStartedAudioCnt;
|
||||
|
||||
public AudioPlayerMultiStream() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("start");
|
||||
|
||||
final int[] mix = new int[STREAMS];
|
||||
final double[] vols = new double[STREAMS];
|
||||
double musicFadeVol = 1.0;
|
||||
long musicFading = 0;
|
||||
final AudioBuffer[] buffers = new AudioBuffer[STREAMS];
|
||||
final AudioPlayerStream[] streams = new AudioPlayerStream[STREAMS];
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
buffers[i] = new AudioBuffer();
|
||||
streams[i] = new AudioPlayerStream();
|
||||
}
|
||||
final AudioBuffer mixBuffer = new AudioBuffer();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted() && !this.exit) {
|
||||
SourceDataLine line = null;
|
||||
try {
|
||||
while (!Thread.currentThread().isInterrupted() && !this.exit) {
|
||||
if (this.stopAudio) {
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
try {
|
||||
streams[i].close();
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
this.musicPlaying = false;
|
||||
this.stopAudio = false;
|
||||
}
|
||||
if (this.stopMusic) {
|
||||
if (this.musicPlaying && musicFading == 0) {
|
||||
musicFading = System.currentTimeMillis();
|
||||
}
|
||||
this.stopMusic = false;
|
||||
}
|
||||
if (musicFading > 0) {
|
||||
final int ms = (int) (System.currentTimeMillis() - musicFading);
|
||||
if (ms >= MUSIC_FADING_MS) {
|
||||
streams[0].close();
|
||||
this.musicPlaying = false;
|
||||
musicFadeVol = 1.0;
|
||||
musicFading = 0;
|
||||
}
|
||||
else {
|
||||
musicFadeVol = 1.0 - ((double) ms / MUSIC_FADING_MS);
|
||||
}
|
||||
}
|
||||
else {
|
||||
musicFadeVol = 1.0;
|
||||
}
|
||||
|
||||
// Load music
|
||||
{
|
||||
if (streams[0].inStream == null) {
|
||||
final AudioData musicQueued = this.musicQueue;
|
||||
if (musicQueued != null) {
|
||||
if (this.setupStream(musicQueued, streams, 0)) {
|
||||
this.musicPlaying = true;
|
||||
}
|
||||
this.musicQueue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load new sfx streams if available
|
||||
for (int i = 1; i < STREAMS; i++) {
|
||||
if (streams[i].inStream == null) {
|
||||
final AudioData data = this.queue.poll();
|
||||
if (data == null) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
this.setupStream(data, streams, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manage streams
|
||||
|
||||
int streamsActive = 0;
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
if (streams[i].inStream != null) {
|
||||
streamsActive++;
|
||||
}
|
||||
buffers[i].size = 0;
|
||||
}
|
||||
|
||||
this.playing = streamsActive > 0;
|
||||
if (!this.playing) {
|
||||
// No stream active
|
||||
|
||||
if (line != null) {
|
||||
// Close line
|
||||
line.drain();
|
||||
line.stop();
|
||||
line.close();
|
||||
line = null;
|
||||
|
||||
LOGGER.debug("Line closed");
|
||||
}
|
||||
|
||||
Thread.sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == null) {
|
||||
// Init line
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
if (streams[i].info != null) {
|
||||
line = (SourceDataLine) AudioSystem.getLine(streams[i].info);
|
||||
if (line != null) {
|
||||
line.open(streams[i].info.getFormats()[0]);
|
||||
line.start();
|
||||
LOGGER.debug("Line opened");
|
||||
this.applyOptions(line, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line == null) {
|
||||
Thread.sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.applyOptions(line, false);
|
||||
|
||||
// Read stream buffers
|
||||
|
||||
boolean readMore;
|
||||
do {
|
||||
readMore = false;
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
if (streams[i].convertedAudio != null && buffers[i].size < buffers[i].bytes.length) {
|
||||
final int read = streams[i].convertedAudio.read(buffers[i].bytes, buffers[i].size, buffers[i].bytes.length - buffers[i].size);
|
||||
if (read == -1) {
|
||||
streams[i].close();
|
||||
if (i == 0) {
|
||||
this.musicPlaying = false;
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("End of stream " + i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
buffers[i].size += read;
|
||||
readMore |= buffers[i].size < buffers[i].bytes.length;
|
||||
|
||||
//if (LOGGER.isDebugEnabled()) {
|
||||
// LOGGER.debug("Read buffer " + i + " " + buffers[i].size);
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (readMore && !this.exit);
|
||||
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
if (i == 0) {
|
||||
vols[i] = this.musicVolume * musicFadeVol;
|
||||
}
|
||||
else {
|
||||
vols[i] = this.sfxVolume;
|
||||
}
|
||||
}
|
||||
|
||||
int val;
|
||||
int samplesAvailable;
|
||||
mixBuffer.size = mixBuffer.bytes.length;
|
||||
for (int n = 0; n < mixBuffer.bytes.length; n += 2) {
|
||||
// Collect samples, factor in volume at this point
|
||||
samplesAvailable = 0;
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
if (buffers[i].size > n + 1) {
|
||||
mix[samplesAvailable++] = (int) (((buffers[i].bytes[n] & 0xFF) | (short) (buffers[i].bytes[n + 1] << 8)) * vols[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Mix samples
|
||||
val = 0;
|
||||
if (samplesAvailable == 1) {
|
||||
val = mix[0];
|
||||
}
|
||||
else if (samplesAvailable > 1) {
|
||||
val = mix[0] + 32768;
|
||||
for (int i = 1; i < samplesAvailable; i++) {
|
||||
mix[i] += 32768;
|
||||
|
||||
if ((val < 32768) || (mix[i] < 32768)) {
|
||||
val = val * mix[i] / 32768;
|
||||
}
|
||||
else {
|
||||
val = 2 * (val + mix[i]) - (val * mix[i]) / 32768 - 65536;
|
||||
}
|
||||
|
||||
if (val == 65536) {
|
||||
val = 65535;
|
||||
}
|
||||
}
|
||||
val -= 32768;
|
||||
}
|
||||
|
||||
// Write sample
|
||||
if (samplesAvailable > 0) {
|
||||
mixBuffer.bytes[n] = (byte) (val & 0xFF);
|
||||
mixBuffer.bytes[n + 1] = (byte) (val >> 8);
|
||||
}
|
||||
else {
|
||||
mixBuffer.size = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Write mix buffer
|
||||
if (mixBuffer.size > 0) {
|
||||
//LOGGER.debug("Write " + mixBuffer.size);
|
||||
line.write(mixBuffer.bytes, 0, mixBuffer.size);
|
||||
}
|
||||
|
||||
this.notStartedAudioCnt = 0;
|
||||
}
|
||||
}
|
||||
catch (final InterruptedException exc) {
|
||||
LOGGER.info(exc.getClass().getName());
|
||||
break;
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
LOGGER.error(exc.getMessage(), exc);
|
||||
|
||||
if (++this.notStartedAudioCnt > 5) {
|
||||
this.notStartedAudioCnt = 0;
|
||||
this.exit = true;
|
||||
LOGGER.error("Too many audio playback errors, shutting down audio player.");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (line != null) {
|
||||
line.close();
|
||||
line = null;
|
||||
LOGGER.debug("Line closed");
|
||||
}
|
||||
for (int i = 0; i < STREAMS; i++) {
|
||||
try {
|
||||
streams[i].close();
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
this.musicPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.playing = false;
|
||||
LOGGER.debug("end");
|
||||
}
|
||||
|
||||
private boolean setupStream(final AudioData data, final AudioPlayerStream[] streams, final int idx) throws Exception {
|
||||
try {
|
||||
streams[idx].inStream = data.inputStream();
|
||||
streams[idx].inAudio = AudioSystem.getAudioInputStream(streams[idx].inStream);
|
||||
|
||||
final AudioFormat inFormat = streams[idx].inAudio.getFormat();
|
||||
final int ch = inFormat.getChannels();
|
||||
final float rate = inFormat.getSampleRate();
|
||||
final AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
|
||||
streams[idx].info = new Info(SourceDataLine.class, outFormat);
|
||||
streams[idx].convertedAudio = AudioSystem.getAudioInputStream(outFormat, streams[idx].inAudio);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Start stream " + idx + " " + data.name() + ": in " + inFormat + "; out " + outFormat);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.error(ex.getMessage(), ex);
|
||||
streams[idx].close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopAudio() {
|
||||
try {
|
||||
this.queue.clear();
|
||||
this.stopAudio = true;
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAudioOptions(final AudioStreamOptions options) {
|
||||
this.sfxMute = !options.isEnable();
|
||||
this.sfxVolume = this.sfxMute ? 0.0 : options.getVolume();
|
||||
}
|
||||
|
||||
public void updateMusicAudioOptions(final AudioStreamOptions options) {
|
||||
this.musicMute = !options.isEnable();
|
||||
if (this.musicMute) {
|
||||
this.musicVolume = 0.0;
|
||||
}
|
||||
else {
|
||||
this.musicVolume = options.getVolume();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyOptions(final SourceDataLine line, final boolean force) {
|
||||
if (force) {
|
||||
final FloatControl control = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
control.setValue((float) Math.log10(1.0) * 20.0f);
|
||||
}
|
||||
if (force || this.curMute != (this.sfxMute && this.musicMute)) {
|
||||
this.curMute = (this.sfxMute && this.musicMute);
|
||||
|
||||
final BooleanControl control = (BooleanControl) line.getControl(BooleanControl.Type.MUTE);
|
||||
control.setValue(this.curMute);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return this.playing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockingQueue<AudioData> getQueue() {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
public AudioData getMusicQueue() {
|
||||
return this.musicQueue;
|
||||
}
|
||||
|
||||
public void setMusicQueue(final AudioData musicQueue) {
|
||||
this.musicQueue = musicQueue;
|
||||
}
|
||||
|
||||
public boolean isMusicPlaying() {
|
||||
return this.musicPlaying;
|
||||
}
|
||||
|
||||
public boolean isStopMusic() {
|
||||
return this.stopMusic;
|
||||
}
|
||||
|
||||
public void setStopMusic(final boolean stopMusic) {
|
||||
this.stopMusic = stopMusic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
this.exit = true;
|
||||
}
|
||||
|
||||
private static class AudioPlayerStream implements AutoCloseable {
|
||||
|
||||
private InputStream inStream;
|
||||
|
||||
private AudioInputStream inAudio;
|
||||
|
||||
private AudioInputStream convertedAudio;
|
||||
|
||||
private Info info;
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (this.convertedAudio != null) {
|
||||
try {
|
||||
this.convertedAudio.close();
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
LOGGER.error(exc.getMessage(), exc);
|
||||
}
|
||||
this.convertedAudio = null;
|
||||
}
|
||||
if (this.inAudio != null) {
|
||||
try {
|
||||
this.inAudio.close();
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
LOGGER.error(exc.getMessage(), exc);
|
||||
}
|
||||
this.inAudio = null;
|
||||
}
|
||||
if (this.inStream != null) {
|
||||
try {
|
||||
this.inStream.close();
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
LOGGER.error(exc.getMessage(), exc);
|
||||
}
|
||||
this.inStream = null;
|
||||
}
|
||||
this.info = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AudioBuffer {
|
||||
|
||||
byte[] bytes = new byte[2048];
|
||||
|
||||
int size;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.BooleanControl;
|
||||
import javax.sound.sampled.DataLine.Info;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
|
||||
import de.extio.game_engine.audio.AudioOptions.AudioStreamOptions;
|
||||
|
||||
final class AudioPlayerSingleStream implements AudioPlayer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AudioPlayerSingleStream.class);
|
||||
|
||||
private final BlockingQueue<AudioData> queue = new LinkedBlockingQueue<>(5);
|
||||
|
||||
private volatile boolean stopAudio;
|
||||
|
||||
private volatile boolean playing;
|
||||
|
||||
private volatile double volume;
|
||||
|
||||
private volatile boolean mute;
|
||||
|
||||
private volatile boolean exit;
|
||||
|
||||
private double curVolume;
|
||||
|
||||
private boolean curMute;
|
||||
|
||||
private int notStartedAudioCnt;
|
||||
|
||||
public AudioPlayerSingleStream() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("start");
|
||||
|
||||
while (!Thread.currentThread().isInterrupted() && !this.exit) {
|
||||
try {
|
||||
final AudioData data = this.queue.peek();
|
||||
if (data == null) {
|
||||
this.playing = false;
|
||||
Thread.sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
this.queue.remove();
|
||||
|
||||
this.stopAudio = false;
|
||||
try (final InputStream stream = data.inputStream(); final AudioInputStream in = AudioSystem.getAudioInputStream(stream)) {
|
||||
final AudioFormat inFormat = in.getFormat();
|
||||
final int ch = inFormat.getChannels();
|
||||
final float rate = inFormat.getSampleRate();
|
||||
final AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
|
||||
final Info info = new Info(SourceDataLine.class, outFormat);
|
||||
LOGGER.debug("Start {}: in {}; out {}", data.name(), inFormat, outFormat);
|
||||
|
||||
try (final SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
|
||||
final AudioInputStream inConverted = AudioSystem.getAudioInputStream(outFormat, in)) {
|
||||
|
||||
if (line != null) {
|
||||
line.open(outFormat);
|
||||
line.start();
|
||||
|
||||
boolean fading = false;
|
||||
double fadingVol = 0.0;
|
||||
this.notStartedAudioCnt = 0;
|
||||
this.applyOptions(line, true);
|
||||
|
||||
final byte[] buffer = new byte[4096];
|
||||
for (int n = 0; n != -1; n = inConverted.read(buffer, 0, buffer.length)) {
|
||||
line.write(buffer, 0, n);
|
||||
|
||||
if (Thread.currentThread().isInterrupted() || this.exit) {
|
||||
break;
|
||||
}
|
||||
else if (this.stopAudio) {
|
||||
if (!fading) {
|
||||
fading = true;
|
||||
fadingVol = this.volume;
|
||||
}
|
||||
|
||||
fadingVol -= 0.01;
|
||||
if (fadingVol <= 0.0) {
|
||||
break;
|
||||
}
|
||||
|
||||
final FloatControl control = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
control.setValue((float) Math.log10(fadingVol) * 20.0f);
|
||||
}
|
||||
else {
|
||||
this.applyOptions(line, false);
|
||||
}
|
||||
}
|
||||
|
||||
line.drain();
|
||||
line.stop();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
LOGGER.debug("Stop {}", data.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
if (exc instanceof InterruptedException) {
|
||||
LOGGER.info(exc.getClass().getName());
|
||||
break;
|
||||
}
|
||||
LOGGER.error(exc.getMessage(), exc);
|
||||
|
||||
if (++this.notStartedAudioCnt > 5) {
|
||||
this.notStartedAudioCnt = 0;
|
||||
this.exit = true;
|
||||
LOGGER.error("Too many audio playback errors, shutting down audio player.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.playing = false;
|
||||
LOGGER.debug("end");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopAudio() {
|
||||
try {
|
||||
this.queue.clear();
|
||||
this.stopAudio = true;
|
||||
}
|
||||
catch (final Exception exc) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAudioOptions(final AudioStreamOptions options) {
|
||||
this.volume = options.getVolume();
|
||||
this.mute = !options.isEnable();
|
||||
}
|
||||
|
||||
private void applyOptions(final SourceDataLine line, final boolean force) {
|
||||
if (force || this.volume != this.curVolume) {
|
||||
this.curVolume = this.volume;
|
||||
|
||||
if (this.curVolume >= 0.0 && this.curVolume <= 1.0) {
|
||||
final FloatControl control = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
control.setValue((float) Math.log10(this.curVolume) * 20.0f);
|
||||
}
|
||||
}
|
||||
if (force || this.curMute != this.mute) {
|
||||
this.curMute = this.mute;
|
||||
|
||||
final BooleanControl control = (BooleanControl) line.getControl(BooleanControl.Type.MUTE);
|
||||
control.setValue(this.curMute);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return this.playing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockingQueue<AudioData> getQueue() {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
this.exit = true;
|
||||
}
|
||||
|
||||
}
|
||||
23
src/main/java/de/extio/game_engine/audio/AudioStrategy.java
Normal file
23
src/main/java/de/extio/game_engine/audio/AudioStrategy.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
|
||||
interface AudioStrategy {
|
||||
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
void run();
|
||||
|
||||
void queueSfx(Collection<StaticResource> audioFiles);
|
||||
|
||||
void clearSfx();
|
||||
|
||||
void queueMusic(Collection<StaticResource> audioFiles);
|
||||
|
||||
void clearMusic();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
import de.extio.game_engine.util.rng.XorShift128Random;
|
||||
|
||||
final class AudioStrategyJavaMixer implements AudioStrategy {
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(AudioStrategyJavaMixer.class);
|
||||
|
||||
private final static int SFX_PLAYERS = 3;
|
||||
|
||||
private final static int MUSIC_INITIAL_DELAY_MS = 1000;
|
||||
|
||||
private final AudioOptions clientEngineData;
|
||||
|
||||
private final AudioLoader audioLoader;
|
||||
|
||||
private final Random random = new XorShift128Random();
|
||||
|
||||
private final List<AudioThread> sfxAudioPlayers = new ArrayList<>(SFX_PLAYERS);
|
||||
|
||||
private final Set<StaticResource> sfxQueue = new LinkedHashSet<>();
|
||||
|
||||
private final List<AudioPlayerReference> sfxPlayersBySize = new ArrayList<>(SFX_PLAYERS);
|
||||
|
||||
private AudioThread musicAudioPlayer;
|
||||
|
||||
private final List<StaticResource> musicFiles = new ArrayList<>();
|
||||
|
||||
private StaticResource musicPlayOpener = null;
|
||||
|
||||
private final Deque<StaticResource> musicPlayQueue = new ArrayDeque<>();
|
||||
|
||||
private long musicFirstQueued;
|
||||
|
||||
public AudioStrategyJavaMixer(final AudioOptions clientEngineData, final AudioLoader audioLoader) {
|
||||
this.clientEngineData = clientEngineData;
|
||||
this.audioLoader = audioLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
{
|
||||
final var musicAudioPlayer = new AudioPlayerSingleStream();
|
||||
final var musicThread = Thread.ofPlatform()
|
||||
.name("Audio Music")
|
||||
.daemon(true)
|
||||
.start(musicAudioPlayer);
|
||||
musicThread.start();
|
||||
this.musicAudioPlayer = new AudioThread(musicThread, musicAudioPlayer);
|
||||
}
|
||||
|
||||
for (int i = 0; i < SFX_PLAYERS; i++) {
|
||||
final AudioPlayer audioPlayer = new AudioPlayerSingleStream();
|
||||
final var audioThread = Thread.ofPlatform()
|
||||
.name("Audio SFX " + i)
|
||||
.daemon(true)
|
||||
.start(audioPlayer);
|
||||
|
||||
this.sfxAudioPlayers.add(new AudioThread(audioThread, audioPlayer));
|
||||
this.sfxPlayersBySize.add(new AudioPlayerReference(audioPlayer));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.musicAudioPlayer.audioPlayer().exit();
|
||||
this.musicAudioPlayer.thread().interrupt();
|
||||
|
||||
for (int i = 0; i < SFX_PLAYERS; i++) {
|
||||
this.sfxAudioPlayers.get(i).audioPlayer.exit();
|
||||
this.sfxAudioPlayers.get(i).thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Music
|
||||
|
||||
if (!this.clientEngineData.getMusicOptions().isEnable()) {
|
||||
if (this.musicAudioPlayer.audioPlayer().isPlaying()) {
|
||||
this.musicAudioPlayer.audioPlayer().stopAudio();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.musicPlayQueue.isEmpty() && (this.musicPlayOpener != null || !this.musicFiles.isEmpty())) {
|
||||
if (this.musicPlayOpener != null) {
|
||||
this.musicPlayQueue.add(this.musicPlayOpener);
|
||||
this.musicPlayOpener = null;
|
||||
}
|
||||
|
||||
final List<StaticResource> music = new ArrayList<>(this.musicFiles);
|
||||
Collections.shuffle(music, this.random);
|
||||
this.musicPlayQueue.addAll(music);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Music play queue: " + this.musicPlayQueue);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.musicAudioPlayer.audioPlayer().isPlaying() || !this.musicPlayQueue.isEmpty()) {
|
||||
this.musicAudioPlayer.audioPlayer().updateAudioOptions(this.clientEngineData.getMusicOptions());
|
||||
}
|
||||
|
||||
if (!this.musicAudioPlayer.audioPlayer().isPlaying() &&
|
||||
!this.musicPlayQueue.isEmpty() &&
|
||||
System.currentTimeMillis() - this.musicFirstQueued >= MUSIC_INITIAL_DELAY_MS) {
|
||||
final StaticResource nextFile = this.musicPlayQueue.pop();
|
||||
|
||||
this.audioLoader.getQueue().offer(new AudioLoaderRequest(nextFile, false, stream -> this.musicAudioPlayer.audioPlayer().getQueue().offer(new AudioData(nextFile.resourceName(), stream))));
|
||||
}
|
||||
}
|
||||
|
||||
// SFX
|
||||
|
||||
if (!this.sfxQueue.isEmpty()) {
|
||||
for (int i = 0; i < SFX_PLAYERS; i++) {
|
||||
this.sfxAudioPlayers.get(i).audioPlayer().updateAudioOptions(this.clientEngineData.getSfxOptions());
|
||||
this.sfxPlayersBySize.get(i).queueSize = this.sfxPlayersBySize.get(i).audioPlayer.getQueue().size();
|
||||
}
|
||||
Collections.sort(this.sfxPlayersBySize);
|
||||
|
||||
while (!this.sfxQueue.isEmpty()) {
|
||||
int i = 0;
|
||||
final Iterator<StaticResource> it = this.sfxQueue.iterator();
|
||||
do {
|
||||
this.sfxPlayersBySize.get(i).queueSize++;
|
||||
|
||||
final AudioPlayer player = this.sfxPlayersBySize.get(i).audioPlayer;
|
||||
final StaticResource nextFile = it.next();
|
||||
it.remove();
|
||||
|
||||
this.audioLoader.getQueue().offer(new AudioLoaderRequest(nextFile, true, stream -> player.getQueue().offer(new AudioData(nextFile.resourceName(), stream))));
|
||||
} while (it.hasNext() && ++i < this.sfxPlayersBySize.size() && this.sfxPlayersBySize.get(i - 1).queueSize > this.sfxPlayersBySize.get(i).queueSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueSfx(final Collection<StaticResource> audioFile) {
|
||||
if (!this.clientEngineData.getSfxOptions().isEnable()) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Not queued audio file (audio disabled): " + audioFile);
|
||||
}
|
||||
}
|
||||
else if (this.sfxQueue.addAll(audioFile)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Queued audio file: " + audioFile);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Not queued audio file (already queued): " + audioFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSfx() {
|
||||
this.sfxQueue.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueMusic(final Collection<StaticResource> audioFiles) {
|
||||
StaticResource opener = null;
|
||||
final List<StaticResource> musicAudioFiles = new ArrayList<>(audioFiles.size() - 1);
|
||||
boolean first = true;
|
||||
for (final StaticResource audioFile : audioFiles) {
|
||||
if (first) {
|
||||
opener = audioFile;
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
musicAudioFiles.add(audioFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (musicAudioFiles.equals(this.musicFiles)) {
|
||||
LOGGER.debug("The selected tracks are already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.musicAudioPlayer.audioPlayer().isPlaying()) {
|
||||
this.musicFirstQueued = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
this.clearMusic();
|
||||
this.musicPlayOpener = opener;
|
||||
this.musicFiles.addAll(musicAudioFiles);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Queued music: " + this.musicFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearMusic() {
|
||||
this.musicFiles.clear();
|
||||
this.musicPlayQueue.clear();
|
||||
this.musicPlayOpener = null;
|
||||
|
||||
this.musicAudioPlayer.audioPlayer().stopAudio();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Stopped and cleared music player");
|
||||
}
|
||||
}
|
||||
|
||||
static class AudioPlayerReference implements Comparable<AudioPlayerReference> {
|
||||
|
||||
private int queueSize;
|
||||
|
||||
private final AudioPlayer audioPlayer;
|
||||
|
||||
AudioPlayerReference(final AudioPlayer audioPlayer) {
|
||||
this.audioPlayer = audioPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(final AudioPlayerReference o) {
|
||||
return Integer.compare(this.queueSize, o.queueSize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static record AudioThread(Thread thread, AudioPlayer audioPlayer) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package de.extio.game_engine.audio;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
import de.extio.game_engine.util.rng.XorShift128Random;
|
||||
|
||||
final class AudioStrategySoftwareMixing implements AudioStrategy {
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(AudioStrategySoftwareMixing.class);
|
||||
|
||||
private final static int MUSIC_INITIAL_DELAY_MS = 1000;
|
||||
|
||||
private final AudioOptions clientEngineData;
|
||||
|
||||
private final AudioLoader audioLoader;
|
||||
|
||||
private final Random random = new XorShift128Random();
|
||||
|
||||
private AudioThread audioPlayer;
|
||||
|
||||
private final Set<StaticResource> sfxQueue = new LinkedHashSet<>();
|
||||
|
||||
private final List<StaticResource> musicFiles = new ArrayList<>();
|
||||
|
||||
private final Deque<StaticResource> musicPlayQueue = new ArrayDeque<>();
|
||||
|
||||
private long musicFirstQueued;
|
||||
|
||||
public AudioStrategySoftwareMixing(final AudioOptions clientEngineData, final AudioLoader audioLoader) {
|
||||
this.clientEngineData = clientEngineData;
|
||||
this.audioLoader = audioLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
final var multiStreamPlayer = new AudioPlayerMultiStream();
|
||||
final var multiStreamThread = Thread.ofPlatform()
|
||||
.name("Audio MultiStream")
|
||||
.daemon(true)
|
||||
.start(multiStreamPlayer);
|
||||
this.audioPlayer = new AudioThread(multiStreamThread, multiStreamPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.audioPlayer.audioPlayer().exit();
|
||||
this.audioPlayer.thread().interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.audioPlayer.audioPlayer().isPlaying() || !this.sfxQueue.isEmpty() || !this.musicPlayQueue.isEmpty() || !this.musicFiles.isEmpty()) {
|
||||
this.audioPlayer.audioPlayer().updateAudioOptions(this.clientEngineData.getSfxOptions());
|
||||
this.audioPlayer.audioPlayer().updateMusicAudioOptions(this.clientEngineData.getMusicOptions());
|
||||
}
|
||||
|
||||
// Music
|
||||
|
||||
if (!this.clientEngineData.getMusicOptions().isEnable()) {
|
||||
if (this.audioPlayer.audioPlayer().isMusicPlaying()) {
|
||||
this.audioPlayer.audioPlayer().setStopMusic(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.musicPlayQueue.isEmpty() && !this.musicFiles.isEmpty()) {
|
||||
final List<StaticResource> music = new ArrayList<>(this.musicFiles);
|
||||
Collections.shuffle(music, this.random);
|
||||
this.musicPlayQueue.addAll(music);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Music play queue: " + this.musicPlayQueue);
|
||||
}
|
||||
}
|
||||
if (!this.musicPlayQueue.isEmpty() && this.audioPlayer.audioPlayer().getMusicQueue() == null && !this.audioPlayer.audioPlayer().isMusicPlaying() && (System.currentTimeMillis() - this.musicFirstQueued) >= MUSIC_INITIAL_DELAY_MS) {
|
||||
final StaticResource nextFile = this.musicPlayQueue.pop();
|
||||
this.musicFirstQueued = System.currentTimeMillis();
|
||||
this.audioLoader.getQueue().offer(new AudioLoaderRequest(nextFile, false, stream -> this.audioPlayer.audioPlayer().setMusicQueue(new AudioData(nextFile.resourceName(), stream))));
|
||||
}
|
||||
}
|
||||
|
||||
// Sfx
|
||||
|
||||
if (!this.sfxQueue.isEmpty()) {
|
||||
final Iterator<StaticResource> it = this.sfxQueue.iterator();
|
||||
while (it.hasNext()) {
|
||||
final StaticResource nextFile = it.next();
|
||||
this.audioLoader.getQueue().offer(new AudioLoaderRequest(nextFile, true, stream -> this.audioPlayer.audioPlayer().getQueue().offer(new AudioData(nextFile.resourceName(), stream))));
|
||||
}
|
||||
this.sfxQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSfx() {
|
||||
this.sfxQueue.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueSfx(final Collection<StaticResource> audioFiles) {
|
||||
if (!this.clientEngineData.getSfxOptions().isEnable()) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Not queued audio file (audio disabled): " + audioFiles);
|
||||
}
|
||||
}
|
||||
else if (this.sfxQueue.addAll(audioFiles)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Queued audio file: " + audioFiles);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Not queued audio file (already queued): " + audioFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueMusic(final Collection<StaticResource> audioFiles) {
|
||||
Optional<StaticResource> opener = null;
|
||||
final List<StaticResource> musicAudioFiles = new ArrayList<>(Math.min(1, audioFiles.size() - 1));
|
||||
for (final StaticResource audioFile : audioFiles) {
|
||||
if (opener == null) {
|
||||
opener = Optional.ofNullable(audioFile);
|
||||
}
|
||||
else {
|
||||
musicAudioFiles.add(audioFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (musicAudioFiles.equals(this.musicFiles)) {
|
||||
this.musicFirstQueued = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
this.clearMusic();
|
||||
if (opener != null && opener.isPresent()) {
|
||||
this.musicPlayQueue.offer(opener.get());
|
||||
}
|
||||
this.musicFiles.addAll(musicAudioFiles);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Queued music: Opener: " + (opener != null ? opener.orElse(null) : null) + " Files: " + this.musicFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearMusic() {
|
||||
this.musicFiles.clear();
|
||||
this.musicPlayQueue.clear();
|
||||
|
||||
this.audioPlayer.audioPlayer().setStopMusic(true);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Stopped and cleared music");
|
||||
}
|
||||
}
|
||||
|
||||
private static record AudioThread(Thread thread, AudioPlayerMultiStream audioPlayer) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,4 +4,13 @@ import java.util.List;
|
||||
|
||||
public record StaticResource(List<String> pathSegments, String resourceName) {
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
if (this.pathSegments == null || this.pathSegments.isEmpty()) {
|
||||
return this.resourceName;
|
||||
} else {
|
||||
return String.join("/", this.pathSegments) + "/" + this.resourceName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ de.extio.game_engine.storage.StorageServiceAutoConfiguration
|
||||
de.extio.game_engine.module.ModuleServiceAutoConfiguration
|
||||
de.extio.game_engine.i18n.LocalizationAutoConfiguration
|
||||
de.extio.game_engine.resource.StaticResourceAutoConfiguration
|
||||
de.extio.game_engine.audio.AudioAutoConfiguration
|
||||
|
||||
Reference in New Issue
Block a user