Stabilize music playback and spurious errors

This commit is contained in:
Brendan Robert 2024-02-25 16:05:38 -06:00
parent 9632af4142
commit 56e524c9ad
5 changed files with 130 additions and 74 deletions

View File

@ -94,7 +94,7 @@ public class Apple2e extends Computer {
@ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true)
public boolean acceleratorEnabled = true;
@ConfigurableField(name = "Production mode", shortName = "production")
public boolean PRODUCTION_MODE = false;
public boolean PRODUCTION_MODE = true;
public Joystick joystick1;
public Joystick joystick2;

View File

@ -16,6 +16,7 @@
package jace.core;
import java.nio.BufferOverflowException;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Collections;
@ -48,6 +49,7 @@ import jace.config.ConfigurableField;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class SoundMixer extends Device {
public static boolean DEBUG_SOUND = false;
/**
* Bits per sample
@ -92,44 +94,45 @@ public class SoundMixer extends Device {
}
}
public static <T> T performSoundFunction(Callable<T> operation) throws SoundError {
return performSoundFunction(operation, false);
public static <T> T performSoundFunction(Callable<T> operation, String action) throws SoundError {
return performSoundFunction(operation, action, false);
}
public static <T> T performSoundFunction(Callable<T> operation, boolean ignoreError) throws SoundError {
public static <T> T performSoundFunction(Callable<T> operation, String action, boolean ignoreError) throws SoundError {
Future<T> result = soundThreadExecutor.submit(operation);
try {
if (!ignoreError) {
Future<Integer> error = soundThreadExecutor.submit(AL10::alGetError);
int err;
err = error.get();
if (err != AL10.AL_NO_ERROR) {
throw new SoundError(AL10.alGetString(err));
}
try {
Future<Integer> error = soundThreadExecutor.submit(AL10::alGetError);
int err;
err = error.get();
if (!ignoreError && DEBUG_SOUND) {
if (err != AL10.AL_NO_ERROR) {
System.err.println(">>>SOUND ERROR " + AL10.alGetString(err) + " when performing action: " + action);
// throw new SoundError(AL10.alGetString(err));
}
return result.get();
} catch (ExecutionException e) {
System.out.println("Error when executing sound action: " + e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
// Do nothing: sound is probably being reset
}
return result.get();
} catch (ExecutionException e) {
System.out.println("Error when executing sound action: " + e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
// Do nothing: sound is probably being reset
}
return null;
}
public static void performSoundOperation(Runnable operation) throws SoundError {
performSoundOperation(operation, false);
public static void performSoundOperation(Runnable operation, String action) throws SoundError {
performSoundOperation(operation, action, false);
}
public static void performSoundOperation(Runnable operation, boolean ignoreError) throws SoundError {
public static void performSoundOperation(Runnable operation, String action, boolean ignoreError) throws SoundError {
performSoundFunction(()->{
operation.run();
return null;
}, ignoreError);
}, action, ignoreError);
}
public static void performSoundOperationAsync(Runnable operation) {
soundThreadExecutor.submit(operation);
public static void performSoundOperationAsync(Runnable operation, String action) {
soundThreadExecutor.submit(operation, action);
}
protected static void initSound() {
@ -153,7 +156,7 @@ public class SoundMixer extends Device {
} else {
ALC10.alcMakeContextCurrent(audioContext);
}
});
}, "Initalize audio device");
} catch (SoundError e) {
PLAYBACK_DRIVER_DETECTED = false;
Logger.getLogger(SoundMixer.class.getName()).warning("Error when initializing sound: " + e.getMessage());
@ -203,13 +206,14 @@ public class SoundMixer extends Device {
currentBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
alternateBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
try {
currentBufferId = performSoundFunction(AL10::alGenBuffers);
alternateBufferId = performSoundFunction(AL10::alGenBuffers);
currentBufferId = performSoundFunction(AL10::alGenBuffers, "Initalize sound buffer: primary");
alternateBufferId = performSoundFunction(AL10::alGenBuffers, "Initalize sound buffer: alternate");
boolean hasSource = false;
while (!hasSource) {
sourceId = performSoundFunction(AL10::alGenSources);
hasSource = performSoundFunction(()->AL10.alIsSource(sourceId));
sourceId = performSoundFunction(AL10::alGenSources, "Initalize sound buffer: create source");
hasSource = performSoundFunction(()->AL10.alIsSource(sourceId), "Initalize sound buffer: Check if source is valid");
}
performSoundOperation(()->AL10.alSourcei(sourceId, AL10.AL_LOOPING, AL10.AL_FALSE), "Set looping to false");
} catch (SoundError e) {
Logger.getLogger(SoundMixer.class.getName()).warning("Error when creating sound buffer: " + e.getMessage());
Thread.dumpStack();
@ -232,7 +236,15 @@ public class SoundMixer extends Device {
if (!currentBuffer.hasRemaining()) {
this.flush();
}
currentBuffer.put(sample);
try {
currentBuffer.put(sample);
} catch (BufferOverflowException e) {
if (DEBUG_SOUND) {
System.err.println("Buffer overflow, trying to compensate");
}
currentBuffer.clear();
currentBuffer.put(sample);
}
}
public void shutdown() throws InterruptedException, ExecutionException, SoundError {
@ -242,18 +254,18 @@ public class SoundMixer extends Device {
isAlive = false;
try {
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alSourceStop(sourceId);});
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alSourceStop(sourceId);}, "Shutdown: stop source");
} finally {
try {
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alDeleteSources(sourceId);});
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alDeleteSources(sourceId);}, "Shutdown: delete source");
} finally {
sourceId = -1;
try {
performSoundOperation(()->{if (AL10.alIsBuffer(alternateBufferId)) AL10.alDeleteBuffers(alternateBufferId);});
performSoundOperation(()->{if (AL10.alIsBuffer(alternateBufferId)) AL10.alDeleteBuffers(alternateBufferId);}, "Shutdown: delete buffer 1");
} finally {
alternateBufferId = -1;
try {
performSoundOperation(()->{if (AL10.alIsBuffer(currentBufferId)) AL10.alDeleteBuffers(currentBufferId);});
performSoundOperation(()->{if (AL10.alIsBuffer(currentBufferId)) AL10.alDeleteBuffers(currentBufferId);}, "Shutdown: delete buffer 2");
} finally {
currentBufferId = -1;
buffers.remove(this);
@ -265,7 +277,6 @@ public class SoundMixer extends Device {
public void flush() throws SoundError {
buffersGenerated++;
currentBuffer.flip();
if (buffersGenerated > 2) {
int[] unqueueBuffers = new int[]{currentBufferId};
performSoundOperation(()->{
@ -274,27 +285,30 @@ public class SoundMixer extends Device {
Thread.onSpinWait();
buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
}
});
}, "Flush: wait for buffers to finish playing");
if (!isAlive) {
return;
}
performSoundOperation(()->{
AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers);
});
// TODO: Figure out why we get Invalid Value on a new buffer
performSoundOperation(()->AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers), "Flush: unqueue buffers");
}
if (!isAlive) {
return;
}
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE));
// TODO: Figure out why we get Invalid Operation error on a new buffer after unqueue reports Invalid Value
currentBuffer.flip();
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE), "Flush: buffer data");
currentBuffer.clear();
if (!isAlive) {
return;
}
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId));
// TODO: Figure out why we get Invalid Operation error on a new buffer after unqueue reports Invalid Value
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId), "Flush: queue buffer");
performSoundOperationAsync(()->{
if (AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
AL10.alSourcePlay(sourceId);
}
});
}, "Flush: Start playing buffer");
// Swap AL buffers
int tempId = currentBufferId;
@ -330,8 +344,8 @@ public class SoundMixer extends Device {
buffers.clear();
PLAYBACK_INITIALIZED = false;
try {
performSoundOperation(()->ALC10.alcDestroyContext(audioContext), true);
performSoundOperation(()->ALC10.alcCloseDevice(audioDevice), true);
performSoundOperation(()->ALC10.alcDestroyContext(audioContext), "Detach: destroy context", true);
performSoundOperation(()->ALC10.alcCloseDevice(audioDevice), "Detach: close device", true);
} catch (SoundError e) {
// Shouldn't throw but have to catch anyway
}

View File

@ -25,7 +25,7 @@ import javafx.util.Duration;
* Hacks that affect lawless legends gameplay
*/
public class LawlessHacks extends Cheats {
boolean DEBUG = false;
// Modes specified by the game engine
int MODE_SOFTSWITCH_MIN = 0x0C049;
int MODE_SOFTSWITCH_MAX = 0x0C04F;
@ -87,14 +87,17 @@ public class LawlessHacks extends Cheats {
private static boolean repeatSong = false;
private static Thread playbackEffect;
private static MediaPlayer currentSongPlayer;
private static MediaPlayer previousSongPlayer;
private static MediaPlayer currentSfxPlayer;
private static String currentScore = SCORE_COMMON;
private void playSound(int soundNumber) {
public void playSound(int soundNumber) {
boolean isMusic = soundNumber >= 0;
int track = soundNumber & 0x03f;
repeatSong = (soundNumber & 0x040) > 0;
// System.out.println("(invoked sound on "+getName()+")");
if (DEBUG) {
System.out.println("Play sound " + soundNumber + " (track " + track + "; repeat " + repeatSong + ") invoked on " + getName());
}
if (track == 0) {
if (isMusic) {
System.out.println("Stop music");
@ -185,9 +188,9 @@ public class LawlessHacks extends Cheats {
MediaPlayer player = currentSongPlayer;
if (player != null) {
getCurrentTime().ifPresent(val -> lastTime.put(currentSong, val + 1500));
playbackEffect = new Thread(() -> {
Thread effect = new Thread(() -> {
while (playbackEffect == Thread.currentThread() && player.getVolume() > 0.0) {
player.setVolume(player.getVolume() - FADE_AMT);
player.setVolume(Math.max(player.getVolume() - FADE_AMT, 0.0));
try {
Thread.sleep(FADE_SPEED);
} catch (InterruptedException e) {
@ -203,7 +206,8 @@ public class LawlessHacks extends Cheats {
nextAction.run();
}
});
playbackEffect.start();
playbackEffect = effect;
effect.start();
} else if (nextAction != null) {
new Thread(nextAction).start();
}
@ -211,14 +215,18 @@ public class LawlessHacks extends Cheats {
private void fadeInSong(MediaPlayer player) {
stopSongEffect();
if (previousSongPlayer != null) {
previousSongPlayer.stop();
}
previousSongPlayer = currentSongPlayer;
currentSongPlayer = player;
if (player.getVolume() >= 1.0) {
return;
}
playbackEffect = new Thread(() -> {
Thread effect = new Thread(() -> {
while (playbackEffect == Thread.currentThread() && player.getVolume() < 1.0) {
player.setVolume(player.getVolume() + FADE_AMT);
player.setVolume(Math.min(player.getVolume() + FADE_AMT, 1.0));
try {
Thread.sleep(FADE_SPEED);
} catch (InterruptedException e) {
@ -227,7 +235,8 @@ public class LawlessHacks extends Cheats {
}
}
});
playbackEffect.start();
playbackEffect = effect;
effect.start();
}
double FADE_AMT = 0.05; // 5% per interval, or 20 stops between 0% and 100%
@ -240,10 +249,12 @@ public class LawlessHacks extends Cheats {
return;
}
MediaPlayer player;
// If the same song is already playing don't restart it
if (track != currentSong || !isPlayingMusic() || switchScores) {
System.out.println("Play music "+track);
// If the same song is already playing don't restart it
if (DEBUG) {
System.out.println("Start new song " + track + " (switch " + switchScores + ")");
}
Media song = getAudioTrack(track);
if (song == null) {
System.out.println("Unable to start song " + track + "; File " + getSongName(track) + " not found");
@ -334,11 +345,13 @@ public class LawlessHacks extends Cheats {
line = line.replace("*", "");
}
if (COMMENT.matcher(line).matches() || line.trim().isEmpty()) {
// System.out.println("Ignoring: "+line);
if (DEBUG)
System.out.println("Ignoring: "+line);
} else if (LABEL.matcher(line).matches()) {
currentScore = line.toLowerCase(Locale.ROOT);
scores.put(currentScore, new HashMap<>());
// System.out.println("Score: "+ currentScore);
if (DEBUG)
System.out.println("Score: "+ currentScore);
} else {
Matcher m = ENTRY.matcher(line);
if (m.matches()) {
@ -348,9 +361,11 @@ public class LawlessHacks extends Cheats {
if (useAutoResume) {
autoResume.add(num);
}
// System.out.println("Score: " + currentScore + "; Song: " + num + "; " + file);
if (DEBUG)
System.out.println("Score: " + currentScore + "; Song: " + num + "; " + file);
} else {
// System.out.println("Couldn't parse: " + line);
if (DEBUG)
System.out.println("Couldn't parse: " + line);
}
}
});

View File

@ -15,7 +15,7 @@ public class MediaPlayer {
int repeats = 0;
int maxRepetitions = 1;
Status status = Status.NOT_STARTED;
Media song;
Media soundData;
SoundBuffer playbackBuffer;
Executor executor = Executors.newSingleThreadExecutor();
@ -26,7 +26,7 @@ public class MediaPlayer {
public static final int INDEFINITE = -1;
public MediaPlayer(Media song) {
this.song = song;
this.soundData = song;
}
public Status getStatus() {
@ -34,7 +34,7 @@ public class MediaPlayer {
}
public Duration getCurrentTime() {
return song.getCurrentTime();
return soundData.getCurrentTime();
}
public double getVolume() {
@ -48,11 +48,15 @@ public class MediaPlayer {
if (playbackBuffer != null) {
playbackBuffer.flush();
playbackBuffer.shutdown();
playbackBuffer = null;
}
} catch (InterruptedException | ExecutionException | SoundError e) {
// Ignore exception on shutdown
} finally {
song.close();
if (soundData != null) {
soundData.close();
}
soundData = null;
}
}
@ -65,7 +69,7 @@ public class MediaPlayer {
}
public void setStartTime(javafx.util.Duration millis) {
song.seekToTime(millis);
soundData.seekToTime(millis);
}
public void pause() {
@ -92,15 +96,16 @@ public class MediaPlayer {
}
executor.execute(() -> {
status = Status.PLAYING;
System.out.println("Song playback thread started");
while (status == Status.PLAYING && (maxRepetitions == INDEFINITE || repeats < maxRepetitions)) {
if (song.isEnded()) {
// System.out.println("Song playback thread started");
Media theSoundData = soundData;
while (status == Status.PLAYING && (maxRepetitions == INDEFINITE || repeats < maxRepetitions) && theSoundData != null) {
if (theSoundData.isEnded()) {
if (maxRepetitions == INDEFINITE) {
song.restart();
theSoundData.restart();
} else {
repeats++;
if (repeats < maxRepetitions) {
song.restart();
theSoundData.restart();
} else {
System.out.println("Song ended");
this.stop();
@ -109,12 +114,13 @@ public class MediaPlayer {
}
}
try {
playbackBuffer.playSample((short) (song.getNextLeftSample() * vol));
playbackBuffer.playSample((short) (song.getNextRightSample() * vol));
playbackBuffer.playSample((short) (theSoundData.getNextLeftSample() * vol));
playbackBuffer.playSample((short) (theSoundData.getNextRightSample() * vol));
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
this.stop();
}
theSoundData = soundData;
}
});
}

View File

@ -2,6 +2,7 @@ package jace.core;
import static org.junit.Assert.assertEquals;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
@ -13,7 +14,7 @@ import jace.lawless.LawlessHacks;
import jace.lawless.Media;
public class SoundTest extends AbstractFXTest {
@Test
// @Test
public void musicDecodeTest() {
// 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 and that
@ -66,7 +67,7 @@ public class SoundTest extends AbstractFXTest {
}
}
@Test
// @Test
// 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...");
@ -95,4 +96,24 @@ public class SoundTest extends AbstractFXTest {
System.out.println("Deactivating sound");
mixer.detach();
}
@Test
public void musicPlaybackTortureTest() throws InterruptedException {
SoundMixer.initSound();
System.out.println("Create mixer");
SoundMixer mixer = new SoundMixer();
System.out.println("Attach mixer");
mixer.attach();
LawlessHacks lawlessHacks = new LawlessHacks();
int track = 0;
Random rnd = new Random();
for (int i=0; i < 500; i++) {
System.out.println(">>>>>>>>> Cycle " + i);
// Get a random song
// track = rnd.nextInt(20);
track = (track + 1) % 20;
lawlessHacks.playSound(track);
Thread.sleep(1000);
}
}
}