Skip to content

Buf.kt

src/jvmMain/kotlin/afield/audio/synth/Buf.kt · 291 lines

@file:afield.runtime.completion.CompletionCategory("Synthesis")
@file:Suppress("DEPRECATION")

Bulk float-array arithmetic for the synthesis graph, written against the Java Vector API.

A Signal renders into a FloatArray a block at a time, 512 frames by default, so the unit of work in this package is a few hundred samples rather than one. Buf is where that inner loop lives: fills and copies, buffer-by-buffer and buffer-by-scalar arithmetic, fused multiply-add, clamp and absolute value, the peak, RMS and sum reductions, a Hann window and a cross-fade. Signal, Sampling, Filters, FM, Envelopes, Spectral, FaustDsp and FieldAudioEngine's mix loop all go through it.

Each function takes its destination first and an off/len pair last, defaulting to the whole array from off. Every caller in the package passes 0 and the current block's frame count, because the buffers grow to the largest block asked for so far and never shrink: len is the live prefix, not the array's size. The vectorised functions step by LANES for as long as a full vector remains and finish the rest in a scalar tail loop, so no length is a special case. Nothing here allocates an array. Destinations are written in place, and the three reductions return a Float.

Three of the functions are not vectorised: copy is System.arraycopy, and hann and hannWindow evaluate a cosine per sample in a plain loop.

Two of them have a guard or a convention that the signature does not show. div returns 0 where the divisor's magnitude is at or below epsilon, 1e-15 by default, rather than an infinity or a NaN. rms divides the sum of squares by len, the window it was asked for, rather than by the length of the array.

package afield.audio.synth

import jdk.incubator.vector.FloatVector
import jdk.incubator.vector.VectorOperators
import jdk.incubator.vector.VectorSpecies

object Buf

The operations themselves, as one object: Buf.add(dst, src, 0, n), Buf.peak(buf), and so on.

SPECIES is FloatVector.SPECIES_PREFERRED and LANES is its lane count. Both are resolved when the object initialises, so the vector width is whatever the host CPU offers rather than a compile-time constant, and code that reads LANES gets the width the rest of the file is actually stepping by.

object Buf {

val SPECIES: VectorSpecies<Float>

FloatVector.SPECIES_PREFERRED: the shape this JVM prefers on the CPU it is running on.

    @JvmField
    val SPECIES: VectorSpecies<Float> = FloatVector.SPECIES_PREFERRED

val LANES: Int

Floats per vector, SPECIES.length(). The stride of every vectorised loop below.

    @JvmField
    val LANES: Int = SPECIES.length()

fill / clear / copy

fun fill(dst: FloatArray, value: Float, off: Int = 0, len: Int = dst.size - off)

Fill dst with a constant value.

    fun fill(dst: FloatArray, value: Float, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len
        val vec = FloatVector.broadcast(SPECIES, value)
        var i = off
        val bound = SPECIES.loopBound(len) + off
        while (i < bound) { vec.intoArray(dst, i); i += LANES }
        while (i < end) { dst[i++] = value }
    }

fun clear(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) = fill(dst, 0f, off, len)

Zero the buffer.

fun copy(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off)

Copy src into dst.

    fun copy(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        System.arraycopy(src, off, dst, off, len)
    }

arithmetic (buffer ⊕ buffer)

fun add(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off)

dsti += srci

    fun add(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i)
                .add(FloatVector.fromArray(SPECIES, src, i))
                .intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] += src[i]; i++ }
    }

fun sub(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off)

dsti -= srci

    fun sub(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i)
                .sub(FloatVector.fromArray(SPECIES, src, i))
                .intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] -= src[i]; i++ }
    }

fun mul(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off)

dsti *= srci

    fun mul(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i)
                .mul(FloatVector.fromArray(SPECIES, src, i))
                .intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] *= src[i]; i++ }
    }

fun div(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off, epsilon: Float = 1e-15f)

dsti /= srci, guarding against near-zero divisors.

    fun div(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off, epsilon: Float = 1e-15f) {
        val end = off + len
        val veps = FloatVector.broadcast(SPECIES, epsilon)
        val vzero = FloatVector.broadcast(SPECIES, 0f)
        var i = off
        val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            val numer = FloatVector.fromArray(SPECIES, dst, i)
            val denom = FloatVector.fromArray(SPECIES, src, i)
            val safe = denom.abs().compare(VectorOperators.GT, veps)
            numer.div(denom)
                .blend(vzero, safe.not())
                .intoArray(dst, i)
            i += LANES
        }
        while (i < end) {
            val d = src[i]
            dst[i] = if (kotlin.math.abs(d) > epsilon) dst[i] / d else 0f
            i++
        }
    }

arithmetic (buffer ⊕ scalar)

fun addScalar(dst: FloatArray, scalar: Float, off: Int = 0, len: Int = dst.size - off)

dsti += scalar

    fun addScalar(dst: FloatArray, scalar: Float, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; val v = FloatVector.broadcast(SPECIES, scalar)
        var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i).add(v).intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] += scalar; i++ }
    }

fun scale(dst: FloatArray, scalar: Float, off: Int = 0, len: Int = dst.size - off)

dsti *= scalar

    fun scale(dst: FloatArray, scalar: Float, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; val v = FloatVector.broadcast(SPECIES, scalar)
        var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i).mul(v).intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] *= scalar; i++ }
    }

