From fc6debf3c7e0f2faf6a4ea58025732ff8996d281 Mon Sep 17 00:00:00 2001 From: Lenni0451 <20379977+Lenni0451@users.noreply.github.com> Date: Wed, 8 May 2024 18:40:32 +0200 Subject: [PATCH] Recoded sound systems --- .../audio/export/impl/JavaxAudioExporter.java | 4 +- .../export/impl/OpenALAudioExporter.java | 13 +- .../audio/soundsystem/OpenALSoundSystem.java | 308 ---------------- .../audio/soundsystem/SoundSystem.java | 51 +++ .../{ => impl}/JavaxSoundSystem.java | 114 +++--- .../soundsystem/impl/OpenALSoundSystem.java | 329 ++++++++++++++++++ .../noteblocktool/frames/ExportFrame.java | 18 +- .../noteblocktool/frames/SongPlayerFrame.java | 155 +++------ 8 files changed, 503 insertions(+), 489 deletions(-) delete mode 100644 src/main/java/net/raphimc/noteblocktool/audio/soundsystem/OpenALSoundSystem.java create mode 100644 src/main/java/net/raphimc/noteblocktool/audio/soundsystem/SoundSystem.java rename src/main/java/net/raphimc/noteblocktool/audio/soundsystem/{ => impl}/JavaxSoundSystem.java (50%) create mode 100644 src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/OpenALSoundSystem.java diff --git a/src/main/java/net/raphimc/noteblocktool/audio/export/impl/JavaxAudioExporter.java b/src/main/java/net/raphimc/noteblocktool/audio/export/impl/JavaxAudioExporter.java index 6ca73b1..6744d1d 100644 --- a/src/main/java/net/raphimc/noteblocktool/audio/export/impl/JavaxAudioExporter.java +++ b/src/main/java/net/raphimc/noteblocktool/audio/export/impl/JavaxAudioExporter.java @@ -23,7 +23,7 @@ import net.raphimc.noteblocktool.audio.SoundMap; import net.raphimc.noteblocktool.audio.export.AudioExporter; import net.raphimc.noteblocktool.audio.export.AudioMerger; -import net.raphimc.noteblocktool.audio.soundsystem.JavaxSoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.impl.JavaxSoundSystem; import net.raphimc.noteblocktool.util.SoundSampleUtil; import javax.sound.sampled.AudioFormat; @@ -88,7 +88,7 @@ private Map loadSounds(final AudioFormat format) { try { Map sounds = new HashMap<>(); for (Map.Entry entry : SoundMap.SOUNDS.entrySet()) { - sounds.put(entry.getKey(), readSound(format, JavaxSoundSystem.class.getResourceAsStream(entry.getValue()))); + sounds.put(entry.getKey(), this.readSound(format, JavaxSoundSystem.class.getResourceAsStream(entry.getValue()))); } return sounds; } catch (Throwable e) { diff --git a/src/main/java/net/raphimc/noteblocktool/audio/export/impl/OpenALAudioExporter.java b/src/main/java/net/raphimc/noteblocktool/audio/export/impl/OpenALAudioExporter.java index 3b631af..c77cbd0 100644 --- a/src/main/java/net/raphimc/noteblocktool/audio/export/impl/OpenALAudioExporter.java +++ b/src/main/java/net/raphimc/noteblocktool/audio/export/impl/OpenALAudioExporter.java @@ -20,30 +20,33 @@ import net.raphimc.noteblocklib.model.SongView; import net.raphimc.noteblocklib.util.Instrument; import net.raphimc.noteblocktool.audio.export.AudioExporter; -import net.raphimc.noteblocktool.audio.soundsystem.OpenALSoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.impl.OpenALSoundSystem; import javax.sound.sampled.AudioFormat; import java.util.function.Consumer; public class OpenALAudioExporter extends AudioExporter { - public OpenALAudioExporter(final SongView songView, final AudioFormat format, final Consumer progressConsumer) { + private final OpenALSoundSystem soundSystem; + + public OpenALAudioExporter(final OpenALSoundSystem soundSystem, final SongView songView, final AudioFormat format, final Consumer progressConsumer) { super(songView, format, progressConsumer); + this.soundSystem = soundSystem; } @Override protected void processNote(Instrument instrument, float volume, float pitch, float panning) { - OpenALSoundSystem.playNote(instrument, volume, pitch, panning); + this.soundSystem.playNote(instrument, volume, pitch, panning); } @Override protected void writeSamples() { - OpenALSoundSystem.renderSamples(this.sampleOutputStream, this.samplesPerTick); + this.soundSystem.renderSamples(this.sampleOutputStream, this.samplesPerTick); } @Override protected void finish() { - OpenALSoundSystem.stopAllSources(); + this.soundSystem.stopSounds(); } } diff --git a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/OpenALSoundSystem.java b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/OpenALSoundSystem.java deleted file mode 100644 index 28127f6..0000000 --- a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/OpenALSoundSystem.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * This file is part of NoteBlockTool - https://github.com/RaphiMC/NoteBlockTool - * Copyright (C) 2022-2024 RK_01/RaphiMC and contributors - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 . - */ -package net.raphimc.noteblocktool.audio.soundsystem; - -import com.google.common.io.ByteStreams; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import net.raphimc.noteblocklib.util.Instrument; -import net.raphimc.noteblocktool.audio.SoundMap; -import net.raphimc.noteblocktool.audio.export.SampleOutputStream; -import org.lwjgl.openal.*; -import org.lwjgl.system.MemoryUtil; - -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import java.io.BufferedInputStream; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.*; - -public class OpenALSoundSystem { - - private static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("OpenAL Sound System").setDaemon(true).build()); - private static final Map INSTRUMENT_BUFFERS = new EnumMap<>(Instrument.class); - private static final List PLAYING_SOURCES = new CopyOnWriteArrayList<>(); - private static int MAX_MONO_SOURCES = 256; - private static AudioFormat AUDIO_FORMAT = null; - private static long DEVICE = 0L; - private static long CONTEXT = 0L; - private static ScheduledFuture TICK_TASK; - private static Thread SHUTDOWN_HOOK; - private static ByteBuffer CAPTURE_BUFFER; - - public static void initPlayback(final int maxSounds) { - MAX_MONO_SOURCES = maxSounds; - AUDIO_FORMAT = null; - DEVICE = ALC10.alcOpenDevice((ByteBuffer) null); - if (DEVICE <= 0L) { - throw new RuntimeException("Could not open device"); - } - checkError("Could not open device"); - init(new int[]{ - ALC11.ALC_MONO_SOURCES, MAX_MONO_SOURCES, - SOFTOutputLimiter.ALC_OUTPUT_LIMITER_SOFT, ALC10.ALC_TRUE, - 0 - }); - } - - public static void initCapture(final int maxSounds, final AudioFormat audioFormat) { - MAX_MONO_SOURCES = maxSounds; - AUDIO_FORMAT = audioFormat; - DEVICE = SOFTLoopback.alcLoopbackOpenDeviceSOFT((ByteBuffer) null); - if (DEVICE <= 0L) { - throw new RuntimeException("Could not open device"); - } - checkError("Could not open device"); - init(new int[]{ - ALC11.ALC_MONO_SOURCES, MAX_MONO_SOURCES, - SOFTOutputLimiter.ALC_OUTPUT_LIMITER_SOFT, ALC10.ALC_TRUE, - ALC10.ALC_FREQUENCY, (int) AUDIO_FORMAT.getSampleRate(), - SOFTLoopback.ALC_FORMAT_CHANNELS_SOFT, getAlSoftChannelFormat(AUDIO_FORMAT), - SOFTLoopback.ALC_FORMAT_TYPE_SOFT, getAlSoftFormatType(AUDIO_FORMAT), - 0 - }); - CAPTURE_BUFFER = MemoryUtil.memAlloc((int) AUDIO_FORMAT.getSampleRate() * AUDIO_FORMAT.getChannels() * AUDIO_FORMAT.getSampleSizeInBits() / 8 * 30); - } - - private static void init(final int[] attributes) { - final ALCCapabilities alcCapabilities = ALC.createCapabilities(DEVICE); - checkError("Could not create alcCapabilities"); - - if (!alcCapabilities.OpenALC11) { - throw new RuntimeException("OpenAL 1.1 is not supported"); - } - if (!alcCapabilities.ALC_SOFT_output_limiter) { - throw new RuntimeException("ALC_SOFT_output_limiter is not supported"); - } - - CONTEXT = ALC10.alcCreateContext(DEVICE, attributes); - checkError("Could not create context"); - if (!ALC10.alcMakeContextCurrent(CONTEXT)) { - throw new RuntimeException("Could not make context current"); - } - - AL.createCapabilities(alcCapabilities); - checkError("Could not create alCapabilities"); - - AL10.alListener3f(AL10.AL_POSITION, 0F, 0F, 0F); - checkError("Could not set listener position"); - AL10.alListener3f(AL10.AL_VELOCITY, 0F, 0F, 0F); - checkError("Could not set listener velocity"); - AL10.alListenerfv(AL10.AL_ORIENTATION, new float[]{0F, 0F, -1F, 0F, 1F, 0F}); - checkError("Could not set listener orientation"); - - for (Map.Entry entry : SoundMap.SOUNDS.entrySet()) { - INSTRUMENT_BUFFERS.put(entry.getKey(), loadWav(OpenALSoundSystem.class.getResourceAsStream(entry.getValue()))); - } - - TICK_TASK = SCHEDULER.scheduleAtFixedRate(OpenALSoundSystem::tick, 0, 100, TimeUnit.MILLISECONDS); - Runtime.getRuntime().addShutdownHook(SHUTDOWN_HOOK = new Thread(() -> { - SHUTDOWN_HOOK = null; - OpenALSoundSystem.destroy(); - })); - - System.out.println("Initialized OpenAL on " + ALC10.alcGetString(DEVICE, ALC11.ALC_ALL_DEVICES_SPECIFIER)); - } - - public static int getMaxMonoSources() { - return MAX_MONO_SOURCES; - } - - public static void playNote(final Instrument instrument, final float volume, final float pitch, final float panning) { - if (PLAYING_SOURCES.size() >= MAX_MONO_SOURCES) { - AL10.alDeleteSources(PLAYING_SOURCES.remove(0)); - checkError("Could not delete audio source"); - } - - final int source = AL10.alGenSources(); - checkError("Could not generate audio source"); - if (source > 0) { - AL10.alSourcei(source, AL10.AL_BUFFER, INSTRUMENT_BUFFERS.get(instrument)); - checkError("Could not set audio source buffer"); - AL10.alSourcef(source, AL10.AL_GAIN, volume); - checkError("Could not set audio source volume"); - AL10.alSourcef(source, AL10.AL_PITCH, pitch); - checkError("Could not set audio source pitch"); - AL10.alSource3f(source, AL10.AL_POSITION, panning * 2F, 0F, 0F); - checkError("Could not set audio source position"); - - AL10.alSourcePlay(source); - checkError("Could not play audio source"); - PLAYING_SOURCES.add(source); - } - } - - public static void renderSamples(final SampleOutputStream outputStream, final int sampleCount) { - final int samplesLength = sampleCount * AUDIO_FORMAT.getChannels(); - if (samplesLength * AUDIO_FORMAT.getSampleSizeInBits() / 8 > CAPTURE_BUFFER.capacity()) { - throw new IllegalArgumentException("Sample count too high"); - } - SOFTLoopback.alcRenderSamplesSOFT(DEVICE, CAPTURE_BUFFER, sampleCount); - checkError("Could not render samples"); - if (AUDIO_FORMAT.getSampleSizeInBits() == 8) { - for (int i = 0; i < samplesLength; i++) { - outputStream.writeSample(CAPTURE_BUFFER.get(i)); - } - } else if (AUDIO_FORMAT.getSampleSizeInBits() == 16) { - for (int i = 0; i < samplesLength; i++) { - outputStream.writeSample(CAPTURE_BUFFER.getShort(i * 2)); - } - } else if (AUDIO_FORMAT.getSampleSizeInBits() == 32) { - for (int i = 0; i < samplesLength; i++) { - outputStream.writeSample(CAPTURE_BUFFER.getInt(i * 4)); - } - } - } - - public static void stopAllSources() { - for (int source : PLAYING_SOURCES) { - AL10.alDeleteSources(source); - checkError("Could not delete audio source"); - } - PLAYING_SOURCES.clear(); - } - - public static void destroy() { - if (SHUTDOWN_HOOK != null) { - Runtime.getRuntime().removeShutdownHook(SHUTDOWN_HOOK); - SHUTDOWN_HOOK = null; - } - if (TICK_TASK != null) { - TICK_TASK.cancel(true); - TICK_TASK = null; - } - INSTRUMENT_BUFFERS.values().forEach(AL10::alDeleteBuffers); - INSTRUMENT_BUFFERS.clear(); - PLAYING_SOURCES.forEach(AL10::alDeleteSources); - PLAYING_SOURCES.clear(); - if (CONTEXT != 0L) { - ALC10.alcMakeContextCurrent(0); - ALC10.alcDestroyContext(CONTEXT); - CONTEXT = 0L; - } - if (DEVICE != 0L) { - ALC10.alcCloseDevice(DEVICE); - DEVICE = 0L; - } - if (CAPTURE_BUFFER != null) { - MemoryUtil.memFree(CAPTURE_BUFFER); - CAPTURE_BUFFER = null; - } - } - - public static void setMasterVolume(final float volume) { - AL10.alListenerf(AL10.AL_GAIN, volume); - checkError("Could not set listener gain"); - } - - public static int getPlayingSources() { - return PLAYING_SOURCES.size(); - } - - private static void tick() { - PLAYING_SOURCES.removeIf(source -> { - final int state = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE); - checkError("Could not get audio source state"); - if (state != AL10.AL_PLAYING) { - AL10.alDeleteSources(source); - checkError("Could not delete audio source"); - return true; - } - - return false; - }); - } - - private static int loadWav(final InputStream inputStream) { - final int buffer = AL10.alGenBuffers(); - checkError("Could not generate audio buffer"); - try { - final AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(inputStream)); - final AudioFormat audioFormat = audioInputStream.getFormat(); - - final byte[] audioBytes = ByteStreams.toByteArray(audioInputStream); - final ByteBuffer audioBuffer = MemoryUtil.memAlloc(audioBytes.length).put(audioBytes); - audioBuffer.flip(); - AL10.alBufferData(buffer, getAlAudioFormat(audioFormat), audioBuffer, (int) audioFormat.getSampleRate()); - checkError("Could not set audio buffer data"); - MemoryUtil.memFree(audioBuffer); - } catch (Throwable e) { - throw new RuntimeException("Could not load audio buffer", e); - } - - return buffer; - } - - private static int getAlAudioFormat(final AudioFormat audioFormat) { - if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { - if (audioFormat.getChannels() == 1) { - if (audioFormat.getSampleSizeInBits() == 8) { - return AL10.AL_FORMAT_MONO8; - } else if (audioFormat.getSampleSizeInBits() == 16) { - return AL10.AL_FORMAT_MONO16; - } - } else if (audioFormat.getChannels() == 2) { - if (audioFormat.getSampleSizeInBits() == 8) { - return AL10.AL_FORMAT_STEREO8; - } else if (audioFormat.getSampleSizeInBits() == 16) { - return AL10.AL_FORMAT_STEREO16; - } - } - } - - throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); - } - - private static int getAlSoftChannelFormat(final AudioFormat audioFormat) { - if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { - if (audioFormat.getChannels() == 1) { - return SOFTLoopback.ALC_MONO_SOFT; - } else if (audioFormat.getChannels() == 2) { - return SOFTLoopback.ALC_STEREO_SOFT; - } - } - - throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); - } - - private static int getAlSoftFormatType(final AudioFormat audioFormat) { - if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { - if (audioFormat.getSampleSizeInBits() == 8) { - return SOFTLoopback.ALC_BYTE_SOFT; - } else if (audioFormat.getSampleSizeInBits() == 16) { - return SOFTLoopback.ALC_SHORT_SOFT; - } else if (audioFormat.getSampleSizeInBits() == 32) { - return SOFTLoopback.ALC_INT_SOFT; - } - } - - throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); - } - - private static void checkError(final String message) { - final int error = ALC10.alcGetError(DEVICE); - if (error != ALC10.ALC_NO_ERROR) { - throw new RuntimeException("OpenAL error: " + message + " (" + error + ")"); - } - } - -} diff --git a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/SoundSystem.java b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/SoundSystem.java new file mode 100644 index 0000000..cedd264 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/SoundSystem.java @@ -0,0 +1,51 @@ +/* + * This file is part of NoteBlockTool - https://github.com/RaphiMC/NoteBlockTool + * Copyright (C) 2022-2024 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 . + */ +package net.raphimc.noteblocktool.audio.soundsystem; + +import net.raphimc.noteblocklib.util.Instrument; + +public abstract class SoundSystem implements AutoCloseable { + + protected final int maxSounds; + protected float masterVolume = 1F; + + public SoundSystem(final int maxSounds) { + this.maxSounds = maxSounds; + } + + public void setMasterVolume(final float volume) { + this.masterVolume = volume; + } + + public abstract void playNote(final Instrument instrument, final float volume, final float pitch, final float panning); + + public void writeSamples() { + } + + public abstract void stopSounds(); + + @Override + public abstract void close(); + + public int getMaxSounds() { + return this.maxSounds; + } + + public abstract int getSoundCount(); + +} diff --git a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/JavaxSoundSystem.java b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/JavaxSoundSystem.java similarity index 50% rename from src/main/java/net/raphimc/noteblocktool/audio/soundsystem/JavaxSoundSystem.java rename to src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/JavaxSoundSystem.java index b6c4030..cea4d80 100644 --- a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/JavaxSoundSystem.java +++ b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/JavaxSoundSystem.java @@ -15,11 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package net.raphimc.noteblocktool.audio.soundsystem; +package net.raphimc.noteblocktool.audio.soundsystem.impl; import com.google.common.io.ByteStreams; import net.raphimc.noteblocklib.util.Instrument; import net.raphimc.noteblocktool.audio.SoundMap; +import net.raphimc.noteblocktool.audio.soundsystem.SoundSystem; import net.raphimc.noteblocktool.util.SoundSampleUtil; import javax.sound.sampled.AudioFormat; @@ -31,62 +32,82 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; +import java.util.EnumMap; import java.util.HashMap; import java.util.Map; -public class JavaxSoundSystem { +public class JavaxSoundSystem extends SoundSystem { private static final AudioFormat FORMAT = new AudioFormat(44100, 16, 1, true, false); - private static Map sounds; - private static int samplesPerTick; - private static SourceDataLine dataLine; - private static long[] buffer = new long[0]; - private static Map mutationCache; - public static void init(final float playbackSpeed) { - try { - sounds = loadSounds(); + private final Map sounds; + private final int samplesPerTick; + private final SourceDataLine dataLine; + private final Map mutationCache; + private long[] buffer = new long[0]; - samplesPerTick = (int) (FORMAT.getSampleRate() / playbackSpeed); - dataLine = AudioSystem.getSourceDataLine(FORMAT); - dataLine.open(FORMAT, (int) FORMAT.getSampleRate()); - dataLine.start(); - mutationCache = new HashMap<>(); - } catch (Throwable t) { - throw new RuntimeException("Could not initialize audio system", t); + public JavaxSoundSystem(final int maxSounds, final float playbackSpeed) { + super(maxSounds); + + try { + this.sounds = this.loadSounds(); + this.samplesPerTick = (int) (FORMAT.getSampleRate() / playbackSpeed); + this.dataLine = AudioSystem.getSourceDataLine(FORMAT); + this.dataLine.open(FORMAT, (int) FORMAT.getSampleRate()); + this.dataLine.start(); + this.mutationCache = new HashMap<>(); + } catch (Throwable e) { + throw new RuntimeException("Could not initialize javax audio system", e); } } - public static void destroy() { - dataLine.stop(); - sounds = null; - buffer = new long[0]; - mutationCache = null; + @Override + public void playNote(Instrument instrument, float volume, float pitch, float panning) { + String key = instrument.ordinal() + "\0" + volume + "\0" + pitch; + int[] samples = this.mutationCache.computeIfAbsent(key, k -> SoundSampleUtil.mutate(this.sounds.get(instrument), volume * this.masterVolume, pitch)); + if (this.buffer.length < samples.length) this.buffer = Arrays.copyOf(this.buffer, samples.length); + for (int i = 0; i < samples.length; i++) this.buffer[i] += samples[i]; + } + + @Override + public void writeSamples() { + long[] samples = Arrays.copyOfRange(this.buffer, 0, this.samplesPerTick); + this.dataLine.write(this.write(samples), 0, samples.length * 2); + if (this.buffer.length > this.samplesPerTick) this.buffer = Arrays.copyOfRange(this.buffer, this.samplesPerTick, this.buffer.length); + else if (this.buffer.length != 0) this.buffer = new long[0]; + } + + @Override + public void stopSounds() { + this.dataLine.flush(); + } + + @Override + public void close() { + this.dataLine.stop(); } - public static void playNote(final Instrument instrument, final float volume, final float pitch) { - String key = instrument.name() + "\0" + volume + "\0" + pitch; - int[] samples = mutationCache.computeIfAbsent(key, k -> SoundSampleUtil.mutate(sounds.get(instrument), volume, pitch)); - if (buffer.length < samples.length) buffer = Arrays.copyOf(buffer, samples.length); - for (int i = 0; i < samples.length; i++) buffer[i] += samples[i]; + @Override + public void setMasterVolume(float volume) { + super.setMasterVolume(volume); + this.mutationCache.clear(); } - public static void tick() { - long[] samples = Arrays.copyOfRange(buffer, 0, samplesPerTick); - dataLine.write(write(samples), 0, samples.length * 2); - if (buffer.length > samplesPerTick) buffer = Arrays.copyOfRange(buffer, samplesPerTick, buffer.length); - else if (buffer.length != 0) buffer = new long[0]; + @Override + public int getMaxSounds() { + return 0; } - public static void flushDataLine() { - dataLine.flush(); + @Override + public int getSoundCount() { + return 0; } - private static Map loadSounds() { + private Map loadSounds() { try { - Map sounds = new HashMap<>(); + Map sounds = new EnumMap<>(Instrument.class); for (Map.Entry entry : SoundMap.SOUNDS.entrySet()) { - sounds.put(entry.getKey(), readSound(JavaxSoundSystem.class.getResourceAsStream(entry.getValue()))); + sounds.put(entry.getKey(), this.readSound(JavaxSoundSystem.class.getResourceAsStream(entry.getValue()))); } return sounds; } catch (Throwable e) { @@ -94,7 +115,7 @@ private static Map loadSounds() { } } - private static int[] readSound(final InputStream is) { + private int[] readSound(final InputStream is) { try { AudioInputStream in = AudioSystem.getAudioInputStream(new BufferedInputStream(is)); if (!in.getFormat().matches(FORMAT)) in = AudioSystem.getAudioInputStream(FORMAT, in); @@ -103,20 +124,7 @@ private static int[] readSound(final InputStream is) { final int sampleSize = FORMAT.getSampleSizeInBits() / 8; final int[] samples = new int[audioBytes.length / sampleSize]; for (int i = 0; i < samples.length; i++) { - ByteBuffer buffer = ByteBuffer.wrap(audioBytes, i * sampleSize, sampleSize).order(ByteOrder.LITTLE_ENDIAN); - switch (FORMAT.getSampleSizeInBits()) { - case 8: - samples[i] = buffer.get(); - break; - case 16: - samples[i] = buffer.getShort(); - break; - case 32: - samples[i] = buffer.getInt(); - break; - default: - throw new UnsupportedOperationException("Unsupported sample size: " + FORMAT.getSampleSizeInBits()); - } + samples[i] = ByteBuffer.wrap(audioBytes, i * sampleSize, sampleSize).order(ByteOrder.LITTLE_ENDIAN).getShort(); } return samples; @@ -125,7 +133,7 @@ private static int[] readSound(final InputStream is) { } } - private static byte[] write(final long[] samples) { + private byte[] write(final long[] samples) { byte[] out = new byte[samples.length * 2]; for (int i = 0; i < samples.length; i++) { long sample = samples[i]; diff --git a/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/OpenALSoundSystem.java b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/OpenALSoundSystem.java new file mode 100644 index 0000000..92364d8 --- /dev/null +++ b/src/main/java/net/raphimc/noteblocktool/audio/soundsystem/impl/OpenALSoundSystem.java @@ -0,0 +1,329 @@ +/* + * This file is part of NoteBlockTool - https://github.com/RaphiMC/NoteBlockTool + * Copyright (C) 2022-2024 RK_01/RaphiMC and contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 . + */ +package net.raphimc.noteblocktool.audio.soundsystem.impl; + +import com.google.common.io.ByteStreams; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import net.raphimc.noteblocklib.util.Instrument; +import net.raphimc.noteblocktool.audio.SoundMap; +import net.raphimc.noteblocktool.audio.export.SampleOutputStream; +import net.raphimc.noteblocktool.audio.soundsystem.SoundSystem; +import org.lwjgl.openal.*; +import org.lwjgl.system.MemoryUtil; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class OpenALSoundSystem extends SoundSystem { + + private static OpenALSoundSystem instance; + + public static OpenALSoundSystem createPlayback(final int maxSounds) { + if (instance != null) { + throw new IllegalStateException("OpenAL sound system already initialized"); + } + instance = new OpenALSoundSystem(maxSounds); + return instance; + } + + public static OpenALSoundSystem createCapture(final int maxSounds, final AudioFormat captureAudioFormat) { + if (instance != null) { + throw new IllegalStateException("OpenAL sound system already initialized"); + } + instance = new OpenALSoundSystem(maxSounds, captureAudioFormat); + return instance; + } + + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat("OpenAL Sound System").setDaemon(true).build()); + private final Map instrumentBuffers = new EnumMap<>(Instrument.class); + private final List playingSources = new CopyOnWriteArrayList<>(); + private final AudioFormat audioFormat; + private long device; + private long context; + private Thread shutdownHook; + private ByteBuffer captureBuffer; + + private OpenALSoundSystem(final int maxSounds) { //Playback + this(maxSounds, null); + } + + private OpenALSoundSystem(final int maxSounds, final AudioFormat captureAudioFormat) { //Capture + super(maxSounds); + + this.audioFormat = captureAudioFormat; + int[] attributes; + if (captureAudioFormat == null) { + this.device = ALC10.alcOpenDevice((ByteBuffer) null); + attributes = new int[]{ + ALC11.ALC_MONO_SOURCES, this.maxSounds, + SOFTOutputLimiter.ALC_OUTPUT_LIMITER_SOFT, ALC10.ALC_TRUE, + 0 + }; + } else { + this.device = SOFTLoopback.alcLoopbackOpenDeviceSOFT((ByteBuffer) null); + attributes = new int[]{ + ALC11.ALC_MONO_SOURCES, this.maxSounds, + SOFTOutputLimiter.ALC_OUTPUT_LIMITER_SOFT, ALC10.ALC_TRUE, + ALC10.ALC_FREQUENCY, (int) this.audioFormat.getSampleRate(), + SOFTLoopback.ALC_FORMAT_CHANNELS_SOFT, this.getAlSoftChannelFormat(this.audioFormat), + SOFTLoopback.ALC_FORMAT_TYPE_SOFT, this.getAlSoftFormatType(this.audioFormat), + 0 + }; + } + if (this.device <= 0L) { + throw new RuntimeException("Could not open device"); + } + this.checkError("Could not open device"); + + final ALCCapabilities alcCapabilities = ALC.createCapabilities(this.device); + this.checkError("Could not create alcCapabilities"); + + if (!alcCapabilities.OpenALC11) { + throw new RuntimeException("OpenAL 1.1 is not supported"); + } + if (!alcCapabilities.ALC_SOFT_output_limiter) { + throw new RuntimeException("ALC_SOFT_output_limiter is not supported"); + } + + this.context = ALC10.alcCreateContext(this.device, attributes); + this.checkError("Could not create context"); + if (!ALC10.alcMakeContextCurrent(this.context)) { + throw new RuntimeException("Could not make context current"); + } + + AL.createCapabilities(alcCapabilities); + this.checkError("Could not create alCapabilities"); + + AL10.alListener3f(AL10.AL_POSITION, 0F, 0F, 0F); + this.checkError("Could not set listener position"); + AL10.alListener3f(AL10.AL_VELOCITY, 0F, 0F, 0F); + this.checkError("Could not set listener velocity"); + AL10.alListenerfv(AL10.AL_ORIENTATION, new float[]{0F, 0F, -1F, 0F, 1F, 0F}); + this.checkError("Could not set listener orientation"); + + for (Map.Entry entry : SoundMap.SOUNDS.entrySet()) { + this.instrumentBuffers.put(entry.getKey(), this.loadWav(OpenALSoundSystem.class.getResourceAsStream(entry.getValue()))); + } + + this.scheduler.scheduleAtFixedRate(this::tick, 0, 100, TimeUnit.MILLISECONDS); + Runtime.getRuntime().addShutdownHook(this.shutdownHook = new Thread(() -> { + this.shutdownHook = null; + this.close(); + })); + + if (captureAudioFormat != null) { + this.captureBuffer = MemoryUtil.memAlloc((int) this.audioFormat.getSampleRate() * this.audioFormat.getChannels() * this.audioFormat.getSampleSizeInBits() / 8 * 30); + } + + System.out.println("Initialized OpenAL on " + ALC10.alcGetString(this.device, ALC11.ALC_ALL_DEVICES_SPECIFIER)); + } + + @Override + public void playNote(Instrument instrument, float volume, float pitch, float panning) { + if (this.playingSources.size() >= this.maxSounds) { + AL10.alDeleteSources(this.playingSources.remove(0)); + this.checkError("Could not delete audio source"); + } + + final int source = AL10.alGenSources(); + this.checkError("Could not generate audio source"); + if (source > 0) { + AL10.alSourcei(source, AL10.AL_BUFFER, this.instrumentBuffers.get(instrument)); + this.checkError("Could not set audio source buffer"); + AL10.alSourcef(source, AL10.AL_GAIN, volume); + this.checkError("Could not set audio source volume"); + AL10.alSourcef(source, AL10.AL_PITCH, pitch); + this.checkError("Could not set audio source pitch"); + AL10.alSource3f(source, AL10.AL_POSITION, panning * 2F, 0F, 0F); + this.checkError("Could not set audio source position"); + + AL10.alSourcePlay(source); + this.checkError("Could not play audio source"); + this.playingSources.add(source); + } + } + + public void renderSamples(final SampleOutputStream outputStream, final int sampleCount) { + final int samplesLength = sampleCount * this.audioFormat.getChannels(); + if (samplesLength * this.audioFormat.getSampleSizeInBits() / 8 > this.captureBuffer.capacity()) { + throw new IllegalArgumentException("Sample count too high"); + } + SOFTLoopback.alcRenderSamplesSOFT(this.device, this.captureBuffer, sampleCount); + this.checkError("Could not render samples"); + if (this.audioFormat.getSampleSizeInBits() == 8) { + for (int i = 0; i < samplesLength; i++) { + outputStream.writeSample(this.captureBuffer.get(i)); + } + } else if (this.audioFormat.getSampleSizeInBits() == 16) { + for (int i = 0; i < samplesLength; i++) { + outputStream.writeSample(this.captureBuffer.getShort(i * 2)); + } + } else if (this.audioFormat.getSampleSizeInBits() == 32) { + for (int i = 0; i < samplesLength; i++) { + outputStream.writeSample(this.captureBuffer.getInt(i * 4)); + } + } + } + + @Override + public void stopSounds() { + for (int source : this.playingSources) { + AL10.alDeleteSources(source); + this.checkError("Could not delete audio source"); + } + this.playingSources.clear(); + } + + @Override + public void close() { + if (this.shutdownHook != null) { + Runtime.getRuntime().removeShutdownHook(this.shutdownHook); + this.shutdownHook = null; + } + this.scheduler.shutdownNow(); + this.instrumentBuffers.values().forEach(AL10::alDeleteBuffers); + this.instrumentBuffers.clear(); + this.playingSources.forEach(AL10::alDeleteSources); + this.playingSources.clear(); + if (this.context != 0L) { + ALC10.alcMakeContextCurrent(0); + ALC10.alcDestroyContext(this.context); + this.context = 0L; + } + if (this.device != 0L) { + ALC10.alcCloseDevice(this.device); + this.device = 0L; + } + if (this.captureBuffer != null) { + MemoryUtil.memFree(this.captureBuffer); + this.captureBuffer = null; + } + instance = null; + } + + @Override + public void setMasterVolume(float volume) { + AL10.alListenerf(AL10.AL_GAIN, volume); + this.checkError("Could not set listener gain"); + } + + @Override + public int getSoundCount() { + return this.playingSources.size(); + } + + private void tick() { + this.playingSources.removeIf(source -> { + final int state = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE); + this.checkError("Could not get audio source state"); + if (state != AL10.AL_PLAYING) { + AL10.alDeleteSources(source); + this.checkError("Could not delete audio source"); + return true; + } + + return false; + }); + } + + private int loadWav(final InputStream inputStream) { + final int buffer = AL10.alGenBuffers(); + this.checkError("Could not generate audio buffer"); + try { + final AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new BufferedInputStream(inputStream)); + final AudioFormat audioFormat = audioInputStream.getFormat(); + + final byte[] audioBytes = ByteStreams.toByteArray(audioInputStream); + final ByteBuffer audioBuffer = MemoryUtil.memAlloc(audioBytes.length).put(audioBytes); + audioBuffer.flip(); + AL10.alBufferData(buffer, this.getAlAudioFormat(audioFormat), audioBuffer, (int) audioFormat.getSampleRate()); + this.checkError("Could not set audio buffer data"); + MemoryUtil.memFree(audioBuffer); + } catch (Throwable e) { + throw new RuntimeException("Could not load audio buffer", e); + } + + return buffer; + } + + private int getAlAudioFormat(final AudioFormat audioFormat) { + if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { + if (audioFormat.getChannels() == 1) { + if (audioFormat.getSampleSizeInBits() == 8) { + return AL10.AL_FORMAT_MONO8; + } else if (audioFormat.getSampleSizeInBits() == 16) { + return AL10.AL_FORMAT_MONO16; + } + } else if (audioFormat.getChannels() == 2) { + if (audioFormat.getSampleSizeInBits() == 8) { + return AL10.AL_FORMAT_STEREO8; + } else if (audioFormat.getSampleSizeInBits() == 16) { + return AL10.AL_FORMAT_STEREO16; + } + } + } + + throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); + } + + private int getAlSoftChannelFormat(final AudioFormat audioFormat) { + if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { + if (audioFormat.getChannels() == 1) { + return SOFTLoopback.ALC_MONO_SOFT; + } else if (audioFormat.getChannels() == 2) { + return SOFTLoopback.ALC_STEREO_SOFT; + } + } + + throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); + } + + private int getAlSoftFormatType(final AudioFormat audioFormat) { + if (audioFormat.getEncoding() == AudioFormat.Encoding.PCM_SIGNED || audioFormat.getEncoding() == AudioFormat.Encoding.PCM_UNSIGNED) { + if (audioFormat.getSampleSizeInBits() == 8) { + return SOFTLoopback.ALC_BYTE_SOFT; + } else if (audioFormat.getSampleSizeInBits() == 16) { + return SOFTLoopback.ALC_SHORT_SOFT; + } else if (audioFormat.getSampleSizeInBits() == 32) { + return SOFTLoopback.ALC_INT_SOFT; + } + } + + throw new IllegalArgumentException("Unsupported audio format: " + audioFormat); + } + + private void checkError(final String message) { + final int error = ALC10.alcGetError(this.device); + if (error != ALC10.ALC_NO_ERROR) { + throw new RuntimeException("OpenAL error: " + message + " (" + error + ")"); + } + } + +} diff --git a/src/main/java/net/raphimc/noteblocktool/frames/ExportFrame.java b/src/main/java/net/raphimc/noteblocktool/frames/ExportFrame.java index 8be0e11..4751de6 100644 --- a/src/main/java/net/raphimc/noteblocktool/frames/ExportFrame.java +++ b/src/main/java/net/raphimc/noteblocktool/frames/ExportFrame.java @@ -33,7 +33,7 @@ import net.raphimc.noteblocktool.audio.export.AudioExporter; import net.raphimc.noteblocktool.audio.export.impl.JavaxAudioExporter; import net.raphimc.noteblocktool.audio.export.impl.OpenALAudioExporter; -import net.raphimc.noteblocktool.audio.soundsystem.OpenALSoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.impl.OpenALSoundSystem; import net.raphimc.noteblocktool.util.filefilter.SingleFileFilter; import javax.sound.sampled.AudioFormat; @@ -240,6 +240,7 @@ private File openFileChooser() { } private void doExport(final File outFile) { + OpenALSoundSystem openALSoundSystem = null; try { Map songPanels = new ConcurrentHashMap<>(); SwingUtilities.invokeAndWait(() -> { @@ -266,12 +267,12 @@ private void doExport(final File outFile) { true, false ); - if (this.soundSystem.getSelectedIndex() == 0 && this.format.getSelectedIndex() != 0) OpenALSoundSystem.initCapture(8192, format); + if (this.soundSystem.getSelectedIndex() == 0 && this.format.getSelectedIndex() != 0) openALSoundSystem = OpenALSoundSystem.createCapture(8192, format); if (this.loadedSongs.size() == 1) { JPanel songPanel = songPanels.get(this.loadedSongs.get(0)); JProgressBar progressBar = (JProgressBar) songPanel.getComponent(1); try { - this.exportSong(this.loadedSongs.get(0), format, outFile, progress -> { + this.exportSong(this.loadedSongs.get(0), openALSoundSystem, format, outFile, progress -> { SwingUtilities.invokeLater(() -> { int value = (int) (progress * 100); progressBar.setValue(value); @@ -302,12 +303,13 @@ private void doExport(final File outFile) { String extension = this.format.getSelectedItem().toString().toLowerCase(); for (ListFrame.LoadedSong song : this.loadedSongs) { + final OpenALSoundSystem finalOpenALSoundSystem = openALSoundSystem; threadPool.submit(() -> { JPanel songPanel = songPanels.get(song); JProgressBar progressBar = (JProgressBar) songPanel.getComponent(1); try { File file = new File(outFile, song.getFile().getName().substring(0, song.getFile().getName().lastIndexOf('.')) + "." + extension); - this.exportSong(song, format, file, progress -> { + this.exportSong(song, finalOpenALSoundSystem, format, file, progress -> { uiQueue.offer(() -> { int value = (int) (progress * 100); progressBar.setValue(value); @@ -316,7 +318,7 @@ private void doExport(final File outFile) { }); }); uiQueue.offer(() -> { - progressPanel.remove(songPanel); + this.progressPanel.remove(songPanel); this.progressPanel.revalidate(); this.progressPanel.repaint(); }); @@ -359,7 +361,7 @@ private void doExport(final File outFile) { t.printStackTrace(); JOptionPane.showMessageDialog(this, "Failed to export songs:\n" + t.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } finally { - if (this.soundSystem.getSelectedIndex() == 0 && this.format.getSelectedIndex() != 0) OpenALSoundSystem.destroy(); + if (openALSoundSystem != null) openALSoundSystem.close(); SwingUtilities.invokeLater(() -> { this.format.setEnabled(true); this.soundSystem.setEnabled(true); @@ -374,7 +376,7 @@ private void doExport(final File outFile) { } } - private void exportSong(final ListFrame.LoadedSong song, final AudioFormat format, final File file, final Consumer progressConsumer) throws InterruptedException, IOException { + private void exportSong(final ListFrame.LoadedSong song, final OpenALSoundSystem soundSystem, final AudioFormat format, final File file, final Consumer progressConsumer) throws InterruptedException, IOException { if (this.format.getSelectedIndex() == 0) { this.writeNbsSong(song, file); } else { @@ -384,7 +386,7 @@ private void exportSong(final ListFrame.LoadedSong song, final AudioFormat forma } AudioExporter exporter; - if (this.soundSystem.getSelectedIndex() == 0) exporter = new OpenALAudioExporter(songView, format, progressConsumer); + if (this.soundSystem.getSelectedIndex() == 0) exporter = new OpenALAudioExporter(soundSystem, songView, format, progressConsumer); else exporter = new JavaxAudioExporter(songView, format, progressConsumer); exporter.render(); diff --git a/src/main/java/net/raphimc/noteblocktool/frames/SongPlayerFrame.java b/src/main/java/net/raphimc/noteblocktool/frames/SongPlayerFrame.java index e34ef3f..41ff412 100644 --- a/src/main/java/net/raphimc/noteblocktool/frames/SongPlayerFrame.java +++ b/src/main/java/net/raphimc/noteblocktool/frames/SongPlayerFrame.java @@ -26,8 +26,9 @@ import net.raphimc.noteblocklib.player.SongPlayer; import net.raphimc.noteblocklib.util.Instrument; import net.raphimc.noteblocklib.util.SongResampler; -import net.raphimc.noteblocktool.audio.soundsystem.JavaxSoundSystem; -import net.raphimc.noteblocktool.audio.soundsystem.OpenALSoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.SoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.impl.JavaxSoundSystem; +import net.raphimc.noteblocktool.audio.soundsystem.impl.OpenALSoundSystem; import net.raphimc.noteblocktool.elements.FastScrollPane; import net.raphimc.noteblocktool.elements.NewLineLabel; import net.raphimc.noteblocktool.util.DefaultSongPlayerCallback; @@ -38,16 +39,12 @@ import java.awt.event.WindowEvent; import java.text.DecimalFormat; import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.IntSupplier; public class SongPlayerFrame extends JFrame implements DefaultSongPlayerCallback { - private static final String UNAVAILABLE_MESSAGE = "Your system does not support any sound system.\nPlaying songs is not supported."; + private static final String UNAVAILABLE_MESSAGE = "An error occurred while initializing the sound system.\nPlease make sure that your system supports the selected sound system."; private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); private static SongPlayerFrame instance; - private static SoundSystem forcedSoundSystem; - private static boolean songPlayerUnavailable; private static Point lastPosition; private static int lastMaxSounds = 256; private static int lastVolume = 50; @@ -64,10 +61,6 @@ public static void open(final ListFrame.LoadedSong song, final SongView view) instance.dispose(); } instance = new SongPlayerFrame(song, view); - if (songPlayerUnavailable) { - JOptionPane.showMessageDialog(instance, UNAVAILABLE_MESSAGE, "Error", JOptionPane.ERROR_MESSAGE); - return; - } if (lastPosition != null) instance.setLocation(lastPosition); instance.maxSoundsSpinner.setValue(lastMaxSounds); instance.volumeSlider.setValue(lastVolume); @@ -83,7 +76,7 @@ public static void close() { private final ListFrame.LoadedSong song; private final SongPlayer songPlayer; private final Timer updateTimer; - private final JComboBox soundSystemComboBox = new JComboBox<>(new String[]{SoundSystem.OPENAL.getName(), SoundSystem.JAVAX.getName()}); + private final JComboBox soundSystemComboBox = new JComboBox<>(new String[]{"OpenAL (better sound quality)", "Javax (better system compatibility, mono only)"}); private final JSpinner maxSoundsSpinner = new JSpinner(new SpinnerNumberModel(256, 64, 8192, 64)); private final JSlider volumeSlider = new JSlider(0, 100, 50); private final JButton playStopButton = new JButton("Play"); @@ -91,7 +84,7 @@ public static void close() { private final JSlider progressSlider = new JSlider(0, 100, 0); private final JLabel soundCount = new JLabel("Sounds: 0/" + DECIMAL_FORMAT.format(this.maxSoundsSpinner.getValue())); private final JLabel progressLabel = new JLabel("Current Position: 00:00:00"); - private SoundSystem soundSystem = SoundSystem.OPENAL; + private SoundSystem soundSystem; private float volume = 1F; private SongPlayerFrame(final ListFrame.LoadedSong song, final SongView view) { @@ -108,10 +101,6 @@ private SongPlayerFrame(final ListFrame.LoadedSong song, final SongView view) this.initComponents(); this.initFrameHandler(); - if (forcedSoundSystem != null) { - this.soundSystem = forcedSoundSystem; - this.soundSystemComboBox.setSelectedIndex(this.soundSystem.ordinal()); - } this.setMinimumSize(this.getSize()); } @@ -151,9 +140,8 @@ private void initComponents() { this.volumeSlider.setMajorTickSpacing(25); this.volumeSlider.setMinorTickSpacing(5); this.volumeSlider.addChangeListener(e -> { - if (songPlayerUnavailable) return; this.volume = this.volumeSlider.getValue() / 100F; - if (this.soundSystem.equals(SoundSystem.OPENAL)) OpenALSoundSystem.setMasterVolume(this.volume); + if (this.soundSystem != null) this.soundSystem.setMasterVolume(this.volume); lastVolume = this.volumeSlider.getValue(); }); }); @@ -208,7 +196,6 @@ private void initComponents() { GBC.create(southPanel).grid(0, gridy++).insets(5, 5, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.progressSlider, () -> { this.progressSlider.addChangeListener(e -> { - if (songPlayerUnavailable) return; //Skip updates if the value is set directly if (!this.progressSlider.getValueIsAdjusting()) return; if (!this.songPlayer.isRunning()) { @@ -223,36 +210,15 @@ private void initComponents() { buttonPanel.setLayout(new GridLayout(1, 3, 5, 0)); buttonPanel.add(this.playStopButton); this.playStopButton.addActionListener(e -> { - if (songPlayerUnavailable) return; if (this.songPlayer.isRunning()) { this.songPlayer.stop(); this.songPlayer.setTick(0); - this.soundSystem.stopSounds(); + if (this.soundSystem != null) this.soundSystem.stopSounds(); } else { - SoundSystem selectedSoundSystem = SoundSystem.values()[this.soundSystemComboBox.getSelectedIndex()]; - if (!this.soundSystem.equals(selectedSoundSystem) || this.soundSystem.getMaxSounds() != (int) this.maxSoundsSpinner.getValue()) { - this.soundSystem.destroy(); - this.soundSystem = selectedSoundSystem; - } - try { - this.soundSystem.init((int) this.maxSoundsSpinner.getValue(), this.songPlayer.getSongView().getSpeed()); - } catch (Throwable t) { - t.printStackTrace(); - JOptionPane.showMessageDialog(this, "Failed to initialize the " + this.soundSystem.getName() + " sound system:\n" + t.getClass().getSimpleName() + ": " + t.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); - try { - this.soundSystem = SoundSystem.values()[(this.soundSystem.ordinal() + 1) % SoundSystem.values().length]; - this.soundSystem.init((int) this.maxSoundsSpinner.getValue(), this.songPlayer.getSongView().getSpeed()); - forcedSoundSystem = this.soundSystem; - this.soundSystemComboBox.setEnabled(false); - this.soundSystemComboBox.setSelectedIndex(this.soundSystem.ordinal()); - } catch (Throwable ex) { - ex.printStackTrace(); - songPlayerUnavailable = true; - return; - } + if (this.initSoundSystem()) { + this.soundSystem.setMasterVolume(this.volume); + this.songPlayer.play(); } - if (this.soundSystem.equals(SoundSystem.OPENAL)) OpenALSoundSystem.setMasterVolume(this.volume); - this.songPlayer.play(); } }); buttonPanel.add(this.pauseResumeButton); @@ -267,6 +233,32 @@ private void initComponents() { } } + private boolean initSoundSystem() { + int currentIndex = -1; + if (this.soundSystem instanceof OpenALSoundSystem) currentIndex = 0; + else if (this.soundSystem instanceof JavaxSoundSystem) currentIndex = 1; + + try { + if (this.soundSystem == null || this.soundSystemComboBox.getSelectedIndex() != currentIndex || this.soundSystem.getMaxSounds() != (int) this.maxSoundsSpinner.getValue()) { + if (this.soundSystem != null) this.soundSystem.close(); + + if (this.soundSystemComboBox.getSelectedIndex() == 0) { + this.soundSystem = OpenALSoundSystem.createPlayback(((Number) this.maxSoundsSpinner.getValue()).intValue()); + } else if (this.soundSystemComboBox.getSelectedIndex() == 1) { + this.soundSystem = new JavaxSoundSystem(((Number) this.maxSoundsSpinner.getValue()).intValue(), this.songPlayer.getSongView().getSpeed()); + } else { + throw new UnsupportedOperationException(UNAVAILABLE_MESSAGE); + } + } + return this.soundSystem != null; + } catch (Throwable t) { + this.soundSystem = null; + t.printStackTrace(); + JOptionPane.showMessageDialog(this, UNAVAILABLE_MESSAGE, "Error", JOptionPane.ERROR_MESSAGE); + } + return false; + } + private void initFrameHandler() { this.addWindowListener(new WindowAdapter() { @Override @@ -279,7 +271,7 @@ public void windowClosing(WindowEvent e) { public void windowClosed(WindowEvent e) { SongPlayerFrame.this.songPlayer.stop(); SongPlayerFrame.this.updateTimer.stop(); - SongPlayerFrame.this.soundSystem.stopSounds(); + SongPlayerFrame.this.soundSystem.close(); } }); } @@ -297,18 +289,14 @@ private void tick() { if (this.progressSlider.getMaximum() != tickCount) this.progressSlider.setMaximum(tickCount); this.progressSlider.setValue(this.songPlayer.getTick()); } else { - this.soundSystemComboBox.setEnabled(forcedSoundSystem == null); + this.soundSystemComboBox.setEnabled(true); this.maxSoundsSpinner.setEnabled(true); this.playStopButton.setText("Play"); this.pauseResumeButton.setText("Pause"); this.pauseResumeButton.setEnabled(false); this.progressSlider.setValue(0); } - if (this.soundSystem.equals(SoundSystem.JAVAX)) { - this.soundCount.setText("Sounds: 0/0"); - } else { - this.soundCount.setText("Sounds: " + DECIMAL_FORMAT.format(this.soundSystem.getSoundCount()) + "/" + DECIMAL_FORMAT.format(this.maxSoundsSpinner.getValue())); - } + this.soundCount.setText("Sounds: " + DECIMAL_FORMAT.format(this.soundSystem.getSoundCount()) + "/" + DECIMAL_FORMAT.format(this.soundSystem.getMaxSounds())); int msLength = (int) (this.songPlayer.getTick() / this.songPlayer.getSongView().getSpeed()); this.progressLabel.setText("Current Position: " + String.format("%02d:%02d:%02d", msLength / 3600, (msLength / 60) % 60, msLength % 60)); @@ -316,73 +304,14 @@ private void tick() { @Override public void playNote(Instrument instrument, float volume, float pitch, float panning) { - if (songPlayerUnavailable) return; - - if (this.soundSystem.equals(SoundSystem.OPENAL)) { - OpenALSoundSystem.playNote(instrument, volume, pitch, panning); - } else if (this.soundSystem.equals(SoundSystem.JAVAX)) { - JavaxSoundSystem.playNote(instrument, volume * this.volume, pitch); - } + this.soundSystem.playNote(instrument, volume, pitch, panning); } @Override public void playNotes(java.util.List notes) { for (Note note : notes) this.playNote(note); - if (this.soundSystem.equals(SoundSystem.JAVAX)) JavaxSoundSystem.tick(); - } - - private enum SoundSystem { - OPENAL("OpenAL (better sound quality)", (maxSounds, playbackSpeed) -> OpenALSoundSystem.initPlayback(maxSounds), OpenALSoundSystem::getMaxMonoSources, OpenALSoundSystem::getPlayingSources, OpenALSoundSystem::stopAllSources, OpenALSoundSystem::destroy), - JAVAX("Javax (better system compatibility, mono only)", (maxSounds, playbackSpeed) -> JavaxSoundSystem.init(playbackSpeed), () -> 0, () -> 0, JavaxSoundSystem::flushDataLine, JavaxSoundSystem::destroy); - - private final String name; - private final BiConsumer init; - private final IntSupplier maxSounds; - private final IntSupplier soundCount; - private final Runnable stopSounds; - private final Runnable destroy; - private boolean initialized; - - SoundSystem(final String name, final BiConsumer init, final IntSupplier maxSounds, final IntSupplier soundCount, final Runnable stopSounds, final Runnable destroy) { - this.name = name; - this.init = init; - this.maxSounds = maxSounds; - this.soundCount = soundCount; - this.stopSounds = stopSounds; - this.destroy = destroy; - } - - public String getName() { - return this.name; - } - - public void init(final int maxSounds, final float playbackSpeed) { - if (this.initialized) return; - this.init.accept(maxSounds, playbackSpeed); - this.initialized = true; - } - - public int getMaxSounds() { - if (!this.initialized) return 0; - return this.maxSounds.getAsInt(); - } - - public int getSoundCount() { - if (!this.initialized) return 0; - return this.soundCount.getAsInt(); - } - - public void stopSounds() { - if (!this.initialized) return; - this.stopSounds.run(); - } - - public void destroy() { - if (!this.initialized) return; - this.destroy.run(); - this.initialized = false; - } + this.soundSystem.writeSamples(); } }