diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt index 7c0b04810..47ad778fc 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt @@ -9,7 +9,6 @@ import com.badlogic.gdx.utils.JsonValue 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 @@ -36,7 +35,8 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { private var capsuleHeight = 28 private var capsuleMosaicSize = capsuleHeight / 2 + 1 - private val BUTTON_SIZE = 40 + private val BUTTON_WIDTH = 48 + private val BUTTON_HEIGHT = 40 private val nameStrMaxLen = 180 private val nameFBO = FrameBuffer(Pixmap.Format.RGBA8888, 1024, capsuleHeight, false) @@ -48,7 +48,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { TextureRegionPack(it, maskOffWidth, capsuleHeight) } private val controlButtons = ModMgr.getGdxFile("musicplayer", "gui/control_buttons.tga").let { - TextureRegionPack(it, BUTTON_SIZE, BUTTON_SIZE) + TextureRegionPack(it, BUTTON_WIDTH, BUTTON_HEIGHT) } private val MODE_IDLE = 0 @@ -77,8 +77,8 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { setAsAlwaysVisible() // test code - val albumDir = App.customMusicDir + "/Gapless Test" -// val albumDir = App.customMusicDir + "/FurryJoA 2023 Live" +// val albumDir = App.customMusicDir + "/Gapless Test 2" + val albumDir = App.customMusicDir + "/FurryJoA 2023 Live" // val albumDir = App.customMusicDir + "/Audio Test" val playlistFile = JsonFetcher.invoke("$albumDir/playlist.json") @@ -155,6 +155,8 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { // printdbg(this, "setMusicName $str; strLen = $nameLengthOld -> $nameLength; overflown=$nameOverflown; transitionTime=$TRANSITION_LENGTH") } + private var mouseOnButton: Int? = null + override fun updateUI(delta: Float) { // process transition request if (transitionRequest != null) { @@ -217,9 +219,58 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } } + // mouse is over which button? + if (mode == MODE_MOUSE_UP && relativeMouseY.toFloat() in _posY + 10f .. _posY + 10f + BUTTON_HEIGHT) { + mouseOnButton = if (relativeMouseX.toFloat() in Toolkit.hdrawWidthf - 120f .. Toolkit.hdrawWidthf - 120f + 5 * BUTTON_WIDTH) { + ((relativeMouseX.toFloat() - (Toolkit.hdrawWidthf - 120f)) / BUTTON_WIDTH).toInt() + } + else null + } + else { + mouseOnButton = null + } + + + // make button work + if (!playControlButtonLatched && mouseOnButton != null && Terrarum.mouseDown) { + playControlButtonLatched = true + when (mouseOnButton) { + 0 -> { // album + + } + 1 -> { // prev +// ingame.musicGovernor.playPrevMusic() + } + 2 -> { // stop + if (AudioMixer.musicTrack.isPlaying) { + AudioMixer.requestFadeOut(AudioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) + AudioMixer.musicTrack.nextTrack = null + ingame.musicGovernor.stopMusic() + } + else { + ingame.musicGovernor.startMusic() + } + } + 3 -> { // next + AudioMixer.requestFadeOut(AudioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { +// ingame.musicGovernor.startMusic() // it works without this? + } + } + 4 -> { // playlist + + } + } + } + else if (!Terrarum.mouseDown) { + playControlButtonLatched = false + } + + // printdbg(this, "mode = $mode; req = $transitionRequest") } + private var playControlButtonLatched = false + // 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) @@ -324,7 +375,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { drawBaloon(batch, _posX, _posY, width.toFloat(), (height - capsuleHeight.toFloat()).coerceAtLeast(0f)) drawText(batch, posXforMusicLine, _posY) drawFreqMeter(batch, posXforMusicLine + widthForFreqMeter - 18f, _posY + height - (capsuleHeight / 2) + 1f) - drawControls(batch, _posX, _posY) + drawControls(App.UPDATE_RATE, batch, _posX, _posY) batch.color = Color.WHITE } @@ -355,7 +406,10 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { batch.draw(baloonTexture.get(2, 2), x + capsuleMosaicSize + width, y + capsuleMosaicSize + height, capsuleMosaicSize.toFloat(), capsuleMosaicSize.toFloat()) } - private fun drawControls(batch: SpriteBatch, posX: Float, posY: Float) { + private val playControlAnimAkku = FloatArray(5) + private val playControlAnimLength = 0.2f + + private fun drawControls(delta: Float, batch: SpriteBatch, posX: Float, posY: Float) { val (alpha, reverse) = if (mode < MODE_MOUSE_UP && modeNext == MODE_MOUSE_UP) (transitionAkku / TRANSITION_LENGTH).let { if (it.isNaN()) 0f else it } to false else if (mode == MODE_MOUSE_UP && modeNext < MODE_MOUSE_UP) @@ -367,13 +421,29 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { if (alpha > 0f) { val alpha0 = alpha.coerceIn(0f, 1f).organicOvershoot().coerceAtMost(1f) - batch.color = colourControlButton mul Color(1f, 1f, 1f, (if (reverse) 1f - alpha0 else alpha0).pow(3f)) - val posX = Toolkit.hdrawWidthf - 120f + val internalWidth =minOf(240f, width - 20f) + val separation = internalWidth / 5f + val anchorX = Toolkit.hdrawWidthf val posY = posY + 10f for (i in 0..4) { + batch.color = Color(1f, 1f, 1f, + 0.75f * (if (reverse) 1f - alpha0 else alpha0).pow(3f) + (playControlAnimAkku[i].pow(2f) * 1.2f) + ) + + val offset = i - 2 + val posX = anchorX + offset * separation + val iconY = if (!AudioMixer.musicTrack.isPlaying && i == 2) 1 else 0 - batch.draw(controlButtons.get(i, iconY), posX + i * (BUTTON_SIZE + 8) + 4, posY) + batch.draw(controlButtons.get(i, iconY), (posX - BUTTON_WIDTH / 2).roundToFloat(), posY.roundToFloat()) + + // update playControlAnimAkku + if (mouseOnButton == i && mode == MODE_MOUSE_UP && modeNext == MODE_MOUSE_UP) + playControlAnimAkku[i] = (playControlAnimAkku[i] + (delta / playControlAnimLength)).coerceIn(0f, 1f) + else + playControlAnimAkku[i] = (playControlAnimAkku[i] - (delta / playControlAnimLength)).coerceIn(0f, 1f) } + +// printdbg(this, "playControlAnimAkku=${playControlAnimAkku.joinToString()}") } } diff --git a/assets/mods/musicplayer/README_for_the_best_audio_quality.md b/assets/mods/musicplayer/README_for_the_best_audio_quality.md new file mode 100644 index 000000000..8816f3d52 --- /dev/null +++ b/assets/mods/musicplayer/README_for_the_best_audio_quality.md @@ -0,0 +1,31 @@ +## Sampling Rate + +The basegame is build assuming the sampling rate of 48000 Hz. + +Any audio files with lower sampling rate will be resampled on-the-fly by the game's audio engine, +but doing so may introduce artefacts, most notably fast periodic clicks, which may be audible in certain +circumstances. For the best results, please resample your audio files to 48000 Hz beforehand. + + +## Mono Incompatibility + +The audio engine does not support monaural audio. Please convert your mono audio file to stereo beforehand. + + +## Gapless Playback + +The basegame (and by the extension this music player) does support the Gapless Playback. + +However, because of the inherent limitation of the MP3 format, the Gapless Playback is not achievable +without extensive hacks. If you do care, please convert your MP3 files into WAV or OGG format. + + +## SACD-Quality WAV File Incompatibility + +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. + + +## tl;dr + +Stereo, 48 kHz, 16 bit, WAV or OGG. \ No newline at end of file diff --git a/assets/mods/musicplayer/gui/control_buttons.tga b/assets/mods/musicplayer/gui/control_buttons.tga index e77f3cf61..16126a9e2 100644 --- a/assets/mods/musicplayer/gui/control_buttons.tga +++ b/assets/mods/musicplayer/gui/control_buttons.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7054fd4efe43f62653f18729ee55cf53bde9f1e908e9534b0f7ac2e5c78951c -size 64018 +oid sha256:0342c3b2704abb76ae3de17f9b9a0e98e3c7c81a46ecd69a2cf8787979410cb8 +size 76818 diff --git a/assets/mods/musicplayer/writing_playlist.md b/assets/mods/musicplayer/writing_playlist.md new file mode 100644 index 000000000..faa6d9ffc --- /dev/null +++ b/assets/mods/musicplayer/writing_playlist.md @@ -0,0 +1,27 @@ +The playlists (or albums) are stored under (userdata)/Custom/Music/(album name) + +The name of the directory is used as the album title, and the lexicographic sorting of the files is used +as the playing order, so it is advised to name your files as 01.ogg, 02.ogg, 03.ogg, etc. + +To actually give titles to the files, you must write `playlist.json` with following format: + +```json +{ + "diskJockeyingMode": "continuous", /* "continuous" allows the Gapless Playback, "intermittent" will put random length of pause (in a range of 30 to 60 seconds) between tracks */ + "shuffled": false, /* self-explanatory, often used with "diskJockeyingMode": "intermittent" */ + "titles": { + "01.ogg": "Lorem Ipsum", + "02.ogg": "Dolor Sit Amet", + "03.ogg": "Consectetur Adipiscing", + "04.ogg": "Sed Do Tempor" + /* these are the filename-to-song-title lookup table the music player actually looks for */ + } +} +``` + + +### Limitations + +On certain filesystem and platform combination, you cannot use non-ASCII character on the album title +due to an incompatibility with the Java's File implementation. Song titles on `playlist.json` has no +such limitation. \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/AudioMixer.kt b/src/net/torvald/terrarum/audio/AudioMixer.kt index 1e74e8483..13ffa614d 100644 --- a/src/net/torvald/terrarum/audio/AudioMixer.kt +++ b/src/net/torvald/terrarum/audio/AudioMixer.kt @@ -6,6 +6,7 @@ import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio import com.badlogic.gdx.utils.Disposable import com.jme3.math.FastMath import net.torvald.terrarum.* +import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED import net.torvald.terrarum.audio.dsp.* @@ -235,6 +236,7 @@ object AudioMixer: Disposable { var fadeinFired: Boolean = false, var fadeTarget: Double = 0.0, var fadeStart: Double = 0.0, + var callback: () -> Unit = {}, ) private val fadeReqs = HashMap().also { map -> @@ -322,13 +324,16 @@ object AudioMixer: Disposable { track.volume = req.fadeTarget // stop streaming if fadeBus is muted - if (req.fadeTarget == 0.0 && track == fadeBus) { + if (req.fadeTarget == 0.0 && (track == musicTrack || track == fadeBus)) { musicTrack.stop() musicTrack.currentTrack = null - + } + if (req.fadeTarget == 0.0 && (track == musicTrack || track == fadeBus)) { ambientTrack.stop() ambientTrack.currentTrack = null } + + req.callback() } } else if (req.fadeinFired) { @@ -340,6 +345,8 @@ object AudioMixer: Disposable { track.volume = req.fadeTarget req.fadeinFired = false } + + req.callback } } @@ -369,18 +376,20 @@ object AudioMixer: Disposable { if (!musicTrack.isPlaying && musicTrack.nextTrack != null) { musicTrack.queueNext(null) fadeBus.volume = 1.0 + musicTrack.volume = 1.0 musicTrack.play() } if (!ambientTrack.isPlaying && ambientTrack.nextTrack != null) { ambientTrack.queueNext(null) requestFadeIn(ambientTrack, DEFAULT_FADEOUT_LEN * 4, 1.0, 0.00001) + ambientTrack.volume = 1.0 ambientTrack.play() } } fun startMusic(song: MusicContainer) { - if (musicTrack.isPlaying == true) { + if (musicTrack.isPlaying) { requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN) } musicTrack.nextTrack = song @@ -402,7 +411,7 @@ object AudioMixer: Disposable { requestFadeOut(ambientTrack, DEFAULT_FADEOUT_LEN * 4) } - fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double, target: Double = 0.0, source: Double? = null) { + fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { val req = fadeReqs[track]!! if (!req.fadeoutFired) { req.fadeLength = length.coerceAtLeast(1.0/1024.0) @@ -410,10 +419,11 @@ object AudioMixer: Disposable { req.fadeoutFired = true req.fadeTarget = target * track.maxVolume req.fadeStart = source ?: fadeBus.volume + req.callback = jobAfterFadeout } } - fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null) { + fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { val req = fadeReqs[track]!! if (!req.fadeinFired) { req.fadeLength = length.coerceAtLeast(1.0/1024.0) @@ -421,6 +431,7 @@ object AudioMixer: Disposable { req.fadeinFired = true req.fadeTarget = target * track.maxVolume req.fadeStart = source ?: fadeBus.volume + req.callback = jobAfterFadeout } } diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt index dd9c3830c..a30326e00 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt @@ -259,13 +259,30 @@ class TerrarumMusicGovernor : MusicGovernor() { protected var ambState = 0 protected var ambFired = false - private fun stopMusic(song: MusicContainer?) { - musicState = STATE_INTERMISSION - intermissionAkku = 0f - intermissionLength = if (diskJockeyingMode == "intermittent") 30f + 30f * Math.random().toFloat() else 0f // 30s-60s - musicFired = false - if (musicStopHooks.isNotEmpty()) musicStopHooks.forEach { if (song != null) { it(song) } } - printdbg(this, "StopMusic Intermission: $intermissionLength seconds") + private fun stopMusic(song: MusicContainer?, callStopMusicHook: Boolean = true) { + if (intermissionLength < Float.POSITIVE_INFINITY) { + musicState = STATE_INTERMISSION + intermissionAkku = 0f + intermissionLength = + if (diskJockeyingMode == "intermittent") 30f + 30f * Math.random().toFloat() else 0f // 30s-60s + musicFired = false + if (callStopMusicHook && musicStopHooks.isNotEmpty()) musicStopHooks.forEach { + if (song != null) { + it(song) + } + } + printdbg(this, "StopMusic Intermission: $intermissionLength seconds") + } + } + + fun stopMusic(callStopMusicHook: Boolean = true, pauseLen: Float = Float.POSITIVE_INFINITY) { + stopMusic(AudioMixer.musicTrack.currentTrack, callStopMusicHook) + intermissionLength = pauseLen + printdbg(this, "StopMusic Intermission2: $intermissionLength seconds") + } + + fun startMusic() { + startMusic(pullNextMusicTrack()) } private fun startMusic(song: MusicContainer) { @@ -274,16 +291,19 @@ class TerrarumMusicGovernor : MusicGovernor() { // INGAME.sendNotification("Now Playing $EMDASH ${song.name}") if (musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(song) } musicState = STATE_PLAYING + intermissionLength = 42.42424f } // MixerTrackProcessor will call this function externally to make gapless playback work fun pullNextMusicTrack(callNextMusicHook: Boolean = false): MusicContainer { +// printStackTrace(this) + // prevent same song to play twice in row (for the most time) if (musicBin.isEmpty()) { restockMUsicBin() } return songs[musicBin.removeAt(0)].also { mus -> - if (musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(mus) } + if (callNextMusicHook && musicStartHooks.isNotEmpty()) musicStartHooks.forEach { it(mus) } } }