From a236f496410e928fa4b32c28ea16c261f8635e9a Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 9 Jul 2024 01:27:12 +0900 Subject: [PATCH] is the new musicplayer working? --- .../musicplayer/gui/MusicPlayerControl.kt | 73 +++----- .../README_for_the_best_audio_quality.md | 7 +- .../mods/musicplayer/gui/control_buttons.tga | 2 +- src/net/torvald/terrarum/MusicService.kt | 171 ++++++++++++++++-- .../gameactors/FixtureJukebox.kt | 5 - .../gameactors/FixtureMusicalTurntable.kt | 6 - .../terrarum/transaction/Transaction.kt | 19 +- 7 files changed, 198 insertions(+), 85 deletions(-) diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt index ad7c7d917..f0da327a7 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt @@ -137,18 +137,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } } - /*ingame.musicStreamer.addMusicStartHook { music -> - setMusicName(music.name) - if (mode <= MODE_PLAYING) - transitionRequest = MODE_PLAYING - } - - ingame.musicStreamer.addMusicStopHook { music -> - setIntermission() - if (mode <= MODE_PLAYING) - transitionRequest = MODE_IDLE - }*/ - setPlaylistDisplayVars(playlist) return playlist @@ -447,18 +435,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { if (index < currentPlaylist.musicList.size) { // if selected song != currently playing if (App.audioMixer.musicTrack.currentTrack == null || currentPlaylist.musicList[index] != App.audioMixer.musicTrack.currentTrack) { - // FIXME the olde way -- must be replaced with one that utilises MusicService - // rebuild playlist - //ingame.backgroundMusicPlayer.queueIndexFromPlaylist(index) - // fade out - /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { - if (!shouldPlayerBeDisabled) { - ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode - iHitTheStopButton = false - stopRequested = false - } - }*/ - MusicService.playNthSongInPlaylist(index) { iHitTheStopButton = false stopRequested = false @@ -476,22 +452,9 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // if selected album is not the same album currently playing, queue that album immediately // (navigating into the selected album involves too much complication :p) if (MusicService.currentPlaylist?.source != albumsList[index].canonicalPath) { - // FIXME the olde way -- must be replaced with one that utilises MusicService - // fade out - /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { - loadNewAlbum(albumsList[index]) - if (!shouldPlayerBeDisabled) { - ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode - iHitTheStopButton = false - stopRequested = false - } - resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) - }*/ - val playlist = loadNewAlbum(albumsList[index]) - MusicService.putNewPlaylist(playlist) { + MusicService.putNewPlaylistAndResumePlayback(playlist) { resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) - MusicService.resumePlaylistPlayback({}, {}) } } } @@ -505,8 +468,27 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // printdbg(this, "mode = $mode; req = $transitionRequest") + + // update music name disp + val musicTrack = App.audioMixer.musicTrack + val musicTrackPlaying = musicTrack.isPlaying + val musicNow = musicTrack.currentTrack + //// music changed, do something + if ((oldMusicTrackPlaying && !musicTrack.isPlaying) || (oldSong != null && musicNow == null)) { + setIntermission() + if (mode <= MODE_PLAYING && !transitionOngoing) transitionRequest = MODE_IDLE + } + else if ((!oldMusicTrackPlaying && musicTrack.isPlaying && musicNow != null) || (oldSong == null && musicNow != null) || (musicNow != oldSong && musicNow != null)) { + setMusicName(musicNow.name) + if (mode <= MODE_PLAYING && !transitionOngoing) transitionRequest = MODE_PLAYING + } + oldSong = musicNow + oldMusicTrackPlaying = musicTrackPlaying } + private var oldSong: AudioBank? = null + private var oldMusicTrackPlaying = false + private var iHitTheStopButton = false private var stopRequested = false @@ -536,18 +518,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { } } - /*private fun getPrevSongFromPlaylist(): MusicContainer? { - val list = songsInGovernor.slice(songsInGovernor.indices) // make copy of the list - val nowPlaying = App.audioMixer.musicTrack.currentTrack ?: return null - - // find current index - val currentIndex = list.indexOfFirst { it == nowPlaying } - if (currentIndex < 0) return null - - val prevIndex = (currentIndex - 1).fmod(list.size) - return list[prevIndex] - }*/ - // private fun smoothstep(x: Float) = (x*x*(3f-2f*x)).coerceIn(0f, 1f) // private fun smootherstep(x: Float) = (x*x*x*(x*(6f*x-15f)+10f)).coerceIn(0f, 1f) @@ -710,7 +680,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { // debug codes //// transaction state - if (MusicService.transactionLocked) { + /*if (MusicService.transactionLocked) { batch.color = Color.RED Toolkit.drawTextCentered(batch, App.fontSmallNumbers, "LOCKED", Toolkit.drawWidth, 0, _posY.toInt() + height + 5) } @@ -736,6 +706,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { batch.color = Color.LIGHT_GRAY App.fontSmallNumbers.draw(batch, "Playlist InternalIndices", 10f, App.scr.hf - 30f) App.fontSmallNumbers.draw(batch, "..", 10f, App.scr.hf - 16f) + */ // end of debug codes diff --git a/assets/mods/musicplayer/README_for_the_best_audio_quality.md b/assets/mods/musicplayer/README_for_the_best_audio_quality.md index 28c0ebf30..b7fe1e08e 100644 --- a/assets/mods/musicplayer/README_for_the_best_audio_quality.md +++ b/assets/mods/musicplayer/README_for_the_best_audio_quality.md @@ -29,7 +29,12 @@ playback cannot change its sampling rate mid-stream. The audio engine cannot resample an audio file with sampling rate greater than 48000 Hz, nor is capable of reading anything that is not in 16-bit bit-depth. +## Ogg Album Art Incompatibility + +Ogg files with album arts are not reliably recognised. Remove them using the following FFmpeg command: + + ffmpeg -i inputfile.ogg -map a -c copy outputfile.ogg ## tl;dr -Stereo, 48 kHz, 16 bit, WAV or OGG. \ No newline at end of file +Stereo, 48 kHz, 16 bit, WAV or OGG. diff --git a/assets/mods/musicplayer/gui/control_buttons.tga b/assets/mods/musicplayer/gui/control_buttons.tga index 909c495ba..35c64dcb9 100644 --- a/assets/mods/musicplayer/gui/control_buttons.tga +++ b/assets/mods/musicplayer/gui/control_buttons.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2de8ed38e2106f81d4b84e669bacbd96dfbcdc0e9fe84a5d8edfba15115679c6 +oid sha256:9f0d22027149cd728706ee906c7074c73a787fd08c0c908b039b59e4db2ebe51 size 76818 diff --git a/src/net/torvald/terrarum/MusicService.kt b/src/net/torvald/terrarum/MusicService.kt index 191e1ec42..69d920c55 100644 --- a/src/net/torvald/terrarum/MusicService.kt +++ b/src/net/torvald/terrarum/MusicService.kt @@ -67,7 +67,7 @@ object MusicService : TransactionListener() { currentPlaybackState.set(STATE_PLAYING) } - fun getRandomMusicInterval() = 20f + Math.random().toFloat() * 4f // longer gap (20s to 24s) + fun getRandomMusicInterval() = 16f + Math.random().toFloat() * 4f // longer gap (16s to 20s) private fun enterIntermissionAndWaitForPlaylist() { val djmode = currentPlaylist?.diskJockeyingMode ?: "intermittent" @@ -85,7 +85,7 @@ object MusicService : TransactionListener() { } fun onMusicFinishing(audio: AudioBank) { - printdbg(this, "onMusicFinishing ${audio.name}") +// printdbg(this, "onMusicFinishing ${audio.name}") enterIntermissionAndWaitForPlaylist() } @@ -108,19 +108,19 @@ object MusicService : TransactionListener() { } override fun onSuccess(state: TransactionState) { - printdbg(this, "FIREPLAY started music (${nextMusic.name})") - enterSTATE_PLAYING() +// printdbg(this, "FIREPLAY started music (${nextMusic.name})") + currentPlaybackState.set(STATE_PLAYING) // force PLAYING } override fun onFailure(e: Throwable, state: TransactionState) { - printdbg(this, "FIREPLAY resume OK but startMusic failed, entering intermission") +// printdbg(this, "FIREPLAY resume OK but startMusic failed, entering intermission") enterIntermissionAndWaitForPlaylist() // will try again } }) }, /* onFailure: (Throwable) -> Unit */ { - printdbg(this, "FIREPLAY resume failed, entering intermission") +// printdbg(this, "FIREPLAY resume failed, entering intermission") enterIntermissionAndWaitForPlaylist() // will try again }, // onFinally: () -> Unit @@ -130,7 +130,7 @@ object MusicService : TransactionListener() { ) } else { - printdbg(this, "FIREPLAY no-op: playTransaction is ongoing") +// printdbg(this, "FIREPLAY no-op: playTransaction is ongoing") } } STATE_PLAYING -> { @@ -140,7 +140,9 @@ object MusicService : TransactionListener() { waitAkku += delta if (waitAkku >= waitTime && currentPlaylist != null) { - enterSTATE_FIREPLAY() + // force FIREPLAY + waitAkku = 0f + currentPlaybackState.set(STATE_FIREPLAY) } } } @@ -240,8 +242,99 @@ object MusicService : TransactionListener() { } } + /** + * Puts the given playlist to this object if the transaction successes. If the given playlist is same as the + * current playlist, the transaction will successfully finish immediately; otherwise the given playlist will + * 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 + * @param onSuccess What to do after the transaction + */ + private fun createTransactionPlaylistChangeAndPlayImmediately(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit): Transaction { + return object : Transaction { + var oldPlaylist: TerrarumMusicPlaylist? = null + var oldState = currentPlaybackState.get() + var oldAkku = waitAkku + var oldTime = waitTime + + override fun start(state: TransactionState) { + oldPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist? + if (oldPlaylist == playlist) return + + playlist.reset() + + // request fadeout + if (App.audioMixer.musicTrack.isPlaying) { + var fadedOut = false + + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + // put new playlist + state["currentPlaylist"] = playlist + + fadedOut = true + } + + waitUntil { fadedOut } + } + else { + // put new playlist + state["currentPlaylist"] = playlist + } + + oldState = currentPlaybackState.get() + oldAkku = waitAkku + oldTime = waitTime + + enterSTATE_INTERMISSION(0f) + enterSTATE_FIREPLAY() + } + + override fun onSuccess(state: TransactionState) { + oldPlaylist?.dispose() + + (state["currentPlaylist"] as TerrarumMusicPlaylist?)?.let { + // set songFinishedHook for every song + it.musicList.forEach { + it.songFinishedHook = { + onMusicFinishing(it) + } + } + + // set gaplessness of the Music track + App.audioMixer.musicTrack.let { track -> + track.doGaplessPlayback = (it.diskJockeyingMode == "continuous") + if (track.doGaplessPlayback) { + track.pullNextTrack = { + track.currentTrack = MusicService.currentPlaylist!!.queueNext() + } + } + } + } + + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) { + e.printStackTrace() + + currentPlaybackState.set(oldState) + waitAkku = oldAkku + waitTime = oldTime + } + } + } + private fun createTransactionForNextMusicInPlaylist(onSuccess: () -> Unit): Transaction { return object : Transaction { + var oldState = currentPlaybackState.get() + var oldAkku = waitAkku + var oldTime = waitTime + override fun start(state: TransactionState) { var fadedOut = false var err: Throwable? = null @@ -259,21 +352,34 @@ object MusicService : TransactionListener() { waitUntil { fadedOut || err != null } if (err != null) throw err!! + + oldState = currentPlaybackState.get() + oldAkku = waitAkku + oldTime = waitTime + + enterSTATE_INTERMISSION(0f) + enterSTATE_FIREPLAY() } override fun onSuccess(state: TransactionState) { - enterSTATE_INTERMISSION(0f) - enterSTATE_FIREPLAY() onSuccess() } override fun onFailure(e: Throwable, state: TransactionState) { e.printStackTrace() + + currentPlaybackState.set(oldState) + waitAkku = oldAkku + waitTime = oldTime } } } private fun createTransactionForPrevMusicInPlaylist(onSuccess: () -> Unit): Transaction { return object : Transaction { + var oldState = currentPlaybackState.get() + var oldAkku = waitAkku + var oldTime = waitTime + override fun start(state: TransactionState) { var fadedOut = false var err: Throwable? = null @@ -294,11 +400,16 @@ object MusicService : TransactionListener() { waitUntil { fadedOut || err != null } if (err != null) throw err!! + + oldState = currentPlaybackState.get() + oldAkku = waitAkku + oldTime = waitTime + + enterSTATE_INTERMISSION(0f) + enterSTATE_FIREPLAY() } override fun onSuccess(state: TransactionState) { - enterSTATE_INTERMISSION(0f) - enterSTATE_FIREPLAY() onSuccess() } override fun onFailure(e: Throwable, state: TransactionState) { @@ -306,12 +417,20 @@ object MusicService : TransactionListener() { (state["currentPlaylist"] as TerrarumMusicPlaylist).queueNext() e.printStackTrace() + + currentPlaybackState.set(oldState) + waitAkku = oldAkku + waitTime = oldTime } } } private fun createTransactionForNthMusicInPlaylist(index: Int, onSuccess: () -> Unit): Transaction { return object : Transaction { + var oldState = currentPlaybackState.get() + var oldAkku = waitAkku + var oldTime = waitTime + override fun start(state: TransactionState) { var fadedOut = false var err: Throwable? = null @@ -331,15 +450,24 @@ object MusicService : TransactionListener() { waitUntil { fadedOut || err != null } if (err != null) throw err!! + + oldState = currentPlaybackState.get() + oldAkku = waitAkku + oldTime = waitTime + + enterSTATE_INTERMISSION(0f) + enterSTATE_FIREPLAY() } override fun onSuccess(state: TransactionState) { - enterSTATE_INTERMISSION(0f) - enterSTATE_FIREPLAY() onSuccess() } override fun onFailure(e: Throwable, state: TransactionState) { e.printStackTrace() + + currentPlaybackState.set(oldState) + waitAkku = oldAkku + waitTime = oldTime } } } @@ -369,6 +497,10 @@ object MusicService : TransactionListener() { private fun createTransactionForPlaylistResume(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit): Transaction { return object : Transaction { + var oldState = currentPlaybackState.get() + var oldAkku = waitAkku + var oldTime = waitTime + override fun start(state: TransactionState) { enterSTATE_FIREPLAY() } @@ -378,6 +510,11 @@ object MusicService : TransactionListener() { } override fun onFailure(e: Throwable, state: TransactionState) { e.printStackTrace() + + currentPlaybackState.set(oldState) + waitAkku = oldAkku + waitTime = oldTime + onFailure(e) } } @@ -447,6 +584,12 @@ object MusicService : TransactionListener() { else runTransaction(createTransactionPlaylistChange(playlist, {})) } + fun putNewPlaylistAndResumePlayback(playlist: TerrarumMusicPlaylist, onSuccess: (() -> Unit)? = null) { + if (onSuccess != null) + runTransaction(createTransactionPlaylistChangeAndPlayImmediately(playlist, onSuccess)) + else + runTransaction(createTransactionPlaylistChangeAndPlayImmediately(playlist, {})) + } fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit, onFinally: () -> Unit) { runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally) } diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt index 8949cf19f..a5ae350f1 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt @@ -135,11 +135,6 @@ class FixtureJukebox : Electric, PlaysMusic { discCurrentlyPlaying = index - // FIXME the olde way -- must be replaced with one that utilises MusicService - /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2f) { - startAudio(musicNowPlaying!!) { loadEffector(it) } - }*/ - MusicService.playMusicalFixture( /* action: () -> Unit */ { startAudio(musicNowPlaying!!) { loadEffector(it) } diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt index f009ba61a..47f2c8416 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt @@ -116,12 +116,6 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { App.printdbg(this, "Stop music $title - $artist") } - - // FIXME the olde way -- must be replaced with one that utilises MusicService - /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 2f) { - startAudio(musicNowPlaying!!) { loadEffector(it) } - }*/ - MusicService.playMusicalFixture( /* action: () -> Unit */ { startAudio(musicNowPlaying!!) { loadEffector(it) } diff --git a/src/net/torvald/terrarum/transaction/Transaction.kt b/src/net/torvald/terrarum/transaction/Transaction.kt index f4b0692de..af07363b1 100644 --- a/src/net/torvald/terrarum/transaction/Transaction.kt +++ b/src/net/torvald/terrarum/transaction/Transaction.kt @@ -56,6 +56,7 @@ abstract class TransactionListener { printdbg(this, "Accepting transaction $transaction") Thread { val state = getCurrentStatusForTransaction() + var wasSuccessful = false val currentLock = transactionLockingClass.get() if (currentLock == null) { acquireLock(transaction) @@ -63,23 +64,27 @@ abstract class TransactionListener { transaction.start(state) // if successful: commitTransaction(state) - // notify the success - transaction.onSuccess(state) + + wasSuccessful = true } catch (e: Throwable) { // if failed, notify the failure - System.err.println("Transaction failure: generic") + System.err.println("Transaction failure: generic (failed transaction: $transaction)") e.printStackTrace() transaction.onFailure(e, state) } finally { + if (wasSuccessful) { + // notify the success + transaction.onSuccess(state) + } releaseLock() onFinally() } } else { - System.err.println("Transaction failure: locked") - transaction.onFailure(LockedException(transaction, this, currentLock), state) + System.err.println("Transaction failure: locked (failed transaction: $transaction)") + transaction.onFailure(LockedException(this, currentLock), state) } }.start() } @@ -88,8 +93,8 @@ abstract class TransactionListener { protected abstract fun commitTransaction(state: TransactionState) } -class LockedException(offendingTransaction: Transaction, listener: TransactionListener, lockedBy: Transaction) : - Exception("Transaction '$offendingTransaction' is rejected because the class '$listener' is locked by '$lockedBy'") +class LockedException(listener: TransactionListener, lockedBy: Transaction) : + Exception("The class '$listener' is locked by '$lockedBy'") @JvmInline value class TransactionState(val valueTable: HashMap) { operator fun get(key: String) = valueTable[key]