Skip to content

Synthesis

Field's own signal graph — the part of the audio stack that makes samples rather than arranging them. One value type carries the whole thing: Signal is a block-rendered mono source, SignalProcessor is a Signal that reads another one, and / chains the two, so saw / OnePole(800.p) is a filtered sawtooth and nothing else had to be declared. Control values are Param, which a plain number becomes with .p, and which can itself be driven by a signal. audio { … } closes a mono or stereo graph into an AudioPatchSpec; return one as a box's last expression and the box stays lit for as long as it is making sound. Underneath is a shared OpenAL engine, one process-wide, with warm-up passes available so the first buffer is not also the JIT's first look at your graph. On top of the oscillator / filter / effect / envelope families sit three larger things that are not Field's own DSP: Faust, whose compiler is embedded as WebAssembly and turns a #faust box into a running node in the same graph; the dough ports, hand-rolled float math whose inaccuracy is part of the sound; and the spectral and granular layers, which are Field's own. Visualizers.kt is how you look at any of it — every function there is an extension on a type declared in this category. Start from synth.kt, which is the terse surface a box actually imports; Sound: from a pattern to something you can hear is the prose, Faust is the embedded compiler with worked examples, and DoughDSP: hear the primitives plays the ports.

43 source file(s), 1534 public symbol(s), 763 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

CsoundInstance.kt

A running Csound you can render offline or play through Field's audio engine, with queued score events, live recompilation and thread-safe control channels.

src/jvmMain/kotlin/afield/audio/csound/CsoundInstance.kt — read the source

Used in: How to make sound in Field

CsoundInstance

class CsoundInstance(orchestra: String, score: String = "", val sampleRate: Int = 48_000, val ksmps: Int = 32, val outputs: Int = 2, val inputs: Int = 0, options: List<String> = emptyList()) : Closeable

Csound inside Field: one running Csound, with its own orchestra, score and channels.

Declaration Summary
val sampleRate: Int
val ksmps: Int
val outputs: Int
val inputs: Int
data class Problem(val message: String, val line: Int) Something Csound said about the orchestra; line is 1-based in the orchestra text, or -1.
var finished: Boolean True once the score has ended (an e, or the last event finished); render then gives silence.
var framesRendered: Long Frames rendered so far.
val problems: List<Problem> What Csound reported while compiling — warnings, mostly, when the constructor returned.
val version: Int Csound's version as a number: 6181 is 6.18.1.
val scoreTime: Double Csound's score clock, in seconds.
fun event(line: String) A score line, i1 0 2 440 or f1 0 4096 10 1, delivered at the next block.
fun event(type: Char, vararg fields: Number) A score line built from numbers: event('i', 1, 0, 2, 440).
fun compile(orchestra: String) Compile more orchestra code into the running Csound — a new or redefined instr — at the next block. Errors arrive as messages.
fun setChannel(name: String, value: Number) Write control channel name; chnget "name" sees it on the next k-cycle. Any thread.
fun channel(name: String): Double Read control channel name, as a chnset last left it. Any thread; 0 for an unknown channel.
fun setStringChannel(name: String, value: String) Write string channel name; chnget "name" into an S variable sees it. Any thread.
fun stringChannel(name: String): String Read string channel name, as chnset Sval, "name" last left it. "" for an unknown channel.
fun setInvalue(name: String, value: Number) The value invalue "name" reads, at k-rate, on the rendering thread. Any thread may set it. Numbers only: Csound does not tell the host how large an invalue string buffer is, so strings go through setStringChannel and chnget.
fun onOutvalue(name: String, callback: (Any) -> Unit): Closeable Call callback when the orchestra's outvalue "name", … sends something: a Double for a k-rate value, a String for an S value. Delivered by poll on the polling thread. A k-rate outvalue runs every control cycle; the callback is called once per poll, and only when the value changed. A string is delivered every time it changes.
fun outvalue(name: String): Any? The last value outvalue "name" sent, or null. Any thread.
fun tableLength(number: Int): Int The length of function table number, or -1 when there is none.
fun writeTable(number: Int, values: DoubleArray) Copy values into function table number (csoundTableCopyIn), under the perform lock, so the next block reads the new contents. The table keeps its length; values must have exactly that many.
fun writeTable(number: Int, values: FloatArray) writeTable from floats.
fun hints(name: String): ControlHints? The ControlHints chn_k declared for channel name, or null when it declared none.
fun channels(): List<ChannelInfo> Every channel the running orchestra has allocated, as Csound lists them (csoundListChannels): those declared with chn_k/chn_a/chn_S and those a chnget/chnset created. A control channel carries its ControlHints when chn_k declared any.
data class ChannelInfo(val name: String, val kind: Int, val input: Boolean, val output: Boolean, val hints: ControlHints?) One allocated channel. kind is CsoundNative.CONTROL_CHANNEL, CsoundNative.AUDIO_CHANNEL or CsoundNative.STRING_CHANNEL; input/output are the direction bits Csound holds, which accumulate: a chnget or chnset anywhere in the orchestra adds its direction, so a chn_k "gain", 1 read by chnget was listed as both in this checkout's pinned Csound. Read them as "something uses it this way", not as the declared imode.
data class ControlHints(val type: Int, val default: Double, val min: Double, val max: Double) A channel's declared range: chn_k "name", imode, itype, idflt, imin, imax. type 1 integer, 2 linear, 3 exponential.
fun midi(status: Int, data1: Int, data2: Int) A MIDI message to the orchestra, as if from a device: massign, notnum, veloc, midic7 read it. Bytes are queued and read by Csound's host-MIDI callback on the next block.
fun noteOn(channel: Int, pitch: Int, velocity: Int) Note on, 0-based channel; velocity 0 is a note off.
fun noteOff(channel: Int, pitch: Int) Note off, 0-based channel.
fun controlChange(channel: Int, controller: Int, value: Int) A controller, 0-based channel.
fun compileNow(orchestra: String): CompileResult Compile orchestra into the running Csound now — a new or redefined instr — and say what Csound said. Takes the perform lock, so it waits for the current block and holds the next one for as long as the compile takes (milliseconds). Problems' lines are lines of orchestra.
fun scoreNow(score: String): CompileResult Read score text into the running Csound now: its events start relative to the current score time.
data class CompileResult(val ok: Boolean, val problems: List<Problem>, val printed: List<String>) What compileNow or scoreNow did.
fun declareAudioInput(name: String) Declare a-rate input channel name: chnget "name" into an a-variable reads what CsoundNode feeds it from a Field signal. The first block reads ksmps frames of silence: that is the input's latency, one control period, so that a render of any length always has a whole block of input ready.
fun feedAudio(name: String, samples: FloatArray, n: Int) Push n frames of samples into a-rate input channel name. The rendering thread's call.
fun declareAudioOutput(name: String) Declare a-rate output channel name: what the orchestra chnsets into it is captured every block, and pullAudio (or audioSignal) reads it back in step with render.
fun pullAudio(name: String, out: FloatArray, n: Int) Read n frames captured from a-rate output channel name into out. The rendering thread's call.
fun table(number: Int): DoubleArray? The values of function table number (an f statement or ftgen), or null when there is none.
fun onMessage(callback: (String) -> Unit): Closeable Call callback with every whole line Csound prints, except its routine end-of-performance chatter (isRoutineChatter), which only console keeps. Delivered by poll.
val console: List<String> The most recent lines Csound printed, oldest first (at most 200).
fun poll(): Int Hand every line Csound has printed since the last poll to onMessage callbacks, on this thread.
fun render(outL: FloatArray, outR: FloatArray, numFrames: Int, inL: FloatArray? = null, inR: FloatArray? = null) Render numFrames of Csound's output into outL and outR. The owning thread's call.
fun renderOffline(seconds: Double? = null, maxSeconds: Double = 600.0, chunk: Int = 1024): Pair<FloatArray, FloatArray> Render without an audio device: seconds of output, or — when null — until the score ends (capped at maxSeconds). Messages are polled as it goes.
fun renderToWav(file: File, seconds: Double? = null): File renderOffline into a 16-bit stereo .wav. Returns the file.
fun node(input: StereoRenderable? = null, audioIn: Map<String, afield.audio.synth.Signal> = emptyMap()): CsoundNode This Csound as a node in Field's audio graph, optionally fed by input.
override fun close()
val isClosed: Boolean
override fun toString(): String

CsoundCompileError

class CsoundCompileError(message: String, val problems: List<CsoundInstance.Problem>, val printed: List<String>)

The orchestra or score did not compile. problems carry what Csound said, placed on lines.

CsoundNode

class CsoundNode(val csound: CsoundInstance, private val input: StereoRenderable? = null, audioIn: Map<String, afield.audio.synth.Signal> = emptyMap()) : StereoNode()

A CsoundInstance in Field's audio graph. A StereoNode because Csound is stateful: the node's per-block cache keeps a graph that reuses it from performing twice. Whatever thread renders the graph — the audio thread, live — becomes the thread that runs Csound's perform loop, and the only thread that touches its audio.

Declaration Summary
val csound: CsoundInstance
fun connect(name: String, signal: afield.audio.synth.Signal) Feed signal into a-rate channel name from the next block on, replacing what fed it before. Safe from any thread; the signal is rendered on the audio thread, as part of this node's block.
fun disconnect(name: String) Stop feeding channel name; the orchestra reads silence there.
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
val outL: afield.audio.synth.Signal Csound's left outs as a Field signal.
val outR: afield.audio.synth.Signal Csound's right outs as a Field signal.
fun audioOut(name: String): afield.audio.synth.Signal The a-rate channel name the orchestra writes (chnset asig, "name"), as a Field signal. One signal per name: asking twice gives the same one.
override fun toString(): String

CsoundLanguage.kt

Csound as a box Main Language: Run, Launch and Alt-Enter on .csd text, with opcode completion and error lines mapped back into the box.

src/jvmMain/kotlin/afield/audio/csound/CsoundLanguage.kt — read the source

Top-level functions

Declaration Summary
fun installCsoundLanguage()
fun installCsoundLanguagePlugin()
fun installCsoundLineNotes() Line notes for a Csound box. Registered late, so it is the newest provider for a Csound box's code.
fun csoundLineNote(doc: CsdDocument, line: Int, playing: CsoundInstance?): LineNotes.Note? The note for 0-based box line line, or null. Everything here is read from the text, except a channel's value, which is read from the Csound that is playing (and marked volatile):
fun csoundTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isCsoundRoot: Boolean?

CsoundExecution

class CsoundExecution(val inside: Box) : BoxExecution

Csound as a box language: set a box's Main Language to Csound and its children hold .csd text — <CsOptions>, <CsInstruments>, <CsScore> — or a bare orchestra.

Declaration Summary
override val languageName: String
class Running(val csound: CsoundInstance, val node: CsoundNode, val document: CsdDocument, private val closeables: List<Closeable>) : Closeable What a launch holds: the instance, its audio node and registration, and the document it came from.
var running: Running? The Csound playing this box, or null when it is stopped.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
sealed class Dispatch What Alt-Enter did, for the box's output and for tests.
fun dispatchFor(whole: String, fragment: String, line: Int): Pair<Dispatch, String> Decide what Alt-Enter on fragment, starting at 0-based box line line of whole, should do, and the text to send. Pure: no Csound needed, which is what makes it testable on its own.
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
data class Row(val text: String, val kind: String, val tail: String, val documentation: String, val category: String = "") One completion row.
fun completions(code: String, position: Int): Pair<Int, List<Row>>? The rows for the cursor at position in code, and where the replaced word starts.

PdInstance.kt

A running libpd instance as a Field object: PdInstance opens patches, renders them offline or as a PdNode in a synth graph, and passes messages in and out.

src/jvmMain/kotlin/afield/audio/pd/PdInstance.kt — read the source

PdInstance

class PdInstance(val sampleRate: Int = 48_000, val inputs: Int = 0, val outputs: Int = 2) : Closeable

