is the new musicplayer working?

This commit is contained in:
minjaesong
2024-07-09 01:27:12 +09:00
parent 86c6f008f9
commit a236f49641
7 changed files with 198 additions and 85 deletions

View File

@@ -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) setPlaylistDisplayVars(playlist)
return playlist return playlist
@@ -447,18 +435,6 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() {
if (index < currentPlaylist.musicList.size) { if (index < currentPlaylist.musicList.size) {
// if selected song != currently playing // if selected song != currently playing
if (App.audioMixer.musicTrack.currentTrack == null || currentPlaylist.musicList[index] != App.audioMixer.musicTrack.currentTrack) { 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) { MusicService.playNthSongInPlaylist(index) {
iHitTheStopButton = false iHitTheStopButton = false
stopRequested = 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 // 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 (MusicService.currentPlaylist?.source != albumsList[index].canonicalPath) { 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]) val playlist = loadNewAlbum(albumsList[index])
MusicService.putNewPlaylist(playlist) { MusicService.putNewPlaylistAndResumePlayback(playlist) {
resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) 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") // 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 iHitTheStopButton = false
private var stopRequested = 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 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)
@@ -710,7 +680,7 @@ class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() {
// debug codes // debug codes
//// transaction state //// transaction state
if (MusicService.transactionLocked) { /*if (MusicService.transactionLocked) {
batch.color = Color.RED batch.color = Color.RED
Toolkit.drawTextCentered(batch, App.fontSmallNumbers, "LOCKED", Toolkit.drawWidth, 0, _posY.toInt() + height + 5) 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 batch.color = Color.LIGHT_GRAY
App.fontSmallNumbers.draw(batch, "Playlist InternalIndices", 10f, App.scr.hf - 30f) App.fontSmallNumbers.draw(batch, "Playlist InternalIndices", 10f, App.scr.hf - 30f)
App.fontSmallNumbers.draw(batch, "..", 10f, App.scr.hf - 16f) App.fontSmallNumbers.draw(batch, "..", 10f, App.scr.hf - 16f)
*/
// end of debug codes // end of debug codes

View File

@@ -29,6 +29,11 @@ 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 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. 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 ## tl;dr

View File

@@ -67,7 +67,7 @@ object MusicService : TransactionListener() {
currentPlaybackState.set(STATE_PLAYING) 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() { private fun enterIntermissionAndWaitForPlaylist() {
val djmode = currentPlaylist?.diskJockeyingMode ?: "intermittent" val djmode = currentPlaylist?.diskJockeyingMode ?: "intermittent"
@@ -85,7 +85,7 @@ object MusicService : TransactionListener() {
} }
fun onMusicFinishing(audio: AudioBank) { fun onMusicFinishing(audio: AudioBank) {
printdbg(this, "onMusicFinishing ${audio.name}") // printdbg(this, "onMusicFinishing ${audio.name}")
enterIntermissionAndWaitForPlaylist() enterIntermissionAndWaitForPlaylist()
} }
@@ -108,19 +108,19 @@ object MusicService : TransactionListener() {
} }
override fun onSuccess(state: TransactionState) { override fun onSuccess(state: TransactionState) {
printdbg(this, "FIREPLAY started music (${nextMusic.name})") // printdbg(this, "FIREPLAY started music (${nextMusic.name})")
enterSTATE_PLAYING() currentPlaybackState.set(STATE_PLAYING) // force PLAYING
} }
override fun onFailure(e: Throwable, state: TransactionState) { 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 enterIntermissionAndWaitForPlaylist() // will try again
} }
}) })
}, },
/* onFailure: (Throwable) -> Unit */ /* onFailure: (Throwable) -> Unit */
{ {
printdbg(this, "FIREPLAY resume failed, entering intermission") // printdbg(this, "FIREPLAY resume failed, entering intermission")
enterIntermissionAndWaitForPlaylist() // will try again enterIntermissionAndWaitForPlaylist() // will try again
}, },
// onFinally: () -> Unit // onFinally: () -> Unit
@@ -130,7 +130,7 @@ object MusicService : TransactionListener() {
) )
} }
else { else {
printdbg(this, "FIREPLAY no-op: playTransaction is ongoing") // printdbg(this, "FIREPLAY no-op: playTransaction is ongoing")
} }
} }
STATE_PLAYING -> { STATE_PLAYING -> {
@@ -140,7 +140,9 @@ object MusicService : TransactionListener() {
waitAkku += delta waitAkku += delta
if (waitAkku >= waitTime && currentPlaylist != null) { 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 { private fun createTransactionForNextMusicInPlaylist(onSuccess: () -> Unit): Transaction {
return object : Transaction { return object : Transaction {
var oldState = currentPlaybackState.get()
var oldAkku = waitAkku
var oldTime = waitTime
override fun start(state: TransactionState) { override fun start(state: TransactionState) {
var fadedOut = false var fadedOut = false
var err: Throwable? = null var err: Throwable? = null
@@ -259,21 +352,34 @@ object MusicService : TransactionListener() {
waitUntil { fadedOut || err != null } waitUntil { fadedOut || err != null }
if (err != null) throw err!! if (err != null) throw err!!
oldState = currentPlaybackState.get()
oldAkku = waitAkku
oldTime = waitTime
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
} }
override fun onSuccess(state: TransactionState) { override fun onSuccess(state: TransactionState) {
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
onSuccess() onSuccess()
} }
override fun onFailure(e: Throwable, state: TransactionState) { override fun onFailure(e: Throwable, state: TransactionState) {
e.printStackTrace() e.printStackTrace()
currentPlaybackState.set(oldState)
waitAkku = oldAkku
waitTime = oldTime
} }
} }
} }
private fun createTransactionForPrevMusicInPlaylist(onSuccess: () -> Unit): Transaction { private fun createTransactionForPrevMusicInPlaylist(onSuccess: () -> Unit): Transaction {
return object : Transaction { return object : Transaction {
var oldState = currentPlaybackState.get()
var oldAkku = waitAkku
var oldTime = waitTime
override fun start(state: TransactionState) { override fun start(state: TransactionState) {
var fadedOut = false var fadedOut = false
var err: Throwable? = null var err: Throwable? = null
@@ -294,11 +400,16 @@ object MusicService : TransactionListener() {
waitUntil { fadedOut || err != null } waitUntil { fadedOut || err != null }
if (err != null) throw err!! if (err != null) throw err!!
oldState = currentPlaybackState.get()
oldAkku = waitAkku
oldTime = waitTime
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
} }
override fun onSuccess(state: TransactionState) { override fun onSuccess(state: TransactionState) {
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
onSuccess() onSuccess()
} }
override fun onFailure(e: Throwable, state: TransactionState) { override fun onFailure(e: Throwable, state: TransactionState) {
@@ -306,12 +417,20 @@ object MusicService : TransactionListener() {
(state["currentPlaylist"] as TerrarumMusicPlaylist).queueNext() (state["currentPlaylist"] as TerrarumMusicPlaylist).queueNext()
e.printStackTrace() e.printStackTrace()
currentPlaybackState.set(oldState)
waitAkku = oldAkku
waitTime = oldTime
} }
} }
} }
private fun createTransactionForNthMusicInPlaylist(index: Int, onSuccess: () -> Unit): Transaction { private fun createTransactionForNthMusicInPlaylist(index: Int, onSuccess: () -> Unit): Transaction {
return object : Transaction { return object : Transaction {
var oldState = currentPlaybackState.get()
var oldAkku = waitAkku
var oldTime = waitTime
override fun start(state: TransactionState) { override fun start(state: TransactionState) {
var fadedOut = false var fadedOut = false
var err: Throwable? = null var err: Throwable? = null
@@ -331,15 +450,24 @@ object MusicService : TransactionListener() {
waitUntil { fadedOut || err != null } waitUntil { fadedOut || err != null }
if (err != null) throw err!! if (err != null) throw err!!
oldState = currentPlaybackState.get()
oldAkku = waitAkku
oldTime = waitTime
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
} }
override fun onSuccess(state: TransactionState) { override fun onSuccess(state: TransactionState) {
enterSTATE_INTERMISSION(0f)
enterSTATE_FIREPLAY()
onSuccess() onSuccess()
} }
override fun onFailure(e: Throwable, state: TransactionState) { override fun onFailure(e: Throwable, state: TransactionState) {
e.printStackTrace() 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 { private fun createTransactionForPlaylistResume(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit): Transaction {
return object : Transaction { return object : Transaction {
var oldState = currentPlaybackState.get()
var oldAkku = waitAkku
var oldTime = waitTime
override fun start(state: TransactionState) { override fun start(state: TransactionState) {
enterSTATE_FIREPLAY() enterSTATE_FIREPLAY()
} }
@@ -378,6 +510,11 @@ object MusicService : TransactionListener() {
} }
override fun onFailure(e: Throwable, state: TransactionState) { override fun onFailure(e: Throwable, state: TransactionState) {
e.printStackTrace() e.printStackTrace()
currentPlaybackState.set(oldState)
waitAkku = oldAkku
waitTime = oldTime
onFailure(e) onFailure(e)
} }
} }
@@ -447,6 +584,12 @@ object MusicService : TransactionListener() {
else else
runTransaction(createTransactionPlaylistChange(playlist, {})) 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) { fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit, onFinally: () -> Unit) {
runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally) runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally)
} }

View File

@@ -135,11 +135,6 @@ class FixtureJukebox : Electric, PlaysMusic {
discCurrentlyPlaying = index 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( MusicService.playMusicalFixture(
/* action: () -> Unit */ { /* action: () -> Unit */ {
startAudio(musicNowPlaying!!) { loadEffector(it) } startAudio(musicNowPlaying!!) { loadEffector(it) }

View File

@@ -116,12 +116,6 @@ class FixtureMusicalTurntable : Electric, PlaysMusic {
App.printdbg(this, "Stop music $title - $artist") 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( MusicService.playMusicalFixture(
/* action: () -> Unit */ { /* action: () -> Unit */ {
startAudio(musicNowPlaying!!) { loadEffector(it) } startAudio(musicNowPlaying!!) { loadEffector(it) }

View File

@@ -56,6 +56,7 @@ abstract class TransactionListener {
printdbg(this, "Accepting transaction $transaction") printdbg(this, "Accepting transaction $transaction")
Thread { Thread {
val state = getCurrentStatusForTransaction() val state = getCurrentStatusForTransaction()
var wasSuccessful = false
val currentLock = transactionLockingClass.get() val currentLock = transactionLockingClass.get()
if (currentLock == null) { if (currentLock == null) {
acquireLock(transaction) acquireLock(transaction)
@@ -63,23 +64,27 @@ abstract class TransactionListener {
transaction.start(state) transaction.start(state)
// if successful: // if successful:
commitTransaction(state) commitTransaction(state)
// notify the success
transaction.onSuccess(state) wasSuccessful = true
} }
catch (e: Throwable) { catch (e: Throwable) {
// if failed, notify the failure // if failed, notify the failure
System.err.println("Transaction failure: generic") System.err.println("Transaction failure: generic (failed transaction: $transaction)")
e.printStackTrace() e.printStackTrace()
transaction.onFailure(e, state) transaction.onFailure(e, state)
} }
finally { finally {
if (wasSuccessful) {
// notify the success
transaction.onSuccess(state)
}
releaseLock() releaseLock()
onFinally() onFinally()
} }
} }
else { else {
System.err.println("Transaction failure: locked") System.err.println("Transaction failure: locked (failed transaction: $transaction)")
transaction.onFailure(LockedException(transaction, this, currentLock), state) transaction.onFailure(LockedException(this, currentLock), state)
} }
}.start() }.start()
} }
@@ -88,8 +93,8 @@ abstract class TransactionListener {
protected abstract fun commitTransaction(state: TransactionState) protected abstract fun commitTransaction(state: TransactionState)
} }
class LockedException(offendingTransaction: Transaction, listener: TransactionListener, lockedBy: Transaction) : class LockedException(listener: TransactionListener, lockedBy: Transaction) :
Exception("Transaction '$offendingTransaction' is rejected because the class '$listener' is locked by '$lockedBy'") Exception("The class '$listener' is locked by '$lockedBy'")
@JvmInline value class TransactionState(val valueTable: HashMap<String, Any?>) { @JvmInline value class TransactionState(val valueTable: HashMap<String, Any?>) {
operator fun get(key: String) = valueTable[key] operator fun get(key: String) = valueTable[key]