diff --git a/src/net/torvald/terrarum/audio/AudioMixer.kt b/src/net/torvald/terrarum/audio/AudioMixer.kt index 612fbad31..d14b9a690 100644 --- a/src/net/torvald/terrarum/audio/AudioMixer.kt +++ b/src/net/torvald/terrarum/audio/AudioMixer.kt @@ -6,10 +6,10 @@ import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio import com.badlogic.gdx.utils.Disposable import com.jme3.math.FastMath import net.torvald.terrarum.App -import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.ModMgr import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED +import net.torvald.terrarum.audio.dsp.* import net.torvald.terrarum.concurrent.ThreadExecutor import net.torvald.terrarum.modulebasegame.MusicContainer import net.torvald.terrarum.tryDispose diff --git a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt index cd5af7638..026a19237 100644 --- a/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt +++ b/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt @@ -2,10 +2,10 @@ package net.torvald.terrarum.audio import com.badlogic.gdx.utils.Queue import net.torvald.reflection.forceInvoke +import net.torvald.terrarum.audio.dsp.NullFilter import net.torvald.terrarum.sqr import kotlin.math.absoluteValue import kotlin.math.sqrt -import kotlin.math.tanh /** * Created by minjaesong on 2023-11-17. diff --git a/src/net/torvald/terrarum/audio/TerrarumAudioFilter.kt b/src/net/torvald/terrarum/audio/TerrarumAudioFilter.kt deleted file mode 100644 index 9a784c16e..000000000 --- a/src/net/torvald/terrarum/audio/TerrarumAudioFilter.kt +++ /dev/null @@ -1,445 +0,0 @@ -package net.torvald.terrarum.audio - -import com.jme3.math.FastMath -import com.jme3.math.FastMath.sin -import net.torvald.terrarum.App.measureDebugTime -import net.torvald.terrarum.audio.AudioMixer.SPEED_OF_SOUND -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF -import net.torvald.terrarum.roundToFloat -import java.io.File -import kotlin.math.absoluteValue -import kotlin.math.roundToInt -import kotlin.math.tanh - -abstract class TerrarumAudioFilter { - var bypass = false - protected abstract fun thru(inbuf: List, outbuf: List) - operator fun invoke(inbuf: List, outbuf: List) { - if (bypass) { - outbuf.forEachIndexed { index, outTrack -> - System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) - } - } - else thru(inbuf, outbuf) - } -} - -object NullFilter : TerrarumAudioFilter() { - override fun thru(inbuf: List, outbuf: List) { - outbuf.forEachIndexed { index, outTrack -> - System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) - } - } -} - -object SoftClp : TerrarumAudioFilter() { - val downForce = arrayOf(1.0f, 1.0f) - - override fun thru(inbuf: List, outbuf: List) { - downForce.fill(1.0f) - - for (ch in inbuf.indices) { - val inn = inbuf[ch] - val out = outbuf[ch] - - for (i in inn.indices) { - val u = inn[i] * 0.95f - val v = tanh(u) - val diff = (v.absoluteValue / u.absoluteValue) - out[i] = v - - if (!diff.isNaN()) { - downForce[ch] = minOf(downForce[ch], diff) - } - } - } - } -} - -class Scope : TerrarumAudioFilter() { - val backbufL = Array((4096f / BUFFER_SIZE * 4).roundToInt().coerceAtLeast(1)) { FloatArray(BUFFER_SIZE / 4) } - val backbufR = Array((4096f / BUFFER_SIZE * 4).roundToInt().coerceAtLeast(1)) { FloatArray(BUFFER_SIZE / 4) } - - private val sqrt2p = 0.7071067811865475 - - override fun thru(inbuf: List, outbuf: List) { - // shift buffer - for (i in backbufL.lastIndex downTo 1) { - backbufL[i] = backbufL[i - 1] - backbufR[i] = backbufR[i - 1] - } - backbufL[0] = FloatArray(BUFFER_SIZE / 4) - backbufR[0] = FloatArray(BUFFER_SIZE / 4) - - // plot dots - for (i in 0 until BUFFER_SIZE/4) { - val y0 = inbuf[0][i] * 0.7 - val x0 = -inbuf[1][i] * 0.7 // rotate the domain by -90 deg - - val x = (+x0*sqrt2p -y0*sqrt2p) * 1.414 - val y = (-x0*sqrt2p -y0*sqrt2p) * 1.414 // further rotate by -45 deg then flip along the y axis - - backbufL[0][i] = x.toFloat() - backbufR[0][i] = y.toFloat() - } - - // copy samples over - outbuf.forEachIndexed { index, outTrack -> - System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) - } - } -} - - -class Lowpass(cutoff0: Float): TerrarumAudioFilter() { - - var cutoff = cutoff0.toDouble(); private set - private var alpha: Float = 0f - - init { - setCutoff(cutoff0) - } - - fun setCutoff(cutoff: Float) { - val RC: Float = 1f / (cutoff * FastMath.TWO_PI) - val dt: Float = 1f / SAMPLING_RATEF - alpha = dt / (RC + dt) - this.cutoff = cutoff.toDouble() - } - - fun setCutoff(cutoff: Double) { -// println("LP Cutoff: $cutoff") - val RC: Double = 1.0 / (cutoff * Math.PI * 2.0) - val dt: Double = 1.0 / SAMPLING_RATEF - alpha = (dt / (RC + dt)).toFloat() - this.cutoff = cutoff - } - - val in0 = FloatArray(2) - val out0 = FloatArray(2) - - override fun thru(inbuf: List, outbuf: List) { - for (ch in outbuf.indices) { - val out = outbuf[ch] - val inn = inbuf[ch] - - - out[0] = out0[ch] + alpha * (inn[0] - out0[ch]) - - for (i in 1 until outbuf[ch].size) { - out[i] = out[i-1] + alpha * (inn[i] - out[i-1]) - } - - out0[ch] = outbuf[ch].last() - in0[ch] = inbuf[ch].last() - } - } - -} - - -class Highpass(cutoff0: Float): TerrarumAudioFilter() { - - var cutoff = cutoff0.toDouble(); private set - private var alpha: Float = 0f - - val in0 = FloatArray(2) - val out0 = FloatArray(2) - - init { - setCutoff(cutoff0) - } - - fun setCutoff(cutoff: Float) { -// println("LP Cutoff: $cutoff") - val RC: Float = 1f / (cutoff * FastMath.TWO_PI) - val dt: Float = 1f / SAMPLING_RATEF - alpha = RC / (RC + dt) - this.cutoff = cutoff.toDouble() - } - - fun setCutoff(cutoff: Double) { -// println("LP Cutoff: $cutoff") - val RC: Double = 1.0 / (cutoff * Math.PI * 2.0) - val dt: Double = 1.0 / SAMPLING_RATEF - alpha = (RC / (RC + dt)).toFloat() - this.cutoff = cutoff - } - - override fun thru(inbuf: List, outbuf: List) { - for (ch in outbuf.indices) { - val out = outbuf[ch] - val inn = inbuf[ch] - - out[0] = alpha * (out0[ch] + inn[0] - in0[ch]) - - for (i in 1 until outbuf[ch].size) { - out[i] = alpha * (out[i-1] + inn[i] - inn[i-1]) - } - - out0[ch] = outbuf[ch].last() - in0[ch] = inbuf[ch].last() - } - } - -} - - -object Buffer : TerrarumAudioFilter() { - init { - bypass = true - } - - override fun thru(inbuf: List, outbuf: List) { - bypass = true - } -} - -/** - * The input audio must be monaural - * - * @param pan -1 for far-left, 0 for centre, 1 for far-right - * @param soundSpeed speed of the sound in meters per seconds - * @param earDist distance between ears in meters - */ -class BinoPan(var pan: Float, var earDist: Float = 0.18f): TerrarumAudioFilter() { - - private val PANNING_CONST = 3.0 // 3dB panning rule - - private val delayLine = FloatArray(BUFFER_SIZE / 4) - - private fun getFrom(index: Float, buf0: FloatArray, buf1: FloatArray): Float { - val index = index.toInt() // TODO resampling - return if (index >= 0) buf1[index] - else buf0[buf0.size + index] - } - - private val delays = arrayOf(0f, 0f) - private val mults = arrayOf(1f, 1f) - - override fun thru(inbuf: List, outbuf: List) { - val angle = pan * 1.5707963f - val timeDiffMax = earDist / SPEED_OF_SOUND * SAMPLING_RATEF - val delayInSamples = (timeDiffMax * sin(angle)).absoluteValue - val volMultDbThis = PANNING_CONST * pan.absoluteValue - val volMultFsThis = decibelsToFullscale(volMultDbThis).toFloat() - val volMUltFsOther = 1f / volMultFsThis - - if (pan >= 0) { - delays[0] = delayInSamples - delays[1] = 0f - } - else { - delays[0] = 0f - delays[1] = delayInSamples - } - - if (pan >= 0) { - mults[0] = volMUltFsOther - mults[1] = volMultFsThis - } - else { - mults[0] = volMultFsThis - mults[1] = volMUltFsOther - } - - for (ch in 0..1) { - for (i in 0 until BUFFER_SIZE / 4) { - outbuf[ch][i] = getFrom(i - delays[ch], delayLine, inbuf[0]) * mults[ch] - } - } - - push(inbuf[0], delayLine) - } -} - -class Bitcrush(var steps: Int, var inputGain: Float = 1f): TerrarumAudioFilter() { - override fun thru(inbuf: List, outbuf: List) { - for (ch in outbuf.indices) { - for (i in 0 until BUFFER_SIZE / 4) { - val inn = ((inbuf[ch][i] * inputGain).coerceIn(-1f, 1f) + 1f) / 2f // 0f..1f - val stepped = (inn * (steps - 1)).roundToFloat() / (steps - 1) - val out = (stepped * 2f) - 1f // -1f..1f - outbuf[ch][i] = out - } - } - } -} - -class Reverb(val delayMS: Float = 36f, var feedback: Float = 0.92f, var lowpass: Float = 1200f): TerrarumAudioFilter() { - - private val highpass = 80f - - private var delay = (SAMPLING_RATEF * delayMS / 1000f).roundToInt() - private val bufSize = delay + 2 - - private val buf = Array(2) { FloatArray(bufSize) } - - private fun unshift(sample: Float, buf: FloatArray) { - for (i in bufSize - 1 downTo 1) { - buf[i] = buf[i - 1] - } - buf[0] = sample - } - - private val out0 = FloatArray(2) - - override fun thru(inbuf: List, outbuf: List) { - val RCLo: Float = 1f / (lowpass * FastMath.TWO_PI) - val RCHi: Float = 1f / (highpass * FastMath.TWO_PI) - val dt: Float = 1f / SAMPLING_RATEF - val alphaLo = dt / (RCLo + dt) - val alphaHi = RCHi / (RCHi + dt) - - for (ch in outbuf.indices) { - for (i in 0 until BUFFER_SIZE / 4) { - val inn = inbuf[ch][i] - - // reverb - val rev = buf[ch][delay - 1] - val out = inn - rev * feedback - - // fill lpbuf - val lp0 = buf[ch][0] - val lp = lp0 + alphaLo * (out - lp0) - unshift(lp, buf[ch]) - - outbuf[ch][i] = out - } - } - } -} - -class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() { - - val fftLen: Int - private val convFFT: Array -// private val convFFTpartd: Array> // index: Channel, partition, frequencies - private val inbuf: Array - - private val BLOCKSIZE = BUFFER_SIZE / 4 - - var processingSpeed = 1f; private set - -// private val partSizes: IntArray -// private val partOffsets: IntArray - - init { - if (!ir.exists()) { - throw IllegalArgumentException("Impulse Response file '${ir.path}' does not exist.") - } - - val sampleCount = (ir.length().toInt() / 8)//.coerceAtMost(65536) - fftLen = FastMath.nextPowerOfTwo(sampleCount) - - println("IR '${ir.path}' Sample Count = $sampleCount; FFT Length = $fftLen") - - val conv = Array(2) { FloatArray(fftLen) } - inbuf = Array(2) { FloatArray(fftLen) } - - ir.inputStream().let { - for (i in 0 until sampleCount) { - val f1 = Float.fromBits(it.read().and(255) or - it.read().and(255).shl(8) or - it.read().and(255).shl(16) or - it.read().and(255).shl(24)) - val f2 = Float.fromBits(it.read().and(255) or - it.read().and(255).shl(8) or - it.read().and(255).shl(16) or - it.read().and(255).shl(24)) - conv[0][i] = f1 - conv[1][i] = f2 - } - - it.close() - } - - // fourier-transform the 'conv' - convFFT = Array(2) { - FFT.fft(conv[it]) - } - -// println("convFFT Length = ${convFFT[0].size}") - - - } - - - /** - * https://thewolfsound.com/fast-convolution-fft-based-overlap-add-overlap-save-partitioned/ - */ - override fun thru(inbuf: List, outbuf: List) { -// println("Convolv thru") - - val t1 = System.nanoTime() - for (ch in outbuf.indices) { - push(inbuf[ch].applyGain(gain), this.inbuf[ch]) - - var inputFFT: ComplexArray? = null - var Y: ComplexArray? = null - lateinit var y: FloatArray - measureDebugTime("audio.convolve.inputFFT") { inputFFT = FFT.fft(this.inbuf[ch]) } - measureDebugTime("audio.convolve.multiply") { Y = inputFFT!! * convFFT[ch] } - measureDebugTime("audio.convolve.inputIFFT") { y = FFT.ifftAndGetReal(Y!!) } - val u = y.takeLast(BLOCKSIZE).toFloatArray() - - - /*val inputFFTs = FFT.fft(this.inbuf[ch]).sliceUnevenly() - val u = convFFTpartd[ch].zip(inputFFTs).map { (convFFT, inputFFT) -> - val Y = multiply(inputFFT, convFFT) - FFT.ifftAndGetReal(Y) - }.last().takeLast(BLOCKSIZE).toFloatArray()*/ - - - - System.arraycopy(u, 0, outbuf[ch], 0, BLOCKSIZE) - } - val t2 = System.nanoTime() - val ptime = (t2 - t1).toFloat() - val realtime = BLOCKSIZE / SAMPLING_RATEF * 1000000000L - processingSpeed = realtime / ptime - } - -} - -object XYtoMS: TerrarumAudioFilter() { - override fun thru(inbuf: List, outbuf: List) { - for (i in 0 until BUFFER_SIZE / 4) { - val X = inbuf[0][i] - val Y = inbuf[1][i] - val M = (X + Y) / 2f - val S = (X - Y) / 2f - outbuf[0][i] = M - outbuf[1][i] = S - } - } -} - -object MStoXY: TerrarumAudioFilter() { - override fun thru(inbuf: List, outbuf: List) { - for (i in 0 until BUFFER_SIZE / 4) { - val M = inbuf[0][i] - val S = inbuf[1][i] - val X = M + S - val Y = M - S - outbuf[0][i] = X - outbuf[1][i] = Y - } - } -} - -class Gain(var gain: Float): TerrarumAudioFilter() { - override fun thru(inbuf: List, outbuf: List) { - for (i in 0 until BUFFER_SIZE / 4) { - outbuf[0][i] = inbuf[0][i] * gain - outbuf[1][i] = inbuf[1][i] * gain - } - } -} - -fun FloatArray.applyGain(gain: Float = 1f) = this.map { it * gain }.toFloatArray() -fun push(samples: FloatArray, buf: FloatArray) { - System.arraycopy(buf, samples.size, buf, 0, buf.size - samples.size) - System.arraycopy(samples, 0, buf, buf.size - samples.size, samples.size) -} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt index b915b1a99..c9c75ddc3 100644 --- a/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt +++ b/src/net/torvald/terrarum/audio/TerrarumAudioMixerTrack.kt @@ -4,12 +4,12 @@ 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.App +import net.torvald.terrarum.audio.dsp.NullFilter +import net.torvald.terrarum.audio.dsp.TerrarumAudioFilter import net.torvald.terrarum.getHashStr import net.torvald.terrarum.hashStrMap import net.torvald.terrarum.modulebasegame.MusicContainer -import java.lang.Thread.MAX_PRIORITY import kotlin.math.log10 import kotlin.math.pow diff --git a/src/net/torvald/terrarum/audio/dsp/BinoPan.kt b/src/net/torvald/terrarum/audio/dsp/BinoPan.kt new file mode 100644 index 000000000..1dad2b2b7 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/BinoPan.kt @@ -0,0 +1,65 @@ +package net.torvald.terrarum.audio.dsp + +import com.jme3.math.FastMath +import net.torvald.terrarum.audio.AudioMixer +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack +import net.torvald.terrarum.audio.decibelsToFullscale +import kotlin.math.absoluteValue + +/** + * The input audio must be monaural + * + * @param pan -1 for far-left, 0 for centre, 1 for far-right + * @param soundSpeed speed of the sound in meters per seconds + * @param earDist distance between ears in meters + */ +class BinoPan(var pan: Float, var earDist: Float = 0.18f): TerrarumAudioFilter() { + + private val PANNING_CONST = 3.0 // 3dB panning rule + + private val delayLine = FloatArray(TerrarumAudioMixerTrack.BUFFER_SIZE / 4) + + private fun getFrom(index: Float, buf0: FloatArray, buf1: FloatArray): Float { + val index = index.toInt() // TODO resampling + return if (index >= 0) buf1[index] + else buf0[buf0.size + index] + } + + private val delays = arrayOf(0f, 0f) + private val mults = arrayOf(1f, 1f) + + override fun thru(inbuf: List, outbuf: List) { + val angle = pan * 1.5707963f + val timeDiffMax = earDist / AudioMixer.SPEED_OF_SOUND * TerrarumAudioMixerTrack.SAMPLING_RATEF + val delayInSamples = (timeDiffMax * FastMath.sin(angle)).absoluteValue + val volMultDbThis = PANNING_CONST * pan.absoluteValue + val volMultFsThis = decibelsToFullscale(volMultDbThis).toFloat() + val volMUltFsOther = 1f / volMultFsThis + + if (pan >= 0) { + delays[0] = delayInSamples + delays[1] = 0f + } + else { + delays[0] = 0f + delays[1] = delayInSamples + } + + if (pan >= 0) { + mults[0] = volMUltFsOther + mults[1] = volMultFsThis + } + else { + mults[0] = volMultFsThis + mults[1] = volMUltFsOther + } + + for (ch in 0..1) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + outbuf[ch][i] = getFrom(i - delays[ch], delayLine, inbuf[0]) * mults[ch] + } + } + + push(inbuf[0], delayLine) + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Bitcrush.kt b/src/net/torvald/terrarum/audio/dsp/Bitcrush.kt new file mode 100644 index 000000000..8e3032441 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Bitcrush.kt @@ -0,0 +1,17 @@ +package net.torvald.terrarum.audio.dsp + +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack +import net.torvald.terrarum.roundToFloat + +class Bitcrush(var steps: Int, var inputGain: Float = 1f): TerrarumAudioFilter() { + override fun thru(inbuf: List, outbuf: List) { + for (ch in outbuf.indices) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + val inn = ((inbuf[ch][i] * inputGain).coerceIn(-1f, 1f) + 1f) / 2f // 0f..1f + val stepped = (inn * (steps - 1)).roundToFloat() / (steps - 1) + val out = (stepped * 2f) - 1f // -1f..1f + outbuf[ch][i] = out + } + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Buffer.kt b/src/net/torvald/terrarum/audio/dsp/Buffer.kt new file mode 100644 index 000000000..bb3bdec48 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Buffer.kt @@ -0,0 +1,11 @@ +package net.torvald.terrarum.audio.dsp + +object Buffer : TerrarumAudioFilter() { + init { + bypass = true + } + + override fun thru(inbuf: List, outbuf: List) { + bypass = true + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Convolv.kt b/src/net/torvald/terrarum/audio/dsp/Convolv.kt new file mode 100644 index 000000000..9f773a7cd --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Convolv.kt @@ -0,0 +1,154 @@ +package net.torvald.terrarum.audio.dsp + +import com.jme3.math.FastMath +import net.torvald.terrarum.App.measureDebugTime +import net.torvald.terrarum.audio.* +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE +import java.io.File + +class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() { + + val fftLen: Int + private val convFFT: Array + private val convFFTpartd: Array> // index: Channel, partition, frequencies + private val inputPartd: Array> // index: Channel, partition, frequencies + private val inbuf: Array + + private val BLOCKSIZE = TerrarumAudioMixerTrack.BUFFER_SIZE / 4 + + var processingSpeed = 1f; private set + + private val partSizes: IntArray + private val partOffsets: IntArray + + init { + if (!ir.exists()) { + throw IllegalArgumentException("Impulse Response file '${ir.path}' does not exist.") + } + + val sampleCount = (ir.length().toInt() / 8)//.coerceAtMost(65536) + fftLen = FastMath.nextPowerOfTwo(sampleCount) + + println("IR '${ir.path}' Sample Count = $sampleCount; FFT Length = $fftLen") + + val conv = Array(2) { FloatArray(fftLen) } + inbuf = Array(2) { FloatArray(fftLen) } + + ir.inputStream().let { + for (i in 0 until sampleCount) { + val f1 = Float.fromBits(it.read().and(255) or + it.read().and(255).shl(8) or + it.read().and(255).shl(16) or + it.read().and(255).shl(24)) + val f2 = Float.fromBits(it.read().and(255) or + it.read().and(255).shl(8) or + it.read().and(255).shl(16) or + it.read().and(255).shl(24)) + conv[0][i] = f1 + conv[1][i] = f2 + } + + it.close() + } + + // fourier-transform the 'conv' + convFFT = Array(2) { + FFT.fft(conv[it]) + } + +// println("convFFT Length = ${convFFT[0].size}") + + + // fill up part* dictionary + // define "master" array + var c = BUFFER_SIZE / 4 + val master0 = arrayListOf(c) + while (c < fftLen) { + master0.add(c) + c *= 2 + } + partSizes = master0.toIntArray() + partOffsets = master0.toIntArray().also { it[0] = 0 } + + + convFFTpartd = Array(2) { + Array(partSizes.size) { + ComplexArray(FloatArray(2*partSizes[it])) + } + } + inputPartd = Array(2) { + Array(partSizes.size) { + FloatArray(partSizes[it]) + } + } + fillUnevenly(convFFT[0], convFFTpartd[0]) + fillUnevenly(convFFT[1], convFFTpartd[1]) + } + + private fun fillUnevenly(source: ComplexArray, dest: Array) { + for (i in partSizes.indices) { + val len = 2*partSizes[i] + val offset = 2*partOffsets[i] + System.arraycopy(source.reim, offset, dest[i].reim, 0, len) + } + } + private fun fillUnevenly(source: FloatArray, dest: Array) { + for (i in partSizes.indices) { + val len = partSizes[i] + val offset = partOffsets[i] + System.arraycopy(source, offset, dest[i], 0, len) + } + } + private fun concatParts(source: List, dest: ComplexArray) { + for (i in partSizes.indices) { + val len = 2*partSizes[i] + val offset = 2*partOffsets[i] + System.arraycopy(source[i].reim, 0, dest.reim, offset, len) + } + } + + + private val targetY = ComplexArray(FloatArray(fftLen * 2)) + + /** + * https://thewolfsound.com/fast-convolution-fft-based-overlap-add-overlap-save-partitioned/ + */ + override fun thru(inbuf: List, outbuf: List) { +// println("Convolv thru") + + val t1 = System.nanoTime() + for (ch in outbuf.indices) { + push(inbuf[ch].applyGain(gain), this.inbuf[ch]) + lateinit var u: FloatArray + + + measureDebugTime("audio.convolve") { + val inputFFT = FFT.fft(this.inbuf[ch]) + val Y = inputFFT * convFFT[ch] + val y = FFT.ifftAndGetReal(Y) + u = y.takeLast(BLOCKSIZE).toFloatArray() + } + + + // doesn't work AND slightly slower than the lines above + /*measureDebugTime("audio.convolve") { + // orthodox uneven-partitioning + fillUnevenly(this.inbuf[ch], inputPartd[ch]) + val partY = inputPartd[ch].mapIndexed { i, inputSamples -> + FFT.fft(inputSamples) * convFFTpartd[ch][i] + } + concatParts(partY, targetY) + val y = FFT.ifftAndGetReal(targetY) + u = y.takeLast(BLOCKSIZE).toFloatArray() + }*/ + + + System.arraycopy(u, 0, outbuf[ch], 0, BLOCKSIZE) + } + val t2 = System.nanoTime() + val ptime = (t2 - t1).toFloat() + val realtime = BLOCKSIZE / TerrarumAudioMixerTrack.SAMPLING_RATEF * 1000000000L + processingSpeed = realtime / ptime + } + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Gain.kt b/src/net/torvald/terrarum/audio/dsp/Gain.kt new file mode 100644 index 000000000..c92cb86d9 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Gain.kt @@ -0,0 +1,12 @@ +package net.torvald.terrarum.audio.dsp + +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack + +class Gain(var gain: Float): TerrarumAudioFilter() { + override fun thru(inbuf: List, outbuf: List) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + outbuf[0][i] = inbuf[0][i] * gain + outbuf[1][i] = inbuf[1][i] * gain + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Highpass.kt b/src/net/torvald/terrarum/audio/dsp/Highpass.kt new file mode 100644 index 000000000..2aa14f38f --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Highpass.kt @@ -0,0 +1,50 @@ +package net.torvald.terrarum.audio.dsp + +import com.jme3.math.FastMath +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack + +class Highpass(cutoff0: Float): TerrarumAudioFilter() { + + var cutoff = cutoff0.toDouble(); private set + private var alpha: Float = 0f + + val in0 = FloatArray(2) + val out0 = FloatArray(2) + + init { + setCutoff(cutoff0) + } + + fun setCutoff(cutoff: Float) { +// println("LP Cutoff: $cutoff") + val RC: Float = 1f / (cutoff * FastMath.TWO_PI) + val dt: Float = 1f / TerrarumAudioMixerTrack.SAMPLING_RATEF + alpha = RC / (RC + dt) + this.cutoff = cutoff.toDouble() + } + + fun setCutoff(cutoff: Double) { +// println("LP Cutoff: $cutoff") + val RC: Double = 1.0 / (cutoff * Math.PI * 2.0) + val dt: Double = 1.0 / TerrarumAudioMixerTrack.SAMPLING_RATEF + alpha = (RC / (RC + dt)).toFloat() + this.cutoff = cutoff + } + + override fun thru(inbuf: List, outbuf: List) { + for (ch in outbuf.indices) { + val out = outbuf[ch] + val inn = inbuf[ch] + + out[0] = alpha * (out0[ch] + inn[0] - in0[ch]) + + for (i in 1 until outbuf[ch].size) { + out[i] = alpha * (out[i-1] + inn[i] - inn[i-1]) + } + + out0[ch] = outbuf[ch].last() + in0[ch] = inbuf[ch].last() + } + } + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Lowpass.kt b/src/net/torvald/terrarum/audio/dsp/Lowpass.kt new file mode 100644 index 000000000..20f71162a --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Lowpass.kt @@ -0,0 +1,50 @@ +package net.torvald.terrarum.audio.dsp + +import com.jme3.math.FastMath +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack + +class Lowpass(cutoff0: Float): TerrarumAudioFilter() { + + var cutoff = cutoff0.toDouble(); private set + private var alpha: Float = 0f + + init { + setCutoff(cutoff0) + } + + fun setCutoff(cutoff: Float) { + val RC: Float = 1f / (cutoff * FastMath.TWO_PI) + val dt: Float = 1f / TerrarumAudioMixerTrack.SAMPLING_RATEF + alpha = dt / (RC + dt) + this.cutoff = cutoff.toDouble() + } + + fun setCutoff(cutoff: Double) { +// println("LP Cutoff: $cutoff") + val RC: Double = 1.0 / (cutoff * Math.PI * 2.0) + val dt: Double = 1.0 / TerrarumAudioMixerTrack.SAMPLING_RATEF + alpha = (dt / (RC + dt)).toFloat() + this.cutoff = cutoff + } + + val in0 = FloatArray(2) + val out0 = FloatArray(2) + + override fun thru(inbuf: List, outbuf: List) { + for (ch in outbuf.indices) { + val out = outbuf[ch] + val inn = inbuf[ch] + + + out[0] = out0[ch] + alpha * (inn[0] - out0[ch]) + + for (i in 1 until outbuf[ch].size) { + out[i] = out[i-1] + alpha * (inn[i] - out[i-1]) + } + + out0[ch] = outbuf[ch].last() + in0[ch] = inbuf[ch].last() + } + } + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/NullFilter.kt b/src/net/torvald/terrarum/audio/dsp/NullFilter.kt new file mode 100644 index 000000000..4c6177543 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/NullFilter.kt @@ -0,0 +1,9 @@ +package net.torvald.terrarum.audio.dsp + +object NullFilter : TerrarumAudioFilter() { + override fun thru(inbuf: List, outbuf: List) { + outbuf.forEachIndexed { index, outTrack -> + System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Reverb.kt b/src/net/torvald/terrarum/audio/dsp/Reverb.kt new file mode 100644 index 000000000..4e890e855 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Reverb.kt @@ -0,0 +1,49 @@ +package net.torvald.terrarum.audio.dsp + +import com.jme3.math.FastMath +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack +import kotlin.math.roundToInt + +class Reverb(val delayMS: Float = 36f, var feedback: Float = 0.92f, var lowpass: Float = 1200f): TerrarumAudioFilter() { + + private val highpass = 80f + + private var delay = (TerrarumAudioMixerTrack.SAMPLING_RATEF * delayMS / 1000f).roundToInt() + private val bufSize = delay + 2 + + private val buf = Array(2) { FloatArray(bufSize) } + + private fun unshift(sample: Float, buf: FloatArray) { + for (i in bufSize - 1 downTo 1) { + buf[i] = buf[i - 1] + } + buf[0] = sample + } + + private val out0 = FloatArray(2) + + override fun thru(inbuf: List, outbuf: List) { + val RCLo: Float = 1f / (lowpass * FastMath.TWO_PI) + val RCHi: Float = 1f / (highpass * FastMath.TWO_PI) + val dt: Float = 1f / TerrarumAudioMixerTrack.SAMPLING_RATEF + val alphaLo = dt / (RCLo + dt) + val alphaHi = RCHi / (RCHi + dt) + + for (ch in outbuf.indices) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + val inn = inbuf[ch][i] + + // reverb + val rev = buf[ch][delay - 1] + val out = inn - rev * feedback + + // fill lpbuf + val lp0 = buf[ch][0] + val lp = lp0 + alphaLo * (out - lp0) + unshift(lp, buf[ch]) + + outbuf[ch][i] = out + } + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/Scope.kt b/src/net/torvald/terrarum/audio/dsp/Scope.kt new file mode 100644 index 000000000..e9b42ef08 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/Scope.kt @@ -0,0 +1,40 @@ +package net.torvald.terrarum.audio.dsp + +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack +import kotlin.math.roundToInt + +class Scope : TerrarumAudioFilter() { + val backbufL = Array((4096f / TerrarumAudioMixerTrack.BUFFER_SIZE * 4).roundToInt().coerceAtLeast(1)) { FloatArray( + TerrarumAudioMixerTrack.BUFFER_SIZE / 4) } + val backbufR = Array((4096f / TerrarumAudioMixerTrack.BUFFER_SIZE * 4).roundToInt().coerceAtLeast(1)) { FloatArray( + TerrarumAudioMixerTrack.BUFFER_SIZE / 4) } + + private val sqrt2p = 0.7071067811865475 + + override fun thru(inbuf: List, outbuf: List) { + // shift buffer + for (i in backbufL.lastIndex downTo 1) { + backbufL[i] = backbufL[i - 1] + backbufR[i] = backbufR[i - 1] + } + backbufL[0] = FloatArray(TerrarumAudioMixerTrack.BUFFER_SIZE / 4) + backbufR[0] = FloatArray(TerrarumAudioMixerTrack.BUFFER_SIZE / 4) + + // plot dots + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE /4) { + val y0 = inbuf[0][i] * 0.7 + val x0 = -inbuf[1][i] * 0.7 // rotate the domain by -90 deg + + val x = (+x0*sqrt2p -y0*sqrt2p) * 1.414 + val y = (-x0*sqrt2p -y0*sqrt2p) * 1.414 // further rotate by -45 deg then flip along the y axis + + backbufL[0][i] = x.toFloat() + backbufR[0][i] = y.toFloat() + } + + // copy samples over + outbuf.forEachIndexed { index, outTrack -> + System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/SoftClp.kt b/src/net/torvald/terrarum/audio/dsp/SoftClp.kt new file mode 100644 index 000000000..96fa483c8 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/SoftClp.kt @@ -0,0 +1,28 @@ +package net.torvald.terrarum.audio.dsp + +import kotlin.math.absoluteValue +import kotlin.math.tanh + +object SoftClp : TerrarumAudioFilter() { + val downForce = arrayOf(1.0f, 1.0f) + + override fun thru(inbuf: List, outbuf: List) { + downForce.fill(1.0f) + + for (ch in inbuf.indices) { + val inn = inbuf[ch] + val out = outbuf[ch] + + for (i in inn.indices) { + val u = inn[i] * 0.95f + val v = tanh(u) + val diff = (v.absoluteValue / u.absoluteValue) + out[i] = v + + if (!diff.isNaN()) { + downForce[ch] = minOf(downForce[ch], diff) + } + } + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/TerrarumAudioFilter.kt b/src/net/torvald/terrarum/audio/dsp/TerrarumAudioFilter.kt new file mode 100644 index 000000000..61a545a10 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/TerrarumAudioFilter.kt @@ -0,0 +1,20 @@ +package net.torvald.terrarum.audio.dsp + +abstract class TerrarumAudioFilter { + var bypass = false + protected abstract fun thru(inbuf: List, outbuf: List) + operator fun invoke(inbuf: List, outbuf: List) { + if (bypass) { + outbuf.forEachIndexed { index, outTrack -> + System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size) + } + } + else thru(inbuf, outbuf) + } +} + +fun FloatArray.applyGain(gain: Float = 1f) = this.map { it * gain }.toFloatArray() +fun push(samples: FloatArray, buf: FloatArray) { + System.arraycopy(buf, samples.size, buf, 0, buf.size - samples.size) + System.arraycopy(samples, 0, buf, buf.size - samples.size, samples.size) +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/dsp/XYtoMS.kt b/src/net/torvald/terrarum/audio/dsp/XYtoMS.kt new file mode 100644 index 000000000..c14691529 --- /dev/null +++ b/src/net/torvald/terrarum/audio/dsp/XYtoMS.kt @@ -0,0 +1,29 @@ +package net.torvald.terrarum.audio.dsp + +import net.torvald.terrarum.audio.TerrarumAudioMixerTrack + +object XYtoMS: TerrarumAudioFilter() { + override fun thru(inbuf: List, outbuf: List) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + val X = inbuf[0][i] + val Y = inbuf[1][i] + val M = (X + Y) / 2f + val S = (X - Y) / 2f + outbuf[0][i] = M + outbuf[1][i] = S + } + } +} + +object MStoXY: TerrarumAudioFilter() { + override fun thru(inbuf: List, outbuf: List) { + for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) { + val M = inbuf[0][i] + val S = inbuf[1][i] + val X = M + S + val Y = M - S + outbuf[0][i] = X + outbuf[1][i] = Y + } + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt b/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt index 671caa9f4..416bca25a 100644 --- a/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt +++ b/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt @@ -8,7 +8,7 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion import net.torvald.terrarum.* import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.audio.AudioMixer -import net.torvald.terrarum.audio.Lowpass +import net.torvald.terrarum.audio.dsp.Lowpass import net.torvald.terrarum.audio.TerrarumAudioMixerTrack import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.blockproperties.BlockPropUtil diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index fb49c6707..62a428462 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -13,8 +13,7 @@ import net.torvald.terrarum.Terrarum.getWorldSaveFiledesc import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED import net.torvald.terrarum.audio.AudioMixer -import net.torvald.terrarum.audio.Lowpass -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack +import net.torvald.terrarum.audio.dsp.Lowpass import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF import net.torvald.terrarum.blockproperties.BlockPropUtil import net.torvald.terrarum.blockstats.MinimapComposer diff --git a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt index 005a276b2..35254a1b6 100644 --- a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt +++ b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt @@ -18,7 +18,7 @@ import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZEF import net.torvald.terrarum.audio.AudioMixer -import net.torvald.terrarum.audio.Lowpass +import net.torvald.terrarum.audio.dsp.Lowpass import net.torvald.terrarum.audio.TerrarumAudioMixerTrack import net.torvald.terrarum.clut.Skybox import net.torvald.terrarum.console.CommandDict diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt index 0d1918bcf..87ebedb7c 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIInventoryFull.kt @@ -1,6 +1,5 @@ package net.torvald.terrarum.modulebasegame.ui -import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.OrthographicCamera import com.badlogic.gdx.graphics.g2d.SpriteBatch @@ -8,10 +7,6 @@ import com.jme3.math.FastMath import net.torvald.terrarum.* import net.torvald.terrarum.App.* import net.torvald.terrarum.audio.AudioMixer -import net.torvald.terrarum.audio.Lowpass -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE -import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid import net.torvald.terrarum.ui.Toolkit @@ -20,7 +15,6 @@ import net.torvald.terrarum.ui.UIHandler import net.torvald.terrarum.ui.UIItemHorizontalFadeSlide import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack import net.torvald.unicode.* -import kotlin.math.pow /** * Created by minjaesong on 2017-10-21. diff --git a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt index b14df1e5c..f40595b23 100644 --- a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt +++ b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt @@ -14,6 +14,7 @@ import net.torvald.terrarum.Terrarum.mouseTileY import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.audio.* import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE +import net.torvald.terrarum.audio.dsp.* import net.torvald.terrarum.controller.TerrarumController import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.fmod