Create basic Audio API

This commit is contained in:
2021-03-23 22:52:57 +01:00
parent e3d7ce2b73
commit b8f316f9d5
15 changed files with 484 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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());
}
}