mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-11 11:04:05 +09:00
audio filters moved to its own files
This commit is contained in:
@@ -6,10 +6,10 @@ import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio
|
|||||||
import com.badlogic.gdx.utils.Disposable
|
import com.badlogic.gdx.utils.Disposable
|
||||||
import com.jme3.math.FastMath
|
import com.jme3.math.FastMath
|
||||||
import net.torvald.terrarum.App
|
import net.torvald.terrarum.App
|
||||||
import net.torvald.terrarum.App.printdbg
|
|
||||||
import net.torvald.terrarum.ModMgr
|
import net.torvald.terrarum.ModMgr
|
||||||
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE
|
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE
|
||||||
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED
|
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.concurrent.ThreadExecutor
|
||||||
import net.torvald.terrarum.modulebasegame.MusicContainer
|
import net.torvald.terrarum.modulebasegame.MusicContainer
|
||||||
import net.torvald.terrarum.tryDispose
|
import net.torvald.terrarum.tryDispose
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package net.torvald.terrarum.audio
|
|||||||
|
|
||||||
import com.badlogic.gdx.utils.Queue
|
import com.badlogic.gdx.utils.Queue
|
||||||
import net.torvald.reflection.forceInvoke
|
import net.torvald.reflection.forceInvoke
|
||||||
|
import net.torvald.terrarum.audio.dsp.NullFilter
|
||||||
import net.torvald.terrarum.sqr
|
import net.torvald.terrarum.sqr
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlin.math.tanh
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by minjaesong on 2023-11-17.
|
* Created by minjaesong on 2023-11-17.
|
||||||
|
|||||||
@@ -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<FloatArray>, outbuf: List<FloatArray>)
|
|
||||||
operator fun invoke(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
// 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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<ComplexArray>
|
|
||||||
// private val convFFTpartd: Array<Array<ComplexArray>> // index: Channel, partition, frequencies
|
|
||||||
private val inbuf: Array<FloatArray>
|
|
||||||
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
// 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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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<FloatArray>, outbuf: List<FloatArray>) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,12 @@ import com.badlogic.gdx.Gdx
|
|||||||
import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio
|
import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio
|
||||||
import com.badlogic.gdx.utils.Disposable
|
import com.badlogic.gdx.utils.Disposable
|
||||||
import com.badlogic.gdx.utils.Queue
|
import com.badlogic.gdx.utils.Queue
|
||||||
import net.torvald.reflection.forceInvoke
|
|
||||||
import net.torvald.terrarum.App
|
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.getHashStr
|
||||||
import net.torvald.terrarum.hashStrMap
|
import net.torvald.terrarum.hashStrMap
|
||||||
import net.torvald.terrarum.modulebasegame.MusicContainer
|
import net.torvald.terrarum.modulebasegame.MusicContainer
|
||||||
import java.lang.Thread.MAX_PRIORITY
|
|
||||||
import kotlin.math.log10
|
import kotlin.math.log10
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
|||||||
65
src/net/torvald/terrarum/audio/dsp/BinoPan.kt
Normal file
65
src/net/torvald/terrarum/audio/dsp/BinoPan.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/net/torvald/terrarum/audio/dsp/Bitcrush.kt
Normal file
17
src/net/torvald/terrarum/audio/dsp/Bitcrush.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/net/torvald/terrarum/audio/dsp/Buffer.kt
Normal file
11
src/net/torvald/terrarum/audio/dsp/Buffer.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package net.torvald.terrarum.audio.dsp
|
||||||
|
|
||||||
|
object Buffer : TerrarumAudioFilter() {
|
||||||
|
init {
|
||||||
|
bypass = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
bypass = true
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/net/torvald/terrarum/audio/dsp/Convolv.kt
Normal file
154
src/net/torvald/terrarum/audio/dsp/Convolv.kt
Normal file
@@ -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<ComplexArray>
|
||||||
|
private val convFFTpartd: Array<Array<ComplexArray>> // index: Channel, partition, frequencies
|
||||||
|
private val inputPartd: Array<Array<FloatArray>> // index: Channel, partition, frequencies
|
||||||
|
private val inbuf: Array<FloatArray>
|
||||||
|
|
||||||
|
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<ComplexArray>) {
|
||||||
|
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<FloatArray>) {
|
||||||
|
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<ComplexArray>, 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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
src/net/torvald/terrarum/audio/dsp/Gain.kt
Normal file
12
src/net/torvald/terrarum/audio/dsp/Gain.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
for (i in 0 until TerrarumAudioMixerTrack.BUFFER_SIZE / 4) {
|
||||||
|
outbuf[0][i] = inbuf[0][i] * gain
|
||||||
|
outbuf[1][i] = inbuf[1][i] * gain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/net/torvald/terrarum/audio/dsp/Highpass.kt
Normal file
50
src/net/torvald/terrarum/audio/dsp/Highpass.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
src/net/torvald/terrarum/audio/dsp/Lowpass.kt
Normal file
50
src/net/torvald/terrarum/audio/dsp/Lowpass.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
src/net/torvald/terrarum/audio/dsp/NullFilter.kt
Normal file
9
src/net/torvald/terrarum/audio/dsp/NullFilter.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package net.torvald.terrarum.audio.dsp
|
||||||
|
|
||||||
|
object NullFilter : TerrarumAudioFilter() {
|
||||||
|
override fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
outbuf.forEachIndexed { index, outTrack ->
|
||||||
|
System.arraycopy(inbuf[index], 0, outTrack, 0, outTrack.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/net/torvald/terrarum/audio/dsp/Reverb.kt
Normal file
49
src/net/torvald/terrarum/audio/dsp/Reverb.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/net/torvald/terrarum/audio/dsp/Scope.kt
Normal file
40
src/net/torvald/terrarum/audio/dsp/Scope.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/net/torvald/terrarum/audio/dsp/SoftClp.kt
Normal file
28
src/net/torvald/terrarum/audio/dsp/SoftClp.kt
Normal file
@@ -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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/net/torvald/terrarum/audio/dsp/TerrarumAudioFilter.kt
Normal file
20
src/net/torvald/terrarum/audio/dsp/TerrarumAudioFilter.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package net.torvald.terrarum.audio.dsp
|
||||||
|
|
||||||
|
abstract class TerrarumAudioFilter {
|
||||||
|
var bypass = false
|
||||||
|
protected abstract fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>)
|
||||||
|
operator fun invoke(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
29
src/net/torvald/terrarum/audio/dsp/XYtoMS.kt
Normal file
29
src/net/torvald/terrarum/audio/dsp/XYtoMS.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package net.torvald.terrarum.audio.dsp
|
||||||
|
|
||||||
|
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack
|
||||||
|
|
||||||
|
object XYtoMS: TerrarumAudioFilter() {
|
||||||
|
override fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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<FloatArray>, outbuf: List<FloatArray>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion
|
|||||||
import net.torvald.terrarum.*
|
import net.torvald.terrarum.*
|
||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
||||||
import net.torvald.terrarum.audio.AudioMixer
|
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.audio.TerrarumAudioMixerTrack
|
||||||
import net.torvald.terrarum.blockproperties.Block
|
import net.torvald.terrarum.blockproperties.Block
|
||||||
import net.torvald.terrarum.blockproperties.BlockPropUtil
|
import net.torvald.terrarum.blockproperties.BlockPropUtil
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import net.torvald.terrarum.Terrarum.getWorldSaveFiledesc
|
|||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
||||||
import net.torvald.terrarum.audio.AudioMixer
|
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.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF
|
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF
|
||||||
import net.torvald.terrarum.blockproperties.BlockPropUtil
|
import net.torvald.terrarum.blockproperties.BlockPropUtil
|
||||||
import net.torvald.terrarum.blockstats.MinimapComposer
|
import net.torvald.terrarum.blockstats.MinimapComposer
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
|||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZEF
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZEF
|
||||||
import net.torvald.terrarum.audio.AudioMixer
|
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.audio.TerrarumAudioMixerTrack
|
||||||
import net.torvald.terrarum.clut.Skybox
|
import net.torvald.terrarum.clut.Skybox
|
||||||
import net.torvald.terrarum.console.CommandDict
|
import net.torvald.terrarum.console.CommandDict
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package net.torvald.terrarum.modulebasegame.ui
|
package net.torvald.terrarum.modulebasegame.ui
|
||||||
|
|
||||||
import com.badlogic.gdx.graphics.Camera
|
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.graphics.OrthographicCamera
|
import com.badlogic.gdx.graphics.OrthographicCamera
|
||||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||||
@@ -8,10 +7,6 @@ import com.jme3.math.FastMath
|
|||||||
import net.torvald.terrarum.*
|
import net.torvald.terrarum.*
|
||||||
import net.torvald.terrarum.App.*
|
import net.torvald.terrarum.App.*
|
||||||
import net.torvald.terrarum.audio.AudioMixer
|
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.langpack.Lang
|
||||||
import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid
|
import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid
|
||||||
import net.torvald.terrarum.ui.Toolkit
|
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.terrarum.ui.UIItemHorizontalFadeSlide
|
||||||
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
|
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
|
||||||
import net.torvald.unicode.*
|
import net.torvald.unicode.*
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by minjaesong on 2017-10-21.
|
* Created by minjaesong on 2017-10-21.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import net.torvald.terrarum.Terrarum.mouseTileY
|
|||||||
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
||||||
import net.torvald.terrarum.audio.*
|
import net.torvald.terrarum.audio.*
|
||||||
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE
|
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.controller.TerrarumController
|
||||||
import net.torvald.terrarum.gameworld.GameWorld
|
import net.torvald.terrarum.gameworld.GameWorld
|
||||||
import net.torvald.terrarum.gameworld.fmod
|
import net.torvald.terrarum.gameworld.fmod
|
||||||
|
|||||||
Reference in New Issue
Block a user