Skip to content

Commit

Permalink
Added loopback capture to OpenALSoundSystem
Browse files Browse the repository at this point in the history
  • Loading branch information
RaphiMC committed May 7, 2024
1 parent f32c9ff commit 1a9230f
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ public static void playNote(final Instrument instrument, final float volume, fin
}
}

public static int getPlayingSounds() {
return PLAYING_SOUNDS.size();
}

public static void stopAllSounds() {
final List<Clip> playingSounds = new ArrayList<>(PLAYING_SOUNDS);
PLAYING_SOUNDS.clear();
Expand All @@ -111,6 +107,10 @@ public static void destroy() {
SOUNDS.clear();
}

public static int getPlayingSounds() {
return PLAYING_SOUNDS.size();
}

private static void tick() {
PLAYING_SOUNDS.removeIf(clip -> {
if (clip.isRunning()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.EnumMap;
Expand All @@ -40,18 +41,48 @@ public class OpenALSoundSystem {
private static final Map<Instrument, Integer> INSTRUMENT_BUFFERS = new EnumMap<>(Instrument.class);
private static final List<Integer> 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 init(final int maxSounds) {
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");

Expand All @@ -62,9 +93,7 @@ public static void init(final int maxSounds) {
throw new RuntimeException("ALC_SOFT_output_limiter is not supported");
}

// TODO: Loopback support (for audio export)

CONTEXT = ALC10.alcCreateContext(DEVICE, new int[]{ALC11.ALC_MONO_SOURCES, MAX_MONO_SOURCES, SOFTOutputLimiter.ALC_OUTPUT_LIMITER_SOFT, 1, 0});
CONTEXT = ALC10.alcCreateContext(DEVICE, attributes);
checkError("Could not create context");
if (!ALC10.alcMakeContextCurrent(CONTEXT)) {
throw new RuntimeException("Could not make context current");
Expand Down Expand Up @@ -134,6 +163,28 @@ public static void playNote(final Instrument instrument, final float volume, fin
}
}

public static void renderSamples(final SampleOutputStream outputStream, final int sampleCount) throws IOException {
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);
Expand Down Expand Up @@ -164,6 +215,10 @@ public static void destroy() {
ALC10.alcCloseDevice(DEVICE);
DEVICE = 0L;
}
if (CAPTURE_BUFFER != null) {
MemoryUtil.memFree(CAPTURE_BUFFER);
CAPTURE_BUFFER = null;
}
}

public static void setMasterVolume(final float volume) {
Expand Down Expand Up @@ -226,7 +281,33 @@ private static int getAlAudioFormat(final AudioFormat audioFormat) {
}
}

throw new RuntimeException("Unsupported audio format: " + audioFormat);
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package net.raphimc.noteblocktool.audio;

import javax.sound.sampled.AudioFormat;
import java.io.IOException;
import java.io.OutputStream;

public class SampleOutputStream extends OutputStream {

private final OutputStream outputStream;
private final AudioFormat audioFormat;

public SampleOutputStream(final OutputStream outputStream, final AudioFormat audioFormat) {
if (audioFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED && audioFormat.getEncoding() != AudioFormat.Encoding.PCM_UNSIGNED) {
throw new IllegalArgumentException("Unsupported audio format: " + audioFormat);
}
this.outputStream = outputStream;
this.audioFormat = audioFormat;
}

@Override
public void write(final int b) throws IOException {
this.outputStream.write(b);
}

public void writeSample(final int sample) throws IOException {
switch (this.audioFormat.getSampleSizeInBits()) {
case 8:
this.write(sample);
break;
case 16:
this.write16Bit(sample);
break;
case 32:
this.write32Bit(sample);
break;
default:
throw new UnsupportedOperationException("Unsupported sample size: " + this.audioFormat.getSampleSizeInBits());
}
}

private void write16Bit(final int sample) throws IOException {
if (this.audioFormat.isBigEndian()) {
this.write((sample >> 8) & 0xFF);
this.write(sample & 0xFF);
} else {
this.write(sample & 0xFF);
this.write((sample >> 8) & 0xFF);
}
}

private void write32Bit(final int sample) throws IOException {
if (this.audioFormat.isBigEndian()) {
this.write((sample >> 24) & 0xFF);
this.write((sample >> 16) & 0xFF);
this.write((sample >> 8) & 0xFF);
this.write(sample & 0xFF);
} else {
this.write(sample & 0xFF);
this.write((sample >> 8) & 0xFF);
this.write((sample >> 16) & 0xFF);
this.write((sample >> 24) & 0xFF);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public void playNote(Note note) {


private enum SoundSystem {
OPENAL("OpenAL", OpenALSoundSystem::init, OpenALSoundSystem::getMaxMonoSources, OpenALSoundSystem::getPlayingSources, OpenALSoundSystem::stopAllSources, OpenALSoundSystem::destroy),
OPENAL("OpenAL", OpenALSoundSystem::initPlayback, OpenALSoundSystem::getMaxMonoSources, OpenALSoundSystem::getPlayingSources, OpenALSoundSystem::stopAllSources, OpenALSoundSystem::destroy),
JAVAX("Javax", JavaxSoundSystem::init, JavaxSoundSystem::getMaxSounds, JavaxSoundSystem::getPlayingSounds, JavaxSoundSystem::stopAllSounds, JavaxSoundSystem::destroy);

private final String name;
Expand Down

0 comments on commit 1a9230f

Please sign in to comment.