feat(io): bundle background music as classpath resources
Switch OggMusicBackend from streaming an external, git-ignored music/ folder (relative to the run directory) to streaming bundled classpath resources under /music/, so the four manor tracks ship inside the JAR and play out of the box — consistent with how fonts are bundled. The play loop reopens the resource each iteration via a BufferedInputStream (the vorbisspi SPI needs mark/reset to read the OGG header). Missing tracks still fall back to silence. Add the four converted OGG Vorbis tracks (manor-theme/cellar-drone/musicbox/ lament) under src/main/resources/music/, un-ignore that path, and re-anchor the gitignore so only the top-level MP3 source folder stays ignored. Update docs/music.md for the bundled approach and conversion recipes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
Semesterprojekt/.gitignore
vendored
6
Semesterprojekt/.gitignore
vendored
@@ -44,5 +44,7 @@ build/
|
|||||||
# Local save games and settings
|
# Local save games and settings
|
||||||
saves/
|
saves/
|
||||||
|
|
||||||
# External background-music files (not bundled)
|
# Source audio (MP3 originals); only the converted .ogg under
|
||||||
music/
|
# src/main/resources/music/ is tracked and bundled. Anchored so it
|
||||||
|
# ignores the top-level source folder but not the resources path.
|
||||||
|
/music/
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Background music (GUI)
|
# 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.
|
- Tracks live as `.ogg` (OGG Vorbis) files in `src/main/resources/music/`, so they
|
||||||
This folder is git-ignored and never bundled in the JAR.
|
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.
|
- 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
|
`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);
|
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.
|
seamlessly across rooms.
|
||||||
- Volume is the **Music** setting (Off / Low / Medium / High) in the Settings menu,
|
- Volume is the **Music** setting (Off / Low / Medium / High) in the Settings menu,
|
||||||
persisted to `saves/settings.json`. Off disables playback.
|
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.
|
- 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/<track>`) 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).
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ import javax.sound.sampled.AudioInputStream;
|
|||||||
import javax.sound.sampled.AudioSystem;
|
import javax.sound.sampled.AudioSystem;
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
import javax.sound.sampled.SourceDataLine;
|
import javax.sound.sampled.SourceDataLine;
|
||||||
import java.nio.file.Files;
|
import java.io.BufferedInputStream;
|
||||||
import java.nio.file.Path;
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams OGG Vorbis tracks from the external {@code music/} folder through a
|
* Streams OGG Vorbis tracks bundled on the classpath under {@code /music/}
|
||||||
* {@link SourceDataLine}, looping, with ~400 ms gain fades. Decoding uses the
|
* through a {@link SourceDataLine}, looping, with ~400 ms gain fades. Decoding
|
||||||
* vorbisspi/jorbis {@code javax.sound} SPI. All playback runs on a daemon
|
* uses the vorbisspi/jorbis {@code javax.sound} SPI. All playback runs on a
|
||||||
* thread; missing/undecodable files are logged and stay silent. GUI-only —
|
* daemon thread; missing/undecodable files are logged and stay silent. GUI-only
|
||||||
* verified manually.
|
* — verified manually.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public final class OggMusicBackend implements MusicBackend {
|
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_MS = 400;
|
||||||
private static final int FADE_STEPS = 20;
|
private static final int FADE_STEPS = 20;
|
||||||
private static final int BUFFER = 4096;
|
private static final int BUFFER = 4096;
|
||||||
@@ -35,13 +35,13 @@ public final class OggMusicBackend implements MusicBackend {
|
|||||||
public synchronized void fadeTo(String track, float gainDb) {
|
public synchronized void fadeTo(String track, float gainDb) {
|
||||||
stopCurrent();
|
stopCurrent();
|
||||||
targetGainDb = gainDb;
|
targetGainDb = gainDb;
|
||||||
Path file = Path.of(MUSIC_DIR, track);
|
String resource = MUSIC_DIR + track;
|
||||||
if (!Files.isReadable(file)) {
|
if (getClass().getResource(resource) == null) {
|
||||||
log.warn("Music file not found, staying silent: {}", file);
|
log.warn("Music resource not found, staying silent: {}", resource);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopping = false;
|
stopping = false;
|
||||||
playThread = new Thread(() -> playLoop(file, gainDb), "music");
|
playThread = new Thread(() -> playLoop(resource, gainDb), "music");
|
||||||
playThread.setDaemon(true);
|
playThread.setDaemon(true);
|
||||||
playThread.start();
|
playThread.start();
|
||||||
}
|
}
|
||||||
@@ -80,11 +80,14 @@ public final class OggMusicBackend implements MusicBackend {
|
|||||||
playThread = null;
|
playThread = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playLoop(Path file, float gainDb) {
|
private void playLoop(String resource, float gainDb) {
|
||||||
SourceDataLine l = null;
|
SourceDataLine l = null;
|
||||||
try {
|
try {
|
||||||
while (!stopping) {
|
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 base = in.getFormat();
|
||||||
AudioFormat pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
AudioFormat pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||||
base.getSampleRate(), 16, base.getChannels(),
|
base.getSampleRate(), 16, base.getChannels(),
|
||||||
@@ -106,7 +109,7 @@ public final class OggMusicBackend implements MusicBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Music playback failed for {}: {}", file, e.getMessage());
|
log.warn("Music playback failed for {}: {}", resource, e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
if (l != null) {
|
if (l != null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
BIN
Semesterprojekt/src/main/resources/music/cellar-drone.ogg
Normal file
BIN
Semesterprojekt/src/main/resources/music/cellar-drone.ogg
Normal file
Binary file not shown.
BIN
Semesterprojekt/src/main/resources/music/lament.ogg
Normal file
BIN
Semesterprojekt/src/main/resources/music/lament.ogg
Normal file
Binary file not shown.
BIN
Semesterprojekt/src/main/resources/music/manor-theme.ogg
Normal file
BIN
Semesterprojekt/src/main/resources/music/manor-theme.ogg
Normal file
Binary file not shown.
BIN
Semesterprojekt/src/main/resources/music/musicbox.ogg
Normal file
BIN
Semesterprojekt/src/main/resources/music/musicbox.ogg
Normal file
Binary file not shown.
Reference in New Issue
Block a user