Skip to content

Oscillators.kt

src/jvmMain/kotlin/afield/audio/synth/Oscillators.kt · 1081 lines

@file:afield.runtime.completion.CompletionCategory("Synthesis")
package afield.audio.synth

import java.util.concurrent.ThreadLocalRandom
import kotlin.math.*

The sources: oscillators, noise, and the samplers that stand in for one. All of them are Signals taking Params, so a frequency can itself be an oscillator and FM costs nothing to ask for.

SawOsc, SquareOsc, PulseOsc and TriOsc are corrected with PolyBLEP, a polynomial stand-in for a bandlimited step subtracted at each discontinuity, so they hold up at the top of the keyboard where a naive ramp would fold. SineOsc has nothing to correct, and Phasor's bare 0-to-1 ramp is the phase source to start from when shaping a waveform of your own.

SuperSaw is a detuned bank with the spread, the blend, the detune curve and the phase alignment all exposed. The Super* voices beside it are continuous ports of the SuperDirt synths of the same names, so a pattern written for Tidal has something to play. WavetableOsc reads a single cycle out of a FloatArray, and Wavetables builds the ordinary ones by summing harmonics.

The three noise generators differ in spectrum: WhiteNoise is flat, PinkNoise is a bank of one-pole filters over white and falls about 3 dB per octave, BrownNoise is a leaky random walk and falls about 6. Impulse emits one sample of 1 each time its phase wraps, which is the trigger SampleAndHold is usually waiting for.

synth.kt carries lowercase aliases for the common ones (sine, saw, white), and each one hands back a fresh instance every time it is read.

Phasor – raw phase ramp [0, 1)

class Phasor

Outputs a rising ramp from 0 to 1 at the given freq. Useful as a building block for custom wave-shaping.

class Phasor(
    val freq: Param = Param(440f),
    initialPhase: Double = 0.0
) : Signal() {
    private var phase: Double = initialPhase
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            output[i] = phase.toFloat()
            phase += freqBuf[i].toDouble() * invSr
            phase -= floor(phase)
        }
    }
}

SineOsc – sine wave oscillator

class SineOsc

class SineOsc(
    val freq: Param = Param(440f),
    /** Phase modulation input (radians). */
    val phaseMod: Param = Param(0f)
) : Signal() {
    private var phase: Double = 0.0
    private var freqBuf = FloatArray(0)
    private var pmBuf   = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            pmBuf   = FloatArray(numFrames)
        }
        freq.render(freqBuf, numFrames, ctx)
        phaseMod.render(pmBuf, numFrames, ctx)
        val twoPi = 2.0 * PI
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            output[i] = sin(twoPi * phase + pmBuf[i].toDouble()).toFloat()
            phase += freqBuf[i].toDouble() * invSr
            phase -= floor(phase)
        }
    }
}

PolyBLEP helpers (shared by Saw, Square, Pulse, Tri)

Polynomial bandlimited step (PolyBLEP) correction. Smooths discontinuities to suppress aliasing.

private fun polyBlep(t: Double, dt: Double): Double {
    if (t < dt) {
        val x = t / dt
        return x + x - x * x - 1.0
    }
    if (t > 1.0 - dt) {
        val x = (t - 1.0) / dt
        return x * x + x + x + 1.0
    }
    return 0.0
}

SawOsc – antialiased sawtooth

class SawOsc

class SawOsc(
    val freq: Param = Param(440f)
) : Signal() {
    private var phase: Double = 0.0
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val dt = abs(freqBuf[i].toDouble()) * invSr
            // naive saw: 2*phase - 1
            var sample = 2.0 * phase - 1.0
            sample -= polyBlep(phase, dt)
            output[i] = sample.toFloat()
            phase += dt
            phase -= floor(phase)
        }
    }
}

SquareOsc – antialiased square wave

class SquareOsc

class SquareOsc(
    val freq: Param = Param(440f)
) : Signal() {
    private var phase: Double = 0.0
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val dt = abs(freqBuf[i].toDouble()) * invSr
            var sample = if (phase < 0.5) 1.0 else -1.0
            sample += polyBlep(phase, dt)
            sample -= polyBlep((phase + 0.5) % 1.0, dt)
            output[i] = sample.toFloat()
            phase += dt
            phase -= floor(phase)
        }
    }
}

PulseOsc – variable-width pulse

class PulseOsc

class PulseOsc(
    val freq: Param = Param(440f),
    /** Pulse width 0…1 (0.5 = square). */
    val width: Param = Param(0.5f)
) : Signal() {
    private var phase: Double = 0.0
    private var freqBuf  = FloatArray(0)
    private var widthBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf  = FloatArray(numFrames)
            widthBuf = FloatArray(numFrames)
        }
        freq.render(freqBuf, numFrames, ctx)
        width.render(widthBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val dt = abs(freqBuf[i].toDouble()) * invSr
            val pw = widthBuf[i].toDouble().coerceIn(0.01, 0.99)
            var sample = if (phase < pw) 1.0 else -1.0
            sample += polyBlep(phase, dt)
            sample -= polyBlep((phase + (1.0 - pw)) % 1.0, dt)
            output[i] = sample.toFloat()
            phase += dt
            phase -= floor(phase)
        }
    }
}

