Stabilize music playback and spurious errors
This commit is contained in:
parent
9632af4142
commit
56e524c9ad
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue