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:
2026-06-02 09:21:11 +02:00
parent d8e1a662bc
commit aa802582ec
7 changed files with 42 additions and 22 deletions

View File

@@ -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 {

Binary file not shown.

Binary file not shown.