TriOsc – antialiased triangle

class TriOsc

class TriOsc(
    val freq: Param = Param(440f)
) : Signal() {
    private var phase: Double = 0.0
    private var integrator: Double = 0.0
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val f = abs(freqBuf[i].toDouble())
            val dt = f * invSr
            // PolyBLEP square
            var sq = if (phase < 0.5) 1.0 else -1.0
            sq += polyBlep(phase, dt)
            sq -= polyBlep((phase + 0.5) % 1.0, dt)
            // leaky integration → triangle
            integrator += sq * dt * 4.0
            integrator *= 0.999  // leak to prevent DC drift
            output[i] = integrator.toFloat()
            phase += dt
            phase -= floor(phase)
        }
    }
}

SuperSaw – classic stacked detuned saw bank

class SuperSaw

class SuperSaw(
    val freq: Param = Param(110f),
    /** Maximum spread of the detuned bank in cents. */
    val detuneCents: Param = Param(20f),
    /** 0 = only center voice, 1 = full detuned bank. */
    val blend: Param = Param(0.85f),
    /** Shapes how strongly the outer voices pull away from center. */
    val detuneCurve: Param = Param(1.4f),
    /** 0 = aligned phases, 1 = evenly spread initial phases. */
    val phaseSpread: Param = Param(1f),
    val voiceCount: Int = 7,
    val normalize: Boolean = true
) : Signal() {
    private val safeVoiceCount = voiceCount.coerceAtLeast(1)
    private val centerIndex = safeVoiceCount / 2
    private val phases = DoubleArray(safeVoiceCount) { 0.0 }
    private var initializedPhases = false

    private var freqBuf = FloatArray(0)
    private var detuneBuf = FloatArray(0)
    private var blendBuf = FloatArray(0)
    private var curveBuf = FloatArray(0)
    private var phaseSpreadBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

body — 185 lines, from line 258
    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            detuneBuf = FloatArray(numFrames)
            blendBuf = FloatArray(numFrames)
            curveBuf = FloatArray(numFrames)
            phaseSpreadBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        detuneCents.render(detuneBuf, numFrames, ctx)
        blend.render(blendBuf, numFrames, ctx)
        detuneCurve.render(curveBuf, numFrames, ctx)
        phaseSpread.render(phaseSpreadBuf, numFrames, ctx)

        if (!initializedPhases) {
            initializePhases(phaseSpreadBuf.firstOrNull() ?: 1f)
            initializedPhases = true
        }

        val invSr = ctx.inverseSampleRate.toDouble()

        for (i in 0 until numFrames) {
            val hz = kotlin.math.abs(freqBuf[i].toDouble()).coerceAtLeast(0.0)
            val cents = detuneBuf[i].coerceAtLeast(0f)
            val curve = curveBuf[i].coerceAtLeast(0.1f)
            val mix = blendBuf[i].coerceIn(0f, 1f)

            var center = 0.0
            var sides = 0.0
            var sideCount = 0

            for (voice in 0 until safeVoiceCount) {
                val detuneUnit = detuneUnitForVoice(voice, curve)
                val voiceCents = cents * detuneUnit
                val voiceFreq = hz * centsToRatio(voiceCents.toDouble())
                val dt = voiceFreq * invSr
                val phase = phases[voice]

                var sample = 2.0 * phase - 1.0
                sample -= polyBlep(phase, dt)

                if (voice == centerIndex) {
                    center = sample
                } else {
                    sides += sample
                    sideCount++
                }

                var nextPhase = phase + dt
                nextPhase -= floor(nextPhase)
                phases[voice] = nextPhase
            }

            val sideSignal = if (sideCount > 0) sides / sideCount else 0.0
            val mixed = center * (1f - mix) + sideSignal * mix
            val norm = if (normalize) 0.9 else 1.0
            output[i] = (mixed * norm).toFloat()
        }
    }

    private fun initializePhases(spread: Float) {
        val phaseAmount = spread.coerceIn(0f, 1f)
        for (voice in 0 until safeVoiceCount) {
            val offset = if (safeVoiceCount == 1) 0.0 else voice.toDouble() / safeVoiceCount.toDouble()
            phases[voice] = offset * phaseAmount
        }
    }

    private fun detuneUnitForVoice(index: Int, curve: Float): Float {
        if (safeVoiceCount == 1) return 0f
        val centered = (index - centerIndex).toFloat()
        val normalized = centered / centerIndex.coerceAtLeast(1).toFloat()
        val magnitude = kotlin.math.abs(normalized).toDouble().pow(curve.toDouble()).toFloat()
        return kotlin.math.sign(normalized) * magnitude
    }

    private fun centsToRatio(cents: Double): Double = 2.0.pow(cents / 1200.0)
}

