musicplayer: working stop and next button, button pos on transition

This commit is contained in:
minjaesong
2023-12-29 04:47:32 +09:00
parent fe762e9396
commit 4f87b8bf9d
6 changed files with 184 additions and 25 deletions

View File

@@ -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()}")
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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<TerrarumAudioMixerTrack, FadeRequest>().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
}
}

View File

@@ -259,14 +259,31 @@ class TerrarumMusicGovernor : MusicGovernor() {
protected var ambState = 0
protected var ambFired = false
private fun stopMusic(song: MusicContainer?) {
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
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) } }
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) {
AudioMixer.startMusic(song)
@@ -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) }
}
}