Files
Terrarum/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt
2023-11-20 03:15:43 +09:00

300 lines
11 KiB
Kotlin

package net.torvald.terrarum.audio
import com.badlogic.gdx.utils.Queue
import net.torvald.reflection.forceInvoke
import net.torvald.terrarum.audio.AudioMixer.masterVolume
import net.torvald.terrarum.audio.AudioMixer.musicVolume
import kotlin.math.absoluteValue
/**
* Created by minjaesong on 2023-11-17.
*/
class MixerTrackProcessor(val bufferSize: Int, val rate: Int, val track: TerrarumAudioMixerTrack): Runnable {
companion object {
val BACK_BUF_COUNT = 1
}
@Volatile var running = true; private set
@Volatile var paused = false; private set
private val pauseLock = java.lang.Object()
private val emptyBuf = FloatArray(bufferSize / 4)
internal val streamBuf = AudioProcessBuf(bufferSize)
internal val sideChainBufs = Array(track.sidechainInputs.size) { AudioProcessBuf(bufferSize) }
private var fout0 = listOf(emptyBuf, emptyBuf)
private var fout1 = listOf(emptyBuf, emptyBuf)
var maxSigLevel = arrayOf(0.0, 0.0); private set
var hasClipping = arrayOf(false, false); private set
private var breakBomb = false
private fun printdbg(msg: Any) {
if (true) println("[AudioAdapter ${track.name}] $msg")
}
override fun run() {
// while (running) { // uncomment to multithread
/*synchronized(pauseLock) { // uncomment to multithread
if (!running) { // may have changed while waiting to
// synchronize on pauseLock
breakBomb = true
}
if (paused) {
try {
pauseLock.wait() // will cause this Thread to block until
// another thread calls pauseLock.notifyAll()
// Note that calling wait() will
// relinquish the synchronized lock that this
// thread holds on pauseLock so another thread
// can acquire the lock to call notifyAll()
// (link with explanation below this code)
}
catch (ex: InterruptedException) {
breakBomb = true
}
if (!running) { // running might have changed since we paused
breakBomb = true
}
}
}
if (breakBomb) break*/ // uncomment to multithread
// Your code here
// fetch deviceBufferSize amount of sample from the disk
if (!track.isMaster && !track.isBus && track.streamPlaying) {
streamBuf.fetchBytes {
val bytesRead = track.currentTrack?.gdxMusic?.forceInvoke<Int>("read", arrayOf(it))
if (bytesRead == null || bytesRead <= 0) { // some class (namely Mp3) may return 0 instead of negative value
// printdbg("Finished reading audio stream")
track.currentTrack?.gdxMusic?.forceInvoke<Int>("reset", arrayOf())
track.streamPlaying = false
track.fireSongFinishHook()
}
}
}
// also fetch samples from sidechainInputs
// TODO
// combine all the inputs
// TODO this code just uses streamBuf
var samplesL0: FloatArray? = null
var samplesR0: FloatArray? = null
var samplesL1: FloatArray? = null
var samplesR1: FloatArray? = null
var bufEmpty = false
if (track.isMaster || track.isBus) {
// TEST CODE must combine all the inputs
track.sidechainInputs[TerrarumAudioMixerTrack.INDEX_BGM]?.let {
samplesL0 = it.first.processor.fout0[0].applyVolume(musicVolume)
samplesR0 = it.first.processor.fout0[1].applyVolume(musicVolume)
samplesL1 = it.first.processor.fout1[0].applyVolume(musicVolume)
samplesR1 = it.first.processor.fout1[1].applyVolume(musicVolume)
}
/*track.sidechainInputs[0].let {
if (it != null) {
val f0 = it.first.pcmQueue.removeFirstOrElse {
bufEmpty = true
listOf(emptyBuf, emptyBuf)
}
samplesL0 = f0[0]
samplesR0 = f0[1]
val f1 = it.first.pcmQueue.removeFirstOrElse {
bufEmpty = true
listOf(emptyBuf, emptyBuf)
}
samplesL1 = f1[0]
samplesR1 = f1[1]
}
else {
samplesL0 = emptyBuf
samplesR0 = emptyBuf
samplesL1 = emptyBuf
samplesR1 = emptyBuf
bufEmpty = true
}
}*/
}
else {
samplesL0 = streamBuf.getL0(track.volume)
samplesR0 = streamBuf.getR0(track.volume)
samplesL1 = streamBuf.getL1(track.volume)
samplesR1 = streamBuf.getR1(track.volume)
}
if (samplesL0 != null /*&& samplesL1 != null && samplesR0 != null && samplesR1 != null*/) {
// run the input through the stack of filters
val filterStack = track.filters.filter { !it.bypass && it !is NullFilter }
if (filterStack.isEmpty()) {
fout1 = listOf(samplesL1!!, samplesR1!!)
}
else {
var fin0 = listOf(samplesL0!!, samplesR0!!)
var fin1 = listOf(samplesL1!!, samplesR1!!)
fout0 = fout1
fout1 = listOf(FloatArray(bufferSize / 4), FloatArray(bufferSize / 4))
filterStack.forEachIndexed { index, it ->
it(fin0, fin1, fout0, fout1)
fin0 = fout0
fin1 = fout1
if (index < filterStack.lastIndex) {
fout0 = listOf(FloatArray(bufferSize / 4), FloatArray(bufferSize / 4))
fout1 = listOf(FloatArray(bufferSize / 4), FloatArray(bufferSize / 4))
}
}
}
fout1.map { it.maxOf { it.absoluteValue } }.forEachIndexed { index, fl ->
maxSigLevel[index] = fl.toDouble()
}
hasClipping.fill(false)
fout1.forEachIndexed { index, floats ->
var lastSample = floats[0]
for (i in 1 until floats.size) {
val currentSample = floats[i]
if (lastSample * currentSample > 0.0 && lastSample.absoluteValue >= 1.0 && currentSample.absoluteValue >= 1.0) {
hasClipping[index] = true
break
}
lastSample = currentSample
}
}
}
else {
maxSigLevel.fill(0.0)
hasClipping.fill(false)
}
// by this time, the output buffer is filled with processed results, pause the execution
if (!track.isMaster) {
this.pause()
}
else {
if (samplesL0 != null /*&& samplesL1 != null && samplesR0 != null && samplesR1 != null*/) {
// spin until queue is sufficiently empty
/*while (track.pcmQueue.size >= BACK_BUF_COUNT && running) { // uncomment to multithread
Thread.sleep(1)
}*/
// printdbg("PUSHE; Queue size: ${track.pcmQueue.size}")
val masvol = masterVolume
track.volume = masvol
track.pcmQueue.addLast(fout1.map { it.applyVolume(masvol) })
}
// spin
// Thread.sleep(((1000*bufferSize) / 8L / rate).coerceAtLeast(1L)) // uncomment to multithread
// wake sidechain processors
resumeSidechainsRecursively(track, track.name)
}
// } // uncomment to multithread
}
private fun resumeSidechainsRecursively(track: TerrarumAudioMixerTrack?, caller: String) {
track?.getSidechains()?.forEach {
if (it?.processor?.running == true) {
it.processor.resume()
it.getSidechains().forEach {
if (it?.processor?.running == true) {
it.processor.resume()
resumeSidechainsRecursively(it, caller + caller)
}
}
}
}
}
fun stop() {
running = false
// you might also want to interrupt() the Thread that is
// running this Runnable, too, or perhaps call:
resume()
// to unblock
}
fun pause() {
// printdbg("PAUSE")
// you may want to throw an IllegalStateException if !running
paused = true
}
fun resume() {
// printdbg("RESUME")
synchronized(pauseLock) {
paused = false
pauseLock.notifyAll() // Unblocks thread
}
}
private fun FloatArray.applyVolume(musicVolume: Double) = FloatArray(this.size) { (this[it] * musicVolume).toFloat() }
}
private fun <T> Queue<T>.removeFirstOrElse(function: () -> T): T {
return if (this.isEmpty) {
this.removeFirst()
}
else {
function()
}
}
class FeedSamplesToAdev(val bufferSize: Int, val rate: Int, val track: TerrarumAudioMixerTrack) : Runnable {
init {
if (!track.isMaster) throw IllegalArgumentException("Track is not master")
}
val sleepTime = (1000000000.0 * ((bufferSize / 4.0) / TerrarumAudioMixerTrack.SAMPLING_RATED)).toLong()
val sleepMS = sleepTime / 1000000
val sleepNS = (sleepTime % 1000000).toInt()
private fun printdbg(msg: Any) {
if (true) println("[AudioAdapter ${track.name}] $msg")
}
@Volatile private var exit = false
override fun run() {
while (!exit) {
val writeQueue = track.pcmQueue
val queueSize = writeQueue.size
if (queueSize > 0) {
// printdbg("PULL; Queue size: $queueSize")
val samples = writeQueue.removeFirst()
track.adev!!.writeSamples(samples)
}
// else {
// printdbg("QUEUE EMPTY QUEUE EMPTY QUEUE EMPTY ")
// }
// Thread.sleep(sleepMS, sleepNS)
}
}
fun stop() {
exit = true
}
}