Create FFT (backed by radix-2 algorithm) function in smnp.dsp
This commit is contained in:
@@ -55,6 +55,10 @@ extend list {
|
||||
return output;
|
||||
}
|
||||
|
||||
function sublist(beginIndex: int, endIndex: int) {
|
||||
return this as (item, index) ^ item % index >= beginIndex and index < endIndex;
|
||||
}
|
||||
|
||||
function isEmpty() {
|
||||
return this.size == 0;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package io.smnp.ext.dsp
|
||||
|
||||
import io.smnp.ext.dsp.function.FftFunction
|
||||
import io.smnp.ext.dsp.function.PlotFunction
|
||||
import io.smnp.ext.provider.NativeModuleProvider
|
||||
import org.pf4j.Extension
|
||||
|
||||
@Extension
|
||||
class DspModule : NativeModuleProvider("smnp.dsp") {
|
||||
override fun functions() = listOf(PlotFunction())
|
||||
override fun functions() = listOf(PlotFunction(), FftFunction())
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.smnp.ext.dsp.function
|
||||
|
||||
import io.smnp.callable.function.Function
|
||||
import io.smnp.callable.function.FunctionDefinitionTool
|
||||
import io.smnp.callable.signature.Signature.Companion.simple
|
||||
import io.smnp.ext.dsp.lib.fft.FourierTransform.fft
|
||||
import io.smnp.type.enumeration.DataType.FLOAT
|
||||
import io.smnp.type.enumeration.DataType.INT
|
||||
import io.smnp.type.matcher.Matcher.Companion.listOf
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
class FftFunction : Function("fft") {
|
||||
override fun define(new: FunctionDefinitionTool) {
|
||||
new function simple(listOf(INT, FLOAT)) body { _, (signal) ->
|
||||
val x = (signal.unwrap() as List<Number>).map { it.toDouble() }
|
||||
Value.list(fft(x).map { Value.float(it.mod.toFloat()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.smnp.ext.dsp.lib.complex
|
||||
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.pow
|
||||
|
||||
data class Complex(val re: Double, val im: Double) {
|
||||
|
||||
constructor(a: Double) : this(kotlin.math.cos(a), kotlin.math.sin(a))
|
||||
|
||||
val mod: Double
|
||||
get() = squaresSum.pow(0.5)
|
||||
|
||||
private val squaresSum: Double
|
||||
get() = re.pow(2.0) + im.pow(2.0)
|
||||
|
||||
val arg: Double
|
||||
get() = atan2(im, re)
|
||||
|
||||
override fun toString() = "$re${if(im > 0) "+$im" else "$im"}"
|
||||
|
||||
operator fun plus(z: Complex) = Complex(re + z.re, im + z.im)
|
||||
|
||||
operator fun minus(z: Complex) = Complex(re - z.re, im - z.im)
|
||||
|
||||
operator fun times(z: Complex) = Complex(re * z.re - im * z.im, re * z.im + im * z.re)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.smnp.ext.dsp.lib.fft
|
||||
|
||||
import io.smnp.ext.dsp.lib.complex.Complex
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.log
|
||||
import kotlin.math.pow
|
||||
|
||||
object FourierTransform {
|
||||
|
||||
fun fft(signal: List<Double>): List<Complex> {
|
||||
return radix2(padSignal(signal).map { Complex(it, 0.0) })
|
||||
}
|
||||
|
||||
private fun padSignal(signal: List<Double>): List<Double> {
|
||||
val totalSignalSize = 2.0.pow(nextPow2(signal.size.toDouble()))
|
||||
val paddingSize = (totalSignalSize - signal.size).toInt()
|
||||
return signal + List(paddingSize) { 0.0 }
|
||||
}
|
||||
|
||||
private fun nextPow2(x: Double) = ceil(log(abs(x), 2.0))
|
||||
|
||||
private fun radix2(signal: List<Complex>): List<Complex> {
|
||||
val N = signal.size
|
||||
|
||||
if(N == 1) return listOf(signal[0])
|
||||
|
||||
if(N % 2 != 0) throw IllegalArgumentException("The number of samples should be power of 2")
|
||||
|
||||
val even = radix2(signal.filterIndexed { index, _ -> index % 2 == 0 })
|
||||
val odd = radix2(signal.filterIndexed { index, _ -> index % 2 != 0 })
|
||||
|
||||
val output = MutableList(N) { Complex(0.0, 0.0) }
|
||||
even.zip(odd).forEachIndexed { k, (e, o) ->
|
||||
val z = Complex(-2 * k * Math.PI / N)
|
||||
output[k] = e + z * o
|
||||
output[k+N/2] = e - z * o
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user