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("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("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 Queue.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 } }