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(); } }