Skip to content

Commit

Permalink
(#23) replaced dependency on javax.sound line events with self-sent e…
Browse files Browse the repository at this point in the history
…vents
  • Loading branch information
lucasstarsz committed Jul 10, 2021
1 parent 583ae92 commit bc50ef6
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 12 deletions.
21 changes: 10 additions & 11 deletions src/main/java/tech/fastj/systems/audio/AudioEventListener.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package tech.fastj.systems.audio;

import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

/** An event listener for the {@link MemoryAudio} class. */
public class AudioEventListener implements LineListener {
public class AudioEventListener {

private Runnable audioOpenAction;
private Runnable audioCloseAction;
Expand All @@ -20,12 +19,10 @@ public class AudioEventListener implements LineListener {

private static final Map<LineEvent.Type, Consumer<AudioEventListener>> AudioEventProcessor = Map.of(
LineEvent.Type.OPEN, audioEventListener -> audioEventListener.audioOpenAction.run(),
LineEvent.Type.CLOSE, audioEventListener -> audioEventListener.audioCloseAction.run(),
LineEvent.Type.START, audioEventListener -> {
switch (audioEventListener.audio.getPreviousPlaybackState()) {
case Paused: {
audioEventListener.audioResumeAction.run();
break;
}
case Stopped: {
audioEventListener.audioStartAction.run();
Expand All @@ -37,14 +34,14 @@ public class AudioEventListener implements LineListener {
switch (audioEventListener.audio.getCurrentPlaybackState()) {
case Paused: {
audioEventListener.audioPauseAction.run();
break;
}
case Stopped: {
audioEventListener.audioStopAction.run();
break;
}
}
}
},
LineEvent.Type.CLOSE, audioEventListener -> audioEventListener.audioCloseAction.run()
);

/**
Expand All @@ -55,7 +52,6 @@ public class AudioEventListener implements LineListener {
*/
AudioEventListener(Audio audio) {
this.audio = Objects.requireNonNull(audio);
this.audio.getAudioSource().addLineListener(this);
}

/**
Expand Down Expand Up @@ -166,9 +162,12 @@ public void setAudioResumeAction(Runnable audioResumeAction) {
this.audioResumeAction = audioResumeAction;
}

@Override
public void update(LineEvent event) {
LineEvent.Type audioEventType = event.getType();
AudioEventProcessor.get(audioEventType).accept(this);
/**
* Fires an audio event to the event listener.
*
* @param audioEvent The event fired.
*/
public void fireEvent(LineEvent audioEvent) {
AudioEventProcessor.get(audioEvent.getType()).accept(this);
}
}
10 changes: 10 additions & 0 deletions src/main/java/tech/fastj/systems/audio/AudioManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** The manager of all audio-based content. */
public class AudioManager {

private static final Map<String, MemoryAudio> MemoryAudioFiles = new HashMap<>();
private static final Map<String, StreamedAudio> StreamedAudioFiles = new HashMap<>();
private static ExecutorService audioEventExecutor = Executors.newWorkStealingPool();

/**
* Checks whether the computer supports audio output.
Expand Down Expand Up @@ -178,6 +181,9 @@ public static void reset() {
}
});
StreamedAudioFiles.clear();

audioEventExecutor.shutdownNow();
audioEventExecutor = Executors.newWorkStealingPool();
}

/** Safely generates a {@link Clip} object, crashing the engine if something goes wrong. */
Expand Down Expand Up @@ -231,4 +237,8 @@ static SourceDataLine newSourceDataLine(AudioFormat audioFormat) {
return null;
}
}

static void fireAudioEvent(Audio audio, LineEvent audioEventType) {
audioEventExecutor.submit(() -> audio.getAudioEventListener().fireEvent(audioEventType));
}
}
47 changes: 47 additions & 0 deletions src/main/java/tech/fastj/systems/audio/MemoryAudioPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
Expand All @@ -27,6 +28,15 @@ static void playAudio(MemoryAudio audio) {
AudioInputStream audioInputStream = audio.getAudioInputStream();
clip.open(audioInputStream);

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.OPEN,
audio.getAudioSource().getLongFramePosition()
)
);

playOrLoopAudio(audio);
} catch (LineUnavailableException | IOException exception) {
FastJEngine.error(CrashMessages.theGameCrashed("an error while trying to play sound."), exception);
Expand All @@ -46,6 +56,15 @@ static void pauseAudio(MemoryAudio audio) {

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Paused;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.STOP,
audio.getAudioSource().getLongFramePosition()
)
);
}

/** See {@link MemoryAudio#resume()}. */
Expand All @@ -70,11 +89,30 @@ static void stopAudio(MemoryAudio audio) {
}

clip.stop();

clip.flush();
clip.close();

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Stopped;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.STOP,
audio.getAudioSource().getLongFramePosition()
)
);

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.CLOSE,
audio.getAudioSource().getLongFramePosition()
)
);
}

