From 56e524c9ad079e62770b10039a0144170249e1a9 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Sun, 25 Feb 2024 16:05:38 -0600 Subject: [PATCH] Stabilize music playback and spurious errors --- .../src/main/java/jace/apple2e/Apple2e.java | 2 +- .../src/main/java/jace/core/SoundMixer.java | 100 ++++++++++-------- .../main/java/jace/lawless/LawlessHacks.java | 47 +++++--- .../main/java/jace/lawless/MediaPlayer.java | 30 +++--- .../src/test/java/jace/core/SoundTest.java | 25 ++++- 5 files changed, 130 insertions(+), 74 deletions(-) diff --git a/Platform/Apple/tools/jace/src/main/java/jace/apple2e/Apple2e.java b/Platform/Apple/tools/jace/src/main/java/jace/apple2e/Apple2e.java index 6e8f43f2..1d5ced8c 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/apple2e/Apple2e.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/apple2e/Apple2e.java @@ -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; diff --git a/Platform/Apple/tools/jace/src/main/java/jace/core/SoundMixer.java b/Platform/Apple/tools/jace/src/main/java/jace/core/SoundMixer.java index 25b5fa94..223ed8b7 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/core/SoundMixer.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/core/SoundMixer.java @@ -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 performSoundFunction(Callable operation) throws SoundError { - return performSoundFunction(operation, false); + public static T performSoundFunction(Callable operation, String action) throws SoundError { + return performSoundFunction(operation, action, false); } - public static T performSoundFunction(Callable operation, boolean ignoreError) throws SoundError { + public static T performSoundFunction(Callable operation, String action, boolean ignoreError) throws SoundError { Future result = soundThreadExecutor.submit(operation); - try { - if (!ignoreError) { - Future error = soundThreadExecutor.submit(AL10::alGetError); - int err; - err = error.get(); - if (err != AL10.AL_NO_ERROR) { - throw new SoundError(AL10.alGetString(err)); - } + try { + Future 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 } diff --git a/Platform/Apple/tools/jace/src/main/java/jace/lawless/LawlessHacks.java b/Platform/Apple/tools/jace/src/main/java/jace/lawless/LawlessHacks.java index e513daa2..0b834ed6 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/lawless/LawlessHacks.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/lawless/LawlessHacks.java @@ -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); } } }); diff --git a/Platform/Apple/tools/jace/src/main/java/jace/lawless/MediaPlayer.java b/Platform/Apple/tools/jace/src/main/java/jace/lawless/MediaPlayer.java index 00d1f370..7c02d843 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/lawless/MediaPlayer.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/lawless/MediaPlayer.java @@ -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; } }); } diff --git a/Platform/Apple/tools/jace/src/test/java/jace/core/SoundTest.java b/Platform/Apple/tools/jace/src/test/java/jace/core/SoundTest.java index d3169a07..a9f0be6c 100644 --- a/Platform/Apple/tools/jace/src/test/java/jace/core/SoundTest.java +++ b/Platform/Apple/tools/jace/src/test/java/jace/core/SoundTest.java @@ -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); + } + } }