transaction-based music playback managing wip

This commit is contained in:
minjaesong
2024-07-04 21:34:27 +09:00
parent 7582eae1ee
commit 69571b6a3f
15 changed files with 688 additions and 132 deletions

8
.idea/kotlinc.xml generated
View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Kotlin2JvmCompilerArguments"> <component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="17" /> <option name="jvmTarget" value="21" />
</component> </component>
<component name="KotlinCommonCompilerArguments"> <component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="1.8" /> <option name="apiVersion" value="1.9" />
<option name="languageVersion" value="1.8" /> <option name="languageVersion" value="1.9" />
</component> </component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.0" /> <option name="version" value="1.9.23-release-779" />
</component> </component>
</project> </project>

View File

@@ -1,17 +1,16 @@
package net.torvald.terrarum.musicplayer package net.torvald.terrarum.musicplayer
import net.torvald.terrarum.IngameInstance
import net.torvald.terrarum.ModMgr import net.torvald.terrarum.ModMgr
import net.torvald.terrarum.ModuleEntryPoint import net.torvald.terrarum.ModuleEntryPoint
import net.torvald.terrarum.modulebasegame.TerrarumIngame import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.musicplayer.gui.MusicPlayer import net.torvald.terrarum.musicplayer.gui.MusicPlayerControl
/** /**
* Created by minjaesong on 2023-12-23. * Created by minjaesong on 2023-12-23.
*/ */
class EntryPoint : ModuleEntryPoint() { class EntryPoint : ModuleEntryPoint() {
override fun invoke() { override fun invoke() {
ModMgr.GameExtraGuiLoader.register { ingame: TerrarumIngame -> MusicPlayer(ingame) } ModMgr.GameExtraGuiLoader.register { ingame: TerrarumIngame -> MusicPlayerControl(ingame) }
} }
override fun dispose() { override fun dispose() {

View File

@@ -10,7 +10,6 @@ import com.badlogic.gdx.utils.JsonValue
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.audio.audiobank.MusicContainer import net.torvald.terrarum.audio.audiobank.MusicContainer
import net.torvald.terrarum.gameworld.fmod import net.torvald.terrarum.gameworld.fmod
@@ -33,7 +32,7 @@ import kotlin.math.*
* *
* Created by minjaesong on 2023-12-23. * Created by minjaesong on 2023-12-23.
*/ */
class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() {
private val STRIP_W = 9f private val STRIP_W = 9f
private val METERS_WIDTH = 2 * STRIP_W private val METERS_WIDTH = 2 * STRIP_W
@@ -96,20 +95,21 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
/** Returns the internal playlist of the MusicGovernor */ /** Returns the internal playlist of the MusicGovernor */
private val songsInGovernor: List<MusicContainer> private val songsInGovernor: List<MusicContainer>
get() = ingame.musicGovernor.extortField<List<MusicContainer>>("songs")!! get() = ingame.backgroundMusicPlayer.extortField<List<MusicContainer>>("songs")!!
private val shouldPlayerBeDisabled: Boolean /*private val shouldPlayerBeDisabled: Boolean
get() { get() {
return App.audioMixer.dynamicTracks.any { it.isPlaying && it.trackingTarget is PlaysMusic } return MusicService.transactionLocked
} //return App.audioMixer.dynamicTracks.any { it.isPlaying && it.trackingTarget is PlaysMusic }
}*/
/** Returns the playlist name from the MusicGovernor. Getting the value from the MusicGovernor /** Returns the playlist name from the MusicGovernor. Getting the value from the MusicGovernor
* is recommended as an ingame interaction may cancel the playback from the playlist from the MusicPlayer * is recommended as an ingame interaction may cancel the playback from the playlist from the MusicPlayer
* (e.g. interacting with a jukebox) */ * (e.g. interacting with a jukebox) */
private val internalPlaylistName: String private val internalPlaylistName: String
get() = ingame.musicGovernor.playlistName get() = ingame.backgroundMusicPlayer.playlistName
fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String) { fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String): TerrarumMusicPlaylist {
fun String.isNum(): Boolean { fun String.isNum(): Boolean {
try { try {
this.toInt() this.toInt()
@@ -120,7 +120,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
} }
} }
ingame.musicGovernor.queueDirectory(path, shuffled, diskJockeyingMode) { filename -> val playlist = ingame.backgroundMusicPlayer.queueDirectory(path, shuffled, diskJockeyingMode, false) { filename ->
fileToName?.get(filename).let { fileToName?.get(filename).let {
if (it == null) if (it == null)
filename.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.let { filename.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.let {
@@ -136,19 +136,21 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
} }
} }
ingame.musicGovernor.addMusicStartHook { music -> ingame.backgroundMusicPlayer.addMusicStartHook { music ->
setMusicName(music.name) setMusicName(music.name)
if (mode <= MODE_PLAYING) if (mode <= MODE_PLAYING)
transitionRequest = MODE_PLAYING transitionRequest = MODE_PLAYING
} }
ingame.musicGovernor.addMusicStopHook { music -> ingame.backgroundMusicPlayer.addMusicStopHook { music ->
setIntermission() setIntermission()
if (mode <= MODE_PLAYING) if (mode <= MODE_PLAYING)
transitionRequest = MODE_IDLE transitionRequest = MODE_IDLE
} }
setPlaylistDisplayVars(songsInGovernor) setPlaylistDisplayVars(songsInGovernor)
return playlist
} }
private var currentMusicName = "" private var currentMusicName = ""
@@ -186,7 +188,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
private var mouseOnList: Int? = null private var mouseOnList: Int? = null
override fun updateImpl(delta: Float) { override fun updateImpl(delta: Float) {
val shouldPlayerBeDisabled = shouldPlayerBeDisabled val transactionLocked = MusicService.transactionLocked
// process transition request // process transition request
if (transitionRequest != null) { if (transitionRequest != null) {
@@ -345,16 +347,10 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
1 -> { // prev 1 -> { // prev
// prev song // prev song
if (mode < MODE_SHOW_LIST) { if (mode < MODE_SHOW_LIST) {
getPrevSongFromPlaylist()?.let { ingame.musicGovernor.unshiftPlaylist(it) } MusicService.playPrevSongInPlaylist {
if (!shouldPlayerBeDisabled) { ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
App.audioMixer.requestFadeOut( iHitTheStopButton = false
App.audioMixer.musicTrack, stopRequested = false
AudioMixer.DEFAULT_FADEOUT_LEN / 3f
) {
ingame.musicGovernor.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false
stopRequested = false
}
} }
} }
// prev page in the playlist // prev page in the playlist
@@ -372,18 +368,31 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
2 -> { // stop 2 -> { // stop
if (mode < MODE_SHOW_LIST) { // disable stop button entirely on MODE_SHOW_LIST if (mode < MODE_SHOW_LIST) { // disable stop button entirely on MODE_SHOW_LIST
// when the button is STOP
if (App.audioMixer.musicTrack.isPlaying) { if (App.audioMixer.musicTrack.isPlaying) {
val thisMusic = App.audioMixer.musicTrack.currentTrack // FIXME the olde way -- must be replaced with one that utilises MusicService
/*val thisMusic = App.audioMixer.musicTrack.currentTrack
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f)
App.audioMixer.musicTrack.nextTrack = null App.audioMixer.musicTrack.nextTrack = null
ingame.musicGovernor.stopMusic(this) ingame.backgroundMusicPlayer.stopMusic(this)
if (thisMusic is MusicContainer) thisMusic.let { ingame.musicGovernor.queueMusicToPlayNext(it) } if (thisMusic is MusicContainer) thisMusic.let { ingame.backgroundMusicPlayer.queueMusicToPlayNext(it) }
iHitTheStopButton = true iHitTheStopButton = true*/
MusicService.stopPlaylistPlayback {
iHitTheStopButton = true
}
} }
else if (!shouldPlayerBeDisabled) { // when the button is PLAY
ingame.musicGovernor.startMusic(this) else if (!App.audioMixer.musicTrack.isPlaying) {
// FIXME the olde way -- must be replaced with one that utilises MusicService
/*ingame.backgroundMusicPlayer.startMusic(this)
iHitTheStopButton = false iHitTheStopButton = false
stopRequested = false stopRequested = false*/
MusicService.resumePlaylistPlayback {
iHitTheStopButton = false
stopRequested = false
}
} }
} }
} }
@@ -391,15 +400,10 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
3 -> { // next 3 -> { // next
// next song // next song
if (mode < MODE_SHOW_LIST) { if (mode < MODE_SHOW_LIST) {
if (!shouldPlayerBeDisabled) { MusicService.playNextSongInPlaylist {
App.audioMixer.requestFadeOut( ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
App.audioMixer.musicTrack, iHitTheStopButton = false
AudioMixer.DEFAULT_FADEOUT_LEN / 3f stopRequested = false
) {
ingame.musicGovernor.startMusic(this) // required for "intermittent" mode, does seemingly nothing on "continuous" mode
iHitTheStopButton = false
stopRequested = false
}
} }
} }
// next page in the playlist // next page in the playlist
@@ -435,28 +439,34 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
} }
} }
} }
// make playlist clicking work // make playlist clicking (change song within the playlist) work
else if (listViewPanelScroll == 1f && mouseOnList != null) { else if (listViewPanelScroll == 1f && mouseOnList != null) {
val index = playlistScroll + mouseOnList!! val index = playlistScroll + mouseOnList!!
val list = songsInGovernor val list = songsInGovernor
if (index < list.size) { if (index < list.size) {
// if selected song != currently playing // if selected song != currently playing
if (App.audioMixer.musicTrack.currentTrack == null || list[index] != App.audioMixer.musicTrack.currentTrack) { if (App.audioMixer.musicTrack.currentTrack == null || list[index] != App.audioMixer.musicTrack.currentTrack) {
// FIXME the olde way -- must be replaced with one that utilises MusicService
// rebuild playlist // rebuild playlist
ingame.musicGovernor.queueIndexFromPlaylist(index) //ingame.backgroundMusicPlayer.queueIndexFromPlaylist(index)
// fade out // fade out
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) {
if (!shouldPlayerBeDisabled) { if (!shouldPlayerBeDisabled) {
ingame.musicGovernor.startMusic(this) // required for "intermittent" mode ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false iHitTheStopButton = false
stopRequested = false stopRequested = false
} }
}*/
MusicService.playNthSongInPlaylist(index) {
ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false
stopRequested = false
} }
} }
} }
} }
// make album list clicking work // make album list clicking (send new playlist to the MusicService) work
else if (listViewPanelScroll == 0f && mouseOnList != null) { else if (listViewPanelScroll == 0f && mouseOnList != null) {
val index = albumlistScroll + mouseOnList!! val index = albumlistScroll + mouseOnList!!
val list = albumsList//.map { albumPropCache[it] } val list = albumsList//.map { albumPropCache[it] }
@@ -464,16 +474,23 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
if (index < list.size) { if (index < list.size) {
// if selected album is not the same album currently playing, queue that album immediately // if selected album is not the same album currently playing, queue that album immediately
// (navigating into the selected album involves too much complication :p) // (navigating into the selected album involves too much complication :p)
if (ingame.musicGovernor.playlistSource != albumsList[index].canonicalPath) { if (ingame.backgroundMusicPlayer.playlistSource != albumsList[index].canonicalPath) {
// FIXME the olde way -- must be replaced with one that utilises MusicService
// fade out // fade out
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) {
loadNewAlbum(albumsList[index]) loadNewAlbum(albumsList[index])
if (!shouldPlayerBeDisabled) { if (!shouldPlayerBeDisabled) {
ingame.musicGovernor.startMusic(this) // required for "intermittent" mode ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false iHitTheStopButton = false
stopRequested = false stopRequested = false
} }
resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer)
}*/
val playlist = loadNewAlbum(albumsList[index])
MusicService.putNewPlaylist(playlist) {
resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer)
App.audioMixer.startMusic(playlist.getCurrent())
} }
} }
} }
@@ -487,16 +504,16 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
// printdbg(this, "mode = $mode; req = $transitionRequest") // printdbg(this, "mode = $mode; req = $transitionRequest")
if (shouldPlayerBeDisabled || iHitTheStopButton) { /*if (shouldPlayerBeDisabled || iHitTheStopButton) {
if (!stopRequested) { if (!stopRequested) {
stopRequested = true stopRequested = true
ingame.musicGovernor.stopMusic(this) ingame.backgroundMusicPlayer.stopMusic(this)
} }
} }
else if (ingame.musicGovernor.playCaller is PlaysMusic && !jukeboxStopMonitorAlert && !App.audioMixer.musicTrack.isPlaying) { else*/ if (ingame.backgroundMusicPlayer.playCaller is PlaysMusic && !jukeboxStopMonitorAlert && !App.audioMixer.musicTrack.isPlaying) {
jukeboxStopMonitorAlert = true jukeboxStopMonitorAlert = true
val interval = ingame.musicGovernor.getRandomMusicInterval() val interval = ingame.backgroundMusicPlayer.getRandomMusicInterval()
ingame.musicGovernor.stopMusic(this, false, interval) ingame.backgroundMusicPlayer.stopMusic(this, false, interval)
} }
else if (App.audioMixer.musicTrack.isPlaying) { else if (App.audioMixer.musicTrack.isPlaying) {
jukeboxStopMonitorAlert = false jukeboxStopMonitorAlert = false
@@ -508,7 +525,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
private var stopRequested = false private var stopRequested = false
private fun resetAlbumlistScroll() { private fun resetAlbumlistScroll() {
val currentlyPlaying = albumsList.indexOfFirst { it.canonicalPath.replace('\\', '/') == ingame.musicGovernor.playlistSource } val currentlyPlaying = albumsList.indexOfFirst { it.canonicalPath.replace('\\', '/') == ingame.backgroundMusicPlayer.playlistSource }
if (currentlyPlaying >= 0) { if (currentlyPlaying >= 0) {
albumlistScroll = (currentlyPlaying / PLAYLIST_LINES) * PLAYLIST_LINES albumlistScroll = (currentlyPlaying / PLAYLIST_LINES) * PLAYLIST_LINES
} }
@@ -527,7 +544,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
} }
} }
private fun getPrevSongFromPlaylist(): MusicContainer? { /*private fun getPrevSongFromPlaylist(): MusicContainer? {
val list = songsInGovernor.slice(songsInGovernor.indices) // make copy of the list val list = songsInGovernor.slice(songsInGovernor.indices) // make copy of the list
val nowPlaying = App.audioMixer.musicTrack.currentTrack ?: return null val nowPlaying = App.audioMixer.musicTrack.currentTrack ?: return null
@@ -537,7 +554,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
val prevIndex = (currentIndex - 1).fmod(list.size) val prevIndex = (currentIndex - 1).fmod(list.size)
return list[prevIndex] return list[prevIndex]
} }*/
// 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)
@@ -919,7 +936,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
val pnum = i + albumlistScroll val pnum = i + albumlistScroll
val currentlyPlaying = if (pnum in albumsList.indices) { val currentlyPlaying = if (pnum in albumsList.indices) {
val m1 = ingame.musicGovernor.playlistSource val m1 = ingame.backgroundMusicPlayer.playlistSource
val m2 = albumsList[pnum].canonicalPath.replace('\\', '/') val m2 = albumsList[pnum].canonicalPath.replace('\\', '/')
(m1 == m2) (m1 == m2)
} }
@@ -1279,14 +1296,14 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
val albumArt: TextureRegion? = null val albumArt: TextureRegion? = null
) )
private fun loadNewAlbum(albumDir: File) { private fun loadNewAlbum(albumDir: File): TerrarumMusicPlaylist {
val albumProp = albumPropCache[albumDir] val albumProp = albumPropCache[albumDir]
App.audioMixer.musicTrack.let { track -> App.audioMixer.musicTrack.let { track ->
track.doGaplessPlayback = (albumProp.diskJockeyingMode == "continuous") track.doGaplessPlayback = (albumProp.diskJockeyingMode == "continuous")
if (track.doGaplessPlayback) { if (track.doGaplessPlayback) {
track.pullNextTrack = { track.pullNextTrack = {
track.currentTrack = ingame.musicGovernor.pullNextMusicTrack(true) track.currentTrack = ingame.backgroundMusicPlayer.pullNextMusicTrack(true)
setMusicName(track.currentTrack?.name ?: "") setMusicName(track.currentTrack?.name ?: "")
} }
} }
@@ -1294,7 +1311,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() {
currentlySelectedAlbum = albumProp currentlySelectedAlbum = albumProp
registerPlaylist(albumDir.absolutePath, albumProp.fileToName, albumProp.shuffled, albumProp.diskJockeyingMode) return registerPlaylist(albumDir.absolutePath, albumProp.fileToName, albumProp.shuffled, albumProp.diskJockeyingMode)
// scroll playlist to the page current song is // scroll playlist to the page current song is
} }

View File

@@ -1,6 +1,6 @@
package net.torvald.terrarum package net.torvald.terrarum
open class MusicGovernor { open class BackgroundMusicPlayer {
open fun update(ingameInstance: IngameInstance, delta: Float) { open fun update(ingameInstance: IngameInstance, delta: Float) {

View File

@@ -1,12 +1,8 @@
package net.torvald.terrarum package net.torvald.terrarum
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.utils.Disposable
import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
import net.torvald.terrarum.audio.audiobank.MusicContainer
import net.torvald.terrarum.audio.dsp.BinoPan
import net.torvald.terrarum.gameactors.Actor import net.torvald.terrarum.gameactors.Actor
import net.torvald.terrarum.gameactors.ActorID import net.torvald.terrarum.gameactors.ActorID
import net.torvald.terrarum.gameactors.ActorWithBody import net.torvald.terrarum.gameactors.ActorWithBody
@@ -37,7 +33,6 @@ import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.util.* import java.util.*
import java.util.concurrent.locks.Lock import java.util.concurrent.locks.Lock
import java.util.function.Consumer
import kotlin.math.min import kotlin.math.min
/** /**
@@ -586,7 +581,7 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo
noticelet.sendNotification(itemID, itemCount) noticelet.sendNotification(itemID, itemCount)
} }
open val musicGovernor: MusicGovernor = MusicGovernor() open val backgroundMusicPlayer: BackgroundMusicPlayer = BackgroundMusicPlayer()
} }
inline fun Lock.lock(body: () -> Unit) { inline fun Lock.lock(body: () -> Unit) {

View File

@@ -0,0 +1,259 @@
package net.torvald.terrarum
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
/**
* To play the music, create a transaction then pass it to the `runTransaction(Transaction)`
*
* Created by minjaesong on 2024-06-28.
*/
object MusicService : TransactionListener() {
private var currentPlaylist: TerrarumMusicPlaylist? = null
override fun getCurrentStatusForTransaction(): TransactionState {
return TransactionState(
mutableMapOf(
"currentPlaylist" to currentPlaylist
)
)
}
override fun commitTransaction(state: TransactionState) {
this.currentPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist?
}
/**
* 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)
*
* 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. Default behaviour is: `App.audioMixer.startMusic(playlist.getCurrent())`
*/
private fun createTransactionPlaylistChange(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit = {
App.audioMixer.startMusic(playlist.getCurrent())
}): Transaction {
return object : Transaction {
var oldPlaylist: TerrarumMusicPlaylist? = null
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 {
/* do nothing */
}
}
override fun onSuccess(state: TransactionState) {
oldPlaylist?.dispose()
onSuccess()
}
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionForNextMusicInPlaylist(onSuccess: () -> Unit): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
var fadedOut = false
// 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
}
waitUntil { fadedOut }
}
override fun onSuccess(state: TransactionState) {
onSuccess()
}
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionForPrevMusicInPlaylist(onSuccess: () -> Unit): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
var fadedOut = false
// 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
}
waitUntil { fadedOut }
}
override fun onSuccess(state: TransactionState) {
onSuccess()
}
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionForNthMusicInPlaylist(index: Int, onSuccess: () -> Unit): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
var fadedOut = false
// 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
}
waitUntil { fadedOut }
}
override fun onSuccess(state: TransactionState) { onSuccess() }
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionForPlaylistStop(onSuccess: () -> Unit): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
var fadedOut = false
// request fadeout
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) {
fadedOut = true
}
waitUntil { fadedOut }
}
override fun onSuccess(state: TransactionState) { onSuccess() }
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionForPlaylistResume(onSuccess: () -> Unit): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getCurrent())
}
override fun onSuccess(state: TransactionState) { onSuccess() }
override fun onFailure(e: Throwable, state: TransactionState) {}
}
}
private fun createTransactionPausePlaylistForMusicalFixture(
action: () -> Unit,
musicFinished: () -> Boolean,
onSuccess: () -> Unit,
onFailure: (Throwable) -> Unit
): Transaction {
return object : Transaction {
override fun start(state: TransactionState) {
var fadedOut = false
// 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()
fadedOut = true
}
waitUntil { fadedOut }
// wait until the interjected music finishes
waitUntil { musicFinished() }
}
override fun onSuccess(state: TransactionState) { onSuccess() }
override fun onFailure(e: Throwable, state: TransactionState) { onFailure(e) }
}
// note to self: wait() and notify() using a lock object is impractical as the Java thread can wake up
// randomly regardless of the notify(), which results in the common pattern of
// while (!condition) { lock.wait() }
// and if we need extra condition (i.e. musicFinished()), it's just a needlessly elaborate way of spinning,
// UNLESS THE THING MUST BE SYNCHRONISED WITH SOMETHING
}
private fun waitUntil(escapeCondition: () -> Boolean) {
while (!escapeCondition()) {
Thread.sleep(4L)
}
}
fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: (() -> Unit)? = null) {
if (onSuccess != null)
runTransaction(createTransactionPlaylistChange(playlist, onSuccess))
else
runTransaction(createTransactionPlaylistChange(playlist))
}
fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally)
}
fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) {
runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure))
}
fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, onFinally: () -> Unit = {}) {
runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure), onFinally)
}
fun playNextSongInPlaylist(onSuccess: () -> Unit) {
runTransaction(createTransactionForNextMusicInPlaylist(onSuccess))
}
fun playNextSongInPlaylist(onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionForNextMusicInPlaylist(onSuccess), onFinally)
}
fun playPrevSongInPlaylist(onSuccess: () -> Unit) {
runTransaction(createTransactionForPrevMusicInPlaylist(onSuccess))
}
fun playPrevSongInPlaylist(onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionForPrevMusicInPlaylist(onSuccess), onFinally)
}
fun playNthSongInPlaylist(index: Int, onSuccess: () -> Unit) {
runTransaction(createTransactionForNthMusicInPlaylist(index, onSuccess))
}
fun playNthSongInPlaylist(index: Int, onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionForNthMusicInPlaylist(index, onSuccess), onFinally)
}
fun stopPlaylistPlayback(onSuccess: () -> Unit) {
runTransaction(createTransactionForPlaylistStop(onSuccess))
}
fun stopPlaylistPlayback(onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionForPlaylistStop(onSuccess), onFinally)
}
fun resumePlaylistPlayback(onSuccess: () -> Unit) {
runTransaction(createTransactionForPlaylistResume(onSuccess))
}
fun resumePlaylistPlayback(onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionForPlaylistResume(onSuccess), onFinally)
}
}

