mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
musicplayer: working stop and next button, button pos on transition
This commit is contained in:
@@ -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()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
assets/mods/musicplayer/README_for_the_best_audio_quality.md
Normal file
31
assets/mods/musicplayer/README_for_the_best_audio_quality.md
Normal 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.
|
||||
Binary file not shown.
27
assets/mods/musicplayer/writing_playlist.md
Normal file
27
assets/mods/musicplayer/writing_playlist.md
Normal 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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user