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
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
The orchestra or score did not compile. problems carry what Csound said, placed on lines.
CsoundNode
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.
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
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
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
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.
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
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
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
The operations themselves, as one object: Buf.add(dst, src, 0, n), Buf.peak(buf), and so on.
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
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
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
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
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 outPhaser— LFO-swept cascaded notchesChamberlinLowpass— the "superdough" 2-pole lowpass and its resonance curveCurvedADSR— time-based ADSR with click-free retriggerSoftDistort— dough's expm1 k-curve soft clipperZawOscandPulzeOsc— 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Which part of the waveform a Rectifier keeps.
Constants: HALF_WAVE, FULL_WAVE, NEGATIVE_HALF.
Rectifier
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
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
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
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
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
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
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
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
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
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
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
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:
faustControlsturns the program's ownhslider,buttonandhgroupdeclarations 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.faustDiagramdisplays Faust's own SVG block diagram of the program.faustWaveformrenders a program offline and plots it. Nothing there touches the audio engine, so it works where there is no audio device at all.faustCompilecompiles 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
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:
computeon its own: 7.6µs, about 350 times realtime. Generated Faust is not the bottleneck.render, end to end throughFaustDsp: 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
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.
computeon a compiled program takes ~7.6µs of the 2667µs a 128-frame block has at 48kHz; end to end throughFaustDspit is ~60µs, and what separates those two figures is the JVM↔WebAssembly boundary rather than Faust.synth.faust.smokeasserts a floor of ten times realtime, so playing is not a performance problem here. - The compiler is slow.
compilesettles 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:
- 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. warmInBackgroundpays the 13–22s while Field is starting, rather than at the moment someone first runs a box.compileis 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
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
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
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
[scale:...] metadata — how a slider's travel maps onto its value.
Constants: LINEAR, LOG, EXP.
FaustControl
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
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
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
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
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
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
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
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
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
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
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
Constants: LPF, HPF, BPF, NOTCH, APF, PEAKING, LOW_SHELF, HIGH_SHELF.
Biquad
| 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
Constants: LP, HP, BP, NOTCH.
SVF
| Declaration | Summary |
|---|---|
val mode: SVFMode |
|
val cutoff: Param |
|
val resonance: Param |
|
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) |
MoogLadder
| Declaration | Summary |
|---|---|
val cutoff: Param |
|
val resonance: Param |
|
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) |
CombFilter
| 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
| 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
| 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: aCARRIER_HZsine whose amplitude isLOUDNESS_GAIN · f(t). Recovered as√2 · RMS / LOUDNESS_GAIN. The carrier is chosen so one period is exactly 128 samples atSRand the RMS window,RMS_WINDOW, holds two whole periods: then a steady sine's RMS is exactlyA/√2and the window adds no ripple of its own.Encoding.PITCH: a sine atPITCH_BASE_HZ · 2^f(t)and constant level, sofspans a fraction of an octave. Recovered aslog2(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
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
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
| 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
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
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
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
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
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
| 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
| Declaration | Summary |
|---|---|
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) |
PinkNoise
| Declaration | Summary |
|---|---|
override fun generate(output: FloatArray, numFrames: Int, ctx: SynthContext) |
BrownNoise
| 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 withaliases; - any writable numeric property, wrapped in a
ScalarPathStatethat gives a plainInt/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
ParamSet
ParamSetScope
class ParamSetScope internal constructor(private val set: ParamSet)
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
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
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
How a delay line reads between whole samples: LINEAR mixes two neighbours, CUBIC fits four.
Constants: LINEAR, CUBIC.
FractionalDelay
| 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
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
| 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
Constants: HANN, SINE, TRIANGLE, TUKEY, GAUSSIAN, PERCUSSIVE, REVERSE_PERCUSSIVE, BLACKMAN.
GrainDirection
Constants: FORWARD, REVERSE, BIDIRECTIONAL.
GrainCloud
| 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
| 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
| 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
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.
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
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
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
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
| 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
| 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
SpectralWavetableRecorder
| 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
| 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):
SubharmonicZeroCrosstimes 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.SubharmonicPLLis 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.SubharmonicQuadPLLfirst splits the input into a quadrature pair withHilbertPair, 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
Tracks pitch by timing the interval between positive-going zero crossings.
| Declaration | Summary |
|---|---|
override fun track(x: Double) |
|
override fun reset() |
SubharmonicPLL
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
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
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
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
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
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
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
Rendering function for one bank voice.
| Declaration | Summary |
|---|---|
fun render(phase: Double): Double |
PhaseVoice
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
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
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
AudioSnapshot
A still copy of some audio: left, an optional right, and the sampleRate the samples were taken at.
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
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, andaudioInreturn fresh instances each time they are referenced /chains a Signal into a SignalProcessoraudio { ... }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() }
Ableton Link
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
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. |