Skip to content

OscillatorBankArrays.kt

src/jvmMain/kotlin/afield/audio/engine/OscillatorBankArrays.kt · 183 lines

@file:Suppress("MemberVisibilityCanBePrivate", "unused")

package afield.audio.engine

Banks built as arrays of the scalar models: ResonatorBankArray holds one Resonator per frequency and TrackingResonatorBankArray one TrackingResonator, each updated sample by sample.

import java.util.concurrent.ForkJoinPool

class ResonatorBankArray

class ResonatorBankArray(
    frequencies: FloatArray,
    alphas: FloatArray,
    betas: FloatArray = alphas,
    private val sampleRate: Float
) {

val resonators: Array<Resonator>

    val resonators: Array<Resonator> = Array(frequencies.size) { i ->
        Resonator(frequencies[i], alphas[i], betas[i], sampleRate)
    }

val size: Int

    val size: Int
        get() = resonators.size

init

    init {
        require(frequencies.size == alphas.size) { "frequencies and alphas must have same size" }
        require(alphas.size == betas.size) { "alphas and betas must have same size" }
        require(sampleRate > 0.0f) { "sampleRate must be > 0" }
    }

fun update(sample: Float)

    fun update(sample: Float) {
        for (resonator in resonators) {
            resonator.update(sample)
        }
    }

fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1)

    fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1) {
        for (resonator in resonators) {
            resonator.update(samples, offset, length, stride)
        }
    }

fun updateConcurrent

    fun updateConcurrent(
        samples: FloatArray,
        offset: Int = 0,
        length: Int = samples.size - offset,
        stride: Int = 1,
        taskCount: Int = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
    ) {
        updateConcurrentResonators(resonators, taskCount) { resonator ->
            resonator.update(samples, offset, length, stride)
        }
    }

fun getComplex(dest: FloatArray, offset: Int = 0)

    fun getComplex(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size * 2) { "Buffer passed to getComplex() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].real
            dest[offset + size + i] = resonators[i].imag
        }
    }

fun getPowers(dest: FloatArray, offset: Int = 0)

    fun getPowers(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size) { "Buffer passed to getPowers() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].power
        }
    }

fun getAmplitudes(dest: FloatArray, offset: Int = 0)

    fun getAmplitudes(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size) { "Buffer passed to getAmplitudes() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].amplitude
        }
    }
}

class TrackingResonatorBankArray

class TrackingResonatorBankArray(
    naturalFrequencies: FloatArray,
    alphas: FloatArray,
    betas: FloatArray = alphas,
    private val sampleRate: Float,
    gammas: FloatArray = trackingResonatorGammaHeuristics(naturalFrequencies, sampleRate)
) {

val resonators: Array<TrackingResonator>

    val resonators: Array<TrackingResonator> = Array(naturalFrequencies.size) { i ->
        TrackingResonator(naturalFrequencies[i], alphas[i], betas[i], sampleRate, gammas[i])
    }

val size: Int

    val size: Int
        get() = resonators.size

init

    init {
        require(naturalFrequencies.size == alphas.size) { "frequencies and alphas must have same size" }
        require(alphas.size == betas.size) { "alphas and betas must have same size" }
        require(betas.size == gammas.size) { "betas and gammas must have same size" }
        require(sampleRate > 0.0f) { "sampleRate must be > 0" }
    }

fun update(sample: Float)

    fun update(sample: Float) {
        for (resonator in resonators) {
            resonator.update(sample)
        }
    }

fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1)

    fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1) {
        for (resonator in resonators) {
            resonator.update(samples, offset, length, stride)
        }
    }

fun updateConcurrent

    fun updateConcurrent(
        samples: FloatArray,
        offset: Int = 0,
        length: Int = samples.size - offset,
        stride: Int = 1,
        taskCount: Int = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
    ) {
        updateConcurrentResonators(resonators, taskCount) { resonator ->
            resonator.update(samples, offset, length, stride)
        }
    }

fun setPowerTimeConstant(tau: Float = 1.0f)

    fun setPowerTimeConstant(tau: Float = 1.0f) {
        for (resonator in resonators) {
            resonator.setPowerTimeConstant(tau)
        }
    }

fun getComplex(dest: FloatArray, offset: Int = 0)

    fun getComplex(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size * 2) { "Buffer passed to getComplex() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].real
            dest[offset + size + i] = resonators[i].imag
        }
    }

fun getPowers(dest: FloatArray, offset: Int = 0)

    fun getPowers(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size) { "Buffer passed to getPowers() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].power
        }
    }

fun getAmplitudes(dest: FloatArray, offset: Int = 0)

    fun getAmplitudes(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size) { "Buffer passed to getAmplitudes() is not large enough" }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].amplitude
        }
    }

fun getResonantFrequencies(dest: FloatArray, offset: Int = 0)

    fun getResonantFrequencies(dest: FloatArray, offset: Int = 0) {
        require(dest.size - offset >= size) {
            "Buffer passed to getResonantFrequencies() is not large enough"
        }
        for (i in resonators.indices) {
            dest[offset + i] = resonators[i].resonantFrequency
        }
    }
}

private inline fun <T> updateConcurrentResonators(
    resonators: Array<T>,
    taskCount: Int,
    crossinline update: (T) -> Unit
) {
    require(taskCount > 0) { "taskCount must be > 0" }
    if (resonators.isEmpty()) return

    val chunkSize = ((resonators.size + taskCount - 1) / taskCount).coerceAtLeast(1)
    val tasks = ArrayList<java.util.concurrent.ForkJoinTask<*>>()
    var start = 0
    while (start < resonators.size) {
        val chunkStart = start
        val chunkEnd = (chunkStart + chunkSize).coerceAtMost(resonators.size)
        tasks += ForkJoinPool.commonPool().submit {
            for (i in chunkStart until chunkEnd) {
                update(resonators[i])
            }
        }
        start = chunkEnd
    }
    tasks.forEach { it.join() }
}