private fun wrapPhase(phase: Double): Double = phase - floor(phase)

private fun pulseSample(phase: Double, dt: Double, width: Double): Double {
    val wrapped = wrapPhase(phase)
    val pw = width.coerceIn(0.01, 0.99)
    var sample = if (wrapped < pw) 1.0 else -1.0
    sample += polyBlep(wrapped, dt)
    sample -= polyBlep((wrapped + (1.0 - pw)) % 1.0, dt)
    return sample
}

private fun squareSample(phase: Double, dt: Double): Double {
    val wrapped = wrapPhase(phase)
    var sample = if (wrapped < 0.5) 1.0 else -1.0
    sample += polyBlep(wrapped, dt)
    sample -= polyBlep((wrapped + 0.5) % 1.0, dt)
    return sample
}

private fun varSawSample(phase: Double, width: Double): Double {
    val wrapped = wrapPhase(phase)
    val w = width.coerceIn(0.01, 0.99)
    return if (wrapped < w) {
        -1.0 + 2.0 * (wrapped / w)
    } else {
        1.0 - 2.0 * ((wrapped - w) / (1.0 - w))
    }
}

private fun triangleSample(phase: Double): Double {
    val wrapped = wrapPhase(phase)
    return 1.0 - 4.0 * abs(wrapped - 0.5)
}

private fun semitoneRatio(semitones: Double): Double = 2.0.pow(semitones / 12.0)

private fun midiFromHz(hz: Double): Double = 69.0 + 12.0 * log2(hz / 440.0)

private fun mix(a: Double, b: Double, amount: Float): Double {
    val t = amount.coerceIn(0f, 1f).toDouble()
    return a * (1.0 - t) + b * t
}

private class MoogLadderCore {
    private val stage = DoubleArray(4)

    fun process(input: Double, cutoff: Double, resonance: Double, sampleRate: Double): Float {
        val freq = cutoff.coerceIn(20.0, sampleRate * 0.499)
        val res = resonance.coerceIn(0.0, 1.0) * 4.0
        val f = 2.0 * sin(PI * freq / sampleRate)
        val fb = res * (1.0 - 0.15 * f * f)
        val inp = input - fb * stage[3]

        stage[0] += f * (tanh(inp) - tanh(stage[0]))
        stage[1] += f * (tanh(stage[0]) - tanh(stage[1]))
        stage[2] += f * (tanh(stage[1]) - tanh(stage[2]))
        stage[3] += f * (tanh(stage[2]) - tanh(stage[3]))

        return stage[3].toFloat()
    }
}

private class OnePoleHighPassCore {
    private var low = 0.0

    fun process(input: Double, cutoff: Double, sampleRate: Double): Float {
        val freq = cutoff.coerceIn(1.0, sampleRate * 0.499)
        val b1 = exp(-2.0 * PI * freq / sampleRate)
        val a0 = 1.0 - b1
        low = a0 * input + b1 * low
        return (input - low).toFloat()
    }
}

private class HeldNoiseCore {
    private var phase = 0.0
    private var current = 0.0

    fun next(freq: Double, invSr: Double, rng: ThreadLocalRandom, amplitude: Double = 1.0): Double {
        phase += abs(freq) * invSr
        if (phase >= 1.0) {
            phase -= floor(phase)
            current = (rng.nextDouble() * 2.0 - 1.0) * amplitude
        }
        return current
    }
}

private class LFNoise1Core {
    private var phase = 0.0
    private var current = 0.0
    private var target = 0.0

    fun next(freq: Double, invSr: Double, rng: ThreadLocalRandom): Double {
        val rate = abs(freq)
        if (rate <= 1e-6) return current
        phase += rate * invSr
        if (phase >= 1.0) {
            phase -= floor(phase)
            current = target
            target = rng.nextDouble() * 2.0 - 1.0
        }
        return current + (target - current) * phase
    }
}

Tidal-inspired Super* synths

class SuperSquare

Continuous port of SuperDirt's supersquare voice: two pulse oscillators into a moog-style low-pass with an audio-rate-tracked LFO.

