From 3238aa198168cdca445277e3facbe702651e1adf Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 18 Nov 2023 02:58:57 +0900 Subject: [PATCH] seemingly working but thread is not gracefully dying --- src/net/torvald/terrarum/audio/AudioMixer.kt | 2 +- .../terrarum/audio/MixerTrackProcessor.kt | 177 ++++++++++++++---- .../audio/OpenALBufferedAudioDevice.kt | 14 ++ .../terrarum/audio/TerrarumAudioMixerTrack.kt | 29 ++- 4 files changed, 179 insertions(+), 43 deletions(-) diff --git a/src/net/torvald/terrarum/audio/AudioMixer.kt b/src/net/torvald/terrarum/audio/AudioMixer.kt index 5ed1e04bc..1cea6381c 100644 --- a/src/net/torvald/terrarum/audio/AudioMixer.kt +++ b/src/net/torvald/terrarum/audio/AudioMixer.kt @@ -32,7 +32,7 @@ object AudioMixer: Disposable { private val masterTrack = TerrarumAudioMixerTrack("Master", true).also { master -> tracks.forEach { master.addSidechainInput(it, 1.0) } -// master.filters[0] = Lowpass(240, TerrarumAudioMixerTrack.SAMPLING_RATE) + master.filters[0] = Lowpass(240, TerrarumAudioMixerTrack.SAMPLING_RATE) } private val musicTrack: TerrarumAudioMixerTrack diff --git a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt index 97acb5510..b579d2a35 100644 --- a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt +++ b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt @@ -1,5 +1,6 @@ package net.torvald.terrarum.audio +import com.badlogic.gdx.utils.Queue import net.torvald.reflection.forceInvoke import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE @@ -7,21 +8,19 @@ import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RAT * Created by minjaesong on 2023-11-17. */ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrack): Runnable { - @Volatile - private var running = true - - @Volatile - private var paused = false + @Volatile private var running = true + @Volatile private var paused = false 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(FloatArray(bufferSize / 4), FloatArray(bufferSize / 4)) - private var fout1 = listOf(FloatArray(bufferSize / 4), FloatArray(bufferSize / 4)) + private var fout0 = listOf(emptyBuf, emptyBuf) + private var fout1 = listOf(emptyBuf, emptyBuf) override fun run() { @@ -69,17 +68,49 @@ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrac // TODO this code just uses streamBuf - var samplesL0: FloatArray - var samplesR0: FloatArray - var samplesL1: FloatArray - var samplesR1: FloatArray + var samplesL0: FloatArray? = null + var samplesR0: FloatArray? = null + var samplesL1: FloatArray? = null + var samplesR1: FloatArray? = null + + var bufEmpty = false if (track.isMaster) { // TEST CODE must combine all the inputs - samplesL0 = track.sidechainInputs[0]!!.first.processor.fout0[0] - samplesR0 = track.sidechainInputs[0]!!.first.processor.fout0[1] - samplesL1 = track.sidechainInputs[0]!!.first.processor.fout1[0] - samplesR1 = track.sidechainInputs[0]!!.first.processor.fout1[1] + track.sidechainInputs[0]?.let { + samplesL0 = it.first.processor.fout0[0] + samplesR0 = it.first.processor.fout0[1] + samplesL1 = it.first.processor.fout1[0] + samplesR1 = it.first.processor.fout1[1] + } + + + /*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) @@ -89,25 +120,27 @@ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrac } - // run the input through the stack of filters - val filterStack = track.filters.filter { !it.bypass && it !is NullFilter } + 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)) + 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)) + 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)) + } } } } @@ -118,11 +151,21 @@ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrac this.pause() } else { - track.adev!!.setVolume(AudioMixer.masterVolume.toFloat()) - val samples = interleave(fout1[0], fout1[1]) - track.adev!!.writeSamples(samples, 0, samples.size) - Thread.sleep(1) + if (samplesL0 != null && samplesL1 != null && samplesR0 != null && samplesR1 != null) { + // spin until queue is sufficiently empty + while (track.pcmQueue.size > 3) { + Thread.sleep(6) + } + + println("[AudioAdapter ${track.name}] Pushing to queue (queue size: ${track.pcmQueue.size})") + track.pcmQueue.addLast(fout1) + } + + // spin + Thread.sleep(12) + + // wake sidechain processors track.getSidechains().forEach { it?.processor?.resume() } @@ -130,6 +173,8 @@ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrac } + + println("[AudioAdapter ${track.name}] MixerTrackProcessor EXIT") } private fun interleave(f1: FloatArray, f2: FloatArray) = FloatArray(f1.size + f2.size) { @@ -155,4 +200,62 @@ class MixerTrackProcessor(val bufferSize: Int, val track: TerrarumAudioMixerTrac pauseLock.notifyAll() // Unblocks thread } } -} \ No newline at end of file +} + +private fun Queue.removeFirstOrElse(function: () -> T): T { + return if (this.isEmpty) { + this.removeFirst() + } + else { + function() + } +} + + +class FeedSamplesToAdev(val track: TerrarumAudioMixerTrack) : Runnable { + init { + if (!track.isMaster) throw IllegalArgumentException("Track is not master") + } + + 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 + + if (writeQueue.notEmpty()) { + + printdbg("Taking samples from queue (queue size: ${writeQueue.size})") + + val samples = writeQueue.removeFirst() +// playhead.position = writeQueue.size + +// printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...") +// printdbg(""+(0..42).joinToString { String.format("%.2f", samples[it]) }) + + track.adev!!.writeSamples(samples) + +// printdbg("P${playhead.index+1} go back to spinning") + + } + else if (writeQueue.isEmpty) { +// printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") + + // TODO: wait for 1-2 seconds then finally stop the device +// playhead.audioDevice.stop() + + } + + + Thread.sleep(1) + } + printdbg("FeedSamplesToAdev EXIT") + } + + fun stop() { + exit = true + } +} diff --git a/src/net/torvald/terrarum/audio/OpenALBufferedAudioDevice.kt b/src/net/torvald/terrarum/audio/OpenALBufferedAudioDevice.kt index 2076ea92c..753b7fd01 100644 --- a/src/net/torvald/terrarum/audio/OpenALBufferedAudioDevice.kt +++ b/src/net/torvald/terrarum/audio/OpenALBufferedAudioDevice.kt @@ -96,6 +96,20 @@ class OpenALBufferedAudioDevice( writeSamples(bytes!!, 0, numSamples * 2) } + private fun interleave(f1: FloatArray, f2: FloatArray) = FloatArray(f1.size + f2.size) { + if (it % 2 == 0) f1[it / 2] else f2[it / 2] + } + + /** + * @param samples multitrack + * @param numSamples number of samples per channel + */ + fun writeSamples(samples: List) { + interleave(samples[0], samples[1]).let { + writeSamples(it, 0, it.size) + } + } + private fun audioObtainSource(isMusic: Boolean): Int { val obtainSourceMethod = OpenALLwjgl3Audio::class.java.getDeclaredMethod("obtainSource", java.lang.Boolean.TYPE) obtainSourceMethod.isAccessible = true diff --git a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt index 8eeb89f0b..486942656 100644 --- a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt +++ b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt @@ -3,6 +3,7 @@ package net.torvald.terrarum.audio import com.badlogic.gdx.Gdx import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio import com.badlogic.gdx.utils.Disposable +import com.badlogic.gdx.utils.Queue import net.torvald.reflection.forceInvoke import net.torvald.terrarum.getHashStr import net.torvald.terrarum.modulebasegame.MusicContainer @@ -104,6 +105,10 @@ class TerrarumAudioMixerTrack(val name: String, val isMaster: Boolean = false): get() = currentTrack?.gdxMusic?.isPlaying override fun dispose() { + if (isMaster) { + queueDispatcher.stop() + queueDispatcherThread.join() + } processor.stop() processorThread.join() adev?.dispose() @@ -114,15 +119,29 @@ class TerrarumAudioMixerTrack(val name: String, val isMaster: Boolean = false): // 1st ring of the hell: the THREADING HELL // - internal var processor = MixerTrackProcessor(4096, this) + val BUFFER_SIZE = 4096 + + internal var processor = MixerTrackProcessor(BUFFER_SIZE, this) private val processorThread = Thread(processor).also { it.start() } + internal var pcmQueue = Queue>() + private lateinit var queueDispatcher: FeedSamplesToAdev + private lateinit var queueDispatcherThread: Thread - private fun interleave(f1: FloatArray, f2: FloatArray) = FloatArray(f1.size + f2.size) { - if (it % 2 == 0) f1[it / 2] else f2[it / 2] + init { + pcmQueue.addLast(listOf(FloatArray(BUFFER_SIZE / 4), FloatArray(BUFFER_SIZE / 4))) + pcmQueue.addLast(listOf(FloatArray(BUFFER_SIZE / 4), FloatArray(BUFFER_SIZE / 4))) + + if (isMaster) { + queueDispatcher = FeedSamplesToAdev(this) + queueDispatcherThread = Thread(queueDispatcher).also { + it.start() + } + } } + } -fun fullscaleToDecibels(fs: Double) = 10.0 * log10(fs) -fun decibelsToFullscale(db: Double) = 10.0.pow(db / 10.0) \ No newline at end of file +fun fullscaleToDecibels(fs: Double) = 20.0 * log10(fs) +fun decibelsToFullscale(db: Double) = 10.0.pow(db / 20.0) \ No newline at end of file