diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt index 9f2010d7c..6f16bc13c 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt @@ -376,7 +376,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) App.audioMixer.musicTrack.nextTrack = null ingame.musicGovernor.stopMusic(this) - thisMusic?.let { ingame.musicGovernor.queueMusicToPlayNext(it) } + if (thisMusic is MusicContainer) thisMusic.let { ingame.musicGovernor.queueMusicToPlayNext(it) } iHitTheStopButton = true } else if (!shouldPlayerBeDisabled) { @@ -472,7 +472,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { iHitTheStopButton = false stopRequested = false } - resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack) + resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) } } } diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 688d6f36d..a9923a32d 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -17,6 +17,7 @@ import com.github.strikerx3.jxinput.XInputDevice; import kotlin.jvm.functions.Function0; import kotlin.text.Charsets; import net.torvald.getcpuname.GetCpuName; +import net.torvald.terrarum.audio.AudioBank; import net.torvald.terrarum.audio.AudioMixer; import net.torvald.terrarum.audio.MusicContainer; import net.torvald.terrarum.audio.dsp.BinoPan; @@ -554,11 +555,11 @@ public class App implements ApplicationListener { CommonResourcePool.INSTANCE.addToLoadingList("title_health1", () -> new Texture(Gdx.files.internal("./assets/graphics/gui/health_take_a_break.tga"))); CommonResourcePool.INSTANCE.addToLoadingList("title_health2", () -> new Texture(Gdx.files.internal("./assets/graphics/gui/health_distance.tga"))); - CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bup", () -> new MusicContainer("haptic_bup", Gdx.files.internal("./assets/audio/effects/haptic_bup.ogg").file(), false, true, (MusicContainer m) -> { return null; })); - CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bap", () -> new MusicContainer("haptic_bap", Gdx.files.internal("./assets/audio/effects/haptic_bap.ogg").file(), false, true, (MusicContainer m) -> { return null; })); - CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bop", () -> new MusicContainer("haptic_bop", Gdx.files.internal("./assets/audio/effects/haptic_bop.ogg").file(), false, true, (MusicContainer m) -> { return null; })); - CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bep", () -> new MusicContainer("haptic_bep", Gdx.files.internal("./assets/audio/effects/haptic_bep.ogg").file(), false, true, (MusicContainer m) -> { return null; })); - CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bip", () -> new MusicContainer("haptic_bip", Gdx.files.internal("./assets/audio/effects/haptic_bip.ogg").file(), false, true, (MusicContainer m) -> { highPrioritySoundPlaying = false; return null; })); + CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bup", () -> new MusicContainer("haptic_bup", Gdx.files.internal("./assets/audio/effects/haptic_bup.ogg").file(), false, true, (AudioBank m) -> { return null; })); + CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bap", () -> new MusicContainer("haptic_bap", Gdx.files.internal("./assets/audio/effects/haptic_bap.ogg").file(), false, true, (AudioBank m) -> { return null; })); + CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bop", () -> new MusicContainer("haptic_bop", Gdx.files.internal("./assets/audio/effects/haptic_bop.ogg").file(), false, true, (AudioBank m) -> { return null; })); + CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bep", () -> new MusicContainer("haptic_bep", Gdx.files.internal("./assets/audio/effects/haptic_bep.ogg").file(), false, true, (AudioBank m) -> { return null; })); + CommonResourcePool.INSTANCE.addToLoadingList("sound:haptic_bip", () -> new MusicContainer("haptic_bip", Gdx.files.internal("./assets/audio/effects/haptic_bip.ogg").file(), false, true, (AudioBank m) -> { highPrioritySoundPlaying = false; return null; })); // make loading list CommonResourcePool.INSTANCE.loadAll(); diff --git a/src/net/torvald/terrarum/audio/AudioBank.kt b/src/net/torvald/terrarum/audio/AudioBank.kt new file mode 100644 index 000000000..5b360db4a --- /dev/null +++ b/src/net/torvald/terrarum/audio/AudioBank.kt @@ -0,0 +1,32 @@ +package net.torvald.terrarum.audio + +import com.badlogic.gdx.utils.Disposable +import java.io.File + +/** + * Created by minjaesong on 2024-04-05. + */ +abstract class AudioBank : Disposable { + + companion object { + fun fromMusic(name: String, file: File, looping: Boolean = false, toRAM: Boolean = false, songFinishedHook: (AudioBank) -> Unit = {}): AudioBank { + return MusicContainer(name, file, looping, toRAM, songFinishedHook) + } + } + + protected val hash = System.nanoTime() + + abstract fun makeCopy(): AudioBank + + abstract val name: String + + abstract val samplingRate: Int + abstract val channels: Int + abstract val totalSizeInSamples: Long + abstract fun currentPositionInSamples(): Long + + abstract fun readBytes(buffer: ByteArray): Int + abstract fun reset() + + abstract val songFinishedHook: (AudioBank) -> Unit +} \ 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 d5d357d5b..a88651973 100644 --- a/src/net/torvald/terrarum/audio/AudioMixer.kt +++ b/src/net/torvald/terrarum/audio/AudioMixer.kt @@ -485,7 +485,7 @@ class AudioMixer : Disposable { private var ambientStopped = true - fun startMusic(song: MusicContainer) { + fun startMusic(song: AudioBank) { if (musicTrack.isPlaying) { requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN) } @@ -496,7 +496,7 @@ class AudioMixer : Disposable { requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN) } - fun startAmb(song: MusicContainer) { + fun startAmb(song: AudioBank) { val ambientTrack = if (!ambientTrack1.streamPlaying.get()) ambientTrack1 else if (!ambientTrack2.streamPlaying.get()) @@ -513,7 +513,7 @@ class AudioMixer : Disposable { // fade will be processed by the update() } - fun startAmb1(song: MusicContainer) { + fun startAmb1(song: AudioBank) { if (ambientTrack1.isPlaying == true) { requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN) } @@ -521,7 +521,7 @@ class AudioMixer : Disposable { // fade will be processed by the update() } - fun startAmb2(song: MusicContainer) { + fun startAmb2(song: AudioBank) { if (ambientTrack2.isPlaying == true) { requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN) } diff --git a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt index 73aeee29c..948b164f5 100644 --- a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt +++ b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt @@ -98,12 +98,6 @@ class MixerTrackProcessor(bufferSize: Int, val rate: Int, val track: TerrarumAud bytesRead += read0(buffer, bytesRead) } - // if isLooping=true, do gapless sampleRead but reads from itself - else if (track.currentTrack?.looping == true && bytesRead < buffer.size) { - track.currentTrack?.reset() - - bytesRead += read0(buffer, bytesRead) - } bytesRead }, { purgeStreamBuf() }).also { diff --git a/src/net/torvald/terrarum/audio/MusicCache.kt b/src/net/torvald/terrarum/audio/MusicCache.kt index ec817435c..cefa72186 100644 --- a/src/net/torvald/terrarum/audio/MusicCache.kt +++ b/src/net/torvald/terrarum/audio/MusicCache.kt @@ -5,9 +5,9 @@ import net.torvald.terrarum.tryDispose class MusicCache(val trackName: String) : Disposable { - private val cache = HashMap() + private val cache = HashMap() - fun getOrPut(music: MusicContainer?): MusicContainer? { + fun getOrPut(music: AudioBank?): AudioBank? { if (music != null) return cache.getOrPut(music.name) { music.makeCopy() } return null diff --git a/src/net/torvald/terrarum/audio/MusicContainer.kt b/src/net/torvald/terrarum/audio/MusicContainer.kt index 8b378ac41..7a54beb8e 100644 --- a/src/net/torvald/terrarum/audio/MusicContainer.kt +++ b/src/net/torvald/terrarum/audio/MusicContainer.kt @@ -7,41 +7,51 @@ import com.badlogic.gdx.backends.lwjgl3.audio.Ogg import com.badlogic.gdx.backends.lwjgl3.audio.OggInputStream import com.badlogic.gdx.backends.lwjgl3.audio.Wav import com.badlogic.gdx.files.FileHandle -import com.badlogic.gdx.utils.Disposable import com.jcraft.jorbis.VorbisFile import javazoom.jl.decoder.Bitstream import net.torvald.reflection.extortField import net.torvald.reflection.forceInvoke +import net.torvald.terrarum.App.printdbg import net.torvald.unsafe.UnsafeHelper import net.torvald.unsafe.UnsafePtr import java.io.File import java.io.FileInputStream import javax.sound.sampled.AudioSystem -data class MusicContainer( - val name: String, +class MusicContainer( + override val name: String, val file: File, val looping: Boolean = false, val toRAM: Boolean = false, - internal var songFinishedHook: (MusicContainer) -> Unit = {} -): Disposable { - val samplingRate: Int + override var songFinishedHook: (AudioBank) -> Unit = {} +): AudioBank() { + override val samplingRate: Int + override val channels: Int val codec: String var samplesReadCount = 0L; internal set - var samplesTotal: Long + override val totalSizeInSamples: Long + private val totalSizeInBytes: Long private val gdxMusic: Music = Gdx.audio.newMusic(FileHandle(file)) private var soundBuf: UnsafePtr? = null; private set - private val hash = System.nanoTime() - + private val bytesPerSample: Int + init { gdxMusic.isLooping = looping // gdxMusic.setOnCompletionListener(songFinishedHook) + channels = when (gdxMusic) { + is Wav.Music -> gdxMusic.extortField("input")!!.channels + is Ogg.Music -> gdxMusic.extortField("input")!!.channels + else -> 2 + } + + bytesPerSample = 2 * channels + samplingRate = when (gdxMusic) { is Wav.Music -> { val rate = gdxMusic.extortField("input")!!.sampleRate @@ -80,55 +90,147 @@ data class MusicContainer( if (it.last() == "Music") it.dropLast(1).last() else it.last() } - samplesTotal = when (gdxMusic) { + totalSizeInSamples = when (gdxMusic) { is Wav.Music -> getWavFileSampleCount(file) is Ogg.Music -> getOggFileSampleCount(file) is Mp3.Music -> getMp3FileSampleCount(file) else -> Long.MAX_VALUE } + totalSizeInBytes = totalSizeInSamples * 2 * channels if (toRAM) { - if (samplesTotal == Long.MAX_VALUE) throw IllegalStateException("Could not read sample count") + if (totalSizeInSamples == Long.MAX_VALUE) throw IllegalStateException("Could not read sample count") val readSize = 8192 var readCount = 0L val readBuf = ByteArray(readSize) - soundBuf = UnsafeHelper.allocate(4L * samplesTotal) + soundBuf = UnsafeHelper.allocate(totalSizeInBytes) - while (readCount < samplesTotal) { - val read = gdxMusic.forceInvoke("read", arrayOf(readBuf))!!.toLong() + while (readCount < totalSizeInBytes) { + gdxMusic.forceInvoke("read", arrayOf(readBuf))!!.toLong() // its return value will be useless for looping=true + val read = minOf(readSize.toLong(), (totalSizeInBytes - readCount)) UnsafeHelper.memcpyRaw(readBuf, UnsafeHelper.getArrayOffset(readBuf), null, soundBuf!!.ptr + readCount, read) readCount += read + +// printdbg(this, "read $readCount/$totalSizeInBytes bytes (${readCount/(2*channels)}/$totalSizeInSamples samples)") } } } - fun readBytes(buffer: ByteArray): Int { + private fun read0(buffer: ByteArray, bytesRead: Int): Int { + val tmpBuf = ByteArray(buffer.size - bytesRead) + val newRead = readBytes(tmpBuf) + + System.arraycopy(tmpBuf, 0, buffer, bytesRead, tmpBuf.size) + + return newRead + } + + override fun readBytes(buffer: ByteArray): Int { if (soundBuf == null) { val bytesRead = gdxMusic.forceInvoke("read", arrayOf(buffer)) ?: 0 - samplesReadCount += bytesRead / 4 + samplesReadCount += bytesRead / bytesPerSample + + if (looping && bytesRead < buffer.size) { + reset() + + val remainder = buffer.size - bytesRead + + val fullCopyCounts = remainder / totalSizeInBytes + val partialCopyCountsInBytes = (remainder % totalSizeInBytes).toInt() + + var start = UnsafeHelper.getArrayOffset(buffer).toInt() + bytesRead + + val fullbuf = ByteArray(totalSizeInBytes.toInt()) + // make full block copies + for (i in 0 until fullCopyCounts) { + gdxMusic.forceInvoke("read", arrayOf(fullbuf)) + reset() + + System.arraycopy(fullbuf, 0, buffer, start, fullbuf.size) + + start += totalSizeInBytes.toInt() + } + + // copy the remainders from the start of the samples + val partialBuf = ByteArray(partialCopyCountsInBytes) + gdxMusic.forceInvoke("read", arrayOf(partialBuf)) + System.arraycopy(partialBuf, 0, buffer, start, partialCopyCountsInBytes) + + samplesReadCount += partialCopyCountsInBytes / bytesPerSample + } + return bytesRead } else { - val bytesToRead = minOf(buffer.size.toLong(), 4 * (samplesTotal - samplesReadCount)) - if (bytesToRead <= 0) return bytesToRead.toInt() + val bytesToRead = minOf(buffer.size.toLong(), 2 * channels * (totalSizeInSamples - samplesReadCount)) - UnsafeHelper.memcpyRaw(null, soundBuf!!.ptr + samplesReadCount * 4, buffer, UnsafeHelper.getArrayOffset(buffer), bytesToRead) + if (!looping && bytesToRead <= 0) return bytesToRead.toInt() +// if (looping) printdbg(this, "toRAM music loop (bytes cursor: $samplesReadCount/$totalSizeInSamples, bytesToRead=$bytesToRead, buffer.size=${buffer.size})") + + UnsafeHelper.memcpyRaw( + null, + soundBuf!!.ptr + samplesReadCount * bytesPerSample, + buffer, + UnsafeHelper.getArrayOffset(buffer), + bytesToRead + ) + + samplesReadCount += bytesToRead / bytesPerSample + + + // reached the end of the "tape" + if (looping && bytesToRead < buffer.size) { + + val remainder = buffer.size - bytesToRead + + val fullCopyCounts = remainder / totalSizeInBytes + val partialCopyCountsInBytes = remainder % totalSizeInBytes + + var start = UnsafeHelper.getArrayOffset(buffer) + bytesToRead + + // make full block copies + for (i in 0 until fullCopyCounts) { + UnsafeHelper.memcpyRaw( + null, + soundBuf!!.ptr, + buffer, + start, + totalSizeInBytes + ) + + start += totalSizeInBytes + } + + // copy the remainders from the start of the "tape" + UnsafeHelper.memcpyRaw( + null, + soundBuf!!.ptr, + buffer, + start, + partialCopyCountsInBytes + ) + + samplesReadCount = partialCopyCountsInBytes / bytesPerSample + } + + if (looping) return buffer.size - samplesReadCount += bytesToRead / 4 return bytesToRead.toInt() } } - fun reset() { + override fun reset() { samplesReadCount = 0L gdxMusic.forceInvoke("reset", arrayOf()) } + override fun currentPositionInSamples() = samplesReadCount + private fun getWavFileSampleCount(file: File): Long { return try { val ais = AudioSystem.getAudioInputStream(file) @@ -186,7 +288,7 @@ data class MusicContainer( soundBuf?.destroy() } - fun makeCopy(): MusicContainer { + override fun makeCopy(): AudioBank { val new = MusicContainer(name, file, looping, false, songFinishedHook) synchronized(this) { diff --git a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt index f12ad4096..84719dc46 100644 --- a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt +++ b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt @@ -42,7 +42,7 @@ class TerrarumAudioMixerTrack( acc or (c shl (5*i)) } - var currentTrack: MusicContainer? = null + var currentTrack: AudioBank? = null set(value) { field = if (value == null) null @@ -50,7 +50,7 @@ class TerrarumAudioMixerTrack( musicCache.getOrPut(value) } - var nextTrack: MusicContainer? = null + var nextTrack: AudioBank? = null set(value) { field = if (value == null) null diff --git a/src/net/torvald/terrarum/gameactors/Actor.kt b/src/net/torvald/terrarum/gameactors/Actor.kt index b7bfe027a..476ac0a38 100644 --- a/src/net/torvald/terrarum/gameactors/Actor.kt +++ b/src/net/torvald/terrarum/gameactors/Actor.kt @@ -6,6 +6,7 @@ import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.App.printdbgerr import net.torvald.terrarum.INGAME import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.audio.AudioBank import net.torvald.terrarum.audio.MusicContainer import net.torvald.terrarum.audio.TerrarumAudioMixerTrack import net.torvald.terrarum.audio.TrackVolume @@ -216,7 +217,7 @@ abstract class Actor : Comparable, Runnable { }*/ - open @Event fun onAudioInterrupt(music: MusicContainer) { + open @Event fun onAudioInterrupt(music: AudioBank) { music.songFinishedHook(music) } diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt index dee4880e5..ad4f1a03c 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.utils.GdxRuntimeException import com.jme3.math.FastMath import net.torvald.terrarum.* import net.torvald.terrarum.App.printdbg +import net.torvald.terrarum.audio.AudioBank import net.torvald.terrarum.audio.AudioMixer import net.torvald.terrarum.audio.MusicContainer import net.torvald.terrarum.gameworld.WorldTime.Companion.DAY_LENGTH @@ -199,17 +200,15 @@ class TerrarumMusicGovernor : MusicGovernor() { var playCaller: Any? = null; private set var stopCallTime: Long? = null; private set - private fun stopMusic0(song: MusicContainer?, callStopMusicHook: Boolean = true, customPauseLen: Float? = null) { + private fun stopMusic0(song: AudioBank?, callStopMusicHook: Boolean = true, customPauseLen: Float? = null) { musicState = if (customPauseLen == Float.POSITIVE_INFINITY) STATE_INIT else STATE_INTERMISSION // printdbg(this, "stopMusic1 customLen=$customPauseLen, stateNow: $musicState, called by") // printStackTrace(this) intermissionAkku = 0f intermissionLength = customPauseLen ?: getRandomMusicInterval() musicFired = false - if (callStopMusicHook && musicStopHooks.isNotEmpty()) musicStopHooks.forEach { - if (song != null) { - it(song) - } + if (callStopMusicHook && musicStopHooks.isNotEmpty() && song is MusicContainer) musicStopHooks.forEach { + it(song) } // printdbg(this, "StopMusic Intermission: $intermissionLength seconds") } diff --git a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt index 957821768..70dc5813a 100644 --- a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt +++ b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt @@ -664,7 +664,7 @@ class BasicDebugInfoWindow : UICanvas() { if (it.length > 7) it.replace(" ", "").let { it.substring(0 until minOf(it.length, 7)) } else it }, - "C:${track.currentTrack?.codec ?: ""}", + //"C:${track.currentTrack?.codec ?: ""}", "R:${track.currentTrack?.samplingRate ?: ""}", ).forEachIndexed { i, s -> // gauge background @@ -673,7 +673,7 @@ class BasicDebugInfoWindow : UICanvas() { // fill the song title line with a progress bar if (i == 0 && track.currentTrack != null) { - val perc = (track.currentTrack!!.samplesReadCount.toFloat() / track.currentTrack!!.samplesTotal).coerceAtMost(1f) + val perc = (track.currentTrack!!.currentPositionInSamples().toFloat() / track.currentTrack!!.totalSizeInSamples).coerceAtMost(1f) batch.color = COL_PROGRESS_GRAD2 Toolkit.fillArea(batch, x.toFloat(), faderY - (i + 1) * 16f, STRIP_W * perc, 14f) batch.color = COL_PROGRESS_GRAD