diff --git a/src/net/torvald/terrarum/audio/AudioProcessBuf.kt b/src/net/torvald/terrarum/audio/AudioProcessBuf.kt index 85749e06f..9beff14bf 100644 --- a/src/net/torvald/terrarum/audio/AudioProcessBuf.kt +++ b/src/net/torvald/terrarum/audio/AudioProcessBuf.kt @@ -1,64 +1,127 @@ package net.torvald.terrarum.audio +import com.jme3.math.FastMath +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE +import net.torvald.terrarum.ceilToInt +import net.torvald.terrarum.floorToInt import net.torvald.terrarum.serialise.toUint +import org.dyn4j.Epsilon +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.sin /** - * Audio is assumed to be 16 bits + * Audio is assumed to be 2 channels, 16 bits * * Created by minjaesong on 2023-11-17. */ -class AudioProcessBuf(val size: Int) { +class AudioProcessBuf(inputSamplingRate: Int, val audioReadFun: (ByteArray) -> Int?, val onAudioFinished: () -> Unit) { - var buf0 = ByteArray(size); private set - var buf1 = ByteArray(size); private set + private val doResample = inputSamplingRate != SAMPLING_RATE - var fbuf0 = FloatArray(size / 2); private set - var fbuf1 = FloatArray(size / 2); private set + companion object { + private val epsilon: Double = Epsilon.E - private fun shift(): ByteArray { - buf0 = buf1 - buf1 = ByteArray(size) - return buf1 + private val TAPS = 4 // 2*a tap lanczos intp. Lower = greater artefacts + + fun L(x: Double): Double = if (x.absoluteValue < epsilon) + 1.0 + else if (-TAPS <= x && x < TAPS) + (TAPS * sin(PI * x) * sin(PI * x / TAPS)) / (PI * PI * x * x) + else + 0.0 + + private val BS = BUFFER_SIZE / 4 } - private fun updateFloats() { - fbuf0 = fbuf1 - fbuf1 = FloatArray(size / 2) { - val i16 = (buf1[2*it].toUint() or buf1[2*it+1].toUint().shl(8)).toShort() - i16 / 32767f + private val gcd = FastMath.getGCD(inputSamplingRate, SAMPLING_RATE) // 300 for 44100, 48000 + + private val samplesIn = inputSamplingRate / gcd // 147 for 44100 + private val samplesOut = SAMPLING_RATE / gcd // 160 for 48000 + + private val internalBufferSize = samplesOut * ((BS.toFloat()) / samplesOut).ceilToInt() // (512 / 160) -> 640 for 44100, 48000 + + + private fun resampleBlock(inn: FloatArray, out: FloatArray) { + fun getInn(i: Int) = if (i in inn.indices) inn[i] else 0f + + for (sampleIdx in out.indices) { + val x = (inn.size.toDouble() / out.size) * sampleIdx + var sx = 0.0 + for (i in x.floorToInt() - TAPS + 1..x.floorToInt() + TAPS) { + sx += getInn(i) * L(x - i) + } + out[sampleIdx] = sx.toFloat() } } - fun fetchBytes(action: (ByteArray) -> Unit) { - action(shift()) - updateFloats() + var validSamplesInBuf = 0 + + val foutL = FloatArray(internalBufferSize) // 640 for (44100, 48000), 512 for (48000, 48000) with BUFFER_SIZE = 512 * 4 + val foutR = FloatArray(internalBufferSize) // 640 for (44100, 48000), 512 for (48000, 48000) with BUFFER_SIZE = 512 * 4 + + fun fetchBytes() { + val readCount = ((internalBufferSize - validSamplesInBuf) / samplesOut) * samplesIn // in samples (441 or 588 for 44100, 48000) + val writeCount = ((internalBufferSize - validSamplesInBuf) / samplesOut) * samplesOut // in samples (480 or 640 for 44100, 48000) + val readBuf = ByteArray(readCount * 4) + val finL = FloatArray(readCount) + val finR = FloatArray(readCount) + val foutL = FloatArray(writeCount) + val foutR = FloatArray(writeCount) + + fun getFromReadBuf(i: Int, bytesRead: Int) = if (i < bytesRead) readBuf[i].toUint() else 0 + + try { + val bytesRead = audioReadFun(readBuf) + + if (bytesRead == null || bytesRead <= 0) onAudioFinished() + else { + for(c in 0 until readCount) { + val sl = (getFromReadBuf(4 * c + 0, bytesRead) or getFromReadBuf(4 * c + 1, bytesRead).shl(8)).toShort() + val sr = (getFromReadBuf(4 * c + 2, bytesRead) or getFromReadBuf(4 * c + 3, bytesRead).shl(8)).toShort() + + val fl = sl / 32767f + val fr = sr / 32767f + + finL[c] = fl + finR[c] = fr + } + } + } + catch (e: Throwable) { + e.printStackTrace() + } + finally { + if (doResample) { + // perform resampling + resampleBlock(finL, foutL) + resampleBlock(finR, foutR) + + // fill in the output buffers + System.arraycopy(foutL, 0, this.foutL, validSamplesInBuf, writeCount) + System.arraycopy(foutR, 0, this.foutR, validSamplesInBuf, writeCount) + } + else { + // fill in the output buffers + System.arraycopy(finL, 0, this.foutL, validSamplesInBuf, writeCount) + System.arraycopy(finR, 0, this.foutR, validSamplesInBuf, writeCount) + } + + validSamplesInBuf += writeCount + } } - // reusing a buffer causes tons of blips in the sound? how?? - /*private val L0buf = FloatArray(size / 4) - private val R0buf = FloatArray(size / 4) - private val L1buf = FloatArray(size / 4) - private val R1buf = FloatArray(size / 4) + fun getLR(volume: Double): Pair { + // copy into the out + val outL = FloatArray(BS) { (foutL[it] * volume).toFloat() } + val outR = FloatArray(BS) { (foutR[it] * volume).toFloat() } + // shift bytes in the fout + System.arraycopy(foutL, BS, foutL, 0, validSamplesInBuf - BS) + System.arraycopy(foutR, BS, foutR, 0, validSamplesInBuf - BS) + // decrement necessary variables + validSamplesInBuf -= BS - fun getL0(volume: Double): FloatArray { - for (i in L0buf.indices) { L0buf[i] = (volume * fbuf0[2*i]).toFloat() } - return L0buf + return outL to outR } - fun getR0(volume: Double): FloatArray { - for (i in R0buf.indices) { R0buf[i] = (volume * fbuf0[2*i+1]).toFloat() } - return R0buf - } - fun getL1(volume: Double): FloatArray { - for (i in L1buf.indices) { L1buf[i] = (volume * fbuf1[2*i]).toFloat() } - return L1buf - } - fun getR1(volume: Double): FloatArray { - for (i in R1buf.indices) { R1buf[i] = (volume * fbuf1[2*i+1]).toFloat() } - return R1buf - }*/ - fun getL0(volume: Double) = FloatArray(size / 4) { (volume * fbuf0[2*it]).toFloat() } - fun getR0(volume: Double) = FloatArray(size / 4) { (volume * fbuf0[2*it+1]).toFloat() } - fun getL1(volume: Double) = FloatArray(size / 4) { (volume * fbuf1[2*it]).toFloat() } - fun getR1(volume: Double) = FloatArray(size / 4) { (volume * fbuf1[2*it+1]).toFloat() } - } \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt index b4af76cd7..a297c3fff 100644 --- a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt +++ b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt @@ -27,8 +27,7 @@ class MixerTrackProcessor(val bufferSize: Int, val rate: Int, val track: Terraru private val emptyBuf = FloatArray(bufferSize / 4) - internal val streamBuf = AudioProcessBuf(bufferSize) - internal val sideChainBufs = Array(track.sidechainInputs.size) { AudioProcessBuf(bufferSize) } + internal var streamBuf: AudioProcessBuf? = null private var fout1 = listOf(emptyBuf, emptyBuf) @@ -43,6 +42,16 @@ class MixerTrackProcessor(val bufferSize: Int, val rate: Int, val track: Terraru private fun printdbg(msg: Any) { if (true) App.printdbg("AudioAdapter ${track.name}", msg) } + + private fun allocateStreamBuf(track: TerrarumAudioMixerTrack) { + streamBuf = AudioProcessBuf(track.currentTrack!!.samplingRate, { + track.currentTrack?.gdxMusic?.forceInvoke("read", arrayOf(it)) + }, { + track.stop() + this.streamBuf = null + }) + } + override fun run() { // while (running) { // uncomment to multithread /*synchronized(pauseLock) { // uncomment to multithread @@ -91,20 +100,8 @@ class MixerTrackProcessor(val bufferSize: Int, val rate: Int, val track: Terraru // fetch deviceBufferSize amount of sample from the disk if (track.trackType != TrackType.MASTER && track.trackType != TrackType.BUS && track.streamPlaying) { - streamBuf.fetchBytes { - try { - 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.stop() - } - } - catch (e: Throwable) { - e.printStackTrace() - it.fill(0) - } - - } + if (streamBuf == null && track.currentTrack != null) allocateStreamBuf(track) + streamBuf!!.fetchBytes() } var samplesL1: FloatArray @@ -129,15 +126,17 @@ class MixerTrackProcessor(val bufferSize: Int, val rate: Int, val track: Terraru } // source channel: skip processing if there's no active input // else if (track.getSidechains().any { it != null && !it.isBus && !it.isMaster && !it.streamPlaying } && !track.streamPlaying) { - else if (!track.streamPlaying) { + else if (!track.streamPlaying || streamBuf == null) { samplesL1 = emptyBuf samplesR1 = emptyBuf bufEmpty = true } else { - samplesL1 = streamBuf.getL1(track.volume) - samplesR1 = streamBuf.getR1(track.volume) + streamBuf!!.getLR(track.volume).let { + samplesL1 = it.first + samplesR1 = it.second + } } if (!bufEmpty) { diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt index f07192a65..dfcc85801 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt @@ -19,6 +19,8 @@ data class MusicContainer( gdxMusic.setOnCompletionListener(songFinishedHook) } + val samplingRate: Int = 44100 // TODO + override fun toString() = if (name.isEmpty()) file.nameWithoutExtension else name }