Create basic Audio API
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package com.bartlomiejpluta.base.engine.audio.asset;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.common.asset.Asset;
|
||||
import lombok.NonNull;
|
||||
|
||||
public class SoundAsset extends Asset {
|
||||
public SoundAsset(@NonNull String uid, @NonNull String source) {
|
||||
super(uid, source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.bartlomiejpluta.base.engine.audio.manager;
|
||||
|
||||
import com.bartlomiejpluta.base.api.audio.Sound;
|
||||
import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset;
|
||||
import com.bartlomiejpluta.base.engine.core.al.engine.AudioEngine;
|
||||
import com.bartlomiejpluta.base.engine.core.al.source.AudioSource;
|
||||
import com.bartlomiejpluta.base.engine.error.AppException;
|
||||
import com.bartlomiejpluta.base.engine.project.config.ProjectConfiguration;
|
||||
import com.bartlomiejpluta.base.engine.util.res.ResourcesManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
|
||||
public class DefaultSoundManager implements SoundManager {
|
||||
private final Map<String, SoundAsset> assets = new HashMap<>();
|
||||
private final Set<String> loadedBuffers = new HashSet<>();
|
||||
private final AudioEngine engine;
|
||||
private final ResourcesManager resourcesManager;
|
||||
private final ProjectConfiguration configuration;
|
||||
|
||||
@Override
|
||||
public void registerAsset(SoundAsset asset) {
|
||||
log.info("Registering [{}] sound asset under UID: [{}]", asset.getSource(), asset.getUid());
|
||||
assets.put(asset.getUid(), asset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sound loadObject(String uid) {
|
||||
if (!loadedBuffers.contains(uid)) {
|
||||
log.info("Loading [{}] sound", uid);
|
||||
var asset = assets.get(uid);
|
||||
|
||||
if (asset == null) {
|
||||
throw new AppException("The sound asset with UID: [%s] does not exist", uid);
|
||||
}
|
||||
|
||||
var source = configuration.projectFile("audio", asset.getSource());
|
||||
var buffer = resourcesManager.loadResourceAsByteBuffer(source);
|
||||
engine.loadVorbis(asset.getUid(), buffer);
|
||||
loadedBuffers.add(uid);
|
||||
}
|
||||
|
||||
return engine.createSource(uid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disposeSound(Sound sound) {
|
||||
engine.disposeSource((AudioSource) sound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bartlomiejpluta.base.engine.audio.manager;
|
||||
|
||||
import com.bartlomiejpluta.base.api.audio.Sound;
|
||||
import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset;
|
||||
import com.bartlomiejpluta.base.engine.common.manager.AssetManager;
|
||||
|
||||
public interface SoundManager extends AssetManager<SoundAsset, Sound> {
|
||||
void disposeSound(Sound sound);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.base.engine.context.manager;
|
||||
|
||||
import com.bartlomiejpluta.base.api.context.Context;
|
||||
import com.bartlomiejpluta.base.api.runner.GameRunner;
|
||||
import com.bartlomiejpluta.base.engine.audio.manager.SoundManager;
|
||||
import com.bartlomiejpluta.base.engine.context.model.DefaultContext;
|
||||
import com.bartlomiejpluta.base.engine.core.engine.GameEngine;
|
||||
import com.bartlomiejpluta.base.engine.gui.manager.FontManager;
|
||||
@@ -39,6 +40,7 @@ public class DefaultContextManager implements ContextManager {
|
||||
private final ClassLoader classLoader;
|
||||
private final Inflater inflater;
|
||||
private final WidgetDefinitionManager widgetDefinitionManager;
|
||||
private final SoundManager soundManager;
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
@@ -55,6 +57,7 @@ public class DefaultContextManager implements ContextManager {
|
||||
project.getAnimationAssets().forEach(animationManager::registerAsset);
|
||||
project.getFontAssets().forEach(fontManager::registerAsset);
|
||||
project.getWidgetDefinitionAssets().forEach(widgetDefinitionManager::registerAsset);
|
||||
project.getSoundAssets().forEach(soundManager::registerAsset);
|
||||
|
||||
log.info("Creating game runner instance");
|
||||
var runnerClass = classLoader.<GameRunner>loadClass(project.getRunner());
|
||||
@@ -70,6 +73,7 @@ public class DefaultContextManager implements ContextManager {
|
||||
.fontManager(fontManager)
|
||||
.inflater(inflater)
|
||||
.widgetDefinitionManager(widgetDefinitionManager)
|
||||
.soundManager(soundManager)
|
||||
.gameRunner(gameRunner)
|
||||
.projectName(project.getName())
|
||||
.build();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bartlomiejpluta.base.engine.context.model;
|
||||
|
||||
import com.bartlomiejpluta.base.api.animation.Animation;
|
||||
import com.bartlomiejpluta.base.api.audio.Sound;
|
||||
import com.bartlomiejpluta.base.api.camera.Camera;
|
||||
import com.bartlomiejpluta.base.api.context.Context;
|
||||
import com.bartlomiejpluta.base.api.entity.Entity;
|
||||
@@ -10,6 +11,7 @@ import com.bartlomiejpluta.base.api.input.Input;
|
||||
import com.bartlomiejpluta.base.api.map.handler.MapHandler;
|
||||
import com.bartlomiejpluta.base.api.runner.GameRunner;
|
||||
import com.bartlomiejpluta.base.api.screen.Screen;
|
||||
import com.bartlomiejpluta.base.engine.audio.manager.SoundManager;
|
||||
import com.bartlomiejpluta.base.engine.core.engine.GameEngine;
|
||||
import com.bartlomiejpluta.base.engine.gui.manager.FontManager;
|
||||
import com.bartlomiejpluta.base.engine.gui.manager.WidgetDefinitionManager;
|
||||
@@ -58,6 +60,9 @@ public class DefaultContext implements Context {
|
||||
@NonNull
|
||||
private final WidgetDefinitionManager widgetDefinitionManager;
|
||||
|
||||
@NonNull
|
||||
private final SoundManager soundManager;
|
||||
|
||||
@Getter
|
||||
@NonNull
|
||||
private final GameRunner gameRunner;
|
||||
@@ -80,6 +85,8 @@ public class DefaultContext implements Context {
|
||||
|
||||
private final List<GUI> guis = new LinkedList<>();
|
||||
|
||||
private final List<Sound> sounds = new LinkedList<>();
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void init(@NonNull Screen screen, @NonNull Input input, @NonNull Camera camera) {
|
||||
@@ -133,6 +140,31 @@ public class DefaultContext implements Context {
|
||||
return gui;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sound createSound(String soundUid) {
|
||||
return soundManager.loadObject(soundUid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disposeSound(Sound sound) {
|
||||
soundManager.disposeSound(sound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playSound(String soundUid) {
|
||||
var sound = soundManager.loadObject(soundUid);
|
||||
sounds.add(sound);
|
||||
sound.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playSound(String soundUid, float gain) {
|
||||
var sound = soundManager.loadObject(soundUid);
|
||||
sound.setGain(gain);
|
||||
sounds.add(sound);
|
||||
sound.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return engine.isRunning();
|
||||
@@ -183,6 +215,14 @@ public class DefaultContext implements Context {
|
||||
if (map != null) {
|
||||
map.update(dt);
|
||||
}
|
||||
|
||||
for (var iterator = sounds.iterator(); iterator.hasNext(); ) {
|
||||
var sound = iterator.next();
|
||||
if (!sound.isPlaying()) {
|
||||
iterator.remove();
|
||||
soundManager.disposeSound(sound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.bartlomiejpluta.base.engine.core.al.buffer;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.error.AppException;
|
||||
import com.bartlomiejpluta.base.internal.gc.Disposable;
|
||||
import lombok.Getter;
|
||||
import org.lwjgl.stb.STBVorbisInfo;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import static org.lwjgl.openal.AL10.*;
|
||||
import static org.lwjgl.stb.STBVorbis.*;
|
||||
import static org.lwjgl.system.MemoryUtil.NULL;
|
||||
|
||||
|
||||
public class AudioBuffer implements Disposable {
|
||||
private final ShortBuffer pcm;
|
||||
|
||||
@Getter
|
||||
private final int id;
|
||||
|
||||
public AudioBuffer(ByteBuffer buffer) {
|
||||
id = alGenBuffers();
|
||||
try (var info = STBVorbisInfo.malloc()) {
|
||||
pcm = readVorbis(buffer, info);
|
||||
alBufferData(id, info.channels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, pcm, info.sample_rate());
|
||||
}
|
||||
}
|
||||
|
||||
private ShortBuffer readVorbis(ByteBuffer vorbis, STBVorbisInfo info) {
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
var error = stack.mallocInt(1);
|
||||
var decoder = stb_vorbis_open_memory(vorbis, error, null);
|
||||
|
||||
if (decoder == NULL) {
|
||||
throw new AppException("Failed to open OGG Vorbis file: " + error.get(0));
|
||||
}
|
||||
|
||||
stb_vorbis_get_info(decoder, info);
|
||||
|
||||
int channels = info.channels();
|
||||
|
||||
int lengthSamples = stb_vorbis_stream_length_in_samples(decoder);
|
||||
|
||||
var pcm = MemoryUtil.memAllocShort(lengthSamples);
|
||||
|
||||
pcm.limit(stb_vorbis_get_samples_short_interleaved(decoder, channels, pcm) * channels);
|
||||
stb_vorbis_close(decoder);
|
||||
|
||||
return pcm;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
alDeleteBuffers(id);
|
||||
if (pcm != null) {
|
||||
MemoryUtil.memFree(pcm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bartlomiejpluta.base.engine.core.al.engine;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.common.init.Initianizable;
|
||||
import com.bartlomiejpluta.base.engine.core.al.listener.AudioListener;
|
||||
import com.bartlomiejpluta.base.engine.core.al.source.AudioSource;
|
||||
import com.bartlomiejpluta.base.internal.gc.Cleanable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public interface AudioEngine extends Initianizable, Cleanable {
|
||||
AudioListener getListener();
|
||||
|
||||
void loadVorbis(String name, ByteBuffer vorbis);
|
||||
|
||||
AudioSource createSource(String name);
|
||||
|
||||
void disposeSource(AudioSource source);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.bartlomiejpluta.base.engine.core.al.engine;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.core.al.buffer.AudioBuffer;
|
||||
import com.bartlomiejpluta.base.engine.core.al.listener.AudioListener;
|
||||
import com.bartlomiejpluta.base.engine.core.al.source.AudioSource;
|
||||
import com.bartlomiejpluta.base.engine.error.AppException;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.lwjgl.openal.AL;
|
||||
import org.lwjgl.openal.ALC;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.lwjgl.openal.ALC10.*;
|
||||
import static org.lwjgl.openal.ALC11.alcOpenDevice;
|
||||
import static org.lwjgl.system.MemoryUtil.NULL;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DefaultAudioEngine implements AudioEngine {
|
||||
private final Map<String, AudioBuffer> buffers = new HashMap<>();
|
||||
private final List<AudioSource> sources = new LinkedList<>();
|
||||
|
||||
private long device;
|
||||
private long context;
|
||||
|
||||
@Getter
|
||||
private AudioListener listener;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
log.info("Initializing default audio device");
|
||||
device = alcOpenDevice((ByteBuffer) null);
|
||||
|
||||
if (device == NULL) {
|
||||
throw new AppException("Failed to open the default OpenAL device");
|
||||
}
|
||||
|
||||
log.info("Initializing audio context");
|
||||
var deviceCapabilities = ALC.createCapabilities(device);
|
||||
context = alcCreateContext(device, (IntBuffer) null);
|
||||
|
||||
if (context == NULL) {
|
||||
throw new AppException("Failed to create OpenAL context");
|
||||
}
|
||||
|
||||
alcMakeContextCurrent(context);
|
||||
AL.createCapabilities(deviceCapabilities);
|
||||
|
||||
log.info("Initializing audio listener");
|
||||
listener = new AudioListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadVorbis(String name, ByteBuffer vorbis) {
|
||||
var buffer = new AudioBuffer(vorbis);
|
||||
buffers.put(name, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioSource createSource(String name) {
|
||||
var buffer = buffers.get(name);
|
||||
|
||||
if (buffer == null) {
|
||||
throw new AppException("Audio buffer with name [%s] does not exist", name);
|
||||
}
|
||||
|
||||
var source = new AudioSource();
|
||||
source.setBuffer(buffer);
|
||||
|
||||
sources.add(source);
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disposeSource(AudioSource source) {
|
||||
source.dispose();
|
||||
sources.remove(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanUp() {
|
||||
log.info("Disposing audio sources");
|
||||
sources.forEach(AudioSource::dispose);
|
||||
log.info("{} audio sources have been disposed", sources.size());
|
||||
|
||||
log.info("Disposing audio buffers");
|
||||
buffers.forEach((name, buffer) -> buffer.dispose());
|
||||
log.info("{} audio buffers have been disposed", buffers.size());
|
||||
|
||||
if (context != NULL) {
|
||||
log.info("Disposing audio context");
|
||||
alcDestroyContext(context);
|
||||
} else {
|
||||
log.warn("Audio context is NULL and will not be disposed");
|
||||
}
|
||||
|
||||
if (device != NULL) {
|
||||
log.info("Closing audio device");
|
||||
alcCloseDevice(device);
|
||||
} else {
|
||||
log.warn("Audio device is NULL and will not be closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.bartlomiejpluta.base.engine.core.al.listener;
|
||||
|
||||
import com.bartlomiejpluta.base.api.audio.Listener;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
import static org.lwjgl.openal.AL10.*;
|
||||
import static org.lwjgl.openal.AL11.alListener3f;
|
||||
|
||||
public class AudioListener implements Listener {
|
||||
|
||||
public AudioListener() {
|
||||
this(new Vector3f(0, 0, 0));
|
||||
}
|
||||
|
||||
public AudioListener(Vector3fc position) {
|
||||
alListener3f(AL_POSITION, position.x(), position.y(), position.z());
|
||||
alListener3f(AL_VELOCITY, 0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(Vector3fc position) {
|
||||
alListener3f(AL_POSITION, position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpeed(Vector3fc speed) {
|
||||
alListener3f(AL_VELOCITY, speed.x(), speed.y(), speed.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrientation(Vector3fc at, Vector3fc up) {
|
||||
var data = new float[6];
|
||||
data[0] = at.x();
|
||||
data[1] = at.y();
|
||||
data[2] = at.z();
|
||||
data[3] = up.x();
|
||||
data[4] = up.y();
|
||||
data[5] = up.z();
|
||||
alListenerfv(AL_ORIENTATION, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.bartlomiejpluta.base.engine.core.al.source;
|
||||
|
||||
import com.bartlomiejpluta.base.api.audio.Sound;
|
||||
import com.bartlomiejpluta.base.engine.core.al.buffer.AudioBuffer;
|
||||
import com.bartlomiejpluta.base.internal.gc.Disposable;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
import static org.lwjgl.openal.AL10.*;
|
||||
import static org.lwjgl.openal.AL11.alGenSources;
|
||||
|
||||
public class AudioSource implements Sound, Disposable {
|
||||
private final int id = alGenSources();
|
||||
|
||||
public void setBuffer(AudioBuffer buffer) {
|
||||
stop();
|
||||
alSourcei(id, AL_BUFFER, buffer.getId());
|
||||
}
|
||||
|
||||
public void setParameter(int param, float value) {
|
||||
alSourcef(id, param, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(Vector3fc position) {
|
||||
alSource3f(id, AL_POSITION, position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpeed(Vector3fc speed) {
|
||||
alSource3f(id, AL_VELOCITY, speed.x(), speed.y(), speed.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(float gain) {
|
||||
alSourcef(id, AL_GAIN, gain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
alSourcePlay(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
alSourcePause(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
alSourceStop(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return alGetSourcei(id, AL_SOURCE_STATE) == AL_PLAYING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeat(boolean repeat) {
|
||||
alSourcei(id, AL_LOOPING, repeat ? AL_TRUE : AL_FALSE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRelative(boolean relative) {
|
||||
alSourcei(id, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stop();
|
||||
alDeleteSources(id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bartlomiejpluta.base.engine.project.model;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset;
|
||||
import com.bartlomiejpluta.base.engine.gui.asset.FontAsset;
|
||||
import com.bartlomiejpluta.base.engine.gui.asset.WidgetDefinitionAsset;
|
||||
import com.bartlomiejpluta.base.engine.world.animation.asset.AnimationAsset;
|
||||
@@ -43,4 +44,7 @@ public class Project {
|
||||
|
||||
@NonNull
|
||||
private final List<WidgetDefinitionAsset> widgetDefinitionAssets;
|
||||
|
||||
@NonNull
|
||||
private final List<SoundAsset> soundAssets;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bartlomiejpluta.base.engine.project.serial;
|
||||
|
||||
import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset;
|
||||
import com.bartlomiejpluta.base.engine.gui.asset.FontAsset;
|
||||
import com.bartlomiejpluta.base.engine.gui.asset.WidgetDefinitionAsset;
|
||||
import com.bartlomiejpluta.base.engine.project.model.Project;
|
||||
@@ -32,6 +33,7 @@ public class ProtobufProjectDeserializer extends ProjectDeserializer {
|
||||
.animationAssets(proto.getAnimationsList().stream().map(this::parseAnimationAsset).collect(toList()))
|
||||
.fontAssets(proto.getFontsList().stream().map(this::parseFontAsset).collect(toList()))
|
||||
.widgetDefinitionAssets(proto.getWidgetsList().stream().map(this::parseWidgetAsset).collect(toList()))
|
||||
.soundAssets(proto.getSoundsList().stream().map(this::parseSoundAsset).collect(toList()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -62,4 +64,8 @@ public class ProtobufProjectDeserializer extends ProjectDeserializer {
|
||||
private AnimationAsset parseAnimationAsset(ProjectProto.AnimationAsset proto) {
|
||||
return new AnimationAsset(proto.getUid(), proto.getSource(), proto.getRows(), proto.getColumns());
|
||||
}
|
||||
|
||||
private SoundAsset parseSoundAsset(ProjectProto.SoundAsset proto) {
|
||||
return new SoundAsset(proto.getUid(), proto.getSource());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user