Update memory allocation for media playback and added load/tortute tests for sound routines

This commit is contained in:
Brendan Robert 2024-02-12 13:33:22 -06:00
parent a3e9a44254
commit 461e6ced00
3 changed files with 123 additions and 60 deletions

View File

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

View File

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

View File

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