Pure Data inside Field: one running Pd, with its own patches, its own [dac~] and its own [s](workspace.md#stdlib-collections-kt--s)/[r](workspace.md#stdlib-collections-kt--r) names.

Declaration Summary
val sampleRate: Int
val inputs: Int
val outputs: Int
data class Message(val receiver: String, val selector: String, val args: List<Any>) A message Pd sent to Field. selector is bang, float, symbol, list, or a message name.
val blockSize: Int Pd renders in blocks of this many frames — 64 in every libpd build.
var framesRendered: Long Frames rendered so far; Pd's own clock, in samples.
data class Midi(val kind: String, val channel: Int, val a: Int, val b: Int) A MIDI event a patch sent with noteout, ctlout, pgmout or bendout.
fun onMidi(callback: (Midi) -> Unit): Closeable Call callback with every MIDI event the patch sends. Delivered by poll.
fun noteOn(channel: Int, pitch: Int, velocity: Int) A note to the patch's notein: 0-based channel, velocity 0 for a note-off. Queued.
fun controlChange(channel: Int, controller: Int, value: Int) A controller to the patch's ctlin. Queued.
fun programChange(channel: Int, value: Int) A program change to pgmin. Queued.
fun pitchBend(channel: Int, value: Int) A pitch bend, -8192 to 8191, to bendin. Queued.
fun openPatch(file: File, source: String? = null): PdPatch Open a .pd file. Abstractions and soundfiler paths resolve relative to its folder.
fun openPatchText(text: String, name: String = "inline", source: String = text): PdPatch Open a patch written as text — the contents of a .pd file.
val patches: List<PdPatch> The patches open in this Pd.
fun send(receiver: String, value: Number) Send a number to [r receiver]. Queued; delivered at the next block or flush.
fun bang(receiver: String) Send a bang to [r receiver].
fun symbol(receiver: String, symbol: String) Send a symbol to [r receiver].
fun list(receiver: String, vararg items: Any) Send a list of numbers and symbols to [r receiver].
fun message(receiver: String, selector: String, vararg args: Any) Send selector args… to [r receiver] — a Pd message like set 3 or, to Pd itself, message("pd", "dsp", 0).
fun flush() Deliver queued messages now, then poll. For when nothing is calling render.
fun receive(name: String, callback: (Message) -> Unit): Closeable Call callback with every message sent to [s name] in any patch of this Pd.
fun onPrint(callback: (String) -> Unit): Closeable Call callback with every whole line Pd prints — print, errors, warnings. Delivered by poll.
fun lastValue(name: String): Message? The last message seen on each received name, safe to read from any thread.
val console: List<String> The most recent lines Pd printed, oldest first (at most 200).
fun poll(): Int Hand every message Pd has sent since the last poll to its callbacks, on this thread.
fun arraySize(name: String): Int The length of Pd array name (a [table name] or an #X array), or -1 when there is none.
fun readArray(name: String, offset: Int = 0, count: Int = -1): FloatArray Read count values of array name from offset. Throws when the array is missing or too short.
fun writeArray(name: String, values: FloatArray, offset: Int = 0) Write values into array name from offset. Throws when the array is missing or too short.
fun resizeArray(name: String, size: Int) Resize array name to size values.
fun render(outL: FloatArray, outR: FloatArray, numFrames: Int, inL: FloatArray? = null, inR: FloatArray? = null) Render numFrames of Pd's output into outL and outR. The owning thread's call.
fun renderOffline(seconds: Double, chunk: Int = 512): Pair<FloatArray, FloatArray> Render seconds of this Pd without an audio device, delivering Pd's messages to their callbacks as it goes. The whole path a test or a documentation figure needs.
val node: PdNode by lazy This Pd as a node in Field's audio graph. See PdNode.
override fun close() Close every patch, free the Pd and its hooks. Safe to call twice.
val isClosed: Boolean
override fun toString(): String

PdPatchError

class PdPatchError(message: String, val problems: List<PdPatch.Problem>) : RuntimeException(message)

Pd could not open a patch at all. problems carries what it said.

PdPatch

class PdPatch internal constructor(val instance: PdInstance, internal val handle: MemorySegment, val dollarZero: Int, val file: File, val source: String) : Closeable

One open patch in a PdInstance.

Declaration Summary
val instance: PdInstance
val dollarZero: Int
val file: File
val source: String
data class Problem(val message: String, val line: Int) Something Pd said while opening the patch, placed on the line of source it is about when that can be told. line is 0-based within source; -1 when it cannot be placed.
var problems: List<Problem> What Pd complained about while opening this patch — objects it could not create, mostly.
override fun close() Close this patch; the rest of its instance keeps running.
override fun toString(): String

PdNode

class PdNode(val pd: PdInstance, private val input: afield.audio.synth.StereoRenderable? = null) : StereoNode()

A PdInstance in Field's audio graph: audio(pd.node) plays it.

Declaration Summary
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

PdLanguage.kt

Pd as a box Main Language: PdExecution runs a child's code as a patch and a selected line as a Pd message, and installPdLanguage registers it.

src/jvmMain/kotlin/afield/audio/pd/PdLanguage.kt — read the source

Top-level functions

Declaration Summary
fun parsePdMessageLine(line: String): Triple<String, String, List<Any>>? Read one message line — receiver selector-or-value args… — as PdExecution sends it. A trailing ; and a leading one are both allowed, as in a Pd message box. Numbers become floats, the rest symbols. freq 440 is a float; go bang a bang; voice set 3 two the message set 3 two.
fun installPdLanguage()
fun installPdLanguagePlugin()
fun installPdLineNotes() Line notes for a Pd box: what an object is wired to, and the last value through an [s](workspace.md#stdlib-collections-kt--s) or [r](workspace.md#stdlib-collections-kt--r). Registered late so it is the newest provider for code among those that take a Pd box.
fun pdLineNote(graph: PdPatchGraph, line: Int, playing: PdInstance?): LineNotes.Note? The note for 0-based source line of a patch, split the way every language's notes are: a value line with its results in bold, over a muted meta line.
fun pdTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isPdRoot: Boolean?

PdExecution

class PdExecution(val inside: Box) : BoxExecution

Pure Data as a box language: set a box's Main Language to Pd and its children hold patches.

Declaration Summary
override val languageName: String
var running: PdLaunchState? The Pd playing this box's patch, or null when it is stopped.
class PdLaunchState(val pd: PdInstance, val patch: PdPatch, val graph: PdPatchGraph, private val closeables: List<Closeable>) : Closeable One run's Pd, audio registration and listeners; closed as a unit.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)

PdNative.kt

Finds the optional libpd native library (PdLibrary) and binds its C functions for PdInstance.

src/jvmMain/kotlin/afield/audio/pd/PdNative.kt — read the source

PdLibrary

object PdLibrary

Where libpd is, and what to do when it is not there.

Declaration Summary
val fileName: String The platform's file name for the multi-instance libpd.
val searchedPaths: List<Path> Every path resolvedPath tries, in order — quoted in the "not built" error.
val resolvedPath: Path? Not cached: building it during a live session should be picked up by the next box run.
val isAvailable: Boolean True when a libpd is where searchedPaths looks.
val HOW_TO_BUILD: String The one instruction a missing library needs.
fun describe(): String One line for a box or a test to print.

PdObjects.kt

The catalog of object names the built libpd can create, used by PdObjects.suggest to correct a mistyped object box.

src/jvmMain/kotlin/afield/audio/pd/PdObjects.kt — read the source

PdObjects

object PdObjects

The objects this Pd can create, and the nearest of them to one it could not.

Declaration Summary
const val FILE_NAME The file catalog reads, beside the library.
fun catalog(): Set<String> The names in the catalog beside the resolved libpd; empty when there is none.
fun suggest(text: String, known: Set<String> = catalog()): List<String> What text — an object box's contents, osc 440 — probably meant, as whole corrected texts (osc~ 440); empty when nothing in known is close.
fun hint(text: String, known: Set<String> = catalog()): String "did you mean [osc~ 440]?" for text, or "" when suggest has nothing.

PdPatchGraph.kt

Reads Pd patch text into a graph of objects and connections without running Pd, and draws it into a box with showPdPatch.

src/jvmMain/kotlin/afield/audio/pd/PdPatchGraph.kt — read the source

Top-level functions

Declaration Summary
fun Box.showPdPatch(graph: PdPatchGraph, channel: String = "patch") Draw graph in this box's channel output, labelled, without stalling the thread that asks.

PdPatchGraph

class PdPatchGraph private constructor(val objects: List<Node>, val connections: List<Connection>)

A Pd patch read as a graph: its objects, where they sit, and what is wired to what.

Declaration Summary
val objects: List<Node> The top-level objects, in Pd's index order.
val connections: List<Connection> The top-level connections.
data class Node(val index: Int, val kind: String, val x: Int, val y: Int, val text: String, val line: Int) One object. kind is obj, msg, floatatom, symbolatom, listbox, text or pd for a subpatch; text is what Pd shows in its box (osc~ 440); line is 0-based in the source.
data class Connection(val from: Int, val outlet: Int, val to: Int, val inlet: Int, val line: Int) #X connect from outlet to inlet, with the 0-based source line it was written on.
fun objectOnLine(line: Int): Node? The object written on source line, or null.
fun connectionOnLine(line: Int): Connection? The connection written on source line, or null.
fun into(index: Int): List<Connection> Connections arriving at object index.
fun outOf(index: Int): List<Connection> Connections leaving object index.
val sendNames: Set<String> Every name sent to by an [s](workspace.md#stdlib-collections-kt--s) in the patch.
val receiveNames: Set<String> Every name listened on by an [r](workspace.md#stdlib-collections-kt--r) in the patch.
fun describeLine(line: Int, lastValue: (String) -> PdInstance.Message? = { null }): String? A sentence about source line, for a line note; null for a line that says nothing.
fun ports(index: Int): Pair<Int, Int> How many inlets and outlets object index shows: the larger of what Pd's class is known to have (KNOWN_PORTS, the commonest vanilla classes) and one more than the highest one wired in this patch. The text alone cannot say more — an abstraction's ports are in another file — so an unknown object with nothing wired shows one of each.
fun toFLines(labels: Boolean = true, charWidth: Double = 7.0, boxHeight: Double = 18.0): List<FLine> The patch drawn as Pd draws it — a box per object at its own coordinates, its inlets as tabs along the top edge and its outlets along the bottom, spread across the box's width with the first at the left edge and the last at the right, and a wire per connection from the outlet it leaves to the inlet it enters — as FLines for Box.lines(...). Pd's y axis grows downward, as Field's 2D outputs do, so coordinates carry over unchanged.

AudioReturnValue.kt

The step between a box whose last expression is a patch and a patch you can hear.

audio { … } builds an AudioPatchSpec and stops there, holding no device and making no sound. This file is the other half: a return-value handler that recognises an AudioPatchSpec, a bare Signal or a StereoRenderable coming back from a box, registers it with FieldAudioEngine so it joins the mix, and unregisters it when the box stops. The two bare forms are wrapped with AudioPatchSpec.from first, so returning an oscillator and returning a patch built around one reach the same place.

What the handler launches is a coroutine that mostly yields. Being cancellable is the work: it is registered as one of the box's jobs, it re-checks the box's run generation on every pass, and it closes the engine handle in a finally. A re-run therefore silences the previous patch instead of stacking a second copy of it on top.

The same loop redraws the box's inline output. When box execution has marked the returned value's line as an inline summary target, the loop pushes the patch's summaryHtml, a waveform and a spectrum drawn from its own rolling trace, back to the editor twelve times a second, beginning 120 ms after the launch.

src/jvmMain/kotlin/afield/audio/synth/AudioReturnValue.kt — read the source

Top-level functions

Declaration Summary
fun registerAudioReturnValue() Teaches box execution about AudioPatchSpec, Signal and StereoRenderable. Called once during return-value registration, not from box code.

Buf.kt

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.

src/jvmMain/kotlin/afield/audio/synth/Buf.kt — read the source

Buf

object Buf

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

Declaration Summary
val SPECIES: VectorSpecies<Float> FloatVector.SPECIES_PREFERRED: the shape this JVM prefers on the CPU it is running on.
val LANES: Int Floats per vector, SPECIES.length(). The stride of every vectorised loop below.
fun fill(dst: FloatArray, value: Float, off: Int = 0, len: Int = dst.size - off) Fill dst with a constant value.
fun clear(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) Zero the buffer.
fun copy(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) Copy src into dst.
fun add(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) dsti -= srci
fun mul(dst: FloatArray, src: FloatArray, off: Int = 0, len: Int = dst.size - off) dsti *= srci
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 addScalar(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) dsti *= scalar
fun fma(dst: FloatArray, a: FloatArray, b: FloatArray, c: FloatArray, off: Int = 0, len: Int = dst.size - off) dsti = ai * bi + ci (FMA)
fun scaleAndAdd(dst: FloatArray, scalar: Float, addend: FloatArray, off: Int = 0, len: Int = dst.size - off) dsti = dsti * scalar + addendi
fun clamp(dst: FloatArray, lo: Float, hi: Float, off: Int = 0, len: Int = dst.size - off) dsti = clamp(dsti, lo, hi)
fun abs(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) dsti = |dsti|
fun peak(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float Peak absolute value.
fun rms(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float RMS (root-mean-square) level.
fun sum(src: FloatArray, off: Int = 0, len: Int = src.size - off): Float Sum all elements.
fun hann(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) Apply a Hann window in-place.
fun hannWindow(dst: FloatArray, off: Int = 0, len: Int = dst.size - off) Generate a Hann window into dst.
fun crossfade(dst: FloatArray, a: FloatArray, b: FloatArray, mix: Float, off: Int = 0, len: Int = dst.size - off) dsti = ai * (1-mix) + bi * mix (cross-fade)

CsoundBox.kt

The box-facing side of Csound: an orchestra and a score as a sound source, with control channels both ways.

val cs = csound("""
chn_k "freq", 1, 2, 220, 55, 880
instr 1
  kf chnget "freq"
  a1 vco2 0.1, kf
  outs a1, a1
endin
""", "i1 0 z")
csoundControls(cs)
cs

Returning the CsoundLaunch as the box's last expression is what makes it sound: it joins FieldAudioEngine as a afield.audio.csound.CsoundNode, so Csound's perform loop runs on the audio thread and nowhere else, and stopping the box stops the sound and frees the Csound. What Csound prints goes to the box's output; a compile error is marked on the box's own line.

Every launch is its own Csound, so two boxes can both use chnget "freq" without hearing each other. libcsound is an optional native library; see CsoundLibrary.

src/jvmMain/kotlin/afield/audio/synth/CsoundBox.kt — read the source

Used in: Csound

Top-level functions

Declaration Summary
fun Box.csound(orchestra: String, score: String = "f0 z", gain: Float = 1f, printToBox: Boolean = true, input: StereoRenderable? = null, ksmps: Int = 32, audioIn: Map<String, Signal> = emptyMap()): CsoundLaunch Compile an orchestra and score, ready to play.
fun Box.csoundOffline(orchestra: String, score: String, seconds: Double? = null, sampleRate: Int = 48_000, wav: File? = null, channel: String = "main", maxPoints: Int = 2400, before: (CsoundInstance) -> Unit = {}): Pair<FloatArray, FloatArray> Render an orchestra and score with no audio device and put the result in this box's output: a player on "$channel-audio" and a waveform on channel. Optionally writes a .wav too.
fun csoundChannels(orchestra: String): List<CsoundChannel> The chn_k "name", imode, itype, idflt, imin, imax declarations in orchestra. A channel declared without a range gets 0..1.
fun Box.csoundControls(launch: CsoundLaunch, channel: String = "controls"): Int A slider for every input chn_k channel the orchestra declares, with Csound's own default and range, writing that channel as it moves. The orchestra is the one place the range is written; this is pdControls for Csound. Each channel starts at its declared default.
fun registerCsoundLaunchReturnValue() Teach Field that a CsoundLaunch is something a box can be running: it joins the audio engine, the loop that holds the box open polls Csound's messages every frame on the main thread, and stopping the box removes it from the engine and frees the Csound.
fun Box.csoundOf(name: String): afield.audio.csound.CsoundExecution.Running The playing Csound of the Csound box named name in this box's document, so Kotlin can talk to it: csoundOf("drone").csound.setChannel("cutoff", 900).
fun renderSignalsOffline(seconds: Double, vararg signals: Signal, sampleRate: Int = 48_000, block: Int = 256): List<FloatArray> Render signals side by side, offline, in block-frame audio blocks — what the audio thread would hear, without a device. All of them advance through the same cycles, so a graph they share (a afield.audio.csound.CsoundNode and the signals taken from it) is rendered once per block, as live.

CsoundUnavailable

class CsoundUnavailable(message: String) : IllegalStateException(message)

libcsound has not been built; the message says how to build it.

CsoundLaunch

class CsoundLaunch internal constructor(val csound: CsoundInstance, val orchestra: String, val gain: Float, private val printBox: Box?, private val input: StereoRenderable? = null, private val audioIn: Map<String, Signal> = emptyMap()) : Closeable

A running Csound, and the handle a box holds it by.

Declaration Summary
val csound: CsoundInstance
val orchestra: String
val gain: Float
val node: afield.audio.csound.CsoundNode by lazy What joins the audio engine.
fun audioOut(name: String): Signal The a-rate channel the orchestra chnsets into name, as a Field signal.
val outL: Signal Csound's left and right outs as Field signals.
val outR: Signal
fun setString(name: String, value: String) Write string channel name.
fun getString(name: String): String Read string channel name.
fun onOutvalue(name: String, callback: (Any) -> Unit): Closeable Call callback on the main thread with what outvalue "name", … sends (a Double or a String).
fun setInvalue(name: String, value: Number) Set what invalue "name" reads.
fun table(number: Int): DoubleArray? Function table number's values.
fun writeTable(number: Int, values: FloatArray) Overwrite function table number; the next block reads it.
fun noteOn(channel: Int, pitch: Int, velocity: Int) A note on to massigned instruments, 0-based channel.
fun noteOff(channel: Int, pitch: Int) A note off, 0-based channel.
fun set(name: String, value: Number) Write control channel name; chnget "name" reads it on the next k-cycle.
fun get(name: String): Double Read control channel name, as chnset last left it.
fun event(line: String) A score line, i1 0 1 440, played at the next audio block.
fun event(type: Char, vararg fields: Number) A score line from numbers: event('i', 1, 0, 0.5, 440).
fun compile(orchestra: String) Compile more instruments into the running Csound.
fun every(seconds: Double, block: CsoundLaunch.() -> Unit): Closeable Call block every seconds while the launch plays, on the box's main-thread loop — the place to write a score from Kotlin: cs.every(0.25) { event('i', 1, 0, 0.4, 60) }. The first call is on the first frame. Timing is the main loop's (a frame or so of jitter); for sample-accurate rhythm, put the timing in the score and let Kotlin choose the notes.
fun poll(): Int Deliver what Csound has printed since the last call and run any every blocks that are due. The launch loop does this every frame.
override fun close()
override fun toString(): String

CsoundChannel

data class CsoundChannel(val name: String, val mode: Int, val default: Double, val min: Double, val max: Double)

One chn_k declaration, as csoundChannels reads it.

Declaration Summary
val isInput: Boolean chn_k mode 1 or 3: Csound reads it, so Field may write it.

Dough.kt

dough itself, run as WebAssembly inside GraalVM, and the two wrappers that hand its output to Field's audio engine.

DoughEngine loads dough.wasm into a polyglot context and drives it through a small JavaScript shim: events go in as slash-separated key/value paths, the module's dsp() is called one 128-frame block at a time, and the samples are read back out of wasm memory. This is dough's own voice allocator, event parser, filters and effects rather than a reimplementation of them. DoughDsp.kt is the separate Kotlin port of a handful of dough's primitives, and the two paths never meet.

The engine does not know where its own buffers are until the module tells it. dough_init calls back into js_init once with a query-string-shaped mirror of its pointers, which is where event_input, output, frame, framebuffer, pcm and active_voices all come from. A pointer the module does not report is not an error: currentFrame and activeVoices answer null, and framebufferCopy answers an empty array.

Two properties shape everything built on this. A polyglot context holding js admits one thread, so whichever thread constructs a DoughEngine is the only one that may call any of its methods, activeVoices included; GraalVM refuses a second thread rather than serialising it. And the shim imports nothing but the memory, js_panic and js_init, so the guest has no host clock and no host randomness: the same events and the same number of renderBlock calls give the same floats, run after run, with no tolerance needed to compare two runs.

DoughAudioSink is the engine wrapped as a StereoNode, with commands queued rather than applied so that a caller never reaches the engine itself, and at most maxCommandsPerRender of them drained per block. It renders in whole 128-frame blocks and copies only the frames it was asked for, so a request that is not a multiple of 128 drops the tail of its last block, which is a gap in the sound rather than a delay in it; FieldAudioEngine's 512-frame default is a multiple of 128 and loses nothing. parkDough registers a sink with that engine and returns a ParkedDough closing both.

Read those two against the thread rule above: the sink builds its engine in its own constructor, on whichever thread called it, and then renders it from the audio thread, and neither the sink nor parkDough has a caller in the tree. The realtime path that does run is PatSpatialRoom's, which builds each engine inside the thread that will render it, one engine per spatial source, since an engine is built with a single orbit and cannot keep simultaneous sources apart on its own.

src/jvmMain/kotlin/afield/audio/synth/Dough.kt — read the source

Used in: How to make sound in Field

Top-level functions

Declaration Summary
fun testDough() Boot an engine, play one sawtooth note and print eight blocks of it. A hand-run smoke, not a test.
fun parkDough(wasmPath: Path = Paths.get("./src/jvmMain/resources/dough.wasm"), engine: FieldAudioEngine = audioEngine(), gain: Float = 1f, warmUpCycles: Int = 0, maxCommandsPerRender: Int = 256): ParkedDough Build a DoughAudioSink, register it with an audio engine, and hand back the pair.

DoughEngine

class DoughEngine(wasmPath: Path, private val sampleRate: Int = 44_100) : Closeable

dough's wasm module, loaded and ready to be told what to play.

Declaration Summary
fun evaluatePath(path: String) Send one event written as a dough path, as in /dough/play/s/saw/n/60/gain/.3.
fun evaluatePath(path: String, extra: Map<String, Any?>) Send one event written as a dough path, with extra keys added to it or overriding it.
fun evaluate(event: Map<String, Any?>) Send one event as a map, for a caller that already has its keys and values apart.
fun renderBlock(outLeft: FloatArray, outRight: FloatArray) Run the module for one block and de-interleave it into outLeft and outRight.
fun currentTimeSeconds(): Double The module's own clock, in seconds.
fun currentFrame(): Int? The module's frame counter, or null if it never reported where that counter lives.
fun activeVoices(): Int? How many of MAX_VOICES are sounding, or null if the engine did not report the pointer.
fun framebufferCopy(): FloatArray A copy of the module's visualisation framebuffer, or an empty array if it has none.
fun voiceFrames(out: FloatArray): Int Every sounding voice's group and its last output frame, as the module left them after the last renderBlock.
override fun close() Close the polyglot context, releasing the module and the 200 MB of wasm memory it was given.
class JsHost The two callbacks the wasm module imports, exposed to the shim as a host object.

DoughAudioSink

class DoughAudioSink(wasmPath: Path, sampleRate: Int = 44_100, private val maxCommandsPerRender: Int = 256) : StereoNode(), AutoCloseable

A DoughEngine presented as a StereoNode, with its commands queued instead of applied.

Declaration Summary
fun evaluatePath(path: String) Queue a dough path for the next block. Callable from any thread, and it never blocks.
fun evaluate(event: Map<String, Any?>) Queue an event map for the next block. Drained after every queued path command.
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun close()

ParkedDough

class ParkedDough internal constructor(private val sink: DoughAudioSink, private val handle: AudioPatchHandle) : AutoCloseable

A running dough engine and its registration with FieldAudioEngine, as one thing to close.

Declaration Summary
fun evaluatePath(path: String)
fun evaluate(event: Map<String, Any?>)
override fun close()

DoughDsp.kt

Native Kotlin ports of low-level DSP from dough, carrying dough's AGPL-3.0 terms.

Ported from dough.c (https://codeberg.org/uzu/dough), which is licensed AGPL-3.0. These are line-by-line ports, so this file is a derivative work and the licence travels with it.

Within dough.c, the Dattorro reverb (PlateReverb here) is itself noted as adapted from el-visio's MIT-licensed implementation of the algorithm in Jon Dattorro, "Effect Design Part 1: Reverberator and Other Filters" (1997): https://ccrma.stanford.edu/~dattorro/EffectDesignPart1.pdf

This file complements the WASM binding in Dough.kt rather than competing with it: that path runs the real dough engine and nothing here affects it. What follows is the subset of dough's primitives that the rest of afield.audio.synth does not cover.

  • DoughMath — fast float approximations (log2/exp2/pow/sin)
  • PlateReverb — Dattorro plate reverb, mono in, stereo out
  • Phaser — LFO-swept cascaded notches
  • ChamberlinLowpass — the "superdough" 2-pole lowpass and its resonance curve
  • CurvedADSR — time-based ADSR with click-free retrigger
  • SoftDistort — dough's expm1 k-curve soft clipper
  • ZawOsc and PulzeOsc — naive saw and pulse, aliasing included

Skipped as already covered by this package: polyBLEP saw/pulse/square/ tri (Oscillators.kt, in double precision), white/pink/brown noise (same coefficients), the Audio-EQ-Cookbook biquad (Filters.kt), delay (Effects.kt), flanger/chorus (Effects.kt), bit crush + sample-rate reduction (Effects.kt BitCrusher), and lag (Envelopes.kt Slew). The voice allocator, event/mini-notation parser, orbits and scheduler are out of scope — use the WASM engine for those.

The oscillators here live alongside the rest of the port rather than in Oscillators.kt so that the licence attribution above covers one file.

src/jvmMain/kotlin/afield/audio/synth/DoughDsp.kt — read the source

Used in: DoughDSP: hear the primitives

DoughMath

object DoughMath

dough's hand-rolled float math. Cheaper and less accurate than kotlin.math; the inaccuracy is audible and is part of the character of ChamberlinLowpass and Phaser, which recompute coefficients with these on every sample.

Declaration Summary
const val PI_F: Float
fun log2(x: Float): Float Approximate base-2 logarithm via the exponent bits.
fun exp2(x: Float): Float Approximate 2^x: exact power-of-two step from the exponent bits, times a 4th-order polynomial over the fractional part.
fun pow(x: Float, y: Float): Float Approximate x^y. Returns NaN for x < 0, matching dough.
fun expm1(x: Float): Float Approximate e^x - 1.
fun powHalf(x: Float): Float Approximate 0.5^x.
fun pow10(x: Float): Float Approximate 10^x.
fun modPi(x: Float): Float Wraps x into [-PI, PI].
fun parSin(x: Float): Float "Sine" made from a parabola — cheap, ~1e-3 absolute error.
fun parCos(x: Float): Float parSin shifted by a quarter turn.
fun sinFast(x: Float): Float Sine via a Padé approximant — more accurate and costlier than parSin.
fun ftz(x: Float, limit: Float = 1e-15f): Float Flush values below limit to zero — keeps denormals out of feedback paths.
fun lerpCurve(x: Float, y0: Float, y1: Float, exponent: Float): Float Interpolate y0 → y1 over x in [0, 1] with a curve exponent: 0 = linear, > 0 = ease-in, < 0 = ease-out.

PlateReverb

class PlateReverb(private val source: Signal, val preDelay: Param = Param(0.1f), val preFilter: Param = Param(0.85f), val inputDiffusion1: Param = Param(0.75f), val inputDiffusion2: Param = Param(0.625f), val decayDiffusion: Param = Param(0.70f), val decay: Param = Param(0.75f), val damping: Param = Param(0.95f), val mix: Param = Param(0.3f), val scaleToSampleRate: Boolean = true) : StereoNode(), LiveGraphInspectable

Jon Dattorro's plate reverb: a pre-delay and input-diffusion chain feeding a figure-of-eight tank of two cross-coupled halves, each with its own all-pass diffusers and damping lowpass. The stereo image comes from summing seven taps per side at points scattered through the tank, so it is decorrelated rather than merely panned — this is what makes it sound different from FreeVerb and SpringReverb.

Declaration Summary
val preDelay: Param Pre-delay, 0…1 of ~109 ms.
val preFilter: Param Input lowpass coefficient, 0…1 (higher = brighter).
val inputDiffusion1: Param Gain of the first two input all-pass diffusers.
val inputDiffusion2: Param Gain of the second two input all-pass diffusers.
val decayDiffusion: Param Tank all-pass diffusion.
val decay: Param Tank feedback, clamped to 0.99.
val damping: Param Tank lowpass coefficient, 0…1 (higher = brighter tail).
val mix: Param Dry/wet mix (0 = dry, 1 = wet).
val scaleToSampleRate: Boolean
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>

Phaser

class Phaser(val rate: Param = Param(0.5f), val depth: Param = Param(0.5f), val center: Param = Param(1000f), val sweep: Param = Param(600f), val mix: Param = Param(1f)) : SignalProcessor()

Two cascaded notch filters swept by a sine LFO, the second offset 282 Hz above the first. Detuning is exponential (in cents), so the sweep sounds even across the range, and depth widens the notches by lowering their Q rather than by moving them further.

Declaration Summary
val rate: Param LFO rate in Hz.
val depth: Param 0…1; widens the notches by dropping Q from 2.0 to 0.1.
val center: Param Centre frequency of the first notch, in Hz.
val sweep: Param LFO sweep range in cents.
val mix: Param Dry/wet mix (0 = dry, 1 = wet).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

ChamberlinLowpass

class ChamberlinLowpass(val cutoff: Param = Param(1000f), val resonance: Param = Param(0f)) : SignalProcessor()

Two-pole Chamberlin-style state-variable lowpass, in the exact form strudel/superdough uses. Distinct from SVF (trapezoidal, zero-delay feedback) in both topology and, more audibly, in the resonance curve: damping is 0.5^((resonance + 0.125) / 0.125), so the useful range is squeezed into the top of the resonance knob and it snarls rather than whistles as it approaches self-oscillation.

Declaration Summary
val cutoff: Param Cutoff in Hz.
val resonance: Param 0…1; higher is more resonant.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

CurvedADSR

class CurvedADSR(val gate: Signal = Gate(), val attack: Param = Param(0.01f), val decay: Param = Param(0.1f), val sustain: Param = Param(0.7f), val release: Param = Param(0.3f), val attackCurve: Float = 2f, val decayCurve: Float = 2f) : Signal()

ADSR with separately shaped attack and decay curves.

Declaration Summary
val gate: Signal
val attack: Param
val decay: Param
val sustain: Param
val release: Param
val attackCurve: Float Attack shaping exponent; > 1 eases in.
val decayCurve: Float Decay/release shaping exponent; > 1 eases out.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SoftDistort

class SoftDistort(val amount: Param = Param(1f), val postGain: Param = Param(1f)) : SignalProcessor()

The soft clipper from superdough: y = (1+k)x / (1 + k|x|) with k = e^amount - 1, so amount maps to drive exponentially and the output stays bounded by (1+k)/k however hard it is pushed.

Declaration Summary
val amount: Param Drive; 0 = clean.
val postGain: Param Output gain applied after shaping.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

ZawOsc

class ZawOsc(val freq: Param = Param(440f)) : Signal()

Naive sawtooth — the raw phase ramp, with no band-limiting.

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

PulzeOsc

class PulzeOsc(val freq: Param = Param(440f), val duty: Param = Param(0.5f)) : Signal()

Naive variable-width pulse — a hard comparator on the phase ramp, with no band-limiting. dough calls this pulze; see ZawOsc on why the aliasing is wanted, and PulseOsc for the clean version.

Declaration Summary
val freq: Param
val duty: Param Duty cycle 0…1 (0.5 = square).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

DoughDspSoundDocs.kt

Headless offline sound-render harness for the dough DSP ports in DoughDsp.kt: every audible example on the docs page is produced by running one of those primitives on the CPU.

Each scene in SCENES renders a real graph. A pluck into PlateReverb, a sawtooth through a sweeping Phaser or ChamberlinLowpass, an oscillator gated by CurvedADSR, a sine driven through SoftDistort, raw ZawOsc and PulzeOsc tones: none of them is a synthesized or hand-authored waveform. There is no realtime engine, no audio device, no GPU and no Field window. Signals are block-rendered into a FloatArray, the same way DoughDspPlotGallery renders the samples it plots, then written to a 16-bit PCM WAV through WavFloatArrayWriter. The render is deterministic, so a committed docs .wav regenerates byte-stably at docs-build.

This is the default renderer for the audio="…" docs-capture mode (see public/docs_hooks.py). A fence can name a different one with audio-renderer=; CaSoundDocs and FaustPolySoundDocs are two harnesses of this shape that other pages reach that way.

The public building blocks are the top-level dough* helpers below: doughRenderMono, doughRenderStereoMono, doughRenderGated and doughPluckEnv to render, then doughAbsEnvelope, doughShortTimeRms, doughShortTimeBrightness and doughWaveWindow to shape what is plotted. The docs page's runner blocks call those same functions, so the code a reader sees building a primitive and plotting it is the code that ran, and the graph it builds is the graph the audio= WAV was rendered from. afield.audio.synth is auto-imported into every box, so a docs fence calls them unqualified.

As a FIELD_TEST (synth.dough-dsp.sound) it renders every scene to a temp dir and checks the samples it got: a valid WAV header on disk whose declared sample rate and data size agree with the bytes, and then per scene, that the reverb tail decays, that the ADSR rhythm has silent gaps, that the filter sweep gets brighter, that the drive sweep adds harmonics, and that the raw oscillator is periodic. It writes no committed assets while doing that.

Add a sound by appending one Scene to SCENES.

src/jvmMain/kotlin/afield/audio/synth/DoughDspSoundDocs.kt — read the source

Used in: DoughDSP: hear the primitives

Top-level functions

Declaration Summary
fun doughPluckEnv(tau: Float, holdSec: Float): Signal A finite pluck envelope: exp(-t / tau) (tau seconds) hard-gated to zero after holdSec.
fun doughRenderMono(signal: Signal, seconds: Float, sampleRate: Int = DoughDspSoundDocs.SR): FloatArray Offline-render a mono signal for seconds into a FloatArray (no engine).
fun doughRenderStereoMono(node: StereoRenderable, seconds: Float, sampleRate: Int = DoughDspSoundDocs.SR): FloatArray Offline-render a stereo node for seconds and fold L+R to a mono FloatArray.
fun doughRenderGated(voice: Signal, gate: Gate, schedule: List<Pair<Boolean, Float>>, sampleRate: Int = DoughDspSoundDocs.SR): FloatArray Offline-render voice while driving gate through a (open?, seconds) schedule.
fun doughAbsEnvelope(samples: FloatArray, buckets: Int): List<Double> Bucketed abs-peak envelope of samples as a buckets-long list (for showSignals).
fun doughShortTimeRms(samples: FloatArray, windowMs: Double, points: Int, sampleRate: Int = DoughDspSoundDocs.SR): List<Double> Short-time RMS of samples over points windows of windowMs ms.
fun doughShortTimeBrightness(samples: FloatArray, windowMs: Double, points: Int, sampleRate: Int = DoughDspSoundDocs.SR): List<Double> Per-window ratio of first-difference (high-frequency) energy to total energy — a brightness proxy.
fun doughWaveWindow(samples: FloatArray, atSeconds: Float, cycles: Int, freqHz: Float, sampleRate: Int = DoughDspSoundDocs.SR): List<Double> cycles periods of samples at freqHz, starting atSeconds in — for overlaying waveforms.
fun Box.plotDoughSound(name: String) Plot a DoughDspSoundDocs scene into this box. Kept for convenience; the docs page now inlines the real construction instead. Defined as a top-level Box extension in afield.audio.synth (auto-imported) so it resolves against a box's implicit receiver like showSignals(...).

DoughDspSoundDocs

object DoughDspSoundDocs

The catalogue of rendered scenes, the two entry points the docs page calls, and the FIELD_TEST that renders every scene and checks it.

Declaration Summary
class PlotData(val series: List<Pair<String, List<Number>>>, val x: List<Number>? = null, val xTitle: String? = null, val yTitle: String? = null) Named series for the plot, sharing an x (sample index by default, or x).
class Scene(val name: String, val title: String, val blurb: String, val render: () -> FloatArray, val plot: (FloatArray) -> PlotData, val verify: (FloatArray) -> Unit = {}) One audible example: what to render, what to plot from it, and what has to be true of it.
val SCENES: List<Scene>
fun renderWav(name: String, outPath: String) Offline-render scene name and write a mono 16-bit PCM WAV to outPath.
fun plotInto(box: Box, name: String) Plot scene name's behaviour into box's output as a dark-mode plotly line plot.
fun main(args: Array<String>)

DoughLive.kt

Box-level dough: sound("/dough/play/note/60/s/sine/...") from any box, and thisRMS, the level that box's own sounds are making right now.

The paths are dough's, unchanged, so a path copied from a browser demo running dough.wasm plays the same here: DoughEngine runs the same module. One DoughLive serves the whole process. It builds its engine on its own render thread (a DoughEngine admits only the thread that built it) and passes audio to FieldAudioEngine through a short PatSpatialRing, so nothing dough does, booting included, happens on the audio thread.

Every box gets a dough group number, and Box.sound tags the box's events with it. After every block the render thread reads each sounding voice's group and output sample out of the module's memory (DoughEngine.voiceFrames) and keeps a running mean square per group, so Box.thisRMS is measured from what the box's voices actually produced, not estimated from the numbers in its paths.

src/jvmMain/kotlin/afield/audio/synth/DoughLive.kt — read the source

Top-level functions

Declaration Summary
fun doughPathEvent(path: String): Map<String, String> A dough path as the key/value event the module reads: "/dough/play/s/saw/n/60" is {dough=play, s=saw, n=60}.
fun doughPatternEvent(event: PatEvent<String>, cps: Double): Map<String, Any?> A pattern event as the dough event that plays it, for patterns written the Tidal/Strudel way: the event's value names a note or a sound, and its annotations are dough parameters.
fun doughWasmPath(): Path Where dough.wasm is: the checkout's copy when Field runs from the repository root, otherwise the classpath resource, copied out to a temporary file once.
fun Box.sound(path: String, key: String = path, minInterval: Double = 0.125, force: Boolean = false): Boolean Play a dough path from this box, unless the same key played less than minInterval seconds ago.
fun Box.soundNow(path: String): Boolean Play a dough path from this box now, whatever played before: sound with force = true.

Top-level values

Declaration Summary
val Box.thisRMS: Double How loud this box's own dough voices are right now, measured: about 1 for a sine at gain/1, 0.2 for one at gain/0.2, 0 when nothing this box played is still sounding.

DoughLive

class DoughLive internal constructor(private val wasmPath: Path, val sampleRate: Int, val targetFillFrames: Int = 1024, val levelTimeConstant: Double = 0.04) : AutoCloseable

The process-wide realtime dough engine behind Box.sound and Box.thisRMS.

Declaration Summary
val sampleRate: Int
val targetFillFrames: Int Frames the render thread keeps in the ring: the latency it adds.
val levelTimeConstant: Double Time constant of the level meter, in seconds.
val ring The ring the render thread fills and the audio thread drains.
var ready: Boolean True once the engine has booted on the render thread.
var lastError: Throwable? The last error the render thread hit, from booting or from an event the module refused.
var droppedBlocks: Long Blocks rendered and dropped because nothing was draining the ring.
var framesRendered: Long Frames the render thread has rendered (or, with no device draining it, dropped) since it started.
val heardFrame: Long The frame that is being heard now: framesRendered less what is still waiting in the ring.
class PatternVoice internal constructor(private val live: DoughLive, val pattern: PatPattern<String>, val cps: Double, val originFrame: Long, val group: Int?, val gain: Double = 1.0) A pattern play is playing. stop ends it after the block in progress; notes already started finish their own envelopes.
fun play(pattern: PatPattern<String>, cps: Double = 0.5, group: Int? = null, originFrame: Long = framesRendered, gain: Double = 1.0): PatternVoice Play pattern at cps cycles per second until PatternVoice.stop, from cycle 0 at originFrame (now, by default; pass another voice's originFrame to play in step with it).
fun play(path: String, group: Int? = null) Queue one dough path, tagged with group when it is not null.
fun level(group: Int): Double The measured level of group's voices, scaled so a sine at gain 1 reads about 1.
val mixLevel: Double The measured level of every voice together, on the same scale as level.
override fun close()

DoughVoiceMeter

class DoughVoiceMeter(sampleRate: Int, val timeConstant: Double = 0.04, private val publishEvery: Int = 4)

Per-group running mean square of a DoughEngine's voices, one sample per voice per block.

Declaration Summary
var published: Map<Int, Double> Mean square per group, as of the last publish.
var publishedTotal: Double Mean square of every voice together, as of the last publish.
fun afterBlock(engine: DoughEngine) Fold the engine's current voices in. Call once after every DoughEngine.renderBlock.

Effects.kt

The mono effects: delays, reverbs, distortions and dynamics, each one a SignalProcessor that / pipes a signal into.

saw / Delay(0.25.p) is the whole calling convention. Every control is a Param rendered once per sample, so a delay time or a compressor threshold can itself be a signal, and a sweep of one lands sample-accurate rather than once a block. All of them take and return mono; stereo comes after them, from Pan or StereoSignal.

The delay-line readers are not uniform, and it is audible. Delay interpolates between samples with a cubic fit; Chorus and Flanger read the nearest whole sample, so a slow sweep through either of those has a faint stepping to it that Delay does not. Compressor and Limiter also keep a running trace of their own gain reduction, which is what the meters in Visualizers.kt draw.

src/jvmMain/kotlin/afield/audio/synth/Effects.kt — read the source

Used in: Visualizers, Displays, and Little Instruments

Delay

class Delay(val delayTime: Param = Param(0.3f), val feedback: Param = Param(0.5f), val mix: Param = Param(0.5f), val maxDelaySec: Float = 5f) : SignalProcessor()

A feedback delay line, read at fractional sample positions.

Declaration Summary
val delayTime: Param Delay time in seconds.
val feedback: Param Feedback amount (-1…+1).
val mix: Param Dry/wet mix (0 = all dry, 1 = all wet).
val maxDelaySec: Float
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

FreeVerb

class FreeVerb(val roomSize: Param = Param(0.8f), val damping: Param = Param(0.5f), val mix: Param = Param(0.3f)) : SignalProcessor()

Schroeder reverb, following the Freeverb topology: eight comb filters in parallel, each with a one-pole damper in its feedback path, summed and then passed through four allpasses in series.

Declaration Summary
val roomSize: Param
val damping: Param
val mix: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SpringReverb

class SpringReverb(val tension: Param = Param(0.55f), val decay: Param = Param(0.72f), val tone: Param = Param(0.65f), val mix: Param = Param(0.28f)) : SignalProcessor()

A spring tank, built as six short delay lines summed with alternating polarity and then dispersed through four allpasses in series.

Declaration Summary
val tension: Param Overall spring tension / metallic dispersion (0…1).
val decay: Param Feedback tail length (0…1).
val tone: Param Brightness of the spring return (0 = dark, 1 = bright).
val mix: Param Dry/wet mix (0 = dry, 1 = wet).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

ConvolutionReverb

class ConvolutionReverb(val impulse: AudioSample, val mix: Param = Param(0.35f), val gain: Param = Param(1f), val normalize: Boolean = true, val maxIrSamples: Int = 8192) : SignalProcessor()

Convolves the input with an impulse response directly in the time domain, one dot product per output sample.

Declaration Summary
val impulse: AudioSample
val mix: Param Dry/wet mix (0 = dry, 1 = convolved).
val gain: Param Wet signal trim after convolution.
val normalize: Boolean
val maxIrSamples: Int
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Chorus

class Chorus(val rate: Param = Param(1.5f), val depth: Param = Param(0.003f), val mix: Param = Param(0.5f), val voices: Int = 3) : SignalProcessor()

Several taps of one modulated delay line, averaged: voices readers of a 50 ms buffer, each swept by its own LFO at rate and started a fraction of a cycle apart from its neighbours.

Declaration Summary
val rate: Param LFO rate in Hz.
val depth: Param Modulation depth in seconds.
val mix: Param Dry/wet mix (0…1).
val voices: Int Number of chorus voices.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Waveshaper

class Waveshaper(val transferFn: (Float) -> Float, val drive: Param = Param(1f)) : SignalProcessor()

Memoryless distortion: multiply by drive, then push each sample through transferFn on its own.

Declaration Summary
val transferFn: (Float) -> Float Transfer function: input sample → output sample.
val drive: Param Pre-gain (drive).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

RectifierMode

enum class RectifierMode

Which part of the waveform a Rectifier keeps.

Constants: HALF_WAVE, FULL_WAVE, NEGATIVE_HALF.

Rectifier

class Rectifier(val mode: RectifierMode = RectifierMode.FULL_WAVE, val drive: Param = Param(1f), val bias: Param = Param(0f), val mix: Param = Param(1f)) : SignalProcessor()

Folds a waveform against zero: drive and bias first, then the mode's rule, then mix against the dry input.

Declaration Summary
val mode: RectifierMode
val drive: Param
val bias: Param
val mix: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

StagedWaveFolder

class StagedWaveFolder(val drive: Param = Param(2f), val threshold: Param = Param(0.7f), val stageGain: Param = Param(1.35f), val symmetry: Param = Param(0f), val bias: Param = Param(0f), val mix: Param = Param(1f), val makeup: Param = Param(0.8f), val stages: Int = 3) : SignalProcessor()

A wavefolder: anything that leaves the window between two thresholds is reflected back inside, and that happens stages times over with gain in between.

Declaration Summary
val drive: Param
val threshold: Param
val stageGain: Param Additional gain applied between folding stages.
val symmetry: Param Positive values open the positive side and tighten the negative side.
val bias: Param Pre-fold DC offset / bias.
val mix: Param Wet/dry mix.
val makeup: Param Output trim after the final stage.
val stages: Int
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Compressor

class Compressor(val threshold: Param = Param(-12f), val ratio: Param = Param(4f), val attack: Param = Param(0.005f), val release: Param = Param(0.05f), val makeupGain: Param = Param(0f), val kneeWidth: Float = 6f) : SignalProcessor()

A peak compressor with a soft knee: above threshold the level is reduced towards ratio, and attack and release decide how quickly the gain follows.

Declaration Summary
val threshold: Param Threshold in dB.
val ratio: Param Compression ratio (e.g. 4 = 4:1).
val attack: Param Attack time in seconds.
val release: Param Release time in seconds.
val makeupGain: Param Makeup gain in dB.
val kneeWidth: Float Knee width in dB (0 = hard knee).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Limiter

class Limiter(val ceiling: Param = Param(-0.3f), val release: Param = Param(0.05f)) : SignalProcessor()

A brick wall at ceiling dB: gain drops the instant a sample would exceed it and recovers over release.

Declaration Summary
val ceiling: Param Ceiling in dB (e.g. -0.3).
val release: Param Release time in seconds.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

BitCrusher

class BitCrusher(val bits: Param = Param(8f), val downsample: Param = Param(1f)) : SignalProcessor()

Quantises amplitude to bits and holds each sample for downsample samples: the two halves of digital degradation, separated.

Declaration Summary
val bits: Param Effective bit depth (1…32).
val downsample: Param Downsample factor (1 = none, 4 = quarter rate).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Flanger

class Flanger(val rate: Param = Param(0.3f), val depth: Param = Param(0.003f), val feedback: Param = Param(0.7f), val mix: Param = Param(0.5f)) : SignalProcessor()

One short delay line, swept by an internal LFO at rate and fed back on itself.

Declaration Summary
val rate: Param LFO rate in Hz.
val depth: Param Max delay time in seconds.
val feedback: Param Feedback (-1…+1).
val mix: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

RingModulator

class RingModulator(val carrierFreq: Param = Param(300f)) : SignalProcessor()

Multiplies the input by an internal sine at carrierFreq.

Declaration Summary
val carrierFreq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Envelopes.kt

Control shapes: Gate, ADSR, AR, Line and BreakpointEnvelope generate one, Slew limits how fast one can move, and EnvelopeFollower recovers one from a signal that already exists.

Everything here is a Signal rather than a SignalProcessor, so / does not reach it. An envelope is applied by multiplying, SawOsc(220.p) * env, and the two that read a signal take it as a constructor argument instead of through an input. What comes out is a stream of samples like any other, so an envelope can be handed to a filter cutoff or a delay time as readily as to an amplitude.

Times are in seconds throughout, and the gated envelopes watch a signal rather than wait for a method call: ADSR follows the level of its gate and holds at sustain while it is high, AR fires on the rising edge and runs to the end regardless. Every curve argument is an exponent applied to a normalised ramp, so 1 is linear, below 1 starts fast and above 1 starts slow.

src/jvmMain/kotlin/afield/audio/synth/Envelopes.kt — read the source

Used in: Visualizers, Displays, and Little Instruments

Gate

class Gate : Signal()

A gate signal: outputs 1.0 while open, 0.0 while closed. Thread-safe – call open / close from any thread.

Declaration Summary
var isOpen: Boolean
fun open()
fun close()
fun trigger()
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

ADSR

class ADSR(val gate: Signal = Gate(), val attack: Param = Param(0.01f), val decay: Param = Param(0.1f), val sustain: Param = Param(0.7f), val release: Param = Param(0.5f), val curve: Float = 1f) : Signal()

Classic ADSR envelope generator. Driven by a gate signal: - Gate > 0 → begin attack → decay → sustain (hold) - Gate ≤ 0 → begin release from current level

Declaration Summary
val gate: Signal
val attack: Param
val decay: Param
val sustain: Param
val release: Param
val curve: Float Shape curve: 1 = linear, 0.5 = fast-exponential, 2 = slow-log.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
fun retrigger() Retrigger: reset to attack stage (callable from audio thread).

AR

class AR(val gate: Signal = Gate(), val attack: Param = Param(0.01f), val release: Param = Param(0.3f), val curve: Float = 1f) : Signal()

Simplified two-stage envelope. Triggers on rising edge of gate. Runs attack→release once, then stays at zero until re-triggered.

Declaration Summary
val gate: Signal
val attack: Param
val release: Param
val curve: Float
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Line

class Line(val start: Float = 0f, val end: Float = 1f, val duration: Float = 1f) : Signal()

Generates a linear ramp from start to end over duration seconds, then holds at end. Call trigger to restart.

Declaration Summary
val start: Float
val end: Float
val duration: Float
var running
fun trigger()
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Breakpoint

data class Breakpoint(val time: Float, val value: Float, val curve: Float = 1f)

One corner of a BreakpointEnvelope: reach value at time seconds.

BreakpointEnvelope

class BreakpointEnvelope(vararg points: Breakpoint) : Signal()

An envelope of as many segments as you care to give it, each one a Breakpoint naming a time and a level to arrive at.

Declaration Summary
var running
fun trigger()
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

EnvelopeFollower

class EnvelopeFollower(val input: Signal, val attackTime: Param = Param(0.005f), val releaseTime: Param = Param(0.05f), val mode: Mode = Mode.PEAK) : Signal()

Tracks the amplitude envelope of an input signal. Uses separate attack and release smoothing coefficients.

Declaration Summary
val input: Signal
val attackTime: Param
val releaseTime: Param
val mode: Mode
enum class Mode
var env
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Slew

class Slew(val input: Signal, val riseTime: Param = Param(0.01f), val fallTime: Param = Param(0.01f)) : Signal()

Limits how fast a signal can change. riseTime and fallTime are in seconds (time to traverse full range).

Declaration Summary
val input: Signal
val riseTime: Param
val fallTime: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

FM.kt

Frequency-modulation synthesis in two pieces: FMOperator is one modulated sine, and FMPatch wires several of them into the classic algorithms.

An operator is phase modulation rather than true frequency modulation. Its phase advances at freq, and the modulation is added to that phase where the sine is read rather than to the frequency, so it does not accumulate. Operators are connected by assigning to pmInput, and since the modulator is an ordinary Signal the modulation index is a gain in front of it: car.pmInput = mod * Param(3f). feedbackAmount adds an operator's own last sample to its phase, and unlike the rest it is a plain Float fixed at construction rather than a Param.

FMPatch is three builders over that one part, each returning a bare Signal to be enveloped and piped onward like any other source. twoOp is a modulator into a carrier at a frequency ratio, threeOp is a three-operator serial chain, and fourOp takes an algorithm number choosing between a serial chain, two modulators summed into one, two independent stacks mixed, and four carriers added together. Any other number throws.

src/jvmMain/kotlin/afield/audio/synth/FM.kt — read the source

FMOperator

class FMOperator(val freq: Param = Param(440f), val amplitude: Param = Param(1f), val feedbackAmount: Float = 0f) : Signal()

An FM (phase-modulation) operator: sine oscillator with an external phase-modulation input and optional self-feedback.

Declaration Summary
val freq: Param Carrier / operator frequency.
val amplitude: Param Amplitude (useful for stacking operators).
val feedbackAmount: Float Self-feedback amount (0 = none, ~0.7 = metallic, >1 = chaotic).
var pmInput: Signal? External phase-modulation input (set to modulator * index).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

FMPatch

object FMPatch

Convenience builder for classic FM algorithms.

Declaration Summary
fun twoOp(carrierFreq: Float, ratio: Float = 2f, index: Param = Param(2f), feedback: Float = 0f): Signal 2-operator FM: mod → car
fun threeOp(freq: Float, ratios: FloatArray = floatArrayOf(1f, 2f, 3f), indices: FloatArray = floatArrayOf(1f, 1f, 0.5f), feedback: Float = 0f): Signal 3-operator serial chain: op3 → op2 → op1 (carrier)
fun fourOp(freq: Float, ratios: FloatArray = floatArrayOf(1f, 1f, 2f, 3f), indices: FloatArray = floatArrayOf(0f, 1f, 1f, 0.5f), algorithm: Int = 1, feedback: Float = 0f): Signal 4-operator FM with DX7-style algorithm selection.

FaustBox.kt

The box-facing side of Faust: compile a program, play it, show what it declares.

faust is the call that matters. It strips the #faust marker, compiles, instantiates, and returns a FaustLaunch:

faust("""
#faust
import("stdfaust.lib");
freq = hslider("freq [unit:Hz]", 220, 50, 1000, 0.01);
process = os.sawtooth(freq) : fi.lowpass(2, 1200) * 0.2 <: _,_;
""")

Returning that launch as the box's last expression is what ties the sound to the box: it stays lit while the program runs, and stopping the box stops the sound and closes the DSP's GraalVM context. registerFaustLaunchReturnValue is the handler that does both. It exists because the generic audio handler would close the audio patch and leave the context alive, which is one leaked context per box run.

Everything else is a separate call, so a box shows only what it wants to:

  • faustControls turns the program's own hslider, button and hgroup declarations into a Field control group. Nobody writes the sliders, and a control group is what the phone's Group page publishes, so they are reachable from a phone at no extra cost.
  • faustDiagram displays Faust's own SVG block diagram of the program.
  • faustWaveform renders a program offline and plots it. Nothing there touches the audio engine, so it works where there is no audio device at all.
  • faustCompile compiles without playing, for looking at a program's interface or its diagram.

Every line number the compiler reports is shifted back onto the box's own lines before a marker is placed, because the #faust line and any blank lines above it are not part of what the compiler saw. stripFaustHeader removes them and reports how many lines went.

src/jvmMain/kotlin/afield/audio/synth/FaustBox.kt — read the source

Used in: Faust

Top-level functions

Declaration Summary
fun Box.faust(source: String, gain: Float = 1f, controls: Boolean = true, diagram: Boolean = false, input: StereoRenderable? = null, channel: String = "controls", lineOffset: Int = -1): FaustLaunch A box that is a signal processor.
fun Box.faustCompile(source: String): FaustProgram Compile source without running it — for inspecting a program's interface or its diagram.
fun Box.faustControls(launch: FaustLaunch, channel: String = "controls") Turn a Faust program's own UI declaration into a Field control group.
fun Box.faustDiagram(program: FaustProgram, channel: String = "diagram") Show Faust's block diagram for a program.
fun Box.faustWaveform(program: FaustProgram, seconds: Double = 0.02, sampleRate: Int = 48_000, channel: String = "output", maxPoints: Int = 2000) Render a program offline and plot the result.
fun registerFaustLaunchReturnValue() Teach Field that a FaustLaunch is something a box can be running.

FaustLaunch

class FaustLaunch internal constructor(val program: FaustProgram, val dsp: FaustDsp, internal val node: FaustNode, val gain: Float) : Closeable

A running Faust program, and the handle a box holds it by.

Declaration Summary
val program: FaustProgram
val dsp: FaustDsp
val gain: Float
val controls: List<FaustControl> The controls this program declares.
val meters: List<FaustControl> The meters (hbargraph/vbargraph) this program declares.
operator fun set(label: String, value: Double) Set a control by label, address or qualified name. Silently ignores an unknown name.
fun set(control: FaustControl, value: Double) Set a control.
fun meter(label: String): Float Read a meter's most recent value, or 0 for an unknown name.
fun meterValues(): Map<String, Float> Every meter's most recent value, by label.
fun press(label: String, holdMs: Long = 0L) Press and release a button, holdMs apart.
override fun close()
override fun toString(): String

FaustDsp.kt

One compiled Faust program, instantiated and ready to make sound.

A FaustProgram is bytes; FaustDsp is the running thing. It holds its own WebAssembly instance in its own GraalVM context, and render pulls audio out of it a block at a time, which is exactly the shape StereoRenderable wants: a Faust program sits in Field's audio graph next to an oscillator with no adaptation. FaustNode is what puts it there.

It is fast, and the cost is the boundary rather than the DSP

Measured for a sawtooth-through-a-filter voice at 48kHz, where a 128-frame block has 2667µs to render in:

  • compute on its own: 7.6µs, about 350 times realtime. Generated Faust is not the bottleneck.
  • render, end to end through FaustDsp: 60.4µs.

The gap is the JVM↔WebAssembly boundary, and two things keep it as small as that: the output is copied with one bulk Value.readBuffer per channel rather than a sample at a time, and the JS entry points are resolved once at construction rather than per block. What is left is dominated by the per-block Context.enter, which is where to look next if it ever matters. The figures above are one machine's; what synth.faust.smoke holds to is a floor of ten times realtime.

The compiler, by contrast, is slow: see FaustEngine, which keeps it behind a separate context for that reason.

The context is single-threaded, so parameters are queued

A GraalVM context cannot be entered by two threads at once, and render runs on the audio thread. So setParam does not touch WebAssembly at all: it enqueues, and render drains the queue before computing. That is also the right audio-engineering answer, with no lock on the audio thread and no parameter changing halfway through a block.

A queue rather than a last-write-wins map, because a button is momentary: collapsing a press and its release into one value would silently swallow a note.

src/jvmMain/kotlin/afield/audio/synth/FaustDsp.kt — read the source

FaustDsp

class FaustDsp internal constructor(val program: FaustProgram, val sampleRate: Int) : Closeable

One compiled Faust program instantiated in its own GraalVM context, rendering a block at a time.

Declaration Summary
val program: FaustProgram
val sampleRate: Int
val numInputs: Int Channel counts as the compiled module reports them, not as the JSON claimed.
val numOutputs: Int
fun setParam(index: Int, value: Float) Queue a parameter change. Safe from any thread; applied at the start of the next render.
fun setParam(control: FaustControl, value: Double) Queue a change by control, honouring its declared range.
fun meterValue(index: Int): Float The last sampled value of a meter control, or 0 if it has not been rendered yet.
fun meterValues(): Map<Int, Float> Every meter's last sampled value, by control index.
fun render(outL: FloatArray, outR: FloatArray, numFrames: Int, inL: FloatArray? = null, inR: FloatArray? = null) Render numFrames of audio.
override fun close()
override fun toString(): String

FaustNode

class FaustNode(val dsp: FaustDsp, private val input: StereoRenderable? = null) : StereoNode()

A FaustDsp as a node in Field's audio graph.

Declaration Summary
val dsp: FaustDsp
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)

FaustEditorExtension.kt

The #faust editor extension and the vocabulary it completes against.

Both are static resources rather than strings built here. faust.js is served to the browser as the AMD module bithero/faust, which bootstrapMonaco.js lists alongside the other language extensions, and it fetches faust-vocabulary.json itself at install time. This object reads the two files verbatim, so the node probe in FaustEditorExtensionWorkout exercises the code and data that ship rather than a copy of them.

The vocabulary is extracted by tools/generate_faust_vocabulary.py from Faust's standard libraries, which are not on disk: they ship packed inside the vendored compiler's libfaust-wasm.data. Completion therefore offers the thousand-odd functions that the same vendored compiler will resolve import("stdfaust.lib") against, with their real signatures.

src/jvmMain/kotlin/afield/audio/synth/FaustEditorExtension.kt — read the source

Top-level functions

Declaration Summary
fun faustEditorExtensionJavascript(): String FaustEditorExtension.javascript(), as a top-level function.

FaustEditorExtension

object FaustEditorExtension

The two resources, each read on demand and never cached.

Declaration Summary
const val id: String The extension's name in the editor: the #faust marker without its #.
fun javascript(): String The extension's source, faust.js.
fun vocabularyJson(): String The generated completion vocabulary, faust-vocabulary.json.

FaustEngine.kt

The Faust compiler, running inside Field.

Faust is a functional language for signal processing that compiles to efficient code. Field embeds the compiler itself as WebAssembly, in src/jvmMain/resources/faust/ with a PROVENANCE.md beside it, and runs it in the same GraalVM runtime that already hosts dough.wasm. There is nothing to install on any platform: the build that loads is the one committed in the repository, and synth.faust.smoke asserts its version against EXPECTED_LIBFAUST_VERSION.

Two halves, opposite performance

Measured on an aarch64 Mac under GraalVM CE 25.0.2, and the reason the object is shaped the way it is:

  • Generated DSP is fast. compute on a compiled program takes ~7.6µs of the 2667µs a 128-frame block has at 48kHz; end to end through FaustDsp it is ~60µs, and what separates those two figures is the JVM↔WebAssembly boundary rather than Faust. synth.faust.smoke asserts a floor of ten times realtime, so playing is not a performance problem here.
  • The compiler is slow. compile settles at ~600ms once Truffle has warmed, against ~30ms for the same call under Node's V8, and the first compile of a process pays 13–22s of warm-up.

Three things follow:

  1. The compiler gets its own GraalVM context, never shared with a FaustDsp. A 600ms compile holding a lock the audio thread wants every 2.67ms would be audible as a dropout.
  2. warmInBackground pays the 13–22s while Field is starting, rather than at the moment someone first runs a box.
  3. compile is memoised on the hash of (source, arguments), so re-running an unchanged box, or undoing back to a program already seen, costs nothing.

Errors

The vendored compiler is an Emscripten build with exception catching disabled, so every failure arrives as an opaque "Exception catching is disabled" RuntimeError. The real message is only available afterwards, from getErrorAfterException(), and it is well formed: "probe:2 : ERROR : undefined symbol : sawtoothX". FaustCompileError parses that into something an editor marker can use.

src/jvmMain/kotlin/afield/audio/synth/FaustEngine.kt — read the source

Top-level functions

Declaration Summary
fun stripFaustHeader(source: String): Pair<String, Int> Remove a leading #faust marker line, returning the source and the number of lines removed.

FaustEngine

object FaustEngine

Compiles Faust source to a FaustProgram, once per distinct (source, arguments) pair.

Declaration Summary
const val EXPECTED_LIBFAUST_VERSION: String The libfaust build vendored in resources/faust; asserted by the smoke test.
const val DEFAULT_ARGS: String Default compiler arguments.
fun warmInBackground() Start loading and warming the compiler on a background thread.
fun libfaustVersion(): String The libfaust version actually loaded, e.g. 2.86.2. Boots the compiler if needed.
fun compile(name: String, source: String, args: String = DEFAULT_ARGS): FaustProgram Compile Faust source to a WebAssembly module plus its metadata.
fun blockDiagram(name: String, source: String, args: String = DEFAULT_ARGS): Map<String, String> The Faust block diagram for source, as SVG.
fun clearCache() Drop the memoised programs. Used by tests that need a cold-ish measurement.
fun shutdown() Close the compiler and release its context.

FaustProgram

data class FaustProgram(val name: String, val source: String, val wasm: ByteArray, val dspJson: String, val ui: FaustUi)

A compiled Faust program: the WebAssembly module and what the compiler said about it.

Declaration Summary
val name: String
val source: String The source that was compiled, with any #faust header already removed.
val wasm: ByteArray
val dspJson: String
val ui: FaustUi
val inputs: Int
val outputs: Int
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

FaustCompileError

class FaustCompileError(val faustMessage: String, val line: Int?, val raw: String) : Exception(faustMessage)

A Faust compile error, with the line the compiler blamed.

Declaration Summary
val faustMessage: String
val line: Int?
val raw: String

FaustUi

data class FaustUi(val inputs: Int, val outputs: Int, val controls: List<FaustControl>)

What a Faust program declares about its own controls, flattened.

Declaration Summary
val inputs: Int
val outputs: Int
val controls: List<FaustControl> Every control, groups flattened. Alphabetical within a group — see writable.
val writable: List<FaustControl> Controls a person can move.
val meters: List<FaustControl> hbargraph/vbargraph: values the DSP reports back, for meters.

FaustControlType

enum class FaustControlType(val faustName: String)

The Faust control widgets Field knows how to show.

Constants: HSLIDER, VSLIDER, NENTRY, BUTTON, CHECKBOX, HBARGRAPH, VBARGRAPH.

FaustScale

enum class FaustScale

[scale:...] metadata — how a slider's travel maps onto its value.

Constants: LINEAR, LOG, EXP.

FaustControl

data class FaustControl(val type: FaustControlType, val label: String, val groupPath: List<String>, val address: String, val index: Int, val init: Double, val min: Double, val max: Double, val step: Double, val unit: String? = null, val scale: FaustScale = FaustScale.LINEAR, val tooltip: String? = null)

One control declared by a Faust program.

Declaration Summary
val type: FaustControlType
val label: String
val groupPath: List<String>
val address: String
val index: Int
val init: Double
val min: Double
val max: Double
val step: Double
val unit: String?
val scale: FaustScale
val tooltip: String?
val isReadOnly: Boolean A bargraph is the DSP talking to the user, not the other way round.
val isMomentary: Boolean
val isToggle: Boolean
val qualifiedLabel: String Label with its groups, e.g. filter/cutoff, for a flat control surface.
val displayLabel: String Label with its unit, e.g. cutoff (Hz).

FaustPattern.kt

A pattern plays a Faust program.

The join between two things that each need nothing added to them. Field's pattern language, mini-notation over the Pat algebra in rational cycle time, says when and what pitch. A compiled Faust program is one always-on DSP with sliders. This file is small because neither side is modified and nothing is recompiled.

The whole conversion is one function, faustNotesForArc: a pattern event's rational arc becomes an integer frame on the bank's own clock, and its atom becomes a frequency in Hz. Everything after that is integers, which is why the timing can be asserted in samples.

src/jvmMain/kotlin/afield/audio/synth/FaustPattern.kt — read the source

Used in: Faust

Top-level functions

Declaration Summary
fun faustNotesForArc(pattern: PatPattern<String>, arc: PatArc, cps: Double, sampleRate: Int, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double = 1.0, frameOrigin: Long = 0L): FaustPatternNotes Turn one query of pattern into notes on a frame clock.
fun faustPatternNotes(pattern: PatPattern<String>, cycles: Int, cps: Double = 1.0, sampleRate: Int = 48_000, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double = 1.0): FaustPatternNotes Every note in cycles whole cycles of pattern, from frame 0.
fun faustParamAutomation(values: PatPattern<Double>, control: FaustControl, arc: PatArc, cps: Double, sampleRate: Int, frameOrigin: Long = 0L): List<FaustParamChange> Turn a pattern of numbers into shared-parameter changes for control.
fun faustParamAutomation(values: PatPattern<Double>, control: FaustControl, cycles: Int, cps: Double = 1.0, sampleRate: Int = 48_000): List<FaustParamChange> faustParamAutomation over cycles whole cycles from frame 0.
fun faustPatternRender(program: FaustProgram, pattern: PatPattern<String>, cycles: Int, cps: Double = 1.0, voices: Int = 8, sampleRate: Int = 48_000, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double? = null, mixGain: Double = 1.0, blockFrames: Int = 128, tailSeconds: Double = 0.25, automation: List<FaustParamChange> = emptyList(), stealReleaseFrames: Long = FaustVoiceBank.RETRIGGER_FRAMES): FaustPatternRender Render cycles cycles of pattern through a polyphonic bank of program, offline.
fun faustPatternRender(source: String, pattern: PatPattern<String>, cycles: Int, cps: Double = 1.0, voices: Int = 8, sampleRate: Int = 48_000, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double? = null, name: String = "faustpoly", stealReleaseFrames: Long = FaustVoiceBank.RETRIGGER_FRAMES): FaustPatternRender faustPatternRender from Faust source, compiling it first.
fun faustEnvelope(x: FloatArray, sampleRate: Int, releaseMs: Double = 30.0): FloatArray A peak-following envelope: instant attack, exponential release.
fun faustOnsets(x: FloatArray, sampleRate: Int, openFraction: Double = 0.02, closeFraction: Double = 0.002, minGapMs: Double = 3.0): List<Int> The frame of every note attack in x.
fun faustSilentGaps(x: FloatArray, sampleRate: Int, threshold: Double = 1e-4, minMs: Double = 3.0): List<IntRange> Maximal runs of near-silence in x, as first..last frame ranges.
fun faustFundamentalHz(x: FloatArray, from: Int, count: Int, sampleRate: Int, minHz: Double = 40.0, maxHz: Double = 3000.0, octaveTolerance: Double = 0.92): Double The fundamental of x[from until from+count], in Hz, by normalised autocorrelation.
fun faustNormalisedStep(x: FloatArray, from: Int, count: Int): Double The largest sample-to-sample step in x[from until from+count], as a fraction of the window's own peak.
fun faustSpectrumPeaks(x: FloatArray, from: Int, size: Int, sampleRate: Int, minHz: Double = 20.0, maxHz: Double = 20_000.0, relThreshold: Double = 0.1): List<FaustPeak> The spectral peaks of x[from…], Hann-windowed, in the band [minHz, maxHz].
fun Box.faustPoly(source: String, voices: Int = 8, gain: Float = 1f, mixGain: Double = 1.0, controls: Boolean = true, channel: String = "controls", stealReleaseFrames: Long = FaustVoiceBank.RETRIGGER_FRAMES, lineOffset: Int = -1): FaustPolyLaunch A box that is a polyphonic instrument.
fun Box.faustPolyControls(launch: FaustPolyLaunch, channel: String = "controls") Build the control group for a polyphonic launch: the shared controls only.
fun Box.showFaustPattern(source: String, pattern: PatPattern<String>, cycles: Int = 2, cps: Double = 1.0, voices: Int = 8, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double? = null, automate: Map<String, PatPattern<Double>> = emptyMap(), stealReleaseFrames: Long = FaustVoiceBank.RETRIGGER_FRAMES, channel: String = "main", maxPoints: Int = 2400): FaustPatternRender Render a pattern through a polyphonic Faust program and put it in this box's output — a player, a waveform, and the note count.
fun registerFaustPolyLaunchReturnValue() Teach Field that a FaustPolyLaunch is something a box can be running.

Top-level values

Declaration Summary
const val FAUST_PATTERN_SUSTAIN: Double The default fraction of its arc a note holds its gate for.

FaustPatternNotes

data class FaustPatternNotes(val notes: List<FaustNote>, val unpitched: Int)

The notes a pattern query produced, and how many of its events had no pitch to play.

Declaration Summary
val size: Int
override fun toString(): String

FaustPatternRender

class FaustPatternRender(val left: FloatArray, val right: FloatArray, val sampleRate: Int, val notes: List<FaustNote>, val voices: Int, val unpitched: Int, val steals: Long, val tailCuts: Long, val peakConcurrency: Int, val automationApplied: Long = 0L)

A rendered pattern, with the notes that were asked for and what the allocator did with them.

Declaration Summary
val left: FloatArray
val right: FloatArray
val sampleRate: Int
val notes: List<FaustNote>
val voices: Int
val unpitched: Int Events with no pitch, skipped rather than invented.
val steals: Long
val tailCuts: Long
val peakConcurrency: Int
val automationApplied: Long Shared-parameter changes that landed — see faustParamAutomation.
fun wav(normalizePeak: Float? = null): ByteArray Encode these samples as stereo 16-bit PCM WAV; normalize only when explicitly requested.
val frames: Int
val seconds: Double
fun mono(): FloatArray The mono sum, which is what every measurement here is taken on.
val peak: Float
override fun toString(): String

FaustPeak

data class FaustPeak(val hz: Double, val magnitude: Double)

One spectral peak: where it is and how strong.

Declaration Summary
override fun toString(): String

FaustPolyLaunch

class FaustPolyLaunch internal constructor(val program: FaustProgram, val bank: FaustVoiceBank, internal val node: FaustPolyNode, val gain: Float) : Closeable

A polyphonic Faust program, and the handle a box holds it by.

Declaration Summary
val program: FaustProgram
val bank: FaustVoiceBank
val gain: Float
val sharedControls: List<FaustControl> The controls a person can move: everything the program declares except freq, gate and gain, which the notes drive. A cutoff slider on a polyphonic instrument moves all eight voices at once.
operator fun set(label: String, value: Double) Set a shared control by label on every voice.
fun play(hz: Double, seconds: Double = 0.25, gain: Double = 1.0, afterSeconds: Double = 0.0) Play one note now, or afterSeconds from now.
fun note(midi: Number, seconds: Double = 0.25, gain: Double = 1.0, afterSeconds: Double = 0.0) Play a MIDI note number — launch.note(60) is middle C.
fun chord(midis: List<Number>, seconds: Double = 0.5, gain: Double = 1.0) Play a chord, every note struck on the same frame — the thing a monophonic path cannot do.
fun play(pattern: PatPattern<String>, cps: Double = 1.0, sustain: Double = FAUST_PATTERN_SUSTAIN, gain: Double? = null, aheadSeconds: Double = 0.4, automate: Map<String, PatPattern<Double>> = emptyMap()): FaustPatternFeeder Keep feeding this bank from pattern, one cycle at a time, staying aheadSeconds ahead of the audio.
override fun close()
override fun toString(): String

FaustPatternFeeder

class FaustPatternFeeder internal constructor(private val bank: FaustVoiceBank, val pattern: PatPattern<String>, val cps: Double, val sustain: Double, val gain: Double, val aheadSeconds: Double, val automated: Map<FaustControl, PatPattern<Double>> = emptyMap())

Turns a pattern into notes for a FaustVoiceBank, one cycle at a time, ahead of the audio thread.

Declaration Summary
val pattern: PatPattern<String>
val cps: Double
val sustain: Double
val gain: Double
val aheadSeconds: Double
val automated: Map<FaustControl, PatPattern<Double>> Shared controls patterned alongside the notes.
var cyclesFed: Long Cycles fed so far.
var unpitched: Long Events the pattern produced that carried no pitch, and so were skipped.
fun stop() Stop feeding. Notes already scheduled still play; FaustVoiceBank.allNotesOff silences those.
val isRunning: Boolean
override fun toString(): String

FaustPoly.kt

A Faust program played polyphonically, by a pattern.

FaustDsp is one instance of one program: a drone with knobs. This is the other thing a Faust program can be, an instrument: N copies of the same compiled DSP allocated to notes, with the notes coming from Field's own pattern language. Nothing in Faust is recompiled to make that happen. A Faust factory built with internal memory keeps its whole state at offset 0 of its own WebAssembly memory, so N WebAssembly.Instances of one module are N independent voices for free.

One context, N instances

Each FaustDsp builds its own GraalVM context, and what is left of a block after compute is dominated by the per-block Context.enter (see FaustDsp for the figures). N voices as N FaustDsps would pay that entry N times a block, which would make the cost of polyphony a boundary cost rather than a DSP cost. FaustVoiceBank hosts every voice in one context instead, one enter a block whatever N is. That needs a host shim of its own: FaustDsp's keeps its instance in module-level JS globals and can hold only one.

Both arrangements are timed against each other by synth.faust.poly, which prints the µs-per-block curve for each and asserts that the one shipped here renders a block inside the block's own budget at 1, 2, 4, 8 and 16 voices.

The notes land on the sample, not on the block

A 128-frame block at 48kHz is 2.67ms, and quantising a note onset to that boundary is audible. So render does not write parameters once per block. It enters the context once, then walks the block in chunks that end where the next event begins: apply the writes due at this exact frame, compute up to the next event, repeat. A note therefore begins on the sample it was scheduled for, and synth.faust.poly asserts that in samples rather than in milliseconds.

This is also why FaustDsp.setParam is not used: that queue is drained once per render call, which is block granularity by construction.

The controls are the program's own, and it says so when they are missing

Faust's polyphonic convention is freq / gate / gain, the same three names its -nvoices architecture looks for, and they are discovered out of the program's declared UI by label rather than by index. gain is optional; a program missing freq or gate is refused by FaustPolyControls, with a message naming what is missing, what the program did declare, and the line of Faust that would fix it. Writing to control index 0 instead would produce silence, which carries no diagnosis at all.

src/jvmMain/kotlin/afield/audio/synth/FaustPoly.kt — read the source

Used in: How to make sound in Field

FaustVoiceBank

class FaustVoiceBank internal constructor(val program: FaustProgram, val sampleRate: Int, val voices: Int, val controls: FaustPolyControls, val mixGain: Double = 1.0, val idleThreshold: Float = 1e-5f, val stealReleaseFrames: Long = RETRIGGER_FRAMES) : Closeable

N instances of one compiled Faust program, allocated to notes and summed.

Declaration Summary
val program: FaustProgram
val sampleRate: Int
val voices: Int
val controls: FaustPolyControls Discovered freq/gate/gain.
val mixGain: Double Every voice's output is summed and multiplied by this on the way out.
val idleThreshold: Float Below this, for a whole chunk and with its gate down, a voice counts as finished and can be reallocated without cutting a tail.
val stealReleaseFrames: Long How long a stolen voice holds its gate down before raising it for the incoming note, in samples.
val numInputs: Int Channel counts as the compiled module reports them.
val numOutputs: Int
val frame: Long Absolute frames rendered so far — the bank's clock, and the only clock a note is timed against.
var steals: Long How many gate-on voices were cut short because every voice was busy. Ideally zero.
var tailCuts: Long How many released but still ringing voices were reused. Audible as a shortened tail, not a cut note.
var lateNotes: Long Notes that arrived after the frame they were meant to start on, and were played immediately.
var notesPlayed: Long Notes started.
var peakConcurrency: Int The greatest number of voices sounding at once, so far. The measure of polyphony actually reached.
fun schedule(notes: Collection<FaustNote>) Schedule notes for their own frames on this bank's clock.
fun schedule(note: FaustNote) Schedule one note.
fun play(hz: Double, seconds: Double = 0.25, gain: Double = 1.0, afterSeconds: Double = 0.0) Schedule a note starting afterSeconds from now, for seconds.
fun automate(changes: Collection<FaustParamChange>) Schedule shared-parameter changes for their own frames — a cutoff sweep, a resonance, a detune.
var automationApplied: Long Shared-parameter changes applied so far.
fun pendingCount(): Int Notes scheduled but not yet started — those handed over and not yet drained, plus those the audio thread is holding as of its last block.
fun clearSchedule() Drop every note and parameter change that has not happened yet. Sounding notes finish.
fun allNotesOff() Gate every voice down now, and forget the schedule.
fun setAll(control: FaustControl, value: Double) Set a control on every voice — a cutoff, a resonance, anything that is not per-note.
operator fun set(label: String, value: Double) Set a control by label on every voice. Silently ignores an unknown name.
fun render(outL: FloatArray, outR: FloatArray, numFrames: Int) Render numFrames of the mix, advancing the bank's clock by exactly that many frames.
fun activeVoices(): Int How many voices are sounding right now (gate up, or still ringing).
fun voiceHz(voice: Int): Double Which pitch a voice was last given, in Hz, or 0 if it has never played.
fun paramValue(voice: Int, control: FaustControl): Float Read a control back out of one voice's WebAssembly memory.
override fun close()
override fun toString(): String

FaustNote

data class FaustNote(val startFrame: Long, val endFrame: Long, val hz: Double, val gain: Double = 1.0, val label: String = "")

One note for a FaustVoiceBank: when, for how long, at what pitch.

Declaration Summary
val startFrame: Long
val endFrame: Long
val hz: Double Pitch in Hz, written to the program's declared freq control (clamped to its declared range).
val gain: Double Written to the program's declared gain control, if it declares one.
val label: String What the pattern atom said, carried through for debugging and for plots.
val lengthFrames: Long
override fun toString(): String

FaustParamChange

data class FaustParamChange(val frame: Long, val control: FaustControl, val value: Double)

One shared-parameter change, on a frame of the bank's clock.

Declaration Summary
override fun toString(): String

FaustPolyControls

data class FaustPolyControls(val freq: FaustControl, val gate: FaustControl, val gain: FaustControl?)

The three controls a polyphonic Faust program has to declare, found by label.

Declaration Summary
val freq: FaustControl
val gate: FaustControl
val gain: FaustControl? Optional: a program may bake its own level in.

FaustPolyNode

class FaustPolyNode(val bank: FaustVoiceBank) : StereoNode()

A FaustVoiceBank as a node in Field's audio graph.

Declaration Summary
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)

FieldAudioEngine.kt

The file that opens an audio device: the OpenAL output stream Field plays through, the optional capture stream audioInput() reads from, and the mixer that sums every registered patch into it.

AudioPatchSpec is one thing to hear, either a mono Signal with a gain and a pan or a StereoRenderable with a gain. A box builds one by returning audio { … } from synth.kt rather than by calling the constructors here. FieldAudioEngine is the device and the list of patches on it; shared(), reached from a box as audioEngine(), is the single instance a process gets, and its sample rate and buffer size are whatever the first caller in the session asked for. register takes a spec and hands back an AudioPatchHandle, and closing that handle is the only way a patch leaves the mix.

Rendering is pulled by the device rather than driven by a clock. One daemon thread keeps a queue of OpenAL buffers filled, bufferSize frames each, and renders the next block only when the source reports an earlier buffer as processed, so the mix advances as fast as the sound card consumes it and no faster. A SynthContext is advanced once per block and handed to every patch. Patches accumulate rather than replace: each renderer adds into the same pair of buffers, a mono one through an equal-power pan law that puts a centred signal at 0.707 in both channels rather than at half in each. The sum is clamped to [-1, 1] and written out as 16-bit stereo, and a sample that is not finite becomes silence and is counted in nonFiniteSamplesSanitized, which reaches the warning,audio log channel at most once every two seconds.

Three properties of that loop account for most silence. outputActive means the device opened, not that audio is flowing: it is set when the OpenAL source and its buffers exist and cleared only when the thread exits, while rendering depends on that source continuing to report processed buffers. A machine too loaded to service its own audio daemon therefore renders nothing with every flag still reading true, and a patch that counts its own generateStereo calls is what separates that case from a wiring mistake. If the device cannot be opened at all, the thread prints why and returns without retrying, so start() does nothing from then on and patches register into a mix nobody renders; destroyShared() forces a fresh attempt. And input is the newest block the capture device has rather than a continuous stream: at most bufferSize frames are taken per block, and a block with nothing ready reads as silence.

enableLinkOutput sends the same mix on to Ableton Link through LinkAudioInterface, and can mute the local device while it does by zeroing the OpenAL source gain; the mix is rendered either way. A Link interface opened anywhere else in the process is picked up per block without this file being told about it, so a patch can reach Link without the call.

src/jvmMain/kotlin/afield/audio/synth/FieldAudioEngine.kt — read the source

AudioPatchSpec

class AudioPatchSpec internal constructor(internal var renderer: Renderer, val warmUpCycles: Int = 0) : LiveGraphInspectable

One thing to hear: a signal graph, the gain and pan it plays at, and how many blocks to render it for before it joins the mix.

Declaration Summary
val warmUpCycles: Int Blocks to render and throw away on the registering thread before the patch joins the mix.
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String

AudioPatchHandle

class AudioPatchHandle internal constructor(private val engine: FieldAudioEngine, private val id: String, private val ownership: afield.runtime.lifecycle.Ownership? = null) : AutoCloseable

A registration rather than a patch: closing it takes the spec out of the mix and does nothing else.

Declaration Summary
override fun close()
override fun toString(): String

FieldAudioEngine

class FieldAudioEngine private constructor(val sampleRate: Int = 48_000, val bufferSize: Int = 512, private val queuedBufferCount: Int = 12, private val inputChannels: Int = 2) : AutoCloseable

The audio device and the patches playing through it.

Declaration Summary
val sampleRate: Int
val bufferSize: Int
var monitorLocalOutput: Boolean Whether the local OpenAL source is audible.
var inputActive: Boolean Whether a capture device is open.
var outputActive: Boolean Whether the output device opened, which is not the same as audio flowing.
var nonFiniteSamplesSanitized: Long How many NaN or infinite samples have been replaced with silence since the engine started, counted once per sample and per channel.
fun monoInput(): Signal The capture device's newest block as a mono Signal, starting the engine if it is not running.
fun stereoInput(): StereoSignal The capture device's newest block as a StereoSignal, starting the engine if it is not running.
fun enableLinkOutput(link: LinkAudioInterface = LinkAudioInterface.shared(), name: String = "Field4", maxFrames: Int = bufferSize, monitorLocal: Boolean = true): FieldAudioEngine Publish the mix to Ableton Link as 16-bit stereo, and return this engine so the call chains.
fun disableLinkOutput(): FieldAudioEngine Drop the engine's own reference to Link and unmute the local device.
override fun toString(): String
fun register(spec: AudioPatchSpec): AudioPatchHandle Put spec in the mix and return the handle that takes it out again.
fun start() Open the devices and begin rendering, or return at once if that has already been attempted.
override fun close() Ask the audio thread to stop and wait up to two seconds for it, releasing the OpenAL device, source, buffers and capture stream on the way out.

Filters.kt

The filters, and the one saturator that keeps them company. Each is a SignalProcessor, so saw / SVF(cutoff = 800.p) is how one gets used, and every coefficient is recomputed per sample from its Params, which is what lets a cutoff be swept at audio rate.

Which one to reach for. SVF is the default for anything modulated: it is the state-variable form under a topology-preserving transform, and it stays well behaved while the cutoff is moving. Biquad is the Audio EQ Cookbook set, second order in eight responses including the two shelves and a peaking EQ, and is what to use for a fixed curve. OnePole is 6 dB per octave and costs almost nothing. MoogLadder and DiodeLadder are four-pole resonant lowpasses that keep the character of the circuits they are named after.

The rest are more particular. CombFilter and AllpassFilter are the classic reverb building blocks offered raw, in feedforward or feedback form; the reverbs in Effects.kt build their own rather than compose these. FormantFilter parks three bandpasses on vowel formants and morphs between A, E, I, O and U on a single control. DCBlocker is the one-line high-pass that removes the offset rectification and asymmetric shaping leave behind. Saturator is a bare tanh with a drive in front of it, which is often what a ladder filter's output wants next.

src/jvmMain/kotlin/afield/audio/synth/Filters.kt — read the source

Used in: Subharmonic Synthesis: an Octave Below, Agent transcript gallery

Top-level functions

Declaration Summary
fun Signal.lowpass(cutoffHz: Float, q: Float = 0.707f): Signal This signal through a 12 dB/octave Biquad lowpass at cutoffHz: bass.lowpass(150f).
fun Signal.highpass(cutoffHz: Float, q: Float = 0.707f): Signal This signal through a 12 dB/octave Biquad highpass at cutoffHz. See lowpass.

OnePole

class OnePole(val cutoff: Param = Param(1000f), val mode: Mode = Mode.LP) : SignalProcessor()

First-order (6 dB/oct) one-pole filter. mode: LP for lowpass, HP for highpass.

Declaration Summary
val cutoff: Param
val mode: Mode
enum class Mode
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

BiquadType

enum class BiquadType

Constants: LPF, HPF, BPF, NOTCH, APF, PEAKING, LOW_SHELF, HIGH_SHELF.

Biquad

class Biquad(val type: BiquadType = BiquadType.LPF, val cutoff: Param = Param(1000f), val q: Param = Param(0.707f), val gainDb: Param = Param(0f)) : SignalProcessor()

Declaration Summary
val type: BiquadType
val cutoff: Param
val q: Param
val gainDb: Param Gain in dB for PEAKING / LOW_SHELF / HIGH_SHELF.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SVFMode

enum class SVFMode

Constants: LP, HP, BP, NOTCH.

SVF

class SVF(val mode: SVFMode = SVFMode.LP, val cutoff: Param = Param(1000f), val resonance: Param = Param(0.5f)) : SignalProcessor()

Declaration Summary
val mode: SVFMode
val cutoff: Param
val resonance: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

MoogLadder

class MoogLadder(val cutoff: Param = Param(1000f), val resonance: Param = Param(0.5f)) : SignalProcessor()

Declaration Summary
val cutoff: Param
val resonance: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

CombFilter

class CombFilter(val delayTime: Param = Param(0.01f), val feedback: Param = Param(0.7f), val mode: CombMode = CombMode.FEEDBACK, maxDelaySec: Float = 1f) : SignalProcessor()

Declaration Summary
val delayTime: Param Delay time in seconds.
val feedback: Param Feedback/feedforward gain (-1…+1 for stability).
val mode: CombMode
enum class CombMode
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

AllpassFilter

class AllpassFilter(val delayTime: Param = Param(0.005f), val coefficient: Param = Param(0.5f), maxDelaySec: Float = 0.5f) : SignalProcessor()

Declaration Summary
val delayTime: Param
val coefficient: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

DCBlocker

class DCBlocker : SignalProcessor()

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

FormantFilter

class FormantFilter(val morph: Param = Param(0f)) : SignalProcessor()

Declaration Summary
val morph: Param Vowel morph: 0=A, 1=E, 2=I, 3=O, 4=U (interpolated).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

DiodeLadder

class DiodeLadder(val cutoff: Param = Param(1000f), val resonance: Param = Param(0.5f)) : SignalProcessor()

Declaration Summary
val cutoff: Param
val resonance: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Saturator

class Saturator(val drive: Param = Param(1f)) : SignalProcessor()

Declaration Summary
val drive: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

FnSignal.kt

Plays a Functions.Fn of time as audio, evaluating it at every sample rather than once per block.

FnSignal wraps the function and toSignal is the extension that builds one, so an envelope or frequency contour written as an Fn can drive a synth graph sample-accurately.

src/jvmMain/kotlin/afield/audio/synth/FnSignal.kt — read the source

Used in: A function, played and read back

Top-level functions

Declaration Summary
fun Functions.Fn.toSignal(): Signal This function of time, in seconds, as a Signal evaluated at every sample — see FnSignal.

FnSignal

class FnSignal(val fn: Functions.Fn) : Signal()

A Functions.Fn of time, in seconds, heard as a Signal — evaluated at every sample.

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

FnSoundDocs.kt

One Functions.Fn taken through sound and back, and the distance between what went in and what came out — the harness behind public/docs/chain_fn_sound_roundtrip.md.

The route is four parts of Field that do not know about each other. contour is built from formula ops only, so afield.math.functions.FnInterval can certify its range on any interval. It enters a Dough graph either per sample, through FnSignal, or per block, through Param { … }. The graph is rendered offline by doughRenderMono, written to a 16-bit WAV without normalisation — so the amplitude in the file is the amplitude the function asked for — and read back from that file. The analysis is the in-process pair PatOnsetDetection and PatPitchDetection already provide: an RMS envelope (patEnergyEnvelope) for loudness, and YIN (patF0Frames) for pitch.

Two encodings of one function

  • Encoding.LOUDNESS: a CARRIER_HZ sine whose amplitude is LOUDNESS_GAIN · f(t). Recovered as √2 · RMS / LOUDNESS_GAIN. The carrier is chosen so one period is exactly 128 samples at SR and the RMS window, RMS_WINDOW, holds two whole periods: then a steady sine's RMS is exactly A/√2 and the window adds no ripple of its own.
  • Encoding.PITCH: a sine at PITCH_BASE_HZ · 2^f(t) and constant level, so f spans a fraction of an octave. Recovered as log2(hz / PITCH_BASE_HZ) from voiced YIN frames only.

What is measured, per encoding and route

Every analysis frame is one sample of the population: its recovered value against f at the frame centre (RoundTrip.maxAbsError, RoundTrip.rmsError); the delay that best aligns them (RoundTrip.bestLagSamples); and whether the recovered value lies inside the certified range of f over the samples the frame actually read (RoundTrip.insideCertified) — the only interval a windowed measurement can be expected to land in. Blip is the narrow feature: a bump with a 3 ms sigma, shorter than both analysis windows, and how much of its height comes back.

No engine, no device, no Field window. The renders are deterministic, so the committed .wavs regenerate byte-stably; as a FIELD_TEST everything is written to a temp dir.

src/jvmMain/kotlin/afield/audio/synth/FnSoundDocs.kt — read the source

FnSoundDocs

object FnSoundDocs

Renders and measures the Fn sound round trip; see the file overview.

Declaration Summary
const val SR: Int Sample rate of every render and of both published clips.
const val BLOCK: Int Frames per doughRenderMono block — the granularity of a Param { … } supplier.
const val SECONDS: Double
const val CARRIER_HZ: Double 128 samples per period at SR, so RMS_WINDOW holds exactly two.
const val LOUDNESS_GAIN: Double
const val PITCH_BASE_HZ: Double
const val PITCH_LEVEL: Double
const val RMS_WINDOW: Int
const val RMS_HOP: Int
const val PITCH_MIN_HZ: Double The register YIN is told about: contour stays inside 0.2..0.71, so about 253..360 Hz.
const val PITCH_MAX_HZ: Double
const val BLIP_AT: Double Where the narrow bump sits, its sigma, and its height above what is under it.
const val BLIP_SIGMA: Double
const val BLIP_HEIGHT: Double
val DOMAIN: Functions.Domain
fun contour(): Functions.Fn The function taken on the trip: a resting level, a broad swell, a step up to a plateau that wobbles at 6 Hz, and one bump narrower than either analysis window. Formula ops only, so it certifies.
enum class Encoding
enum class Route How the function enters the graph: FnSignal at every sample, or Param { … } once a block.
fun pitchHz(fn: Functions.Fn): Functions.Fn The frequency contour the PITCH encoding plays, PITCH_BASE_HZ · 2^f, still an Fn.
fun signal(fn: Functions.Fn, encoding: Encoding, route: Route): Signal The Dough graph for one encoding and route.
fun render(encoding: Encoding, route: Route, fn: Functions.Fn = contour()): FloatArray Offline-render one encoding and route; no normalisation anywhere.
data class Frame(val firstSample: Int, val lastSample: Int, val centerSeconds: Double, val recovered: Double) One analysis frame: the samples it read, [firstSample, lastSample], the time the analysis attributes its value to, and what it recovered — NaN for a YIN frame that was not voiced. A YIN frame's time is PatF0Frame.measuredCenterSample, the centre of the samples it compared.
fun analyse(samples: FloatArray, encoding: Encoding): List<Frame> Analyse samples that were read back from a .wav.
data class Blip(val truePeak: Double, val base: Double, val recoveredPeak: Double) How much of the narrow bump came back: its true height over the base, and the recovered one.
class RoundTrip(val encoding: Encoding, val route: Route, val fn: Functions.Fn, val wav: File, val frames: List<Frame>) Everything one trip measured. wav is the file the analysis read.
fun roundTrip(encoding: Encoding, route: Route = Route.PER_SAMPLE, dir: File = File(System.getProperty("java.io.tmpdir"), "fn-sound-roundtrip"), fn: Functions.Fn = contour()): RoundTrip Render, write the .wav into dir, read it back from the file, and analyse what was read.
fun renderWav(name: String, outPath: String) Offline-render scene loudness or pitch (the per-sample route) to a mono 16-bit WAV.
fun main(args: Array<String>)

HarmonicPhaseTracker.kt

Pitch and phase tracking for harmonically rich monophonic input, through HarmonicPhaseTracker.

A bank of complex harmonic demodulators votes on the fundamental's phase velocity, and a decimated autocorrelation search reacquires the period when the bank falls into an octave trap. The tracker is a PhaseTracker, so its state can drive a TrackedPhaseBank.

src/jvmMain/kotlin/afield/audio/synth/HarmonicPhaseTracker.kt — read the source

HarmonicPhaseTracker

class HarmonicPhaseTracker(override val sampleRate: Double = 48_000.0, val minHz: Double = 35.0, val maxHz: Double = 350.0, val initialHz: Double = 100.0, val harmonics: Int = 12, val captureHz: Double = 4.0, val frequencyTrackingTimeSeconds: Double = 0.030, var phaseFollow: Double = 1.0, val residualOutlierHz: Double = 7.0, val harmonicWeightExponent: Double = 0.5, val gateLevel: Double = 0.01, val partialGateScale: Double = 0.35, broadbandPowerTimeSeconds: Double = 0.020, confidenceTimeSeconds: Double = 0.025, val lockThreshold: Double = 0.65, val coarseDecimation: Int = 8, val coarseWindow: Int = 384, val coarseHop: Int = 96, val coarseMinCorrelation: Double = 0.60, val coarseRelativePeak: Double = 0.93, val coarseDeadbandHz: Double = 1.5, val coarseUnlockedPull: Double = 0.65, val coarseLockedPull: Double = 0.12) : PhaseTracker

Track ONE latent periodicity from MANY harmonics of a spectrally complex monophonic input.

Declaration Summary
override val sampleRate: Double
val minHz: Double
val maxHz: Double
val initialHz: Double
val harmonics: Int Number of harmonic demodulators. Channels above Nyquist automatically stop voting.
val captureHz: Double Fine-track capture bandwidth measured in FUNDAMENTAL Hz. Harmonic k receives a baseband LP cutoff of approximately k * this value.
val frequencyTrackingTimeSeconds: Double Time constant with which the stable omega estimate absorbs the fused residual.
var phaseFollow: Double How much of the instantaneous fused residual appears in phaseAdvance. 0 = only stable omega; 1 = strongly cycle-following. Values >1 are allowed experimentally.
val residualOutlierHz: Double Fundamental-Hz distance from the robust median after which a partial is rejected.
val harmonicWeightExponent: Double Mildly prefer low partials: weight is energy / k^harmonicWeightExponent.
val gateLevel: Double Input RMS-ish level below which the tracker is considered gated.
val partialGateScale: Double Baseband partial amplitude needed to cast a vote, relative to gateLevel.
val lockThreshold: Double Confidence above which TrackedPhaseState.locked is true.
val coarseDecimation: Int Raw samples averaged into one coarse sample. 8 -> 6 kHz at a 48 kHz audio rate.
val coarseWindow: Int Number of downsampled samples in each normalized-correlation comparison.
val coarseHop: Int Re-run coarse correlation this many downsampled samples.
val coarseMinCorrelation: Double Absolute normalized-correlation floor for accepting a periodicity candidate.
val coarseRelativePeak: Double Accept peaks this close to the best correlation and choose the shortest-lag one. This avoids selecting the equally-good 2T / 3T autocorrelation peaks.
val coarseDeadbandHz: Double Ignore coarse/fine disagreement smaller than this. Fine tracking has better precision.
val coarseUnlockedPull: Double Coarse pull while fine confidence is weak.
val coarseLockedPull: Double Coarse pull when the fine bank claims to be confident (principally octave-trap correction).
override val state
var coarseHz: Double Last accepted coarse-periodicity estimate, useful for diagnostics/UI.
var coarseCorrelation: Double Normalized autocorrelation score of coarseHz.
override fun process(input: Float): TrackedPhaseState
override fun reset()

LaunchBundle.kt

+ for the audio return values, so one box can launch more than one thing.

A box launches whatever its last expression evaluates to, and ReturnValueRegistry reads a LaunchBundle, or any Iterable, as a request to launch every item in it. afield supplies the + that builds a bundle from a Frames or from a bundle already in hand; this file supplies it for the three audio types a box is likely to be holding on the left, so audio { SineOsc(220.0) } + pat("bd sd").respond { … } is one box playing a drone and a pattern.

A chain of + produces one flat list rather than a nest: the bundle flattens what it already has each time it grows, and the registry flattens again before it dispatches. Each item then goes through the same handler chain it would have reached on its own, the box reads as running until the last of them stops, and an item no handler recognises is logged on the warn,execution channel rather than dropped in silence.

src/jvmMain/kotlin/afield/audio/synth/LaunchBundle.kt — read the source

Top-level functions

Declaration Summary
operator fun AudioPatchSpec.plus(other: Any?): LaunchBundle Launch this patch and other together. other is anything the registry has a handler for.
operator fun StereoRenderable.plus(other: Any?): LaunchBundle Launch this stereo source and other together; the source is wrapped in a patch on the way.
operator fun PatResponseSpec<*>.plus(other: Any?): LaunchBundle Launch this responder and other together, both stopping when the box does.

Oscillators.kt

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.

src/jvmMain/kotlin/afield/audio/synth/Oscillators.kt — read the source

Used in: Csound

Phasor

class Phasor(val freq: Param = Param(440f), initialPhase: Double = 0.0) : Signal()

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

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SineOsc

class SineOsc(val freq: Param = Param(440f), val phaseMod: Param = Param(0f)) : Signal()

Declaration Summary
val freq: Param
val phaseMod: Param Phase modulation input (radians).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SawOsc

class SawOsc(val freq: Param = Param(440f)) : Signal()

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SquareOsc

class SquareOsc(val freq: Param = Param(440f)) : Signal()

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

PulseOsc

class PulseOsc(val freq: Param = Param(440f), val width: Param = Param(0.5f)) : Signal()

Declaration Summary
val freq: Param
val width: Param Pulse width 0…1 (0.5 = square).
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

TriOsc

class TriOsc(val freq: Param = Param(440f)) : Signal()

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperSaw

class SuperSaw(val freq: Param = Param(110f), val detuneCents: Param = Param(20f), val blend: Param = Param(0.85f), val detuneCurve: Param = Param(1.4f), val phaseSpread: Param = Param(1f), val voiceCount: Int = 7, val normalize: Boolean = true) : Signal()

Declaration Summary
val freq: Param
val detuneCents: Param Maximum spread of the detuned bank in cents.
val blend: Param 0 = only center voice, 1 = full detuned bank.
val detuneCurve: Param Shapes how strongly the outer voices pull away from center.
val phaseSpread: Param 0 = aligned phases, 1 = evenly spread initial phases.
val voiceCount: Int
val normalize: Boolean
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperSquare

class SuperSquare(val freq: Param = Param(110f), val voice: Param = Param(0.5f), val semitone: Param = Param(12f), val resonance: Param = Param(0.2f), val lfo: Param = Param(1f), val rate: Param = Param(1f), val pitch1: Param = Param(1f)) : Signal()

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

Declaration Summary
val freq: Param
val voice: Param Pulse width for both oscillators. Values near 0 or 1 nearly mute the sound.
val semitone: Param Detune of the secondary oscillator in semitones below the main pitch.
val resonance: Param Filter resonance amount.
val lfo: Param Additional filter sweep range above the tracked base cutoff.
val rate: Param Multiplier for the internal LFO rate.
val pitch1: Param Scales the tracked cutoff floor.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperPWM

class SuperPWM(val freq: Param = Param(110f), val voice: Param = Param(0.5f), val semitone: Param = Param(12f), val resonance: Param = Param(0.2f), val lfo: Param = Param(1f), val rate: Param = Param(1f), val pitch1: Param = Param(1f), val secondarySweep: Param = Param(0.65f)) : Signal()

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

Declaration Summary
val freq: Param
val voice: Param Phase offset between the multiplied pulse pairs, expressed in oscillator cycles.
val semitone: Param Detune of the secondary oscillator in semitones below the main pitch.
val resonance: Param Filter resonance amount for the tracked low-pass stage.
val lfo: Param Additional filter sweep range above the tracked base cutoff.
val rate: Param Multiplier for the internal LFO rate.
val pitch1: Param Scales the tracked cutoff floor.
val secondarySweep: Param Replaces SuperDirt's note-duration sweep on the second filter in this continuous port.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperComparator

class SuperComparator(val freq: Param = Param(110f), val voice: Param = Param(0.5f), val resonance: Param = Param(0.5f), val lfo: Param = Param(1f), val rate: Param = Param(1f), val pitch1: Param = Param(1f), val shape: Param = Param(0.5f)) : Signal()

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

Declaration Summary
val freq: Param
val voice: Param Scales the internal comparator frequencies. Higher values sound breathier.
val resonance: Param Filter resonance amount.
val lfo: Param Amount of LFO and random movement applied to the filter.
val rate: Param Multiplier for the internal LFO rate.
val pitch1: Param Scales the tracked cutoff floor.
val shape: Param Fixed width for the raw port's VarSaw source.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperChip

class SuperChip(val freq: Param = Param(110f), val pitch2: Param = Param(2f), val pitch3: Param = Param(3f), val voice: Param = Param(0f)) : Signal()

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

Declaration Summary
val freq: Param
val pitch2: Param
val pitch3: Param
val voice: Param Bit-pattern selector for which of the three oscillators are active.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperNoise

class SuperNoise(val freq: Param = Param(110f), val pitch1: Param = Param(1f), val resonance: Param = Param(0f), val voice: Param = Param(0f)) : Signal()

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

Declaration Summary
val freq: Param
val pitch1: Param Scales the tracked band-pass center frequency.
val resonance: Param Filter resonance amount.
val voice: Param 0 = digital stepped noise, 1 = brown/white blend.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SuperFork

class SuperFork(val freq: Param = Param(440f)) : Signal()

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.

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

WavetableOsc

class WavetableOsc(val table: FloatArray, val freq: Param = Param(440f)) : Signal()

Declaration Summary
val table: FloatArray
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Wavetables

object Wavetables

Declaration Summary
fun sine(size: Int = 2048): FloatArray
fun saw(size: Int = 2048, harmonics: Int = 64): FloatArray Band-limited sawtooth (first harmonics partials via additive).
fun square(size: Int = 2048, harmonics: Int = 64): FloatArray Band-limited square (odd harmonics only).
fun triangle(size: Int = 2048, harmonics: Int = 64): FloatArray Band-limited triangle (odd harmonics, alternating sign, 1/n²).

WhiteNoise

class WhiteNoise : Signal()

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

PinkNoise

class PinkNoise : Signal()

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

BrownNoise

class BrownNoise : Signal()

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

Impulse

class Impulse(val freq: Param = Param(1f)) : Signal()

Declaration Summary
val freq: Param
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SampleAndHold

class SampleAndHold(val input: Signal, val trigger: Signal) : Signal()

Declaration Summary
val input: Signal
val trigger: Signal
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

ParamQuery.kt

The synth-facing head of the graph-query layer: turn a PathSet into a set of drivable parameters.

Two kinds of slot qualify, behind one ParamBinding interface:

  • a native Param (base + supplier + modulation), grouped by identity so several addresses for one Param collapse into a single entry with aliases;
  • any writable numeric property, wrapped in a ScalarPathState that gives a plain Int/Float/Double/… field the same base/supplier/modulation semantics, coercing back to the field's declared type on write.

That second case is what makes any object's numeric field modulatable, which is why the generic query core underneath knows nothing about audio.

The split runs along the same line. Path resolution, grouping and ScalarPathState carry no runloop dependency, and this file is where the runloop-bound half of the param layer is confined.

src/jvmMain/kotlin/afield/audio/synth/ParamQuery.kt — read the source

Used in: Querying object graphs

Top-level functions

Declaration Summary
fun PathSet.params(): ParamSet Narrow a generic PathSet to the slots that can be driven as parameters — native Params and writable numeric fields.
fun Any?.params(pattern: String = "**", options: GraphWalkOptions = GraphWalkOptions()): ParamSet Query this object graph for drivable parameters.
fun tickGraphBindings(contextName: String, deltaTime: Float, sampleRate: Float = if (deltaTime > 0f) 1f / deltaTime else 60f) Drive every attached supplier/modulation once, for hosts that are not the runloop.
infix fun Any?.matches(pattern: String): ParamSet Sugar: patch matches "**\/cutoff".

ParamModulationMode

enum class ParamModulationMode

Constants: REPLACE, ADD.

ParamPath

class ParamPath internal constructor(private val primaryRef: GraphPathRef, val aliases: List<String>, private val binding: ParamBinding)

Declaration Summary
val aliases: List<String>
val path: String
val rawPath: String
val isNativeParam: Boolean
val param: Param
fun paramOrNull(): Param?
var base: Float
var supplier: (SynthContext.() -> Float)?
var modulation: Signal?
fun clearModulation(): ParamPath
fun clearSupplier(): ParamPath
fun modulate(signal: Signal, mode: ParamModulationMode = ParamModulationMode.REPLACE): ParamPath
override fun toString(): String

ParamSet

class ParamSet internal constructor(private val root: Any?, internal val entries: List<ParamPath>) : Iterable<ParamPath>, ToHTML

Declaration Summary
val size: Int
val isEmpty: Boolean
fun paths(): List<String>
fun params(): List<Param>
fun bases(): List<Float>
fun matching(pattern: String): ParamSet Narrow by pattern, on exactly the terms graphPaths uses — globs, case and \| alike.
fun set(value: Float): ParamSet
fun set(values: Iterable<Float>): ParamSet
fun set(block: ParamSetScope.(ParamPath, Int) -> Float): ParamSet
fun offset(delta: Float): ParamSet
fun scale(factor: Float): ParamSet
fun clamp(minValue: Float, maxValue: Float): ParamSet
fun random(range: ClosedFloatingPointRange<Float>, random: Random = Random.Default): ParamSet
fun choose(values: Iterable<Float>, random: Random = Random.Default): ParamSet
fun modulate(signal: Signal, mode: ParamModulationMode = ParamModulationMode.REPLACE): ParamSet
fun modulate(mode: ParamModulationMode = ParamModulationMode.REPLACE, block: ParamSetScope.(ParamPath, Int) -> Signal?): ParamSet
fun clearModulation(): ParamSet
fun supply(block: SynthContext.(ParamPath, Int, ParamSetScope) -> Float): ParamSet
fun clearSuppliers(): ParamSet
fun rewrite(block: ParamSetScope.() -> Unit): ParamSet
override fun iterator(): Iterator<ParamPath>
override fun toString(): String
override fun toHTML(): String

ParamSetScope

class ParamSetScope internal constructor(private val set: ParamSet)

Declaration Summary
val size: Int
val entries: List<ParamPath>
val values: List<Float>
val mean: Float
val min: Float
val max: Float
operator fun get(index: Int): ParamPath
fun matching(pattern: String): ParamSet
fun setAll(value: Float)
fun set(values: Iterable<Float>)
fun normalize(range: ClosedFloatingPointRange<Float> = 0f..1f)
fun jitter(amount: Float, random: Random = Random.Default)
fun ramp(range: ClosedFloatingPointRange<Float>)

PdBox.kt

The box-facing side of Pure Data: a patch as a sound source, with messages both ways.

val patch = pd("""
#X obj 20 20 r freq;
#X obj 20 60 osc~ 220;
#X obj 20 100 *~ 0.1;
#X obj 20 140 dac~;
#X connect 0 0 1 0;
#X connect 1 0 2 0;
#X connect 2 0 3 0;
#X connect 2 0 3 1;
""")
controlGroup { slider("freq", 100.0, 1000.0, initialValue = 220.0) { patch.send("freq", it) } }
patch

Returning the PdLaunch as the box's last expression is what makes it sound: it joins FieldAudioEngine as a afield.audio.pd.PdNode — the same seam Faust and patSynth use — and stopping the box stops the sound and frees the Pd. Everything Pd prints (print, errors) goes to the box's output, and an object Pd could not create is marked on the box's own line.

Every launch is its own Pd, so two boxes can both use [r freq] without hearing each other. libpd is an optional native library; see PdLibrary.

src/jvmMain/kotlin/afield/audio/synth/PdBox.kt — read the source

Used in: Pure Data, Agent transcript gallery

Top-level functions

Declaration Summary
fun Box.pd(patch: String, gain: Float = 1f, printToBox: Boolean = true, lineOffset: Int = -1, input: StereoRenderable? = null, drawPatch: Boolean = true): PdLaunch Open a Pure Data patch written as text, ready to play.
fun Box.pd(file: File, gain: Float = 1f, printToBox: Boolean = true): PdLaunch Open a .pd file — one drawn in Pd itself — ready to play. Abstractions next to it are found.
fun Box.pdBox(name: String): Box The Pd box named name in this box's document — a child of a Pd root, whose code is a patch — so Kotlin can pdSend to the patch it is playing.
fun Box.pdSend(receiver: String, vararg values: Any) Send values to [r receiver] in the patch this Pd box is playing, as running the line receiver values… in the box itself does. Numbers of any kind (an Int is fine) arrive as Pd floats; no values is a bang; values that start with a number are a list; a first value that is a String is the message's selector, as pdSend("voice", "set", 3) sends set 3.
fun Box.pdControls(launch: PdLaunch, channel: String = "controls"): Int A slider for every horizontal or vertical Pd slider (hsl/vsl) in the patch that has a receive name, a checkbox for every toggle (tgl), and a number slider for every nbx.
fun pdGuiControls(patchText: String): List<PdGuiControl> The hsl, vsl, tgl and nbx objects in patchText that have a receive name.
fun registerPdLaunchReturnValue() Teach Field that a PdLaunch is something a box can be running.
fun Box.pdWaveform(patch: String, seconds: Double = 0.02, sampleRate: Int = 48_000, channel: String = "output", maxPoints: Int = 2000, before: (PdInstance) -> Unit = {}): Pair<FloatArray, FloatArray> Render a patch offline and plot it — no audio device, no audio engine.
fun renderPdWav(path: File, patch: String, seconds: Double, sampleRate: Int = 48_000, before: (PdInstance) -> Unit = {}): File Render a patch offline to a stereo 16-bit .wav. No audio device is opened.

PdUnavailable

class PdUnavailable(message: String) : IllegalStateException(message)

libpd is not built; the message says how to build it.

PdLaunch

class PdLaunch internal constructor(val pd: PdInstance, val patch: PdPatch, val gain: Float, private val printBox: Box?, private val input: StereoRenderable? = null) : Closeable

A running Pd patch, and the handle a box holds it by.

Declaration Summary
val pd: PdInstance
val patch: PdPatch
val gain: Float
val node: afield.audio.pd.PdNode by lazy What joins the audio engine: this launch's Pd, fed by its input when it has one.
val dollarZero: Int This patch's $0: send("${launch.dollarZero}-freq", 440) reaches [r $0-freq].
fun send(receiver: String, value: Number) Send a number to [r receiver].
fun bang(receiver: String) Send a bang to [r receiver].
fun symbol(receiver: String, symbol: String) Send a symbol to [r receiver].
fun list(receiver: String, vararg items: Any) Send a list to [r receiver].
fun message(receiver: String, selector: String, vararg args: Any) Send selector args… to [r receiver].
fun receive(name: String, callback: (PdInstance.Message) -> Unit): Closeable Call callback with what the patch sends to [s name], on the box's main-thread loop.
fun lastValue(name: String): PdInstance.Message? The last message seen on a received name.
fun noteOn(channel: Int, pitch: Int, velocity: Int) A note to the patch's notein, 0-based channel; velocity 0 is a note-off.
fun controlChange(channel: Int, controller: Int, value: Int) A controller to the patch's ctlin.
fun onMidi(callback: (PdInstance.Midi) -> Unit): Closeable Call callback with what the patch sends out of noteout/ctlout, on the main-thread loop.
fun connectMidi(input: String? = null, output: String? = null): Closeable Wire hardware MIDI to the patch: notes and controllers from the first input whose name contains input reach its notein/ctlin, and what it sends from noteout/ctlout goes to the first output whose name contains output. Either may be left null. Channels are 0-based on both sides, as Java Sound counts them. Closing the result (or the launch) closes the ports.
fun readArray(name: String, offset: Int = 0, count: Int = -1): FloatArray Read Pd array name.
fun writeArray(name: String, values: FloatArray, offset: Int = 0) Write into Pd array name.
fun arraySize(name: String): Int Length of Pd array name, or -1.
fun poll(): Int Deliver what Pd has sent since the last call. The launch loop does this every frame.
override fun close()
override fun toString(): String

PdGuiControl

data class PdGuiControl(val kind: String, val receive: String, val label: String, val min: Double, val max: Double, val init: Double)

One Pd GUI object with a receive name, as pdControls reads it.

Declaration Summary
val kind: String
val receive: String
val label: String
val min: Double
val max: Double
val init: Double
val toggle: Boolean

Physical.kt

Two processors over one delay line read at fractional sample positions: FractionalDelay, a plain echo, and KarplusStrong, the same line closed into a loop and tuned to a pitch.

DelayInterpolation picks how the line reads between whole samples. LINEAR mixes the two neighbours, CUBIC, the default, fits four of them. The choice matters most inside a feedback loop, where the read happens again on every pass.

FractionalDelay is the echo: delayTime in seconds against a line sized from maxDelaySec and the sample rate it first sees, feedback clamped just inside ±1 so each pass is quieter than the last, and mix crossfading dry against delayed. Delay in Effects.kt does the same job with a fixed cubic read; this one lets the interpolation be chosen.

KarplusStrong closes that line into a loop one period of freq long. Two things happen inside it. brightness crossfades the delayed sample against the one before it, an average of two taps that acts as a lowpass, so the high harmonics fade a pass at a time while the low ones hold, and the loop length has that average's own delay subtracted from it, which keeps the pitch where it was as brightness moves. decay scales the whole of what goes back round on each pass.

The excitation reaches the loop through a second line, delayed by pickPosition of a period and subtracted from itself before excitationMix scales it in. That is a comb with notches every 1 / pickPosition harmonics, which is the standard stand-in for where along a string it was plucked, so moving the control changes the colour of the attack rather than its level. It is a SignalProcessor, so burst / KarplusStrong(freq = 110.p) is how it gets used, and with clearOnTrigger a rising edge on trigger empties both lines so a new note starts clean.

src/jvmMain/kotlin/afield/audio/synth/Physical.kt — read the source

DelayInterpolation

enum class DelayInterpolation

How a delay line reads between whole samples: LINEAR mixes two neighbours, CUBIC fits four.

Constants: LINEAR, CUBIC.

FractionalDelay

class FractionalDelay(val delayTime: Param = Param(0.2f), val feedback: Param = Param(0.35f), val mix: Param = Param(0.5f), val maxDelaySec: Float = 5f, val interpolation: DelayInterpolation = DelayInterpolation.CUBIC) : SignalProcessor()

Declaration Summary
val delayTime: Param
val feedback: Param
val mix: Param
val maxDelaySec: Float
val interpolation: DelayInterpolation
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

KarplusStrong

class KarplusStrong(val freq: Param = Param(110f), val decay: Param = Param(0.995f), val brightness: Param = Param(0.5f), val excitationMix: Param = Param(1f), val pickPosition: Param = Param(0.15f), val trigger: Signal = Const(0f), val clearOnTrigger: Boolean = true, val maxDelaySec: Float = 2f, val interpolation: DelayInterpolation = DelayInterpolation.CUBIC) : SignalProcessor()

Karplus-Strong resonator driven by its input signal.

Declaration Summary
val freq: Param
val decay: Param
val brightness: Param
val excitationMix: Param
val pickPosition: Param
val trigger: Signal
val clearOnTrigger: Boolean
val maxDelaySec: Float
val interpolation: DelayInterpolation
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Sampling.kt

Playback of recorded audio, from reading a buffer straight through to scattering it into grains. AudioSample is the buffer, Sampler plays one back, and the three grain clouds take one apart.

Every read goes through a four-point Hermite fit that wraps at both ends of the buffer rather than clamping, so a position past the last sample continues from the first. Sampler starts on the rising edge of its gate and either runs to the end or loops between loopStart and loopEnd; it scales its read rate by the ratio of the sample's own rate to the engine's, so a recording made at 48 kHz plays at pitch on a 44.1 kHz output.

The three clouds spawn grains the same way and differ in where the grains read from. GrainCloud reads a fixed AudioSample and sums to mono. StereoGrainCloud reads the same kind of sample but gives each grain a pan position, a left/right read-head separation and independent per-channel scatter, so it is a StereoNode rather than a Signal. LiveGrainCloud has no sample at all and grains a ring buffer that its own input is writing.

Spawning is by accumulated density rather than by a clock: density is grains per second, densityJitter varies the gap between onsets, and each cloud holds maxGrains voices, 128 by default. A spawn that finds every voice busy is dropped rather than stealing one, so pushing density past what maxGrains can carry thins the texture instead of cutting grains short. spray, pitchJitterCents, grainSizeJitter and gainJitter each widen one property per grain at spawn time, GrainDirection decides whether a grain reads forwards, backwards or picks one per grain, and GrainEnvelope is the window over a grain's life, eight shapes, with randomEnvelope drawing a fresh one each time.

LiveGrainCloud also has to stay out of the recorder's way. A forward-reading grain is placed at least its own travel distance back from the write head, as far as the ring allows, so it does not overtake the recorder while it sounds. Its record control is a slewed gain on the write rather than a switch, and at zero the write head stops advancing, so the grains go on working over whatever the ring was holding.

src/jvmMain/kotlin/afield/audio/synth/Sampling.kt — read the source

AudioSample

class AudioSample(val data: FloatArray, val sampleRate: Float = 44100f)

A block of audio in memory: a FloatArray of samples, and the rate they were captured at.

Declaration Summary
val data: FloatArray
val sampleRate: Float Original sample rate of the data.
val lengthSamples: Int
val lengthSeconds: Float
fun readInterp(position: Double): Float Read with Hermite interpolation at fractional position.
override fun toString(): String

Sampler

class Sampler(val sample: AudioSample, val rate: Param = Param(1f), val gate: Signal? = null, val loop: Boolean = false, val loopStart: Int = 0, val loopEnd: Int = -1) : Signal()

Declaration Summary
val sample: AudioSample
val rate: Param Playback rate (1 = original speed).
val gate: Signal? Gate signal: rising edge triggers playback.
val loop: Boolean
val loopStart: Int Loop start in samples (0 = beginning).
val loopEnd: Int Loop end in samples (-1 = end of sample).
fun trigger() Trigger playback from the start.
fun stop() Stop playback.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

GrainEnvelope

enum class GrainEnvelope

Constants: HANN, SINE, TRIANGLE, TUKEY, GAUSSIAN, PERCUSSIVE, REVERSE_PERCUSSIVE, BLACKMAN.

GrainDirection

enum class GrainDirection

Constants: FORWARD, REVERSE, BIDIRECTIONAL.

GrainCloud

class GrainCloud(val source: AudioSample, val position: Param = Param(0.5f), val grainSize: Param = Param(0.05f), val density: Param = Param(20f), val pitch: Param = Param(1f), val pitchJitterCents: Param = Param(0f), val spray: Param = Param(0f), val envelope: GrainEnvelope = GrainEnvelope.HANN, val randomEnvelope: Boolean = false, val maxGrains: Int = 128, val grainSizeJitter: Param = Param(0f), val densityJitter: Param = Param(0f), val gain: Param = Param(1f), val gainJitter: Param = Param(0f), val direction: GrainDirection = GrainDirection.FORWARD) : Signal()

Declaration Summary
val source: AudioSample
val position: Param Normalized position in source (0…1).
val grainSize: Param Grain duration in seconds.
val density: Param Grains spawned per second.
val pitch: Param Playback rate per grain.
val pitchJitterCents: Param Per-grain random pitch range in cents (± this amount).
val spray: Param Random position scatter in seconds.
val envelope: GrainEnvelope Envelope shape used by each grain.
val randomEnvelope: Boolean If true, choose a random GrainEnvelope for each spawned grain.
val maxGrains: Int Maximum simultaneous grains.
val grainSizeJitter: Param Per-grain random grain-duration range in seconds (± this amount).
val densityJitter: Param Per-grain random inter-onset variation. 0 = steady, 1 = up to ±100%.
val gain: Param Base gain applied when a grain is spawned.
val gainJitter: Param Per-grain random gain variation as a fraction of gain.
val direction: GrainDirection Direction mode used when each grain is spawned.
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

StereoGrainCloud

class StereoGrainCloud(val source: AudioSample, val position: Param = Param(0.5f), val grainSize: Param = Param(0.05f), val density: Param = Param(20f), val pitch: Param = Param(1f), val pitchJitterCents: Param = Param(0f), val spray: Param = Param(0f), val spread: Param = Param(0.5f), val pan: Param = Param(0f), val stereoWidth: Param = Param(0f), val stereoWidthRandom: Param = Param(0f), val stereoScatter: Param = Param(0f), val envelope: GrainEnvelope = GrainEnvelope.HANN, val randomEnvelope: Boolean = false, val maxGrains: Int = 128, val grainSizeJitter: Param = Param(0f), val densityJitter: Param = Param(0f), val gain: Param = Param(1f), val gainJitter: Param = Param(0f), val direction: GrainDirection = GrainDirection.FORWARD) : StereoNode()

Declaration Summary
val source: AudioSample
val position: Param
val grainSize: Param
val density: Param
val pitch: Param
val pitchJitterCents: Param
val spray: Param
val spread: Param Stereo spread: 0 = mono center, 1 = full stereo.
val pan: Param Center pan for new grains (-1 = left, 0 = center, 1 = right).
val stereoWidth: Param Per-grain left/right read-head separation in seconds.
val stereoWidthRandom: Param Per-grain random variation added to stereoWidth, in seconds.
val stereoScatter: Param Independent per-channel source-position scatter in seconds.
val envelope: GrainEnvelope
val randomEnvelope: Boolean
val maxGrains: Int
val grainSizeJitter: Param
val densityJitter: Param
val gain: Param
val gainJitter: Param
val direction: GrainDirection
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

LiveGrainCloud

class LiveGrainCloud(val input: Signal, val position: Param = Param(0.5f), val grainSize: Param = Param(0.05f), val density: Param = Param(20f), val pitch: Param = Param(1f), val pitchJitterCents: Param = Param(0f), val spray: Param = Param(0f), val record: Param = Param(1f), val envelope: GrainEnvelope = GrainEnvelope.HANN, val randomEnvelope: Boolean = false, val maxGrains: Int = 128, val bufferSeconds: Float = 5f, val grainSizeJitter: Param = Param(0f), val densityJitter: Param = Param(0f), val gain: Param = Param(1f), val gainJitter: Param = Param(0f), val direction: GrainDirection = GrainDirection.FORWARD) : Signal()

Declaration Summary
val input: Signal
val position: Param Normalized delay through the live buffer: 0 = recent, 1 = oldest.
val grainSize: Param
val density: Param
val pitch: Param
val pitchJitterCents: Param
val spray: Param Random source-position scatter in seconds.
val record: Param Recording gate/gain for writing into the circular buffer.
val envelope: GrainEnvelope
val randomEnvelope: Boolean
val maxGrains: Int
val bufferSeconds: Float
val grainSizeJitter: Param
val densityJitter: Param
val gain: Param
val gainJitter: Param
val direction: GrainDirection
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

Signal.kt

The value types the rest of the synthesis category is written in, and the per-block contract they render under. Signal is a mono source rendered a block at a time; Param is a control value that is itself a Signal, so anything with a knob can be driven by anything that makes samples; SignalProcessor is a Signal that reads another one; StereoRenderable and StereoNode are the two-channel half.

SynthContext is what a block of rendering is handed: the sample rate and buffer size, how long the graph has been running, the Ableton Link reading taken at the top of the block, and a scratch-buffer pool to borrow from instead of allocating. Its cycleId is also the cache key that makes sharing safe, since Signal.render evaluates a node once per block and replays the result to every later reader. That is why val o = sine; o + o * 0.5f is one oscillator heard twice rather than two that have drifted apart.

Arithmetic on signals builds graph nodes rather than numbers, and / is overloaded twice: a Signal on the right divides sample by sample, a SignalProcessor on the right pipes into it. Most of the nodes those operators construct are private. Const, Mix, Pan and StereoSignal are public, for graphs you would rather assemble by hand.

Nothing here opens an audio device. Closing a graph with audio { … } and returning it from a box is what reaches FieldAudioEngine; synth.kt is the terse surface that does it.

src/jvmMain/kotlin/afield/audio/synth/Signal.kt — read the source

Used in: How to make sound in Field, Faust, A pattern that moves particles

Top-level functions

Declaration Summary
fun Signal.hold(): Signal Mono signals already memoize per audio block, so this is an intent marker.
operator fun Float.plus(sig: Signal): Signal
operator fun Float.times(sig: Signal): Signal
operator fun Float.minus(sig: Signal): Signal
operator fun Float.plus(param: Param): Param
operator fun Double.plus(sig: Signal): Signal
operator fun Double.times(sig: Signal): Signal
operator fun Double.minus(sig: Signal): Signal
operator fun Double.plus(param: Param): Param
operator fun Int.plus(param: Param): Param
operator fun Int.times(sig: Signal): Signal
fun StereoRenderable.hold(): StereoRenderable Gives a stereo source the per-block cache, wrapping it in a StereoHold only if it is not already a StereoNode.

SynthContext

class SynthContext(val sampleRate: Float, val bufferSize: Int)

The per-block runtime a signal graph renders against: the sample rate and buffer size it was opened with, time since it started, and whatever Ableton Link reported at the top of this block.

Declaration Summary
val sampleRate: Float Sample rate in Hz (e.g. 48000).
val bufferSize: Int Maximum buffer size: the number of frames rendered per audio-engine block.
val inverseSampleRate: Float
val nyquist: Float
var cycleId: Long Monotonically increasing id; bumped once per process cycle.
var time: Float Elapsed time in seconds since the synth started (beginning of current buffer).
var linkTiming: LinkTimingSnapshot? Latest Ableton Link timing snapshot captured at the beginning of this audio block.
val linkBeat: Double
val linkPhase: Double
val linkTempo: Double
val linkPeers: Long
fun warmUp(signal: Signal, cycles: Int = 15_000) Force-render a signal graph cycles times on the current thread.
fun acquireBuf(size: Int = bufferSize): FloatArray Borrow a buffer (at least size floats).
fun releaseBuf(buf: FloatArray) Return a buffer to the pool.
override fun toString(): String

Signal

abstract class Signal

One mono audio source, rendered a block at a time — the atom the whole synthesis category is built from.

Declaration Summary
fun render(output: FloatArray, numFrames: Int, ctx: SynthContext) Evaluate (or replay cache) into output. Call this from consuming nodes – never call generate directly.
operator fun plus(other: Signal): Signal
open operator fun plus(value: Double): Signal+1 overload · also Float
operator fun minus(other: Signal): Signal
open operator fun minus(value: Double): Signal+1 overload · also Float
operator fun times(other: Signal): Signal
open operator fun times(value: Double): Signal+1 overload · also Float
operator fun div(other: Signal): Signal
open operator fun div(value: Double): Signal+1 overload · also Float
operator fun div(processor: SignalProcessor): Signal
operator fun unaryMinus(): Signal
fun through(processor: SignalProcessor): Signal Pipe this signal through a SignalProcessor (filter, effect, …).
infix fun mix(other: Signal): Signal
override fun toString(): String

Param

open class Param(base: Float = 0f) : Signal()

A control value that is also a Signal, so anything a graph can take can be modulated.

Declaration Summary
var base: Float
var supplier: (SynthContext.() -> Float)? Optional lambda evaluated once per cycle to produce a target value. Receiver is SynthContext, giving access to SynthContext.time, SynthContext.sampleRate, etc.
var mod: Signal? Optional modulation signal added on top of base / supplier.
open override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
fun modulate(signal: Signal): Param Convenience: set modulation and return this for chaining.
override operator fun plus(value: Double): Param+1 overload · also Float
operator fun plus(value: Int): Param
override operator fun minus(value: Double): Param+1 overload · also Float
operator fun minus(value: Int): Param
override fun toString(): String

ParamScope

class ParamScope internal constructor()

The receiver inside Param { … }: the block reads the audio clock off it and hands values back through yield.

Declaration Summary
val context: SynthContext
val time: Float
val sampleRate: Float
val bufferSize: Int
val cycleId: Long
val inverseSampleRate: Float
val nyquist: Float
val linkTiming: LinkTimingSnapshot?
val linkBeat: Double
val linkPhase: Double
val linkTempo: Double
val linkPeers: Long
fun acquireBuf(size: Int = bufferSize): FloatArray
fun releaseBuf(buf: FloatArray)
suspend fun yield(value: Float)
suspend fun yieldAll(values: Iterator<Float>)+2 overloads · also Iterable<Float>, Sequence<Float>

SignalProcessor

abstract class SignalProcessor : Signal()

A Signal that reads another one: the base class every filter and effect in this category extends.

Declaration Summary
var input: Signal The signal feeding this processor.

Const

class Const(val value: Float) : Signal()

A signal that holds one number.

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

Mix

class Mix(vararg sources: Signal) : Signal(), LiveGraphInspectable

Sums any number of signals, with add to extend the set after construction.

Declaration Summary
fun add(signal: Signal): Mix
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>

StereoRenderable

interface StereoRenderable

Anything that can fill a left and a right buffer for one block — the stereo counterpart of Signal, and the currency of the stereo half of the graph: it is what AudioPatchSpec.from accepts, what a Pan or a Faust node is, and what the offline renderers take.

Declaration Summary
fun render(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)

StereoNode

abstract class StereoNode : StereoRenderable

Stereo equivalent of Signal's per-cycle memoization.

Declaration Summary
final override fun render(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

StereoHold

class StereoHold(private val source: StereoRenderable) : StereoNode(), LiveGraphInspectable

Wraps a bare StereoRenderable in StereoNode's per-block cache.

Declaration Summary
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>

StereoSignal

class StereoSignal(val left: Signal, val right: Signal) : StereoNode()

Two mono signals as a stereo pair: left into the left channel, right into the right, each rendered on its own.

Declaration Summary
val left: Signal
val right: Signal
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun toString(): String

Pan

class Pan(private val source: Signal, val pan: Param = Param(0f)) : StereoNode(), LiveGraphInspectable

Places a mono signal in the stereo image at pan: -1 hard left, 0 centred, +1 hard right, with anything outside that clamped.

Declaration Summary
val pan: Param
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>

Spectral.kt

The frequency-domain end of the synth: an FFT, two short-time transforms built on it, and a wavetable that keeps its waveforms as spectra.

FFT is radix-2 and in place, over interleaved [re, im, re, im, …] buffers whose length is a power of two. It is not a shared service: PhaseVocoder, SpectralFilter and SpectralWavetable each construct one at the size they work at.

PhaseVocoder and SpectralFilter have the same shape. A Hann window is applied on the way in and again on the way out, frames advance by fftSize / hopFactor samples, and the results are overlap-added into a ring that is then drained one sample per output frame. hopFactor is 4 by default, so four windows cover every sample. What they do in between differs. PhaseVocoder reads an AudioSample, and its two controls act on different quantities: timeStretch scales how far the analysis frame advances between hops while the output hop stays fixed, and pitchShift scales each bin's phase advance, so neither one implies the other. SpectralFilter reads its input and multiplies each bin by whatever gain the caller's transferFunction returns for that bin's centre frequency, mirroring the gain onto the negative-frequency half.

SpectralWavetable puts the same transform to a different use. It holds a fixed number of single-cycle frames, eight of 2048 samples by default, and keeps each one as both a waveform and its spectrum. Morphing between two frames interpolates the spectra rather than the waveforms, and band-limiting is then a matter of zeroing every bin above a harmonic limit before transforming back, so one table plays at any pitch with the harmonics that would fold removed. Writes and the frame copy that starts a morph both take a lock, so a box thread can rewrite a frame while the audio thread is playing the table.

SpectralWavetableRecorder and SpectralWavetableOsc are the two ends of that. The recorder passes its input through untouched and, captureRate times a second, blends the most recent frameSize samples of it into the one or two frames bracketing position, unless freeze is positive. The oscillator takes its harmonic limit from the frequency it is playing and the brightness control, renders the morphed waveform once per segment of segmentSize samples, and crossfades across the segment when position moved during it. A table also remembers the position the oscillator last asked for, and that is the position Visualizers.kt draws when a table is displayed in a box.

src/jvmMain/kotlin/afield/audio/synth/Spectral.kt — read the source

FFT

class FFT(val size: Int)

In-place radix-2 FFT and IFFT on interleaved complex buffers.

Declaration Summary
fun forward(data: FloatArray) In-place forward FFT. data length must be 2 * size (interleaved re/im).
fun inverse(data: FloatArray) In-place inverse FFT (with 1/N normalization).
fun realToComplex(real: FloatArray, complex: FloatArray, offset: Int = 0, len: Int = size) Pack a real signal into interleaved complex format (imaginary = 0).
fun complexToReal(complex: FloatArray, real: FloatArray, offset: Int = 0, len: Int = size) Extract real part from interleaved complex.
fun magnitude(complex: FloatArray, mag: FloatArray, len: Int = size) Compute magnitude spectrum.
fun phase(complex: FloatArray, phase: FloatArray, len: Int = size) Compute phase spectrum.
fun polarToComplex(mag: FloatArray, phase: FloatArray, complex: FloatArray, len: Int = size) Reconstruct complex from magnitude + phase.

PhaseVocoder

class PhaseVocoder(val source: AudioSample, val timeStretch: Param = Param(1f), val pitchShift: Param = Param(1f), val fftSize: Int = 2048, val hopFactor: Int = 4) : Signal()

Declaration Summary
val source: AudioSample
val timeStretch: Param Time-stretch ratio: >1 = slower, <1 = faster.
val pitchShift: Param Pitch-shift ratio: >1 = higher, <1 = lower.
val fftSize: Int
val hopFactor: Int
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SpectralFilter

class SpectralFilter(val fftSize: Int = 1024, val hopFactor: Int = 4, val transferFunction: (bin: Int, freqHz: Float) -> Float) : SignalProcessor()

Declaration Summary
val fftSize: Int
val hopFactor: Int
val transferFunction: (bin: Int, freqHz: Float) -> Float
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SpectralWavetable

class SpectralWavetable(val frameSize: Int = 2048, val frameCount: Int = 8, val removeDcOnWrite: Boolean = true, val normalizeOnWrite: Boolean = true)

Declaration Summary
val frameSize: Int
val frameCount: Int
val removeDcOnWrite: Boolean
val normalizeOnWrite: Boolean
var lastAccessPosition: Float
var lastWritePosition: Float
var lastHarmonicLimit: Int
fun seedClassicMorph(): SpectralWavetable
fun version(): Long
fun setFrame(index: Int, waveform: FloatArray, normalize: Boolean = normalizeOnWrite, removeDc: Boolean = removeDcOnWrite): SpectralWavetable
fun setFrame(index: Int, sample: AudioSample, position: Double = 0.0, cycleLength: Double = sample.lengthSamples.toDouble(), normalize: Boolean = normalizeOnWrite, removeDc: Boolean = removeDcOnWrite): SpectralWavetable
fun setFrame(index: Int, fn: (phase: Float) -> Float, normalize: Boolean = normalizeOnWrite, removeDc: Boolean = removeDcOnWrite): SpectralWavetable
fun capture(position: Float, waveform: FloatArray, blend: Float = 1f, normalize: Boolean = normalizeOnWrite, removeDc: Boolean = removeDcOnWrite): SpectralWavetable
fun frame(index: Int): FloatArray
fun waveformAt(position: Float = lastAccessPosition, harmonicLimit: Int = frameSize / 2): FloatArray
fun snapshot(position: Float = lastAccessPosition, harmonicLimit: Int = frameSize / 2): AudioSnapshot

SpectralWavetableRecorder

class SpectralWavetableRecorder(val table: SpectralWavetable, val position: Param = Param(0f), val blend: Param = Param(0.5f), val captureRate: Param = Param(16f), val inputGain: Param = Param(1f), val freeze: Signal = Const(0f), val normalizeWrites: Boolean = true, val removeDcWrites: Boolean = true) : SignalProcessor()

Declaration Summary
val table: SpectralWavetable
val position: Param Morph position to write to, normalized 0…1 across the table.
val blend: Param Blend amount into the addressed frame(s).
val captureRate: Param Capture updates per second.
val inputGain: Param Gain applied before writing into the table.
val freeze: Signal When positive, table writing is paused.
val normalizeWrites: Boolean
val removeDcWrites: Boolean
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

SpectralWavetableOsc

class SpectralWavetableOsc(val table: SpectralWavetable, val freq: Param = Param(110f), val position: Param = Param(0f), val phaseOffset: Param = Param(0f), val detuneCents: Param = Param(0f), val brightness: Param = Param(1f), val segmentSize: Int = 32) : Signal()

Declaration Summary
val table: SpectralWavetable
val freq: Param
val position: Param Morph position, normalized 0…1 across the table frames.
val phaseOffset: Param Phase offset in cycles.
val detuneCents: Param Fine detune in cents.
val brightness: Param 1 = full band-limited table, lower values darken the result.
val segmentSize: Int
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)

Subharmonic.kt

Subharmonic synthesis: follow the pitch of a roughly monophonic input and generate a new tone at a fraction of it — an octave down at division = 2.0, an octave and a fifth at 3.0, a fifth at 1.5.

Three trackers share one output stage (SubharmonicTracker):

  • SubharmonicZeroCross times the gap between positive-going zero crossings. Cheap and instant, and it believes every crossing, so harmonics and noise that add crossings make it jump.
  • SubharmonicPLL is a second-order phase-locked loop with a multiplying phase detector. It owns a continuous phase and frequency estimate, so the output glides instead of jumping, but its error signal scales with the input's level and carries a ripple at twice the input frequency.
  • SubharmonicQuadPLL first splits the input into a quadrature pair with HilbertPair, so it can measure the phase error directly — independent of level and free of that ripple — and adds a frequency discriminator that pulls the loop onto a new note quickly.

None of them divides the input's wrapped phase (sin(phase / 2) jumps by half a cycle every time the phase wraps). Each keeps its own oscillator advancing at the tracked rate divided by SubharmonicTracker.division, so division can be any number, changed every sample.

A tracker is a per-sample object: call SubharmonicTracker.process on each sample, or put it in a signal graph with Subharmonic. It returns the generated subharmonic only; mix the input back in yourself.

src/jvmMain/kotlin/afield/audio/synth/Subharmonic.kt — read the source

Used in: Subharmonic Synthesis: an Octave Below

SubharmonicTracker

abstract class SubharmonicTracker(val sampleRate: Double, var division: Double = 2.0)

The shared output stage of the subharmonic trackers: an oscillator advancing at the tracked rate divided by division.

Declaration Summary
val sampleRate: Double
var division: Double The ratio between the tracked frequency and the generated one. Any value, any time.
var voices: List<SubVoice> Several divisions at once, each with its own gain and phase. Empty (the default) plays the single division. listOf(SubVoice(2.0, 0.7), SubVoice(3.0, 0.2), SubVoice(4.0, 0.1)) is the octave, octave-and-fifth and two-octave stack.
val trackedHz: Double The input frequency the tracker currently believes in, in Hz (0 before it has one).
val level: Double The level the generated tone currently has before any voice gains.
open fun reset() Forget the tracked pitch, phases and level.
fun process(input: Float): Float Process one input sample and return the generated subharmonic.
fun process(input: FloatArray): FloatArray Run every sample of input through process.

SubVoice

data class SubVoice(val division: Double, val gain: Double = 1.0)

One division in a SubharmonicTracker.voices stack.

SubharmonicZeroCross

class SubharmonicZeroCross(sampleRate: Double = 48_000.0, division: Double = 2.0, minInputHz: Double = 40.0, maxInputHz: Double = 400.0, trackingTimeSeconds: Double = 0.03, amplitudeTimeSeconds: Double = 0.02) : SubharmonicTracker(sampleRate, division)

Tracks pitch by timing the interval between positive-going zero crossings.

Declaration Summary
override fun track(x: Double)
override fun reset()

SubharmonicPLL

class SubharmonicPLL(sampleRate: Double = 48_000.0, division: Double = 2.0, val minHz: Double = 35.0, val maxHz: Double = 350.0, val bandwidthHz: Double = 8.0, val damping: Double = 0.707, envelopeTimeSeconds: Double = 0.02, val initialHz: Double = 100.0) : SubharmonicTracker(sampleRate, division)

A second-order phase-locked loop with a multiplying phase detector.

Declaration Summary
val minHz: Double
val maxHz: Double
val bandwidthHz: Double
val damping: Double
val initialHz: Double
override fun track(x: Double)
override fun reset()

HilbertPair

class HilbertPair

A pair of allpass chains whose outputs stay 90° apart: i and q are the in-phase and quadrature parts of the analytic signal, so atan2(i, q) is the input's instantaneous phase and hypot(i, q) its instantaneous amplitude.

Declaration Summary
var i In-phase output: lags q by 90°.
var q Quadrature output.
fun process(x: Double)
fun reset()

SubharmonicQuadPLL

class SubharmonicQuadPLL(sampleRate: Double = 48_000.0, division: Double = 2.0, val minHz: Double = 35.0, val maxHz: Double = 350.0, val bandwidthHz: Double = 8.0, val damping: Double = 0.707, val acquisitionTimeSeconds: Double = 0.02, val gateLevel: Double = 0.01, attackTimeSeconds: Double = 0.005, releaseTimeSeconds: Double = 0.06, val initialHz: Double = 100.0) : SubharmonicTracker(sampleRate, division)

A phase-locked loop driven by the analytic signal rather than the raw input.

Declaration Summary
val minHz: Double
val maxHz: Double
val bandwidthHz: Double
val damping: Double
val acquisitionTimeSeconds: Double
val gateLevel: Double
val initialHz: Double
val phaseTracker Reusable analysis; process advances it once and retains the original sub oscillator.
val state: TrackedPhaseState The latest analysis, owned and updated by the audio thread.
val trackedPhase: Double
val phaseAdvance: Double
val confidence: Double
val locked: Boolean
override fun track(x: Double)
override fun reset()

Subharmonic

class Subharmonic(val tracker: SubharmonicTracker, val division: Signal? = null) : SignalProcessor()

A SubharmonicTracker as a node in a signal graph: bass.through(Subharmonic(SubharmonicPLL())) is the generated subharmonic of bass. The tracker's sample rate has to match the graph's.

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

SamplesSignal

class SamplesSignal(val samples: FloatArray) : Signal()

Recorded audio as a signal: plays samples once from the start, then silence.

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

SubharmonicSoundDocs.kt

Offline renders for the subharmonic page (public/docs/subharmonic_sound.md): every clip on it is one of the trackers in Subharmonic.kt run on a real input — a Kotlin bassline, Dirt-Samples one-shots from the pinned subset, or fourteen seconds of Night of the Living Dead (1968, public domain) committed beside the page.

The page's runner blocks build the same graphs and plot them with subRender; each scene here renders that graph again to a 16-bit WAV for the page's audio= fences. As a FIELD_TEST it renders every scene and checks the claim the page makes about it: that the quadrature PLL lands within a few hertz of each bass note, that the zero-crossing tracker jumps around on unfiltered speech and stops when the speech is lowpassed, and so on.

src/jvmMain/kotlin/afield/audio/synth/SubharmonicSoundDocs.kt — read the source

Used in: Subharmonic Synthesis: an Octave Below

Top-level functions

Declaration Summary
fun subRender(signal: Signal, seconds: Float, vararg watch: SubharmonicTracker, everySeconds: Double = 0.01, sampleRate: Int = SubharmonicSoundDocs.SR): SubRender Offline-render signal for seconds, reading SubharmonicTracker.trackedHz from each of watch every everySeconds (to the nearest 128-sample block; 0 reads after every block).
fun subBassNoteAt(t: Double, octave: Float = 1f): Double The note subBassline is playing at t seconds, times octave.
fun subBassline(octave: Float = 1f): Signal The page's bassline: a sawtooth stepping through SUB_BASS_NOTES, plucked once per note.
fun subBetween(from: Float, to: Float): Signal 1 from from to to seconds and 0 elsewhere: multiply by it to hear something only then.
fun subDirt(token: String, rate: Double = 1.0, sampleRate: Int = SubharmonicSoundDocs.SR): FloatArray The Dirt-Samples one-shot token ("arpy:2") from the pinned subset, folded to mono and resampled to sampleRate played back at rate (0.5 is an octave down and twice as long).
fun subSequence(seconds: Double, hits: List<Pair<Double, FloatArray>>, sampleRate: Int = SubharmonicSoundDocs.SR): FloatArray seconds of silence with each time to samples hit mixed in at its time.
fun subConcat(vararg parts: FloatArray): FloatArray The parts one after another.
fun subFilmClip(): FloatArray Night of the Living Dead (Romero, 1968), 6:05 to 6:19 — "They're coming to get you, Barbara" — from the public-domain print on the Internet Archive, mono at 48 kHz and peak-normalized to 0.9.

Top-level values

Declaration Summary
val SUB_BASS_NOTES The bass notes the page's bassline plays: E2 E2 A2 G2 D2 D2 G2 B2.
const val SUB_BASS_STEP Seconds per bassline note.

SubharmonicSoundDocs

object SubharmonicSoundDocs

Renders and checks the subharmonic page's audio scenes; see the file overview.

Declaration Summary
class Scene(val name: String, val render: () -> FloatArray, val verify: () -> Unit = {}) One clip: the audio it renders and the check that the page's sentence about it is true.
val SCENES: List<Scene>
fun phaseBankGraph(lattice: Boolean = false): Pair<Signal, HarmonicPhaseTracker> Recorded plucks: four seconds dry, then a tracked harmonic or rational bank.
fun trackersGraph(): Pair<Signal, List<SubharmonicTracker>> Four trackers on one bassline, each heard for three seconds.
fun divisionsGraph(): Pair<Signal, SubharmonicTracker> One tracker, six ways of turning its pitch into a tone.
fun kickGraph(): Pair<Signal, SubharmonicTracker> Twenty-four bd:3 kicks: dry, with the raw sub, with the saturated sub.
val ARPY_HZ Fundamentals of arpy:0..arpy:4 at their recorded speed, measured from the pinned files.
fun arpyGraph(): Pair<Signal, SubharmonicTracker> An arpy line an octave down, then highpassed with the sub underneath.
fun filmShapedGraph(): Pair<Signal, SubharmonicTracker> The film voice band-limited and squared up for the tracker, the sub scaled by the voice's level.
class NoteError(val median: Double, val settleMs: Double, val p90: Double)
fun noteError(run: SubRender, index: Int): NoteError How far tracker index in run sits from the bassline note over the second half of every note, and how long after each change it takes to come within 2 Hz (median over notes).
fun render(name: String): FloatArray Offline-render scene name: the samples its WAV holds, before the writer normalizes them.
fun renderWav(name: String, outPath: String) Offline-render scene name and write a mono 16-bit PCM WAV to outPath.
fun main(args: Array<String>)

SubRender

class SubRender(val audio: FloatArray, val times: List<Double>, val hz: List<List<Double>>, val subHz: List<List<Double>>)

An offline render plus what the watched trackers believed along the way.

Declaration Summary
val audio: FloatArray
val times: List<Double> Seconds, one entry per reading.
val hz: List<List<Double>> For each watched tracker, its SubharmonicTracker.trackedHz at each of times.
val subHz: List<List<Double>> For each watched tracker, the frequency it was generating: tracked Hz over its division.

TrackedPhase.kt

Pitch-tracked phase for voice banks: a tracker reduces one periodic sound to phase, phase advance, pitch, level and confidence, and a bank gives every voice its own phase accumulator.

Each voice advances by

theta += ratio * (omega + lock * (phaseAdvance - omega))

This supports undertones, harmonics and irrational ratios without dividing wrapped phase. lock = 0 follows stable pitch; lock = 1 also follows the tracker's phase corrections. It does not force a voice onto an absolute input phase, so changing a ratio preserves continuity.

QuadPhaseTracker is the analysis half of SubharmonicQuadPLL, sharing its HilbertPair and algorithm; Subharmonic keeps its own oscillator and level behaviour. HarmonicPhaseTracker supplies the same state from several partials, including a missing fundamental. Neither tracker separates polyphonic sources.

Trackers, states and voices belong to the audio thread. TrackedPhaseAnalysis caches each sample's state for graph fan-out; reading a live tracker from the Runloop must never advance it. Per-sample paths reuse their storage. Custom renderers must handle their own bandwidth; the bank's Nyquist fade protects the fundamental of each sine voice only.

src/jvmMain/kotlin/afield/audio/synth/TrackedPhase.kt — read the source

TrackedPhaseState

class TrackedPhaseState(val sampleRate: Double)

Mutable, reused per-sample state published by a PhaseTracker.

Declaration Summary
val sampleRate: Double
var phase: Double
var phaseAdvance: Double
var omega: Double
var level: Double
var confidence: Double
var gateOpen: Boolean
var locked: Boolean
var harmonicity: Double Harmonic power coverage, or phase agreement for the quadrature tracker.
var activePartials: Int Number of usable analysis partials on this sample.
val hz: Double
val phaseAdvanceHz: Double
override fun toString(): String

PhaseTracker

interface PhaseTracker

Analysis half of the system. Implementations update one reused state for every sample.

Declaration Summary
val sampleRate: Double
val state: TrackedPhaseState
fun process(input: Float): TrackedPhaseState
fun reset()

QuadPhaseTracker

class QuadPhaseTracker(override val sampleRate: Double = 48_000.0, val minHz: Double = 35.0, val maxHz: Double = 350.0, val bandwidthHz: Double = 8.0, val damping: Double = 0.707, val acquisitionTimeSeconds: Double = 0.02, val gateLevel: Double = 0.01, attackTimeSeconds: Double = 0.005, releaseTimeSeconds: Double = 0.06, val initialHz: Double = 100.0, val lockThreshold: Double = 0.9) : PhaseTracker

Quadrature analysis shared with SubharmonicQuadPLL; use a band-limited monophonic input.

Declaration Summary
override val sampleRate: Double
val minHz: Double
val maxHz: Double
val bandwidthHz: Double
val damping: Double
val acquisitionTimeSeconds: Double
val gateLevel: Double
val initialHz: Double
val lockThreshold: Double
override val state
var locked True while the input is above gateLevel and the loop is in phase with it.
override fun process(input: Float): TrackedPhaseState
override fun reset()

TrackedPhaseBank.kt

An oscillator bank that follows a tracked phase: TrackedPhaseBank renders a list of PhaseVoices, each advancing at a ratio of the pitch a PhaseTracker reports.

Each voice keeps its own continuous phase accumulator, so fractional and irrational ratios stay continuous, and a PhaseRenderer turns that phase into a sample (a sine by default).

src/jvmMain/kotlin/afield/audio/synth/TrackedPhaseBank.kt — read the source

Used in: Subharmonic Synthesis: an Octave Below

PhaseRenderer

fun interface PhaseRenderer

Rendering function for one bank voice.

Declaration Summary
fun render(phase: Double): Double

PhaseVoice

class PhaseVoice(var ratio: Double, var gain: Double = 1.0, var phaseOffset: Double = 0.0, var lock: Double = 1.0, var renderer: PhaseRenderer = PhaseRenderer.SINE)

One trajectory in a TrackedPhaseBank.

Declaration Summary
var ratio: Double
var gain: Double
var phaseOffset: Double
var lock: Double
var renderer: PhaseRenderer
var phase: Double
var lastOutput: Double
fun reset(phase: Double = 0.0)
override fun toString(): String

TrackedPhaseBank

class TrackedPhaseBank(val tracker: PhaseTracker? = null, val voices: MutableList<PhaseVoice> = mutableListOf(), var followInputLevel: Boolean = true, var outputGain: Double = 1.0, val nyquistFadeStart: Double = 0.85, val nyquistFadeEnd: Double = 0.98)

Fan one tracked phase state out into many independently accumulated trajectories.

Declaration Summary
val tracker: PhaseTracker?
val voices: MutableList<PhaseVoice>
var followInputLevel: Boolean
var outputGain: Double
val nyquistFadeStart: Double
val nyquistFadeEnd: Double
fun process(input: Float): Float Analyze input with the owned tracker and render the mono sum.
fun render(state: TrackedPhaseState): Double Render all voices from one already-computed tracked state and return their mono sum.
fun renderVoices(state: TrackedPhaseState, out: DoubleArray): Double Render each voice separately into caller-owned out.
fun reset()

TrackedPhaseSignal.kt

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.

src/jvmMain/kotlin/afield/audio/synth/TrackedPhaseSignal.kt — read the source

TrackedPhaseAnalysis

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.

Declaration Summary
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext)
fun synthesize(bank: TrackedPhaseBank): Signal Render a bank from this analysis. Each output needs its own bank and voice instances.

Visualizers.kt

How to look at the audio. Every function here is an extension on a type declared elsewhere in this category, and none of them changes what you hear.

Most of it happens without being asked for. registerSynthVisualizers runs on each new root box and registers a value printer for AudioPatchSpec, Scope, StereoScope, SpectralWavetable, ADSR, AR, BreakpointEnvelope, EnvelopeFollower, Compressor and Limiter, so returning one of those from a box draws it. The six envelope and dynamics types can be metered at all because each keeps a trace of its own state as it runs; anything else has to be tapped.

Tapping means a Scope. It passes its input through untouched while recording it into a ring buffer, so saw.scope() sits anywhere in a graph without altering it, and snapshot() lifts a still AudioSnapshot out of the ring. From a snapshot come the three renderings the file offers everywhere: waveformSvg, spectrumSvg, and summaryHtml for the two side by side.

The live* family is those same renderings on a timer. Each starts a coroutine on the current box that re-renders at refreshHz, clamped between 1 and 60, and displays into a named channel, then returns its own receiver so the call can sit in the middle of an expression. They need a box context and throw without one. One job is kept per box and channel, a second install cancelling the first, and the job is registered with the box so it stops when the box does.

src/jvmMain/kotlin/afield/audio/synth/Visualizers.kt — read the source

Used in: Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun Signal.scope(historySize: Int = 4096): Scope
fun StereoRenderable.scope(historySize: Int = 4096): StereoScope
fun AudioPatchSpec.snapshot(): AudioSnapshot
fun AudioPatchSpec.waveformSvg(widthPx: Int = 480, heightPx: Int = 96, title: String = "Waveform (Patch)", background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun AudioPatchSpec.spectrumSvg(widthPx: Int = 480, heightPx: Int = 96, title: String = "Spectrum (Patch)", background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun AudioPatchSpec.summaryHtml(widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun AudioPatchSpec.liveSummary(channel: String = "audio", refreshHz: Double = 12.0, widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96): AudioPatchSpec
fun Scope.snapshot(sampleRate: Int = sampleRateHint): AudioSnapshot
fun Scope.waveformSvg(widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): String
fun Scope.spectrumSvg(widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): String
fun Scope.summaryHtml(widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, sampleRate: Int = 48_000): String
fun Scope.liveWaveform(channel: String = "scope", refreshHz: Double = 30.0, widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): Scope
fun Scope.liveSpectrum(channel: String = "scope", refreshHz: Double = 30.0, widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): Scope
fun Scope.liveSummary(channel: String = "scope", refreshHz: Double = 12.0, widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, sampleRate: Int = 48_000): Scope
fun StereoScope.snapshot(sampleRate: Int = sampleRateHint): AudioSnapshot
fun StereoScope.waveformSvg(widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): String
fun StereoScope.spectrumSvg(widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): String
fun StereoScope.summaryHtml(widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, sampleRate: Int = 48_000): String
fun StereoScope.liveWaveform(channel: String = "stereo", refreshHz: Double = 12.0, widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): StereoScope
fun StereoScope.liveSpectrum(channel: String = "stereo", refreshHz: Double = 12.0, widthPx: Int = 480, heightPx: Int = 96, sampleRate: Int = 48_000): StereoScope
fun StereoScope.liveSummary(channel: String = "stereo", refreshHz: Double = 12.0, widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, sampleRate: Int = 48_000): StereoScope
fun SpectralWavetable.waveformSvg(position: Float = lastAccessPosition, harmonicLimit: Int = frameSize / 2, widthPx: Int = 480, heightPx: Int = 96): String
fun SpectralWavetable.spectrumSvg(position: Float = lastAccessPosition, harmonicLimit: Int = frameSize / 2, widthPx: Int = 480, heightPx: Int = 96): String
fun SpectralWavetable.summaryHtml(position: Float = lastAccessPosition, harmonicLimit: Int = frameSize / 2, widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96): String
fun ADSR.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun AR.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun BreakpointEnvelope.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, fontFamily: String = DEFAULT_FONT): String
fun EnvelopeFollower.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, fontFamily: String = DEFAULT_FONT): String
fun Compressor.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun Limiter.stateSvg(widthPx: Int = 480, heightPx: Int = 84, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun ADSR.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun AR.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun BreakpointEnvelope.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun EnvelopeFollower.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun Compressor.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun Limiter.stateHtml(widthPx: Int = 480, heightPx: Int = 84): String
fun registerSynthVisualizers()

AudioSnapshot

data class AudioSnapshot(val left: FloatArray, val right: FloatArray? = null, val sampleRate: Int = 48_000)

A still copy of some audio: left, an optional right, and the sampleRate the samples were taken at.

Declaration Summary
val left: FloatArray
val right: FloatArray?
val sampleRate: Int
val frameCount: Int
val isStereo: Boolean
fun waveformSvg(widthPx: Int = 480, heightPx: Int = 96, title: String = if (isStereo) "Waveform (Stereo)" else "Waveform", background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun spectrumSvg(widthPx: Int = 480, heightPx: Int = 96, title: String = if (isStereo) "Spectrum (Stereo)" else "Spectrum", background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
fun summaryHtml(widthPx: Int = 640, waveformHeightPx: Int = 96, spectrumHeightPx: Int = 96, background: String = DEFAULT_BACKGROUND, primaryColor: String = SYNTH_BLUE, secondaryColor: String = FIELD_ORANGE, fontFamily: String = DEFAULT_FONT): String
override fun toString(): String

Scope

class Scope(historySize: Int = 4096) : SignalProcessor()

A tap on a mono signal: passes its input through unchanged and keeps the last historySize samples of what went by.

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

StereoScope

class StereoScope(private val source: StereoRenderable, historySize: Int = 4096) : StereoNode(), LiveGraphInspectable

The stereo tap. It wraps a StereoRenderable rather than sitting in a mono chain, and keeps both channels.

Declaration Summary
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>

synth.kt

The terse surface a box imports: audio { … } to close a graph, .p to make a number a Param, and lowercase names for the things reached for constantly.

Field's default preamble carries import afield.audio.synth.*, so everything below is already in scope in any box. audio { … } wraps a mono or stereo graph as an AudioPatchSpec; return one as a box's last expression and the shared engine picks it up. Its overloads cover value and lambda, mono and stereo, and a Float or a Param for the gain, which is why audio { saw / lpf(800) * 0.1f } and audio(mySignal, gain = 0.2f) are the same function.

The rest of this file is a tour of the surface, family by family, with a worked snippet for each.

Core ideas:

  • Plain numbers can become Params with .p
  • Lowercase vals such as saw, sine, white, gate, and audioIn return fresh instances each time they are referenced
  • / chains a Signal into a SignalProcessor
  • audio { ... } wraps a mono or stereo graph as something launchable
  • .onBeat(...) quantizes any launchable return value to the next musical boundary
  • Mono signals cache automatically for the duration of an audio block; built-in stereo nodes do the same, and hold() is available for arbitrary stereo renderables
  • Returning a Signal, Pan, StereoSignal, StereoGrainCloud, or AudioPatchSpec from a launched box inserts it into Field's shared audio engine

Basic oscillators

A bare oscillator:

sine

Set frequency with Params:

SineOsc(220.p)
SawOsc(55.p)
PulseOsc(freq = 110.p, width = 0.2.p)

Since the lowercase vals are getters, these are different oscillators:

saw + saw
sine * 0.1f + square * 0.1f

Launchable patches

Return a signal directly from a box:

saw * 0.05f

Or be explicit:

audio { saw * 0.05f }

Stereo output with panning:

audio { (saw * 0.1f).pan(-0.35f) }
audio { Pan(sine * 0.08f, 0.4.p) }

Launch more than one thing from a box either with +:

audio { saw * 0.05f } +
    pat("bd [hh hh] sd").respond(cps = 0.5) { event ->
        __m("pat").patHighlight(event)
    }

Or by returning a list:

listOf(
    audio { saw * 0.05f },
    pat("bd [hh hh] sd").respond(cps = 0.5) { event ->
        __m("pat").patHighlight(event)
    }
)

Quantized launch timing

Delay any launchable return value until the next clock boundary:

audio { saw * 0.05f }.onBeat(every = 4, preroll = 0.02)

This also works for bundles:

(audio { saw * 0.05f } +
    pat("bd [hh hh] sd").respond(cps = 0.5) { event ->
        __m("pat").patHighlight(event)
    }).onBeat(every = 4, preroll = 0.02)

In frames {} / loop {} code you can suspend directly:

frames {
    waitForBeat(every = 4, preroll = 0.02)
    println("downbeat")
}

every is measured in the chosen clock's units, so every = 4 means every fourth rollover of that clock. Use a subdivided clock when you need finer boundaries, or a slower clock / larger every when you want measure-level quantization.

Params and modulation

.p is the quickest way to build Params:

440.p
0.15.p
8.p

Params can also be dynamic:

val cutoff = Param { 400f + 300f * kotlin.math.sin(time * 0.3f) }
saw / MoogLadder(cutoff = cutoff, resonance = 0.2.p)

Or stepped from a sequence, one value per audio block:

val seq = Param(sequenceOf(220f, 330f, 440f, 550f))
SineOsc(seq)

Or as a coroutine-style generator that can yield(...) values and see the current audio-block time directly:

val seq = Param {
    var t = 0
    while (true) {
        t++
        if (t > 10) t = 0
        yield(10f - t + 0.15f * kotlin.math.sin(time * 0.5f))
    }
}
SuperSaw() * seq * 0.05f

A Param can be modulated by another signal:

val freq = 220.p.modulate(sine * 20f)
SineOsc(freq)

Chaining with /

/ keeps processing terse:

saw / Delay()
saw / FreeVerb()
white / SVF(mode = SVFMode.BP, cutoff = 1200.p, resonance = 0.6.p)

Longer chains are fine:

saw / MoogLadder(cutoff = 800.p) / Chorus() / FreeVerb()

Fractional delay is available when you need tuned delay times or resonant feedback structures:

saw / FractionalDelay(delayTime = 0.0125.p, feedback = 0.7.p, mix = 0.6.p)

Remember that signal / signal is still division. Chaining only happens when the right-hand side is a SignalProcessor.

Envelopes and gates

Quick percussive patch:

val g = gate.also { it.open() }
SawOsc(80.p) * AR(gate = g, attack = 0.001.p, release = 0.15.p)

Sustained voice:

val g = gate.also { it.open() }
SineOsc(220.p) * ADSR(
    gate = g,
    attack = 0.01.p,
    decay = 0.08.p,
    sustain = 0.6.p,
    release = 0.3.p
)

Gate can be kept in a top-level var and opened/closed from later fragments:

var g = gate
g.open()
SineOsc(110.p) * ADSR(gate = g, release = 0.5.p)

Later:

g.close()

Mixing and stereo

Layered voices:

Mix(
    SawOsc(110.p),
    SawOsc(110.5.p),
    SineOsc(55.p) * 0.3f
) * 0.08f

Manual left/right signal:

StereoSignal(
    left = saw * 0.08f,
    right = tri * 0.08f
)

Mono source placed in the stereo field:

Pan(white / FreeVerb(), Param(-0.2f))

Audio input

Monitor mono input:

audioIn * 0.8f

Process input with effects:

audioIn / Delay(delayTime = 0.2.p, feedback = 0.35.p, mix = 0.4.p)

Stereo input:

audio(gain = 0.9f) { audioInStereo }

Input into a filter and reverb:

audio(gain = 0.7f) { audioIn / MoogLadder(cutoff = 1800.p) / FreeVerb() }
val link = abletonLink()
println(link.snapshot())

val syncedWobble = Param { linkPhase.toFloat() }
audio { saw(110) * (0.05f + syncedWobble * 0.05f) }

linkAudioOut()
audio { saw * 0.05f }

audio(gain = 0.8f) { linkAudioInStereo() }

Granular and sampling examples

Sample playback:

val sample = AudioSample.generate(0.25f) { t ->
    kotlin.math.sin((t * 55f * 2f * kotlin.math.PI.toFloat()).toDouble()).toFloat()
}
Sampler(sample) * 0.5f

Granular cloud:

val sample = AudioSample.generate(1f) { t ->
    kotlin.math.sin((t * 220f * 2f * kotlin.math.PI.toFloat()).toDouble()).toFloat()
}
GrainCloud(
    sample,
    density = 18.p,
    position = 0.35.p,
    grainSize = 0.08.p,
    grainSizeJitter = 0.025.p,
    densityJitter = 0.5.p,
    gainJitter = 0.35.p,
    direction = GrainDirection.BIDIRECTIONAL
)

Stereo cloud:

val sample = AudioSample.generate(1.5f) { t ->
    kotlin.math.sin((t * 110f * 2f * kotlin.math.PI.toFloat()).toDouble()).toFloat()
}
StereoGrainCloud(sample, density = 12.p, spread = 0.7.p)

Live input cloud:

audio {
    LiveGrainCloud(
        audioIn,
        bufferSeconds = 6f,
        position = 0.35.p,
        density = 24.p,
        grainSize = 0.07.p,
        grainSizeJitter = 0.03.p,
        densityJitter = 0.6.p,
        pitchJitterCents = 300.p,
        gainJitter = 0.4.p,
        direction = GrainDirection.BIDIRECTIONAL
    ) * 0.6f
}

Dirt sample library and polyphonic player:

val lib = dirt("/path/to/Dirt-Samples")
val kit = dirtPlayer(library = lib, voices = 24)
audio { kit }
kit.trigger("bd:3")

Pattern glue for real Dirt tokens:

val lib = dirt("/path/to/Dirt-Samples")
val kit = dirtPlayer(library = lib)
audio { kit }
pat("bd:3 [hh*2|hh:3*2] sn:1").playDirt(kit, cps = 2.0)

FM examples

A simple operator:

FMOperator(freq = 220.p, feedbackAmount = 0.2f)

Patch-style FM:

FMPatch.twoOp(
    carrierFreq = 110f,
    index = 3.p
) * 0.08f

String and unison synthesis

Tuned Karplus-Strong resonator driven by a short noise burst:

val g = gate.also { it.open() }
val burst = white * AR(gate = g, attack = 0.0005.p, release = 0.008.p)
audio { burst / KarplusStrong(freq = 110.p, decay = 0.992.p, brightness = 0.6.p) }

Hammer-like excitation from an oscillator click:

val g = gate.also { it.open() }
val hammer = impulse * AR(gate = g, attack = 0.0001.p, release = 0.002.p) * 0.8f
audio { hammer / KarplusStrong(freq = 220.p, pickPosition = 0.08.p, brightness = 0.75.p) }

SuperSaw:

supersaw
SuperSaw(freq = 55.p, detuneCents = 18.p, blend = 0.9.p)
audio { SuperSaw(freq = 110.p, detuneCents = 24.p, blend = 0.85.p) / MoogLadder(cutoff = 1400.p) }

Tidal-inspired super voices:

supersquare
SuperSquare(freq = 110.p, voice = 0.42.p, lfo = 0.8.p)
audio { SuperPWM(freq = 82.p, voice = 0.18.p, secondarySweep = 0.72.p) * 0.08f }
audio { SuperComparator(freq = 55.p, voice = 0.7.p, resonance = 0.3.p) * 0.06f }
audio { SuperChip(freq = 110.p, pitch2 = 2.p, pitch3 = 3.01.p, voice = 0.35.p) * 0.08f }
audio { SuperNoise(freq = 90.p, voice = 0.4.p, resonance = 0.2.p) * 0.12f }
audio { SuperFork(freq = 440.p) * 0.08f }

Distortion and folding

Gentle full-wave rectification:

audio { saw / Rectifier(mode = RectifierMode.FULL_WAVE, mix = 0.35.p) * 0.08f }

Half-wave clipping into a filter:

audio { sine * 3f / Rectifier(RectifierMode.HALF_WAVE) / SVF(mode = SVFMode.LP, cutoff = 900.p) * 0.05f }

Multi-stage folding with asymmetry:

audio {
    sine * 4f /
        StagedWaveFolder(stages = 4, symmetry = 0.18.p, stageGain = 1.5.p, makeup = 0.4.p) *
        0.08f
}

Spectral wavetable synthesis

Make a wavetable and inspect it:

val wt = wavetable
display("wt", wt)
wt

Use it as a morphable oscillator:

val wt = wavetable
audio {
    SpectralWavetableOsc(
        wt,
        freq = 110.p,
        position = Param { 0.5f + 0.45f * kotlin.math.sin(time * 0.18f) }
    ) * 0.08f
}

Rewrite the table from a live source while listening to the result:

val wt = wavetable
audio { audioIn / SpectralWavetableRecorder(wt, position = 0.2.p, captureRate = 6.p) }
audio {
    SpectralWavetableOsc(
        wt,
        freq = 55.p,
        position = Param { 0.2f + 0.6f * kotlin.math.sin(time * 0.09f) }
    ) / FreeVerb() * 0.08f
}

You can also write explicit frames:

val wt = wavetable
wt.setFrame(0, fn = { phase: Float ->
    kotlin.math.sin((phase * kotlin.math.PI.toFloat() * 2f).toDouble()).toFloat()
})
wt.setFrame(1, fn = { phase: Float -> if (phase < 0.5f) 1f else -1f })
SpectralWavetableOsc(wt, freq = 220.p, position = 0.5.p)

Scheduled notes and piano roll

Build a memoized note process:

val arp = noteProcess {
    val chord = listOf(48, 55, 60, 64)
    var step = 0
    while (true) {
        note(
            pitch = chord[step % chord.size],
            duration = 0.18,
            velocity = 0.85,
            label = "n$step"
        )
        rest(0.25)
        step++
    }
}

Move the playhead and inspect the roll in an output area:

arp.at(3.5)
display("roll", arp.pianoRollSvg())

Inline in the editor, in the style of history / histogram:

__m("arp").pianoRoll(arp.at(4.0))

Chords and explicit timing are supported inside the process:

val motif = notes {
    chord(60, 64, 67, durationSeconds = 0.35, velocity = 0.7)
    rest(0.5)
    note(pitch = 72, duration = 0.2, velocity = 1.0, label = "lead")
    rest(0.25)
    note(pitch = 67, duration = 0.45, start = 1.2)
}

You can also render plain note collections without a process:

val clip = listOf(
    note(60, duration = 0.25, start = 0.0),
    note(64, duration = 0.25, start = 0.25),
    note(67, duration = 0.5, start = 0.5)
)
display("clip", clip.pianoRollSvg(nowSeconds = 0.4, futureSeconds = 1.2))

Note processes are resettable and memoized, so the same future schedule can drive playback logic and visual inspection without re-running the generator for every query.

Tidal-style patterns

Parse a compact pattern language into timed atom events:

val hats = pat("bd [hh hh] sd")
hats.scheduled(cps = 2.0).take(4).toList()

By default, pattern scheduling uses the ambient PatClock in box context if one exists, otherwise the shared musicClock(). Pass clock = null if you want a zero-based schedule instead.

Sync multiple patterns against a shared musical clock:

val master = musicClock()
val sixteenth = master.subdivide(4.0)
pat("bd [hh hh] sd").scheduled(cps = 0.5, clock = sixteenth).take(8).toList()

The shared musicClock() runs at real time, so cps = 1.0 means one cycle per second, which is 60 cycles per minute. If you treat one cycle as a beat, that is 60 BPM.

Clocks can be paused and resumed. Already-dispatched future callbacks are not recalled when a clock pauses:

val master = musicClock()
master.pause()
master.start()

Map atoms into notes so they feed the existing piano-roll / note-process path:

val degrees = pat("0 3 5 7")
val bass = degrees.asNotes(cps = 2.0) { event ->
    val degree = event.value.toIntOrNull() ?: return@asNotes emptyList()
    listOf(
        note(
            pitch = 36 + degree,
            duration = event.duration * 0.95,
            start = event.start,
            label = event.value
        )
    )
}

Or respond directly to the atoms as a launchable event source:

pat("kick <snare rim>").respond(cps = 2.0) { event ->
    box.display("atoms", event.value.toString())
}

Or run the callback slightly ahead of the target event time:

pat("kick <snare rim>").respondAhead(ahead = 0.05, cps = 2.0, clock = musicClock()) { event ->
    box.display("atoms", "${event.value} in ${event.start - now}")
}

The responder callback gets the atom plus timing metadata (start, duration, cycleIndex) and a box reference, so it can be used for audio triggers, visuals, automation, or arbitrary live-coded side effects.

Patterns can also become scalar functions over time. The first shape is a palette of finite-support atoms:

val fn = patFn(
    "g:(0,0.12) tent:(1,0.08,0.8) wendland:(-0.5,0.2,0.4)",
    cps = 2.0,
    range = 0.9
)

fn(0.0)
fn(0.5)

g:(value,sigma) defaults the weight to 1.0. The current shapes are: g, tent, box, wendland, spline, env, and adsr.

Or directly from an already-parsed pattern:

val field = pat("env:(0.7,0.03,0.12) adsr:(0.4,0.01,0.08,0.5,0.2)")
    .asFn(cps = 1.5, range = 0.4)

Or map atoms into typed late-bound function specs:

val animated = pat("a b c") { atom ->
    when (atom) {
        "a" -> gaussianAtom(sigma = 0.08) { x -> kotlin.math.sin(local(x) * 12.0) }
        "b" -> gaussianAtom(sigma = 0.12) { x -> bipolarRandom() * (1.0 - normalized(x)) }
        else -> adsrAtom(attack = 0.01, decay = 0.08, sustain = 0.4, release = 0.2) { x ->
            0.5 + 0.5 * noise(x, frequency = 18.0)
        }
    }
}.asFn(cps = 2.0, maxLookAhead = 0.5)

Graph queries and ParamSet surgery

Find params by path. Note the odd-looking concatenation: this cookbook is one big block comment, and writing the recursive glob as a single literal would put a star-slash in the middle of it, closing the comment right here and breaking the file. In your own code (and in public/docs/graph_query.md) write the glob directly as one string — do not copy the concatenation out of here.

val gains = someSignal matches ("**" + "/gain")
gains

Tweak a whole set together:

(someSignal matches ("**" + "/gain")).set(0.15f)
(someSignal matches ("**" + "/freq")).scale(0.5f)

Randomize and shape a set:

val cutoffs = someSignal matches ("**" + "/cutoff")
cutoffs.random(200f..4000f)
cutoffs.rewrite {
    normalize(300f..1800f)
    jitter(60f)
}

Add shared or per-parameter modulation:

(someSignal matches ("**" + "/gain")).modulate(sine * 0.1f, mode = ParamModulationMode.ADD)

(someSignal matches ("**" + "/freq")).modulate { entry, index ->
    SineOsc((0.08f + index * 0.03f).p) * (entry.base * 0.02f)
}

Drive a set from dynamic supplier expressions:

val freqs = someSignal matches ("**" + "/freq")
freqs.supply { entry, index, group ->
    group.mean + kotlin.math.sin(time * (0.2f + index * 0.05f)) * 12f
}

If you want the generic path wrapper instead of only Params:

val paths = someSignal.graphPaths("**")
paths
paths.matching("**" + "/input").writable()

Visualizers and scopes

Audio patches retain a recent stereo output history, so once a patch is running you can inspect it directly:

val patch = audio(gain = 0.1f) { saw / FreeVerb() }
display("audio", patch.summaryHtml())
patch

If you want to tap an internal point in the graph, use a Scope:

val sc = (saw / MoogLadder(cutoff = 900.p)).scope()
audio { sc * 0.1f }
sc.liveWaveform("scope")

Stereo graphs can be tapped too:

val sc = audioInStereo.scope()
audio(gain = 0.8f) { sc }
sc.liveSpectrum("stereo")

waveformSvg() and spectrumSvg() are snapshots of the current trace. If you call them before the scoped signal has actually been rendered, they will say "Waiting for audio". Use liveWaveform(...), liveSpectrum(...), or liveSummary(...) when you want the output area to keep updating while audio runs.

Stateful processors expose compact state views:

val env = ADSR(gate = gate.also { it.open() }, release = 0.4.p)
display("env", env.stateSvg())

val comp = Compressor(threshold = (-18).p, ratio = 6.p)
display("comp", comp.stateSvg())
audio { audioIn / comp }

Engine access

If you need explicit engine access:

val engine = audioEngine()
val handle = engine.register(audio { saw * 0.05f })
handle.close()

Most live coding should just return the graph from the box and let Field's launch/stop manage registration automatically.

src/jvmMain/kotlin/afield/audio/synth/synth.kt — read the source

Used in: Pure Data, How to make sound in Field

Top-level functions

Declaration Summary
fun audioEngine(sampleRate: Int = 48_000, bufferSize: Int = 512, queuedBufferCount: Int = 12, inputChannels: Int = 2): FieldAudioEngine The process-wide FieldAudioEngine, started if it was not already running.
fun audio(signal: Signal, gain: Param = Param(1f), pan: Param = Param(0f), warmUpCycles: Int = 0): AudioPatchSpec+1 overload · also Float
inline fun audio(gain: Float = 1f, pan: Float = 0f, warmUpCycles: Int = 0, block: () -> Signal): AudioPatchSpec+1 overload · also Param
fun audio(signal: StereoRenderable, gain: Param = Param(1f), warmUpCycles: Int = 0): AudioPatchSpec+1 overload · also Float
inline fun audio(gain: Float = 1f, warmUpCycles: Int = 0, block: () -> StereoRenderable): AudioPatchSpec+1 overload · also Param
fun audioInput(): Signal
fun audioInputStereo(): StereoSignal
fun abletonLink(tempo: Double = 120.0, peerName: String = "Field4", quantum: Double = 4.0): LinkAudioInterface
fun linkTiming(tempo: Double = 120.0, peerName: String = "Field4", quantum: Double = 4.0): LinkTimingSnapshot
fun linkAudioOut(name: String = "Field4", tempo: Double = 120.0, peerName: String = "Field4", quantum: Double = 4.0, monitorLocal: Boolean = true, engine: FieldAudioEngine = audioEngine()): LinkAudioInterface
fun linkAudioIn(channelName: String? = null): Signal
fun linkAudioInStereo(channelName: String? = null): StereoSignal
fun linkAudioInput(channelName: String? = null): Signal
fun linkAudioInputStereo(channelName: String? = null): StereoSignal
fun Signal.pan(position: Param = Param(0f)): Pan+1 overload · also Float
fun openGate(): Gate A gate that is immediately open
fun sine(freq: Number): SineOsc
fun saw(freq: Number): SawOsc
fun square(freq: Number): SquareOsc
fun tri(freq: Number): TriOsc
fun pulse(freq: Number, width: Number = 0.5): PulseOsc
fun fm(freq: Number, ratio: Number = 2, index: Number = 2, feedback: Number = 0): Signal fm(110, index = 3) — twoOp FM patch
fun ar(attack: Number = 0.001, release: Number = 0.15, gate: Gate = openGate()): Signal AR envelope with auto-opened gate
fun adsr(a: Number = 0.01, d: Number = 0.1, s: Number = 0.7, r: Number = 0.3, gate: Gate = openGate()): Signal ADSR envelope with auto-opened gate
fun perc(release: Number = 0.15, attack: Number = 0.001, gate: Gate = openGate()): Signal Percussive hit — short attack, variable release
fun lpf(cutoff: Number = 1000, resonance: Number = 0.2): MoogLadder+1 overload · also Param
fun hpf(cutoff: Number = 1000, resonance: Number = 0.5): SVF
fun bpf(cutoff: Number = 1000, resonance: Number = 0.5): SVF Creates a band-pass SVF filter centered on cutoff, with resonance shaping the band width.
fun reverb(roomSize: Number = 0.8, damping: Number = 0.5, mix: Number = 0.33): FreeVerb Creates a FreeVerb reverb with room size, damping, and dry/wet mix controls.
fun springReverb(tension: Number = 0.55, decay: Number = 0.72, tone: Number = 0.65, mix: Number = 0.28): SpringReverb
fun spring(tension: Number = 0.55, decay: Number = 0.72, tone: Number = 0.65, mix: Number = 0.28): SpringReverb
fun convolutionReverb(impulse: AudioSample, mix: Number = 0.35, gain: Number = 1.0, normalize: Boolean = true, maxIrSamples: Int = 8192): ConvolutionReverb
fun reverbImpulse(lengthSeconds: Number = 0.45, decay: Number = 0.65, diffusion: Number = 0.8, sampleRate: Number = 44100, seed: Int = 0): AudioSample
fun delayEffect(time: Number = 0.3, feedback: Number = 0.5, mix: Number = 0.5): Delay Creates a delay effect with controllable delay time, feedback, and dry/wet mix.
fun chorus(rate: Number = 1.5, depth: Number = 0.003, mix: Number = 0.5): Chorus
fun zaw(freq: Number): ZawOsc
fun pulze(freq: Number, duty: Number = 0.5): PulzeOsc
fun phaser(rate: Number = 0.5, depth: Number = 0.5, center: Number = 1000, sweep: Number = 600, mix: Number = 1.0): Phaser LFO-swept cascaded notches: supersaw / phaser(rate = 0.4)
fun chamberlinLpf(cutoff: Number = 1000, resonance: Number = 0.0): ChamberlinLowpass The superdough 2-pole lowpass — snarlier resonance than lpf.
fun softDistort(amount: Number = 1.0, postGain: Number = 1.0): SoftDistort Soft clipper with exponential drive: saw / softDistort(2)
fun curvedAdsr(a: Number = 0.01, d: Number = 0.1, s: Number = 0.7, r: Number = 0.3, attackCurve: Number = 2.0, decayCurve: Number = 2.0, gate: Gate = openGate()): CurvedADSR ADSR with shaped curves and click-free retrigger, auto-opened gate.
fun Signal.plateReverb(decay: Number = 0.75, damping: Number = 0.95, mix: Number = 0.3, preDelay: Number = 0.1, diffusion: Number = 0.70): PlateReverb Dattorro plate reverb — mono in, decorrelated stereo out: (saw(110) / lpf(1200)).plateReverb(decay = 0.85)
fun Signal.plate(decay: Number = 0.75, damping: Number = 0.95, mix: Number = 0.3): PlateReverb Alias for plateReverb.
fun <A> PatPattern<A>.stack(other: PatPattern<A>): PatPattern<A> Layer two patterns: pat("bd").stack(pat("hh"))
fun PatPattern<String>.fn(cps: Number = 1.0, range: Number = 1.0, outside: Number = 0.0): Functions.Fn Terse .asFn() alias: pat("g:(0,0.08)").fn(cps = 2.0)

Top-level values

Declaration Summary
val Number.p: Param
val sine: SineOsc
val saw: SawOsc
val square: SquareOsc
val pulse: PulseOsc
val tri: TriOsc
val phasor: Phasor
val impulse: Impulse
val white: WhiteNoise
val pink: PinkNoise
val brown: BrownNoise
val supersaw: SuperSaw
val supersquare: SuperSquare
val superpwm: SuperPWM
val supercomparator: SuperComparator
val superchip: SuperChip
val supernoise: SuperNoise
val superfork: SuperFork
val wavetable: SpectralWavetable
val rectifier: Rectifier
val fold: StagedWaveFolder
val gate: Gate
val audioIn: Signal
val audioInStereo: StereoSignal
val Number.hz: Param Self-documenting frequency param: 440.hz
val zaw: ZawOsc Naive, aliasing sawtooth — dough's zaw.
val pulze: PulzeOsc Naive, aliasing pulse — dough's pulze.