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) {
|
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.module.ModuleServiceAutoConfiguration
|
||||||
de.extio.game_engine.i18n.LocalizationAutoConfiguration
|
de.extio.game_engine.i18n.LocalizationAutoConfiguration
|
||||||
de.extio.game_engine.resource.StaticResourceAutoConfiguration
|
de.extio.game_engine.resource.StaticResourceAutoConfiguration
|
||||||
|
de.extio.game_engine.audio.AudioAutoConfiguration
|
||||||
|
|||||||
Reference in New Issue
Block a user