From 74e7e980b7b2e00d6e855b58741e3232b9cc2ebb Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 24 Dec 2023 23:13:50 +0900 Subject: [PATCH] musicplayer: march thru playlist/working track-to-track transition --- .../terrarum/musicplayer/gui/MusicPlayer.kt | 74 ++++++++++++------- .../modulebasegame/TerrarumMusicGovernor.kt | 68 +++++++++++++---- .../terrarum/ui/BasicDebugInfoWindow.kt | 4 +- 3 files changed, 103 insertions(+), 43 deletions(-) diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt index be3fdd384..d80fa9654 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt @@ -8,6 +8,7 @@ import com.badlogic.gdx.graphics.glutils.FrameBuffer import com.jme3.math.FastMath import net.torvald.reflection.extortField import net.torvald.terrarum.* +import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.audio.* import net.torvald.terrarum.modulebasegame.TerrarumIngame import net.torvald.terrarum.ui.BasicDebugInfoWindow @@ -52,6 +53,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { private var modeNext = MODE_IDLE private var transitionAkku = 0f private var transitionRequest: Int? = null + private var transitionOngoing = false private var TRANSITION_LENGTH = 0.44444445f @@ -67,31 +69,44 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { init { setAsAlwaysVisible() + // test code + val diskJockeyingMode = "continuous" // must be read from the playlist.json + registerPlaylist(App.customDir + "/MusicShort", false, diskJockeyingMode) + } + + fun registerPlaylist(path: String, shuffled: Boolean, diskJockeyingMode: String) { + ingame.musicGovernor.queueDirectory(path, shuffled, diskJockeyingMode) + ingame.musicGovernor.addMusicStartHook { music -> setMusicName(music.name) transitionRequest = MODE_NOW_PLAYING } + ingame.musicGovernor.addMusicStopHook { music -> - setIntermission() - transitionRequest = MODE_IDLE + if (diskJockeyingMode == "intermittent") { + setIntermission() + transitionRequest = MODE_IDLE + } } } - private var renderFBOreq: String? = "" - private var renderFBOhistory: String? = "" + private var currentMusicName = "" private var nameLength = 0 + private var nameLengthOld = 0 private var nameOverflown = false private fun setIntermission() { - renderFBOreq = "" + currentMusicName = "" nameOverflown = false } private fun setMusicName(str: String) { - renderFBOreq = str + currentMusicName = str nameLength = App.fontGameFBO.getWidth(str) - TRANSITION_LENGTH = 0.6666667f * (nameLength.coerceAtMost(nameStrMaxLen).toFloat() / nameStrMaxLen) + TRANSITION_LENGTH = 0.8f * ((nameLength.coerceAtMost(nameStrMaxLen).toFloat() - nameLengthOld).absoluteValue / nameStrMaxLen) nameOverflown = (nameLength > nameStrMaxLen) + +// printdbg(this, "setMusicName $str; strLen = $nameLengthOld -> $nameLength; overflown=$nameOverflown; transitionTime=$TRANSITION_LENGTH") } override fun updateUI(delta: Float) { @@ -103,17 +118,17 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } // actually do transition - if (mode != modeNext) { + if (transitionAkku <= TRANSITION_LENGTH) { makeTransition() - if (renderFBOhistory?.isNotBlank() == true) { - renderFBOreq = renderFBOhistory // continuously call the renderNameToFBO - } - transitionAkku += delta - if (transitionAkku > TRANSITION_LENGTH) { +// printdbg(this, "On transition... ($transitionAkku / $TRANSITION_LENGTH); width = $width") + + if (transitionAkku >= TRANSITION_LENGTH) { mode = modeNext +// printdbg(this, "Transition complete: nameLengthOld=${nameLengthOld} -> ${nameLength}") + nameLengthOld = nameLength } } @@ -123,36 +138,43 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { 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) - private fun setUIwidthFromTextWidth(textW: Int, percentage: Float) { - val zeroWidth = METERS_WIDTH - val maxWidth = (textW + METERS_WIDTH + maskOffWidth).roundToInt().toFloat() + private fun setUIwidthFromTextWidth(widthOld: Int, widthNew: Int, percentage: Float) { + val zeroWidth = if (widthOld == 0) METERS_WIDTH else (widthOld + METERS_WIDTH + maskOffWidth).roundToInt().toFloat() + val maxWidth = (widthNew + METERS_WIDTH + maskOffWidth).roundToInt().toFloat() val step = smootherstep(percentage) + +// printdbg(this, "setUIwidth: $zeroWidth -> $maxWidth; perc = $percentage") + width = FastMath.interpolateLinear(step, zeroWidth, maxWidth).roundToInt() } // changes ui width private fun makeTransition() { - transitionDB[mode to modeNext]?.invoke(transitionAkku) + transitionDB[mode to modeNext].let { + if (it == null) throw NullPointerException("No transition for $mode -> $modeNext") + it.invoke(transitionAkku) + } } private val transitionDB = HashMap, (Float) -> Unit>().also { + it[MODE_IDLE to MODE_IDLE] = { akku -> } it[MODE_IDLE to MODE_NOW_PLAYING] = { akku -> - setUIwidthFromTextWidth(nameLength, akku / TRANSITION_LENGTH) + setUIwidthFromTextWidth(nameLengthOld, nameLength, akku / TRANSITION_LENGTH) + } + it[MODE_NOW_PLAYING to MODE_NOW_PLAYING] = { akku -> + setUIwidthFromTextWidth(nameLengthOld, nameLength, akku / TRANSITION_LENGTH) } it[MODE_NOW_PLAYING to MODE_IDLE] = { akku -> - setUIwidthFromTextWidth(nameLength, (1f - (akku / TRANSITION_LENGTH).coerceAtMost(1f))) + setUIwidthFromTextWidth(nameLengthOld, nameLength, akku / TRANSITION_LENGTH) } } override fun renderUI(batch: SpriteBatch, camera: OrthographicCamera) { - if (renderFBOreq != null) { - batch.end() - renderFBOhistory = renderFBOreq?.substring(0) - renderNameToFBO(batch, camera, renderFBOreq!!, 0f..width.toFloat() - METERS_WIDTH.toInt() - maskOffWidth) - batch.begin() - renderFBOreq = null - } + batch.end() + renderNameToFBO(batch, camera, currentMusicName, 0f..width.toFloat() - METERS_WIDTH.toInt() - maskOffWidth) + batch.begin() + val posX = ((Toolkit.drawWidth - width) / 2).toFloat() val posY = (App.scr.height - App.scr.tvSafeGraphicsHeight - height).toFloat() diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt index 50f1118db..b37dedc47 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt @@ -133,9 +133,23 @@ data class MusicContainer( } class TerrarumMusicGovernor : MusicGovernor() { + private val STATE_INIT = 0 + private val STATE_FIREPLAY = 1 + private val STATE_PLAYING = 2 + private val STATE_INTERMISSION = 3 - private val songs: List = - File(App.customMusicDir).listFiles()?.mapNotNull { + + init { + musicState = STATE_INTERMISSION + } + + private var songs: List = emptyList() + private var musicBin: ArrayList = ArrayList() + private var shuffled = true + private var diskJockeyingMode = "intermittent" // intermittent, continuous + + private fun registerSongsFromDir(musicDir: String) { + songs = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { printdbg(this, "Music: ${it.absolutePath}") try { MusicContainer( @@ -151,8 +165,31 @@ class TerrarumMusicGovernor : MusicGovernor() { null } } ?: emptyList() // TODO test code + } - private var musicBin: ArrayList = ArrayList(songs.indices.toList().shuffled()) + private fun restockMUsicBin() { + musicBin = if (shuffled) ArrayList(songs.indices.toList().shuffled()) else ArrayList(songs.indices.toList()) + } + + /** + * @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) { + if (musicState != STATE_INIT && musicState != STATE_INTERMISSION) { + AudioMixer.requestFadeOut(AudioMixer.fadeBus, AudioMixer.DEFAULT_FADEOUT_LEN) // explicit call for fade-out when the game instance quits + stopMusic(AudioMixer.musicTrack.currentTrack) + } + + songs.forEach { it.gdxMusic.tryDispose() } + registerSongsFromDir(musicDir) + + this.shuffled = shuffled + this.diskJockeyingMode = diskJockeyingMode + + restockMUsicBin() + } private val ambients: List = File(App.customAmbientDir).listFiles()?.mapNotNull { @@ -175,6 +212,11 @@ class TerrarumMusicGovernor : MusicGovernor() { private val musicStartHooks = ArrayList<(MusicContainer) -> Unit>() private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>() + init { + queueDirectory(App.customMusicDir, true, "intermittent") + } + + fun addMusicStartHook(f: (MusicContainer) -> Unit) { musicStartHooks.add(f) } @@ -196,10 +238,6 @@ class TerrarumMusicGovernor : MusicGovernor() { private var warningPrinted = false - private val STATE_INIT = 0 - private val STATE_FIREPLAY = 1 - private val STATE_PLAYING = 2 - private val STATE_INTERMISSION = 3 protected var ambState = 0 protected var ambFired = false @@ -207,17 +245,17 @@ class TerrarumMusicGovernor : MusicGovernor() { private fun stopMusic(song: MusicContainer?) { musicState = STATE_INTERMISSION intermissionAkku = 0f - intermissionLength = 30f + 30f * Math.random().toFloat() // 30s-60s + intermissionLength = if (diskJockeyingMode == "intermittent") 30f + 30f * Math.random().toFloat() else 0f // 30s-60s musicFired = false - musicStopHooks.forEach { if (song != null) { it(song) } } - printdbg(this, "Intermission: $intermissionLength seconds") + if (musicStopHooks.isNotEmpty()) musicStopHooks.forEach { if (song != null) { it(song) } } + printdbg(this, "StopMusic Intermission: $intermissionLength seconds") } private fun startMusic(song: MusicContainer) { AudioMixer.startMusic(song) - printdbg(this, "Now playing: $song") + printdbg(this, "startMusic Now playing: $song") // INGAME.sendNotification("Now Playing $EMDASH ${song.name}") - musicStartHooks.forEach { it(song) } + if (musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(song) } musicState = STATE_PLAYING } @@ -227,12 +265,12 @@ class TerrarumMusicGovernor : MusicGovernor() { intermissionAkku = 0f intermissionLength = 30f + 30f * Math.random().toFloat() // 30s-60s ambFired = false - printdbg(this, "Intermission: $intermissionLength seconds") + printdbg(this, "stopAmbient Intermission: $intermissionLength seconds") } private fun startAmbient(song: MusicContainer) { AudioMixer.startAmb(song) - printdbg(this, "Now playing: $song") + printdbg(this, "startAmbient Now playing: $song") // INGAME.sendNotification("Now Playing $EMDASH ${song.name}") ambState = STATE_PLAYING } @@ -252,7 +290,7 @@ class TerrarumMusicGovernor : MusicGovernor() { val song = songs[musicBin.removeAt(0)] // prevent same song to play twice if (musicBin.isEmpty()) { - musicBin = ArrayList(songs.indices.toList().shuffled()) + restockMUsicBin() } startMusic(song) diff --git a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt index 3c661e456..c53635467 100644 --- a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt +++ b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt @@ -459,8 +459,8 @@ class BasicDebugInfoWindow : UICanvas() { val dss = AudioMixer.dynamicTracks dss.forEachIndexed { index, track -> - val px = x - (miniW + 5) * (1 + (index / 11)) - val py = y + (miniH + stripGap) * (index % 11) + val px = x - (miniW + 5) * (1 + (index / 13)) + val py = y + (miniH + stripGap) * (index % 13) drawDynamicSource(batch, px, py, track, index) } }