class SuperSquare(
    val freq: Param = Param(110f),
    /** Pulse width for both oscillators. Values near 0 or 1 nearly mute the sound. */
    val voice: Param = Param(0.5f),
    /** Detune of the secondary oscillator in semitones below the main pitch. */
    val semitone: Param = Param(12f),
    /** Filter resonance amount. */
    val resonance: Param = Param(0.2f),
    /** Additional filter sweep range above the tracked base cutoff. */
    val lfo: Param = Param(1f),
    /** Multiplier for the internal LFO rate. */
    val rate: Param = Param(1f),
    /** Scales the tracked cutoff floor. */
    val pitch1: Param = Param(1f)
) : Signal() {
    private var primaryPhase = 0.0
    private var subPhase = 0.0
    private var lfoPhase = 0.0
    private val filter = MoogLadderCore()

    private var freqBuf = FloatArray(0)
    private var voiceBuf = FloatArray(0)
    private var semiBuf = FloatArray(0)
    private var resBuf = FloatArray(0)
    private var lfoBuf = FloatArray(0)
    private var rateBuf = FloatArray(0)
    private var pitchBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

body — 50 lines, from line 480
    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            voiceBuf = FloatArray(numFrames)
            semiBuf = FloatArray(numFrames)
            resBuf = FloatArray(numFrames)
            lfoBuf = FloatArray(numFrames)
            rateBuf = FloatArray(numFrames)
            pitchBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        voice.render(voiceBuf, numFrames, ctx)
        semitone.render(semiBuf, numFrames, ctx)
        resonance.render(resBuf, numFrames, ctx)
        lfo.render(lfoBuf, numFrames, ctx)
        rate.render(rateBuf, numFrames, ctx)
        pitch1.render(pitchBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val sr = ctx.sampleRate.toDouble()
        val maxCutoff = sr * 0.499

        for (i in 0 until numFrames) {
            val baseFreq = abs(freqBuf[i].toDouble()).coerceIn(0.0, maxCutoff)
            val mainDt = baseFreq * invSr
            val subFreq = (baseFreq / semitoneRatio(semiBuf[i].toDouble())).coerceIn(0.0, maxCutoff)
            val subDt = subFreq * invSr
            val width = voiceBuf[i].coerceIn(0.02f, 0.98f).toDouble()

            val sound =
                0.7 * pulseSample(primaryPhase, mainDt, width) +
                0.3 * pulseSample(subPhase, subDt, width)

            val trackedCutoff =
                (baseFreq * 10.0 * pitchBuf[i].coerceAtLeast(0f)).coerceIn(20.0, maxCutoff)
            val sweepTop =
                (trackedCutoff * (1.0 + lfoBuf[i].coerceAtLeast(0f))).coerceIn(trackedCutoff, maxCutoff)
            val lfoValue = 0.5 + 0.5 * sin(2.0 * PI * lfoPhase)
            val cutoff = trackedCutoff + (sweepTop - trackedCutoff) * lfoValue

            val filtered = filter.process(sound, cutoff, resBuf[i].toDouble(), sr)
            output[i] = (tanh(filtered.toDouble()) * 2.0).toFloat()

            primaryPhase = wrapPhase(primaryPhase + mainDt)
            subPhase = wrapPhase(subPhase + subDt)
            lfoPhase = wrapPhase(lfoPhase + (baseFreq / 64.0) * rateBuf[i].toDouble() * invSr)
        }
    }
}

class SuperPWM

Continuous port of SuperDirt's superpwm voice. The second filter uses secondarySweep instead of a note-duration envelope.

