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)
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)
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)
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
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)
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)
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
}