musicplayer: march thru playlist/working track-to-track transition

This commit is contained in:
minjaesong
2023-12-24 23:13:50 +09:00
parent a19c0608f1
commit 74e7e980b7
3 changed files with 103 additions and 43 deletions

View File

@@ -8,6 +8,7 @@ import com.badlogic.gdx.graphics.glutils.FrameBuffer
import com.jme3.math.FastMath import com.jme3.math.FastMath
import net.torvald.reflection.extortField import net.torvald.reflection.extortField
import net.torvald.terrarum.* import net.torvald.terrarum.*
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.audio.* import net.torvald.terrarum.audio.*
import net.torvald.terrarum.modulebasegame.TerrarumIngame import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.ui.BasicDebugInfoWindow import net.torvald.terrarum.ui.BasicDebugInfoWindow
@@ -52,6 +53,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
private var modeNext = MODE_IDLE private var modeNext = MODE_IDLE
private var transitionAkku = 0f private var transitionAkku = 0f
private var transitionRequest: Int? = null private var transitionRequest: Int? = null
private var transitionOngoing = false
private var TRANSITION_LENGTH = 0.44444445f private var TRANSITION_LENGTH = 0.44444445f
@@ -67,31 +69,44 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
init { init {
setAsAlwaysVisible() 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 -> ingame.musicGovernor.addMusicStartHook { music ->
setMusicName(music.name) setMusicName(music.name)
transitionRequest = MODE_NOW_PLAYING transitionRequest = MODE_NOW_PLAYING
} }
ingame.musicGovernor.addMusicStopHook { music -> ingame.musicGovernor.addMusicStopHook { music ->
setIntermission() if (diskJockeyingMode == "intermittent") {
transitionRequest = MODE_IDLE setIntermission()
transitionRequest = MODE_IDLE
}
} }
} }
private var renderFBOreq: String? = "" private var currentMusicName = ""
private var renderFBOhistory: String? = ""
private var nameLength = 0 private var nameLength = 0
private var nameLengthOld = 0
private var nameOverflown = false private var nameOverflown = false
private fun setIntermission() { private fun setIntermission() {
renderFBOreq = "" currentMusicName = ""
nameOverflown = false nameOverflown = false
} }
private fun setMusicName(str: String) { private fun setMusicName(str: String) {
renderFBOreq = str currentMusicName = str
nameLength = App.fontGameFBO.getWidth(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) nameOverflown = (nameLength > nameStrMaxLen)
// printdbg(this, "setMusicName $str; strLen = $nameLengthOld -> $nameLength; overflown=$nameOverflown; transitionTime=$TRANSITION_LENGTH")
} }
override fun updateUI(delta: Float) { override fun updateUI(delta: Float) {
@@ -103,17 +118,17 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
} }
// actually do transition // actually do transition
if (mode != modeNext) { if (transitionAkku <= TRANSITION_LENGTH) {
makeTransition() makeTransition()
if (renderFBOhistory?.isNotBlank() == true) {
renderFBOreq = renderFBOhistory // continuously call the renderNameToFBO
}
transitionAkku += delta transitionAkku += delta
if (transitionAkku > TRANSITION_LENGTH) { // printdbg(this, "On transition... ($transitionAkku / $TRANSITION_LENGTH); width = $width")
if (transitionAkku >= TRANSITION_LENGTH) {
mode = modeNext 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 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 smootherstep(x: Float) = (x*x*x*(x*(6f*x-15f)+10f)).coerceIn(0f, 1f)
private fun setUIwidthFromTextWidth(textW: Int, percentage: Float) { private fun setUIwidthFromTextWidth(widthOld: Int, widthNew: Int, percentage: Float) {
val zeroWidth = METERS_WIDTH val zeroWidth = if (widthOld == 0) METERS_WIDTH else (widthOld + METERS_WIDTH + maskOffWidth).roundToInt().toFloat()
val maxWidth = (textW + METERS_WIDTH + maskOffWidth).roundToInt().toFloat() val maxWidth = (widthNew + METERS_WIDTH + maskOffWidth).roundToInt().toFloat()
val step = smootherstep(percentage) val step = smootherstep(percentage)
// printdbg(this, "setUIwidth: $zeroWidth -> $maxWidth; perc = $percentage")
width = FastMath.interpolateLinear(step, zeroWidth, maxWidth).roundToInt() width = FastMath.interpolateLinear(step, zeroWidth, maxWidth).roundToInt()
} }
// changes ui width // changes ui width
private fun makeTransition() { 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<Pair<Int, Int>, (Float) -> Unit>().also { private val transitionDB = HashMap<Pair<Int, Int>, (Float) -> Unit>().also {
it[MODE_IDLE to MODE_IDLE] = { akku -> }
it[MODE_IDLE to MODE_NOW_PLAYING] = { 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 -> 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) { override fun renderUI(batch: SpriteBatch, camera: OrthographicCamera) {
if (renderFBOreq != null) { batch.end()
batch.end() renderNameToFBO(batch, camera, currentMusicName, 0f..width.toFloat() - METERS_WIDTH.toInt() - maskOffWidth)
renderFBOhistory = renderFBOreq?.substring(0) batch.begin()
renderNameToFBO(batch, camera, renderFBOreq!!, 0f..width.toFloat() - METERS_WIDTH.toInt() - maskOffWidth)
batch.begin()
renderFBOreq = null
}
val posX = ((Toolkit.drawWidth - width) / 2).toFloat() val posX = ((Toolkit.drawWidth - width) / 2).toFloat()
val posY = (App.scr.height - App.scr.tvSafeGraphicsHeight - height).toFloat() val posY = (App.scr.height - App.scr.tvSafeGraphicsHeight - height).toFloat()

View File

@@ -133,9 +133,23 @@ data class MusicContainer(
} }
class TerrarumMusicGovernor : MusicGovernor() { 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<MusicContainer> =
File(App.customMusicDir).listFiles()?.mapNotNull { init {
musicState = STATE_INTERMISSION
}
private var songs: List<MusicContainer> = emptyList()
private var musicBin: ArrayList<Int> = 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}") printdbg(this, "Music: ${it.absolutePath}")
try { try {
MusicContainer( MusicContainer(
@@ -151,8 +165,31 @@ class TerrarumMusicGovernor : MusicGovernor() {
null null
} }
} ?: emptyList() // TODO test code } ?: emptyList() // TODO test code
}
private var musicBin: ArrayList<Int> = 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<MusicContainer> = private val ambients: List<MusicContainer> =
File(App.customAmbientDir).listFiles()?.mapNotNull { File(App.customAmbientDir).listFiles()?.mapNotNull {
@@ -175,6 +212,11 @@ class TerrarumMusicGovernor : MusicGovernor() {
private val musicStartHooks = ArrayList<(MusicContainer) -> Unit>() private val musicStartHooks = ArrayList<(MusicContainer) -> Unit>()
private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>() private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>()
init {
queueDirectory(App.customMusicDir, true, "intermittent")
}
fun addMusicStartHook(f: (MusicContainer) -> Unit) { fun addMusicStartHook(f: (MusicContainer) -> Unit) {
musicStartHooks.add(f) musicStartHooks.add(f)
} }
@@ -196,10 +238,6 @@ class TerrarumMusicGovernor : MusicGovernor() {
private var warningPrinted = false 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 ambState = 0
protected var ambFired = false protected var ambFired = false
@@ -207,17 +245,17 @@ class TerrarumMusicGovernor : MusicGovernor() {
private fun stopMusic(song: MusicContainer?) { private fun stopMusic(song: MusicContainer?) {
musicState = STATE_INTERMISSION musicState = STATE_INTERMISSION
intermissionAkku = 0f 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 musicFired = false
musicStopHooks.forEach { if (song != null) { it(song) } } if (musicStopHooks.isNotEmpty()) musicStopHooks.forEach { if (song != null) { it(song) } }
printdbg(this, "Intermission: $intermissionLength seconds") printdbg(this, "StopMusic Intermission: $intermissionLength seconds")
} }
private fun startMusic(song: MusicContainer) { private fun startMusic(song: MusicContainer) {
AudioMixer.startMusic(song) AudioMixer.startMusic(song)
printdbg(this, "Now playing: $song") printdbg(this, "startMusic Now playing: $song")
// INGAME.sendNotification("Now Playing $EMDASH ${song.name}") // INGAME.sendNotification("Now Playing $EMDASH ${song.name}")
musicStartHooks.forEach { it(song) } if (musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(song) }
musicState = STATE_PLAYING musicState = STATE_PLAYING
} }
@@ -227,12 +265,12 @@ class TerrarumMusicGovernor : MusicGovernor() {
intermissionAkku = 0f intermissionAkku = 0f
intermissionLength = 30f + 30f * Math.random().toFloat() // 30s-60s intermissionLength = 30f + 30f * Math.random().toFloat() // 30s-60s
ambFired = false ambFired = false
printdbg(this, "Intermission: $intermissionLength seconds") printdbg(this, "stopAmbient Intermission: $intermissionLength seconds")
} }
private fun startAmbient(song: MusicContainer) { private fun startAmbient(song: MusicContainer) {
AudioMixer.startAmb(song) AudioMixer.startAmb(song)
printdbg(this, "Now playing: $song") printdbg(this, "startAmbient Now playing: $song")
// INGAME.sendNotification("Now Playing $EMDASH ${song.name}") // INGAME.sendNotification("Now Playing $EMDASH ${song.name}")
ambState = STATE_PLAYING ambState = STATE_PLAYING
} }
@@ -252,7 +290,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
val song = songs[musicBin.removeAt(0)] val song = songs[musicBin.removeAt(0)]
// prevent same song to play twice // prevent same song to play twice
if (musicBin.isEmpty()) { if (musicBin.isEmpty()) {
musicBin = ArrayList(songs.indices.toList().shuffled()) restockMUsicBin()
} }
startMusic(song) startMusic(song)

View File

@@ -459,8 +459,8 @@ class BasicDebugInfoWindow : UICanvas() {
val dss = AudioMixer.dynamicTracks val dss = AudioMixer.dynamicTracks
dss.forEachIndexed { index, track -> dss.forEachIndexed { index, track ->
val px = x - (miniW + 5) * (1 + (index / 11)) val px = x - (miniW + 5) * (1 + (index / 13))
val py = y + (miniH + stripGap) * (index % 11) val py = y + (miniH + stripGap) * (index % 13)
drawDynamicSource(batch, px, py, track, index) drawDynamicSource(batch, px, py, track, index)
} }
} }