diff --git a/Semesterprojekt/.gitignore b/Semesterprojekt/.gitignore index b39385b..fb2eb16 100644 --- a/Semesterprojekt/.gitignore +++ b/Semesterprojekt/.gitignore @@ -44,5 +44,7 @@ build/ # Local save games and settings saves/ -# External background-music files (not bundled) -music/ +# Source audio (MP3 originals); only the converted .ogg under +# src/main/resources/music/ is tracked and bundled. Anchored so it +# ignores the top-level source folder but not the resources path. +/music/ diff --git a/Semesterprojekt/docs/music.md b/Semesterprojekt/docs/music.md index e0d0686..e94a6bd 100644 --- a/Semesterprojekt/docs/music.md +++ b/Semesterprojekt/docs/music.md @@ -1,9 +1,10 @@ # Background music (GUI) -Background music plays only in the Swing GUI. It is **data-driven and external**: +Background music plays only in the Swing GUI. It is **data-driven and bundled**: -- Put `.ogg` (OGG Vorbis) files in a `music/` folder next to where you run the game. - This folder is git-ignored and never bundled in the JAR. +- Tracks live as `.ogg` (OGG Vorbis) files in `src/main/resources/music/`, so they + are on the classpath and ship inside the JAR — the game plays music out of the box, + with no external folder to set up. - A room plays a track via an optional `music:` field in `rooms.yaml`, e.g. `music: manor-theme.ogg`. Entering a room with a different track fades the old one out and the new one in (sequential, not an overlapping cross-fade); @@ -11,7 +12,21 @@ Background music plays only in the Swing GUI. It is **data-driven and external** seamlessly across rooms. - Volume is the **Music** setting (Off / Low / Medium / High) in the Settings menu, persisted to `saves/settings.json`. Off disables playback. -- Missing files are ignored (silent) — the game never crashes over absent audio. +- Missing tracks are ignored (silent) — the game never crashes over absent audio. - The console version is always silent. -Decoding uses the `vorbisspi` + `jorbis` libraries via the `javax.sound` SPI. +Decoding uses the `vorbisspi` + `jorbis` libraries via the `javax.sound` SPI; +`OggMusicBackend` streams each track from the classpath (`/music/`) on a +daemon thread. + +## Adding or replacing a track + +1. Convert your source audio to OGG Vorbis. With ffmpeg: + `ffmpeg -i in.mp3 -c:a libvorbis -q:a 5 out.ogg` + or with gstreamer (no ffmpeg needed): + `gst-launch-1.0 filesrc location=in.mp3 ! decodebin ! audioconvert ! audioresample ! vorbisenc quality=0.5 ! oggmux ! filesink location=out.ogg` +2. Drop the `.ogg` into `src/main/resources/music/`. +3. Reference it from a room's `music:` field in `rooms.yaml`. + +The four tracks shipped today: `manor-theme.ogg` (ground floor), +`cellar-drone.ogg` (cellar), `musicbox.ogg` (upper floor), `lament.ogg` (chapel). diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/OggMusicBackend.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/OggMusicBackend.java index 72bee18..eeea0be 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/OggMusicBackend.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/OggMusicBackend.java @@ -7,20 +7,20 @@ import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.FloatControl; import javax.sound.sampled.SourceDataLine; -import java.nio.file.Files; -import java.nio.file.Path; +import java.io.BufferedInputStream; +import java.io.InputStream; /** - * Streams OGG Vorbis tracks from the external {@code music/} folder through a - * {@link SourceDataLine}, looping, with ~400 ms gain fades. Decoding uses the - * vorbisspi/jorbis {@code javax.sound} SPI. All playback runs on a daemon - * thread; missing/undecodable files are logged and stay silent. GUI-only — - * verified manually. + * Streams OGG Vorbis tracks bundled on the classpath under {@code /music/} + * through a {@link SourceDataLine}, looping, with ~400 ms gain fades. Decoding + * uses the vorbisspi/jorbis {@code javax.sound} SPI. All playback runs on a + * daemon thread; missing/undecodable files are logged and stay silent. GUI-only + * — verified manually. */ @Slf4j public final class OggMusicBackend implements MusicBackend { - private static final String MUSIC_DIR = "music"; + private static final String MUSIC_DIR = "/music/"; private static final int FADE_MS = 400; private static final int FADE_STEPS = 20; private static final int BUFFER = 4096; @@ -35,13 +35,13 @@ public final class OggMusicBackend implements MusicBackend { public synchronized void fadeTo(String track, float gainDb) { stopCurrent(); targetGainDb = gainDb; - Path file = Path.of(MUSIC_DIR, track); - if (!Files.isReadable(file)) { - log.warn("Music file not found, staying silent: {}", file); + String resource = MUSIC_DIR + track; + if (getClass().getResource(resource) == null) { + log.warn("Music resource not found, staying silent: {}", resource); return; } stopping = false; - playThread = new Thread(() -> playLoop(file, gainDb), "music"); + playThread = new Thread(() -> playLoop(resource, gainDb), "music"); playThread.setDaemon(true); playThread.start(); } @@ -80,11 +80,14 @@ public final class OggMusicBackend implements MusicBackend { playThread = null; } - private void playLoop(Path file, float gainDb) { + private void playLoop(String resource, float gainDb) { SourceDataLine l = null; try { while (!stopping) { - try (AudioInputStream in = AudioSystem.getAudioInputStream(file.toFile())) { + // Reopen the bundled resource each loop; BufferedInputStream gives + // the SPI the mark/reset support it needs to read the OGG header. + try (InputStream res = getClass().getResourceAsStream(resource); + AudioInputStream in = AudioSystem.getAudioInputStream(new BufferedInputStream(res))) { AudioFormat base = in.getFormat(); AudioFormat pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, base.getSampleRate(), 16, base.getChannels(), @@ -106,7 +109,7 @@ public final class OggMusicBackend implements MusicBackend { } } } catch (Exception e) { - log.warn("Music playback failed for {}: {}", file, e.getMessage()); + log.warn("Music playback failed for {}: {}", resource, e.getMessage()); } finally { if (l != null) { try { diff --git a/Semesterprojekt/src/main/resources/music/cellar-drone.ogg b/Semesterprojekt/src/main/resources/music/cellar-drone.ogg new file mode 100644 index 0000000..3aa30a6 Binary files /dev/null and b/Semesterprojekt/src/main/resources/music/cellar-drone.ogg differ diff --git a/Semesterprojekt/src/main/resources/music/lament.ogg b/Semesterprojekt/src/main/resources/music/lament.ogg new file mode 100644 index 0000000..bc89462 Binary files /dev/null and b/Semesterprojekt/src/main/resources/music/lament.ogg differ diff --git a/Semesterprojekt/src/main/resources/music/manor-theme.ogg b/Semesterprojekt/src/main/resources/music/manor-theme.ogg new file mode 100644 index 0000000..e1dce7f Binary files /dev/null and b/Semesterprojekt/src/main/resources/music/manor-theme.ogg differ diff --git a/Semesterprojekt/src/main/resources/music/musicbox.ogg b/Semesterprojekt/src/main/resources/music/musicbox.ogg new file mode 100644 index 0000000..4d13ade Binary files /dev/null and b/Semesterprojekt/src/main/resources/music/musicbox.ogg differ