Skip to content

TrackedPhaseSignal.kt

src/jvmMain/kotlin/afield/audio/synth/TrackedPhaseSignal.kt · 46 lines

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

import kotlin.math.abs

Shares one phase-tracking analysis of a signal between several tracked-phase banks in a graph.

TrackedPhaseAnalysis runs its PhaseTracker over the source once per block and keeps the per-sample states; each TrackedPhaseAnalysis.synthesize call renders a TrackedPhaseBank from those states, so the tracker advances once however many outputs read it.

class TrackedPhaseAnalysis(val source: Signal, val tracker: PhaseTracker = QuadPhaseTracker()) : Signal()

Analyze source once per graph block, retaining a reusable state for each sample. Several synthesize outputs can share this analysis in one graph without advancing its tracker twice. Like other stateful signals, this belongs to one audio context; inspect published values rather than rendering the graph from a UI loop. Rendering this node itself returns the analysis input.

internal var states: Array<TrackedPhaseState>

    internal var states: Array<TrackedPhaseState> = emptyArray()
        private set

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

    override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
        require(abs(ctx.sampleRate - tracker.sampleRate) < 0.5) {
            "TrackedPhaseAnalysis: tracker built for ${tracker.sampleRate} Hz, graph runs at ${ctx.sampleRate} Hz"
        }
        if (states.size < numFrames) states = Array(numFrames) { TrackedPhaseState(tracker.sampleRate) }
        source.render(output, numFrames, ctx)
        for (n in 0 until numFrames) states[n].copyFrom(tracker.process(output[n]))
    }

fun synthesize(bank: TrackedPhaseBank): Signal

Render a bank from this analysis. Each output needs its own bank and voice instances.

    fun synthesize(bank: TrackedPhaseBank): Signal {
        require(bank.tracker == null) { "The analysis owns the tracker; supply a bank without one" }
        return object : Signal() {
            private var scratch = FloatArray(0)
            override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) {
                if (scratch.size < numFrames) scratch = FloatArray(numFrames)
                this@TrackedPhaseAnalysis.render(scratch, numFrames, ctx)
                for (n in 0 until numFrames) output[n] = bank.render(states[n]).toFloat()
            }
        }
    }
}