fused multiply-add

fun fma

dsti = ai * bi + ci (FMA)

    fun fma(
        dst: FloatArray, a: FloatArray, b: FloatArray, c: FloatArray,
        off: Int = 0, len: Int = dst.size - off
    ) {
        val end = off + len; var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, a, i)
                .fma(FloatVector.fromArray(SPECIES, b, i), FloatVector.fromArray(SPECIES, c, i))
                .intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] = a[i] * b[i] + c[i]; i++ }
    }

fun scaleAndAdd

dsti = dsti * scalar + addendi

    fun scaleAndAdd(
        dst: FloatArray, scalar: Float, addend: FloatArray,
        off: Int = 0, len: Int = dst.size - off
    ) {
        val end = off + len; val sv = FloatVector.broadcast(SPECIES, scalar)
        var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i)
                .fma(sv, FloatVector.fromArray(SPECIES, addend, i))
                .intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] = dst[i] * scalar + addend[i]; i++ }
    }

clamp / abs

fun clamp(dst: FloatArray, lo: Float, hi: Float, off: Int = 0, len: Int = dst.size - off)

dsti = clamp(dsti, lo, hi)

    fun clamp(dst: FloatArray, lo: Float, hi: Float, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len
        val vlo = FloatVector.broadcast(SPECIES, lo)
        val vhi = FloatVector.broadcast(SPECIES, hi)
        var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i).max(vlo).min(vhi).intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] = dst[i].coerceIn(lo, hi); i++ }
    }

fun abs(dst: FloatArray, off: Int = 0, len: Int = dst.size - off)

dsti = |dsti|

    fun abs(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len; var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            FloatVector.fromArray(SPECIES, dst, i).abs().intoArray(dst, i); i += LANES
        }
        while (i < end) { dst[i] = kotlin.math.abs(dst[i]); i++ }
    }

reductions

fun peak(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float

Peak absolute value.

    fun peak(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float {
        val end = off + len; var mx = 0f
        var i = off; val bound = SPECIES.loopBound(len) + off
        var vmx = FloatVector.broadcast(SPECIES, 0f)
        while (i < bound) {
            vmx = vmx.max(FloatVector.fromArray(SPECIES, src, i).abs()); i += LANES
        }
        mx = vmx.reduceLanes(VectorOperators.MAX)
        while (i < end) { val a = kotlin.math.abs(src[i]); if (a > mx) mx = a; i++ }
        return mx
    }

fun rms(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float

RMS (root-mean-square) level.

    fun rms(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float {
        val end = off + len; var sum = 0f
        var i = off; val bound = SPECIES.loopBound(len) + off
        var vsum = FloatVector.broadcast(SPECIES, 0f)
        while (i < bound) {
            val v = FloatVector.fromArray(SPECIES, src, i)
            vsum = v.fma(v, vsum); i += LANES
        }
        sum = vsum.reduceLanes(VectorOperators.ADD)
        while (i < end) { sum += src[i] * src[i]; i++ }
        return kotlin.math.sqrt(sum / len)
    }

fun sum(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float

Sum all elements.

    fun sum(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float {
        val end = off + len; var s = 0f
        var i = off; val bound = SPECIES.loopBound(len) + off
        var vs = FloatVector.broadcast(SPECIES, 0f)
        while (i < bound) {
            vs = vs.add(FloatVector.fromArray(SPECIES, src, i)); i += LANES
        }
        s = vs.reduceLanes(VectorOperators.ADD)
        while (i < end) { s += src[i]; i++ }
        return s
    }

windowing

fun hann(dst: FloatArray, off: Int = 0, len: Int = dst.size - off)

Apply a Hann window in-place.

    fun hann(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val end = off + len
        val factor = 2.0 * Math.PI / (len - 1).toDouble()
        for (i in off until end) {
            dst[i] *= (0.5 - 0.5 * Math.cos(factor * (i - off))).toFloat()
        }
    }

fun hannWindow(dst: FloatArray, off: Int = 0, len: Int = dst.size - off)

Generate a Hann window into dst.

    fun hannWindow(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) {
        val factor = 2.0 * Math.PI / (len - 1).toDouble()
        for (i in off until end(off, len)) {
            dst[i] = (0.5 - 0.5 * Math.cos(factor * (i - off))).toFloat()
        }
    }

mix helpers

fun crossfade

dsti = ai * (1-mix) + bi * mix (cross-fade)

    fun crossfade(
        dst: FloatArray, a: FloatArray, b: FloatArray, mix: Float,
        off: Int = 0, len: Int = dst.size - off
    ) {
        val end = off + len
        val vm = FloatVector.broadcast(SPECIES, mix)
        val v1m = FloatVector.broadcast(SPECIES, 1f - mix)
        var i = off; val bound = SPECIES.loopBound(len) + off
        while (i < bound) {
            val va = FloatVector.fromArray(SPECIES, a, i)
            val vb = FloatVector.fromArray(SPECIES, b, i)
            va.mul(v1m).add(vb.mul(vm)).intoArray(dst, i); i += LANES
        }
        val om = 1f - mix
        while (i < end) { dst[i] = a[i] * om + b[i] * mix; i++ }
    }

    private fun end(off: Int, len: Int) = off + len
}