/** See {@link MemoryAudio#seek(long)}. */
Expand Down Expand Up @@ -136,6 +174,15 @@ private static void playOrLoopAudio(MemoryAudio audio) {

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Playing;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.START,
audio.getAudioSource().getLongFramePosition()
)
);
}

private static int denormalizeLoopStart(float normalizedLoopStart, int clipFrameCount) {
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/tech/fastj/systems/audio/StreamedAudioPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import tech.fastj.systems.audio.state.PlaybackState;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import java.io.IOException;
Expand Down Expand Up @@ -62,10 +63,28 @@ static void playAudio(StreamedAudio audio) {

try {
sourceDataLine.open(audioInputStream.getFormat());
AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.OPEN,
audio.getAudioSource().getLongFramePosition()
)
);

sourceDataLine.start();

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Playing;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.START,
audio.getAudioSource().getLongFramePosition()
)
);
} catch (LineUnavailableException exception) {
FastJEngine.error(CrashMessages.theGameCrashed("an error while trying to play sound."), exception);
}
Expand All @@ -84,6 +103,15 @@ static void pauseAudio(StreamedAudio audio) {

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Paused;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.STOP,
audio.getAudioSource().getLongFramePosition()
)
);
}

/** See {@link StreamedAudio#resume()}. */
Expand All @@ -99,6 +127,15 @@ static void resumeAudio(StreamedAudio audio) {

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Playing;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.START,
audio.getAudioSource().getLongFramePosition()
)
);
}

/** See {@link StreamedAudio#stop()}. */
Expand All @@ -116,5 +153,23 @@ static void stopAudio(StreamedAudio audio) {

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Stopped;

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.STOP,
audio.getAudioSource().getLongFramePosition()
)
);

