all-float FFT and convolution

This commit is contained in:
minjaesong
2023-11-25 21:46:49 +09:00
parent 51d1501267
commit 46f93660d0
6 changed files with 437 additions and 660 deletions

View File

@@ -0,0 +1,304 @@
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.TransformType
import org.apache.commons.math3.util.FastMath
data class FComplex(var real: Float = 0f, var imaginary: Float = 0f) {
operator fun times(other: FComplex) = FComplex(
this.real * other.real - this.imaginary * other.imaginary,
this.real * other.imaginary + this.imaginary * other.real
)
}
/**
* Modification of the code form JDSP and Apache Commons Math
*
* Created by minjaesong on 2023-11-25.
*/
object FFT {
// org.apache.commons.math3.transform.FastFouriesTransformer.java:370
fun fft(signal: FloatArray): Array<FComplex> {
val dataRI = arrayOf(signal.copyOf(), FloatArray(signal.size))
transformInPlace(dataRI, DftNormalization.STANDARD, TransformType.FORWARD)
val output = dataRI.toComplexArray()
return getComplex(output, false)
}
// org.apache.commons.math3.transform.FastFouriesTransformer.java:404
fun ifftAndGetReal(y: Array<FComplex>): FloatArray {
val dataRI = Array<FloatArray>(2) { FloatArray(y.size) }
for (i in y.indices) {
dataRI[0][i] = y[i].real
dataRI[1][i] = y[i].imaginary
}
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
/**
* Computes the standard transform of the specified complex data. The
* computation is done in place. The input data is laid out as follows
* <ul>
* <li>{@code dataRI[0][i]} is the real part of the {@code i}-th data point,</li>
* <li>{@code dataRI[1][i]} is the imaginary part of the {@code i}-th data point.</li>
* </ul>
*
* @param dataRI the two dimensional array of real and imaginary parts of the data
* @param normalization the normalization to be applied to the transformed data
* @param type the type of transform (forward, inverse) to be performed
* @throws DimensionMismatchException if the number of rows of the specified
* array is not two, or the array is not rectangular
* @throws MathIllegalArgumentException if the number of data points is not
* a power of two
*/
private fun transformInPlace(dataRI: Array<FloatArray>, normalization: DftNormalization, type: TransformType) {
val dataR = dataRI[0]
val dataI = dataRI[1]
val n = dataR.size
if (n == 1) {
return
}
else if (n == 2) {
val srcR0 = dataR[0]
val srcI0 = dataI[0]
val srcR1 = dataR[1]
val srcI1 = dataI[1]
// X_0 = x_0 + x_1
dataR[0] = srcR0 + srcR1
dataI[0] = srcI0 + srcI1
// X_1 = x_0 - x_1
dataR[1] = srcR0 - srcR1
dataI[1] = srcI0 - srcI1
normalizeTransformedData(dataRI, normalization, type)
return
}
bitReversalShuffle2(dataR, dataI)
// Do 4-term DFT.
// Do 4-term DFT.
if (type == TransformType.INVERSE) {
var i0 = 0
while (i0 < n) {
val i1 = i0 + 1
val i2 = i0 + 2
val i3 = i0 + 3
val srcR0 = dataR[i0]
val srcI0 = dataI[i0]
val srcR1 = dataR[i2]
val srcI1 = dataI[i2]
val srcR2 = dataR[i1]
val srcI2 = dataI[i1]
val srcR3 = dataR[i3]
val srcI3 = dataI[i3]
// 4-term DFT
// X_0 = x_0 + x_1 + x_2 + x_3
dataR[i0] = srcR0 + srcR1 + srcR2 + srcR3
dataI[i0] = srcI0 + srcI1 + srcI2 + srcI3
// X_1 = x_0 - x_2 + j * (x_3 - x_1)
dataR[i1] = srcR0 - srcR2 + (srcI3 - srcI1)
dataI[i1] = srcI0 - srcI2 + (srcR1 - srcR3)
// X_2 = x_0 - x_1 + x_2 - x_3
dataR[i2] = srcR0 - srcR1 + srcR2 - srcR3
dataI[i2] = srcI0 - srcI1 + srcI2 - srcI3
// X_3 = x_0 - x_2 + j * (x_1 - x_3)
dataR[i3] = srcR0 - srcR2 + (srcI1 - srcI3)
dataI[i3] = srcI0 - srcI2 + (srcR3 - srcR1)
i0 += 4
}
}
else {
var i0 = 0
while (i0 < n) {
val i1 = i0 + 1
val i2 = i0 + 2
val i3 = i0 + 3
val srcR0 = dataR[i0]
val srcI0 = dataI[i0]
val srcR1 = dataR[i2]
val srcI1 = dataI[i2]
val srcR2 = dataR[i1]
val srcI2 = dataI[i1]
val srcR3 = dataR[i3]
val srcI3 = dataI[i3]
// 4-term DFT
// X_0 = x_0 + x_1 + x_2 + x_3
dataR[i0] = srcR0 + srcR1 + srcR2 + srcR3
dataI[i0] = srcI0 + srcI1 + srcI2 + srcI3
// X_1 = x_0 - x_2 + j * (x_3 - x_1)
dataR[i1] = srcR0 - srcR2 + (srcI1 - srcI3)
dataI[i1] = srcI0 - srcI2 + (srcR3 - srcR1)
// X_2 = x_0 - x_1 + x_2 - x_3
dataR[i2] = srcR0 - srcR1 + srcR2 - srcR3
dataI[i2] = srcI0 - srcI1 + srcI2 - srcI3
// X_3 = x_0 - x_2 + j * (x_1 - x_3)
dataR[i3] = srcR0 - srcR2 + (srcI3 - srcI1)
dataI[i3] = srcI0 - srcI2 + (srcR1 - srcR3)
i0 += 4
}
}
var lastN0 = 4
var lastLogN0 = 2
while (lastN0 < n) {
val n0 = lastN0 shl 1
val logN0 = lastLogN0 + 1
val wSubN0R = W_SUB_N_R[logN0]
var wSubN0I = W_SUB_N_I[logN0]
if (type == TransformType.INVERSE) {
wSubN0I = -wSubN0I
}
// Combine even/odd transforms of size lastN0 into a transform of size N0 (lastN0 * 2).
var destEvenStartIndex = 0
while (destEvenStartIndex < n) {
val destOddStartIndex = destEvenStartIndex + lastN0
var wSubN0ToRR = 1f
var wSubN0ToRI = 0f
for (r in 0 until lastN0) {
val grR = dataR[destEvenStartIndex + r]
val grI = dataI[destEvenStartIndex + r]
val hrR = dataR[destOddStartIndex + r]
val hrI = dataI[destOddStartIndex + r]
// dest[destEvenStartIndex + r] = Gr + WsubN0ToR * Hr
dataR[destEvenStartIndex + r] = grR + wSubN0ToRR * hrR - wSubN0ToRI * hrI
dataI[destEvenStartIndex + r] = grI + wSubN0ToRR * hrI + wSubN0ToRI * hrR
// dest[destOddStartIndex + r] = Gr - WsubN0ToR * Hr
dataR[destOddStartIndex + r] = grR - (wSubN0ToRR * hrR - wSubN0ToRI * hrI)
dataI[destOddStartIndex + r] = grI - (wSubN0ToRR * hrI + wSubN0ToRI * hrR)
// WsubN0ToR *= WsubN0R
val nextWsubN0ToRR = wSubN0ToRR * wSubN0R - wSubN0ToRI * wSubN0I
val nextWsubN0ToRI = wSubN0ToRR * wSubN0I + wSubN0ToRI * wSubN0R
wSubN0ToRR = nextWsubN0ToRR
wSubN0ToRI = nextWsubN0ToRI
}
destEvenStartIndex += n0
}
lastN0 = n0
lastLogN0 = logN0
}
normalizeTransformedData(dataRI, normalization, type)
}
/**
* Applies the proper normalization to the specified transformed data.
*
* @param dataRI the unscaled transformed data
* @param normalization the normalization to be applied
* @param type the type of transform (forward, inverse) which resulted in the specified data
*/
private fun normalizeTransformedData(
dataRI: Array<FloatArray>,
normalization: DftNormalization, type: TransformType
) {
val dataR = dataRI[0]
val dataI = dataRI[1]
val n = dataR.size
assert(dataI.size == n)
when (normalization) {
DftNormalization.STANDARD -> if (type == TransformType.INVERSE) {
val scaleFactor = 1f / n.toFloat()
var i = 0
while (i < n) {
dataR[i] *= scaleFactor
dataI[i] *= scaleFactor
i++
}
}
DftNormalization.UNITARY -> {
val scaleFactor = (1.0 / FastMath.sqrt(n.toDouble())).toFloat()
var i = 0
while (i < n) {
dataR[i] *= scaleFactor
dataI[i] *= scaleFactor
i++
}
}
else -> throw MathIllegalStateException()
}
}
/**
* Performs identical index bit reversal shuffles on two arrays of identical
* size. Each element in the array is swapped with another element based on
* the bit-reversal of the index. For example, in an array with length 16,
* item at binary index 0011 (decimal 3) would be swapped with the item at
* binary index 1100 (decimal 12).
*
* @param a the first array to be shuffled
* @param b the second array to be shuffled
*/
private fun bitReversalShuffle2(a: FloatArray, b: FloatArray) {
val n = a.size
assert(b.size == n)
val halfOfN = n shr 1
var j = 0
for (i in 0 until n) {
if (i < j) {
// swap indices i & j
var temp = a[i]
a[i] = a[j]
a[j] = temp
temp = b[i]
b[i] = b[j]
b[j] = temp
}
var k = halfOfN
while (k in 1..j) {
j -= k
k = k shr 1
}
j += k
}
}
private val W_SUB_N_R = FFTConsts.W_SUB_N_R.map { it.toFloat() }.toFloatArray()
private val W_SUB_N_I = FFTConsts.W_SUB_N_I.map { it.toFloat() }.toFloatArray()
}

View File

@@ -0,0 +1,55 @@
package net.torvald.terrarum.audio;
/**
* Created by minjaesong on 2023-11-25.
*/
public class FFTConsts {
// org.apache.commons.math3.transform.FastFouriesTransformer.java:58
/**
* {@code W_SUB_N_R[i]} is the real part of
* {@code exp(- 2 * i * pi / n)}:
* {@code W_SUB_N_R[i] = cos(2 * pi/ n)}, where {@code n = 2^i}.
*/
public static final double[] W_SUB_N_R =
{ 0x1.0p0, -0x1.0p0, 0x1.1a62633145c07p-54, 0x1.6a09e667f3bcdp-1
, 0x1.d906bcf328d46p-1, 0x1.f6297cff75cbp-1, 0x1.fd88da3d12526p-1, 0x1.ff621e3796d7ep-1
, 0x1.ffd886084cd0dp-1, 0x1.fff62169b92dbp-1, 0x1.fffd8858e8a92p-1, 0x1.ffff621621d02p-1
, 0x1.ffffd88586ee6p-1, 0x1.fffff62161a34p-1, 0x1.fffffd8858675p-1, 0x1.ffffff621619cp-1
, 0x1.ffffffd885867p-1, 0x1.fffffff62161ap-1, 0x1.fffffffd88586p-1, 0x1.ffffffff62162p-1
, 0x1.ffffffffd8858p-1, 0x1.fffffffff6216p-1, 0x1.fffffffffd886p-1, 0x1.ffffffffff621p-1
, 0x1.ffffffffffd88p-1, 0x1.fffffffffff62p-1, 0x1.fffffffffffd9p-1, 0x1.ffffffffffff6p-1
, 0x1.ffffffffffffep-1, 0x1.fffffffffffffp-1, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0, 0x1.0p0
, 0x1.0p0, 0x1.0p0, 0x1.0p0 };
/**
* {@code W_SUB_N_I[i]} is the imaginary part of
* {@code exp(- 2 * i * pi / n)}:
* {@code W_SUB_N_I[i] = -sin(2 * pi/ n)}, where {@code n = 2^i}.
*/
public static final double[] W_SUB_N_I =
{ 0x1.1a62633145c07p-52, -0x1.1a62633145c07p-53, -0x1.0p0, -0x1.6a09e667f3bccp-1
, -0x1.87de2a6aea963p-2, -0x1.8f8b83c69a60ap-3, -0x1.917a6bc29b42cp-4, -0x1.91f65f10dd814p-5
, -0x1.92155f7a3667ep-6, -0x1.921d1fcdec784p-7, -0x1.921f0fe670071p-8, -0x1.921f8becca4bap-9
, -0x1.921faaee6472dp-10, -0x1.921fb2aecb36p-11, -0x1.921fb49ee4ea6p-12, -0x1.921fb51aeb57bp-13
, -0x1.921fb539ecf31p-14, -0x1.921fb541ad59ep-15, -0x1.921fb5439d73ap-16, -0x1.921fb544197ap-17
, -0x1.921fb544387bap-18, -0x1.921fb544403c1p-19, -0x1.921fb544422c2p-20, -0x1.921fb54442a83p-21
, -0x1.921fb54442c73p-22, -0x1.921fb54442cefp-23, -0x1.921fb54442d0ep-24, -0x1.921fb54442d15p-25
, -0x1.921fb54442d17p-26, -0x1.921fb54442d18p-27, -0x1.921fb54442d18p-28, -0x1.921fb54442d18p-29
, -0x1.921fb54442d18p-30, -0x1.921fb54442d18p-31, -0x1.921fb54442d18p-32, -0x1.921fb54442d18p-33
, -0x1.921fb54442d18p-34, -0x1.921fb54442d18p-35, -0x1.921fb54442d18p-36, -0x1.921fb54442d18p-37
, -0x1.921fb54442d18p-38, -0x1.921fb54442d18p-39, -0x1.921fb54442d18p-40, -0x1.921fb54442d18p-41
, -0x1.921fb54442d18p-42, -0x1.921fb54442d18p-43, -0x1.921fb54442d18p-44, -0x1.921fb54442d18p-45
, -0x1.921fb54442d18p-46, -0x1.921fb54442d18p-47, -0x1.921fb54442d18p-48, -0x1.921fb54442d18p-49
, -0x1.921fb54442d18p-50, -0x1.921fb54442d18p-51, -0x1.921fb54442d18p-52, -0x1.921fb54442d18p-53
, -0x1.921fb54442d18p-54, -0x1.921fb54442d18p-55, -0x1.921fb54442d18p-56, -0x1.921fb54442d18p-57
, -0x1.921fb54442d18p-58, -0x1.921fb54442d18p-59, -0x1.921fb54442d18p-60 };
}

View File

@@ -1,7 +1,5 @@
package net.torvald.terrarum.audio
import com.github.psambit9791.jdsp.transform.FastFourier
import com.github.psambit9791.jdsp.transform.InverseFastFourier
import com.jme3.math.FastMath
import com.jme3.math.FastMath.sin
import net.torvald.terrarum.audio.AudioMixer.SPEED_OF_SOUND
@@ -9,7 +7,6 @@ import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.BUFFER_SIZE
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATEF
import net.torvald.terrarum.roundToFloat
import org.apache.commons.math3.complex.Complex
import java.io.File
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -298,12 +295,11 @@ class Reverb(val delayMS: Float = 36f, var feedback: Float = 0.92f, var lowpass:
}
}
class Convolv(ir: File, val gain: Float = decibelsToFullscale(-12.0).toFloat()): TerrarumAudioFilter() {
class Convolv(ir: File, val gain: Float = decibelsToFullscale(-24.0).toFloat()): TerrarumAudioFilter() {
private val fftLen: Int
private val convFFT: Array<Array<Complex>>
private val inbuf: Array<DoubleArray>
// private val outbuf: Array<DoubleArray>
private val convFFT: Array<Array<FComplex>>
private val inbuf: Array<FloatArray>
private val BLOCKSIZE = BUFFER_SIZE / 4
@@ -317,8 +313,8 @@ class Convolv(ir: File, val gain: Float = decibelsToFullscale(-12.0).toFloat()):
println("IR Sample Count = $sampleCount; FFT Length = $fftLen")
val conv = Array(2) { DoubleArray(fftLen) }
inbuf = Array(2) { DoubleArray(fftLen) }
val conv = Array(2) { FloatArray(fftLen) }
inbuf = Array(2) { FloatArray(fftLen) }
// outbuf = Array(2) { DoubleArray(fftLen) }
ir.inputStream().let {
@@ -331,8 +327,8 @@ class Convolv(ir: File, val gain: Float = decibelsToFullscale(-12.0).toFloat()):
it.read().and(255).shl(8) or
it.read().and(255).shl(16) or
it.read().and(255).shl(24))
conv[0][i] = f1.toDouble()
conv[1][i] = f2.toDouble()
conv[0][i] = f1
conv[1][i] = f2
}
it.close()
@@ -340,7 +336,7 @@ class Convolv(ir: File, val gain: Float = decibelsToFullscale(-12.0).toFloat()):
// fourier-transform the 'conv'
convFFT = Array(2) {
FastFourier(conv[it]).let { it.transform(); it.getComplex(false) }
FFT.fft(conv[it])
}
println("convFFT Length = ${convFFT[0].size}")
@@ -354,72 +350,33 @@ class Convolv(ir: File, val gain: Float = decibelsToFullscale(-12.0).toFloat()):
val t1 = System.nanoTime()
for (ch in outbuf1.indices) {
push(inbuf1[ch].applyGain(gain), inbuf[ch])
push(inbuf1[ch].toDoubleArray(gain), inbuf[ch])
val inputFFT = FastFourier(inbuf[ch]).let { it.transform(); it.getComplex(false) }
val Ny = inputFFT.size// + convFFT[ch].size - 1
// println("inputFFT.size=${inputFFT.size} convFFT[ch].size=${convFFT[ch].size} Ny=$Ny")
val inputFFT = FFT.fft(inbuf[ch])
val Y = multiply(inputFFT, convFFT[ch])
val y = real(ifft(Y))
val y = FFT.ifftAndGetReal(Y)
// val u = y.sliceArray(Ny - BLOCKSIZE until Ny).toFloatArray(gain) // y size == Ny
val u = y.takeLast(BLOCKSIZE).map { it.toFloat() }.toFloatArray()
// println("y size: ${y.size}; Ny=$Ny")
val u = y.takeLast(BLOCKSIZE).toFloatArray()
System.arraycopy(u, 0, outbuf1[ch], 0, BLOCKSIZE)
}
val t2 = System.nanoTime()
val ptime = (t2 - t1).toDouble()
val realtime = BLOCKSIZE / SAMPLING_RATED * 1000000000L
if (realtime >= ptime) {
// println("Processing speed: ${realtime / ptime}x FASTER than realtime")
}
else {
// println("Processing speed: ${ptime / realtime}x SLOWER than realtime")
}
println("Processing speed: ${realtime / ptime}x")
}
private fun real(cs: Array<Complex>): DoubleArray {
return cs.map { it.real }.toDoubleArray()
private fun multiply(X: Array<FComplex>, H: Array<FComplex>): Array<FComplex> {
return Array(X.size) { X[it] * H[it] }
}
private fun ifft(y: Array<Complex>): Array<Complex> {
return InverseFastFourier(y, false).let { it.transform(); it.complex }
}
private fun multiply(X: Array<Complex>, H: Array<Complex>): Array<Complex> {
if (X.size != H.size) throw IllegalArgumentException()
return Array(X.size) {
//X[it].multiply(H[it])
// following is a snippet of the code from org.apache.commons.math3.complex.multiply,
// to remove the non-necessary sanity checks
val a = X[it]
val b = H[it]
val re = a.real * b.real - a.imaginary * b.imaginary
val im = a.real * b.imaginary + a.imaginary * b.real
Complex(re, im)
}
}
private fun push(sample: Double, buf: DoubleArray) {
System.arraycopy(buf, 1, buf, 0, buf.size - 1)
buf[buf.lastIndex] = sample
}
private fun push(samples: DoubleArray, buf: DoubleArray) {
private 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)
}
private fun FloatArray.toDoubleArray(gain: Float = 1f) = this.map { it.toDouble() * gain }.toDoubleArray()
private fun DoubleArray.toFloatArray(gain: Float = 1f) = this.map { it.toFloat() * gain }.toFloatArray()
private fun FloatArray.applyGain(gain: Float = 1f) = this.map { it * gain }.toFloatArray()
}
object XYtoMS: TerrarumAudioFilter() {

View File

@@ -21,9 +21,6 @@ class TerrarumAudioMixerTrack(val name: String, val isMaster: Boolean = false, v
const val SAMPLING_RATEF = 48000f
const val SAMPLING_RATED = 48000.0
const val BUFFER_SIZE = 512 // n ms -> 384 * n
const val INDEX_BGM = 0
const val INDEX_AMB = 1
}
val hash = getHashStr()