From ff433703f495959b603f109c3bf41439e35de7af Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 6 Jul 2024 02:04:27 +0900 Subject: [PATCH] y u no call back :( --- .../musicplayer/gui/MusicPlayerControl.kt | 86 +++--- src/net/torvald/terrarum/App.java | 1 + src/net/torvald/terrarum/MusicService.kt | 288 +++++++++++++++--- .../torvald/terrarum/TerrarumMusicPlaylist.kt | 15 +- .../terrarum/modulebasegame/TerrarumIngame.kt | 3 + .../modulebasegame/TerrarumMusicStreamer.kt | 135 ++------ .../terrarum/modulebasegame/TitleScreen.kt | 5 + .../gameactors/FixtureJukebox.kt | 33 +- .../gameactors/FixtureMusicalTurntable.kt | 34 +-- .../terrarum/transaction/Transaction.kt | 22 +- 10 files changed, 380 insertions(+), 242 deletions(-) diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt index cac879812..ba87f1821 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt @@ -112,7 +112,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { private val internalPlaylistName: String get() = ingame.musicStreamer.playlistName - fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String): TerrarumMusicPlaylist { + private fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String): TerrarumMusicPlaylist { fun String.isNum(): Boolean { try { this.toInt() @@ -123,7 +123,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } } - val playlist = ingame.musicStreamer.queueDirectory(path, shuffled, diskJockeyingMode, false) { filename -> + val playlist = TerrarumMusicPlaylist.fromDirectory(path, shuffled, diskJockeyingMode) { filename -> fileToName?.get(filename).let { if (it == null) filename.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.let { @@ -333,8 +333,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { if (mode < MODE_SHOW_LIST) { if (!transitionOngoing) { transitionRequest = MODE_SHOW_LIST - currentListMode = - 0 // no list transition anim is needed this time, just set the variable + currentListMode = 0 // no list transition anim is needed this time, just set the variable resetAlbumlistScroll() } } @@ -344,7 +343,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } else { if (!transitionOngoing) - transitionRequest = App.audioMixer.musicTrack.isPlaying.toInt() * MODE_MOUSE_UP + transitionRequest = MODE_MOUSE_UP } } @@ -352,7 +351,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // prev song if (mode < MODE_SHOW_LIST) { MusicService.playPrevSongInPlaylist { - ingame.musicStreamer.startMusic(this) // required for "intermittent" mode iHitTheStopButton = false stopRequested = false } @@ -393,10 +391,15 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { iHitTheStopButton = false stopRequested = false*/ - MusicService.resumePlaylistPlayback { - iHitTheStopButton = false - stopRequested = false - } + MusicService.resumePlaylistPlayback( + /* onSuccess: () -> Unit */{ + iHitTheStopButton = false + stopRequested = false + }, + /* onFailure: (Throwable) -> Unit */ { + + } + ) } } } @@ -405,7 +408,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // next song if (mode < MODE_SHOW_LIST) { MusicService.playNextSongInPlaylist { - ingame.musicStreamer.startMusic(this) // required for "intermittent" mode iHitTheStopButton = false stopRequested = false } @@ -427,8 +429,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { if (mode < MODE_SHOW_LIST) { if (!transitionOngoing) { transitionRequest = MODE_SHOW_LIST - currentListMode = - 1 // no list transition anim is needed this time, just set the variable + currentListMode = 1 // no list transition anim is needed this time, just set the variable resetPlaylistScroll() } } @@ -438,7 +439,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } else { if (!transitionOngoing) - transitionRequest = App.audioMixer.musicTrack.isPlaying.toInt() * MODE_MOUSE_UP + transitionRequest = MODE_MOUSE_UP } } } @@ -463,7 +464,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { }*/ MusicService.playNthSongInPlaylist(index) { - ingame.musicStreamer.startMusic(this) // required for "intermittent" mode iHitTheStopButton = false stopRequested = false } @@ -509,23 +509,8 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // printdbg(this, "mode = $mode; req = $transitionRequest") - /*if (shouldPlayerBeDisabled || iHitTheStopButton) { - if (!stopRequested) { - stopRequested = true - ingame.backgroundMusicPlayer.stopMusic(this) - } - } - else*/ if (ingame.musicStreamer.playCaller is PlaysMusic && !jukeboxStopMonitorAlert && !App.audioMixer.musicTrack.isPlaying) { - jukeboxStopMonitorAlert = true - val interval = ingame.musicStreamer.getRandomMusicInterval() - ingame.musicStreamer.stopMusic(this, false, interval) - } - else if (App.audioMixer.musicTrack.isPlaying) { - jukeboxStopMonitorAlert = false - } } - private var jukeboxStopMonitorAlert = true private var iHitTheStopButton = false private var stopRequested = false @@ -726,6 +711,22 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { drawControls(frameDelta, batch, _posX, posYforControls) drawList(camera, frameDelta, batch, _posX, _posY) + + // debug codes + if (MusicService.transactionLocked) { + batch.color = Color.RED + Toolkit.drawTextCentered(batch, App.fontSmallNumbers, "LOCKED", Toolkit.drawWidth, 0, _posY.toInt() + height + 5) + } + else { + batch.color = Color.WHITE + Toolkit.drawTextCentered(batch, App.fontSmallNumbers, "UNLOCKED", Toolkit.drawWidth, 0, _posY.toInt() + height + 5) + } + + batch.color = Color.WHITE + Toolkit.drawTextCentered(batch, App.fontSmallNumbers, "State: ${MusicService.currentPlaybackState.get()}", Toolkit.drawWidth, 0, _posY.toInt() + height + 18) + // end of debug codes + + batch.color = Color.WHITE @@ -1057,6 +1058,8 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { else 0f + val baseCol = if (MusicService.transactionLocked) Color.RED else Color.WHITE + if (alpha > 0f) { val alpha0 = alpha.coerceIn(0f, 1f).organicOvershoot().coerceAtMost(1f) val internalWidth =minOf(widthForMouseUp.toFloat(), width - 20f) @@ -1078,10 +1081,10 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // prev/next button if (i == 1 || i == 3) { // prev/next song button - batch.color = Color(1f, 1f, 1f, alphaBase * (1f - buttonFadePerc)) + batch.color = baseCol * Color(1f, 1f, 1f, alphaBase * (1f - buttonFadePerc)) batch.draw(controlButtons.get(i, 0), btnX, btnY) // prev/next page button - batch.color = Color(1f, 1f, 1f, alphaBase * buttonFadePerc) + batch.color = baseCol * Color(1f, 1f, 1f, alphaBase * buttonFadePerc) batch.draw(controlButtons.get(i, 1), btnX, btnY) } // stop button @@ -1089,13 +1092,13 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // get correct stop/play button val iconY = if (!App.audioMixer.musicTrack.isPlaying) 1 else 0 // fade if avaliable - batch.color = Color(1f, 1f, 1f, alphaBase * (1f - buttonFadePerc)) + batch.color = baseCol * Color(1f, 1f, 1f, alphaBase * (1f - buttonFadePerc)) batch.draw(controlButtons.get(i, iconY), btnX, btnY) // page number with fade for (mode in 0..1) { val alphaNum = if (mode == 0) 1f - listViewPanelScroll else listViewPanelScroll - batch.color = Color(1f, 1f, 1f, alphaBase2 * buttonFadePerc * alphaNum) // don't use mouse-up effect + batch.color = baseCol * Color(1f, 1f, 1f, alphaBase2 * buttonFadePerc * alphaNum) // don't use mouse-up effect val (thisPage, totalPage) = if (mode == 0) albumlistScroll.div(PLAYLIST_LINES).plus(1) to albumsList.size.toFloat().div(PLAYLIST_LINES).ceilToInt() else @@ -1111,7 +1114,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } // else button else { - batch.color = Color(1f, 1f, 1f, alphaBase) + batch.color = baseCol * Color(1f, 1f, 1f, alphaBase) batch.draw(controlButtons.get(i, 0), btnX, btnY) } @@ -1338,7 +1341,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { track.doGaplessPlayback = (albumProp.diskJockeyingMode == "continuous") if (track.doGaplessPlayback) { track.pullNextTrack = { - track.currentTrack = ingame.musicStreamer.pullNextMusicTrack(true) + track.currentTrack = MusicService.currentPlaylist!!.peekNext() setMusicName(track.currentTrack?.name ?: "") } } @@ -1360,4 +1363,13 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } } } -} \ No newline at end of file +} + +private operator fun Color.times(other: Color): Color { + return Color( + this.r * other.r, + this.g * other.g, + this.b * other.b, + this.a * other.a, + ) +} diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 7cb23fc1e..aba94f247 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -1052,6 +1052,7 @@ public class App implements ApplicationListener { printdbg("AppLoader-Static", "Screen before change: " + currentScreen.getClass().getCanonicalName()); currentScreen.hide(); + MusicService.INSTANCE.leaveScene(); currentScreen.dispose(); } else { diff --git a/src/net/torvald/terrarum/MusicService.kt b/src/net/torvald/terrarum/MusicService.kt index d70e24c66..a0a42dd51 100644 --- a/src/net/torvald/terrarum/MusicService.kt +++ b/src/net/torvald/terrarum/MusicService.kt @@ -1,9 +1,12 @@ package net.torvald.terrarum +import net.torvald.terrarum.audio.AudioBank import net.torvald.terrarum.audio.AudioMixer.Companion.DEFAULT_FADEOUT_LEN import net.torvald.terrarum.transaction.Transaction import net.torvald.terrarum.transaction.TransactionListener import net.torvald.terrarum.transaction.TransactionState +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference /** * To play the music, create a transaction then pass it to the `runTransaction(Transaction)` @@ -12,18 +15,147 @@ import net.torvald.terrarum.transaction.TransactionState */ object MusicService : TransactionListener() { - var currentPlaylist: TerrarumMusicPlaylist? = null; private set + private val currentPlaylistReference = AtomicReference(null) + val currentPlaylist: TerrarumMusicPlaylist?; get() = currentPlaylistReference.get() override fun getCurrentStatusForTransaction(): TransactionState { return TransactionState( - mutableMapOf( - "currentPlaylist" to currentPlaylist + hashMapOf( + "currentPlaylist" to currentPlaylistReference.get() ) ) } override fun commitTransaction(state: TransactionState) { - this.currentPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist? + (state["currentPlaylist"] as TerrarumMusicPlaylist?).let { + this.currentPlaylistReference.set(it) + } + } + + + + private const val STATE_INTERMISSION = 0 + private const val STATE_FIREPLAY = 1 + private const val STATE_PLAYING = 2 + + val currentPlaybackState = AtomicInteger(STATE_INTERMISSION) + private var waitAkku = 0f + private var waitTime = 10f + + private fun enterSTATE_INTERMISSION(waitFor: Float) { + currentPlaybackState.set(STATE_INTERMISSION) + waitTime = waitFor + waitAkku = 0f + } + + private fun enterSTATE_FIREPLAY() { + val state = currentPlaybackState.get() + if (state == STATE_FIREPLAY) throw IllegalStateException("Cannot change state FIREPLAY -> FIREPLAY") + if (state == STATE_PLAYING) throw IllegalStateException("Cannot change state PLAYING -> FIREPLAY") + + waitAkku = 0f + currentPlaybackState.set(STATE_FIREPLAY) + } + + private fun enterSTATE_PLAYING() { + val state = currentPlaybackState.get() + if (state == STATE_INTERMISSION) throw IllegalStateException("Cannot change state INTERMISSION -> PLAYING") + if (state == STATE_PLAYING) throw IllegalStateException("Cannot change state PLAYING -> PLAYING") + + currentPlaybackState.set(STATE_PLAYING) + } + + fun getRandomMusicInterval() = 20f + Math.random().toFloat() * 4f // longer gap (20s to 24s) + + private fun enterIntermissionAndWaitForPlaylist() { + val time = when (currentPlaylist?.diskJockeyingMode ?: "intermittent") { + "intermittent" -> getRandomMusicInterval() + "continuous" -> 0f + else -> getRandomMusicInterval() + } + enterSTATE_INTERMISSION(time) + } + + fun enterIntermission() { + enterSTATE_INTERMISSION(getRandomMusicInterval()) + } + + fun onMusicFinishing(audio: AudioBank) { + synchronized(this) { + enterIntermissionAndWaitForPlaylist() + } + } + + private var playTransactionOngoing = false + + fun update(delta: Float) { + when (currentPlaybackState.get()) { + STATE_FIREPLAY -> { + if (!playTransactionOngoing) { + playTransactionOngoing = true + MusicService.resumePlaylistPlayback( + /* onSuccess: () -> Unit */ + { + runTransaction(object : Transaction { + override fun start(state: TransactionState) { + App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getCurrent()) + } + + override fun onSuccess(state: TransactionState) { + enterSTATE_PLAYING() + } + + override fun onFailure(e: Throwable, state: TransactionState) { + enterSTATE_INTERMISSION(getRandomMusicInterval()) // will try again after a random interval + } + }) + }, + /* onFailure: (Throwable) -> Unit */ + { + enterSTATE_INTERMISSION(getRandomMusicInterval()) // will try again after a random interval + }, + // onFinally: () -> Unit + { + playTransactionOngoing = false + } + ) + } + } + STATE_PLAYING -> { + // onMusicFinishing() will be called when the music finishes; it's on the setOnCompletionListener + } + STATE_INTERMISSION -> { + waitAkku += delta + + if (waitAkku >= waitTime && currentPlaylist != null) { + enterSTATE_FIREPLAY() + } + } + } + } + + + fun enterScene(id: String) { + synchronized(this) { + /*val playlist = when (id) { + "title" -> getTitlePlaylist() + "ingame" -> getIngameDefaultPlaylist() + else -> getIngameDefaultPlaylist() + } + + putNewPlaylist(playlist) { + // after the fadeout, we'll... + enterSTATE_FIREPLAY() + }*/ + + stopPlaylistPlayback { } + } + } + + fun leaveScene() { + synchronized(this) { + stopPlaylistPlayback {} + } } /** @@ -32,6 +164,9 @@ object MusicService : TransactionListener() { * be reset as soon as the transaction starts. Note that the resetting behaviour is NOT atomic. (the given * playlist will stay in reset state even if the transaction fails) * + * When the transaction was successful, the old playlist gets disposed of, then the songFinishedHook of + * the songs in the new playlist will be overwritten, before `onSuccess` is called. + * * The old playlist will be disposed of if and only if the transaction was successful. * * @param playlist An instance of a [TerrarumMusicPlaylist] to be changed into @@ -45,7 +180,6 @@ object MusicService : TransactionListener() { override fun start(state: TransactionState) { oldPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist? - if (oldPlaylist == playlist) return playlist.reset() @@ -64,15 +198,27 @@ object MusicService : TransactionListener() { waitUntil { fadedOut } } else { - /* do nothing */ + // put new playlist + state["currentPlaylist"] = playlist } } override fun onSuccess(state: TransactionState) { oldPlaylist?.dispose() + + (state["currentPlaylist"] as TerrarumMusicPlaylist?)?.let { + it.musicList.forEach { + it.songFinishedHook = { + onMusicFinishing(it) + } + } + } + onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + } } } @@ -80,20 +226,30 @@ object MusicService : TransactionListener() { return object : Transaction { override fun start(state: TransactionState) { var fadedOut = false + var err: Throwable? = null // request fadeout App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { - // callback: play next song in the playlist - App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getNext()) - fadedOut = true + try { + // callback: play next song in the playlist + // TODO queue the next song on the playlist, the actual playback will be done by the state machine update + + fadedOut = true + } + catch (e: Throwable) { + err = e + } } - waitUntil { fadedOut } + waitUntil { fadedOut || err != null } + if (err != null) throw err!! } override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + } } } @@ -101,20 +257,30 @@ object MusicService : TransactionListener() { return object : Transaction { override fun start(state: TransactionState) { var fadedOut = false + var err: Throwable? = null // request fadeout App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { - // callback: play prev song in the playlist - App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getPrev()) - fadedOut = true + try { + // callback: play prev song in the playlist + // TODO queue the prev song on the playlist, the actual playback will be done by the state machine update + + fadedOut = true + } + catch (e: Throwable) { + err = e + } } - waitUntil { fadedOut } + waitUntil { fadedOut || err != null } + if (err != null) throw err!! } override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + } } } @@ -122,18 +288,29 @@ object MusicService : TransactionListener() { return object : Transaction { override fun start(state: TransactionState) { var fadedOut = false + var err: Throwable? = null // request fadeout App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { - // callback: play prev song in the playlist - App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getNthSong(index)) - fadedOut = true + try { + // callback: play prev song in the playlist + // TODO queue the nth song on the playlist, the actual playback will be done by the state machine update + + + fadedOut = true + } + catch (e: Throwable) { + err = e + } } - waitUntil { fadedOut } + waitUntil { fadedOut || err != null } + if (err != null) throw err!! } override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + } } } @@ -147,21 +324,32 @@ object MusicService : TransactionListener() { } waitUntil { fadedOut } + + enterSTATE_INTERMISSION(Float.POSITIVE_INFINITY) } - override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onSuccess(state: TransactionState) { + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + } } } - private fun createTransactionForPlaylistResume(onSuccess: () -> Unit): Transaction { + private fun createTransactionForPlaylistResume(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit): Transaction { return object : Transaction { override fun start(state: TransactionState) { - App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getCurrent()) + enterSTATE_FIREPLAY() } - override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) {} + override fun onSuccess(state: TransactionState) { + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + onFailure(e) + } } } @@ -174,22 +362,44 @@ object MusicService : TransactionListener() { return object : Transaction { override fun start(state: TransactionState) { var fadedOut = false + var err: Throwable? = null + println("createTransactionPausePlaylistForMusicalFixture start") // request fadeout App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2.0) { - // callback: let the caller actually take care of playing the audio - action() + println("createTransactionPausePlaylistForMusicalFixture fadeout end") + try { + // callback: let the caller actually take care of playing the audio + action() - fadedOut = true + fadedOut = true + } + catch (e: Throwable) { + err = e + e.printStackTrace() + } } - waitUntil { fadedOut } + waitUntil { fadedOut || err != null } + if (err != null) throw err!! + + // enter intermission state + println("createTransactionPausePlaylistForMusicalFixture fadeout waiting end, entering INTERMISSION state") + enterSTATE_INTERMISSION(Float.POSITIVE_INFINITY) // wait until the interjected music finishes + println("createTransactionPausePlaylistForMusicalFixture waiting for musicFinished()") waitUntil { musicFinished() } } - override fun onSuccess(state: TransactionState) { onSuccess() } - override fun onFailure(e: Throwable, state: TransactionState) { onFailure(e) } + override fun onSuccess(state: TransactionState) { + onSuccess() + enterSTATE_INTERMISSION(getRandomMusicInterval()) + } + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + onFailure(e) + enterSTATE_INTERMISSION(getRandomMusicInterval()) + } } // note to self: wait() and notify() using a lock object is impractical as the Java thread can wake up @@ -215,9 +425,11 @@ object MusicService : TransactionListener() { runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally) } + /** Normal playlist playback will resume after the transaction, after the onSuccess/onFailure */ fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure)) } + /** Normal playlist playback will resume after the transaction, after the onSuccess/onFailure but before the onFinally */ fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, onFinally: () -> Unit = {}) { runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure), onFinally) } @@ -250,10 +462,10 @@ object MusicService : TransactionListener() { runTransaction(createTransactionForPlaylistStop(onSuccess), onFinally) } - fun resumePlaylistPlayback(onSuccess: () -> Unit) { - runTransaction(createTransactionForPlaylistResume(onSuccess)) + fun resumePlaylistPlayback(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { + runTransaction(createTransactionForPlaylistResume(onSuccess, onFailure)) } - fun resumePlaylistPlayback(onSuccess: () -> Unit, onFinally: () -> Unit) { - runTransaction(createTransactionForPlaylistResume(onSuccess), onFinally) + fun resumePlaylistPlayback(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForPlaylistResume(onSuccess, onFailure), onFinally) } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt b/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt index 4d1b9547d..1a07d7579 100644 --- a/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt +++ b/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt @@ -47,14 +47,19 @@ class TerrarumMusicPlaylist( fun getCurrent(): MusicContainer { checkRefill() - return musicList[currentIndexCursor] + return musicList[internalIndices[currentIndexCursor]] } fun getNext(): MusicContainer { checkRefill() currentIndexCursor += 1 - return musicList[currentIndexCursor] + return musicList[internalIndices[currentIndexCursor]] + } + + fun peekNext(): MusicContainer { + checkRefill() + return musicList[internalIndices[currentIndexCursor + 1]] } fun getPrev(): MusicContainer { @@ -75,7 +80,7 @@ class TerrarumMusicPlaylist( currentIndexCursor -= 1 - return musicList[currentIndexCursor] + return musicList[internalIndices[currentIndexCursor]] } @@ -92,6 +97,8 @@ class TerrarumMusicPlaylist( } companion object { + private val validMusicExtensions = hashSetOf("mp3", "wav", "ogg") + /** * Adding songFinishedHook to the songs is a responsibility of the caller. */ @@ -104,7 +111,7 @@ class TerrarumMusicPlaylist( val playlistName = musicDir.substringAfterLast('/') - val playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { + val playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.filter { Companion.validMusicExtensions.contains(it.extension.lowercase()) }?.mapNotNull { printdbg(this, "Music: ${it.absolutePath}") try { MusicContainer( diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index 5b7f7cd74..7dd58a447 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -321,6 +321,8 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { ) loadedTime_t = App.getTIME_T() + + MusicService.enterScene("ingame") } data class NewGameParams( @@ -1007,6 +1009,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { } musicStreamer.update(this, delta) + MusicService.update(delta) //////////////////////// // ui-related updates // diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicStreamer.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicStreamer.kt index 96d391f0f..023719d77 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicStreamer.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicStreamer.kt @@ -36,40 +36,6 @@ class TerrarumMusicStreamer : MusicStreamer() { musicBin = ArrayList(if (shuffled) playlist.shuffled() else playlist.slice(playlist.indices)) } - /** - * Send the playlist to the MusicService to be played at the other play commands. - * - * @param musicDir where the music files are. Absolute path. - * @param shuffled if the tracks are to be shuffled - * @param diskJockeyingMode `intermittent` to give random gap between tracks, `continuous` for continuous playback - */ - fun queueDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, playImmediately: Boolean, fileToName: ((String) -> String) = { name: String -> - name.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.joinToString(" ") - }): TerrarumMusicPlaylist { - // FIXME the olde way -- must be replaced with one that utilises MusicService - /*if (musicState != STATE_INIT && musicState != STATE_INTERMISSION) { - App.audioMixer.requestFadeOut(App.audioMixer.fadeBus, AudioMixer.DEFAULT_FADEOUT_LEN) // explicit call for fade-out when the game instance quits - stopMusic0(App.audioMixer.musicTrack.currentTrack) - } - - playlist.forEach { it.tryDispose() } - registerSongsFromDir(musicDir, fileToName) - - this.shuffled = shuffled - this.diskJockeyingMode = diskJockeyingMode - - restockMusicBin()*/ - - val playlist = TerrarumMusicPlaylist.fromDirectory(musicDir, shuffled, diskJockeyingMode, fileToName) - - if (!playImmediately) - MusicService.putNewPlaylist(playlist) {} - else - MusicService.putNewPlaylist(playlist) - - return playlist - } - /** * Adds a song to the head of the internal playlist (`musicBin`) */ @@ -142,7 +108,8 @@ class TerrarumMusicStreamer : MusicStreamer() { private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>() init { - queueDirectory(App.customMusicDir, true, "intermittent", true) + // TODO queue and play the default playlist + // TerrarumMusicPlaylist.fromDirectory(App.defaultMusicDir, true, "intermittent") } @@ -175,78 +142,19 @@ class TerrarumMusicStreamer : MusicStreamer() { protected var ambState = 0 protected var ambFired = false - fun getRandomMusicInterval() = 20f + Math.random().toFloat() * 4f // longer gap (20s to 24s) + fun getRandomMusicInterval() = MusicService.getRandomMusicInterval() - var stopCaller: Any? = null; private set - var playCaller: Any? = null; private set - var stopCallTime: Long? = null; private set + // call MusicService to fade out + // pauses playlist update + // called by MusicPlayerControl + fun stopMusicPlayback() { - private fun stopMusic0(song: AudioBank?, callStopMusicHook: Boolean = true, customPauseLen: Float? = null) { - musicState = if (customPauseLen == Float.POSITIVE_INFINITY) STATE_INIT else STATE_INTERMISSION -// printdbg(this, "stopMusic1 customLen=$customPauseLen, stateNow: $musicState, called by") -// printStackTrace(this) - intermissionAkku = 0f - intermissionLength = customPauseLen ?: getRandomMusicInterval() - musicFired = false - if (callStopMusicHook && musicStopHooks.isNotEmpty() && song is MusicContainer) musicStopHooks.forEach { - it(song) - } -// printdbg(this, "StopMusic Intermission: $intermissionLength seconds") } - fun stopMusic(caller: Any?, callStopMusicHook: Boolean = true, pauseLen: Float = Float.POSITIVE_INFINITY) { - val timeNow = System.currentTimeMillis() - val trackThis = App.audioMixer.musicTrack.currentTrack + // resumes playlist update + // called by MusicPlayerControl + fun resumeMusicPlayback() { - if (caller is TerrarumMusicStreamer) { - if (stopCaller == null) { -// printdbg(this, "Caller: this, prev caller: $stopCaller, len: $pauseLen, obliging stop request") - stopMusic0(trackThis, callStopMusicHook, pauseLen) - } - else { -// printdbg(this, "Caller: this, prev caller: $stopCaller, len: $pauseLen, ignoring stop request") - } - } - else { -// printdbg(this, "Caller: $caller, prev caller: , len: $pauseLen, obliging stop request") - stopMusic0(trackThis, callStopMusicHook, pauseLen) - } - - stopCaller = caller?.javaClass?.canonicalName - stopCallTime = System.currentTimeMillis() - -// printStackTrace(this) - } - - fun startMusic(caller: Any?) { - playCaller = caller - startMusic0(pullNextMusicTrack()) - } - - private fun startMusic0(song: MusicContainer) { - stopCaller = null - stopCallTime = null - - App.audioMixer.startMusic(song) -// printdbg(this, "startMusic Now playing: ${song.name}, called by:") -// printStackTrace(this) -// INGAME.sendNotification("Now Playing $EMDASH ${song.name}") - if (musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(song) } - musicState = STATE_PLAYING - intermissionLength = 42.42424f - } - - // MixerTrackProcessor will call this function externally to make gapless playback work - fun pullNextMusicTrack(callNextMusicHook: Boolean = false): MusicContainer { -// printStackTrace(this) - - // prevent same song to play twice in row (for the most time) - if (musicBin.isEmpty()) { - restockMusicBin() - } - return musicBin.removeAt(0).also { mus -> - if (callNextMusicHook && musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(mus) } - } } private fun stopAmbient() { @@ -286,12 +194,6 @@ class TerrarumMusicStreamer : MusicStreamer() { override fun update(ingame: IngameInstance, delta: Float) { val timeNow = System.currentTimeMillis() - val callerRecordExpired = (timeNow - (stopCallTime ?: 0L) > 1000) - - if (callerRecordExpired && stopCaller != null) { - stopCaller = null - stopCallTime = null - } // start the song queueing if there is one to play if (firstTime) { @@ -300,12 +202,21 @@ class TerrarumMusicStreamer : MusicStreamer() { if (ambients.isNotEmpty()) ambState = STATE_INTERMISSION } - when (musicState) { STATE_FIREPLAY -> { if (!musicFired) { - musicFired = true - startMusic0(pullNextMusicTrack()) + MusicService.resumePlaylistPlayback( + // onSuccess: () -> Unit + { + musicFired = true + musicState = STATE_PLAYING + }, + // onFailure: (Throwable) -> Unit + { + musicFired = false + musicState = STATE_INTERMISSION + }, + ) } } STATE_PLAYING -> { @@ -428,8 +339,6 @@ class TerrarumMusicStreamer : MusicStreamer() { private val TRACK2_DAWN_ELEV_DN_MAX = -10.0 override fun dispose() { - App.audioMixer.requestFadeOut(App.audioMixer.fadeBus, AudioMixer.DEFAULT_FADEOUT_LEN) // explicit call for fade-out when the game instance quits - stopMusic0(App.audioMixer.musicTrack.currentTrack) stopAmbient() } } diff --git a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt index e042e40d0..711bd2b3c 100644 --- a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt +++ b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt @@ -304,6 +304,8 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { loadThingsWhileIntroIsVisible() printdbg(this, "show() exit") + + MusicService.enterScene("title") } @@ -330,6 +332,9 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { // update UIs // uiContainer.forEach { it?.update(delta) } + + // update MusicService // + MusicService.update(delta) } private val particles = CircularArray(16, true) diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt index 71e31d150..70404d54c 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt @@ -103,11 +103,6 @@ class FixtureJukebox : Electric, PlaysMusic { override fun updateImpl(delta: Float) { super.updateImpl(delta) - - // supress the normal background music playback - if (musicIsPlaying && !flagDespawn) { - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, true) - } } @@ -135,10 +130,8 @@ class FixtureJukebox : Electric, PlaysMusic { } musicNowPlaying = MusicContainer(title, musicFile.file()) { + returnToInitialState() printdbg(this, "Stop music $title - $artist") - - // can't call stopDiscPlayback() because of the recursion - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, pauseLen = (INGAME.musicStreamer as TerrarumMusicStreamer).getRandomMusicInterval()) } discCurrentlyPlaying = index @@ -149,24 +142,18 @@ class FixtureJukebox : Electric, PlaysMusic { }*/ MusicService.playMusicalFixture( - // action: () -> Unit - { + /* action: () -> Unit */ { startAudio(musicNowPlaying!!) { loadEffector(it) } }, - // musicFinished: () -> Boolean - { + /* musicFinished: () -> Boolean */ { !musicIsPlaying }, - // onSuccess: () -> Unit - { + /* onSuccess: () -> Unit */ { }, - // onFailure: (Throwable) -> Unit - { - - }, - // onFinally: () -> Unit - returnToInitialState + /* onFailure: (Throwable) -> Unit */ { + returnToInitialState + } ) @@ -185,8 +172,10 @@ class FixtureJukebox : Electric, PlaysMusic { */ fun stopGracefully() { stopDiscPlayback() - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, pauseLen = (INGAME.musicStreamer as TerrarumMusicStreamer).getRandomMusicInterval()) - + try { + MusicService.enterIntermission() + } + catch (_: Throwable) {} } override fun drawBody(frameDelta: Float, batch: SpriteBatch) { diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt index 83a3be359..1626753e6 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt @@ -92,11 +92,6 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { override fun updateImpl(delta: Float) { super.updateImpl(delta) - - // supress the normal background music playback - if (musicIsPlaying && !flagDespawn) { - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, true) - } } fun playDisc() { @@ -118,10 +113,8 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { } musicNowPlaying = MusicContainer(title, musicFile.file()) { + returnToInitialState() App.printdbg(this, "Stop music $title - $artist") - - // can't call stopDiscPlayback() because of the recursion - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, pauseLen = (INGAME.musicStreamer as TerrarumMusicStreamer).getRandomMusicInterval()) } @@ -131,24 +124,19 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { }*/ MusicService.playMusicalFixture( - // action: () -> Unit - { + /* action: () -> Unit */ { + App.printdbg(this, "call startAudio(${musicNowPlaying?.name})") startAudio(musicNowPlaying!!) { loadEffector(it) } }, - // musicFinished: () -> Boolean - { + /* musicFinished: () -> Boolean */ { !musicIsPlaying }, - // onSuccess: () -> Unit - { + /* onSuccess: () -> Unit */ { }, - // onFailure: (Throwable) -> Unit - { - - }, - // onFinally: () -> Unit - returnToInitialState + /* onFailure: (Throwable) -> Unit */ { + returnToInitialState + } ) @@ -162,8 +150,10 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { */ fun stopGracefully() { stopDiscPlayback() - (INGAME.musicStreamer as TerrarumMusicStreamer).stopMusic(this, pauseLen = (INGAME.musicStreamer as TerrarumMusicStreamer).getRandomMusicInterval()) - + try { + MusicService.enterIntermission() + } + catch (_: Throwable) {} } private fun stopDiscPlayback() { diff --git a/src/net/torvald/terrarum/transaction/Transaction.kt b/src/net/torvald/terrarum/transaction/Transaction.kt index 8fd9b98a1..f2674b23a 100644 --- a/src/net/torvald/terrarum/transaction/Transaction.kt +++ b/src/net/torvald/terrarum/transaction/Transaction.kt @@ -1,10 +1,12 @@ package net.torvald.terrarum.transaction +import net.torvald.terrarum.App.printdbg +import java.util.concurrent.atomic.AtomicReference + /** * Created by minjaesong on 2024-06-28. */ interface Transaction { - /** * Call this function to begin the transaction. * @@ -28,18 +30,23 @@ interface Transaction { abstract class TransactionListener { /** `null` if not locked, a class that acquired the lock if locked */ - var transactionLockedBy: Any? = null; private set - val transactionLocked: Boolean; get() = (transactionLockedBy != null) + private val transactionLockingClass: AtomicReference = AtomicReference(null) + val transactionLocked: Boolean; get() = (transactionLockingClass.get() != null) /** * Transaction modifies a given state to a new state, then applies the new state to the object. * The given `transaction` may only modify values which is passed to it. + * + * Transaction is fully unlocked and the previous locker is unknowable by the time `onFinally` executes. + * Note that `onFinally` runs on the same thread the actual transaction has run (GL context not available). */ fun runTransaction(transaction: Transaction, onFinally: () -> Unit = {}) { + printdbg(this, "Accepting transaction $transaction") Thread { synchronized(this) { val state = getCurrentStatusForTransaction() if (!transactionLocked) { + transactionLockingClass.set(transaction) try { transaction.start(state) // if successful: @@ -49,14 +56,17 @@ abstract class TransactionListener { } catch (e: Throwable) { // if failed, notify the failure + System.err.println("Transaction failure: generic") transaction.onFailure(e, state) } finally { + transactionLockingClass.set(null) onFinally() } } else { - transaction.onFailure(LockedException(this, transactionLockedBy), state) + System.err.println("Transaction failure: locked") + transaction.onFailure(LockedException(this, transactionLockingClass.get()), state) } } }.start() } @@ -65,10 +75,10 @@ abstract class TransactionListener { protected abstract fun commitTransaction(state: TransactionState) } -class LockedException(listener: TransactionListener, lockedBy: Any?) : +class LockedException(listener: TransactionListener, lockedBy: Transaction?) : Exception("Transaction is rejected because the class '${listener.javaClass.canonicalName}' is locked by '${lockedBy?.javaClass?.canonicalName}'") -@JvmInline value class TransactionState(val valueTable: MutableMap) { +@JvmInline value class TransactionState(val valueTable: HashMap) { operator fun get(key: String) = valueTable[key] operator fun set(key: String, value: Any?) { valueTable[key] = value