class SuperPWM(
    val freq: Param = Param(110f),
    /** Phase offset between the multiplied pulse pairs, expressed in oscillator cycles. */
    val voice: Param = Param(0.5f),
    /** Detune of the secondary oscillator in semitones below the main pitch. */
    val semitone: Param = Param(12f),
    /** Filter resonance amount for the tracked low-pass stage. */
    val resonance: Param = Param(0.2f),
    /** Additional filter sweep range above the tracked base cutoff. */
    val lfo: Param = Param(1f),
    /** Multiplier for the internal LFO rate. */
    val rate: Param = Param(1f),
    /** Scales the tracked cutoff floor. */
    val pitch1: Param = Param(1f),
    /** Replaces SuperDirt's note-duration sweep on the second filter in this continuous port. */
    val secondarySweep: Param = Param(0.65f)
) : Signal() {
    private var primaryPhase = 0.0
    private var subPhase = 0.0
    private var lfoPhase = 0.0
    private val firstFilter = MoogLadderCore()
    private val secondFilter = MoogLadderCore()

    private var freqBuf = FloatArray(0)
    private var voiceBuf = FloatArray(0)
    private var semiBuf = FloatArray(0)
    private var resBuf = FloatArray(0)
    private var lfoBuf = FloatArray(0)
    private var rateBuf = FloatArray(0)
    private var pitchBuf = FloatArray(0)
    private var sweepBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

body — 60 lines, from line 567
    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            voiceBuf = FloatArray(numFrames)
            semiBuf = FloatArray(numFrames)
            resBuf = FloatArray(numFrames)
            lfoBuf = FloatArray(numFrames)
            rateBuf = FloatArray(numFrames)
            pitchBuf = FloatArray(numFrames)
            sweepBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        voice.render(voiceBuf, numFrames, ctx)
        semitone.render(semiBuf, numFrames, ctx)
        resonance.render(resBuf, numFrames, ctx)
        lfo.render(lfoBuf, numFrames, ctx)
        rate.render(rateBuf, numFrames, ctx)
        pitch1.render(pitchBuf, numFrames, ctx)
        secondarySweep.render(sweepBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val sr = ctx.sampleRate.toDouble()
        val maxCutoff = sr * 0.499

        for (i in 0 until numFrames) {
            val baseFreq = abs(freqBuf[i].toDouble()).coerceIn(0.0, maxCutoff)
            val mainDt = baseFreq * invSr
            val subFreq = (baseFreq / semitoneRatio(semiBuf[i].toDouble())).coerceIn(0.0, maxCutoff)
            val subDt = subFreq * invSr
            val phaseOffset = voiceBuf[i].toDouble()

            val primary =
                pulseSample(primaryPhase, mainDt, 0.5) *
                pulseSample(primaryPhase - phaseOffset, mainDt, 0.5)
            val secondary =
                pulseSample(subPhase, subDt, 0.5) *
                pulseSample(subPhase - (phaseOffset + 0.1), subDt, 0.5)

            val trackedCutoff =
                (baseFreq * 10.0 * pitchBuf[i].coerceAtLeast(0f)).coerceIn(20.0, maxCutoff)
            val sweepTop =
                (trackedCutoff * (1.0 + lfoBuf[i].coerceAtLeast(0f))).coerceIn(trackedCutoff, maxCutoff)
            val lfoValue = 0.5 + 0.5 * sin(2.0 * PI * lfoPhase)
            val firstCutoff = trackedCutoff + (sweepTop - trackedCutoff) * lfoValue

            val shaped = 0.7 * primary + 0.3 * secondary
            val firstPass = firstFilter.process(shaped, firstCutoff, resBuf[i].toDouble(), sr)
            val secondCutoff =
                (sweepTop * 1.1 * sweepBuf[i].coerceIn(0.05f, 1f)).coerceIn(20.0, maxCutoff)
            val secondPass = secondFilter.process(firstPass.toDouble(), secondCutoff, 0.75, sr)

            output[i] = (tanh(secondPass * 5.0)).toFloat()

            primaryPhase = wrapPhase(primaryPhase + mainDt)
            subPhase = wrapPhase(subPhase + subDt)
            lfoPhase = wrapPhase(lfoPhase + (baseFreq / 32.0) * rateBuf[i].toDouble() * invSr)
        }
    }
}

class SuperComparator

Continuous port of SuperDirt's supercomparator voice: a comparator bank over a skewed ramp into a resonant low-pass.

class SuperComparator(
    val freq: Param = Param(110f),
    /** Scales the internal comparator frequencies. Higher values sound breathier. */
    val voice: Param = Param(0.5f),
    /** Filter resonance amount. */
    val resonance: Param = Param(0.5f),
    /** Amount of LFO and random movement applied to the filter. */
    val lfo: Param = Param(1f),
    /** Multiplier for the internal LFO rate. */
    val rate: Param = Param(1f),
    /** Scales the tracked cutoff floor. */
    val pitch1: Param = Param(1f),
    /** Fixed width for the raw port's VarSaw source. */
    val shape: Param = Param(0.5f)
) : Signal() {
    private val comparatorFreqs = doubleArrayOf(
        1.000, 2.188, 5.091, 8.529, 8.950, 9.305, 13.746, 14.653,
        19.462, 22.003, 24.888, 25.991, 26.085, 30.509, 33.608, 35.081
    )
    private var sourcePhase = 0.0
    private var lfoPhase = 0.0
    private val comparatorPhases = DoubleArray(comparatorFreqs.size)
    private val filter = MoogLadderCore()
    private val noise = LFNoise1Core()

    private var freqBuf = FloatArray(0)
    private var voiceBuf = FloatArray(0)
    private var resBuf = FloatArray(0)
    private var lfoBuf = FloatArray(0)
    private var rateBuf = FloatArray(0)
    private var pitchBuf = FloatArray(0)
    private var shapeBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

body — 54 lines, from line 665
    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            voiceBuf = FloatArray(numFrames)
            resBuf = FloatArray(numFrames)
            lfoBuf = FloatArray(numFrames)
            rateBuf = FloatArray(numFrames)
            pitchBuf = FloatArray(numFrames)
            shapeBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        voice.render(voiceBuf, numFrames, ctx)
        resonance.render(resBuf, numFrames, ctx)
        lfo.render(lfoBuf, numFrames, ctx)
        rate.render(rateBuf, numFrames, ctx)
        pitch1.render(pitchBuf, numFrames, ctx)
        shape.render(shapeBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val sr = ctx.sampleRate.toDouble()
        val maxCutoff = sr * 0.499
        val rng = ThreadLocalRandom.current()

        for (i in 0 until numFrames) {
            val baseFreq = abs(freqBuf[i].toDouble()).coerceIn(0.0, maxCutoff)
            val baseDt = baseFreq * invSr
            val source = varSawSample(sourcePhase, shapeBuf[i].coerceIn(0.05f, 0.95f).toDouble())
            val exponent = voiceBuf[i].toDouble() / 5.0

            var summed = 0.0
            for (index in comparatorFreqs.indices) {
                val comparatorFreq = comparatorFreqs[index].pow(exponent)
                comparatorPhases[index] = wrapPhase(comparatorPhases[index] + comparatorFreq * invSr)
                val threshold = triangleSample(comparatorPhases[index])
                summed += if (source > threshold) 1.0 else 0.0
            }
            val compared = summed / comparatorFreqs.size.toDouble()

            val lfoAmount = lfoBuf[i].coerceAtLeast(0f).toDouble()
            val movement =
                sin(2.0 * PI * lfoPhase) * lfoAmount * baseFreq / 2.0 +
                noise.next(1.0, invSr, rng) * lfoAmount * baseFreq
            val cutoff =
                (pitchBuf[i].coerceAtLeast(0f) * 4.0 * baseFreq + movement).coerceIn(20.0, maxCutoff)

            val filtered = filter.process(compared, cutoff, resBuf[i].toDouble(), sr)
            output[i] = (tanh(filtered * 4.0) * 4.0).toFloat()

            sourcePhase = wrapPhase(sourcePhase + baseDt)
            lfoPhase = wrapPhase(lfoPhase + (baseFreq / 64.0) * rateBuf[i].toDouble() * invSr)
        }
    }
}