AudioManager.fireAudioEvent(
audio,
new LineEvent(
audio.getAudioSource(),
LineEvent.Type.CLOSE,
audio.getAudioSource().getLongFramePosition()
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import tech.fastj.systems.audio.state.PlaybackState;

import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -37,6 +39,12 @@ void checkLoadMemoryAudioInstance_shouldMatchExpectedValues() {
assertEquals(PlaybackState.Stopped, audio.getCurrentPlaybackState(), "After loading the audio into memory, the gotten audio should be in the \"stopped\" playback state.");
assertEquals(PlaybackState.Stopped, audio.getPreviousPlaybackState(), "After loading the audio into memory, the gotten audio's previous playback state should also be \"stopped\".");
assertEquals(0L, audio.getPlaybackPosition(), "After loading the audio into memory, the gotten audio should be at the very beginning with playback position.");
assertNull(audio.getAudioEventListener().getAudioOpenAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio open\" event action.");
assertNull(audio.getAudioEventListener().getAudioStartAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio start\" event action.");
assertNull(audio.getAudioEventListener().getAudioPauseAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio pause\" event action.");
assertNull(audio.getAudioEventListener().getAudioResumeAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio resume\" event action.");
assertNull(audio.getAudioEventListener().getAudioStopAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio stop\" event action.");
assertNull(audio.getAudioEventListener().getAudioCloseAction(), "After loading the audio into memory, the gotten audio's event listener should not contain an \"audio close\" event action.");
}

@Test
Expand Down Expand Up @@ -109,6 +117,74 @@ void trySetLoopCount_toInvalidValue() {
assertEquals(expectedExceptionMessage, exception.getMessage(), "The expected error message should match the actual error message.");
}

@Test
void checkPlayMemoryAudio_shouldTriggerOpenAndStartEvents() throws InterruptedException {
MemoryAudio audio = AudioManager.loadMemoryAudioInstance(TestAudioPath);
AtomicBoolean audioOpenEventBoolean = new AtomicBoolean(false);
AtomicBoolean audioStartEventBoolean = new AtomicBoolean(false);
audio.getAudioEventListener().setAudioOpenAction(() -> audioOpenEventBoolean.set(true));
audio.getAudioEventListener().setAudioStartAction(() -> audioStartEventBoolean.set(true));

audio.play();
TimeUnit.MILLISECONDS.sleep(3);

assertTrue(audioOpenEventBoolean.get(), "After playing the audio, the \"audio open\" event action should have been triggered.");
assertTrue(audioStartEventBoolean.get(), "After playing the audio, the \"audio start\" event action should have been triggered.");
}

@Test
void checkPauseMemoryAudio_shouldTriggerPauseAndStopEvents() throws InterruptedException {
MemoryAudio audio = AudioManager.loadMemoryAudioInstance(TestAudioPath);
AtomicBoolean audioPauseEventBoolean = new AtomicBoolean(false);
AtomicBoolean audioStopEventBoolean = new AtomicBoolean(false);
audio.getAudioEventListener().setAudioPauseAction(() -> audioPauseEventBoolean.set(true));
audio.getAudioEventListener().setAudioStopAction(() -> audioStopEventBoolean.set(true));

audio.play();
TimeUnit.MILLISECONDS.sleep(3);
audio.pause();
TimeUnit.MILLISECONDS.sleep(3);

assertTrue(audioPauseEventBoolean.get(), "After pausing the audio, the \"audio pause\" event action should have been triggered.");
assertTrue(audioStopEventBoolean.get(), "After pausing the audio, the \"audio stop\" event action should have been triggered.");
}

@Test
void checkResumeMemoryAudio_shouldTriggerStartAndResumeEvents() throws InterruptedException {
MemoryAudio audio = AudioManager.loadMemoryAudioInstance(TestAudioPath);
AtomicBoolean audioStartEventBoolean = new AtomicBoolean(true);
AtomicBoolean audioResumeEventBoolean = new AtomicBoolean(false);
audio.getAudioEventListener().setAudioStartAction(() -> audioStartEventBoolean.set(!audioStartEventBoolean.get()));
audio.getAudioEventListener().setAudioResumeAction(() -> audioResumeEventBoolean.set(true));

audio.play();
TimeUnit.MILLISECONDS.sleep(3);
audio.pause();
TimeUnit.MILLISECONDS.sleep(3);
audio.resume();
TimeUnit.MILLISECONDS.sleep(3);

assertTrue(audioResumeEventBoolean.get(), "After resuming the audio, the \"audio resume\" event action should have been triggered.");
assertTrue(audioStartEventBoolean.get(), "After resuming the audio, the \"audio start\" event action should have been triggered.");
}

@Test
void checkStopMemoryAudio_shouldTriggerStopAndCloseEvents() throws InterruptedException {
MemoryAudio audio = AudioManager.loadMemoryAudioInstance(TestAudioPath);
AtomicBoolean audioCloseEventBoolean = new AtomicBoolean(false);
AtomicBoolean audioStopEventBoolean = new AtomicBoolean(false);
audio.getAudioEventListener().setAudioCloseAction(() -> audioCloseEventBoolean.set(true));
audio.getAudioEventListener().setAudioStopAction(() -> audioStopEventBoolean.set(true));

audio.play();
TimeUnit.MILLISECONDS.sleep(3);
audio.stop();
TimeUnit.MILLISECONDS.sleep(3);

assertTrue(audioCloseEventBoolean.get(), "After stopping the audio, the \"audio close\" event action should have been triggered.");
assertTrue(audioStopEventBoolean.get(), "After stopping the audio, the \"audio stop\" event action should have been triggered.");
}

@Test
void checkGetAudioAfterUnloading() {
MemoryAudio audio = AudioManager.loadMemoryAudioInstance(TestAudioPath);
Expand Down
Loading

0 comments on commit bc50ef6

Please sign in to comment.