View File

@@ -0,0 +1,142 @@
package net.torvald.terrarum
import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.GdxRuntimeException
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.audio.audiobank.MusicContainer
import java.io.File
/**
* The `musicList` never (read: should not) gets changed, only the `internalIndices` are being changed as
* the songs are being played.
*
* Created by minjaesong on 2024-06-29.
*/
class TerrarumMusicPlaylist(
/** list of files */
val musicList: List<MusicContainer>,
/** name of the album/playlist shown in the [net.torvald.terrarum.musicplayer.gui.MusicPlayer] */
val name: String,
/** canonicalPath with path separators converted to forward slash */
val source: String,
/** "continuous", "intermittent"; not used by the Playlist itself but by the BackgroundMusicPlayer (aka you are the one who make it actually work) */
val diskJockeyingMode: String,
/** if set, the `internalIndices` will be shuffled accordingly, and this happens automatically. (aka you don't need to worry about) */
val shuffled: Boolean
): Disposable {
private val internalIndices = ArrayList<Int>()
private var currentIndexCursor = musicList.size
init {
reset()
}
fun reset() {
internalIndices.clear()
refillInternalIndices()
refillInternalIndices()
currentIndexCursor = musicList.size
}
private fun checkRefill() {
if (internalIndices.size < currentIndexCursor + 1)
refillInternalIndices()
}
fun getCurrent(): MusicContainer {
checkRefill()
return musicList[currentIndexCursor]
}
fun getNext(): MusicContainer {
checkRefill()
currentIndexCursor += 1
return musicList[currentIndexCursor]
}
fun getPrev(): MusicContainer {
if (currentIndexCursor == 0) {
if (shuffled) {
musicList.indices.toMutableList().also { if (shuffled) it.shuffle() }.reversed().forEach {
internalIndices.add(0, it)
}
currentIndexCursor += musicList.size
}
else {
musicList.indices.reversed().forEach {
internalIndices.add(0, it)
}
currentIndexCursor += musicList.size
}
}
currentIndexCursor -= 1
return musicList[currentIndexCursor]
}
private fun refillInternalIndices() {
internalIndices.addAll(musicList.indices.toMutableList().also { if (shuffled) it.shuffle() })
}
inline fun getNthSong(n: Int) = musicList[n]
override fun dispose() {
musicList.forEach {
it.tryDispose()
}
}
companion object {
/**
* Adding songFinishedHook to the songs is a responsibility of the caller.
*/
fun fromDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, fileToName: ((String) -> String) = { name: String ->
name.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.joinToString(" ")
}): TerrarumMusicPlaylist {
val musicDir = musicDir.replace('\\', '/')
val playlistSource = musicDir
printdbg(this, "registerSongsFromDir $musicDir")
val playlistName = musicDir.substringAfterLast('/')
val playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull {
printdbg(this, "Music: ${it.absolutePath}")
try {
MusicContainer(
fileToName(it.name),
it
)/*.also { muscon ->
printdbg(this, "MusicTitle: ${muscon.name}")
muscon.songFinishedHook = {
if (App.audioMixer.musicTrack.currentTrack == it) {
stopMusic(this, true, getRandomMusicInterval())
}
}
}*/
// adding songFinishedHook must be done by the caller
}
catch (e: GdxRuntimeException) {
e.printStackTrace()
null
}
} ?: emptyList() // TODO test code
return TerrarumMusicPlaylist(
playlist,
playlistName,
musicDir,
diskJockeyingMode,
shuffled
)
}
}
}

