mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-15 08:06:06 +09:00
209 lines
8.0 KiB
Kotlin
209 lines
8.0 KiB
Kotlin
package net.torvald.terrarum.audio
|
|
|
|
import net.torvald.terrarum.App
|
|
import net.torvald.terrarum.App.printdbg
|
|
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.roundToInt
|
|
import kotlin.math.sin
|
|
|
|
private data class Frac(var nom: Int, val denom: Int) {
|
|
fun toDouble() = nom.toDouble() / denom.toDouble()
|
|
fun toFloat() = this.toDouble().toFloat()
|
|
private val denomStrLen = denom.toString().length
|
|
override fun toString() = "${nom.toString().padStart(denomStrLen)} / $denom"
|
|
}
|
|
|
|
/**
|
|
* Audio is assumed to be 2 channels, 16 bits
|
|
*
|
|
* Created by minjaesong on 2023-11-17.
|
|
*/
|
|
class AudioProcessBuf(val inputSamplingRate: Int, val audioReadFun: (ByteArray) -> Int?, val onAudioFinished: () -> Unit) {
|
|
|
|
var playbackSpeed = 1f
|
|
set(value) {
|
|
field = value.coerceIn(0.5f, 2f)
|
|
}
|
|
|
|
private val internalSamplingRate
|
|
get() = inputSamplingRate * playbackSpeed
|
|
|
|
private val doResample
|
|
get() = !(inputSamplingRate == SAMPLING_RATE && (playbackSpeed - 1f).absoluteValue < (1f / 1024f))
|
|
|
|
companion object {
|
|
private val epsilon: Double = Epsilon.E
|
|
|
|
private val TAPS = 4 // 2*a tap lanczos intp. Lower = greater artefacts
|
|
|
|
private val Lcache = HashMap<Long, Double>(1048576)
|
|
fun L(x: Double): Double {
|
|
return Lcache.getOrPut(x.toBits()) { // converting double to longbits allows faster cache lookup?!
|
|
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
|
|
}
|
|
}
|
|
|
|
const val MP3_CHUNK_SIZE = 1152 // 1152 for 32k-48k, 576 for 16k-24k, 384 for 8k-12k
|
|
|
|
|
|
private val bufLut = HashMap<Pair<Int, Int>, Int>()
|
|
|
|
init {
|
|
val bl = arrayOf(
|
|
1152,1380,1814,1792,2304,2634,3502,3456,4608,5141,6874,6912,
|
|
1280,1508,1942,1920,2304,2762,3630,3584,4608,5267,7004,6912,
|
|
1536,1764,2198,2176,2560,3018,3886,3840,4608,5519,7260,7168,
|
|
2048,2276,2710,2688,3072,3530,4398,4352,5120,6023,7772,7680,
|
|
4096,4554,5421,5376,6144,7056,8796,8704,10240,12078,15544,15360
|
|
)
|
|
|
|
arrayOf(48000,44100,32768,32000,24000,22050,16384,16000,12000,11025,8192,8000).forEachIndexed { ri, r ->
|
|
arrayOf(128,256,512,1024,2048).forEachIndexed { bi, b ->
|
|
bufLut[b to r] = bl[bi * 12 + ri] * 2
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getOptimalBufferSize(rate: Int) = bufLut[App.audioBufferSize to rate]!!
|
|
}
|
|
|
|
private val q
|
|
get() = internalSamplingRate.toDouble() / SAMPLING_RATE // <= 1.0
|
|
|
|
private val fetchSize = (App.audioBufferSize.toFloat() / MP3_CHUNK_SIZE).ceilToInt() * MP3_CHUNK_SIZE // fetchSize is always multiple of MP3_CHUNK_SIZE, even if the audio is NOT MP3
|
|
private val internalBufferSize = getOptimalBufferSize(inputSamplingRate)// fetchSize * 3
|
|
|
|
private val PADSIZE = TAPS + 1
|
|
|
|
private fun resampleBlock(innL: FloatArray, innR: FloatArray, outL: FloatArray, outR: FloatArray, outSampleCount: Int) {
|
|
for (sampleIdx in 0 until outSampleCount) {
|
|
val t = sampleIdx.toDouble() * q
|
|
val leftBound = maxOf(0, (t - TAPS + 1).floorToInt())
|
|
val rightBound = minOf(innL.size - 1, (t + TAPS).ceilToInt())
|
|
|
|
|
|
var akkuL = 0.0
|
|
var akkuR = 0.0
|
|
var weightedSum = 0.0
|
|
|
|
for (j in leftBound..rightBound) {
|
|
val w = L(t - j.toDouble())
|
|
akkuL += innL[j] * w
|
|
akkuR += innR[j] * w
|
|
weightedSum += w
|
|
}
|
|
|
|
outL[sampleIdx] = (akkuL / weightedSum).toFloat()
|
|
outR[sampleIdx] = (akkuR / weightedSum).toFloat()
|
|
}
|
|
}
|
|
|
|
var validSamplesInBuf = 0
|
|
|
|
private val finL = FloatArray(fetchSize + 2 * PADSIZE)
|
|
private val finR = FloatArray(fetchSize + 2 * PADSIZE)
|
|
private val fmidL = FloatArray((fetchSize / q + 1.0).toInt() * 2)
|
|
private val fmidR = FloatArray((fetchSize / q + 1.0).toInt() * 2)
|
|
private val foutL = FloatArray(internalBufferSize) // 640 for (44100, 48000), 512 for (48000, 48000) with BUFFER_SIZE = 512 * 4
|
|
private val foutR = FloatArray(internalBufferSize) // 640 for (44100, 48000), 512 for (48000, 48000) with BUFFER_SIZE = 512 * 4
|
|
private val readBuf = ByteArray(fetchSize * 4)
|
|
|
|
init {
|
|
printdbg(this, "App.audioMixerBufferSize=${App.audioBufferSize}")
|
|
}
|
|
|
|
private fun shift(array: FloatArray, size: Int) {
|
|
System.arraycopy(array, size, array, 0, array.size - size)
|
|
for (i in array.size - size until array.size) { array[i] = 0f }
|
|
}
|
|
|
|
fun fetchBytes() {
|
|
val readCount = if (validSamplesInBuf < App.audioBufferSize) fetchSize else 0
|
|
val writeCount = (readCount / q).roundToInt()
|
|
|
|
fun getFromReadBuf(i: Int, bytesRead: Int) = if (i < bytesRead) readBuf[i].toUint() else 0
|
|
|
|
if (readCount > 0) {
|
|
try {
|
|
shift(finL, readCount)
|
|
shift(finR, readCount)
|
|
|
|
val bytesRead = audioReadFun(readBuf)
|
|
// printdbg(this, "Reading audio $readCount samples, got ${bytesRead?.div(4)} samples")
|
|
|
|
if (bytesRead == null || bytesRead <= 0) {
|
|
// printdbg(this, "Music finished; bytesRead = $bytesRead")
|
|
|
|
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[2 * PADSIZE + c] = fl
|
|
finR[2 * PADSIZE + c] = fr
|
|
}
|
|
}
|
|
}
|
|
catch (e: Throwable) {
|
|
e.printStackTrace()
|
|
}
|
|
finally {
|
|
if (doResample) {
|
|
// perform resampling
|
|
resampleBlock(finL, finR, fmidL, fmidR, writeCount)
|
|
|
|
// fill in the output buffers
|
|
System.arraycopy(fmidL, 0, foutL, validSamplesInBuf, writeCount)
|
|
System.arraycopy(fmidR, 0, foutR, validSamplesInBuf, writeCount)
|
|
}
|
|
else {
|
|
// fill in the output buffers
|
|
System.arraycopy(finL, 0, foutL, validSamplesInBuf, writeCount)
|
|
System.arraycopy(finR, 0, foutR, validSamplesInBuf, writeCount)
|
|
}
|
|
|
|
validSamplesInBuf += writeCount
|
|
}
|
|
}
|
|
else {
|
|
// printdbg(this, "Reading audio zero samples; Buffer: $validSamplesInBuf / $internalBufferSize samples")
|
|
}
|
|
|
|
// printdbg(this, "phase = $fPhaseL")
|
|
}
|
|
|
|
fun getLR(): Pair<FloatArray, FloatArray> {
|
|
// copy into the out
|
|
val outL = FloatArray(App.audioBufferSize)
|
|
val outR = FloatArray(App.audioBufferSize)
|
|
System.arraycopy(foutL, 0, outL, 0, App.audioBufferSize)
|
|
System.arraycopy(foutR, 0, outR, 0, App.audioBufferSize)
|
|
// shift bytes in the fout
|
|
System.arraycopy(foutL, App.audioBufferSize, foutL, 0, validSamplesInBuf - App.audioBufferSize)
|
|
System.arraycopy(foutR, App.audioBufferSize, foutR, 0, validSamplesInBuf - App.audioBufferSize)
|
|
for (i in validSamplesInBuf until App.audioBufferSize) {
|
|
foutL[i] = 0f
|
|
foutR[i] = 0f
|
|
}
|
|
// decrement necessary variables
|
|
validSamplesInBuf -= App.audioBufferSize
|
|
|
|
return outL to outR
|
|
}
|
|
} |