mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-11 19:14:05 +09:00
fft: data struct optimisation
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
assets/mods/basegame/audio/convolution/EchoThief - PurgatoryChasm.bin
LFS
Normal file
BIN
assets/mods/basegame/audio/convolution/EchoThief - PurgatoryChasm.bin
LFS
Normal file
Binary file not shown.
BIN
assets/mods/basegame/audio/convolution/EchoThief - WaterplacePark-trimmed.bin
LFS
Normal file
BIN
assets/mods/basegame/audio/convolution/EchoThief - WaterplacePark-trimmed.bin
LFS
Normal file
Binary file not shown.
Binary file not shown.
@@ -198,22 +198,6 @@ SOFTWARE.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
$BULLET Ambient sound recordings:
|
|
||||||
|
|
||||||
- ambient_forest_01.ogg
|
|
||||||
- ambient_meadow_01.ogg
|
|
||||||
- ambient_windy_01.ogg
|
|
||||||
- ambient_woods_01.ogg
|
|
||||||
- crickets_01.ogg
|
|
||||||
- crickets_02.ogg
|
|
||||||
|
|
||||||
Copyright (C) 2012, 2013, 2015, 2016, 2017 Klankbeeld
|
|
||||||
Sound from <http://www.freesound.org/people/klankbeeld/>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$BULLET GraalVM Community Edition
|
$BULLET GraalVM Community Edition
|
||||||
|
|
||||||
GraalVM Community Edition consists of multiple modules. The software as a whole,
|
GraalVM Community Edition consists of multiple modules. The software as a whole,
|
||||||
@@ -253,6 +237,7 @@ SOFTWARE.
|
|||||||
|
|
||||||
|
|
||||||
$BULLET Apache Commons Codec
|
$BULLET Apache Commons Codec
|
||||||
|
|
||||||
Copyright 2002-2023 The Apache Software Foundation
|
Copyright 2002-2023 The Apache Software Foundation
|
||||||
|
|
||||||
This product includes software developed at
|
This product includes software developed at
|
||||||
@@ -263,6 +248,7 @@ The Apache Software Foundation (https://www.apache.org/).
|
|||||||
|
|
||||||
|
|
||||||
$BULLET Apache Commons CSV
|
$BULLET Apache Commons CSV
|
||||||
|
|
||||||
Copyright 2005-2023 The Apache Software Foundation
|
Copyright 2005-2023 The Apache Software Foundation
|
||||||
|
|
||||||
This product includes software developed at
|
This product includes software developed at
|
||||||
@@ -282,6 +268,30 @@ The Apache Software Foundation (http://www.apache.org/).
|
|||||||
This product includes software developed for Orekit by
|
This product includes software developed for Orekit by
|
||||||
CS Systèmes d'Information (http://www.c-s.fr/)
|
CS Systèmes d'Information (http://www.c-s.fr/)
|
||||||
Copyright 2010-2012 CS Systèmes d'Information
|
Copyright 2010-2012 CS Systèmes d'Information
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$BULLET Ambient sound recordings:
|
||||||
|
|
||||||
|
- ambient_forest_01.ogg
|
||||||
|
- ambient_meadow_01.ogg
|
||||||
|
- ambient_windy_01.ogg
|
||||||
|
- ambient_woods_01.ogg
|
||||||
|
- crickets_01.ogg
|
||||||
|
- crickets_02.ogg
|
||||||
|
|
||||||
|
Copyright (C) 2012, 2013, 2015, 2016, 2017 Klankbeeld
|
||||||
|
Sound from <http://www.freesound.org/people/klankbeeld/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$BULLET EchoThief Impulse Response Library
|
||||||
|
|
||||||
|
Copyright 2013-2023 Chris Warren <cwarren@sdsu.edu>
|
||||||
""").split('\n')
|
""").split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -131,14 +131,14 @@ object AudioMixer: Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
convolveBusOpen.filters[0] = Highpass(80f)
|
convolveBusOpen.filters[0] = Highpass(80f)
|
||||||
convolveBusOpen.filters[1] = Convolv(ModMgr.getFile("basegame", "audio/convolution/EchoThief - Cranbrook Art Museum.bin"))
|
convolveBusOpen.filters[1] = Convolv(ModMgr.getFile("basegame", "audio/convolution/EchoThief - PurgatoryChasm.bin"))
|
||||||
convolveBusOpen.filters[2] = Gain(decibelsToFullscale(18.0).toFloat())
|
convolveBusOpen.filters[2] = Gain(decibelsToFullscale(18.0).toFloat()) // don't make it too loud; it'll sound like a shit
|
||||||
convolveBusOpen.volume = 0.5
|
convolveBusOpen.volume = 0.5 // will be controlled by the other updater which surveys the world
|
||||||
|
|
||||||
convolveBusCave.filters[0] = Highpass(80f)
|
convolveBusCave.filters[0] = Highpass(80f)
|
||||||
convolveBusCave.filters[1] = Convolv(ModMgr.getFile("basegame", "audio/convolution/EchoThief - CedarCreekWinery.bin"))
|
convolveBusCave.filters[1] = Convolv(ModMgr.getFile("basegame", "audio/convolution/EchoThief - WaterplacePark-trimmed.bin"))
|
||||||
convolveBusCave.filters[2] = Gain(decibelsToFullscale(18.0).toFloat())
|
convolveBusCave.filters[2] = Gain(decibelsToFullscale(18.0).toFloat())
|
||||||
convolveBusCave.volume = 0.5
|
convolveBusCave.volume = 0.5 // will be controlled by the other updater which surveys the world
|
||||||
|
|
||||||
fadeBus.addSidechainInput(sumBus, 1.0 / 3.0)
|
fadeBus.addSidechainInput(sumBus, 1.0 / 3.0)
|
||||||
fadeBus.addSidechainInput(convolveBusOpen, 2.0 / 3.0)
|
fadeBus.addSidechainInput(convolveBusOpen, 2.0 / 3.0)
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package net.torvald.terrarum.audio
|
package net.torvald.terrarum.audio
|
||||||
|
|
||||||
import org.apache.commons.math3.exception.MathIllegalStateException
|
|
||||||
import org.apache.commons.math3.transform.DftNormalization
|
import org.apache.commons.math3.transform.DftNormalization
|
||||||
import org.apache.commons.math3.transform.TransformType
|
import org.apache.commons.math3.transform.TransformType
|
||||||
import org.apache.commons.math3.util.FastMath
|
|
||||||
|
|
||||||
data class FComplex(var re: Float = 0f, var im: Float = 0f) {
|
class ComplexArray(val res: FloatArray, val ims: FloatArray) {
|
||||||
operator fun times(other: FComplex) = FComplex(
|
val indices: IntProgression
|
||||||
this.re * other.re - this.im * other.im,
|
get() = 0 until size
|
||||||
this.re * other.im + this.im * other.re
|
val size: Int
|
||||||
)
|
get() = res.size
|
||||||
|
|
||||||
|
operator fun times(other: ComplexArray): ComplexArray {
|
||||||
|
val l = size
|
||||||
|
val re = FloatArray(l) { res[it] * other.res[it] - ims[it] * other.ims[it] }
|
||||||
|
val im = FloatArray(l) { res[it] * other.ims[it] + ims[it] * other.res[it] }
|
||||||
|
return ComplexArray(re, im)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,53 +25,16 @@ data class FComplex(var re: Float = 0f, var im: Float = 0f) {
|
|||||||
object FFT {
|
object FFT {
|
||||||
|
|
||||||
// org.apache.commons.math3.transform.FastFouriesTransformer.java:370
|
// org.apache.commons.math3.transform.FastFouriesTransformer.java:370
|
||||||
fun fft(signal: FloatArray): Array<FComplex> {
|
fun fft(signal: FloatArray): ComplexArray {
|
||||||
val dataRI = arrayOf(signal.copyOf(), FloatArray(signal.size))
|
val dataRI = ComplexArray(signal.copyOf(), FloatArray(signal.size))
|
||||||
|
|
||||||
transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.FORWARD)
|
transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.FORWARD)
|
||||||
|
return dataRI
|
||||||
val output = dataRI.toComplexArray()
|
|
||||||
|
|
||||||
return getComplex(output, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// org.apache.commons.math3.transform.FastFouriesTransformer.java:404
|
// org.apache.commons.math3.transform.FastFouriesTransformer.java:404
|
||||||
fun ifftAndGetReal(y: Array<FComplex>): FloatArray {
|
fun ifftAndGetReal(y: ComplexArray): FloatArray {
|
||||||
val dataRI = Array<FloatArray>(2) { FloatArray(y.size) }
|
transformInPlace(y, DftNormalization.STANDARD, TransformType.INVERSE)
|
||||||
for (i in y.indices) {
|
return y.res
|
||||||
dataRI[0][i] = y[i].re
|
|
||||||
dataRI[1][i] = y[i].im
|
|
||||||
}
|
|
||||||
|
|
||||||
transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.INVERSE)
|
|
||||||
|
|
||||||
return dataRI[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Array<FloatArray>.toComplexArray(): Array<FComplex> {
|
|
||||||
return Array(this[0].size) {
|
|
||||||
FComplex(this[0][it], this[1][it])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// com.github.psambit9791.jdsp.transform.FastFourier.java:190
|
|
||||||
/**
|
|
||||||
* Returns the complex value of the fast fourier transformed sequence
|
|
||||||
* @param onlyPositive Set to True if non-mirrored output is required
|
|
||||||
* @throws java.lang.ExceptionInInitializerError if called before executing transform() method
|
|
||||||
* @return Complex[] The complex FFT output
|
|
||||||
*/
|
|
||||||
@Throws(ExceptionInInitializerError::class)
|
|
||||||
fun getComplex(output: Array<FComplex>, onlyPositive: Boolean): Array<FComplex> {
|
|
||||||
val dftout: Array<FComplex> = if (onlyPositive) {
|
|
||||||
val numBins: Int = output.size / 2 + 1
|
|
||||||
Array<FComplex>(numBins) { FComplex() }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Array<FComplex>(output.size) { FComplex() }
|
|
||||||
}
|
|
||||||
System.arraycopy(output, 0, dftout, 0, dftout.size)
|
|
||||||
return dftout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// org.apache.commons.math3.transform.FastFouriesTransformer.java:214
|
// org.apache.commons.math3.transform.FastFouriesTransformer.java:214
|
||||||
@@ -86,12 +54,12 @@ object FFT {
|
|||||||
* @throws MathIllegalArgumentException if the number of data points is not
|
* @throws MathIllegalArgumentException if the number of data points is not
|
||||||
* a power of two
|
* a power of two
|
||||||
*/
|
*/
|
||||||
private fun transformInPlace(dataRI: Array<FloatArray>, normalization: DftNormalization, type: TransformType) {
|
private fun transformInPlace(dataRI: ComplexArray, normalization: DftNormalization, type: TransformType) {
|
||||||
val dataR = dataRI[0]
|
val dataR = dataRI.res
|
||||||
val dataI = dataRI[1]
|
val dataI = dataRI.ims
|
||||||
val n = dataR.size
|
val n = dataR.size
|
||||||
|
|
||||||
if (n == 1) {
|
/*if (n == 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else if (n == 2) {
|
else if (n == 2) {
|
||||||
@@ -108,7 +76,7 @@ object FFT {
|
|||||||
dataI[1] = srcI0 - srcI1
|
dataI[1] = srcI0 - srcI1
|
||||||
normalizeTransformedData(dataRI, normalization, type)
|
normalizeTransformedData(dataRI, normalization, type)
|
||||||
return
|
return
|
||||||
}
|
}*/
|
||||||
|
|
||||||
bitReversalShuffle2(dataR, dataI)
|
bitReversalShuffle2(dataR, dataI)
|
||||||
|
|
||||||
@@ -230,25 +198,26 @@ object FFT {
|
|||||||
* @param type the type of transform (forward, inverse) which resulted in the specified data
|
* @param type the type of transform (forward, inverse) which resulted in the specified data
|
||||||
*/
|
*/
|
||||||
private fun normalizeTransformedData(
|
private fun normalizeTransformedData(
|
||||||
dataRI: Array<FloatArray>,
|
dataRI: ComplexArray,
|
||||||
normalization: DftNormalization, type: TransformType
|
normalization: DftNormalization, type: TransformType
|
||||||
) {
|
) {
|
||||||
val dataR = dataRI[0]
|
val dataR = dataRI.res
|
||||||
val dataI = dataRI[1]
|
val dataI = dataRI.ims
|
||||||
val n = dataR.size
|
val n = dataR.size
|
||||||
assert(dataI.size == n)
|
// assert(dataI.size == n)
|
||||||
when (normalization) {
|
// when (normalization) {
|
||||||
DftNormalization.STANDARD -> if (type == TransformType.INVERSE) {
|
// DftNormalization.STANDARD ->
|
||||||
val scaleFactor = 1f / n.toFloat()
|
if (type == TransformType.INVERSE) {
|
||||||
var i = 0
|
val scaleFactor = 1f / n.toFloat()
|
||||||
while (i < n) {
|
var i = 0
|
||||||
dataR[i] *= scaleFactor
|
while (i < n) {
|
||||||
dataI[i] *= scaleFactor
|
dataR[i] *= scaleFactor
|
||||||
i++
|
dataI[i] *= scaleFactor
|
||||||
|
i++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DftNormalization.UNITARY -> {
|
/* DftNormalization.UNITARY -> {
|
||||||
val scaleFactor = (1.0 / FastMath.sqrt(n.toDouble())).toFloat()
|
val scaleFactor = (1.0 / FastMath.sqrt(n.toDouble())).toFloat()
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
@@ -259,7 +228,7 @@ object FFT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> throw MathIllegalStateException()
|
else -> throw MathIllegalStateException()
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -313,30 +313,29 @@ class Reverb(val delayMS: Float = 36f, var feedback: Float = 0.92f, var lowpass:
|
|||||||
class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() {
|
class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() {
|
||||||
|
|
||||||
private val fftLen: Int
|
private val fftLen: Int
|
||||||
private val convFFT: Array<Array<FComplex>>
|
private val convFFT: Array<ComplexArray>
|
||||||
private val convFFTpartd: Array<Array<Array<FComplex>>> // index: Channel, partition, frequencies
|
// private val convFFTpartd: Array<Array<ComplexArray>> // index: Channel, partition, frequencies
|
||||||
private val inbuf: Array<FloatArray>
|
private val inbuf: Array<FloatArray>
|
||||||
|
|
||||||
private val BLOCKSIZE = BUFFER_SIZE / 4
|
private val BLOCKSIZE = BUFFER_SIZE / 4
|
||||||
|
|
||||||
var processingSpeed = 1f; private set
|
var processingSpeed = 1f; private set
|
||||||
|
|
||||||
private val partSizes: IntArray
|
// private val partSizes: IntArray
|
||||||
private val partOffsets: IntArray
|
// private val partOffsets: IntArray
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!ir.exists()) {
|
if (!ir.exists()) {
|
||||||
throw IllegalArgumentException("Impulse Response file '${ir.path}' does not exist.")
|
throw IllegalArgumentException("Impulse Response file '${ir.path}' does not exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val sampleCount = ir.length().toInt() / 8
|
val sampleCount = (ir.length().toInt() / 8)//.coerceAtMost(65536)
|
||||||
fftLen = FastMath.nextPowerOfTwo(sampleCount)
|
fftLen = FastMath.nextPowerOfTwo(sampleCount)
|
||||||
|
|
||||||
// println("IR Sample Count = $sampleCount; FFT Length = $fftLen")
|
println("IR '${ir.path}' Sample Count = $sampleCount; FFT Length = $fftLen")
|
||||||
|
|
||||||
val conv = Array(2) { FloatArray(fftLen) }
|
val conv = Array(2) { FloatArray(fftLen) }
|
||||||
inbuf = Array(2) { FloatArray(fftLen) }
|
inbuf = Array(2) { FloatArray(fftLen) }
|
||||||
// outbuf = Array(2) { DoubleArray(fftLen) }
|
|
||||||
|
|
||||||
ir.inputStream().let {
|
ir.inputStream().let {
|
||||||
for (i in 0 until sampleCount) {
|
for (i in 0 until sampleCount) {
|
||||||
@@ -362,31 +361,10 @@ class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() {
|
|||||||
|
|
||||||
// println("convFFT Length = ${convFFT[0].size}")
|
// println("convFFT Length = ${convFFT[0].size}")
|
||||||
|
|
||||||
if (fftLen < BUFFER_SIZE) // buffer size is always 4x the samples in the buffer
|
|
||||||
throw Error("FIR size is too small (minimum: $BUFFER_SIZE samples)")
|
|
||||||
|
|
||||||
|
|
||||||
val partitions = ArrayList<Int>()
|
|
||||||
var cnt = fftLen
|
|
||||||
while (cnt > BUFFER_SIZE / 4) {
|
|
||||||
cnt /= 2
|
|
||||||
partitions.add(cnt)
|
|
||||||
}
|
|
||||||
partitions.add(cnt)
|
|
||||||
|
|
||||||
partSizes = partitions.reversed().toIntArray()
|
|
||||||
partOffsets = partSizes.clone().also { it[0] = 0 }
|
|
||||||
|
|
||||||
// allocate arrays
|
|
||||||
convFFTpartd = Array(2) { ch ->
|
|
||||||
Array(partSizes.size) { partNo ->
|
|
||||||
Array(partSizes[partNo]) {
|
|
||||||
convFFT[ch][partOffsets[partNo] + it]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://thewolfsound.com/fast-convolution-fft-based-overlap-add-overlap-save-partitioned/
|
* https://thewolfsound.com/fast-convolution-fft-based-overlap-add-overlap-save-partitioned/
|
||||||
*/
|
*/
|
||||||
@@ -398,12 +376,19 @@ class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() {
|
|||||||
push(inbuf[ch].applyGain(gain), this.inbuf[ch])
|
push(inbuf[ch].applyGain(gain), this.inbuf[ch])
|
||||||
|
|
||||||
val inputFFT = FFT.fft(this.inbuf[ch])
|
val inputFFT = FFT.fft(this.inbuf[ch])
|
||||||
|
val Y = inputFFT * convFFT[ch]
|
||||||
val Y = multiply(inputFFT, convFFT[ch])
|
|
||||||
val y = FFT.ifftAndGetReal(Y)
|
val y = FFT.ifftAndGetReal(Y)
|
||||||
|
|
||||||
val u = y.takeLast(BLOCKSIZE).toFloatArray()
|
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)
|
System.arraycopy(u, 0, outbuf[ch], 0, BLOCKSIZE)
|
||||||
}
|
}
|
||||||
val t2 = System.nanoTime()
|
val t2 = System.nanoTime()
|
||||||
@@ -412,10 +397,6 @@ class Convolv(ir: File, val gain: Float = 1f / 256f): TerrarumAudioFilter() {
|
|||||||
processingSpeed = realtime / ptime
|
processingSpeed = realtime / ptime
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun multiply(X: Array<FComplex>, H: Array<FComplex>): Array<FComplex> {
|
|
||||||
return Array(X.size) { X[it] * H[it] }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object XYtoMS: TerrarumAudioFilter() {
|
object XYtoMS: TerrarumAudioFilter() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class TerrarumAudioMixerTrack(val name: String, val isMaster: Boolean = false, v
|
|||||||
const val SAMPLING_RATE = 48000
|
const val SAMPLING_RATE = 48000
|
||||||
const val SAMPLING_RATEF = 48000f
|
const val SAMPLING_RATEF = 48000f
|
||||||
const val SAMPLING_RATED = 48000.0
|
const val SAMPLING_RATED = 48000.0
|
||||||
const val BUFFER_SIZE = 65536 // n ms -> 384 * n
|
const val BUFFER_SIZE = 512*4 // n ms -> 384 * n
|
||||||
}
|
}
|
||||||
|
|
||||||
val hash = getHashStr()
|
val hash = getHashStr()
|
||||||
|
|||||||
Reference in New Issue
Block a user