Added a crude music player and fixed a lot of OpenAL stuff

- Game will now play all Vorbis files in music/ directory (at runtime)
  - Random order with random pauses (15 to 60 sec)
  - Force start by pressing M
- Added AudioRegistry for performance
- Speakers now can change data correctly
- Added ResourceReaders
  - Classpath and Filesystem for now
- Added local controls. Use with ControlTriggers.localOf(...)

NB: No actual music files included (yet). Place them in music/ directory
of the game directory (e.g. in <project>/run/music).
This commit is contained in:
OLEGSHA 2021-03-26 23:51:51 +03:00
parent 9d7f69892b
commit c49fdfa5ff
Signed by: OLEGSHA
GPG Key ID: E57A4B08D64AFF7A
18 changed files with 527 additions and 61 deletions

View File

@ -32,6 +32,7 @@ import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.server.ServerState;
import ru.windcorp.progressia.test.TestContent;
import ru.windcorp.progressia.test.TestMusicPlayer;
public class ClientProxy implements Proxy {
@ -59,6 +60,8 @@ public class ClientProxy implements Proxy {
ServerState.startServer();
ClientState.connectToLocalServer();
TestMusicPlayer.start();
}
}

View File

@ -23,6 +23,7 @@ import ru.windcorp.progressia.client.audio.backend.AudioReader;
import ru.windcorp.progressia.client.audio.backend.Listener;
import ru.windcorp.progressia.client.audio.backend.SoundType;
import ru.windcorp.progressia.client.audio.backend.Speaker;
import ru.windcorp.progressia.common.resource.Resource;
import static org.lwjgl.openal.AL11.*;
import static org.lwjgl.openal.ALC10.*;
@ -40,7 +41,6 @@ public class AudioManager {
private static List<Speaker> soundSpeakers = new ArrayList<>(SOUNDS_NUM);
private static Speaker musicSpeaker;
private static ArrayList<SoundType> soundsBuffer = new ArrayList<>();
public static void initAL() {
String defaultDeviceName = alcGetString(
@ -82,18 +82,6 @@ public class AudioManager {
return speaker;
}
private static SoundType findSoundType(String soundID) throws Exception {
for (SoundType s : soundsBuffer) {
if (s.getId().equals(soundID)) {
return s;
}
}
throw new Exception(
"ERROR: The selected sound is not loaded or" +
" not exists"
);
}
public static Speaker initSpeaker(SoundType st) {
Speaker speaker = getLastSpeaker();
try {
@ -103,21 +91,10 @@ public class AudioManager {
}
return speaker;
}
public static SoundType getSoundType(String id) {
SoundType st;
try {
st = findSoundType(id);
} catch (Exception ex) {
throw new RuntimeException();
}
return st;
}
public static Speaker initMusicSpeaker(String soundID) {
public static Speaker initMusicSpeaker(SoundType st) {
try {
findSoundType(soundID).initSpeaker(musicSpeaker);
st.initSpeaker(musicSpeaker);
} catch (Exception ex) {
throw new RuntimeException();
}
@ -131,11 +108,11 @@ public class AudioManager {
}
}
public static void loadSound(String path, String id, AudioFormat format) {
public static void loadSound(Resource resource, String id, AudioFormat format) {
if (format == AudioFormat.MONO) {
soundsBuffer.add(AudioReader.readAsMono(path, id));
AudioRegistry.getInstance().register(AudioReader.readAsMono(resource, id));
} else {
soundsBuffer.add(AudioReader.readAsStereo(path, id));
AudioRegistry.getInstance().register(AudioReader.readAsStereo(resource, id));
}
}

View File

@ -0,0 +1,34 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.audio;
import ru.windcorp.progressia.client.audio.backend.SoundType;
import ru.windcorp.progressia.common.util.namespaces.NamespacedInstanceRegistry;
public class AudioRegistry extends NamespacedInstanceRegistry<SoundType> {
private static final AudioRegistry INSTANCE = new AudioRegistry();
/**
* @return the instance
*/
public static AudioRegistry getInstance() {
return INSTANCE;
}
}

View File

@ -18,6 +18,8 @@
package ru.windcorp.progressia.client.audio;
import ru.windcorp.progressia.common.resource.ResourceManager;
public class AudioSystem {
static public void initialize() {
AudioManager.initAL();
@ -28,7 +30,7 @@ public class AudioSystem {
static void loadAudioData() {
AudioManager.loadSound(
"assets/sounds/block_destroy_clap.ogg",
ResourceManager.getResource("assets/sounds/block_destroy_clap.ogg"),
"Progressia:BlockDestroy",
AudioFormat.MONO
);

View File

@ -19,24 +19,33 @@
package ru.windcorp.progressia.client.audio;
import glm.vec._3.Vec3;
import ru.windcorp.progressia.client.audio.backend.SoundType;
import ru.windcorp.progressia.client.audio.backend.Speaker;
public class Music
extends Sound {
public Music(SoundType soundType, int timeLength, float pitch, float gain) {
super(soundType, timeLength, new Vec3(), new Vec3(), pitch, gain);
}
public Music(SoundType soundType) {
super(soundType);
}
public Music(String id, int timeLength, float pitch, float gain) {
super(id, timeLength, new Vec3(), new Vec3(), pitch, gain);
}
public Music(String id) {
super(id);
}
public Music(
String id,
Vec3 velocity,
float pitch,
float gain
) {
this(id);
super.velocity = velocity;
super.pitch = pitch;
super.gain = gain;
@Override
protected Speaker initSpeaker() {
return AudioManager.initMusicSpeaker(soundType);
}
@Override

View File

@ -31,9 +31,13 @@ public class Sound {
protected int timeLength = 0;
protected SoundType soundType;
public Sound(SoundType soundType) {
this.soundType = soundType;
}
public Sound(String id) {
soundType = AudioManager.getSoundType(id);
this(AudioRegistry.getInstance().get(id));
}
public Sound(
@ -50,9 +54,28 @@ public class Sound {
this.pitch = pitch;
this.gain = gain;
}
public Sound(
SoundType soundType,
int timeLength,
Vec3 position,
Vec3 velocity,
float pitch,
float gain
) {
this(soundType);
this.position = position;
this.velocity = velocity;
this.pitch = pitch;
this.gain = gain;
}
protected Speaker initSpeaker() {
return AudioManager.initSpeaker(soundType);
}
public void play(boolean loop) {
Speaker speaker = AudioManager.initSpeaker(soundType);
Speaker speaker = initSpeaker();
speaker.setGain(gain);
speaker.setPitch(pitch);
speaker.setPosition(position);
@ -97,8 +120,8 @@ public class Sound {
return pitch;
}
public int getTimeLength() {
return soundType.getTimeLength();
public double getDuration() {
return soundType.getDuration();
}
}

View File

@ -33,13 +33,11 @@ public class AudioReader {
}
// TODO fix converting from mono-stereo
private static SoundType readAsSpecified(String path, String id, int format) {
private static SoundType readAsSpecified(Resource resource, String id, int format) {
IntBuffer channelBuffer = BufferUtils.createIntBuffer(1);
IntBuffer rateBuffer = BufferUtils.createIntBuffer(1);
Resource res = ResourceManager.getResource(path);
ShortBuffer rawAudio = decodeVorbis(res, channelBuffer, rateBuffer);
ShortBuffer rawAudio = decodeVorbis(resource, channelBuffer, rateBuffer);
return new SoundType(
id,
@ -49,12 +47,12 @@ public class AudioReader {
);
}
public static SoundType readAsMono(String path, String id) {
return readAsSpecified(path, id, AL_FORMAT_MONO16);
public static SoundType readAsMono(Resource resource, String id) {
return readAsSpecified(resource, id, AL_FORMAT_MONO16);
}
public static SoundType readAsStereo(String path, String id) {
return readAsSpecified(path, id, AL_FORMAT_STEREO16);
public static SoundType readAsStereo(Resource resource, String id) {
return readAsSpecified(resource, id, AL_FORMAT_STEREO16);
}
private static ShortBuffer decodeVorbis(

View File

@ -21,6 +21,9 @@ package ru.windcorp.progressia.client.audio.backend;
import ru.windcorp.progressia.common.util.namespaces.Namespaced;
import java.nio.ShortBuffer;
import org.lwjgl.openal.AL10;
import static org.lwjgl.openal.AL11.*;
public class SoundType extends Namespaced {
@ -29,7 +32,7 @@ public class SoundType extends Namespaced {
private int sampleRate;
private int format;
private int audioBuffer;
private int timeLength;
private double duration;
public SoundType(
String id,
@ -47,14 +50,14 @@ public class SoundType extends Namespaced {
private void createAudioBuffer() {
this.audioBuffer = alGenBuffers();
alBufferData(audioBuffer, format, rawAudio, sampleRate);
timeLength = rawAudio.limit() / sampleRate;
duration = rawAudio.limit() / (double) sampleRate / (format == AL10.AL_FORMAT_STEREO16 ? 2 : 1);
}
public void initSpeaker(Speaker speaker) {
speaker.setAudioData(audioBuffer);
}
public int getTimeLength() {
return timeLength;
public double getDuration() {
return duration;
}
}

View File

@ -120,6 +120,7 @@ public class Speaker {
}
public void setAudioData(int audioData) {
stop();
this.audioData = audioData;
alSourcei(this.sourceData, AL_BUFFER, audioData);
}

View File

@ -0,0 +1,53 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.comms.controls;
import java.util.function.Consumer;
import java.util.function.Predicate;
import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.common.comms.controls.PacketControl;
public class ControlTriggerLocalLambda extends ControlTriggerInputBased {
private final Predicate<InputEvent> predicate;
private final Consumer<InputEvent> action;
public ControlTriggerLocalLambda(
String id,
Predicate<InputEvent> predicate,
Consumer<InputEvent> action
) {
super(id);
this.predicate = predicate;
this.action = action;
}
@Override
public PacketControl onInputEvent(InputEvent event) {
if (!predicate.test(event))
return null;
action.accept(event);
return null;
}
}

View File

@ -142,6 +142,96 @@ public class ControlTriggers {
predicates
);
}
//
//
///
///
//
//
//
//
//
//
//
//
//
public static ControlTriggerInputBased localOf(
String id,
Consumer<InputEvent> action,
Predicate<InputEvent> predicate
) {
return new ControlTriggerLocalLambda(id, predicate, action);
}
public static ControlTriggerInputBased localOf(
String id,
Runnable action,
Predicate<InputEvent> predicate
) {
return localOf(
id,
input -> action.run(),
predicate
);
}
@SafeVarargs
public static <I extends InputEvent> ControlTriggerInputBased localOf(
String id,
Class<I> inputType,
Consumer<I> action,
Predicate<I>... predicates
) {
return localOf(
id,
createCheckedAction(inputType, action),
createCheckedCompoundPredicate(inputType, predicates)
);
}
@SafeVarargs
public static <I extends InputEvent> ControlTriggerInputBased localOf(
String id,
Class<I> inputType,
Runnable action,
Predicate<I>... predicates
) {
return localOf(
id,
inputType,
input -> action.run(),
predicates
);
}
@SafeVarargs
public static ControlTriggerInputBased localOf(
String id,
Consumer<InputEvent> action,
Predicate<InputEvent>... predicates
) {
return localOf(
id,
InputEvent.class,
action,
predicates
);
}
@SafeVarargs
public static <I extends InputEvent> ControlTriggerInputBased localOf(
String id,
Runnable action,
Predicate<InputEvent>... predicates
) {
return of(
id,
input -> action.run(),
predicates
);
}
private static <I extends InputEvent> BiConsumer<InputEvent, ControlData> createCheckedDataWriter(
Class<I> inputType,
@ -149,6 +239,13 @@ public class ControlTriggers {
) {
return (inputEvent, control) -> dataWriter.accept(inputType.cast(inputEvent), control);
}
private static <I extends InputEvent> Consumer<InputEvent> createCheckedAction(
Class<I> inputType,
Consumer<I> action
) {
return inputEvent -> action.accept(inputType.cast(inputEvent));
}
private static <I extends InputEvent> Predicate<InputEvent> createCheckedCompoundPredicate(
Class<I> inputType,

View File

@ -0,0 +1,31 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.resource;
import java.io.InputStream;
import ru.windcorp.progressia.Progressia;
public class ClasspathResourceReader implements ResourceReader {
@Override
public InputStream read(String name) {
return Progressia.class.getClassLoader().getResourceAsStream(name);
}
}

View File

@ -0,0 +1,36 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.resource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FilesystemResourceReader implements ResourceReader {
@Override
public InputStream read(String name) {
try {
return Files.newInputStream(Paths.get(name));
} catch (IOException e) {
return null;
}
}
}

View File

@ -30,19 +30,24 @@ import org.lwjgl.BufferUtils;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import ru.windcorp.progressia.Progressia;
import ru.windcorp.progressia.common.util.Named;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class Resource extends Named {
private final ResourceReader resourceReader;
public Resource(String name) {
public Resource(String name, ResourceReader resourceReader) {
super(name);
this.resourceReader = resourceReader;
}
public InputStream getInputStream() {
// TODO Do proper resource lookup
return Progressia.class.getClassLoader().getResourceAsStream(getName());
return getResourceReader().read(getName());
}
public ResourceReader getResourceReader() {
return resourceReader;
}
public Reader getReader() {

View File

@ -19,9 +19,16 @@
package ru.windcorp.progressia.common.resource;
public class ResourceManager {
private static final ResourceReader CLASSPATH_READER = new ClasspathResourceReader();
private static final ResourceReader FILESYSTEM_READER = new FilesystemResourceReader();
public static Resource getResource(String name) {
return new Resource(name);
return new Resource(name, CLASSPATH_READER);
}
public static Resource getFileResource(String name) {
return new Resource(name, FILESYSTEM_READER);
}
public static Resource getTextureResource(String name) {

View File

@ -0,0 +1,26 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.resource;
import java.io.InputStream;
public interface ResourceReader {
InputStream read(String name);
}

View File

@ -287,6 +287,15 @@ public class TestContent {
)
);
logic.register(ControlLogic.of("Test:PlaceTile", TestContent::onTilePlaceReceived));
triggers.register(
ControlTriggers.localOf(
"Test:StartNextMusic",
KeyEvent.class,
TestMusicPlayer::startNextNow,
KeyMatcher.of(GLFW.GLFW_KEY_M).matcher()
)
);
}
private static void register(BlockData x) {

View File

@ -0,0 +1,152 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.apache.logging.log4j.LogManager;
import ru.windcorp.progressia.client.audio.AudioFormat;
import ru.windcorp.progressia.client.audio.AudioManager;
import ru.windcorp.progressia.client.audio.AudioRegistry;
import ru.windcorp.progressia.client.audio.Music;
import ru.windcorp.progressia.client.audio.Sound;
import ru.windcorp.progressia.client.audio.backend.SoundType;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class TestMusicPlayer implements Runnable {
private static final int MIN_SILENCE = 15 * 1000; // 15 seconds
private static final int MAX_SILENCE = 60 * 1000; // one minute
private static TestMusicPlayer instance = null;
private final List<SoundType> compositions = new ArrayList<>();
private final Random random = new Random();
private long nextStart;
private Sound lastStarted = null;
public TestMusicPlayer() {
this.nextStart = System.currentTimeMillis();
instance = this;
}
public static void start() {
Thread thread = new Thread(new TestMusicPlayer(), "Music Thread");
thread.setDaemon(true);
thread.start();
}
@Override
public void run() {
loadCompositions();
if (compositions.isEmpty()) {
LogManager.getLogger().warn("No music found");
return;
}
while (true) {
try {
synchronized (this) {
while (true) {
long now = System.currentTimeMillis();
if (nextStart > now) {
wait(nextStart - now);
} else {
break;
}
}
}
} catch (InterruptedException e) {
LogManager.getLogger().warn("Received interrupt in music thread, terminating thread...");
return;
}
startNextComposition();
}
}
private void loadCompositions() {
try {
Path directory = Paths.get("music");
if (!Files.isDirectory(directory)) {
Files.createDirectories(directory);
}
Iterator<Path> it = Files.walk(directory).filter(Files::isRegularFile).iterator();
int i = 0;
while (it.hasNext()) {
String file = it.next().toString();
if (!file.endsWith(".ogg") && !file.endsWith(".oga")) {
LogManager.getLogger().warn("Skipping " + file + ": not .ogg nor .oga");
}
String id = "Progressia:Music" + (i++);
AudioManager.loadSound(ResourceManager.getFileResource(file.toString()), id, AudioFormat.STEREO);
SoundType composition = AudioRegistry.getInstance().get(id);
compositions.add(composition);
LogManager.getLogger().info("Loaded " + file);
}
} catch (IOException e) {
throw CrashReports.report(e, "Could not load music");
}
}
private synchronized void startNextComposition() {
int index = random.nextInt(compositions.size());
SoundType composition = compositions.get(index);
long now = System.currentTimeMillis();
long durationInMs = (long) (composition.getDuration() * 1000);
long silence = random.nextInt(MAX_SILENCE - MIN_SILENCE) + MIN_SILENCE;
nextStart = now + durationInMs + silence;
lastStarted = new Music(composition);
lastStarted.play(false);
}
public static void startNextNow() {
if (instance == null) return;
synchronized (instance) {
instance.nextStart = System.currentTimeMillis();
instance.notifyAll();
}
}
}