From 977872e77ad152cd3ea51457aa187a13e4abfbd0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 22:50:58 +0200 Subject: [PATCH] feat(io): MusicController + MusicBackend (testable music logic) Co-Authored-By: Claude Sonnet 4.6 --- .../jeanluc/adventure/io/MusicBackend.java | 17 ++++ .../jeanluc/adventure/io/MusicController.java | 52 +++++++++++ .../adventure/io/MusicControllerTest.java | 87 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicBackend.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicController.java create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/MusicControllerTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicBackend.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicBackend.java new file mode 100644 index 0000000..2958115 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicBackend.java @@ -0,0 +1,17 @@ +package thb.jeanluc.adventure.io; + +/** Raw audio operations — the only part that touches sound hardware. */ +public interface MusicBackend { + + /** Fade the current track out, then start {@code track} (looping) and fade in to {@code gainDb}. */ + void fadeTo(String track, float gainDb); + + /** Fade the current track out and stop. */ + void fadeOut(); + + /** Set the playback volume immediately. */ + void setGainDb(float gainDb); + + /** Stop playback and release resources. */ + void shutdown(); +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicController.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicController.java new file mode 100644 index 0000000..ee572b0 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MusicController.java @@ -0,0 +1,52 @@ +package thb.jeanluc.adventure.io; + +import java.util.Objects; + +/** + * Frontend-agnostic music decision logic. Idempotent per track (never restarts + * the same track), fades to silence on a null/blank track, and honours the + * {@link MusicLevel} (OFF disables playback). Delegates raw operations to a + * {@link MusicBackend}. + */ +public final class MusicController { + + private final MusicBackend backend; + private MusicLevel level = MusicLevel.MEDIUM; + private String current; // currently selected track, or null = silence + + public MusicController(MusicBackend backend) { + this.backend = backend; + } + + /** Called per turn with the current room's music field (may be null/blank). */ + public void room(String track) { + if (!level.isOn()) { + return; + } + String next = (track == null || track.isBlank()) ? null : track; + if (Objects.equals(next, current)) { + return; + } + current = next; + if (next == null) { + backend.fadeOut(); + } else { + backend.fadeTo(next, level.gainDb()); + } + } + + /** Applies a new music level (volume / off). */ + public void level(MusicLevel newLevel) { + this.level = newLevel; + if (!newLevel.isOn()) { + backend.fadeOut(); + current = null; // turning back on replays on the next room() call + } else { + backend.setGainDb(newLevel.gainDb()); + } + } + + public void shutdown() { + backend.shutdown(); + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/MusicControllerTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/MusicControllerTest.java new file mode 100644 index 0000000..0b931fc --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/MusicControllerTest.java @@ -0,0 +1,87 @@ +package thb.jeanluc.adventure.io; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class MusicControllerTest { + + /** Records backend calls for assertions. */ + private static final class FakeBackend implements MusicBackend { + final List calls = new ArrayList<>(); + public void fadeTo(String track, float gainDb) { calls.add("fadeTo:" + track); } + public void fadeOut() { calls.add("fadeOut"); } + public void setGainDb(float gainDb) { calls.add("setGain:" + gainDb); } + public void shutdown() { calls.add("shutdown"); } + } + + @Test + void firstRoomWithTrackPlaysAtDefaultLevel() { + FakeBackend b = new FakeBackend(); + new MusicController(b).room("a"); + assertThat(b.calls).containsExactly("fadeTo:a"); + } + + @Test + void sameTrackDoesNotRestart() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); + c.room("a"); + b.calls.clear(); + c.room("a"); + assertThat(b.calls).isEmpty(); + } + + @Test + void differentTrackSwitches() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); + c.room("a"); + c.room("b"); + assertThat(b.calls).containsExactly("fadeTo:a", "fadeTo:b"); + } + + @Test + void nullOrBlankRoomFadesToSilence() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); + c.room("a"); + c.room(null); + c.room(" "); + assertThat(b.calls).containsExactly("fadeTo:a", "fadeOut"); + } + + @Test + void offStopsAndIgnoresRooms() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); + c.room("a"); + c.level(MusicLevel.OFF); + b.calls.clear(); + c.room("b"); + assertThat(b.calls).isEmpty(); + } + + @Test + void levelChangeSetsGain() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); // default MEDIUM + c.level(MusicLevel.HIGH); + assertThat(b.calls).containsExactly("setGain:" + MusicLevel.HIGH.gainDb()); + } + + @Test + void turningOffThenOnReplaysOnNextRoom() { + FakeBackend b = new FakeBackend(); + MusicController c = new MusicController(b); + c.room("a"); + c.level(MusicLevel.OFF); // fadeOut + c.level(MusicLevel.MEDIUM); // setGain + b.calls.clear(); + c.room("a"); // current was reset → plays again + assertThat(b.calls).containsExactly("fadeTo:a"); + } +}