javasweeper/Sound.java

142 lines
3.9 KiB
Java
Raw Normal View History

2023-05-21 15:13:45 -07:00
import java.io.*;
import javax.sound.sampled.*;
public enum Sound {
// The available sound filenames
WIN("win.wav"),
TICK("tick.wav"),
EXPLODE("explode.wav"),
SILENT("silent.wav");
// The directory all the audio files are in
public static final String AUDIO_DIR = "Audio";
// Named pipe to send audio requests to on Replit
public static final File REPLIT_PIPE = new File("/tmp/audio");
// Use env vars and the existance of the above pipe to determine if running inside Replit
public static final boolean REPLIT_API = System.getenv("REPL_ID") != null && REPLIT_PIPE.exists();
// Whether the clips have been initialized yet
private static boolean soundsInitialized = false;
// File containing the sound
private final File FILE;
// SoundBackend that will play the sound
private SoundBackend soundBackend;
// Because Replit does audio totally differently from desktop Java (it
// supposedly supports PulseAudio over VNC but I just could not get that
// working), and I would like to still be able to hear things when I play
// the game directly on my laptop, we have a SoundBackend interface that is
// implemented by both a builtin javax.sound.sampled-powered backend and a
// Replit specific one
private interface SoundBackend {
// plays the specific sound
public void play();
}
private class NativeBackend implements SoundBackend {
// The audio clip associated with the sound
private final Clip CLIP;
public NativeBackend() {
// Create the clip, if possible
Clip clip;
try (AudioInputStream audioIn = AudioSystem.getAudioInputStream(FILE)) {
clip = AudioSystem.getClip();
clip.open(audioIn);
} catch (Exception e) {
// If it didn't work, don't leave a broken clip behind that
// would try to get played
clip = null;
}
CLIP = clip;
}
@Override
public void play() {
// Don't play if the clip couldn't be initialized
if (CLIP == null)
return;
// Rewind the clip and play it
CLIP.stop();
CLIP.setFramePosition(0);
CLIP.start();
}
}
private class ReplitBackend implements SoundBackend {
// The JSON format to send requests to Replit with; the volume is 0.1
// because my ears got blown out the first time I exploded when it was
// at 100% volume (Replit is LOUD)
private static final String REQUEST_FORMAT = """
{
"Volume": 0.1,
"Type": "wav",
"Args": {
"Path": "%s"
}
}""";
// The actual request string to send
private final String REQUEST;
public ReplitBackend() {
REQUEST = String.format(REQUEST_FORMAT, FILE);
}
@Override
public void play() {
// Attempt to send the request
try (FileWriter writer = new FileWriter(REPLIT_PIPE)) {
writer.write(REQUEST);
} catch (IOException ignore) {
// If it didn't work, no biggie, just don't play anything
}
}
}
Sound(String fileName) {
FILE = new File(AUDIO_DIR, fileName);
}
// Initialize the backends for all sounds, if they haven't already been
// initialized yet
public static void initSounds() {
if (soundsInitialized)
return;
soundsInitialized = true;
if (REPLIT_API) {
for (Sound sound : values())
sound.soundBackend = sound.new ReplitBackend();
// HACK: Play an audible sound immediately, so the popup message
// happens now rather than in the middle of a game; SILENT doesn't
// work here, but the popup will block the first sound anyways so it
// doesn't matter
TICK.play();
return;
}
for (Sound sound : values())
sound.soundBackend = sound.new NativeBackend();
// HACK: Play a silent sound as soon as possible to eagerly load all the
// sound stuff now to prevent lag when the first real sound is played
// (yes this actually helps, I tested it)
SILENT.play();
}
// The actual method that users outside this file will call to play sound
public void play() {
// Here is the check for if the option for sound is actually enabled
if (Main.hasSound())
soundBackend.play();
}
}