fix: RAM music should loop as they should now

This commit is contained in:
minjaesong
2024-04-06 00:22:03 +09:00
parent 7416f15def
commit 3ef3f3f653
11 changed files with 180 additions and 51 deletions

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,9 @@ import net.torvald.terrarum.tryDispose
class MusicCache(val trackName: String) : Disposable {
private val cache = HashMap<String, MusicContainer>()
private val cache = HashMap<String, AudioBank>()
fun getOrPut(music: MusicContainer?): MusicContainer? {
fun getOrPut(music: AudioBank?): AudioBank? {
if (music != null)
return cache.getOrPut(music.name) { music.makeCopy() }
return null

View File

@@ -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<Wav.WavInputStream>("input")!!.channels
is Ogg.Music -> gdxMusic.extortField<OggInputStream>("input")!!.channels
else -> 2
}
bytesPerSample = 2 * channels
samplingRate = when (gdxMusic) {
is Wav.Music -> {
val rate = gdxMusic.extortField<Wav.WavInputStream>("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<Int>("read", arrayOf(readBuf))!!.toLong()
while (readCount < totalSizeInBytes) {
gdxMusic.forceInvoke<Int>("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<Int>("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<Int>("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<Int>("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<Int>("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) {

View File

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

View File

@@ -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<Actor>, Runnable {
}*/
open @Event fun onAudioInterrupt(music: MusicContainer) {
open @Event fun onAudioInterrupt(music: AudioBank) {
music.songFinishedHook(music)
}

View File

@@ -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,18 +200,16 @@ 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) {
if (callStopMusicHook && musicStopHooks.isNotEmpty() && song is MusicContainer) musicStopHooks.forEach {
it(song)
}
}
// printdbg(this, "StopMusic Intermission: $intermissionLength seconds")
}

View File

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