class SuperChip

Approximation of SuperDirt's AY-based superchip using three antialiased square voices.

class SuperChip(
    val freq: Param = Param(110f),
    val pitch2: Param = Param(2f),
    val pitch3: Param = Param(3f),
    /** Bit-pattern selector for which of the three oscillators are active. */
    val voice: Param = Param(0f)
) : Signal() {
    private var phaseA = 0.0
    private var phaseB = 0.0
    private var phaseC = 0.0

    private var freqBuf = FloatArray(0)
    private var pitch2Buf = FloatArray(0)
    private var pitch3Buf = FloatArray(0)
    private var voiceBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

body — 45 lines, from line 739
    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            pitch2Buf = FloatArray(numFrames)
            pitch3Buf = FloatArray(numFrames)
            voiceBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        pitch2.render(pitch2Buf, numFrames, ctx)
        pitch3.render(pitch3Buf, numFrames, ctx)
        voice.render(voiceBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val maxFreq = ctx.nyquist.toDouble()

        for (i in 0 until numFrames) {
            val baseFreq = abs(freqBuf[i].toDouble()).coerceIn(0.0, maxFreq)
            val freqB = (baseFreq * pitch2Buf[i].toDouble().coerceAtLeast(0.0)).coerceIn(0.0, maxFreq)
            val freqC = (baseFreq * pitch3Buf[i].toDouble().coerceAtLeast(0.0)).coerceIn(0.0, maxFreq)

            val dtA = baseFreq * invSr
            val dtB = freqB * invSr
            val dtC = freqC * invSr

            val v = voiceBuf[i]
            val voiceB = (((v * 2f) % 1f) + 1f) % 1f
            val voiceC = (((v * 4f) % 1f) + 1f) % 1f
            val gainA = if (v < 0.5f) 1.0 else 0.0
            val gainB = if (voiceB < 0.5f) 1.0 else 0.0
            val gainC = if (voiceC < 0.5f) 1.0 else 0.0

            val sample =
                squareSample(phaseA, dtA) * gainA +
                squareSample(phaseB, dtB) * gainB +
                squareSample(phaseC, dtC) * gainC

            output[i] = (tanh(sample) * 2.0).toFloat()

            phaseA = wrapPhase(phaseA + dtA)
            phaseB = wrapPhase(phaseB + dtB)
            phaseC = wrapPhase(phaseC + dtC)
        }
    }
}

class SuperNoise

Continuous port of SuperDirt's supernoise voice: crossfaded stepped noise and brown/white noise into a resonant filter.

