Skip to content

Commit

Permalink
(#23) fixed audio looping, added some unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasstarsz committed Jun 1, 2021
1 parent 3efccdd commit f8cea2b
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 30 deletions.
47 changes: 44 additions & 3 deletions src/main/java/tech/fastj/systems/audio/Audio.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
/** An audio object used for sound playback. */
public class Audio {

/** Signifies that the audio should loop when it finishes playing. */
/** Signifies that the audio should loop indefinitely when it finishes playing. */
public static final int ContinuousLoop = Clip.LOOP_CONTINUOUSLY;
/** Signifies that the audio should stop looping. */
public static final int StopLooping = 0;

/** Signifies that the audio should start its loop from the beginning of the audio track. */
public static final int LoopFromStart = 0;
/** Signifies that the audio should loop back once it reaches the end of the audio track. */
Expand All @@ -21,6 +24,10 @@ public class Audio {
private final AudioEventListener audioEventListener;
private final Path audioPath;

private int loopStart;
private int loopEnd;
private int loopCount;
boolean shouldLoop;
PlaybackState currentPlaybackState;
PlaybackState previousPlaybackState;

Expand All @@ -31,6 +38,8 @@ public class Audio {
*/
Audio(Path audioPath) {
this.audioPath = audioPath;
loopStart = LoopFromStart;
loopEnd = LoopAtEnd;

clip = Objects.requireNonNull(AudioManager.newClip());
audioEventListener = new AudioEventListener(this);
Expand Down Expand Up @@ -103,6 +112,22 @@ public long getPlaybackPosition() {
return TimeUnit.MILLISECONDS.convert(clip.getMicrosecondPosition(), TimeUnit.MILLISECONDS);
}

public int getLoopStart() {
return loopStart;
}

public int getLoopEnd() {
return loopEnd;
}

public int getLoopCount() {
return loopCount;
}

public boolean shouldLoop() {
return shouldLoop;
}

/**
* Sets the playback position to the specified {@code playbackPosition} value, in milliseconds.
*
Expand All @@ -126,7 +151,9 @@ public void setLoopPoints(int loopStart, int loopEnd) {
throw new IllegalArgumentException("The loop starting point should be less than or equal to the loop ending point.");
}

clip.setLoopPoints(loopStart, loopEnd);
this.loopStart = loopStart;
this.loopEnd = loopEnd;
shouldLoop = true;
}

/**
Expand All @@ -140,7 +167,16 @@ public void setLoopPoints(int loopStart, int loopEnd) {
* @param loopCount The amount of times to loop.
*/
public void setLoopCount(int loopCount) {
clip.loop(loopCount);
if (loopCount < -1) {
throw new IllegalArgumentException("The loop count should not be less than -1.");
}

this.loopCount = loopCount;
shouldLoop = (this.loopCount != StopLooping);
}

public void setShouldLoop(boolean shouldLoop) {
this.shouldLoop = shouldLoop;
}

/**
Expand Down Expand Up @@ -209,6 +245,11 @@ public void stop() {
AudioManager.stopAudio(this);
}

public void stopLoopingNow() {
clip.loop(StopLooping);
shouldLoop = false;
}

@Override
public String toString() {
return "Audio{" +
Expand Down
80 changes: 68 additions & 12 deletions src/main/java/tech/fastj/systems/audio/AudioManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.nio.file.Path;
Expand All @@ -19,6 +22,23 @@ public class AudioManager {

private static final Map<String, Audio> AudioFiles = new HashMap<>();

/**
* Checks whether the computer supports audio output.
*
* @return Whether the computer supports audio output.
*/
public static boolean isOutputSupported() {
Line.Info outputLineInfo = new Line.Info(SourceDataLine.class);
for (Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) {
Mixer mixer = AudioSystem.getMixer(mixerInfo);
if (mixer.isLineSupported(outputLineInfo)) {
return true;
}
}

return false;
}

/**
* Loads all {@link Audio} objects at the specified paths into memory.
*
Expand Down Expand Up @@ -61,7 +81,14 @@ public static Audio getAudio(String audioPath) {
return AudioFiles.get(audioPath);
}

/** Resets the {@code AudioManager}, removing all of its loaded audio files. */
public static void reset() {
AudioFiles.forEach((s, audio) -> {
if (audio.currentPlaybackState != PlaybackState.Stopped) {
audio.stop();
}
});

AudioFiles.clear();
}

Expand All @@ -82,14 +109,19 @@ static Clip newClip() {
static AudioInputStream newAudioStream(Path audioPath) {
try {
return AudioSystem.getAudioInputStream(audioPath.toFile());
} catch (UnsupportedAudioFileException | IOException exception) {
} catch (IOException exception) {
FastJEngine.error(
CrashMessages.theGameCrashed("an error while loading sound."),
CrashMessages.theGameCrashed("an I/O error while loading sound."),
exception
);

return null;
} catch (UnsupportedAudioFileException exception) {
FastJEngine.error(
CrashMessages.theGameCrashed("an audio file reading error."),
new UnsupportedAudioFileException(audioPath.toAbsolutePath() + " seems to be of an unsupported file format.")
);
}

return null;
}

/** See {@link Audio#play()}. */
Expand All @@ -104,7 +136,16 @@ static void playAudio(Audio audio) {
try {
AudioInputStream audioInputStream = audio.getAudioInputStream();
clip.open(audioInputStream);
clip.start();

if (audio.shouldLoop()) {
clip.setLoopPoints(audio.getLoopStart(), audio.getLoopEnd());
clip.loop(audio.getLoopCount());
} else {
clip.start();
}

audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Playing;
} catch (LineUnavailableException | IOException exception) {
FastJEngine.error(CrashMessages.theGameCrashed("an error while trying to play sound."), exception);
}
Expand All @@ -116,9 +157,12 @@ static void pauseAudio(Audio audio) {

if (!clip.isOpen()) {
FastJEngine.warning("Tried to pause audio file \"" + audio.getAudioPath().toString() + "\", but it wasn't being played.");
} else {
clip.stop();
return;
}

clip.stop();
audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Paused;
}

/** See {@link Audio#resume()}. */
Expand All @@ -127,9 +171,18 @@ static void resumeAudio(Audio audio) {

if (!clip.isOpen()) {
FastJEngine.warning("Tried to resume audio file \"" + audio.getAudioPath().toString() + "\", but it wasn't being played.");
return;
}

if (audio.shouldLoop()) {
clip.setLoopPoints(audio.getLoopStart(), audio.getLoopEnd());
clip.loop(audio.getLoopCount());
} else {
clip.start();
}

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

/** See {@link Audio#stop()}. */
Expand All @@ -138,12 +191,15 @@ static void stopAudio(Audio audio) {

if (!clip.isOpen()) {
FastJEngine.warning("Tried to stop audio file \"" + audio.getAudioPath().toString() + "\", but it wasn't being played.");
} else {
clip.stop();
clip.flush();
clip.drain();
clip.close();
return;
}

clip.stop();
clip.flush();
clip.drain();
clip.close();
audio.previousPlaybackState = audio.currentPlaybackState;
audio.currentPlaybackState = PlaybackState.Stopped;
}

/** See {@link Audio#seek(long)}. */
Expand Down
1 change: 1 addition & 0 deletions src/test/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@

opens unittest.testcases.math to org.junit.platform.commons;

opens unittest.testcases.systems.audio to org.junit.platform.commons;
opens unittest.testcases.systems.control to org.junit.platform.commons;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,44 @@

import tech.fastj.engine.FastJEngine;

import tech.fastj.systems.audio.AudioManager;

import java.awt.GraphicsEnvironment;

import unittest.mock.systems.control.MockRunnableSimpleManager;

public class HeadlessHelper {
public class EnvironmentHelper {

public static final boolean isEnvironmentHeadless = headlessTest();
public static final boolean IsEnvironmentHeadless = headlessTest();
public static final boolean DoesEnvironmentSupportAudioOutput = audioOutputTest();

private static boolean headlessTest() {
// because idk how to run github actions in non-headless mode
boolean isHeadless = GraphicsEnvironment.isHeadless();

System.out.println(
"This testing environment is... " + (isHeadless
? "headless. Well that's unfortunate... this device isn't running in headless mode, so some tests cannot be conducted."
? "headless. Well that's unfortunate... this device is running in headless mode, so some tests cannot be conducted."
: "not headless. Good."
)
);

return isHeadless;
}

private static boolean audioOutputTest() {
boolean hasAudioOutput = AudioManager.isOutputSupported();

System.out.println(
"This testing environment... " + (hasAudioOutput
? "supports audio. Good."
: "does not support audio output. Well that's unfortunate... this device does not contain any audio output devices, so audio-related tests cannot be conducted."
)
);

return hasAudioOutput;
}

public static void runFastJWith(Runnable runnable) {
FastJEngine.init("For those sweet, sweet testing purposes", new MockRunnableSimpleManager(runnable));
try {
Expand Down
8 changes: 4 additions & 4 deletions src/test/java/unittest/testcases/graphics/DrawableTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
import java.util.UUID;

import org.junit.jupiter.api.Test;
import unittest.HeadlessHelper;
import unittest.EnvironmentHelper;
import unittest.mock.graphics.MockDrawable;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static unittest.HeadlessHelper.runFastJWith;
import static unittest.EnvironmentHelper.runFastJWith;

class DrawableTests {

Expand Down Expand Up @@ -52,7 +52,7 @@ void checkCollision_betweenPolygon2D_andModel2D() {

@Test
void checkCollision_betweenPolygon2D_andText2D() {
assumeFalse(HeadlessHelper.isEnvironmentHeadless);
assumeFalse(EnvironmentHelper.IsEnvironmentHeadless);

runFastJWith(() -> {
String text = "Hello, world!";
Expand All @@ -67,7 +67,7 @@ void checkCollision_betweenPolygon2D_andText2D() {

@Test
void checkCollision_betweenText2D_andModel2D() {
assumeFalse(HeadlessHelper.isEnvironmentHeadless);
assumeFalse(EnvironmentHelper.IsEnvironmentHeadless);

runFastJWith(() -> {
String text = "Hello, world!";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import unittest.HeadlessHelper;
import unittest.EnvironmentHelper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static unittest.HeadlessHelper.runFastJWith;
import static unittest.EnvironmentHelper.runFastJWith;

class Text2DTests {

@BeforeAll
public static void onlyRunIfNotHeadless() {
assumeFalse(HeadlessHelper.isEnvironmentHeadless);
assumeFalse(EnvironmentHelper.IsEnvironmentHeadless);
}

@Test
Expand Down
Loading

0 comments on commit f8cea2b

Please sign in to comment.