mirror of
https://github.com/badvision/lawless-legends.git
synced 2025-01-18 19:31:49 +00:00
Update memory allocation for media playback and added load/tortute tests for sound routines
This commit is contained in:
parent
a3e9a44254
commit
461e6ced00
@ -20,6 +20,7 @@ package jace.core;
|
|||||||
|
|
||||||
import java.nio.ShortBuffer;
|
import java.nio.ShortBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@ -177,7 +178,7 @@ public class SoundMixer extends Device {
|
|||||||
PLAYBACK_ENABLED = true;
|
PLAYBACK_ENABLED = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<SoundBuffer> buffers = new ArrayList<>();
|
private static List<SoundBuffer> buffers = Collections.synchronizedList(new ArrayList<>());
|
||||||
public static SoundBuffer createBuffer(boolean stereo) throws InterruptedException, ExecutionException, SoundError {
|
public static SoundBuffer createBuffer(boolean stereo) throws InterruptedException, ExecutionException, SoundError {
|
||||||
if (!PLAYBACK_ENABLED) {
|
if (!PLAYBACK_ENABLED) {
|
||||||
System.err.println("Sound playback not enabled, buffer not created.");
|
System.err.println("Sound playback not enabled, buffer not created.");
|
||||||
@ -231,46 +232,7 @@ public class SoundMixer extends Device {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentBuffer.hasRemaining()) {
|
if (!currentBuffer.hasRemaining()) {
|
||||||
buffersGenerated++;
|
this.flush();
|
||||||
currentBuffer.flip();
|
|
||||||
if (buffersGenerated > 2) {
|
|
||||||
int[] unqueueBuffers = new int[]{currentBufferId};
|
|
||||||
performSoundOperation(()->{
|
|
||||||
int buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
|
|
||||||
while (buffersProcessed < 1) {
|
|
||||||
Thread.onSpinWait();
|
|
||||||
buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!isAlive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
performSoundOperation(()->{
|
|
||||||
AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!isAlive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE));
|
|
||||||
if (!isAlive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId));
|
|
||||||
performSoundOperationAsync(()->{
|
|
||||||
if (AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
|
|
||||||
AL10.alSourcePlay(sourceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swap AL buffers
|
|
||||||
int tempId = currentBufferId;
|
|
||||||
currentBufferId = alternateBufferId;
|
|
||||||
alternateBufferId = tempId;
|
|
||||||
// Swap Java buffers
|
|
||||||
ShortBuffer tempBuffer = currentBuffer;
|
|
||||||
currentBuffer = alternateBuffer;
|
|
||||||
alternateBuffer = tempBuffer;
|
|
||||||
}
|
}
|
||||||
currentBuffer.put(sample);
|
currentBuffer.put(sample);
|
||||||
}
|
}
|
||||||
@ -302,6 +264,53 @@ public class SoundMixer extends Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void flush() throws SoundError {
|
||||||
|
buffersGenerated++;
|
||||||
|
currentBuffer.flip();
|
||||||
|
if (buffersGenerated > 2) {
|
||||||
|
int[] unqueueBuffers = new int[]{currentBufferId};
|
||||||
|
performSoundOperation(()->{
|
||||||
|
int buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
|
||||||
|
while (buffersProcessed < 1) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performSoundOperation(()->{
|
||||||
|
AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE));
|
||||||
|
if (!isAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId));
|
||||||
|
performSoundOperationAsync(()->{
|
||||||
|
if (AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
|
||||||
|
AL10.alSourcePlay(sourceId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swap AL buffers
|
||||||
|
int tempId = currentBufferId;
|
||||||
|
currentBufferId = alternateBufferId;
|
||||||
|
alternateBufferId = tempId;
|
||||||
|
// Swap Java buffers
|
||||||
|
ShortBuffer tempBuffer = currentBuffer;
|
||||||
|
currentBuffer = alternateBuffer;
|
||||||
|
alternateBuffer = tempBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveBuffers() {
|
||||||
|
return buffers.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -313,7 +322,7 @@ public class SoundMixer extends Device {
|
|||||||
PLAYBACK_ENABLED = false;
|
PLAYBACK_ENABLED = false;
|
||||||
|
|
||||||
while (!buffers.isEmpty()) {
|
while (!buffers.isEmpty()) {
|
||||||
SoundBuffer buffer = buffers.get(0);
|
SoundBuffer buffer = buffers.remove(0);
|
||||||
try {
|
try {
|
||||||
buffer.shutdown();
|
buffer.shutdown();
|
||||||
} catch (InterruptedException | ExecutionException | SoundError e) {
|
} catch (InterruptedException | ExecutionException | SoundError e) {
|
||||||
@ -342,7 +351,6 @@ public class SoundMixer extends Device {
|
|||||||
return "mixer";
|
return "mixer";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void reconfigure() {
|
public synchronized void reconfigure() {
|
||||||
PLAYBACK_ENABLED = PLAYBACK_DRIVER_DETECTED && !MUTE;
|
PLAYBACK_ENABLED = PLAYBACK_DRIVER_DETECTED && !MUTE;
|
||||||
|
@ -29,8 +29,11 @@ public class Media {
|
|||||||
oggFile = oggStream.readAllBytes();
|
oggFile = oggStream.readAllBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ByteBuffer oggBuffer = null;
|
||||||
|
STBVorbisInfo info = null;
|
||||||
|
ShortBuffer tempSampleBuffer = null;
|
||||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||||
ByteBuffer oggBuffer = MemoryUtil.memAlloc(oggFile.length);
|
oggBuffer = MemoryUtil.memAlloc(oggFile.length);
|
||||||
oggBuffer.put(oggFile);
|
oggBuffer.put(oggFile);
|
||||||
oggBuffer.flip();
|
oggBuffer.flip();
|
||||||
IntBuffer error = stack.callocInt(1);
|
IntBuffer error = stack.callocInt(1);
|
||||||
@ -38,19 +41,28 @@ public class Media {
|
|||||||
if (decoder == null || decoder <= 0) {
|
if (decoder == null || decoder <= 0) {
|
||||||
throw new RuntimeException("Failed to open Ogg Vorbis file. Error: " + getError(error.get(0)) + " -- file is located at " + resourcePath);
|
throw new RuntimeException("Failed to open Ogg Vorbis file. Error: " + getError(error.get(0)) + " -- file is located at " + resourcePath);
|
||||||
}
|
}
|
||||||
STBVorbisInfo info = STBVorbisInfo.malloc(stack);
|
info = STBVorbisInfo.malloc(stack);
|
||||||
STBVorbis.stb_vorbis_get_info(decoder, info);
|
STBVorbis.stb_vorbis_get_info(decoder, info);
|
||||||
totalSamples = STBVorbis.stb_vorbis_stream_length_in_samples(decoder);
|
totalSamples = STBVorbis.stb_vorbis_stream_length_in_samples(decoder);
|
||||||
totalDuration = STBVorbis.stb_vorbis_stream_length_in_seconds(decoder);
|
totalDuration = STBVorbis.stb_vorbis_stream_length_in_seconds(decoder);
|
||||||
sampleRate = info.sample_rate();
|
sampleRate = info.sample_rate();
|
||||||
isStereo = info.channels() == 2;
|
isStereo = info.channels() == 2;
|
||||||
|
|
||||||
sampleBuffer = MemoryUtil.memAllocShort(totalSamples);
|
tempSampleBuffer = MemoryUtil.memAllocShort(totalSamples);
|
||||||
STBVorbis.stb_vorbis_get_samples_short_interleaved(decoder, isStereo?2:1, sampleBuffer);
|
STBVorbis.stb_vorbis_get_samples_short_interleaved(decoder, isStereo?2:1, tempSampleBuffer);
|
||||||
STBVorbis.stb_vorbis_close(decoder);
|
STBVorbis.stb_vorbis_close(decoder);
|
||||||
|
tempSampleBuffer.rewind();
|
||||||
|
// copy sample buffer into byte buffer so we can deallocate, then transfer the buffer contents
|
||||||
|
sampleBuffer = ShortBuffer.allocate(totalSamples*2);
|
||||||
|
sampleBuffer.put(tempSampleBuffer);
|
||||||
sampleBuffer.rewind();
|
sampleBuffer.rewind();
|
||||||
} catch (RuntimeException ex) {
|
} catch (RuntimeException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
|
} finally {
|
||||||
|
if (oggBuffer != null)
|
||||||
|
MemoryUtil.memFree(oggBuffer);
|
||||||
|
if (tempSampleBuffer != null)
|
||||||
|
MemoryUtil.memFree(tempSampleBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +116,8 @@ public class Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
MemoryUtil.memFree(sampleBuffer);
|
if (sampleBuffer != null)
|
||||||
|
sampleBuffer.clear();
|
||||||
if (tempFile != null && tempFile.exists())
|
if (tempFile != null && tempFile.exists())
|
||||||
tempFile.delete();
|
tempFile.delete();
|
||||||
}
|
}
|
||||||
@ -146,4 +159,12 @@ public class Media {
|
|||||||
public float getTotalDuration() {
|
public float getTotalDuration() {
|
||||||
return totalDuration;
|
return totalDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTotalSamples() {
|
||||||
|
return totalSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSampleRate() {
|
||||||
|
return sampleRate;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package jace.core;
|
package jace.core;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -11,19 +13,31 @@ import jace.lawless.LawlessHacks;
|
|||||||
import jace.lawless.Media;
|
import jace.lawless.Media;
|
||||||
|
|
||||||
public class SoundTest extends AbstractFXTest {
|
public class SoundTest extends AbstractFXTest {
|
||||||
// @Test (commented out because it takes a while to run)
|
@Test
|
||||||
public void musicDecodeTest() {
|
public void musicDecodeTest() {
|
||||||
// For every song in the music folder, decode it and print out the duration
|
// For every song in the music folder, decode it and print out the duration
|
||||||
// This is to make sure that the decoding is working properly
|
// This is to make sure that the decoding is working properly and that
|
||||||
|
// we don't have allocation/deallocation issues
|
||||||
LawlessHacks lawless = new LawlessHacks();
|
LawlessHacks lawless = new LawlessHacks();
|
||||||
for (String score : lawless.scores.keySet()) {
|
// Note: This passed 1000 iterations of the test, so it's probably safe to assume there's no obvious memory leaks
|
||||||
lawless.changeMusicScore(score);
|
// for (int repeat = 0; repeat < 1000; repeat++) {
|
||||||
for (int track : lawless.scores.get(score).keySet()) {
|
for (String score : lawless.scores.keySet()) {
|
||||||
System.out.println("Loading score %s, track %d".formatted(score, track));
|
lawless.changeMusicScore(score);
|
||||||
Media m = lawless.getAudioTrack(track);
|
for (int track : lawless.scores.get(score).keySet()) {
|
||||||
System.out.println("Duration: " + m.getTotalDuration());
|
System.out.println("Loading score %s, track %d".formatted(score, track));
|
||||||
|
Media m = lawless.getAudioTrack(track);
|
||||||
|
System.out.println("Duration: " + m.getTotalDuration());
|
||||||
|
int count = 0;
|
||||||
|
while (!m.isEnded()) {
|
||||||
|
count++;
|
||||||
|
m.getNextLeftSample();
|
||||||
|
m.getNextRightSample();
|
||||||
|
}
|
||||||
|
assertEquals("Should read an expected number of samples from the song (%s), counted %s".formatted(m.getTotalSamples(), count), m.getTotalSamples(), count);
|
||||||
|
m.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
@ -52,13 +66,33 @@ public class SoundTest extends AbstractFXTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void speakerTickTest() throws SoundError {
|
// Commented out because it's annoying to hear all the time, but it worked without issues
|
||||||
|
public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException {
|
||||||
System.out.println("Performing speaker tick test...");
|
System.out.println("Performing speaker tick test...");
|
||||||
|
SoundMixer.initSound();
|
||||||
System.out.println("Create mixer");
|
System.out.println("Create mixer");
|
||||||
SoundMixer mixer = new SoundMixer();
|
SoundMixer mixer = new SoundMixer();
|
||||||
System.out.println("Attach mixer");
|
System.out.println("Attach mixer");
|
||||||
mixer.attach();
|
mixer.attach();
|
||||||
|
// We want to create and destroy lots of buffers to make sure we don't have any memory leaks
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
// Print status every 1000 iterations
|
||||||
|
if (i % 1000 == 0) {
|
||||||
|
System.out.println("Iteration %d".formatted(i));
|
||||||
|
}
|
||||||
|
SoundBuffer buffer = SoundMixer.createBuffer(false);
|
||||||
|
for (int j = 0; j < SoundMixer.BUFFER_SIZE*2; j++) {
|
||||||
|
// Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed
|
||||||
|
double x = Math.sin(j*j * 0.0001);
|
||||||
|
buffer.playSample((short) (Short.MAX_VALUE * x));
|
||||||
|
}
|
||||||
|
buffer.flush();
|
||||||
|
buffer.shutdown();
|
||||||
|
}
|
||||||
|
// Assert buffers are empty
|
||||||
|
assertEquals("All buffers should be empty", 0, mixer.getActiveBuffers());
|
||||||
|
System.out.println("Deactivating sound");
|
||||||
|
mixer.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user