class SuperNoise(
    val freq: Param = Param(110f),
    /** Scales the tracked band-pass center frequency. */
    val pitch1: Param = Param(1f),
    /** Filter resonance amount. */
    val resonance: Param = Param(0f),
    /** 0 = digital stepped noise, 1 = brown/white blend. */
    val voice: Param = Param(0f)
) : Signal() {
    private val digitalNoise = HeldNoiseCore()
    private val ladder = MoogLadderCore()
    private val highPass = OnePoleHighPassCore()
    private val white = WhiteNoise()
    private val brown = BrownNoise()

    private var freqBuf = FloatArray(0)
    private var pitchBuf = FloatArray(0)
    private var resBuf = FloatArray(0)
    private var voiceBuf = FloatArray(0)
    private var whiteBuf = FloatArray(0)
    private var brownBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) {
            freqBuf = FloatArray(numFrames)
            pitchBuf = FloatArray(numFrames)
            resBuf = FloatArray(numFrames)
            voiceBuf = FloatArray(numFrames)
            whiteBuf = FloatArray(numFrames)
            brownBuf = FloatArray(numFrames)
        }

        freq.render(freqBuf, numFrames, ctx)
        pitch1.render(pitchBuf, numFrames, ctx)
        resonance.render(resBuf, numFrames, ctx)
        voice.render(voiceBuf, numFrames, ctx)
        white.render(whiteBuf, numFrames, ctx)
        brown.render(brownBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val sr = ctx.sampleRate.toDouble()
        val maxFreq = ctx.nyquist.toDouble()
        val rng = ThreadLocalRandom.current()

        for (i in 0 until numFrames) {
            val baseFreq = (abs(freqBuf[i].toDouble()) * 8.0).coerceIn(0.0, maxFreq)
            val digital = digitalNoise.next(baseFreq, invSr, rng, amplitude = 0.5)

            val analogBlend = (midiFromHz(baseFreq.coerceAtLeast(1.0)) / 127.0).coerceIn(0.0, 1.0)
            val analog = mix(brownBuf[i].toDouble() * 0.5, whiteBuf[i].toDouble() * 0.5, analogBlend.toFloat())
            val blended = mix(digital, analog, voiceBuf[i])

            val cutoff = (baseFreq * 5.0 * pitchBuf[i].coerceAtLeast(0f)).coerceIn(60.0, maxFreq)
            val filtered = ladder.process(blended, cutoff, resBuf[i].toDouble(), sr)
            val highPassed = highPass.process(filtered.toDouble(), 20.0, sr)
            output[i] = highPassed.coerceIn(-1f, 1f) * 0.3f
        }
    }
}

class SuperFork

Continuous tuning-fork tone based on the partial set used by SuperDirt's superfork. The original per-note exponential decays are expected to be supplied by an external envelope here.

class SuperFork(
    val freq: Param = Param(440f)
) : Signal() {
    private val partialRatios = doubleArrayOf(1.0, 6.1, 16.7, 20.1)
    private val partialLevels = doubleArrayOf(0.3, 1.0, 1.3, 2.0)
    private val phases = DoubleArray(partialRatios.size)
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)

        val invSr = ctx.inverseSampleRate.toDouble()
        val maxFreq = ctx.nyquist.toDouble()

        for (i in 0 until numFrames) {
            val baseFreq = abs(freqBuf[i].toDouble()).coerceAtLeast(0.0)
            var sample = 0.0

            for (index in partialRatios.indices) {
                val partialFreq = (baseFreq * partialRatios[index]).coerceIn(0.0, maxFreq)
                sample += 0.2 * partialLevels[index] * sin(2.0 * PI * phases[index])
                phases[index] = wrapPhase(phases[index] + partialFreq * invSr)
            }

            output[i] = (sample / partialRatios.size.toDouble()).toFloat()
        }
    }
}

WavetableOsc – arbitrary waveform playback

class WavetableOsc

class WavetableOsc(
    val table: FloatArray,
    val freq: Param = Param(440f)
) : Signal() {
    private var phase: Double = 0.0
    private val size = table.size
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val pos = phase * size
            val idx = pos.toInt()
            val frac = (pos - idx).toFloat()
            // Hermite interpolation (4-point, 3rd-order)
            val s0 = table[(idx - 1 + size) % size]
            val s1 = table[idx % size]
            val s2 = table[(idx + 1) % size]
            val s3 = table[(idx + 2) % size]
            output[i] = hermite(frac, s0, s1, s2, s3)

            phase += freqBuf[i].toDouble() * invSr
            phase -= floor(phase)
        }
    }
}

internal fun hermite(frac: Float, s0: Float, s1: Float, s2: Float, s3: Float): Float

Hermite 4-point interpolation.

internal fun hermite(frac: Float, s0: Float, s1: Float, s2: Float, s3: Float): Float {
    val c0 = s1
    val c1 = 0.5f * (s2 - s0)
    val c2 = s0 - 2.5f * s1 + 2f * s2 - 0.5f * s3
    val c3 = 0.5f * (s3 - s0) + 1.5f * (s1 - s2)
    return ((c3 * frac + c2) * frac + c1) * frac + c0
}

Wavetables – factory for common single-cycle waveforms

object Wavetables

object Wavetables {

fun sine(size: Int = 2048): FloatArray