View File

@@ -529,7 +529,11 @@ class AudioMixer : Disposable {
// fade will be processed by the update() // fade will be processed by the update()
} }
fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double = DEFAULT_FADEOUT_LEN, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { /**
* Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used
* to control the music playback instead.
*/
internal fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double = DEFAULT_FADEOUT_LEN, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) {
val req = fadeReqs[track]!! val req = fadeReqs[track]!!
if (!req.fadeoutFired) { if (!req.fadeoutFired) {
req.fadeLength = length.coerceAtLeast(1.0/1024.0) req.fadeLength = length.coerceAtLeast(1.0/1024.0)
@@ -541,7 +545,11 @@ class AudioMixer : Disposable {
} }
} }
fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { /**
* Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used
* to control the music playback instead.
*/
internal fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) {
// printdbg(this, "fadein called by") // printdbg(this, "fadein called by")
// printStackTrace(this) // printStackTrace(this)

View File

@@ -62,7 +62,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) {
lateinit var gameWorld: GameWorld lateinit var gameWorld: GameWorld
override val musicGovernor = TerrarumMusicGovernor() override val backgroundMusicPlayer = TerrarumBackgroundMusicPlayer()
init { init {
gameUpdateGovernor = ConsistentUpdateRate gameUpdateGovernor = ConsistentUpdateRate
@@ -396,7 +396,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) {
} }
musicGovernor.update(this, delta) backgroundMusicPlayer.update(this, delta)
} }
@@ -495,7 +495,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) {
// blockMarkings.dispose() // blockMarkings.dispose()
uiPenMenu.dispose() uiPenMenu.dispose()
uiGetPoiName.dispose() uiGetPoiName.dispose()
musicGovernor.dispose() backgroundMusicPlayer.dispose()
} }
fun getPoiNameForExport(w: Int, h: Int, callback: (String) -> Unit) { fun getPoiNameForExport(w: Int, h: Int, callback: (String) -> Unit) {

View File

@@ -11,7 +11,7 @@ import net.torvald.terrarum.audio.audiobank.MusicContainer
import net.torvald.terrarum.gameworld.WorldTime.Companion.DAY_LENGTH import net.torvald.terrarum.gameworld.WorldTime.Companion.DAY_LENGTH
import java.io.File import java.io.File
class TerrarumMusicGovernor : MusicGovernor() { class TerrarumBackgroundMusicPlayer : BackgroundMusicPlayer() {
private val STATE_INIT = 0 private val STATE_INIT = 0
private val STATE_FIREPLAY = 1 private val STATE_FIREPLAY = 1
private val STATE_PLAYING = 2 private val STATE_PLAYING = 2
@@ -22,7 +22,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
musicState = STATE_INTERMISSION musicState = STATE_INTERMISSION
} }
private var songs: List<MusicContainer> = emptyList() private var playlist: List<MusicContainer> = emptyList()
var playlistName = ""; private set var playlistName = ""; private set
/** canonicalPath with path separators converted to forward slash */ /** canonicalPath with path separators converted to forward slash */
var playlistSource = "" ; private set var playlistSource = "" ; private set
@@ -42,7 +42,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
playlistName = musicDir.substringAfterLast('/') playlistName = musicDir.substringAfterLast('/')
songs = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull {
printdbg(this, "Music: ${it.absolutePath}") printdbg(this, "Music: ${it.absolutePath}")
try { try {
MusicContainer( MusicContainer(
@@ -67,77 +67,91 @@ class TerrarumMusicGovernor : MusicGovernor() {
} }
private fun restockMusicBin() { private fun restockMusicBin() {
musicBin = ArrayList(if (shuffled) songs.shuffled() else songs.slice(songs.indices)) 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 musicDir where the music files are. Absolute path.
* @param shuffled if the tracks are to be shuffled * @param shuffled if the tracks are to be shuffled
* @param diskJockeyingMode `intermittent` to give random gap between tracks, `continuous` for continuous playback * @param diskJockeyingMode `intermittent` to give random gap between tracks, `continuous` for continuous playback
*/ */
fun queueDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, fileToName: ((String) -> String)? = null) { fun queueDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, playImmediately: Boolean, fileToName: ((String) -> String) = { name: String ->
if (musicState != STATE_INIT && musicState != STATE_INTERMISSION) { 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 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) stopMusic0(App.audioMixer.musicTrack.currentTrack)
} }
songs.forEach { it.tryDispose() } playlist.forEach { it.tryDispose() }
registerSongsFromDir(musicDir, fileToName) registerSongsFromDir(musicDir, fileToName)
this.shuffled = shuffled this.shuffled = shuffled
this.diskJockeyingMode = diskJockeyingMode this.diskJockeyingMode = diskJockeyingMode
restockMusicBin() 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`) * Adds a song to the head of the internal playlist (`musicBin`)
*/ */
fun queueMusicToPlayNext(music: MusicContainer) { fun xxxqueueMusicToPlayNext(music: MusicContainer) {
musicBin.add(0, music) musicBin.add(0, music)
} }
/** /**
* Unshifts an internal playlist (`musicBin`). The `music` argument must be the song that exists on the `songs`. * Unshifts an internal playlist (`musicBin`). The `music` argument must be the song that exists on the `songs`.
*/ */
fun unshiftPlaylist(music: MusicContainer) { fun xxxunshiftPlaylist(music: MusicContainer) {
val indexAtMusicBin = songs.indexOf(music) val indexAtMusicBin = playlist.indexOf(music)
if (indexAtMusicBin < 0) throw IllegalArgumentException("The music does not exist on the internal songs list ($music)") if (indexAtMusicBin < 0) throw IllegalArgumentException("The music does not exist on the internal songs list ($music)")
// rewrite musicBin // rewrite musicBin
val newMusicBin = if (shuffled) songs.shuffled().toTypedArray().also { val newMusicBin = if (shuffled) playlist.shuffled().toTypedArray().also {
// if shuffled, // if shuffled,
// 1. create a shuffled version of songlist // 1. create a shuffled version of songlist
// 2. swap two songs such that the songs[indexAtMusicBin] comes first // 2. swap two songs such that the songs[indexAtMusicBin] comes first
val swapTo = it.indexOf(songs[indexAtMusicBin]) val swapTo = it.indexOf(playlist[indexAtMusicBin])
val tmp = it[swapTo] val tmp = it[swapTo]
it[swapTo] = it[0] it[swapTo] = it[0]
it[0] = tmp it[0] = tmp
} }
else Array(songs.size - indexAtMusicBin) { offset -> else Array(playlist.size - indexAtMusicBin) { offset ->
val k = offset + indexAtMusicBin val k = offset + indexAtMusicBin
songs[k] playlist[k]
} }
musicBin = ArrayList(newMusicBin.toList()) musicBin = ArrayList(newMusicBin.toList())
} }
fun queueIndexFromPlaylist(indexAtMusicBin: Int) { fun xxxqueueIndexFromPlaylist(indexAtMusicBin: Int) {
if (indexAtMusicBin !in songs.indices) throw IndexOutOfBoundsException("The index is outside of the internal songs list ($indexAtMusicBin/${songs.size})") if (indexAtMusicBin !in playlist.indices) throw IndexOutOfBoundsException("The index is outside of the internal songs list ($indexAtMusicBin/${playlist.size})")
// rewrite musicBin // rewrite musicBin
val newMusicBin = if (shuffled) songs.shuffled().toTypedArray().also { val newMusicBin = if (shuffled) playlist.shuffled().toTypedArray().also {
// if shuffled, // if shuffled,
// 1. create a shuffled version of songlist // 1. create a shuffled version of songlist
// 2. swap two songs such that the songs[indexAtMusicBin] comes first // 2. swap two songs such that the songs[indexAtMusicBin] comes first
val swapTo = it.indexOf(songs[indexAtMusicBin]) val swapTo = it.indexOf(playlist[indexAtMusicBin])
val tmp = it[swapTo] val tmp = it[swapTo]
it[swapTo] = it[0] it[swapTo] = it[0]
it[0] = tmp it[0] = tmp
} }
else Array(songs.size - indexAtMusicBin) { offset -> else Array(playlist.size - indexAtMusicBin) { offset ->
val k = offset + indexAtMusicBin val k = offset + indexAtMusicBin
songs[k] playlist[k]
} }
musicBin = ArrayList(newMusicBin.toList()) musicBin = ArrayList(newMusicBin.toList())
@@ -162,7 +176,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>() private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>()
init { init {
queueDirectory(App.customMusicDir, true, "intermittent") queueDirectory(App.customMusicDir, true, "intermittent", true)
} }
@@ -175,7 +189,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
} }
init { init {
songs.forEach { playlist.forEach {
App.disposables.add(it) App.disposables.add(it)
} }
ambients.forEach { (k, v) -> ambients.forEach { (k, v) ->
@@ -218,7 +232,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val trackThis = App.audioMixer.musicTrack.currentTrack val trackThis = App.audioMixer.musicTrack.currentTrack
if (caller is TerrarumMusicGovernor) { if (caller is TerrarumBackgroundMusicPlayer) {
if (stopCaller == null) { if (stopCaller == null) {
// printdbg(this, "Caller: this, prev caller: $stopCaller, len: $pauseLen, obliging stop request") // printdbg(this, "Caller: this, prev caller: $stopCaller, len: $pauseLen, obliging stop request")
stopMusic0(trackThis, callStopMusicHook, pauseLen) stopMusic0(trackThis, callStopMusicHook, pauseLen)
@@ -316,7 +330,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
// start the song queueing if there is one to play // start the song queueing if there is one to play
if (firstTime) { if (firstTime) {
firstTime = false firstTime = false
if (songs.isNotEmpty()) musicState = STATE_INTERMISSION if (playlist.isNotEmpty()) musicState = STATE_INTERMISSION
if (ambients.isNotEmpty()) ambState = STATE_INTERMISSION if (ambients.isNotEmpty()) ambState = STATE_INTERMISSION
} }
@@ -334,7 +348,7 @@ class TerrarumMusicGovernor : MusicGovernor() {
STATE_INTERMISSION -> { STATE_INTERMISSION -> {
intermissionAkku += delta intermissionAkku += delta
if (intermissionAkku >= intermissionLength && songs.isNotEmpty()) { if (intermissionAkku >= intermissionLength && playlist.isNotEmpty()) {
intermissionAkku = 0f intermissionAkku = 0f
musicState = STATE_FIREPLAY musicState = STATE_FIREPLAY
} }

View File

@@ -262,7 +262,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
override var gameFullyLoaded = false override var gameFullyLoaded = false
internal set internal set
override val musicGovernor = TerrarumMusicGovernor() override val backgroundMusicPlayer = TerrarumBackgroundMusicPlayer()
////////////// //////////////
@@ -1006,7 +1006,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
oldSelectedWireRenderClass = selectedWireRenderClass oldSelectedWireRenderClass = selectedWireRenderClass
} }
musicGovernor.update(this, delta) backgroundMusicPlayer.update(this, delta)
//////////////////////// ////////////////////////
// ui-related updates // // ui-related updates //
@@ -1795,7 +1795,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
catch (e: IllegalArgumentException) {} catch (e: IllegalArgumentException) {}
} }
musicGovernor.dispose() backgroundMusicPlayer.dispose()
super.dispose() super.dispose()
} }
} }

View File

@@ -1,6 +1,5 @@
package net.torvald.terrarum.modulebasegame.gameactors package net.torvald.terrarum.modulebasegame.gameactors
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.jme3.math.FastMath import com.jme3.math.FastMath
@@ -10,7 +9,6 @@ import net.torvald.terrarum.*
import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
import net.torvald.terrarum.audio.AudioMixer.Companion.DEFAULT_FADEOUT_LEN
import net.torvald.terrarum.audio.audiobank.MusicContainer import net.torvald.terrarum.audio.audiobank.MusicContainer
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack import net.torvald.terrarum.audio.TerrarumAudioMixerTrack
import net.torvald.terrarum.audio.dsp.NullFilter import net.torvald.terrarum.audio.dsp.NullFilter
@@ -21,7 +19,7 @@ import net.torvald.terrarum.gameactors.Hitbox
import net.torvald.terrarum.gameactors.Lightbox import net.torvald.terrarum.gameactors.Lightbox
import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.TerrarumMusicGovernor import net.torvald.terrarum.modulebasegame.TerrarumBackgroundMusicPlayer
import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase
import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef
import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper
@@ -108,7 +106,7 @@ class FixtureJukebox : Electric, PlaysMusic {
// supress the normal background music playback // supress the normal background music playback
if (musicIsPlaying && !flagDespawn) { if (musicIsPlaying && !flagDespawn) {
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, true) (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, true)
} }
} }
@@ -127,27 +125,49 @@ class FixtureJukebox : Electric, PlaysMusic {
printdbg(this, "Title: $title, artist: $artist") printdbg(this, "Title: $title, artist: $artist")
musicNowPlaying = MusicContainer(title, musicFile.file()) { val returnToInitialState = {
unloadEffector(musicNowPlaying) unloadEffector(musicNowPlaying)
discCurrentlyPlaying = null discCurrentlyPlaying = null
musicNowPlaying?.tryDispose() musicNowPlaying?.tryDispose()
musicNowPlaying = null musicNowPlaying = null
printdbg(this, "Stop music $title - $artist")
// can't call stopDiscPlayback() because of the recursion
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval())
backLamp.currentFrame = 0 backLamp.currentFrame = 0
playMech.currentFrame = 0 playMech.currentFrame = 0
} }
musicNowPlaying = MusicContainer(title, musicFile.file()) {
printdbg(this, "Stop music $title - $artist")
// can't call stopDiscPlayback() because of the recursion
(INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval())
}
discCurrentlyPlaying = index discCurrentlyPlaying = index
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2f) { // 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) } startAudio(musicNowPlaying!!) { loadEffector(it) }
} }*/
MusicService.playMusicalFixture(
// action: () -> Unit
{
startAudio(musicNowPlaying!!) { loadEffector(it) }
},
// musicFinished: () -> Boolean
{
!musicIsPlaying
},
// onSuccess: () -> Unit
{
},
// onFailure: (Throwable) -> Unit
{
},
// onFinally: () -> Unit
returnToInitialState
)
backLamp.currentFrame = 1 + (index / 2) backLamp.currentFrame = 1 + (index / 2)
@@ -165,7 +185,7 @@ class FixtureJukebox : Electric, PlaysMusic {
*/ */
fun stopGracefully() { fun stopGracefully() {
stopDiscPlayback() stopDiscPlayback()
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval())
} }

View File

@@ -10,7 +10,7 @@ import net.torvald.terrarum.gameactors.AVKey
import net.torvald.terrarum.gameitems.GameItem import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.TerrarumMusicGovernor import net.torvald.terrarum.modulebasegame.TerrarumBackgroundMusicPlayer
import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase
import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef
import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper
@@ -96,7 +96,7 @@ class FixtureMusicalTurntable : Electric, PlaysMusic {
// supress the normal background music playback // supress the normal background music playback
if (musicIsPlaying && !flagDespawn) { if (musicIsPlaying && !flagDespawn) {
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, true) (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, true)
} }
} }
@@ -111,21 +111,47 @@ class FixtureMusicalTurntable : Electric, PlaysMusic {
App.printdbg(this, "Title: $title, artist: $artist") App.printdbg(this, "Title: $title, artist: $artist")
musicNowPlaying = MusicContainer(title, musicFile.file()) { val returnToInitialState = {
unloadEffector(musicNowPlaying) unloadEffector(musicNowPlaying)
musicNowPlaying?.tryDispose() musicNowPlaying?.tryDispose()
musicNowPlaying = null musicNowPlaying = null
}
musicNowPlaying = MusicContainer(title, musicFile.file()) {
App.printdbg(this, "Stop music $title - $artist") App.printdbg(this, "Stop music $title - $artist")
// can't call stopDiscPlayback() because of the recursion // can't call stopDiscPlayback() because of the recursion
(INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval())
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval())
} }
App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 2f) {
// 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) } startAudio(musicNowPlaying!!) { loadEffector(it) }
} }*/
MusicService.playMusicalFixture(
// action: () -> Unit
{
startAudio(musicNowPlaying!!) { loadEffector(it) }
},
// musicFinished: () -> Boolean
{
!musicIsPlaying
},
// onSuccess: () -> Unit
{
},
// onFailure: (Throwable) -> Unit
{
},
// onFinally: () -> Unit
returnToInitialState
)
(sprite as SheetSpriteAnimation).currentRow = 0 (sprite as SheetSpriteAnimation).currentRow = 0
} }
@@ -137,7 +163,7 @@ class FixtureMusicalTurntable : Electric, PlaysMusic {
*/ */
fun stopGracefully() { fun stopGracefully() {
stopDiscPlayback() stopDiscPlayback()
(INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval())
} }

View File

@@ -0,0 +1,76 @@
package net.torvald.terrarum.transaction
/**
* Created by minjaesong on 2024-06-28.
*/
interface Transaction {
/**
* Call this function to begin the transaction.
*
* When started using [TransactionListener.runTransaction], the transaction runs on a separate thread,
* and thus any operation that requires GL Context will fail.
*/
fun start(state: TransactionState)
/**
* Called by [TransactionListener.runTransaction], when the transaction was successful.
*/
fun onSuccess(state: TransactionState)
/**
* Called by [TransactionListener.runTransaction], when the transaction failed.
*/
fun onFailure(e: Throwable, state: TransactionState)
}
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)
/**
* 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.
*/
fun runTransaction(transaction: Transaction, onFinally: () -> Unit = {}) {
Thread { synchronized(this) {
val state = getCurrentStatusForTransaction()
if (!transactionLocked) {
try {
transaction.start(state)
// if successful:
commitTransaction(state)
// notify the success
transaction.onSuccess(state)
}
catch (e: Throwable) {
// if failed, notify the failure
transaction.onFailure(e, state)
}
finally {
onFinally()
}
}
else {
transaction.onFailure(LockedException(this, transactionLockedBy), state)
}
} }.start()
}
protected abstract fun getCurrentStatusForTransaction(): TransactionState
protected abstract fun commitTransaction(state: TransactionState)
}
class LockedException(listener: TransactionListener, lockedBy: Any?) :
Exception("Transaction is rejected because the class '${listener.javaClass.canonicalName}' is locked by '${lockedBy?.javaClass?.canonicalName}'")
@JvmInline value class TransactionState(val valueTable: MutableMap<String, Any?>) {
operator fun get(key: String) = valueTable[key]
operator fun set(key: String, value: Any?) {
valueTable[key] = value
}
}

View File

@@ -96,11 +96,11 @@ class ArrayListMap<K, V> : MutableMap<K, V> {
return super.computeIfAbsent(key, mappingFunction) return super.computeIfAbsent(key, mappingFunction)
} }
override fun computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V, out V?>): V? { override fun computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V & Any, out V?>): V? {
return super.computeIfPresent(key, remappingFunction) return super.computeIfPresent(key, remappingFunction)
} }
override fun merge(key: K, value: V, remappingFunction: BiFunction<in V, in V, out V?>): V? { override fun merge(key: K, value: V & Any, remappingFunction: BiFunction<in V & Any, in V & Any, out V?>): V? {
return super.merge(key, value, remappingFunction) return super.merge(key, value, remappingFunction)
} }