    fun sine(size: Int = 2048): FloatArray {
        val t = FloatArray(size)
        val step = 2.0 * PI / size
        for (i in 0 until size) t[i] = sin(step * i).toFloat()
        return t
    }

fun saw(size: Int = 2048, harmonics: Int = 64): FloatArray

Band-limited sawtooth (first harmonics partials via additive).

    fun saw(size: Int = 2048, harmonics: Int = 64): FloatArray {
        val t = FloatArray(size)
        val step = 2.0 * PI / size
        for (h in 1..harmonics) {
            val amp = 2.0 / (PI * h) * if (h % 2 == 0) 1.0 else -1.0
            for (i in 0 until size) t[i] += (amp * sin(step * i * h)).toFloat()
        }
        return t
    }

fun square(size: Int = 2048, harmonics: Int = 64): FloatArray

Band-limited square (odd harmonics only).

    fun square(size: Int = 2048, harmonics: Int = 64): FloatArray {
        val t = FloatArray(size)
        val step = 2.0 * PI / size
        for (h in 1..harmonics step 2) {
            val amp = 4.0 / (PI * h)
            for (i in 0 until size) t[i] += (amp * sin(step * i * h)).toFloat()
        }
        return t
    }

fun triangle(size: Int = 2048, harmonics: Int = 64): FloatArray

Band-limited triangle (odd harmonics, alternating sign, 1/n²).

    fun triangle(size: Int = 2048, harmonics: Int = 64): FloatArray {
        val t = FloatArray(size)
        val step = 2.0 * PI / size
        var sign = 1.0
        for (h in 1..harmonics step 2) {
            val amp = 8.0 / (PI * PI * h.toDouble() * h.toDouble()) * sign
            for (i in 0 until size) t[i] += (amp * sin(step * i * h)).toFloat()
            sign *= -1.0
        }
        return t
    }
}

Noise generators

class WhiteNoise : Signal()

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        val rng = ThreadLocalRandom.current()
        for (i in 0 until numFrames) {
            output[i] = (rng.nextFloat() * 2f - 1f)
        }
    }
}

class PinkNoise : Signal()

class PinkNoise : Signal() {
    private var b0 = 0.0; private var b1 = 0.0; private var b2 = 0.0
    private var b3 = 0.0; private var b4 = 0.0; private var b5 = 0.0
    private var b6 = 0.0

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        val rng = ThreadLocalRandom.current()
        for (i in 0 until numFrames) {
            val white = rng.nextDouble() * 2.0 - 1.0
            b0 = 0.99886 * b0 + white * 0.0555179
            b1 = 0.99332 * b1 + white * 0.0750759
            b2 = 0.96900 * b2 + white * 0.1538520
            b3 = 0.86650 * b3 + white * 0.3104856
            b4 = 0.55000 * b4 + white * 0.5329522
            b5 = -0.7616 * b5 - white * 0.0168980
            val pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362
            b6 = white * 0.115926
            output[i] = (pink * 0.11).toFloat()  // normalize roughly to [-1, 1]
        }
    }
}

class BrownNoise : Signal()

class BrownNoise : Signal() {
    private var state = 0.0

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        val rng = ThreadLocalRandom.current()
        for (i in 0 until numFrames) {
            state += (rng.nextDouble() * 2.0 - 1.0) * 0.02
            state *= 0.998  // gentle leak
            state = state.coerceIn(-1.0, 1.0)
            output[i] = state.toFloat()
        }
    }
}

Impulse – periodic impulse train

class Impulse

class Impulse(
    val freq: Param = Param(1f)
) : Signal() {
    private var phase: Double = 0.0
    private var freqBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (freqBuf.size < numFrames) freqBuf = FloatArray(numFrames)
        freq.render(freqBuf, numFrames, ctx)
        val invSr = ctx.inverseSampleRate.toDouble()
        for (i in 0 until numFrames) {
            val prevPhase = phase
            phase += abs(freqBuf[i].toDouble()) * invSr
            output[i] = if (floor(phase) > floor(prevPhase)) 1f else 0f
            phase -= floor(phase)
        }
    }
}

SampleAndHold

class SampleAndHold

class SampleAndHold(
    val input: Signal,
    val trigger: Signal
) : Signal() {
    private var held = 0f
    private var prevTrig = 0f
    private var inBuf  = FloatArray(0)
    private var trigBuf = FloatArray(0)

override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        if (inBuf.size < numFrames) {
            inBuf = FloatArray(numFrames)
            trigBuf = FloatArray(numFrames)
        }
        input.render(inBuf, numFrames, ctx)
        trigger.render(trigBuf, numFrames, ctx)
        for (i in 0 until numFrames) {
            val t = trigBuf[i]
            if (t > 0f && prevTrig <= 0f) held = inBuf[i]
            prevTrig = t
            output[i] = held
        }
    }
}