Skip to content

Audio

Sound that arrives from, or leaves for, somewhere other than Field's own signal graph: analysis products written by an offline tool, the network protocols that carry control data, the Dirt sample library on disk, an Ableton Live set read off the filesystem, and Sonic Pi driven as an owned child process with no window of its own. SoundPathMap.kt is the one to read first: the declared, re-derived-from-the-code map of every route Field has from a box to something audible, which Making a sound in Field: every path there is renders as a page. Sonic Pi covers owned mode in full.

80 source file(s), 2122 public symbol(s), 1039 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.

essentiaAnalysis.kt

Readers for two on-disk audio-analysis products, so a box can query them by time.

Nothing here performs analysis or links against a native library — both entry points just parse files that some offline tool has already written.

loadFramesSig reads an Essentia-shaped YAML .sig file into a FramesSigDocument: the tree is flattened into dotted paths, and each leaf is sorted into a scalar, a per-frame FramesSigFloatProduct, or a per-frame FramesSigFloatVectorProduct. sampleAt / floatValueAt / floatVectorAt map a time in seconds to a frame index by assuming frames are evenly spaced across the analysis window described in metadata (analysis.start_time and analysis.length, falling back to the audio length); available / availableArrays list what a given file actually contains. Large .sig files are handled by raising SnakeYAML's code-point limit to the file size plus padding.

loadSound2Vec reads a directory of metadata.csv + embeddings.npy (+ optional manifest.json) into a Sound2VecDataset of Sound2VecClips, each carrying its embedding, its time span within a source audio file, and optional t-SNE 2D/3D coordinates; clips can be looked up by chunk id, or by audio path and time. The bundled .npy reader is narrow: it accepts only C-ordered (non-Fortran) float32 1D or 2D arrays, and by default requires the CSV row count, the embedding row count and the manifest's num_chunks / embedding_dim to agree.

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

Top-level functions

Declaration Summary
fun loadFramesSig(file: File, options: FramesSigLoadOptions = FramesSigLoadOptions()): FramesSigDocument
fun loadFramesSig(path: String, options: FramesSigLoadOptions = FramesSigLoadOptions()): FramesSigDocument
fun loadSound2Vec(dir: File, options: Sound2VecLoadOptions = Sound2VecLoadOptions()): Sound2VecDataset
fun loadSound2Vec(path: String, options: Sound2VecLoadOptions = Sound2VecLoadOptions()): Sound2VecDataset

FramesSigLoadOptions

data class FramesSigLoadOptions(val minimumCodePointLimit: Int = 4_000_000, val codePointLimitPadding: Int = 1_000_000, val defaultSmallVectorWidth: Int = 16)

Declaration Summary
val minimumCodePointLimit: Int
val codePointLimitPadding: Int
val defaultSmallVectorWidth: Int

FramesSigMetadata

data class FramesSigMetadata(val raw: Map<String, Any?>, val version: Map<String, String>, val fileName: String?, val codec: String?, val sampleRate: Int?, val numberChannels: Int?, val bitRate: Int?, val audioLengthSeconds: Double?, val analysisLengthSeconds: Double?, val analysisStartTimeSeconds: Double, val replayGain: Double?)

Declaration Summary
val raw: Map<String, Any?>
val version: Map<String, String>
val fileName: String?
val codec: String?
val sampleRate: Int?
val numberChannels: Int?
val bitRate: Int?
val audioLengthSeconds: Double?
val analysisLengthSeconds: Double?
val analysisStartTimeSeconds: Double
val replayGain: Double?

FramesSigProduct

sealed interface FramesSigProduct

Declaration Summary
val path: String
val frameCount: Int
fun sampleAt(timeSeconds: Double, metadata: FramesSigMetadata): FramesSigTimedSample

FramesSigFloatProduct

data class FramesSigFloatProduct(override val path: String, val values: FloatArray) : FramesSigProduct

Declaration Summary
override val path: String
val values: FloatArray
override val frameCount: Int
override fun sampleAt(timeSeconds: Double, metadata: FramesSigMetadata): FramesSigTimedSample

FramesSigFloatVectorProduct

data class FramesSigFloatVectorProduct(override val path: String, val values: List<FloatArray>) : FramesSigProduct

Declaration Summary
override val path: String
val values: List<FloatArray>
override val frameCount: Int
val vectorWidth: Int
override fun sampleAt(timeSeconds: Double, metadata: FramesSigMetadata): FramesSigTimedSample

FramesSigTimedSample

data class FramesSigTimedSample(val path: String, val index: Int, val timeSeconds: Double, val values: FloatArray)

Declaration Summary
val path: String
val index: Int
val timeSeconds: Double
val values: FloatArray
val value: Float?

FramesSigProductInfo

data class FramesSigProductInfo(val path: String, val frameCount: Int, val vectorWidth: Int, val startTimeSeconds: Double, val endTimeSeconds: Double)

Declaration Summary
val path: String
val frameCount: Int
val vectorWidth: Int
val startTimeSeconds: Double
val endTimeSeconds: Double

FramesSigDocument

data class FramesSigDocument(val source: File, val metadata: FramesSigMetadata, val raw: Map<String, Any?>, val scalarProducts: Map<String, Double>, val floatProducts: Map<String, FramesSigFloatProduct>, val floatVectorProducts: Map<String, FramesSigFloatVectorProduct>, val loadOptions: FramesSigLoadOptions)

Declaration Summary
val source: File
val metadata: FramesSigMetadata
val raw: Map<String, Any?>
val scalarProducts: Map<String, Double>
val floatProducts: Map<String, FramesSigFloatProduct>
val floatVectorProducts: Map<String, FramesSigFloatVectorProduct>
val loadOptions: FramesSigLoadOptions
fun product(path: String): FramesSigProduct?
fun sampleAt(path: String, timeSeconds: Double): FramesSigTimedSample?
fun floatValueAt(path: String, timeSeconds: Double): Float?
fun floatVectorAt(path: String, timeSeconds: Double): FloatArray?
fun available(): List<FramesSigProductInfo>
fun availableArrays(maxVectorWidth: Int = loadOptions.defaultSmallVectorWidth): List<FramesSigProductInfo>
fun availableAll(maxVectorWidth: Int = loadOptions.defaultSmallVectorWidth): List<FramesSigProductInfo>

Sound2VecLoadOptions

data class Sound2VecLoadOptions(val requireManifest: Boolean = false, val requireRowAlignment: Boolean = true)

Declaration Summary
val requireManifest: Boolean
val requireRowAlignment: Boolean

Sound2VecManifest

data class Sound2VecManifest(val raw: Map<String, String>, val modelId: String?, val device: String?, val sampleRate: Int?, val clipSeconds: Double?, val hopSeconds: Double?, val minSeconds: Double?, val numFiles: Int?, val numChunks: Int?, val embeddingDim: Int?, val l2Normalized: Boolean?)

Declaration Summary
val raw: Map<String, String>
val modelId: String?
val device: String?
val sampleRate: Int?
val clipSeconds: Double?
val hopSeconds: Double?
val minSeconds: Double?
val numFiles: Int?
val numChunks: Int?
val embeddingDim: Int?
val l2Normalized: Boolean?

Sound2VecClip

data class Sound2VecClip(val rowIndex: Int, val chunkId: Int, val filePath: String, val relPath: String, val fileName: String, val chunkIndex: Int, val startSec: Double, val endSec: Double, val durationSec: Double, val sampleRate: Int, val numSamples: Int, val tsne2dX: Float?, val tsne2dY: Float?, val tsne3dX: Float?, val tsne3dY: Float?, val tsne3dZ: Float?, val embedding: FloatArray)

Declaration Summary
val rowIndex: Int
val chunkId: Int
val filePath: String
val relPath: String
val fileName: String
val chunkIndex: Int
val startSec: Double
val endSec: Double
val durationSec: Double
val sampleRate: Int
val numSamples: Int
val tsne2dX: Float?
val tsne2dY: Float?
val tsne3dX: Float?
val tsne3dY: Float?
val tsne3dZ: Float?
val embedding: FloatArray
val middleSec: Double
fun contains(timeSeconds: Double): Boolean
fun tsne2dOrNull(): FloatArray?
fun tsne3dOrNull(): FloatArray?
fun tsne2dVecOrNull(): Vec2?
fun tsne3dVecOrNull(): Vec3?
fun audioSubclip(): Sound2VecAudioSubclip

Sound2VecAudioSubclip

data class Sound2VecAudioSubclip(val chunkId: Int, val chunkIndex: Int, val filePath: String, val relPath: String, val fileName: String, val startSec: Double, val endSec: Double, val durationSec: Double, val sampleRate: Int, val numSamples: Int)

Declaration Summary
val chunkId: Int
val chunkIndex: Int
val filePath: String
val relPath: String
val fileName: String
val startSec: Double
val endSec: Double
val durationSec: Double
val sampleRate: Int
val numSamples: Int

Sound2VecDataset

data class Sound2VecDataset(val sourceDir: File, val manifest: Sound2VecManifest?, val clips: List<Sound2VecClip>)

Declaration Summary
val sourceDir: File
val manifest: Sound2VecManifest?
val clips: List<Sound2VecClip>
val embeddingDim: Int
fun clipByChunkId(chunkId: Int): Sound2VecClip?
fun clipsForAudio(filePath: String): List<Sound2VecClip>
fun clipAt(filePath: String, timeSeconds: Double): Sound2VecClip?
fun clipsOverlapping(filePath: String, startSec: Double, endSec: Double): List<Sound2VecClip>
fun tsne2dPoints(): List<Vec2>
fun tsne3dPoints(): List<Vec3>
fun subclipAt(filePath: String, timeSeconds: Double): Sound2VecAudioSubclip?
fun subclipsOverlapping(filePath: String, startSec: Double, endSec: Double): List<Sound2VecAudioSubclip>

Loris.kt

Loris reassigned-bandwidth-enhanced analysis and resynthesis, run as an optional native process.

LorisClient.analyze sends samples to the field_loris executable and returns a LorisModel: a list of LorisPartials, each a track of irregularly timed LorisBreakpoints carrying frequency, amplitude, phase and noise bandwidth. LorisClient.synthesize turns a model back into samples at the input rate, and distill, sift, dilate and morph run Loris's own labelling, time-warping and morphing natively. Requests and results are cached on disk by a hash of the request bytes and the executable, and every LorisResult carries that LorisProvenance.

The executable is not needed to build or run Field; LorisNative.describe says where it was found or how to build it (./gradlew buildLorisNative). Every call except sift has an ...Async form that returns a CompletableFuture, which is the one to use from a box.

val client = LorisClient()
client.analyzeWavAsync(Path.of("voice.wav"), durationSeconds = 2.0).thenAccept { r ->
    display("partials", r.visualizationHtml())
}

src/jvmMain/kotlin/afield/audio/analysis/loris/Loris.kt — read the source

Used in: Loris: partial tracks and resynthesis

LorisOptions

data class LorisOptions(val resolutionHz: Double = 40.0, val windowWidthHz: Double = 80.0, val hopSeconds: Double = 0.005, val frequencyDriftHz: Double = 40.0, val amplitudeFloorDb: Double = -60.0, val minimumFrequencyHz: Double = 20.0)

Loris's independent resolution, window and tracking controls, expressed in Hz/seconds/dB.

Declaration Summary
val resolutionHz: Double
val windowWidthHz: Double
val hopSeconds: Double
val frequencyDriftHz: Double
val amplitudeFloorDb: Double
val minimumFrequencyHz: Double

LorisBreakpoint

data class LorisBreakpoint(val timeSeconds: Double, val frequencyHz: Double, val amplitude: Double, val phaseRadians: Double, val bandwidth: Double)

Irregular native timestamp; bandwidth is a noise-energy parameter, not confidence. Phase is radians.

Declaration Summary
val timeSeconds: Double, val frequencyHz: Double, val amplitude: Double
val phaseRadians: Double, val bandwidth: Double

LorisPartial

data class LorisPartial(val id: Int, val label: Int, val points: List<LorisBreakpoint>)

ID identifies this analysis track; label is Loris's optional channel label, usually zero.

LorisModel

data class LorisModel(val sampleRate: Int, val frameCount: Int, val partials: List<LorisPartial>)

Programmable partial trajectories. Native timestamps and phase are retained without frame pooling.

Declaration Summary
val durationSeconds: Double
fun transformed(timeScale: Double = 1.0, frequencyScale: Double = 1.0, gain: Double = 1.0, bandwidthScale: Double = 1.0): LorisModel Transform envelopes; audition phase/transient behavior rather than assuming exact preservation.

LorisEnvelope

data class LorisEnvelope(val points: List<Pair<Double, Double>>)

Piecewise-linear (time seconds, value) breakpoints, Loris's LinearEnvelope; constant outside its ends.

Declaration Summary
fun valueAt(t: Double): Double

LorisReference

sealed interface LorisReference

What a distillation labels against: a known harmonic track, or Loris's own fundamental estimate.

Declaration Summary
data class Track(val frequencyHz: LorisEnvelope, val harmonic: Int = 1) : LorisReference frequencyHz tracks harmonic harmonic (1 = the fundamental).
data class Estimated(val minimumHz: Double, val maximumHz: Double) : LorisReference Loris's FrequencyReference: estimates the fundamental from the partials, within this range.

LorisProvenance

data class LorisProvenance(val requestHash: String, val executableSha256: String, val upstreamRevision: String, val operation: String, val options: LorisOptions?, val cacheHit: Boolean)

Identity includes exact binary request and executable bytes; results survive subsequent box evaluation.

Declaration Summary
val upstreamRevision: String, val operation: String, val options: LorisOptions?, val cacheHit: Boolean)

LorisResult

data class LorisResult(val model: LorisModel, val samples: FloatArray, val provenance: LorisProvenance)

Samples remain at the input rate; no normalization, clipping or resampling is applied.

Declaration Summary
val peak: Float

LorisNative

object LorisNative

Lazy optional executable discovery; ordinary Field startup/build/tests never require Loris.

Declaration Summary
const val REVISION
val executable: Path?
val isAvailable: Boolean
fun describe(): String

LorisClient

class LorisClient(val cacheDirectory: Path = Path.of("build/field-loris/cache"), val timeoutSeconds: Long = 180)

Offline process boundary with durable binary caching and a bounded timeout; call async from a box.

Declaration Summary
val timeoutSeconds: Long
fun analyzeAsync(samples: FloatArray, sampleRate: Int, options: LorisOptions = LorisOptions(), onProgress: (String) -> Unit = {}): CompletableFuture<LorisResult>
fun analyzeWavAsync(path: Path, startSeconds: Double = 0.0, durationSeconds: Double? = null, options: LorisOptions = LorisOptions(), onProgress: (String) -> Unit = {}): CompletableFuture<LorisResult> WAV convenience uses the existing Field loader's explicit LEFT-channel policy.
fun analyze(samples: FloatArray, sampleRate: Int, options: LorisOptions = LorisOptions(), onProgress: (String) -> Unit = {}): LorisResult
fun synthesizeAsync(model: LorisModel): CompletableFuture<LorisResult>
fun synthesize(model: LorisModel): LorisResult
fun distill(model: LorisModel, reference: LorisReference, distill: Boolean = true, stretch: Double = 0.0, fadeSeconds: Double = .005, gapSeconds: Double = .001): LorisResult Loris's Channelizer labels each partial with the harmonic number it sits nearest to under reference; with distill (the default) Loris's Distiller then joins every partial sharing a label into one, so the result has at most one partial per harmonic plus the unlabeled rest. Partial ids are reassigned 0..n-1. The result's samples are the distilled model resynthesized.
fun distillAsync(model: LorisModel, reference: LorisReference, distill: Boolean = true): CompletableFuture<LorisResult>
fun dilateAsync(model: LorisModel, from: List<Double>, to: List<Double>): CompletableFuture<LorisResult>
fun morphAsync(source: LorisModel, target: LorisModel, frequency: LorisEnvelope, amplitude: LorisEnvelope = frequency, bandwidth: LorisEnvelope = amplitude): CompletableFuture<LorisResult>
fun sift(model: LorisModel, fadeSeconds: Double = .005, dropUnlabeled: Boolean = false): LorisResult Loris's Sieve: of overlapping same-label partials keep the longest; others become label 0 (or are dropped).
fun dilate(model: LorisModel, from: List<Double>, to: List<Double>, durationSeconds: Double = to.last() + (model.durationSeconds - from.last())): LorisResult Loris's Dilator: piecewise-linear time warp taking each time in from to the matching time in to (both strictly increasing). The timeline becomes durationSeconds, by default the last target plus whatever followed the last source marker.
fun morph(source: LorisModel, target: LorisModel, frequency: LorisEnvelope, amplitude: LorisEnvelope = frequency, bandwidth: LorisEnvelope = amplitude, referenceLabel: Int = 1, amplitudeShape: Double = 0.0, durationSeconds: Double = max(source.durationSeconds, target.durationSeconds)): LorisResult Loris's Morpher. Same-label partials of source and target are morphed (label 0 crossfades); each envelope maps time in seconds to 0 = source, 1 = target, independently for frequency, amplitude and noise bandwidth. Distill both sounds against the same harmonic numbering first. referenceLabel names the partial whose frequency guides quiet partials (Loris's reference partial); 0 disables it. amplitudeShape is Loris's log-amplitude morph shape (0 keeps Loris's default).

LorisDocs.kt

The fixture that renders the audio and figures on the Loris docs page from a committed film-dialogue recording.

LorisDocs.render analyses the source WAV with fixed LorisOptions, resynthesizes it unedited and with frequencies scaled by 0.75 and noise bandwidth removed, and writes both WAVs with a provenance.json under public/docs/assets/loris. The ...Html functions return the comparison, transformed and morph panels the page embeds. It needs the native Loris executable.

src/jvmMain/kotlin/afield/audio/analysis/loris/LorisDocs.kt — read the source

LorisDocs

object LorisDocs

Reproducible real-audio documentation fixture; never substitutes synthetic tracks for Loris.

Declaration Summary
val sourcePath: Path
val folder: Path
val options
fun render(): Pair<LorisResult, LorisResult> Analyze committed public-domain film dialogue; render the unedited and transformed models.
fun comparisonHtml(): String
fun transformedHtml(): String
fun whirlpoolHtml(): String
data class Morph(val source: LorisResult, val target: LorisResult, val aligned: LorisResult, val morphed: LorisResult) Two phrases from the same film, labelled, aligned and morphed by Loris.
val morphSource
val morphTarget
val morphEnvelope
fun morph(): Morph
fun morphHtml(): String
fun morphPanelHtml(panel: Int): String One panel of morphHtml: 0 = phrase A, 1 = phrase B aligned, 2 = the morph (each runs the cached pipeline).

LorisStudy.kt

One worked Loris gesture, written in the same breakpoint arithmetic that LorisTransforms.kt applies one edit at a time.

spectralWhirlpool reshapes a window of a LorisModel rather than the whole of it. A sin² bell rises and falls across the window, so the edit begins and ends at zero and every breakpoint outside the window is returned unchanged. Under that bell each partial sweeps four times through ±18 semitones, its amplitude is shuttered at 12 Hz, and its noise bandwidth is pushed towards 0.85 at the peak: a vibrato that turns breathy in the middle and settles again. Pass the result to LorisClient.synthesize to hear it.

src/jvmMain/kotlin/afield/audio/analysis/loris/LorisStudy.kt — read the source

Top-level functions

Declaration Summary
fun LorisModel.spectralWhirlpool(startSeconds: Double = 6.0, durationSeconds: Double = 3.0): LorisModel Example gesture: smooth local ±18-semitone sweeps, 12 Hz amplitude shutters and noisy peaks.

LorisTransforms.kt

Kotlin-side edits of a LorisModel: filtering partials, transposing, stretching, tilting and cropping.

Every function here returns a new model and changes breakpoint envelopes only, so the result is heard by passing it to LorisClient.synthesize. Labelling, sifting, dilation and morphing are Loris's own algorithms and run natively through LorisClient.

src/jvmMain/kotlin/afield/audio/analysis/loris/LorisTransforms.kt — read the source

Used in: Loris: partial tracks and resynthesis, Corpus microscope: from recordings to related windows, Gesture board: drawing toward a sound

Top-level functions

Declaration Summary
fun LorisModel.labelCounts(): Map<Int, Int> Number of partials carrying each label; label 0 is "unlabeled".
fun LorisModel.filterPartials(keep: (LorisPartial) -> Boolean): LorisModel Keep partials matching keep; ids are preserved.
fun LorisModel.keepLabels(labels: Iterable<Int>): LorisModel
fun LorisModel.keepLabels(range: IntRange): LorisModel
fun LorisModel.dropUnlabeled(): LorisModel
fun LorisModel.minimumDuration(seconds: Double): LorisModel
fun LorisModel.minimumPeak(db: Double): LorisModel Drop partials whose loudest breakpoint is below db dBFS (amplitude 1 = 0 dB).
fun LorisModel.shiftPitch(cents: LorisEnvelope): LorisModel+1 overload · also Double Transpose by cents (may vary in time); breakpoints pushed past 0.49 × sampleRate are removed.
fun LorisModel.stretched(factor: Double): LorisModel Uniform time stretch: all breakpoint times and the timeline scale by factor.
fun LorisModel.tilt(dbPerOctave: Double, pivotHz: Double = 1000.0): LorisModel Spectral tilt: amplitude × (f / pivotHz) ^ (dbPerOctave / 6.02...), so +6 dB/oct brightens and −6 dB/oct darkens, with partials at pivotHz unchanged.
fun LorisModel.crop(startSeconds: Double, endSeconds: Double): LorisModel Keep [startSeconds, endSeconds] and move it to time 0. Partials are cut at the boundaries' nearest breakpoints.
fun LorisModel.loudestTimeSeconds(binSeconds: Double = .05): Double Centre of the binSeconds bin with the largest summed breakpoint amplitude: a simple marker for dilate.

Top-level values

Declaration Summary
val LorisPartial.startSeconds: Double Time of the partial's first breakpoint.
val LorisPartial.endSeconds: Double
val LorisPartial.durationSeconds: Double
val LorisPartial.peakAmplitude: Double
val LorisPartial.meanFrequencyHz: Double Amplitude-weighted mean frequency.

CsdDocument.kt

Reads a Csound box's text as a .csd document and maps box lines to its options, orchestra, score and instruments.

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

CsdDocument

class CsdDocument private constructor(val text: String, val options: Section?, val orchestra: Section, val score: Section?)

A Csound box's text, read as a .csd — the unified file format Csound has used since the late 1990s: <CsOptions>, <CsInstruments> and <CsScore> inside <CsoundSynthesizer>.

Declaration Summary
val text: String
val options: Section?
val orchestra: Section
val score: Section?
data class Section(val body: String, val firstLine: Int) One section's body and where it starts. firstLine is 0-based in text; lineCount its lines.
enum class Part What part of the document 0-based box line line is in.
fun partAt(line: Int): Part
val optionFlags: List<String> <CsOptions> flags, one per whitespace-separated word.
data class Instrument(val names: List<String>, val startLine: Int, val endLine: Int) An instr block of the orchestra: its names (numbers or names), and 0-based box lines.
val instruments: List<Instrument> by lazy Every instr … endin block, in order.
fun instrumentAt(line: Int): Instrument? The instrument whose block contains 0-based box line line, or null.
fun instrumentText(instrument: Instrument): String The orchestra text of instrument, instr through endin.
data class Event(val instrument: String, val line: Int) A score i statement: what it plays and its 0-based box line. . carries the last p1, as Csound does.
val events: List<Event> by lazy Every i statement in the score, with carried p1s resolved. Text inside { } loops is counted once.
val declaredChannels: Map<String, Int> by lazy chn_k/chn_a/chn_S declarations: channel name to its 0-based box line.
val channelNames: Set<String> by lazy Every channel name the orchestra uses: declared ones, and the first argument of the bus opcodes.

CsoundNative.kt

Finds and binds libcsound, the optional native library behind CsoundInstance, and explains how to build it when it is missing.

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

CsoundLibrary

object CsoundLibrary

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

Declaration Summary
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 is picked up by the next box run.
val isAvailable: Boolean True when a libcsound 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.

CsoundVocabulary.kt

The opcode list a Csound box completes against, generated from libcsound and joined with the Csound Reference Manual's descriptions.

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

CsoundVocabulary

object CsoundVocabulary

The opcodes a Csound box can complete, with their signatures, as libcsound itself lists them.

Declaration Summary
data class Signature(val outypes: String, val intypes: String)
data class Argument(val name: String, val text: String) One argument as the manual describes it: its name and the first sentence about it.
data class Opcode(val name: String, val signatures: List<Signature>, val description: String? = null, val syntax: List<String> = emptyList(), val arguments: List<Argument> = emptyList(), val page: String? = null, val category: String? = null) An opcode: name and signatures from libcsound; description, syntax, arguments and page from the Csound Reference Manual, when it has an entry for the name.
data class ManualSource(val tag: String, val sha256: String, val licence: String, val described: Int, val undescribed: List<String>) Where the descriptions came from, and under what terms they are shipped.
data class Vocabulary(val csoundVersion: Int, val opcodes: List<Opcode>, val manual: ManualSource? = null)
const val MANUAL_TAG The manual release and checksum tools/fetch_csound_manual.py pins.
const val MANUAL_SHA256
const val MANUAL_LICENCE
fun manualUrl(page: String): String A manual page URL for an opcode with a Opcode.page.
data class ManualEntry(val page: String, val description: String?, val syntax: List<String>, val arguments: List<Argument>, val category: String? = null) What the manual says about one name.
fun readManual(manualDir: File): Map<String, ManualEntry> Read every refentry under opcodes/ of the manual at manualDir into entries by refname. Pure text processing in sorted file order, so the same manual gives the same result.
fun describe(library: Vocabulary, manual: Map<String, ManualEntry>): Vocabulary library with the manual's text joined on by opcode name.
const val RESOURCE_PATH The committed vocabulary's path in a checkout.
fun generate(): Vocabulary Ask libcsound for its opcodes. Needs a built libcsound (CsoundLibrary).
fun toJson(v: Vocabulary): String
fun fromJson(text: String): Vocabulary
val committed: Vocabulary by lazy The committed vocabulary, from the classpath or the checkout. Cached after the first read.
val byName: Map<String, Opcode> by lazy Opcodes by name.
fun describe(name: String, sig: Signature): String One signature as Csound users read one: aSig poscil xVal, xVal, [ij=-1], [io=0]. Array types (k[]) keep their brackets; a type letter this does not know is shown as itself.
fun fetchManual(): File The pinned manual's directory, fetched (checksummed, cached) by tools/fetch_csound_manual.py.
fun main(args: Array<String>) Write the generated vocabulary to RESOURCE_PATH: ./gradlew generateCsoundVocabulary. Names and signatures come from libcsound; descriptions from the pinned manual.

OscillatorBankArrays.kt

Banks built as arrays of the scalar models: ResonatorBankArray holds one Resonator per frequency and TrackingResonatorBankArray one TrackingResonator, each updated sample by sample.

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

ResonatorBankArray

class ResonatorBankArray(frequencies: FloatArray, alphas: FloatArray, betas: FloatArray = alphas, private val sampleRate: Float)

Banks built as arrays of the scalar models: ResonatorBankArray holds one Resonator per frequency and TrackingResonatorBankArray one TrackingResonator, each updated sample by sample.

Declaration Summary
val resonators: Array<Resonator>
val size: Int
fun update(sample: Float)
fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1)
fun updateConcurrent(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1, taskCount: Int = Runtime.getRuntime().availableProcessors().coerceAtLeast(1))
fun getComplex(dest: FloatArray, offset: Int = 0)
fun getPowers(dest: FloatArray, offset: Int = 0)
fun getAmplitudes(dest: FloatArray, offset: Int = 0)

TrackingResonatorBankArray

class TrackingResonatorBankArray(naturalFrequencies: FloatArray, alphas: FloatArray, betas: FloatArray = alphas, private val sampleRate: Float, gammas: FloatArray = trackingResonatorGammaHeuristics(naturalFrequencies, sampleRate))

Declaration Summary
val resonators: Array<TrackingResonator>
val size: Int
fun update(sample: Float)
fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1)
fun updateConcurrent(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1, taskCount: Int = Runtime.getRuntime().availableProcessors().coerceAtLeast(1))
fun setPowerTimeConstant(tau: Float = 1.0f)
fun getComplex(dest: FloatArray, offset: Int = 0)
fun getPowers(dest: FloatArray, offset: Int = 0)
fun getAmplitudes(dest: FloatArray, offset: Int = 0)
fun getResonantFrequencies(dest: FloatArray, offset: Int = 0)

OscillatorModels.kt

Scalar, one-object-per-oscillator reference models: Phasor (a complex phasor advanced by multiplication), Oscillator, Resonator and TrackingResonator.

These are the per-sample building blocks that the array banks in OscillatorBankArrays.kt hold, and a readable counterpart to the vectorized banks in ResonatorBank.kt.

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

Phasor

class Phasor(frequency: Float, private val sampleRate: Float, initialPhaseRadians: Float = 0.0f, private val direction: Float = 1.0f)

Scalar complex phasor reference implementation.

Declaration Summary
var frequency: Float
var real: Float
var imag: Float
val cosine: Float
val sine: Float
fun setFrequency(frequency: Float)
fun update()
fun stabilize()

Oscillator

class Oscillator(frequency: Float, var amplitude: Float = 1.0f, sampleRate: Float, initialPhaseRadians: Float = 0.0f)

Declaration Summary
var amplitude: Float
val phasor
var frequency: Float
fun updateCosine(): Float
fun updateSine(): Float
fun renderCosine(dest: FloatArray, offset: Int = 0, length: Int = dest.size - offset)

Resonator

open class Resonator(frequency: Float, val alpha: Float, val beta: Float = alpha, protected val sampleRate: Float)

Scalar resonator reference implementation.

Declaration Summary
val alpha: Float
val beta: Float
val frequency: Float
val real: Float
val imag: Float
val power: Float
val amplitude: Float
val phase: Float
val timeConstant: Float
open fun update(sample: Float)
fun update(samples: FloatArray, offset: Int = 0, length: Int = samples.size - offset, stride: Int = 1)
open fun reset()
fun stabilize()

TrackingResonator

class TrackingResonator(naturalFrequency: Float, alpha: Float, beta: Float = alpha, sampleRate: Float, val gamma: Float = trackingResonatorGammaHeuristic(naturalFrequency, sampleRate)) : Resonator(naturalFrequency, alpha, beta, sampleRate)

Declaration Summary
val gamma: Float
var trackingPowerThreshold: Float
val resonantFrequency: Float
val instantaneousFrequency: Float
var deltaPhase: Float
override fun update(sample: Float)
fun setPowerTimeConstant(tau: Float = 1.0f)
override fun reset()

ResonatorBank.kt

Vectorized resonator banks for spectral analysis without an FFT: ResonatorBank is a fixed-frequency EMA resonator bank, and TrackingResonatorBank retunes each resonator toward the frequency it hears.

Both store resonator state as split-complex arrays and use the JVM incubator Vector API for the linear passes. resonate and trackingResonate run a bank over a whole signal, and the resonatorAlphaHeuristic and trackingResonatorGammaHeuristic functions choose smoothing coefficients from frequency and sample rate.

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

Used in: Signal examples: time and frequency

Top-level functions

Declaration Summary
fun resonatorAlphaHeuristic(frequency: Float, sampleRate: Float, k: Float = 1.0f, n: Float = 1.0f): Float Resonator EMA coefficient heuristic from Oscillators/noFFT.
fun resonatorAlphaHeuristics(frequencies: FloatArray, sampleRate: Float, k: Float = 1.0f, n: Float = 1.0f): FloatArray
fun trackingResonatorGammaHeuristic(frequency: Float, sampleRate: Float, k: Float = 1.0f, n: Float = 1.0f): Float
fun trackingResonatorGammaHeuristics(frequencies: FloatArray, sampleRate: Float, k: Float = 1.0f, n: Float = 1.0f): FloatArray
fun trackingResonate(input: FloatArray, samplingRate: Float, naturalFrequencies: FloatArray, alphas: FloatArray, betas: FloatArray = alphas, gammas: FloatArray = trackingResonatorGammaHeuristics(naturalFrequencies, samplingRate), hopLength: Int, powerTimeConstant: Float? = null, progress: ((completed: Int, total: Int) -> Unit)? = null): TrackingResonatorAnalysis
fun resonate(input: FloatArray, samplingRate: Float, freqs: FloatArray, alphas: FloatArray, betas: FloatArray, hopLength: Int): FloatArray Port of resonate::resonate for offline analysis.
fun main() Small standalone smoketest for ResonatorBank/resonate.

ResonatorBank

class ResonatorBank(private val numResonators: Int, frequencies: FloatArray, alphas: FloatArray, betas: FloatArray, private val sampleRate: Float)

Kotlin port of the "resonate" EMA resonator bank from noFFT.

Declaration Summary
fun frequencyValue(index: Int): Float
fun alphaValue(index: Int): Float
fun timeConstantValue(index: Int): Float
fun getComplex(dest: FloatArray, offset: Int = 0)
fun getPowers(dest: FloatArray, offset: Int = 0)
fun getAmplitudes(dest: FloatArray, offset: Int = 0)
fun update(sample: Float)
fun update(samples: FloatArray)
fun update(frameData: FloatArray, frameLength: Int, sampleStride: Int)
fun update(frameData: FloatArray, frameOffset: Int, frameLength: Int, sampleStride: Int)
fun update(frameData: FloatArray, frameLength: Int, sampleStride: Int, powers: FloatArray?, amplitudes: FloatArray?)
fun update(frameData: FloatArray, frameOffset: Int, frameLength: Int, sampleStride: Int, powers: FloatArray?, amplitudes: FloatArray?)
fun stabilize()

TrackingResonatorBank

class TrackingResonatorBank(private val numResonators: Int, naturalFrequencies: FloatArray, alphas: FloatArray, betas: FloatArray = alphas, private val sampleRate: Float, gammas: FloatArray = trackingResonatorGammaHeuristics(naturalFrequencies, sampleRate))

A phase-tracking resonator bank.

Declaration Summary
fun naturalFrequencyValue(index: Int): Float
fun resonantFrequencyValue(index: Int): Float
fun instantaneousFrequencyValue(index: Int): Float
fun alphaValue(index: Int): Float
fun betaValue(index: Int): Float
fun gammaValue(index: Int): Float
fun timeConstantValue(index: Int): Float
fun trackingPowerThresholdValue(): Float
fun accumulatedPowerValue(): Float
fun getNaturalFrequencies(dest: FloatArray, offset: Int = 0)
fun getResonantFrequencies(dest: FloatArray, offset: Int = 0)
fun getInstantaneousFrequencies(dest: FloatArray, offset: Int = 0)
fun getComplex(dest: FloatArray, offset: Int = 0)
fun getPowers(dest: FloatArray, offset: Int = 0)
fun getAmplitudes(dest: FloatArray, offset: Int = 0)
fun getPhases(dest: FloatArray, offset: Int = 0)
fun getDeltaPhases(dest: FloatArray, offset: Int = 0)
fun update(sample: Float)
fun update(samples: FloatArray)
fun update(frameData: FloatArray, frameLength: Int, sampleStride: Int)
fun update(frameData: FloatArray, frameOffset: Int, frameLength: Int, sampleStride: Int)
fun update(frameData: FloatArray, frameLength: Int, sampleStride: Int, powers: FloatArray?, amplitudes: FloatArray?)
fun update(frameData: FloatArray, frameOffset: Int, frameLength: Int, sampleStride: Int, powers: FloatArray?, amplitudes: FloatArray?)
fun setPowerTimeConstant(tau: Float = 1.0f)
fun reset()
fun stabilize()

SimpleSpatialAudio.kt

3D sound playback over OpenAL: SimpleSpatialAudioEngine loads WAV files into SoundBuffers, plays them through positioned SoundSources, and applies ReverbSettings or EaxReverbSettings effects.

The engine follows a listener position and orientation, can enable HRTF where OpenAL Soft offers it, and can render to a loopback device at a fixed sample rate instead of the output device.

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

Top-level functions

Declaration Summary
fun SoundBuffer.createSource(looping: Boolean = false, referenceDistance: Float = 1.0f, maxDistance: Float = 1000.0f, rolloffFactor: Float = 1.0f, name: String? = null): SoundSource

SimpleSpatialAudioEngine

class SimpleSpatialAudioEngine(private val distanceModel: Int = AL_INVERSE_DISTANCE_CLAMPED, private val threadLocalContext: Boolean = false, val loopbackSampleRate: Int = 0)

Simple spatial audio engine using LWJGL OpenAL.

Declaration Summary
val loopbackSampleRate: Int
val sourceCount: Int
val bufferCount: Int
val outputDevice: String
var efxMaxAuxSends: Int
var reverbSlotGain: Float
var masterGain: Float
var hrtfStatus: Int?
var hrtfEnabled: Boolean
fun isInitialized(): Boolean
fun isEfxAvailable(): Boolean
fun initialize(enableHrtf: Boolean = true, deviceName: String? = null): Boolean Initialize OpenAL context and set up default listener state.
fun initializeEfx(): Boolean Initialize EFX (effects, auxiliary effect slot) if available.
fun setReverb(settings: ReverbSettings = ReverbSettings()): Boolean Configure standard EFX reverb.
fun setReverb(block: ReverbSettings.() -> Unit): Boolean Configure standard EFX reverb using a settings builder.
fun setEaxReverb(settings: EaxReverbSettings = EaxReverbSettings()): Boolean Configure high-quality EAX reverb (OpenAL Soft).
fun setEaxReverb(block: EaxReverbSettings.() -> Unit): Boolean Configure high-quality EAX reverb using a settings builder.
fun attachReverb(source: SoundSource, send: Int = 0, filter: Int = AL_FILTER_NULL): Boolean Attach the current reverb effect slot to a source.
fun detachReverb(source: SoundSource, send: Int = 0) Detach any reverb effect from a source.
fun disableReverb() Disable the current reverb effect slot.
fun destroy() Clean up OpenAL resources.
fun renderSamples(output: ShortBuffer, frames: Int) Advance a loopback engine by frames stereo frames into caller-owned direct PCM16 storage.
fun loadWav(path: String, forceMono: Boolean = true, name: String? = null): SoundBuffer Load a WAV file and create an OpenAL buffer. Only PCM WAV is supported (8/16-bit, mono or stereo). Stereo is downmixed to mono by default.
fun loadPcm16(pcm16: ShortArray, sampleRate: Int, channels: Int, name: String? = null): SoundBuffer Create an OpenAL buffer from interleaved 16-bit PCM audio.
fun loadAudioSubclip(subclip: Sound2VecAudioSubclip, name: String? = null, outputSampleRate: Int = subclip.sampleRate.takeIf { it > 0 } ?: 44100, outputChannels: Int = 2): SoundBuffer Decode a subclip from an arbitrary audio file into an OpenAL buffer. Uses FFmpeg-backed AudioDecoder, so compressed formats such as mp3 are supported.
fun playAudioSubclip(subclip: Sound2VecAudioSubclip, gain: Float = 1.0f, position: Vec3 = vec(0, 0, 0), name: String? = null): SoundSource Decode and play a subclip immediately as a non-looping source.
fun createSource(buffer: SoundBuffer? = null, looping: Boolean = false, referenceDistance: Float = 1.0f, maxDistance: Float = 1000.0f, rolloffFactor: Float = 1.0f, name: String? = null): SoundSource Create an OpenAL source (optionally with a buffer attached).
fun destroySource(source: SoundSource)
fun destroyBuffer(buffer: SoundBuffer)
fun setListener(position: Vec3, forward: Vec3, up: Vec3, velocity: Vec3? = null) Update listener position/orientation in world space.
fun setListener(px: Float, py: Float, pz: Float, fx: Float, fy: Float, fz: Float, ux: Float, uy: Float, uz: Float) Allocation-free listener update. Forward/up must be finite, nonzero directions.
fun setListenerFromOpenXR(target: OpenXRHmdTarget, positionScale: Float = 1.0f): Boolean Convenience: update listener from an OpenXR head pose.
fun setListenerFromSideBySide(target: SideBySideStereoTarget, positionScale: Float = 1.0f): Boolean Convenience: update listener from a SideBySideStereoTarget. Uses OpenXR head pose if attached, otherwise falls back to the base camera.
fun setListenerFromCamera(camera: Camera, positionScale: Float = 1.0f): Boolean Convenience: update listener from a Camera.
fun availableHrtfSpecifiers(): List<String> List available HRTF specifiers (OpenAL Soft).
fun estimateSourceEnergy(source: SoundSource, windowSeconds: Float = 0.02f): Float Estimate the RMS energy of the currently playing source.

SoundBuffer

data class SoundBuffer(val id: Int, val sampleRate: Int, val channels: Int, val sampleCount: Int, val samples16: ShortArray?, val engine: SimpleSpatialAudioEngine? = null)

Declaration Summary
val id: Int
val sampleRate: Int
val channels: Int
val sampleCount: Int
val samples16: ShortArray?
val engine: SimpleSpatialAudioEngine?

SoundSource

class SoundSource internal constructor(private val engine: SimpleSpatialAudioEngine, val id: Int)

Declaration Summary
val id: Int
var gain: Float
var pitch: Float
var looping: Boolean
fun attachBuffer(buffer: SoundBuffer)
fun detachBuffer()
fun setPosition(position: Vec3)
fun setPosition(x: Float, y: Float, z: Float)
fun setVelocity(velocity: Vec3)
fun setVelocity(x: Float, y: Float, z: Float)
fun setDistanceModel(referenceDistance: Float, maxDistance: Float, rolloffFactor: Float = 1.0f)
val currentTime: Float Gets the current playback position in seconds.
fun seekTo(timeSeconds: Float) Sets the current playback position in seconds.
fun play()
fun pause()
fun stop()
fun isPlaying(): Boolean
fun estimateEnergy(windowSeconds: Float = 0.02f): Float
fun attachReverb(send: Int = 0, filter: Int = AL_FILTER_NULL): Boolean
fun detachReverb(send: Int = 0)
fun destroy()

ReverbSettings

data class ReverbSettings(val density: Float = AL_REVERB_DEFAULT_DENSITY, val diffusion: Float = AL_REVERB_DEFAULT_DIFFUSION, val gain: Float = AL_REVERB_DEFAULT_GAIN, val gainHF: Float = AL_REVERB_DEFAULT_GAINHF, val decayTime: Float = AL_REVERB_DEFAULT_DECAY_TIME, val decayHFRatio: Float = AL_REVERB_DEFAULT_DECAY_HFRATIO, val reflectionsGain: Float = AL_REVERB_DEFAULT_REFLECTIONS_GAIN, val reflectionsDelay: Float = AL_REVERB_DEFAULT_REFLECTIONS_DELAY, val lateReverbGain: Float = AL_REVERB_DEFAULT_LATE_REVERB_GAIN, val lateReverbDelay: Float = AL_REVERB_DEFAULT_LATE_REVERB_DELAY, val airAbsorptionGainHF: Float = AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF, val roomRolloffFactor: Float = AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR, val decayHFLimit: Boolean = AL_REVERB_DEFAULT_DECAY_HFLIMIT != 0)

Declaration Summary
val density: Float
val diffusion: Float
val gain: Float
val gainHF: Float
val decayTime: Float
val decayHFRatio: Float
val reflectionsGain: Float
val reflectionsDelay: Float
val lateReverbGain: Float
val lateReverbDelay: Float
val airAbsorptionGainHF: Float
val roomRolloffFactor: Float
val decayHFLimit: Boolean

EaxReverbSettings

data class EaxReverbSettings(val density: Float = AL_EAXREVERB_DEFAULT_DENSITY, val diffusion: Float = AL_EAXREVERB_DEFAULT_DIFFUSION, val gain: Float = AL_EAXREVERB_DEFAULT_GAIN, val gainHF: Float = AL_EAXREVERB_DEFAULT_GAINHF, val gainLF: Float = AL_EAXREVERB_DEFAULT_GAINLF, val decayTime: Float = AL_EAXREVERB_DEFAULT_DECAY_TIME, val decayHFRatio: Float = AL_EAXREVERB_DEFAULT_DECAY_HFRATIO, val decayLFRatio: Float = AL_EAXREVERB_DEFAULT_DECAY_LFRATIO, val reflectionsGain: Float = AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN, val reflectionsDelay: Float = AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY, val reflectionsPanX: Float = 0.0f, val reflectionsPanY: Float = 0.0f, val reflectionsPanZ: Float = 0.0f, val lateReverbGain: Float = AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN, val lateReverbDelay: Float = AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY, val lateReverbPanX: Float = 0.0f, val lateReverbPanY: Float = 0.0f, val lateReverbPanZ: Float = 0.0f, val echoTime: Float = AL_EAXREVERB_DEFAULT_ECHO_TIME, val echoDepth: Float = AL_EAXREVERB_DEFAULT_ECHO_DEPTH, val modulationTime: Float = AL_EAXREVERB_DEFAULT_MODULATION_TIME, val modulationDepth: Float = AL_EAXREVERB_DEFAULT_MODULATION_DEPTH, val airAbsorptionGainHF: Float = AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF, val hfReference: Float = AL_EAXREVERB_DEFAULT_HFREFERENCE, val lfReference: Float = AL_EAXREVERB_DEFAULT_LFREFERENCE, val roomRolloffFactor: Float = AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR, val decayHFLimit: Boolean = AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT != 0)

Declaration Summary
val density: Float
val diffusion: Float
val gain: Float
val gainHF: Float
val gainLF: Float
val decayTime: Float
val decayHFRatio: Float
val decayLFRatio: Float
val reflectionsGain: Float
val reflectionsDelay: Float
val reflectionsPanX: Float
val reflectionsPanY: Float
val reflectionsPanZ: Float
val lateReverbGain: Float
val lateReverbDelay: Float
val lateReverbPanX: Float
val lateReverbPanY: Float
val lateReverbPanZ: Float
val echoTime: Float
val echoDepth: Float
val modulationTime: Float
val modulationDepth: Float
val airAbsorptionGainHF: Float
val hfReference: Float
val lfReference: Float
val roomRolloffFactor: Float
val decayHFLimit: Boolean

SpatialAudioLoopbackSmoke.kt

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

SpatialAudioLoopbackSmoke

object SpatialAudioLoopbackSmoke

Real OpenAL rendering without speakers: context isolation, binaural rotation, Doppler and cleanup.

Declaration Summary
fun main(args:Array<String>)

TrackingResonatorAnalysis.kt

Stores the output of a TrackingResonatorBank run as a TrackingResonatorAnalysis, with per-frame frequencies and powers, and reads and writes it in a small binary format.

TrackingPartial and TrackingResonatorTrack describe the resonators' tracks through the analysis.

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

TrackingResonatorAnalysis

class TrackingResonatorAnalysis(val sampleRate: Float, val hopLength: Int, val sourceSampleCount: Int, naturalFrequencies: FloatArray, resonantFrequencies: FloatArray, powers: FloatArray, complex: FloatArray, alphas: FloatArray = FloatArray(naturalFrequencies.size), betas: FloatArray = FloatArray(naturalFrequencies.size), gammas: FloatArray = FloatArray(naturalFrequencies.size))

Durable storage for a TrackingResonatorBank analysis.

Declaration Summary
val sampleRate: Float
val hopLength: Int
val sourceSampleCount: Int
val naturalFrequencies: FloatArray
val resonantFrequencies: FloatArray
val powers: FloatArray
val complex: FloatArray
val alphas: FloatArray
val betas: FloatArray
val gammas: FloatArray
val numResonators: Int
val numSlices: Int
val frameCount: Int
val durationSeconds: Float
fun frameTime(frame: Int): Float
fun sampleIndexForFrame(frame: Int): Int
fun naturalFrequency(resonator: Int): Float
fun resonantFrequency(frame: Int, resonator: Int): Float
fun power(frame: Int, resonator: Int): Float
fun amplitude(frame: Int, resonator: Int): Float
fun real(frame: Int, resonator: Int): Float
fun imag(frame: Int, resonator: Int): Float
fun phase(frame: Int, resonator: Int): Float
fun track(resonator: Int): TrackingResonatorTrack
fun topPartials(frame: Int, count: Int, minPower: Float = 0.0f): List<TrackingPartial>
fun resonatorsByMeanPower(count: Int = numResonators, minMeanPower: Float = 0.0f, localMaximaOnly: Boolean = false, minResonatorSpacing: Int = 0, selectionFrequencyTilt: Float = 0.0f, selectionReferenceFrequency: Float = 1000.0f): IntArray
fun meanPower(resonator: Int): Float
fun interpolatedFrequency(resonator: Int, sampleIndex: Int): Float
fun interpolatedFrequencyAt(resonator: Int, samplePosition: Double): Float
fun interpolatedPower(resonator: Int, sampleIndex: Int): Float
fun interpolatedPowerAt(resonator: Int, samplePosition: Double): Float
fun interpolatedAmplitude(resonator: Int, sampleIndex: Int): Float
fun interpolatedAmplitudeAt(resonator: Int, samplePosition: Double): Float
fun interpolatedPhase(resonator: Int, sampleIndex: Int): Float
fun interpolatedPhaseAt(resonator: Int, samplePosition: Double): Float
fun resynthesizeAdditive(length: Int = sourceSampleCount, maxPartials: Int = numResonators, minMeanPower: Float = 0.0f, gain: Float = 1.0f, normalizePeak: Float? = null, pitchShiftSemitones: Float = 0.0f, timeStretch: Float = 1.0f, amplitudeScale: Float = 2.0f, localMaximaOnly: Boolean = false, minResonatorSpacing: Int = 0, selectionFrequencyTilt: Float = 0.0f, amplitudeFrequencyTilt: Float = 0.0f, frequencyTiltReferenceHz: Float = 1000.0f, progress: ((completed: Int, total: Int) -> Unit)? = null): FloatArray
fun transformedSampleCount(timeStretch: Float): Int
fun writeTo(file: File)
fun writeTo(output: OutputStream)

TrackingPartial

data class TrackingPartial(val frame: Int, val resonator: Int, val naturalFrequency: Float, val frequency: Float, val amplitude: Float, val power: Float, val phase: Float)

Declaration Summary
val frame: Int
val resonator: Int
val naturalFrequency: Float
val frequency: Float
val amplitude: Float
val power: Float
val phase: Float

TrackingResonatorTrack

data class TrackingResonatorTrack(val resonator: Int, val naturalFrequency: Float, val frequencies: FloatArray, val amplitudes: FloatArray, val powers: FloatArray, val phases: FloatArray)

Declaration Summary
val resonator: Int
val naturalFrequency: Float
val frequencies: FloatArray
val amplitudes: FloatArray
val powers: FloatArray
val phases: FloatArray

WavFloatArrayLoader.kt

Reads a WAV file into a mono FloatArray: WavFloatArrayLoader.loadWavToFloatArray accepts PCM (8, 16, 24 and 32 bit) and IEEE float (32 and 64 bit) files and keeps the left channel of a stereo file.

The result, WavFloatData, carries the sample rate and can report RMS at a time and apply a filter.

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

WavFloatData

class WavFloatData(val samples: FloatArray, val sampleRate: Int)

Reads a WAV file into a mono FloatArray: WavFloatArrayLoader.loadWavToFloatArray accepts PCM (8, 16, 24 and 32 bit) and IEEE float (32 and 64 bit) files and keeps the left channel of a stereo file.

Declaration Summary
val samples: FloatArray
val sampleRate: Int
fun getRMSAtTime(timeSeconds: Float, windowSeconds: Float = 0.02f): Float Calculates the Root Mean Square (RMS) of a chunk of audio at the given time.
fun applyFilter(filter: afield.math.statistics.FloatFilter): WavFloatData Creates a new WavFloatData by applying a FloatFilter to all samples. This is useful for pre-filtering the entire audio file before playback.

WavFloatArrayLoader

object WavFloatArrayLoader

Declaration Summary
fun loadWavToFloatArray(path: String): WavFloatData Loads a WAV file into a FloatArray with values from -1.0 to 1.0. If the WAV file is stereo, it takes only the left channel.

WavFloatArrayWriter.kt

Writes float signals as 16-bit PCM WAV: WavFloatArrayWriter writes mono or stereo files, optionally normalizing to a peak, and encodeMono16 returns the WAV bytes for display(...) to play in a box.

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

WavFloatArrayWriter

object WavFloatArrayWriter

Writes float signals as 16-bit PCM WAV: WavFloatArrayWriter writes mono or stereo files, optionally normalizing to a peak, and encodeMono16 returns the WAV bytes for display(...) to play in a box.

Declaration Summary
fun writeMono16(path: String, samples: FloatArray, sampleRate: Int, normalizePeak: Float? = null) Writes mono 16-bit PCM WAV from normalized floats.
fun writeMono16(file: File, samples: FloatArray, sampleRate: Int, normalizePeak: Float? = null)
fun encodeMono16(samples: FloatArray, sampleRate: Int, normalizePeak: Float? = null): ByteArray Encodes mono 16-bit PCM WAV from normalized floats and returns the complete .wav bytes.
fun writeStereo16(file: File, left: FloatArray, right: FloatArray, sampleRate: Int, normalizePeak: Float? = null) Writes interleaved stereo 16-bit PCM WAV from two equal-length channels.
fun writeStereo16(path: String, left: FloatArray, right: FloatArray, sampleRate: Int, normalizePeak: Float? = null) writeStereo16 to a path.
fun encodeStereo16(left: FloatArray, right: FloatArray, sampleRate: Int, normalizePeak: Float? = null): ByteArray Encodes interleaved stereo 16-bit PCM WAV and returns the complete .wav bytes.

AmbisonicsEffects.kt

The four Ambisonics effects, each with its per-block parameter view: AmbisonicsEncodeEffect places a mono source in an Ambisonic field, AmbisonicsRotationEffect turns the field with the listener, and AmbisonicsDecodeEffect and AmbisonicsBinauralEffect render it to speakers or headphones.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/AmbisonicsEffects.kt — read the source

AmbisonicsEncodeEffectParams

public class AmbisonicsEncodeEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for an AmbisonicsEncodeEffect. A mutable native view.

Declaration Summary
public fun setDirection(x: Float, y: Float, z: Float) Direction the source should be encoded at. Allocation-free.
public var direction: Vector3 Direction the source is encoded at.
public var order: Int Ambisonic order to encode at. May be lower than the effect's maximum.

AmbisonicsEncodeEffect

public class AmbisonicsEncodeEffect private constructor(handle: MemorySegment, public val maxOrder: Int) : AudioEffect(handle)

Encodes a mono source into an Ambisonic field arriving from a given direction.

Declaration Summary
public val maxOrder: Int Highest Ambisonic order this effect can encode.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val channelCount: Int Number of output channels at maxOrder.
public fun apply(params: AmbisonicsEncodeEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block. Input mono, output Ambisonic.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): AmbisonicsEncodeEffect Increments the reference count and returns an independently closeable wrapper.

AmbisonicsDecodeEffectParams

public class AmbisonicsDecodeEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for an AmbisonicsDecodeEffect. A mutable native view.

Declaration Summary
public var order: Int Ambisonic order to decode. May be lower than the effect's maximum.
public var binaural: Boolean Whether to decode binaurally (HRTF) rather than to the speaker layout.
public var orientation: CoordinateSpace3 The listener's orientation, used to rotate the Ambisonic field into head-relative space.
public var hrtf: Hrtf? The HRTF used for binaural decoding. Held as a raw pointer, so it must stay alive.

AmbisonicsDecodeEffect

public class AmbisonicsDecodeEffect private constructor(handle: MemorySegment, public val maxOrder: Int, public val speakerLayout: SpeakerLayout, private val layoutArena: Arena?) : AudioEffect(handle)

Decodes an Ambisonic field to speakers or, in binaural mode, to headphones.

Declaration Summary
public val maxOrder: Int Highest Ambisonic order this effect can decode.
public val speakerLayout: SpeakerLayout The speaker layout used when not decoding binaurally.
override fun releaseAction(): (MemorySegment) -> Unit
override fun onClosed()
override val typeName: String
public val inputChannelCount: Int Number of Ambisonic input channels expected.
public fun apply(params: AmbisonicsDecodeEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): AmbisonicsDecodeEffect Increments the reference count and returns an independently closeable wrapper.

AmbisonicsBinauralEffectParams

public class AmbisonicsBinauralEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for an AmbisonicsBinauralEffect.

Declaration Summary
public var order: Int Ambisonic order to render.
public var hrtf: Hrtf? The HRTF to render with. Held as a raw pointer.

AmbisonicsBinauralEffect

public class AmbisonicsBinauralEffect private constructor(handle: MemorySegment, public val maxOrder: Int) : AudioEffect(handle)

Renders an Ambisonic field directly to headphones via HRTF.

Declaration Summary
public val maxOrder: Int Highest Ambisonic order this effect can render.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val inputChannelCount: Int Number of Ambisonic input channels expected.
public fun apply(params: AmbisonicsBinauralEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block. Input Ambisonic, output stereo.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): AmbisonicsBinauralEffect Increments the reference count and returns an independently closeable wrapper.

AmbisonicsRotationEffectParams

public class AmbisonicsRotationEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for an AmbisonicsRotationEffect.

Declaration Summary
public var order: Int Ambisonic order to rotate.
public var orientation: CoordinateSpace3 The frame to rotate the field into -- typically the listener's head orientation.

AmbisonicsRotationEffect

public class AmbisonicsRotationEffect private constructor(handle: MemorySegment, public val maxOrder: Int) : AudioEffect(handle)

Rotates an Ambisonic field, so a world-space field can be made head-relative.

Declaration Summary
public val maxOrder: Int Highest Ambisonic order this effect can rotate.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val channelCount: Int Number of Ambisonic channels this effect works on.
public fun apply(params: AmbisonicsRotationEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block. Input and output both Ambisonic with the same channel count.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): AmbisonicsRotationEffect Increments the reference count and returns an independently closeable wrapper.

AudioBuffer.kt

Audio buffers and the settings every Steam Audio effect is created against.

AudioSettings fixes the sampling rate and block size; AudioBuffer holds deinterleaved per-channel samples in native memory for an effect to read or write; SpeakerLayout describes the output channel arrangement.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/AudioBuffer.kt — read the source

AudioSettings

public data class AudioSettings(public val samplingRate: Int = 48_000, public val frameSize: Int = 1024)

Sampling rate and block size, shared by every effect created against them.

Declaration Summary
public val samplingRate: Int Samples per second, for example 48000.
public val frameSize: Int Samples per channel in each processing block.
public val frameDuration: Double Duration of one block, in seconds.

AudioBuffer

public class AudioBuffer private constructor(internal val struct: MemorySegment, internal val samples: MemorySegment, private val channelPointers: MemorySegment, public val channelCount: Int, public val sampleCount: Int)

A block of deinterleaved 32-bit float audio in native memory.

Declaration Summary
public val channelCount: Int Number of channels.
public val sampleCount: Int Number of samples per channel.
public val totalSamples: Int Total number of samples across all channels.
public fun channel(index: Int): MemorySegment A view of one channel's samples.
public fun clear() Sets every sample in every channel to zero.
public fun clearChannel(index: Int) Sets every sample in one channel to zero.
public fun copyFrom(source: AudioBuffer) Copies all samples from source, which must have the same shape.
public fun readChannel(index: Int, destination: FloatArray, destinationOffset: Int = 0) Reads one channel into destination, starting at destinationOffset.
public fun writeChannel(index: Int, source: FloatArray, sourceOffset: Int = 0) Writes source into one channel, starting from sourceOffset.
public operator fun get(channel: Int, sample: Int): Float Reads a single sample. Bounds-checked; prefer channel or readChannel in bulk.
public operator fun set(channel: Int, sample: Int, value: Float) Writes a single sample. Bounds-checked; prefer channel or writeChannel in bulk.
public fun interleaveTo(context: SteamAudioContext, destination: MemorySegment) Writes this buffer's samples to destination in interleaved order.
public fun interleaveTo(context: SteamAudioContext, destination: FloatArray) Interleaves into a scratch MemorySegment and copies out to destination.
public fun interleaveTo(context: SteamAudioContext, destination: FloatBuffer) Interleaves directly into a direct FloatBuffer.
public fun deinterleaveFrom(context: SteamAudioContext, source: MemorySegment) Fills this buffer from interleaved samples in source.
public fun deinterleaveFrom(context: SteamAudioContext, source: FloatArray) Fills this buffer from interleaved samples held on the JVM heap. See interleaveTo.
public fun deinterleaveFrom(context: SteamAudioContext, source: FloatBuffer) Fills this buffer from a direct FloatBuffer of interleaved samples.
public fun mixInto(context: SteamAudioContext, mix: AudioBuffer) Adds this buffer's samples into mix, which must have the same shape.
public fun downmixTo(context: SteamAudioContext, destination: AudioBuffer) Downmixes this buffer to mono in destination.
public fun convertAmbisonicsTo(context: SteamAudioContext, destination: AudioBuffer, from: AmbisonicsType, to: AmbisonicsType) Converts between Ambisonic channel-ordering and normalisation conventions.
public fun withChannelCount(arena: Arena, channels: Int): AudioBuffer A read-only view of this buffer reinterpreted as having fewer channels.
public fun peak(): Float Largest absolute sample value, useful for metering and for tail-decay checks.
public fun isSilent(): Boolean True when every sample is exactly zero.
override fun toString(): String

SpeakerLayout

public class SpeakerLayout private constructor(public val type: SpeakerLayoutType, public val speakers: List<Vector3>)

How audio is laid out across speakers.

Declaration Summary
public val type: SpeakerLayoutType Which standard layout this is, or SpeakerLayoutType.CUSTOM.
public val speakers: List<Vector3> Explicit speaker directions for a custom layout, otherwise empty.
public val channelCount: Int The number of channels this layout implies.
override fun toString(): String

AudioEffect.kt

The base class every Steam Audio effect extends: AudioEffect resets state, reports and flushes the reverb tail, and AudioEffect.drainTail runs that flush loop for a source that has stopped.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/AudioEffect.kt — read the source

AudioEffect

public abstract class AudioEffect internal constructor(handle: MemorySegment) : SteamAudioObject(handle)

Common shape for Steam Audio's audio effects.

Declaration Summary
public abstract fun reset() Clears internal state, including any pending tail.
public abstract val tailSize: Int Number of samples of tail the effect currently holds.
public abstract fun getTail(output: AudioBuffer): AudioEffectState Renders one block of tail audio into output, returning whether more remains.
public fun drainTail(scratch: AudioBuffer, maxBlocks: Int = 1400, onBlock: (AudioBuffer) -> Unit): Int Repeatedly renders tail blocks into scratch, handing each to onBlock, until the effect reports AudioEffectState.TAIL_COMPLETE.

Baking.kt

Offline bakes over a ProbeBatch: the pathing visibility graph and reflection data.

PathBaker with PathBakeSettings precomputes which probes can see each other, which PathEffect then searches at runtime; ReflectionsBaker with ReflectionsBakeSettings stores reverb or static-endpoint reflections so they need not be simulated live.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Baking.kt — read the source

PathBakeSettings

public data class PathBakeSettings(public val scene: Scene, public val probeBatch: ProbeBatch, public val identifier: BakedDataIdentifier = BakedDataIdentifier.PATHING, public val numSamples: Int = 1, public val radius: Float = 1.0f, public val threshold: Float = 0.1f, public val visRange: Float = 50.0f, public val pathRange: Float = 100.0f, public val numThreads: Int = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1))

Precomputes the probe-to-probe visibility graph that PathEffect searches at runtime.

Declaration Summary
public val scene: Scene The scene to bake against. Must be committed.
public val probeBatch: ProbeBatch The probe batch to record visibility into. Must be committed.
public val identifier: BakedDataIdentifier Which slot in the batch the data is stored under.
public val numSamples: Int Rays cast per probe pair to decide mutual visibility.
public val radius: Float Radius around each probe that counts as visible, in metres.
public val threshold: Float Fraction of sample rays that must get through for two probes to count as visible.
public val visRange: Float Maximum distance over which mutual visibility is tested, in metres.
public val pathRange: Float Maximum path length recorded, in metres.
public val numThreads: Int Worker threads to spread the bake across.

PathBaker

public object PathBaker

Bakes the pathing visibility graph. See PathBakeSettings.

Declaration Summary
public fun bake(context: SteamAudioContext, settings: PathBakeSettings, progress: ProgressCallback? = null) Bakes pathing data into settings's probe batch. Blocks until done or cancelled.
public fun cancel(context: SteamAudioContext) Asks an in-progress bake to stop.

ReflectionsBakeSettings

public data class ReflectionsBakeSettings(public val scene: Scene, public val probeBatch: ProbeBatch, public val identifier: BakedDataIdentifier = BakedDataIdentifier.REVERB, public val sceneType: SceneType = SceneType.DEFAULT, public val bakeFlags: ReflectionsBakeFlags = ReflectionsBakeFlags.BAKE_PARAMETRIC, public val numRays: Int = 8192, public val numDiffuseSamples: Int = 1024, public val numBounces: Int = 32, public val simulatedDuration: Float = 2.0f, public val savedDuration: Float = 2.0f, public val order: Int = 1, public val numThreads: Int = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1), public val rayBatchSize: Int = 1, public val irradianceMinDistance: Float = 1.0f, public val bakeBatchSize: Int = 1, public val openClDevice: OpenClDevice? = null, public val radeonRaysDevice: RadeonRaysDevice? = null)

Precomputes reflection data -- impulse responses, or reverb decay times -- at each probe.

Declaration Summary
public val scene: Scene The scene to bake against. Must be committed.
public val probeBatch: ProbeBatch The probe batch to record into. Must be committed.
public val identifier: BakedDataIdentifier Which slot in the batch the data is stored under.
public val sceneType: SceneType Ray tracer backend; must match scene's type.
public val bakeFlags: ReflectionsBakeFlags Whether to bake impulse responses, decay times, or both.
public val numRays: Int Rays cast per probe.
public val numDiffuseSamples: Int Diffuse samples taken per bounce.
public val numBounces: Int Bounces per ray.
public val simulatedDuration: Float Length of impulse response to simulate, in seconds.
public val savedDuration: Float Length of impulse response to store, in seconds.
public val order: Int Ambisonic order to bake at.
public val numThreads: Int Worker threads.
public val rayBatchSize: Int Rays per batch, for the Radeon Rays backend.
public val irradianceMinDistance: Float Minimum distance used when accumulating irradiance, bounding grazing-ray energy.
public val bakeBatchSize: Int Probes baked per batch, trading memory against scheduling overhead.
public val openClDevice: OpenClDevice? OpenCL device, for GPU-backed baking.
public val radeonRaysDevice: RadeonRaysDevice? Radeon Rays device, for GPU-backed baking.

ReflectionsBaker

public object ReflectionsBaker

Bakes reflection data. See ReflectionsBakeSettings.

Declaration Summary
public fun bake(context: SteamAudioContext, settings: ReflectionsBakeSettings, progress: ProgressCallback? = null) Bakes reflection data into settings's probe batch. Blocks until done or cancelled.
public fun cancel(context: SteamAudioContext) Asks an in-progress bake to stop. Call from another thread; see PathBaker.cancel.

BinauralEffect.kt

HRTF-based binaural rendering of a mono source: BinauralEffect and the native-memory BinauralEffectParams it reads each block.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/BinauralEffect.kt — read the source

BinauralEffectParams

public class BinauralEffectParams private constructor(internal val segment: MemorySegment, private val peakDelaySlot: MemorySegment)

Per-block parameters for a BinauralEffect, held directly in native memory.

Declaration Summary
public fun setDirection(x: Float, y: Float, z: Float) Direction from the listener to the source, in the listener's local frame: +x right, +y up, -z ahead.
public var direction: Vector3 The source direction, in the listener's local frame. Allocates on read; see setDirection.
public var interpolation: HrtfInterpolation How HRTF data is interpolated between measured directions.
public var spatialBlend: Float Blend between unspatialised (0) and fully spatialised (1) output.
public var hrtf: Hrtf? The HRTF used to render.
public val leftPeakDelay: Float? Peak delay for the left ear in samples, or null unless trackPeakDelays was set.
public val rightPeakDelay: Float? Peak delay for the right ear in samples, or null unless trackPeakDelays was set.

BinauralEffect

public class BinauralEffect private constructor(handle: MemorySegment) : AudioEffect(handle)

Renders a mono source to stereo using a head-related transfer function, so it appears to come from a specific direction relative to the listener.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun apply(params: BinauralEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): BinauralEffect Increments the reference count and returns an independently closeable wrapper.

Callbacks.kt

Callback interfaces for Steam Audio: PathingVisualizationCallback and ProgressCallback for watching pathing and baking, and CustomSceneCallbacks with HitResult for supplying your own ray tracer in place of Steam Audio's scene.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Callbacks.kt — read the source

PathingVisualizationCallback

public fun interface PathingVisualizationCallback

Receives each probe-to-probe segment the pathing search considered.

Declaration Summary
public fun onSegment(fromX: Float, fromY: Float, fromZ: Float, toX: Float, toY: Float, toZ: Float, occluded: Boolean)

ProgressCallback

public fun interface ProgressCallback

Reports progress of a long-running operation as a fraction in 0..1.

Declaration Summary
public fun onProgress(progress: Float)

HitResult

public class HitResult internal constructor(private val segment: MemorySegment)

What a ray hit, for a custom scene.

Declaration Summary
public var distance: Float Distance along the ray to the hit point. Set this negative to report a miss.
public var triangleIndex: Int Index of the triangle that was hit, or -1 if not meaningful.
public var objectIndex: Int Index of the object that was hit, or -1 if not meaningful.
public var materialIndex: Int Index of the material at the hit point, or -1 if not meaningful.
public fun setNormal(x: Float, y: Float, z: Float) Surface normal at the hit point. Should be unit length.
public fun setMaterial(material: MemorySegment) The acoustic material at the hit point.
public fun setMiss() Records a miss.

CustomSceneCallbacks

public interface CustomSceneCallbacks

Ray-cast callbacks for a SceneType.CUSTOM scene, letting Steam Audio query geometry that lives in your own acceleration structure rather than in a Scene.

Declaration Summary
public fun closestHit(originX: Float, originY: Float, originZ: Float, directionX: Float, directionY: Float, directionZ: Float, minDistance: Float, maxDistance: Float, hit: HitResult) Finds the closest hit along a ray, between minDistance and maxDistance.
public fun anyHit(originX: Float, originY: Float, originZ: Float, directionX: Float, directionY: Float, directionZ: Float, minDistance: Float, maxDistance: Float): Boolean Reports whether a ray hits anything between minDistance and maxDistance.
public fun batchedClosestHit(rayCount: Int, rays: MemorySegment, minDistances: MemorySegment, maxDistances: MemorySegment, hits: MemorySegment) Batched form of closestHit. Defaults to looping over the single-ray version.
public fun batchedAnyHit(rayCount: Int, rays: MemorySegment, minDistances: MemorySegment, maxDistances: MemorySegment, occluded: MemorySegment) Batched form of anyHit. Writes 0 or 1 per ray into occluded.

Devices.kt

Optional compute devices for Steam Audio: EmbreeDevice for the Embree ray tracer, OpenClDeviceList and OpenClDevice for GPU work, and the RadeonRaysDevice and TrueAudioNextDevice built on OpenCL.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Devices.kt — read the source

EmbreeDevice

public class EmbreeDevice private constructor(handle: MemorySegment) : SteamAudioObject(handle)

An Intel Embree device, enabling the SceneType.EMBREE ray tracer.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun retain(): EmbreeDevice Increments the reference count and returns an independently closeable wrapper.

OpenClDeviceDescription

public data class OpenClDeviceDescription(public val platformName: String, public val platformVendor: String, public val platformVersion: String, public val deviceName: String, public val deviceVendor: String, public val deviceVersion: String, public val type: OpenClDeviceType, public val convolutionComputeUnits: Int, public val irUpdateComputeUnits: Int, public val granularity: Int, public val performanceScore: Float)

Describes one OpenCL device that Steam Audio could use.

Declaration Summary
public val platformName: String
public val platformVendor: String
public val platformVersion: String
public val deviceName: String
public val deviceVendor: String
public val deviceVersion: String
public val type: OpenClDeviceType
public val convolutionComputeUnits: Int Compute units reserved for convolution.
public val irUpdateComputeUnits: Int Compute units reserved for impulse-response updates.
public val granularity: Int The device's compute-unit granularity.
public val performanceScore: Float A relative performance score; higher is faster.
override fun toString(): String

OpenClDeviceList

public class OpenClDeviceList private constructor(handle: MemorySegment) : SteamAudioObject(handle)

The set of OpenCL devices available, so you can pick one before committing to it.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val size: Int Number of devices found.
public operator fun get(index: Int): OpenClDeviceDescription Describes the device at index.
public fun toList(): List<OpenClDeviceDescription> All devices found, as descriptions.
public fun retain(): OpenClDeviceList Increments the reference count and returns an independently closeable wrapper.

OpenClDevice

public class OpenClDevice private constructor(handle: MemorySegment) : SteamAudioObject(handle)

An OpenCL device, needed for Radeon Rays and TrueAudio Next.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun retain(): OpenClDevice Increments the reference count and returns an independently closeable wrapper.

RadeonRaysDevice

public class RadeonRaysDevice private constructor(handle: MemorySegment) : SteamAudioObject(handle)

An AMD Radeon Rays device, enabling the SceneType.RADEON_RAYS GPU ray tracer.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun retain(): RadeonRaysDevice Increments the reference count and returns an independently closeable wrapper.

TrueAudioNextDevice

public class TrueAudioNextDevice private constructor(handle: MemorySegment) : SteamAudioObject(handle)

An AMD TrueAudio Next device, enabling ReflectionEffectType.TRUE_AUDIO_NEXT -- GPU-accelerated convolution reflections.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun retain(): TrueAudioNextDevice Increments the reference count and returns an independently closeable wrapper.

DirectEffect.kt

Rendering the direct sound path: distance attenuation, air absorption, directivity, occlusion and transmission.

DirectEffect filters one source's audio per block according to a DirectEffectParams.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/DirectEffect.kt — read the source

DirectEffectParams

public class DirectEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for a DirectEffect, held directly in native memory.

Declaration Summary
public var flags: DirectEffectFlags Which of the quantities below are actually applied.
public var transmissionType: TransmissionType Whether transmission is applied per-band or as a single broadband gain.
public var distanceAttenuation: Float Linear gain from distance falloff, where 1 is no attenuation.
public var directivity: Float Linear gain from the source's directivity pattern, where 1 is omnidirectional.
public var occlusion: Float Fraction of the direct path that reaches the listener: 1 is clear, 0 fully blocked.
public fun setAirAbsorption(band: Int, gain: Float) Per-band gain from air absorption, where 1 is no absorption.
public fun airAbsorption(band: Int): Float Reads back a per-band air absorption gain.
public fun setAirAbsorption(gains: FloatArray) Copies all SteamAudio.NUM_BANDS air absorption gains from gains.
public fun setTransmission(band: Int, gain: Float) Per-band gain for sound transmitted through geometry.
public fun transmission(band: Int): Float Reads back a per-band transmission gain.
public fun setTransmission(gains: FloatArray) Copies all SteamAudio.NUM_BANDS transmission gains from gains.
public fun copyFrom(other: DirectEffectParams) Copies every field from another parameter block, in one native memory copy.

DirectEffect

public class DirectEffect private constructor(handle: MemorySegment, public val channelCount: Int) : AudioEffect(handle)

Applies the direct-path acoustics of a source: distance falloff, air absorption, directivity, occlusion and transmission.

Declaration Summary
public val channelCount: Int The channel count this effect was created for.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun apply(params: DirectEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block. Input and output must both have channelCount channels.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): DirectEffect Increments the reference count and returns an independently closeable wrapper.

EnergyField.kt

Simulated reflection data as energy, and its reconstruction into impulse responses.

EnergyField bins arriving energy by time, direction and band; a Reconstructor turns it into an ImpulseResponse that can be convolved with a signal.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/EnergyField.kt — read the source

EnergyField

public class EnergyField private constructor(handle: MemorySegment) : SteamAudioObject(handle)

Acoustic energy arriving at a point, binned by time, direction and frequency band.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val channelCount: Int Number of Ambisonic channels.
public val binCount: Int Number of time bins per band.
public fun data(): MemorySegment A view of the whole field: channelCount * NUM_BANDS * binCount floats, contiguous.
public fun channel(index: Int): MemorySegment A view of one Ambisonic channel: NUM_BANDS * binCount floats.
public fun band(channelIndex: Int, bandIndex: Int): MemorySegment A view of one band within one channel: binCount floats.
public fun readBand(channelIndex: Int, bandIndex: Int, destination: FloatArray) Copies one band's time bins into destination, which must hold binCount floats.
public fun reset() Zeroes the field.
public fun copyInto(destination: EnergyField) Copies this field into destination, which must have the same shape.
public fun swapWith(other: EnergyField) Exchanges contents with other. Cheap -- useful for double-buffering simulation results.
public fun scaleInto(scalar: Float, destination: EnergyField) Scales this field by scalar, writing into destination.
public fun scaleAccumulateInto(scalar: Float, destination: EnergyField) Scales this field by scalar and adds it into destination.
public fun retain(): EnergyField Increments the reference count and returns an independently closeable wrapper.

ImpulseResponse

public class ImpulseResponse private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A multi-channel impulse response: actual audio, ready to be convolved with a source.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val channelCount: Int Number of Ambisonic channels.
public val sampleCount: Int Number of samples per channel.
public fun data(): MemorySegment A view of all samples: channelCount * sampleCount floats, contiguous.
public fun channel(index: Int): MemorySegment A view of one channel's samples.
public fun readChannel(index: Int, destination: FloatArray) Copies one channel into destination, which must hold sampleCount floats.
public fun reset() Zeroes the impulse response.
public fun copyInto(destination: ImpulseResponse) Copies this impulse response into destination.
public fun swapWith(other: ImpulseResponse) Exchanges contents with other.
public fun scaleInto(scalar: Float, destination: ImpulseResponse) Scales this impulse response by scalar, writing into destination.
public fun scaleAccumulateInto(scalar: Float, destination: ImpulseResponse) Scales by scalar and adds into destination.
public fun retain(): ImpulseResponse Increments the reference count and returns an independently closeable wrapper.

Reconstructor

public class Reconstructor private constructor(handle: MemorySegment) : SteamAudioObject(handle)

Turns EnergyFields into ImpulseResponses.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun reconstruct(energyFields: List<EnergyField>, impulseResponses: List<ImpulseResponse>, duration: Float, order: Int = 1) Reconstructs impulse responses from energy fields, pairwise.
public fun retain(): Reconstructor Increments the reference count and returns an independently closeable wrapper.

Enums.kt

Kotlin enumerations mirroring Steam Audio's C enums (log levels, scene types, HRTF and reflection options, baking variations and the rest), each carrying the native value that crosses the FFM boundary. The bit-flag sets are value classes over an Int, not enums.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Enums.kt — read the source

LogLevel

public enum class LogLevel(override val value: Int) : NativeEnum

Severity of a message passed to a LogCallback.

Constants: INFO, WARNING, ERROR, DEBUG.

SimdLevel

public enum class SimdLevel(override val value: Int) : NativeEnum

SIMD instruction sets Steam Audio may use.

Constants: SSE2, SSE4, AVX, AVX2, AVX512.

OpenClDeviceType

public enum class OpenClDeviceType(override val value: Int) : NativeEnum

Which class of OpenCL device to use.

Constants: ANY, CPU, GPU.

SceneType

public enum class SceneType(override val value: Int) : NativeEnum

The ray-tracing backend a Scene uses.

Constants: DEFAULT, EMBREE, RADEON_RAYS, CUSTOM.

OcclusionType

public enum class OcclusionType(override val value: Int) : NativeEnum

How occlusion is sampled for a source.

Constants: RAYCAST, VOLUMETRIC.

SpeakerLayoutType

public enum class SpeakerLayoutType(override val value: Int) : NativeEnum

The speaker configuration of an audio buffer.

Constants: MONO, STEREO, QUADRAPHONIC, SURROUND_5_1, SURROUND_7_1, CUSTOM.

Declaration Summary
public val channelCount: Int? The channel count implied by this layout, or null for CUSTOM.

AmbisonicsType

public enum class AmbisonicsType(override val value: Int) : NativeEnum

Ambisonic channel ordering and normalisation convention.

Constants: N3D, SN3D, FUMA.

AudioEffectState

public enum class AudioEffectState(override val value: Int) : NativeEnum

Whether an effect still has a reverb tail left to render.

Constants: TAIL_REMAINING, TAIL_COMPLETE.

HrtfType

public enum class HrtfType(override val value: Int) : NativeEnum

Where HRTF data comes from.

Constants: DEFAULT, SOFA.

HrtfNormType

public enum class HrtfNormType(override val value: Int) : NativeEnum

Volume normalisation applied to HRTF data at load time.

Constants: NONE, RMS.

HrtfInterpolation

public enum class HrtfInterpolation(override val value: Int) : NativeEnum

How HRTF data is interpolated between measured directions.

Constants: NEAREST, BILINEAR.

TransmissionType

public enum class TransmissionType(override val value: Int) : NativeEnum

How transmission through geometry is modelled.

Constants: FREQUENCY_INDEPENDENT, FREQUENCY_DEPENDENT.

DistanceAttenuationModelType

public enum class DistanceAttenuationModelType(override val value: Int) : NativeEnum

How distance attenuation is computed.

Constants: DEFAULT, INVERSE_DISTANCE, CALLBACK.

AirAbsorptionModelType

public enum class AirAbsorptionModelType(override val value: Int) : NativeEnum

How air absorption is computed.

Constants: DEFAULT, EXPONENTIAL, CALLBACK.

DeviationModelType

public enum class DeviationModelType(override val value: Int) : NativeEnum

How deviation-based attenuation (for pathing) is computed.

Constants: DEFAULT, CALLBACK.

ReflectionEffectType

public enum class ReflectionEffectType(override val value: Int) : NativeEnum

The algorithm a ReflectionEffect uses to render reflections.

Constants: CONVOLUTION, PARAMETRIC, HYBRID, TRUE_AUDIO_NEXT.

ProbeGenerationType

public enum class ProbeGenerationType(override val value: Int) : NativeEnum

How probe positions are generated within a box.

Constants: CENTROID, UNIFORM_FLOOR.

BakedDataVariation

public enum class BakedDataVariation(override val value: Int) : NativeEnum

What a piece of baked data varies over.

Constants: REVERB, STATIC_SOURCE, STATIC_LISTENER, DYNAMIC.

BakedDataType

public enum class BakedDataType(override val value: Int) : NativeEnum

Which simulation stage a piece of baked data belongs to.

Constants: REFLECTIONS, PATHING.

ContextFlags

public value class ContextFlags(public val bits: Int)

Optional SteamAudioContext behaviour.

Declaration Summary
public infix fun or(other: ContextFlags): ContextFlags
public operator fun contains(other: ContextFlags): Boolean True when every bit set in other is also set here.
override fun toString(): String

DirectEffectFlags

public value class DirectEffectFlags(public val bits: Int)

Which components a DirectEffect should apply.

Declaration Summary
public infix fun or(other: DirectEffectFlags): DirectEffectFlags
public operator fun contains(other: DirectEffectFlags): Boolean
override fun toString(): String

SimulationFlags

public value class SimulationFlags(public val bits: Int)

Which simulation stages a Simulator or Source participates in.

Declaration Summary
public infix fun or(other: SimulationFlags): SimulationFlags
public operator fun contains(other: SimulationFlags): Boolean
override fun toString(): String

DirectSimulationFlags

public value class DirectSimulationFlags(public val bits: Int)

Which direct-path quantities a simulation should compute.

Declaration Summary
public infix fun or(other: DirectSimulationFlags): DirectSimulationFlags
public operator fun contains(other: DirectSimulationFlags): Boolean
override fun toString(): String

ReflectionsBakeFlags

public value class ReflectionsBakeFlags(public val bits: Int)

Which kinds of reflection data a bake should produce.

Declaration Summary
public infix fun or(other: ReflectionsBakeFlags): ReflectionsBakeFlags
public operator fun contains(other: ReflectionsBakeFlags): Boolean
override fun toString(): String

Geometry.kt

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Geometry.kt — read the source

Vector3

public data class Vector3(public val x: Float, public val y: Float, public val z: Float)

A point or direction in 3D space.

Declaration Summary
public val x: Float
public val y: Float
public val z: Float
public operator fun plus(other: Vector3): Vector3
public operator fun minus(other: Vector3): Vector3
public operator fun times(scalar: Float): Vector3
public operator fun unaryMinus(): Vector3
public val length: Float Euclidean length.
public fun normalized(): Vector3 This vector scaled to unit length, or ZERO if it has no length.
public infix fun dot(other: Vector3): Float
public infix fun cross(other: Vector3): Vector3

Matrix4x4

public class Matrix4x4(elements: FloatArray)

A 4x4 transform matrix, stored row-major to match IPLMatrix4x4.

Declaration Summary
public val elements: FloatArray
public operator fun get(row: Int, column: Int): Float
public operator fun times(other: Matrix4x4): Matrix4x4 Matrix product: this * other.
public fun transformPoint(point: Vector3): Vector3 Transforms a point, applying the translation column.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

Box

public data class Box(public val min: Vector3, public val max: Vector3)

An axis-aligned bounding box.

Declaration Summary
public val min: Vector3
public val max: Vector3
public val center: Vector3

Sphere

public data class Sphere(public val center: Vector3, public val radius: Float)

A sphere, used both for probes and for baked-data influence regions.

Declaration Summary
public val center: Vector3
public val radius: Float

CoordinateSpace3

public data class CoordinateSpace3(public val right: Vector3, public val up: Vector3, public val ahead: Vector3, public val origin: Vector3)

A position and orientation: three orthonormal axes plus an origin.

Declaration Summary
public val right: Vector3
public val up: Vector3
public val ahead: Vector3
public val origin: Vector3

Ray

public data class Ray(public val origin: Vector3, public val direction: Vector3)

A ray: an origin and a direction.

Declaration Summary
public val origin: Vector3
public val direction: Vector3

Material

public data class Material(public val absorption: FloatArray, public val scattering: Float, public val transmission: FloatArray)

An acoustic material: per-band absorption and transmission, plus a scattering coefficient.

Declaration Summary
public val absorption: FloatArray Fraction of energy absorbed per frequency band. One entry per SteamAudio.NUM_BANDS.
public val scattering: Float Fraction of energy scattered (rather than reflected specularly) on reflection.
public val transmission: FloatArray Fraction of energy transmitted through the surface, per frequency band.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

Geometry2D.kt

Puts a 2D game's walls, sources and listener into Steam Audio's 3D simulation: World2D fixes the axis mapping and wall height, and WallSegment, WallSet, MovableWall and SharedWalls turn line segments into scene geometry.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Geometry2D.kt — read the source

World2D

public data class World2D(public val floorDepth: Float = -1.75f, public val ceilingDepth: Float = 1.75f, public val earDepth: Float = 0f)

Maps a 2D game world onto the 3D world Steam Audio simulates in.

Declaration Summary
public val floorDepth: Float
public val ceilingDepth: Float
public val earDepth: Float
public val thickness: Float The acoustic thickness of the extruded space.
public fun point(x: Float, y: Float): Vector3 Converts a 2D game point to a world position at earDepth.
public fun point(x: Float, y: Float, z: Float): Vector3 Converts a game point with an explicit z to a world position.
public fun listenerFacing(x: Float, y: Float, facingX: Float, facingY: Float): CoordinateSpace3 Builds a listener (or directional source) frame from a 2D position and a 2D facing direction, both in game coordinates.
public fun segmentTransform(x0: Float, y0: Float, x1: Float, y1: Float, quadLength: Float): Matrix4x4 The transform placing a wall quad of quadLength -- centred on the origin and running along world +x, as MovableWall builds it -- onto the game segment (x0, y0)-(x1, y1).
public fun listenerAtAngle(x: Float, y: Float, radians: Float): CoordinateSpace3 A listener frame from a facing angle in radians, measured counter-clockwise from game +x -- the natural reading of a rotation about +z.

WallSegment

public data class WallSegment(public val x0: Float, public val y0: Float, public val x1: Float, public val y1: Float, public val material: Material = Material.ROCK)

A wall as a 2D line segment, the geometric primitive of a 2D game.

Declaration Summary
public val x0: Float
public val y0: Float
public val x1: Float
public val y1: Float
public val material: Material
public val length: Float Length of the segment in the 2D plane.
public val midX: Float Midpoint in the 2D plane.
public val midY: Float Midpoint in the 2D plane.
public val angle: Float Angle of the segment, counter-clockwise from game +x, in radians.

WallSet

public class WallSet(public val world: World2D, public val segments: List<WallSegment>)

Builds acoustic geometry from 2D wall segments.

Declaration Summary
public val world: World2D The 2D-to-3D convention to build against.
public val segments: List<WallSegment> The wall segments making up this area.
public fun build(scene: Scene, bounds: FloatArray? = null, capped: Boolean = true, doubleSided: Boolean = true, floorMaterial: Material? = null): StaticMesh Builds the walls -- and, unless disabled, a floor and ceiling -- as a single static mesh.
public fun triangleCount(capped: Boolean = true, doubleSided: Boolean = true): Int Number of triangles build will produce, for budgeting.

MovableWall

public class MovableWall private constructor(private val world: World2D, public val length: Float, private val instance: InstancedMesh, private val ownedScene: Scene?, private val ownedMesh: StaticMesh?) : AutoCloseable

A wall segment that can move, slide, rotate or disappear.

Declaration Summary
public val length: Float The length the underlying quad was built at.
public fun addTo(scene: Scene) Adds this wall to scene. Takes effect on the next Scene.commit.
public fun removeFrom(scene: Scene) Removes this wall from scene. Takes effect on the next Scene.commit.
public fun moveTo(scene: Scene, x0: Float, y0: Float, x1: Float, y1: Float) Places this wall along the 2D segment from (x0, y0) to (x1, y1).
public fun moveTo(scene: Scene, segment: WallSegment) Places this wall along segment.
public fun hide(scene: Scene) Moves this wall far outside the world, so it stops affecting the simulation.
override fun close()

SharedWalls

public class SharedWalls internal constructor(public val walls: List<MovableWall>, private val sharedScene: Scene, private val sharedMesh: StaticMesh) : AutoCloseable

A group of MovableWalls sharing one sub-scene.

Declaration Summary
public val walls: List<MovableWall> The walls, each independently positionable.
public operator fun get(index: Int): MovableWall The wall at index.
public val size: Int How many walls this group holds.
public fun addAllTo(scene: Scene) Adds every wall to scene.
override fun close()

Handles.kt

Lifetime management for Steam Audio native objects: SteamAudioObject wraps a reference-counted handle, releases it once on close, and keeps a cleaner as a backstop for objects that are dropped without closing.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Handles.kt — read the source

SteamAudioObject

public abstract class SteamAudioObject internal constructor(internal val handle: MemorySegment) : AutoCloseable

Base class for the reference-counted Steam Audio objects.

Declaration Summary
public val isClosed: Boolean True once close has run.
final override fun close() Decrements the native reference count, releasing the object when the count reaches zero.
override fun toString(): String

Hrtf.kt

The head-related transfer function that binaural rendering filters through: one Hrtf per context, shared by every BinauralEffect and kept open longer than all of them.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Hrtf.kt — read the source

Hrtf

public class Hrtf private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A head-related transfer function: the filter pair that makes a mono source appear to come from a particular direction.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun retain(): Hrtf Increments the reference count and returns an independently closeable wrapper.

Models.kt

Per-source models for Steam Audio's direct path: DistanceAttenuationModel, AirAbsorptionModel and Directivity.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Models.kt — read the source

DistanceAttenuationModel

public class DistanceAttenuationModel private constructor(public val type: DistanceAttenuationModelType, public val minDistance: Float)

How a source's level falls off with distance.

Declaration Summary
public val type: DistanceAttenuationModelType Which model this is.
public val minDistance: Float Distance below which attenuation stops increasing, for inverseDistance.
public fun calculate(context: SteamAudioContext, source: Vector3, listener: Vector3): Float Evaluates this model for a given source and listener position.
override fun toString(): String

AirAbsorptionModel

public class AirAbsorptionModel private constructor(public val type: AirAbsorptionModelType, private val coefficients: FloatArray)

How much high-frequency energy the air removes over distance.

Declaration Summary
public val type: AirAbsorptionModelType Which model this is.
public fun calculate(context: SteamAudioContext, source: Vector3, listener: Vector3, destination: FloatArray) Evaluates per-band air absorption gains for a source and listener position.
override fun toString(): String

Directivity

public class Directivity(public val dipoleWeight: Float = 0f, public val dipolePower: Float = 1f)

How much a source radiates in a given direction.

Declaration Summary
public val dipoleWeight: Float Blend between omnidirectional (0) and dipole (1).
public val dipolePower: Float How sharply the dipole lobe narrows. 1 is a plain figure-of-eight.
public fun calculate(context: SteamAudioContext, source: CoordinateSpace3, listener: Vector3): Float Evaluates the directivity gain for a source with a given orientation and a listener position.
override fun toString(): String

PathEffect.kt

Rendering sound that reaches the listener around obstacles along baked probe paths.

PathEffect spatializes a source using the paths a Simulator found, configured per block by PathEffectParams.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/PathEffect.kt — read the source

PathEffectParams

public class PathEffectParams private constructor(internal val segment: MemorySegment, private val shSnapshot: MemorySegment)

Per-block parameters for a PathEffect.

Declaration Summary
public fun setEqCoefficient(band: Int, gain: Float) Per-band EQ modelling the frequency-dependent loss of sound bending around an obstacle.
public fun eqCoefficient(band: Int): Float Reads back a per-band EQ coefficient.
public var order: Int Ambisonic order to render at.
public var binaural: Boolean Render binaurally via HRTF. Only used when the effect was created with spatialisation.
public var normalizeEq: Boolean Divide the EQ coefficients by their largest value before use.
public var listener: CoordinateSpace3 The listener's frame, used to rotate the world-space field when spatialising.
public var hrtf: Hrtf? The HRTF used for binaural rendering. Held as a raw pointer, so it must stay alive.
public fun copySimulatedFrom(outputs: SimulationOutputs) Takes the simulated path parameters from outputs, preserving the fields the application owns rather than the simulator.
public fun copyShCoefficientsFrom(outputs: SimulationOutputs) Copies the simulator's spherical-harmonic coefficients into this object's own storage and points the struct at the copy, so the audio thread no longer reads simulator memory.
public fun shCoefficient(index: Int): Float The simulated spherical-harmonic coefficients, read back for inspection.
public fun hasNoPath(): Boolean True when every coefficient is zero, meaning no path was found.

PathEffect

public class PathEffect private constructor(handle: MemorySegment, public val maxOrder: Int, public val spatializes: Boolean, public val speakerLayout: SpeakerLayout, private val layoutArena: Arena?) : AudioEffect(handle)

Renders sound that reaches the listener by travelling around geometry rather than through it.

Declaration Summary
public val maxOrder: Int Highest Ambisonic order this effect can render.
public val spatializes: Boolean Whether this effect spatialises internally rather than emitting raw Ambisonics.
public val speakerLayout: SpeakerLayout The speaker layout used when spatialising.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
override fun onClosed()
public val ambisonicChannelCount: Int Number of Ambisonic channels at maxOrder.
public val expectedOutputChannels: Int The number of output channels apply expects.
public fun apply(params: PathEffectParams, input: AudioBuffer, output: AudioBuffer): AudioEffectState Renders one block.
override fun reset()
override val tailSize: Int
override fun getTail(output: AudioBuffer): AudioEffectState
public fun retain(): PathEffect Increments the reference count and returns an independently closeable wrapper.

Probes.kt

Probe points placed through a scene, and the batches that hold their baked data.

ProbeGenerationParams describes how probes are laid out, ProbeArray holds the generated points, and ProbeBatch carries them together with bakes keyed by BakedDataIdentifier.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Probes.kt — read the source

BakedDataIdentifier

public data class BakedDataIdentifier(public val type: BakedDataType, public val variation: BakedDataVariation, public val endpointInfluence: Sphere = Sphere(Vector3.ZERO, 0f))

Names a particular piece of baked data within a ProbeBatch.

Declaration Summary
public val type: BakedDataType Which simulation stage the data is for.
public val variation: BakedDataVariation What the data varies over.
public val endpointInfluence: Sphere The region the endpoint influences.

ProbeGenerationParams

public data class ProbeGenerationParams(public val type: ProbeGenerationType = ProbeGenerationType.UNIFORM_FLOOR, public val spacing: Float = 2.0f, public val height: Float = 1.5f, public val transform: Matrix4x4 = Matrix4x4.IDENTITY)

How probes should be distributed within a region.

Declaration Summary
public val type: ProbeGenerationType Placement strategy.
public val spacing: Float Horizontal spacing between probes, in metres. For ProbeGenerationType.UNIFORM_FLOOR.
public val height: Float Height above the floor at which to place probes, in metres.
public val transform: Matrix4x4 Transform mapping the unit box [-0.5, 0.5]^3 onto the region to fill with probes.

ProbeArray

public class ProbeArray private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A freshly generated set of probe positions, before they are committed to a ProbeBatch.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val size: Int Number of probes generated.
public operator fun get(index: Int): Sphere The probe at index, as a sphere of influence.
public fun generate(scene: Scene, params: ProbeGenerationParams) Generates probes covering a region of scene, replacing anything already generated.
public fun toList(): List<Sphere> All probes, as spheres.
public fun retain(): ProbeArray Increments the reference count and returns an independently closeable wrapper.

ProbeBatch

public class ProbeBatch private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A committed collection of probes, together with whatever has been baked at them.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val size: Int Number of probes in this batch.
public fun addProbe(probe: Sphere) Adds a single probe. Takes effect on the next commit.
public fun addProbeArray(probeArray: ProbeArray) Adds every probe from probeArray. Takes effect on the next commit.
public fun removeProbe(index: Int) Removes the probe at index. Takes effect on the next commit.
public fun commit() Applies pending probe additions and removals.
public fun removeData(identifier: BakedDataIdentifier) Discards the baked data identified by identifier.
public fun dataSize(identifier: BakedDataIdentifier): Long Size in bytes of the baked data identified by identifier.
public fun getEnergyField(identifier: BakedDataIdentifier, probeIndex: Int, energyField: EnergyField) Reads the baked energy field at probeIndex into energyField.
public fun getReverb(identifier: BakedDataIdentifier, probeIndex: Int, reverbTimes: FloatArray) Reads the baked per-band RT60 reverb times at probeIndex into reverbTimes.
public fun save(into: SerializedObject) Serialises this batch, including baked data, into into.
public fun retain(): ProbeBatch Increments the reference count and returns an independently closeable wrapper.

QualityPreset.kt

Measured quality presets for Steam Audio reflections and binaural rendering, as QualityPreset.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/QualityPreset.kt — read the source

QualityPreset

public data class QualityPreset(public val reflectionType: ReflectionEffectType, public val ambisonicOrder: Int, public val hrtfInterpolation: HrtfInterpolation, public val reverbDuration: Float, public val numRays: Int, public val numBounces: Int, public val numDiffuseSamples: Int, public val description: String)

A measured set of quality settings, so the trade-offs do not have to be rediscovered.

Declaration Summary
public val reflectionType: ReflectionEffectType Which reflection algorithm to render with.
public val ambisonicOrder: Int Ambisonic order for the reflected and pathed buses.
public val hrtfInterpolation: HrtfInterpolation HRTF interpolation for the direct path.
public val reverbDuration: Float Reverb length to simulate and render, in seconds.
public val numRays: Int Rays cast per reflection simulation.
public val numBounces: Int Bounces per ray.
public val numDiffuseSamples: Int Diffuse samples per bounce.
public val description: String A one-line description, for logging which preset is active.
public val ambisonicChannelCount: Int Ambisonic channel count for ambisonicOrder.
override fun toString(): String

ReflectionEffect.kt

Rendering simulated reflections onto a source's audio.

ReflectionEffect applies an impulse response or parametric reverb per block, driven by a ReflectionEffectParams; a ReflectionMixer sums several sources' reflections so the expensive final stage runs once.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/ReflectionEffect.kt — read the source

ReflectionEffectParams

public class ReflectionEffectParams private constructor(internal val segment: MemorySegment)

Per-block parameters for a ReflectionEffect.

Declaration Summary
public var type: ReflectionEffectType Which reflection algorithm these parameters describe. Must match the effect.
public var channelCount: Int Number of channels in the impulse response. Must match the effect.
public var irSize: Int Impulse response length in samples. Must match the effect.
public var delay: Int Samples of delay before the reflections begin.
public fun setReverbTime(band: Int, seconds: Float) RT60 reverb decay time per band, in seconds -- how long it takes the reverb to fall by 60 dB.
public fun reverbTime(band: Int): Float Reads back a per-band RT60.
public fun setEqualiser(band: Int, gain: Float) Per-band gain applied to the reflected signal.
public fun equaliser(band: Int): Float Reads back a per-band equaliser gain.
public fun setTrueAudioNext(device: TrueAudioNextDevice, slot: Int) The TrueAudio Next device and slot, for ReflectionEffectType.TRUE_AUDIO_NEXT.
public fun applyShapeOf(effect: ReflectionEffect) Copies effect's shape into the three fields the simulator does not fill in.
public fun copySimulatedFrom(outputs: SimulationOutputs) Takes the simulated reflection parameters from outputs, preserving the three shape fields that describe the effect rather than the simulation.

ReflectionEffect

public class ReflectionEffect private constructor(handle: MemorySegment, public val type: ReflectionEffectType, public val channelCount: Int, public val irSize: Int) : AudioEffect(handle)

Renders the reflected sound of a source: the energy that reaches the listener after bouncing off geometry.

Declaration Summary
public val type: ReflectionEffectType The algorithm this effect was created with.
public val channelCount: Int Number of Ambisonic output channels.
public val irSize: Int Impulse response length in samples.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun apply(params: ReflectionEffectParams, input: AudioBuffer, output: AudioBuffer, mixer: ReflectionMixer? = null): AudioEffectState Renders one block of reflections.
override fun reset()
override val tailSize: Int Samples of tail remaining.
override fun getTail(output: AudioBuffer): AudioEffectState
public fun getTail(output: AudioBuffer, mixer: ReflectionMixer? = null): AudioEffectState Renders one block of tail, optionally accumulating into mixer.
public fun retain(): ReflectionEffect Increments the reference count and returns an independently closeable wrapper.

ReflectionMixer

public class ReflectionMixer private constructor(handle: MemorySegment, public val channelCount: Int) : SteamAudioObject(handle)

A shared Ambisonic bus that several ReflectionEffects accumulate into.

Declaration Summary
public val channelCount: Int Number of Ambisonic channels on this bus.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun apply(params: ReflectionEffectParams, output: AudioBuffer): AudioEffectState Reads the accumulated mix into output and clears the bus for the next block.
public fun reset() Clears the bus.
public fun retain(): ReflectionMixer Increments the reference count and returns an independently closeable wrapper.

Scene.kt

Acoustic geometry: the scene that sound is simulated against, and the meshes in it.

A Scene holds StaticMesh geometry built from vertices, Triangles and materials, and InstancedMesh copies of a sub-scene placed by a transform that can move.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Scene.kt — read the source

Triangle

public data class Triangle(public val a: Int, public val b: Int, public val c: Int)

A triangle, as three indices into a mesh's vertex list.

Scene

public class Scene private constructor(handle: MemorySegment, private val callbackArena: Arena?) : SteamAudioObject(handle)

The geometry sound interacts with: the surfaces that reflect, absorb and occlude.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
override fun onClosed()
public fun commit() Rebuilds the ray-tracing acceleration structure so that pending geometry changes take effect.
public fun retain(): Scene Increments the reference count and returns an independently closeable wrapper.
public fun save(into: SerializedObject) Serialises this scene into into. Only meaningful for non-custom scenes.
public fun saveObj(fileBaseName: Path) Writes the scene geometry to a Wavefront OBJ file, for inspection in a 3D viewer.

StaticMesh

public class StaticMesh private constructor(handle: MemorySegment, public val triangleCount: Int, public val materialCount: Int) : SteamAudioObject(handle)

Fixed geometry within a Scene: a vertex list, a triangle list, and a material per triangle.

Declaration Summary
public val triangleCount: Int Number of triangles, so setMaterial can bounds-check its index.
public val materialCount: Int Number of distinct materials in this mesh.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun addTo(scene: Scene) Adds this mesh to scene. Takes effect on the next Scene.commit.
public fun removeFrom(scene: Scene) Removes this mesh from scene. Takes effect on the next Scene.commit.
public fun setMaterial(scene: Scene, index: Int, material: Material) Replaces one of this mesh's materials. Takes effect on the next Scene.commit.
public fun save(into: SerializedObject) Serialises this mesh into into.
public fun retain(): StaticMesh Increments the reference count and returns an independently closeable wrapper.

InstancedMesh

public class InstancedMesh private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A sub-scene placed into a parent scene by a transform, and movable.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun addTo(scene: Scene) Adds this instance to scene. Takes effect on the next Scene.commit.
public fun removeFrom(scene: Scene) Removes this instance from scene. Takes effect on the next Scene.commit.
public fun updateTransform(scene: Scene, transform: Matrix4x4) Moves this instance. Takes effect on the next Scene.commit.
public fun retain(): InstancedMesh Increments the reference count and returns an independently closeable wrapper.

Serialization.kt

Saving and loading Steam Audio scenes, meshes and probe batches as bytes, through SerializedObject.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Serialization.kt — read the source

SerializedObject

public class SerializedObject private constructor(handle: MemorySegment) : SteamAudioObject(handle)

A byte buffer Steam Audio can serialise objects into and out of.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public val size: Long Number of bytes currently held.
public fun data(): MemorySegment A view of the bytes held, valid until this object is closed or written to again.
public fun toByteArray(): ByteArray Copies the bytes held into a new array.
public fun retain(): SerializedObject Increments the reference count and returns an independently closeable wrapper.

Simulation.kt

Steam Audio's acoustic simulator: SimulationSettings fixes its quality and cost, Simulator runs direct, reflection and pathing simulation over a Scene, and each Source carries its own SimulationInputs in and SimulationOutputs out.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Simulation.kt — read the source

SimulationSettings

public data class SimulationSettings(public val flags: SimulationFlags = SimulationFlags.DIRECT or SimulationFlags.REFLECTIONS, public val sceneType: SceneType = SceneType.DEFAULT, public val reflectionType: ReflectionEffectType = ReflectionEffectType.CONVOLUTION, public val maxNumOcclusionSamples: Int = 16, public val maxNumRays: Int = 4096, public val numDiffuseSamples: Int = 1024, public val maxDuration: Float = 2.0f, public val maxOrder: Int = 1, public val maxNumSources: Int = 8, public val numThreads: Int = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1), public val rayBatchSize: Int = 1, public val numVisSamples: Int = 4, public val samplingRate: Int = 48_000, public val frameSize: Int = 1024)

How the simulator should be configured. Fixed for the simulator's lifetime.

Declaration Summary
public val flags: SimulationFlags Which simulation stages this simulator supports.
public val sceneType: SceneType Ray tracer backend. Must match the Scene type.
public val reflectionType: ReflectionEffectType Reflection algorithm. Must match the ReflectionEffect type.
public val maxNumOcclusionSamples: Int Maximum occlusion samples for volumetric occlusion.
public val maxNumRays: Int Maximum rays cast per reflection simulation.
public val numDiffuseSamples: Int Diffuse samples taken per ray bounce.
public val maxDuration: Float Longest impulse response the simulator will produce, in seconds.
public val maxOrder: Int Ambisonic order of the reflected field.
public val maxNumSources: Int How many sources may be simulated at once.
public val numThreads: Int Worker threads for reflection simulation.
public val rayBatchSize: Int Rays per batch, for the Radeon Rays backend.
public val numVisSamples: Int Visibility samples per probe pair, for pathing.
public val samplingRate: Int Sampling rate, matching your AudioSettings.
public val frameSize: Int Block size, matching your AudioSettings.
public val reflectionChannelCount: Int Number of Ambisonic channels the reflected field will have.

SimulationSharedInputs

public class SimulationSharedInputs private constructor(internal val segment: MemorySegment, private val stubArena: Arena)

Inputs shared by every source in a simulation: where the listener is, and how hard to work.

Declaration Summary
public var listener: CoordinateSpace3 The listener's position and orientation in world space.
public fun setListenerPosition(x: Float, y: Float, z: Float) Sets just the listener's position, leaving the orientation alone.
public var numRays: Int Rays cast per reflection simulation. Must not exceed the simulator's maxNumRays.
public var numBounces: Int How many times each ray may bounce.
public var duration: Float Impulse response duration to simulate, in seconds. Capped by the simulator's maxDuration.
public var order: Int Ambisonic order to simulate. Capped by the simulator's maxOrder.
public var irradianceMinDistance: Float Minimum distance used when accumulating irradiance, which stops a ray that grazes a surface from contributing an unbounded amount of energy.
public fun setPathingVisualizationCallback(callback: PathingVisualizationCallback?) Installs a callback that receives every probe-to-probe segment the pathing search examines, or clears it when callback is null.

SimulationInputs

public class SimulationInputs private constructor(internal val segment: MemorySegment)

Per-source simulation inputs. A mutable native view.

Declaration Summary
public var flags: SimulationFlags Which simulation stages to run for this source.
public var directFlags: DirectSimulationFlags Which direct-path quantities to compute.
public var source: CoordinateSpace3 The source's position and orientation in world space.
public fun setSourcePosition(x: Float, y: Float, z: Float) Sets just the source position, leaving orientation alone and allocating nothing.
public var occlusionType: OcclusionType How occlusion is sampled.
public var occlusionRadius: Float Radius of the source sphere, for OcclusionType.VOLUMETRIC.
public var numOcclusionSamples: Int Rays used for volumetric occlusion.
public var numTransmissionRays: Int Rays used to estimate transmission through geometry.
public fun setReverbScale(band: Int, scale: Float) Per-band scaling applied to the simulated reverb time.
public var hybridReverbTransitionTime: Float Where the hybrid reflection type switches from convolution to parametric, in seconds.
public var hybridReverbOverlapPercent: Float How much the convolution and parametric halves overlap, as a fraction.
public var baked: Boolean Whether to use baked data rather than simulating live.
public var enableValidation: Boolean Extra validation of pathing results. Costs performance.
public var findAlternatePaths: Boolean Whether pathing should look for alternative routes when the direct path is blocked.
public var visRadius: Float Probe visibility test radius, for pathing.
public var visThreshold: Float Fraction of visibility rays that must pass for two probes to count as mutually visible.
public var visRange: Float Maximum distance over which probe visibility is tested.
public var pathingOrder: Int Ambisonic order used for pathing.
public fun setDistanceAttenuationModel(model: DistanceAttenuationModel) The distance attenuation model to use for this source.
public fun setAirAbsorptionModel(model: AirAbsorptionModel) The air absorption model to use for this source.
public fun setDirectivity(directivity: Directivity) The source's directivity pattern.
public fun setPathingProbes(probeBatch: ProbeBatch?) The probe batch to use for pathing, or null for none.
public fun setBakedDataIdentifier(identifier: BakedDataIdentifier) The baked data to look up when baked is set.

SimulationOutputs

public class SimulationOutputs private constructor(internal val segment: MemorySegment)

Simulation results for one source.

Declaration Summary
public fun copyDirectInto(params: DirectEffectParams) Copies the simulated direct-path parameters into params.
public val distanceAttenuation: Float Simulated distance attenuation, as a linear gain.
public val occlusion: Float Simulated occlusion: 1 clear, 0 fully blocked.
public fun airAbsorption(band: Int): Float Simulated per-band air absorption gain.
public fun reverbTime(band: Int): Float Simulated per-band RT60 reverb decay time, in seconds.

Simulator

public class Simulator private constructor(handle: MemorySegment, public val settings: SimulationSettings) : SteamAudioObject(handle)

Runs acoustic simulation: works out what happens to sound travelling from each source to the listener through the Scene.

Declaration Summary
public val settings: SimulationSettings The settings this simulator was created with.
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun setScene(scene: Scene) Sets the scene to simulate against. Takes effect on the next commit.
public fun addProbeBatch(probeBatch: ProbeBatch) Adds a probe batch, for baked reflections or pathing. Takes effect on the next commit.
public fun removeProbeBatch(probeBatch: ProbeBatch) Removes a probe batch. Takes effect on the next commit.
public fun setSharedInputs(flags: SimulationFlags, inputs: SimulationSharedInputs) Publishes listener position and ray budget for the next run.
public fun commit() Applies pending scene, source and probe-batch changes.
public fun runDirect() Runs direct-path simulation: occlusion, transmission, distance attenuation, directivity.
public fun runReflections() Runs reflection simulation. Blocking and expensive -- never call this from the audio thread.
public fun runPathing() Runs pathing simulation. Expensive; same threading rules as runReflections.
public fun retain(): Simulator Increments the reference count and returns an independently closeable wrapper.

Source

public class Source private constructor(handle: MemorySegment) : SteamAudioObject(handle)

One sound source within a Simulator.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
public fun addTo(simulator: Simulator) Adds this source to simulator. Takes effect on the next Simulator.commit.
public fun removeFrom(simulator: Simulator) Removes this source from simulator. Takes effect on the next Simulator.commit.
public fun setInputs(flags: SimulationFlags, inputs: SimulationInputs) Publishes this source's inputs for the next simulation run.
public fun getOutputs(flags: SimulationFlags, outputs: SimulationOutputs) Reads the most recent simulation results into outputs.
public fun retain(): Source Increments the reference count and returns an independently closeable wrapper.

SteamAudio.kt

Library-wide constants for the Steam Audio bindings: version numbers, the native band count and an explicit load entry point, on SteamAudio.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/SteamAudio.kt — read the source

SteamAudio

public object SteamAudio

Library-wide constants and version information.

Declaration Summary
public const val VERSION_MAJOR: Int Major version of the Steam Audio library these bindings were generated against.
public const val VERSION_MINOR: Int Minor version.
public const val VERSION_PATCH: Int Patch version.
public const val VERSION: String Version string, for logging.
public val versionCode: Int The version value the native library expects in IPLContextSettings.version, packed as (major shl 16) or (minor shl 8) or patch.
public val NUM_BANDS: Int Number of frequency bands Steam Audio splits the spectrum into.
public fun load() Loads the native library if it has not already been loaded.
public fun ambisonicChannelCount(order: Int): Int Number of Ambisonic channels for a given order: (order + 1)^2.

SteamAudioContext.kt

The SteamAudioContext every Steam Audio object is created from, with the LogCallback and MemoryAllocator hooks it can install.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/SteamAudioContext.kt — read the source

LogCallback

public fun interface LogCallback

Receives diagnostic messages from the native library.

Declaration Summary
public fun log(level: LogLevel, message: String)

MemoryAllocator

public interface MemoryAllocator

Supplies the memory Steam Audio allocates internally.

Declaration Summary
public fun allocate(size: Long, alignment: Long): MemorySegment Allocates size bytes aligned to alignment, returning MemorySegment.NULL on failure.
public fun free(block: MemorySegment) Frees a block previously returned by allocate.

SteamAudioContext

public class SteamAudioContext private constructor(handle: MemorySegment, private val callbackArena: Arena?) : SteamAudioObject(handle)

The root Steam Audio object. Everything else is created from a context, directly or indirectly, and must be closed before it.

Declaration Summary
override fun releaseAction(): (MemorySegment) -> Unit
override val typeName: String
override fun onClosed()
public fun retain(): SteamAudioContext Increments the reference count and returns an independently closeable wrapper.
public fun calculateRelativeDirection(sourcePosition: Vector3, listenerPosition: Vector3, listenerAhead: Vector3 = Vector3.FORWARD, listenerUp: Vector3 = Vector3.UP): Vector3 Computes the direction from listener to source, expressed in the listener's local frame.

SteamAudioException.kt

How Steam Audio failures reach Kotlin: a native status code becomes a SteamAudioError, and anything other than success is thrown as a SteamAudioException carrying it.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/SteamAudioException.kt — read the source

SteamAudioError

public enum class SteamAudioError(internal val value: Int)

Status codes returned by Steam Audio functions that can fail.

Constants: SUCCESS, FAILURE, OUT_OF_MEMORY, INITIALIZATION.

SteamAudioException

public class SteamAudioException internal constructor(public val error: SteamAudioError, message: String) : RuntimeException(message)

Thrown when a Steam Audio call returns a non-success SteamAudioError.

Declaration Summary
public val error: SteamAudioError

SteamAudioNatives.kt

Locates and loads the Steam Audio shared library, through SteamAudioNatives.

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/SteamAudioNatives.kt — read the source

SteamAudioNatives

public object SteamAudioNatives

Locates and loads the Steam Audio shared library.

Declaration Summary
public enum class Platform(public val directory: String, public val libraryFileName: String, public val dependencies: List<String> = emptyList()) Identifies a supported platform and where its libraries live.
public val platform: Platform by lazy The platform this JVM is running on.
public fun load(): Path Loads the Steam Audio library if it has not already been loaded, and returns the path it was loaded from. Idempotent and thread-safe.
public const val HOW_TO_FETCH: String How to obtain the library, for any message that has to say what to do next.
public val unavailableReason: String? Why load would fail if called right now, or null when the library is loadable.
public val isAvailable: Boolean True when load can find the library. Does not load it.
public fun describe(): String One line for a box or a smoke test to print.
public val loadedFrom: Path? The path the library was loaded from, or null if load has not run yet.

AbletonLinkAudio.kt

Ableton Link from inside Field: a tempo, beat and phase shared with every Link peer on the network, and audio published to and taken from them.

LinkAudioInterface is the whole surface and there is one of it per process. shared opens it or hands back the one already open, currentOrNull asks whether there is one without opening one, and destroyShared closes it so that the next shared builds a new one. Boxes usually arrive through abletonLink, linkTiming, linkAudioOut, linkAudioIn and linkAudioInStereo in synth.kt, none of which ask the caller for an interface.

The timing half is a session state every peer on the network shares. setTempo, setPlaying and requestBeat write into it, enableStartStopSync decides whether transport start and stop travel with it, and LinkTimingSnapshot is the read: tempo, beat, phase, peer count and Link's own microsecond clock, all taken at one instant. FieldAudioEngine takes a snapshot per audio block and hangs it on the SynthContext every Signal renders against, which is where linkBeat, linkPhase and linkTempo come from. ParamScope re-exposes the same values, so a Param expression can read them too.

The audio half is Link Audio, which carries sound between peers of that same session. enableAudioOutput publishes a named channel and FieldAudioEngine.enableLinkOutput is what points the mix at it; channels lists the channels the session is carrying, and monoInput and stereoInput subscribe to one of them and hand back a Signal the graph uses like any other source. Sonic Pi's link_audio reads a channel published this way, and Sonic Pi covers that end of it, including the Link visibility setting that has to be on before either side can see the other.

There are two SessionStates here because Link has two. One is captured and committed under a lock for calls arriving from a box or the UI; the other is captured inside the audio block, so that the beat the block starts on and the buffer rendered for it are committed together. linkAudioCommits, linkAudioCommitFailures and linkAudioInvalidBuffers count how that goes, and they are live-graph children along with the peer count and the channel list, so an open interface can be walked by a live-graph query.

None of the Link bindings are Kotlin — com.ableton.link comes from libs/ableton-link-ffm.jar, over a native Link library the build points at with the ableton.link.library system property.

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

LinkTimingSnapshot

data class LinkTimingSnapshot(val enabled: Boolean, val audioEnabled: Boolean, val peers: Long, val tempo: Double, val beat: Double, val phase: Double, val quantum: Double, val clockMicros: Long, val playing: Boolean)

Declaration Summary
val enabled: Boolean
val audioEnabled: Boolean
val peers: Long
val tempo: Double
val beat: Double
val phase: Double
val quantum: Double
val clockMicros: Long
val playing: Boolean
override fun toString(): String

LinkAudioInterface

class LinkAudioInterface private constructor(tempo: Double, peerName: String, val quantum: Double) : AutoCloseable, LiveGraphInspectable

Declaration Summary
val quantum: Double
var linkAudioCommits: Long
var linkAudioCommitFailures: Long
var linkAudioInvalidBuffers: Long
var closed: Boolean
val enabled: Boolean
val audioEnabled: Boolean
val peers: Long
fun setPeerName(name: String): LinkAudioInterface
fun setTempo(tempo: Double): LinkAudioInterface
fun setPlaying(playing: Boolean): LinkAudioInterface
fun enableStartStopSync(enabled: Boolean = true): LinkAudioInterface
fun requestBeat(beat: Double, quantum: Double = this.quantum): LinkAudioInterface
fun snapshot(quantum: Double = this.quantum): LinkTimingSnapshot
fun enableAudioOutput(name: String = "Field4", maxFrames: Int = 4096, channels: Int = 2): LinkAudioInterface
fun disableAudioOutput(): LinkAudioInterface
fun channels(): List<LinkAudioChannel>
fun stereoInput(channelName: String? = null): StereoSignal
fun monoInput(channelName: String? = null): Signal
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String
override fun close()

AbletonSet.kt

An Ableton Live set read into Field: tracks, clips, tempo, automation and sample analysis.

AbletonSet.read parses an .als, which is gzipped XML — recognised by its magic bytes rather than its extension, so an already-unzipped set reads too. The model runs AbletonSet to AbletonTrack to AbletonClip, with AbletonTempoMap converting between arrangement beats and seconds.

The queries take an arrangement position and answer for that instant: AbletonSet.clipsPlayingAtBeat, AbletonSet.trackVolumesAtBeat and AbletonSet.dominantPitchesAtBeat, each with an AtSeconds twin. AbletonAutomationEnvelope carries the envelopes they read.

A clip names its audio through AbletonSampleRef, which AbletonSampleFileMapper and AbletonSet.resolveSamples resolve to files on this machine. Opening those files is what AbletonSampleVolumeAnalysisCache and AbletonSamplePitchAnalysisCache are for: both memoise in process and on disk, keyed on the sample's path, size, modification time and the analysis config, so a second run is a lookup rather than a re-read.

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

AbletonSet

data class AbletonSet(val source: File, val tempoMap: AbletonTempoMap, val tracks: List<AbletonTrack>, val mainTrack: AbletonTrack? = null)

One Ableton Live set, as read from an .als file by read.

Declaration Summary
val source: File
val tempoMap: AbletonTempoMap
val tracks: List<AbletonTrack>
val mainTrack: AbletonTrack?
val clips: List<AbletonClip>
val allTracks: List<AbletonTrack>
val automationEnvelopes: List<AbletonAutomationEnvelope>
fun clipsPlayingAtBeat(arrangementBeat: Double): List<AbletonClipPlayback>
fun clipsPlayingAtBeat(arrangementBeat: Double, sampleMapper: AbletonSampleFileMapper): List<AbletonClipPlayback>
fun clipsPlayingAtSeconds(arrangementSeconds: Double): List<AbletonClipPlayback>
fun clipsPlayingAtSeconds(arrangementSeconds: Double, sampleMapper: AbletonSampleFileMapper): List<AbletonClipPlayback>
fun trackVolumesAtBeat(arrangementBeat: Double, sampleMapper: AbletonSampleFileMapper, analysisCache: AbletonSampleVolumeAnalysisCache = AbletonSampleVolumeAnalysisCache.shared): List<AbletonTrackVolume>
fun trackVolumesAtSeconds(arrangementSeconds: Double, sampleMapper: AbletonSampleFileMapper, analysisCache: AbletonSampleVolumeAnalysisCache = AbletonSampleVolumeAnalysisCache.shared): List<AbletonTrackVolume>
fun dominantPitchesAtBeat(arrangementBeat: Double, sampleMapper: AbletonSampleFileMapper, pitchCache: AbletonSamplePitchAnalysisCache = AbletonSamplePitchAnalysisCache.shared): List<AbletonClipPitch>
fun dominantPitchesAtSeconds(arrangementSeconds: Double, sampleMapper: AbletonSampleFileMapper, pitchCache: AbletonSamplePitchAnalysisCache = AbletonSamplePitchAnalysisCache.shared): List<AbletonClipPitch>
fun dominantPitchesByTrackAtBeat(arrangementBeat: Double, sampleMapper: AbletonSampleFileMapper, pitchCache: AbletonSamplePitchAnalysisCache = AbletonSamplePitchAnalysisCache.shared): List<AbletonTrackPitch>
fun dominantPitchesByTrackAtSeconds(arrangementSeconds: Double, sampleMapper: AbletonSampleFileMapper, pitchCache: AbletonSamplePitchAnalysisCache = AbletonSamplePitchAnalysisCache.shared): List<AbletonTrackPitch>
fun track(name: String): AbletonTrack?
fun resolveSamples(searchDirectory: File): Map<AbletonClip, File>

AbletonTrack

data class AbletonTrack(val index: Int, val id: String, val kind: String, val name: String, val color: Int?, val clips: List<AbletonClip>, val automation: List<AbletonAutomationEnvelope>)

Declaration Summary
val index: Int
val id: String
val kind: String
val name: String
val color: Int?
val clips: List<AbletonClip>
val automation: List<AbletonAutomationEnvelope>
fun clipsPlayingAtBeat(arrangementBeat: Double): List<AbletonClip>
fun automationValuesAt(arrangementTime: Double): Map<AbletonAutomationTarget, AbletonAutomationValue>

AbletonClip

data class AbletonClip(val id: String, val kind: AbletonClipKind, val name: String, val arrangementTimeBeat: Double, val currentStartBeat: Double, val currentEndBeat: Double, val loop: AbletonClipLoop, val disabled: Boolean, val sample: AbletonSampleRef?, val warpMarkers: List<AbletonWarpMarker>, val isWarped: Boolean?)

Declaration Summary
val id: String
val kind: AbletonClipKind
val name: String
val arrangementTimeBeat: Double
val currentStartBeat: Double
val currentEndBeat: Double
val loop: AbletonClipLoop
val disabled: Boolean
val sample: AbletonSampleRef?
val warpMarkers: List<AbletonWarpMarker>
val isWarped: Boolean?
val durationBeats: Double
fun containsBeat(arrangementBeat: Double): Boolean
fun sourceSecondsAtBeat(arrangementBeat: Double, tempoMap: AbletonTempoMap): Double
fun localBeatAt(arrangementBeat: Double): Double

AbletonClipKind

enum class AbletonClipKind

Constants: Audio, Midi.

AbletonClipLoop

data class AbletonClipLoop(val loopStartSeconds: Double, val loopEndSeconds: Double, val startRelativeBeat: Double, val loopOn: Boolean, val outMarkerSeconds: Double?, val hiddenLoopStartSeconds: Double?, val hiddenLoopEndSeconds: Double?)

Declaration Summary
val loopStartSeconds: Double
val loopEndSeconds: Double
val startRelativeBeat: Double
val loopOn: Boolean
val outMarkerSeconds: Double?
val hiddenLoopStartSeconds: Double?
val hiddenLoopEndSeconds: Double?

AbletonSampleRef

data class AbletonSampleRef(val relativePath: String?, val absolutePath: String?, val defaultDurationSamples: Long?, val defaultSampleRate: Double?, val originalFileSize: Long?, val originalCrc: Long?)

Declaration Summary
val relativePath: String?
val absolutePath: String?
val defaultDurationSamples: Long?
val defaultSampleRate: Double?
val originalFileSize: Long?
val originalCrc: Long?
val defaultDurationSeconds: Double?

AbletonSampleFileMapper

class AbletonSampleFileMapper private constructor(val searchDirectory: File, private val filesByName: Map<String, List<File>>, private val filesBySuffix: Map<String, List<File>>)

Declaration Summary
val searchDirectory: File
fun resolve(clip: AbletonClip): File?
fun resolve(playback: AbletonClipPlayback): File?
fun resolve(sample: AbletonSampleRef?): File?
fun candidates(sample: AbletonSampleRef?): List<File>

AbletonWarpMarker

data class AbletonWarpMarker(val id: String, val sourceSeconds: Double, val clipBeat: Double)

Declaration Summary
val id: String
val sourceSeconds: Double
val clipBeat: Double

AbletonClipPlayback

data class AbletonClipPlayback(val track: AbletonTrack, val clip: AbletonClip, val arrangementBeat: Double, val arrangementSeconds: Double, val sourceSeconds: Double, val sourceFile: File? = null)

Declaration Summary
val track: AbletonTrack
val clip: AbletonClip
val arrangementBeat: Double
val arrangementSeconds: Double
val sourceSeconds: Double
val sourceFile: File?
val sourcePath: String?
val originalSourcePath: String?

AbletonTrackVolume

data class AbletonTrackVolume(val track: AbletonTrack, val arrangementBeat: Double, val arrangementSeconds: Double, val bands: AbletonVolumeBands, val clips: List<AbletonClipVolume>)

Declaration Summary
val track: AbletonTrack
val arrangementBeat: Double
val arrangementSeconds: Double
val bands: AbletonVolumeBands
val clips: List<AbletonClipVolume>
val rms: Double

AbletonClipVolume

data class AbletonClipVolume(val track: AbletonTrack, val clip: AbletonClip, val sourceFile: File, val sourceSeconds: Double, val bands: AbletonVolumeBands)

Declaration Summary
val track: AbletonTrack
val clip: AbletonClip
val sourceFile: File
val sourceSeconds: Double
val bands: AbletonVolumeBands
val rms: Double

AbletonVolumeBands

data class AbletonVolumeBands(val low: Double, val mid: Double, val high: Double)

Declaration Summary
val low: Double
val mid: Double
val high: Double
val rms: Double

AbletonSampleVolumeAnalysisConfig

data class AbletonSampleVolumeAnalysisConfig(val windowSeconds: Double = 0.150, val hopSeconds: Double = 0.005, val lowCutoffHz: Double = 250.0, val highCutoffHz: Double = 2_000.0, val fftSize: Int = 2048)

Declaration Summary
val windowSeconds: Double
val hopSeconds: Double
val lowCutoffHz: Double
val highCutoffHz: Double
val fftSize: Int

AbletonSampleVolumeAnalysis

data class AbletonSampleVolumeAnalysis(val sourceFile: File, val sampleRate: Int, val sampleCount: Int, val config: AbletonSampleVolumeAnalysisConfig, val hopSeconds: Double, val frames: List<AbletonVolumeBands>)

Declaration Summary
val sourceFile: File
val sampleRate: Int
val sampleCount: Int
val config: AbletonSampleVolumeAnalysisConfig
val hopSeconds: Double
val frames: List<AbletonVolumeBands>
val durationSeconds: Double
fun volumeAtSeconds(sourceSeconds: Double): AbletonVolumeBands

AbletonSampleVolumeAnalysisCache

class AbletonSampleVolumeAnalysisCache(private val config: AbletonSampleVolumeAnalysisConfig = AbletonSampleVolumeAnalysisConfig())

Declaration Summary
fun analysisFor(sourceFile: File): AbletonSampleVolumeAnalysis
fun clear()

AbletonPitchEstimate

data class AbletonPitchEstimate(val midiPitch: Int, val velocity: Int, val startSeconds: Double, val endSeconds: Double, val channel: Int)

Declaration Summary
val midiPitch: Int
val velocity: Int
val startSeconds: Double
val endSeconds: Double
val channel: Int
val frequencyHz: Double

AbletonClipPitch

data class AbletonClipPitch(val track: AbletonTrack, val clip: AbletonClip, val sourceFile: File, val sourceSeconds: Double, val pitch: AbletonPitchEstimate)

Declaration Summary
val track: AbletonTrack
val clip: AbletonClip
val sourceFile: File
val sourceSeconds: Double
val pitch: AbletonPitchEstimate

AbletonTrackPitch

data class AbletonTrackPitch(val track: AbletonTrack, val arrangementBeat: Double, val arrangementSeconds: Double, val pitch: AbletonPitchEstimate, val clips: List<AbletonClipPitch>)

Declaration Summary
val track: AbletonTrack
val arrangementBeat: Double
val arrangementSeconds: Double
val pitch: AbletonPitchEstimate
val clips: List<AbletonClipPitch>

AbletonSamplePitchAnalysis

data class AbletonSamplePitchAnalysis(val sourceFile: File, val midiFile: File?, val sourceLastModified: Long, val sourceLength: Long, val notes: List<AbletonPitchEstimate>)

Declaration Summary
val sourceFile: File
val midiFile: File?
val sourceLastModified: Long
val sourceLength: Long
val notes: List<AbletonPitchEstimate>
fun dominantPitchAtSeconds(sourceSeconds: Double): AbletonPitchEstimate?

AbletonSamplePitchAnalysisCache

class AbletonSamplePitchAnalysisCache(private val basicPitchExecutable: File? = configuredBasicPitchExecutable(), private val onsetThreshold: Double = configuredBasicPitchOnsetThreshold())

Declaration Summary
fun analysisFor(sourceFile: File): AbletonSamplePitchAnalysis
fun dominantPitchAtSeconds(sourceFile: File, sourceSeconds: Double): AbletonPitchEstimate?
fun clear()

AbletonAutomationEnvelope

data class AbletonAutomationEnvelope(val id: String, val target: AbletonAutomationTarget, val events: List<AbletonAutomationEvent>)

Declaration Summary
val id: String
val target: AbletonAutomationTarget
val events: List<AbletonAutomationEvent>
fun bracketAt(arrangementTime: Double): Pair<AbletonAutomationEvent?, AbletonAutomationEvent?>
fun valueAt(arrangementTime: Double): AbletonAutomationValue

AbletonAutomationTarget

data class AbletonAutomationTarget(val id: String, val name: String, val path: List<String>, val deviceName: String?, val parameterName: String?, val manual: AbletonAutomationValue?, val range: ClosedFloatingPointRange<Double>?)

Declaration Summary
val id: String
val name: String
val path: List<String>
val deviceName: String?
val parameterName: String?
val manual: AbletonAutomationValue?
val range: ClosedFloatingPointRange<Double>?

AbletonAutomationEvent

data class AbletonAutomationEvent(val id: String, val time: Double, val value: AbletonAutomationValue, val curve: AbletonAutomationCurve?)

Declaration Summary
val id: String
val time: Double
val value: AbletonAutomationValue
val curve: AbletonAutomationCurve?

AbletonAutomationValue

sealed class AbletonAutomationValue

Declaration Summary
data class FloatValue(val value: Double) : AbletonAutomationValue()
data class BoolValue(val value: Boolean) : AbletonAutomationValue()
data class EnumValue(val value: Int) : AbletonAutomationValue()
data object Missing : AbletonAutomationValue()
fun asDoubleOrNull(): Double?

AbletonAutomationCurve

data class AbletonAutomationCurve(val control1X: Double, val control1Y: Double, val control2X: Double, val control2Y: Double)

Declaration Summary
val control1X: Double
val control1Y: Double
val control2X: Double
val control2Y: Double
fun xToY(x: Double): Double

AbletonTempoMap

data class AbletonTempoMap(val defaultBpm: Double, val events: List<AbletonTempoEvent> = emptyList())

Declaration Summary
val defaultBpm: Double
val events: List<AbletonTempoEvent>
fun bpmAtBeat(beat: Double): Double
fun beatToSeconds(beat: Double): Double
fun secondsToBeat(seconds: Double): Double
fun beatsToDurationSeconds(beats: Double, fromBeat: Double = 0.0): Double

AbletonTempoEvent

data class AbletonTempoEvent(val beat: Double, val bpm: Double)

Declaration Summary
val beat: Double
val bpm: Double

AbletonSetDebugDrawing.kt

An AbletonSet drawn as a timeline: a row of clip rectangles per track, an automation curve under it per envelope, at a fixed number of pixels per beat.

AbletonSet.toDebugFLines is the call and returns a flat list of FLines; debugFLineMap is the same drawing under one key, for a box that publishes its lines as a map. Both take an AbletonSetFLineDebugOptions or a builder block, and abletonSetFLineDebugOptions builds one on its own.

Geometry is beats times pixelsPerBeat along x and a stack of fixed-height lanes along y: a track takes clipLaneHeight plus one automationLaneHeight per envelope it carries, with trackGap between tracks. The drawing is at least sixteen beats wide however short the set is, and includeMainTrack decides whether the set's main track is drawn beside its ordinary ones. An envelope is sampled at automationSamples points across that width, and within its lane a higher value sits at a smaller y, normalised by the envelope's declared range, falling back to the spread of its own events, then to a unit interval around its manual value, then to 0 to 1. includeGuides rules a line under each track's clip row and another under the track; markAutomationEvents ticks each envelope event at its own time.

Colour comes from Live's track colour index into a palette of 32, with a golden-ratio hue as the fallback for a track that has no index or a negative one, and a disabled clip is drawn at a lower alpha rather than omitted. Every line carries ableton.* properties naming the track, clip, envelope or event it was drawn from, so the picture can be queried as well as looked at.

There is no connection to a running Live here. The input is an AbletonSet parsed off disk, so the drawing works on a machine that has never had Live installed.

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

Top-level functions

Declaration Summary
fun abletonSetFLineDebugOptions(block: AbletonSetFLineDebugOptionsBuilder.() -> Unit): AbletonSetFLineDebugOptions
fun AbletonSet.toDebugFLines(options: AbletonSetFLineDebugOptions = AbletonSetFLineDebugOptions()): List<FLine>
fun AbletonSet.toDebugFLines(block: AbletonSetFLineDebugOptionsBuilder.() -> Unit): List<FLine>
fun AbletonSet.debugFLineMap(options: AbletonSetFLineDebugOptions = AbletonSetFLineDebugOptions()): Map<String, List<FLine>>
fun AbletonSet.debugFLineMap(block: AbletonSetFLineDebugOptionsBuilder.() -> Unit): Map<String, List<FLine>>

AbletonSetFLineDebugOptions

data class AbletonSetFLineDebugOptions(val pixelsPerBeat: Double = 0.5, val clipLaneHeight: Double = 18.0, val automationLaneHeight: Double = 14.0, val trackGap: Double = 8.0, val clipVerticalPadding: Double = 2.0, val automationSamples: Int = 96, val includeMainTrack: Boolean = true, val includeGuides: Boolean = true, val markAutomationEvents: Boolean = true)

Declaration Summary
val pixelsPerBeat: Double
val clipLaneHeight: Double
val automationLaneHeight: Double
val trackGap: Double
val clipVerticalPadding: Double
val automationSamples: Int
val includeMainTrack: Boolean
val includeGuides: Boolean
val markAutomationEvents: Boolean

AbletonSetFLineDebugOptionsBuilder

class AbletonSetFLineDebugOptionsBuilder

Declaration Summary
var pixelsPerBeat: Double
var clipLaneHeight: Double
var automationLaneHeight: Double
var trackGap: Double
var clipVerticalPadding: Double
var automationSamples: Int
var includeMainTrack: Boolean
var includeGuides: Boolean
var markAutomationEvents: Boolean
fun build(): AbletonSetFLineDebugOptions

AbletonSetSmokeTest.kt

Smoke test for reading Ableton Live sets: clips, automation envelopes, gzipped .als files, sample lookup, track volume and pitch analysis, run against generated fixtures.

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

AbletonSetSmokeTest

object AbletonSetSmokeTest

Runs the Ableton set checks; given a path to a set, prints a summary of its tracks instead.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

BandAnalysis.kt

Band energy from audio that is already playing: a bank of band-pass filters, and the RMS of what each one passes.

BandSplit is the bank and logBands builds one with log-spaced centres. Both read an AudioSnapshot, a copy of what the audio path last produced, so a measurement never renders a synth graph a second time. What comes back is one Double per band in the order the centres were given, which is the shape a frame's worth of code can hand to a shader or a particle force.

val launch = patSynth(stack(pat("bd*4"), pat("hh*16")), cps = 2.0)
val split = logBands(4, lowHz = 70.0, highHz = 8000.0, sampleRate = launch.bank.sampleRate)
val perFrame = launch.bank.sampleRate / 60
frames {
    while (frame()) {
        launch.advanceOffline(perFrame)
        val energy = split.measure(launch.tap.snapshot())
        // energy[0] is the lowest band, and hears the kick; energy[3] is the highest, and
        // hears the hats
    }
}

Something has to be pulling the bank, or the tap stays empty and every band reads zero. Above the box pulls it, which measures without sounding and does not depend on there being an output device; returning the launch as the box's last expression hands it to the audio engine instead, which pulls it when there is a device to play it through. Doing both advances the voices twice.

Per band the reading is the RMS of the filtered window's second half, the first half being the filters' settling. The filters keep their state between calls, so successive overlapping snapshots read as a smoothed series rather than as independent measurements.

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

Used in: A pattern that moves particles

Top-level functions

Declaration Summary
fun logBands(count: Int, lowHz: Double = 60.0, highHz: Double = 9000.0, q: Double = 1.6, sampleRate: Int = 48_000): BandSplit BandSplit centres spread logarithmically over lowHz to highHz.

BandSplit

class BandSplit(val centresHz: DoubleArray, val q: Double = 1.6, val sampleRate: Int = 48_000, val capacity: Int = 8192)

How loud each band is, measured by pushing a window of samples through Field's own Biquads.

Declaration Summary
val centresHz: DoubleArray Band centre frequencies in Hz, low to high.
val q: Double Filter Q. Around 1.5 gives bands that overlap a little rather than leaving gaps.
val sampleRate: Int Sample rate the filters are built for, in Hz. It has to be the rate the measured audio was produced at, since it sets both the filter coefficients and the Nyquist bound every centre in centresHz is checked against.
val capacity: Int Longest window measure will accept.
val bands: Int How many bands there are: centresHz's length, and the length of every measure result.
fun measure(snapshot: AudioSnapshot): DoubleArray Per-band RMS of snapshot's left channel, or of its mono sum when it is stereo.
fun measure(samples: FloatArray, length: Int = samples.size): DoubleArray Per-band RMS of the first length samples of samples.
override fun toString(): String One line naming the band count, the centre frequencies and the Q.

BooleanNetworkSoundDocs.kt

The clips on boolean_network_music_examples.md: each figure's note stream played through the pinned Tidal Dirt-Samples.

BooleanNetworkSoundDocs rebuilds the notes each page fence builds and renders them with patRenderStereoDirt. It runs as the synth.boolean.network.sound FIELD_TEST.

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

BooleanNetworkSoundDocs

object BooleanNetworkSoundDocs

The clips on boolean_network_music_examples.md: each figure's note stream, played through the real Tidal Dirt-Samples.

Declaration Summary
class Scene(val name: String, val listenFor: String, val seconds: Double, val notes: () -> List<ScheduledNote>, val token: (ScheduledNote) -> String = { requireNotNull(it.label) { "note $it has no label to play" } }, val speed: (ScheduledNote) -> Double = { 1.0 }, val mustHear: (ScheduledNote) -> Boolean = { true }, val mustHearWhy: String = "every note") One page figure as sound: the notes it draws, how long it lasts, and the token per note.
fun attractorNotes(): List<ScheduledNote> The first fence: the first kauffmanNetwork(5, 2) seed with an attractor of period 6 or more.
fun dirtTokenNotes(): List<ScheduledNote> The second fence: seed 27's longest attractor, its lanes labelled with Dirt tokens.
fun braidNotes(): List<ScheduledNote> The third fence: three attractors on three clocks, braided.
fun drumGridNotes(): List<ScheduledNote> boolean_networks_examples.md's drum-grid fence: the same rhythm candidate, the same step % 4 gating into kick / snare / hat / clap / percussion lanes, played at eight steps a second for two passes of the 16-step grid the figure draws.
fun drivenNotes(): List<ScheduledNote> The fourth fence: a hand-built network with node 0 forced by a sine-derived square wave.
val SCENES: List<Scene>
fun scene(name: String): Scene Looks a scene up by name, with a message that lists the alternatives.
fun notesToDirtPattern(notes: List<ScheduledNote>, token: (ScheduledNote) -> String, speed: (ScheduledNote) -> Double = { 1.0 }): PatPattern<String> notes as a one-cycle-per-second pattern of Dirt tokens, ready for patRenderStereoDirt at cps = 1.
class Rendered(val scene: Scene, val notes: List<ScheduledNote>, val result: DirtRender.DirtRenderResult) One scene rendered: the audio, and the Dirt bookkeeping that says what played.
fun render(name: String): Rendered Renders name through the pinned Dirt-Samples, strictly.
fun renderWav(name: String, outPath: String) Offline-renders scene name to a stereo 16-bit WAV at outPath.
fun audibleAt(mono: FloatArray, seconds: Double, sampleRate: Int = PatSoundDocs.SR, ratio: Double = 1.5): Boolean Whether a hit starts at seconds in mono: the first-difference energy of the 10 ms after it is more than ratio times that of the 10 ms before it.
fun main(args: Array<String>)

DirtRender.kt

Offline rendering of a pattern through the real Dirt sample library.

The other renderer in this package, patRenderStereo, goes through patVoice: a sine for bd, filtered white noise for sd and hh. That stand-in needs no assets, so it works in CI and on a clean checkout. It also sounds like bursts of white noise, because it is bursts of white noise. It is not the voice behind most of the published audio figures either: DocsAudioProvenance resolves each published clip to the producer that rendered it, DocsAudioProvenanceGuardTest pins the resulting corpus by count and by family, and public/docs/sound.md states the mix in prose. The largest family comes from DoughDspSoundDocs, which never reaches the stand-in voice at all.

The genuine Tidal sample set was already playable, but only live: playDirt drives a polyphonic DirtSamplePlayer through respondAhead, a clock and an audio device, none of which exist inside a docs build or a headless box run. Here the same library, loaded by libraryOrNull from wherever dirtSampleRoot finds it, is mixed straight into a buffer, so a transcript, a docs capture or a println-and-listen box can sound like actual drums with no audio device anywhere.

Fallback happens at two different granularities:

  • No library on this machine — the whole render falls back to patRenderStereo. Nothing breaks, the figures still generate, and DirtRenderResult.usedSamples is false, so a caller can say which one you are hearing.
  • Library present, token missing from it — only that event falls back to the synth voice. A pattern of note numbers, or one naming a drum Dirt has never heard of, must not go silent just because the library loaded.

Both are for a box, where something audible beats an error. A published clip wants the opposite: strict = true turns either case into an exception, and the docs renderers pass DirtRender.requirePinnedLibrary, the checksummed subset ./gradlew fetchDocsDirtSamples downloads, so a clip is made from the same bytes on every machine or not made at all.

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

Used in: How to make sound in Field

Top-level functions

Declaration Summary
fun patRenderStereoDirt(pattern: PatPattern<String>, cycles: Int, cps: Double = 1.0, library: DirtSampleLibrary? = DirtRender.libraryOrNull(), voice: PatVoiceSpec = patVoice(), sampleRate: Int = PatSoundDocs.SR, strict: Boolean = false): DirtRender.DirtRenderResult Schedules pattern over cycles cycles at cps and mixes it from real Dirt samples.
fun Box.showDirtAudio(pattern: PatPattern<String>, cycles: Int = 4, cps: Double = 2.0, library: DirtSampleLibrary? = DirtRender.libraryOrNull(), voice: PatVoiceSpec = patVoice(), channel: String = "main"): DirtRender.DirtRenderResult Renders pattern through the real Dirt samples and puts an audio player in this box's output.

DirtRender

object DirtRender

Where the Dirt sample library is, whether it loaded, and what a finished render was made of.

Declaration Summary
const val ENV_VAR Where the sample set lives, if it lives anywhere. Probed once; see dirtSampleRoot.
class PinnedFile(val path: String, val bytes: Long, val sha256: String) One pinned file: its path inside the library, its size and its SHA-256.
class PinnedManifest(val commit: String, val folders: List<String>, val files: List<PinnedFile>) The pinned subset: which commit of tidalcycles/Dirt-Samples, which folders, which files.
val pinnedManifest: PinnedManifest by lazy The manifest off the classpath.
fun pinnedCacheDir(): Path Where tools/fetch_dirt_samples.py puts the pinned subset, whether or not it is there.
fun pinnedProblems(root: Path = pinnedCacheDir()): List<String> What is wrong with the pinned subset at root: missing files and wrong digests. Empty when it is whole.
fun pinnedSampleRoot(): Path? The pinned, checksummed Dirt-Samples subset, or null when it is not on this machine intact.
const val PINNED_REMEDY How to get the pinned subset; quoted by every failure that needs it.
fun requirePinnedLibrary(): DirtSampleLibrary The library a published clip is rendered from: the pinned subset, and nothing else.
fun dirtSampleRoot(): Path? The Dirt sample root on this machine, or null.
fun libraryOrNull(preload: Boolean = false): DirtSampleLibrary? The shared library for dirtSampleRoot, or null when there is nothing to load.
class DirtRenderResult(val audio: PatSoundDocs.Stereo, val usedSamples: Boolean, val matched: Set<String>, val unmatched: Set<String>, val renderedEvents: Int) A rendered buffer, plus what it was actually made of.

DirtSamples.kt

The TidalCycles Dirt-Samples collection loaded, indexed, and played from Field.

dirt attaches a DirtSampleLibrary to the current box context and dirtPlayer builds a polyphonic DirtSamplePlayer from it; PatPattern.playDirt drives one from a pattern of family:variant tokens. The library scans a directory into DirtSampleFamily groups of DirtSampleDescriptor, each resolving to a StereoAudioSample.

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

Used in: Boolean network music examples, A Petri net that throws sparks

Top-level functions

Declaration Summary
fun dirt(path: String, preload: Boolean = true): DirtSampleLibrary Loads or reuses a shared Dirt sample library and stores it on the current root box.
fun dirt(): DirtSampleLibrary Returns the Dirt sample library installed in the current box context.
fun dirtPlayer(voices: Int = 32, library: DirtSampleLibrary = dirt(), gain: Param = Param(1f)): DirtSamplePlayer Creates a polyphonic sample player for a Dirt sample library.
fun PatPattern<String>.playDirt(player: DirtSamplePlayer, cps: Number = 1.0, ahead: Number = 0.02, dispatchOnMain: Boolean = true, clock: PatClock? = Box.context.get()?.patClock ?: musicClock(), speed: (PatScheduledEvent<String>) -> Float = { 1f }, gain: (PatScheduledEvent<String>) -> Float = { 1f }, pan: (PatScheduledEvent<String>) -> Float = { 0f }): PatResponseSpec<String>

Top-level values

Declaration Summary
var Box.dirtSamples: DirtSampleLibrary The DirtSampleLibrary attached to this box, available to child boxes.

StereoAudioSample

class StereoAudioSample(val left: FloatArray, val right: FloatArray = left, val sampleRate: Float = 44_100f)

Declaration Summary
val left: FloatArray
val right: FloatArray
val sampleRate: Float
val lengthSamples: Int
val lengthSeconds: Float
val isMono: Boolean
fun readLeft(position: Double): Float
fun readRight(position: Double): Float
override fun toString(): String

DirtSampleToken

data class DirtSampleToken(val family: String, val variant: Int = 0)

Declaration Summary
val family: String
val variant: Int
override fun toString(): String

DirtSampleDescriptor

class DirtSampleDescriptor internal constructor(val family: String, val variant: Int, val path: Path, val relativePath: String, private val onFirstLoad: () -> Unit) : LiveGraphInspectable

Declaration Summary
val family: String
val variant: Int
val path: Path
val relativePath: String
fun peekLoaded(): StereoAudioSample?
fun getOrLoad(): StereoAudioSample
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String

DirtSampleFamily

class DirtSampleFamily internal constructor(val name: String, val entries: List<DirtSampleDescriptor>)

Declaration Summary
val name: String
val entries: List<DirtSampleDescriptor>
val size: Int
fun descriptor(index: Int = 0): DirtSampleDescriptor?
fun sample(index: Int = 0): StereoAudioSample?
override fun toString(): String

DirtSampleLibrary

class DirtSampleLibrary private constructor(val root: Path, preload: Boolean) : LiveGraphInspectable, AutoCloseable

Declaration Summary
val root: Path
val families: Map<String, DirtSampleFamily>
val totalSamples: Int
val ready: Boolean
val loadedSamples: Int
fun family(name: String): DirtSampleFamily?
fun descriptor(token: String): DirtSampleDescriptor?
fun sample(token: String): StereoAudioSample?
fun preloadAllAsync(): CompletableFuture<Int>
override fun close()
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String

DirtSamplePlayer

class DirtSamplePlayer(val library: DirtSampleLibrary, val maxVoices: Int = 32, val masterGain: Param = Param(1f)) : StereoNode(), LiveGraphInspectable

Declaration Summary
val library: DirtSampleLibrary
val maxVoices: Int
val masterGain: Param
fun play(token: String, speed: Number = 1.0, gain: Number = 1.0, pan: Number = 0.0, start: Number = 0.0, end: Number = 1.0): Boolean
fun trigger(token: String, speed: Number = 1.0, gain: Number = 1.0, pan: Number = 0.0, start: Number = 0.0, end: Number = 1.0): Boolean
fun trigger(sample: StereoAudioSample, speed: Number = 1.0, gain: Number = 1.0, pan: Number = 0.0, start: Number = 0.0, end: Number = 1.0): Boolean
fun stopAll()
override fun generateStereo(outL: FloatArray, outR: FloatArray, numFrames: Int, ctx: SynthContext)
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String

DocsAudioProvenance.kt

An audit of every playable audio file the documentation publishes, by the family of code that made it.

DocsAudioProvenance re-renders the committed .wavs the fence sweep can reach through their producer and correlates the two, so a figure the code no longer makes is found; UNDERIVABLE lists the ones it cannot reach. DocsAudioProvenanceGuardTest asserts the result.

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

DocsAudioProvenance

object DocsAudioProvenance

Every playable audio artifact the documentation publishes, and what actually made it.

Declaration Summary
val DOCS_DIR: File Where the pages live, relative to the repository root.
enum class Origin(val description: String) How a published clip was made — by family of producer, not by guess.
val ORIGIN_BY_RENDERER: Map<String, Origin> The renderer object named by a page's audio-renderer= attribute, and what it produces.
const val DEFAULT_RENDERER The renderer a fence gets when it does not name one, matching docs_hooks.py.
val UNDERIVABLE: Map<String, String> Audio the site ships that the fence sweep cannot reach, and therefore cannot re-derive.
class PublishedAudio(val page: String, val assetRef: String, val spec: String, val renderer: String, val captureProfile: String? = null) One audio="…" fence found on a page.
fun scan(docsDir: File = DOCS_DIR): List<PublishedAudio> Every audio="…" fence on every page under DOCS_DIR, including reference/.
fun originOf(published: PublishedAudio): Origin? The family a fence's audio should belong to, or null when nothing declares one.
class Finding(val published: PublishedAudio, val origin: Origin?, val reproducibility: Double, val standInSimilarity: Double, val frames: Int, val channels: Int, val sampleRate: Int, val peak: Double, val rms: Double, val failure: String? = null, val skipped: String? = null) What the audit measured for one published asset.
fun profilePlatform(profile: String): String? The os.name prefix a docs capture profile runs on, mirroring CAPTURE_PROFILE_PLATFORMS in public/docs_hooks.py (linux-* is Linux, macos-* is macOS); null for a profile with no platform of its own.
fun audit(published: PublishedAudio, scratch: File): Finding Re-renders published through its declared producer and measures it against the committed asset.
fun sampleRateOf(file: File): Int The committed file's sample rate, out of its own header. The corpus is not all one rate.
fun channelsOf(file: File): Int Declared channel count of a WAV, before it is folded for measurement.
fun readWav(file: File): PatSoundDocs.Stereo Reads a 16-bit PCM WAV into the renderer's own shape.
fun main(args: Array<String>) Audits the whole corpus and prints one line per asset. Writes no committed assets.
const val REPRODUCIBILITY_FLOOR A committed asset must still be what its producer renders.

DoughDspPlotGallery.kt

Response plots of the dough DSP primitives (PlateReverb, Phaser and others), drawn from samples the primitives actually produce.

DoughDspPlotGallery runs each primitive on the CPU and draws the result to PNG with AWT, with no browser or GPU. It runs as the synth.dough-dsp.gallery FIELD_TEST; given an output directory it writes the captures under dev_docs/assets/dough_dsp_gallery/.

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

DoughDspPlotGallery

object DoughDspPlotGallery

Headless response-gallery / plot-capture harness for the dough DSP ports in DoughDsp. It is the visual companion to DoughDspSmokeTest (which asserts the same behaviour numerically): every plotted point comes from actually running a primitive on the CPU — an impulse into PlateReverb, a swept sine into Phaser / ChamberlinLowpass, a ramp into SoftDistort, a gated CurvedADSR, and raw ZawOsc / PulzeOsc periods — never a synthesized or hand-drawn curve.

Declaration Summary
enum class Style
class Series(val label: String, val xs: DoubleArray, val ys: DoubleArray, val color: Color, val style: Style = Style.LINE)
class PlotSpec(val title: String, val xLabel: String, val yLabel: String, val series: List<Series>, val yRange: Pair<Double, Double>? = null, val logX: Boolean = false)
class Plot(val name: String, val build: () -> PlotSpec, val verify: (PlotSpec) -> Unit = {}) A named plot the gallery renders, plus an optional deeper correctness check on its data.
val PLOTS: List<Plot>
fun render(spec: PlotSpec): BufferedImage
fun main(args: Array<String>)

DoughDspSmokeTest.kt

Smoke test for the dough DSP ports: approximations, filter, phaser, reverb and envelope, checked by behaviour rather than by exact samples.

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

DoughDspSmokeTest

object DoughDspSmokeTest

Numeric checks on the dough DSP ports in DoughDsp.kt.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

FaustPolySmokeTest.kt

Smoke test for patterns played through a Faust voice bank: checks that onsets land on the notated sample and that a chord really sounds three voices at once.

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

FaustPolySmokeTest

object FaustPolySmokeTest

A pattern playing a Faust program, measured in samples and in Hz.

Declaration Summary
fun main(args: Array<String>)
fun runTests(outDir: File): Boolean

FaustPolySoundDocs.kt

The two published clips that make polyphony audible: one chord progression, eight voices and one.

faust.md describes a polyphonic instrument in prose, onset tables in samples and peak counts in Hz; these two clips are the part of it a reader can listen to. The path renders offline with no device, so DocsAudioProvenance re-renders both on every build and holds them against the numbers this file asserts.

Why the two clips differ only in voices

Polyphony cannot be demonstrated by a single clip. A recording of a chord and a recording of an arpeggio sound different for reasons that have nothing to do with voices, so a reader has no way to tell a voice bank from a well-chosen pattern. What cannot be explained away is the same notation, the same program and the same gain, rendered at voices = 8 and at voices = 1: the notes are identical, so everything you can hear between them is voice allocation. At one voice every note after the first steals the only voice there is, and that steal is audible: FaustVoiceBank.stealReleaseFrames measures the click it costs.

This is the same demonstration the faust_polyphony starting point makes in its three notes at once box, which is where the shape of it came from. That box displays its two renders as players in its own output; these two fences are how the same comparison reaches the page.

Why the voice is a single sawtooth

The program here is not the detuned pair the rest of faust.md uses. The starting point's chord box records why: two sawtooths 0.5% apart put a beating pair into the band the count is taken in, so four voices reported four peaks and one voice reported two — which muddies the only thing the figure is for. A clip whose stated measurement is a peak count has to be a signal with one partial per note.

Why a peak in this band can only be a note

The smoke test counts spectral peaks between BAND_LO_HZ and BAND_HI_HZ during the first chord, [c4,ef4,g4]. Its fundamentals are 130.81, 155.56 and 196.00 Hz and the lowest second harmonic in the chord is 261.6 Hz, so nothing but a fundamental can appear in that band: counting peaks there counts notes sounding. Eight voices must show three and one voice must show one, and the one-voice render must report steals. Those are the assertions, and faust.md is required to carry both captions verbatim, so its prose cannot drift off the audio it publishes.

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

FaustPolySoundDocs

object FaustPolySoundDocs

Renders the two faust.md polyphony clips, counts the fundamentals in each and writes the WAVs; main is the synth.faust.poly.sound entrypoint.

Declaration Summary
const val SR Sample rate both clips are rendered and written at, in Hz.
const val CPS Cycles per second, matching the example on PAGE exactly — see progression.
const val CYCLES How many cycles. Four chords fit in one, so this plays the progression twice, in 4s.
const val NOTE_GAIN Per-note gain. Three notes at once at full scale would clip, and that would not be the mixer.
const val BAND_LO_HZ Bottom of the fundamental-only band the peak count is taken in.
const val BAND_HI_HZ Top of it: below the first chord's lowest second harmonic, 261.6 Hz.
const val MEASURE_AT_SECONDS Where the peak count is taken, in seconds, and over how many samples.
const val MEASURE_WINDOW How many samples the peak count is taken over: 0.17s at SR, which resolves 5.9 Hz.
val SOURCE: String The instrument. One sawtooth per voice through a resonant lowpass, freq/gate/gain as Faust's own polyphonic convention names them.
fun progression(): PatPattern<String> The progression, character for character the one the faustPoly example on PAGE plays.
fun cutoffPattern(): PatPattern<Double> The cutoff pattern the example automates, likewise verbatim.
class Figure(val name: String, val voices: Int, val pageLabel: String, val expectedFundamentals: Int, val expectSteals: Boolean) One published clip: the same notation at a different voice count.
const val PAGE The page these are published on. Its prose is checked against FIGURES, not trusted.
val FIGURES: List<Figure> The two figures, in the order the page presents them.
fun figure(name: String): Figure Looks a figure up by name, with a message that lists the alternatives.
fun render(name: String): FaustPatternRender Renders figure name offline — no audio device, no window, no GPU.
fun renderWav(name: String, outPath: String) Offline-renders figure name to a mono 16-bit WAV at outPath.
fun fundamentalsInFirstChord(audio: FaustPatternRender): List<FaustPeak> Peaks in the fundamental-only band during the first chord of audio.
fun main(args: Array<String>) Renders both figures, audits them, and writes them.

FaustSmokeTest.kt

The Faust binding, end to end, without audio hardware.

What this is really guarding is the contract with a vendored third-party compiler: nothing here is Field's own code being clever, it is a 3MB WebAssembly build of libfaust behaving the way FaustEngine's KDoc claims it does. Every assertion below corresponds to something that was discovered the hard way and would break silently on a version bump:

  • the Emscripten environment shim still boots (a libfaust-wasm.js that detects Node would call require('fs') and die),
  • createDSPFactory still returns its wasm in data as an embind vector rather than code,
  • a bad program still reports name:line : ERROR : ..., which is the whole diagnostics story,
  • generateAuxFiles(-svg) still throws after writing the diagram,
  • and the UI JSON still carries index, which is the only reason a slider can move a parameter.

The timeout is generous: the first compile in a process pays 13–22s of Truffle warm-up.

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

FaustSmokeTest

object FaustSmokeTest

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

FaustSoundDocs.kt

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

FaustSoundDocs

object FaustSoundDocs

The single-program clips on faust.md: the sawtooth through a low-pass with its cutoff moved halfway through, and the detuned pair of sawtooths dry and through zita reverb.

Declaration Summary
const val SR Sample rate of every clip; zita's own fs argument in REVERB says 48000 too.
const val BLOCK Frames per block, the audio engine's.
val LOWPASS: String The page's first program, without the #faust marker a box needs.
val REVERB: String The page's effect program.
const val LOWPASS_HALF_SECONDS Seconds the low-pass clip plays at each cutoff.
const val LOWER_CUTOFF_HZ The cutoff the second half of the low-pass clip moves to, in Hz.
const val DRY_SECONDS Seconds the dry pair plays before it stops, in both reverb clips.
const val REVERB_SECONDS Length of each reverb clip: the dry pair, then its tail.
val FIGURES The clips, by the name their fences ask for.
fun render(name: String): Pair<FloatArray, FloatArray> Renders clip name as left and right channels.
fun renderWav(name: String, outPath: String) Offline-renders clip name to a stereo 16-bit WAV at outPath: the docs fence's entry point.
fun main(args: Array<String>)

Midi.kt

A Standard MIDI File surface for Field.

readMidiClip reads a .mid into a MidiClip and MidiClip.writeTo writes one back as a format-1 file; Sequence.toMidiClip converts a javax.sound.midi.Sequence already in hand. AbletonSet.kt reads MIDI through this file rather than carrying its own helpers, so the note-on to note-off pairing in Sequence.readTimedNotes, along with Sequence.tempoEvents and tickToSeconds, lives here.

Time is carried in beats (quarter notes) in the Field-native model, because that is what the pattern stack speaks. Ticks and PPQ are a file-format detail and appear only at the edges, in MidiTempoEvent, MidiTimedNote and the reader and writer.

MidiClip.quantized and MidiClip.transposed return edited copies; quantize and gridError snap and score a MidiNote list against a beat grid, and GeneralMidi holds the program numbers and the percussion channel as named constants.

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

Top-level functions

Declaration Summary
fun Sequence.tempoEvents(): List<MidiTempoEvent> Every tempo change in this sequence, in tick order, with the MIDI default (120 BPM) prepended so the list is never empty.
fun Sequence.trackName(index: Int): String? The track name (meta 0x03) of track index, or null.
fun tickToSeconds(tick: Long, tempos: List<MidiTempoEvent>, resolution: Int): Double Wall-clock seconds at tick, integrating tempos at resolution ticks per quarter.
fun ticksToSeconds(ticks: Long, microsPerQuarter: Int, resolution: Int): Double Seconds spanned by ticks at a fixed tempo. Lifted unchanged from AbletonSet.kt.
fun Sequence.readTimedNotes(): List<MidiTimedNote> Pair up the note-ons and note-offs in this sequence and return the resulting notes, in the order their note-offs were seen.
fun Sequence.readControlChanges(): List<MidiControlChange> Every control-change event in this sequence, in tick order.
fun readMidiClip(file: File): MidiClip Read file as a Standard MIDI File and return it as a MidiClip, preserving the file's PPQ, tempo map, track split and track names.
fun Sequence.toMidiClip(name: String? = null): MidiClip Convert a javax.sound.midi.Sequence into a MidiClip.
fun Sequence.readTimeSignature(): Pair<Int, Int>? The time signature from meta 0x58, or null if the file states none.
fun quantize(notes: List<MidiNote>, gridBeats: Double, strength: Double = 1.0, swing: Double = 0.0): List<MidiNote> Move notes onto a grid of gridBeats.
fun gridError(notes: List<MidiNote>, gridBeats: Double): Double How far notes sit from a grid of gridBeats, as a mean absolute error in beats. 0.0 means perfectly on the grid. Useful for asserting that quantize did something.

Top-level values

Declaration Summary
const val MIDI_DEFAULT_PPQ: Int Default pulses-per-quarter-note for files Field writes. 480 divides 2, 3, 4, 5, 6 and 8 evenly.
const val MIDI_DEFAULT_MICROS_PER_QUARTER: Int The MIDI default tempo, used whenever a file states none: 500000 us/quarter == 120 BPM.

MidiTempoEvent

data class MidiTempoEvent(val tick: Long, val microsPerQuarter: Int)

A tempo change at tick, expressed the way a Standard MIDI File expresses it.

Declaration Summary
val tick: Long
val microsPerQuarter: Int
val bpm: Double Beats per minute for this tempo. 500000 us/quarter -> 120.0.

MidiTimedNote

data class MidiTimedNote(val pitch: Int, val velocity: Int, val channel: Int, val startTick: Long, val endTick: Long, val startSeconds: Double, val endSeconds: Double, val trackIndex: Int)

A note-on/note-off pair recovered from a Sequence, with both its tick span and its wall-clock span already resolved through the file's tempo map.

Declaration Summary
val pitch: Int
val velocity: Int
val channel: Int
val startTick: Long
val endTick: Long
val startSeconds: Double
val endSeconds: Double
val trackIndex: Int Index of the Sequence track this note was read from, 0-based.
val durationSeconds: Double
val durationTicks: Long

MidiControlChange

data class MidiControlChange(val controller: Int, val value: Int, val beats: Double, val channel: Int = 0)

A control-change (CC) event: controller set to value at beats.

Declaration Summary
val controller: Int
val value: Int
val beats: Double
val channel: Int

MidiNote

data class MidiNote(val pitch: Int, val startBeats: Double, val durationBeats: Double, val velocity: Int = 100, val channel: Int = 0)

One note in musical time. startBeats and durationBeats are quarter notes from the start of the clip; pitch is a MIDI note number (60 == middle C) and velocity is 1..127.

Declaration Summary
val pitch: Int
val startBeats: Double
val durationBeats: Double
val velocity: Int
val channel: Int
val endBeats: Double
val frequencyHz: Double Concert pitch of this note, via the same conversion the rest of the synth package uses.
fun shifted(beats: Double): MidiNote This note moved by beats.
fun transposed(semitones: Int): MidiNote This note transposed by semitones, clamped into the legal MIDI range.

MidiTrackData

data class MidiTrackData(val notes: List<MidiNote> = emptyList(), val name: String? = null, val controlChanges: List<MidiControlChange> = emptyList(), val program: Int? = null)

One named track of a MidiClip. A track maps to one MTrk chunk on disk.

Declaration Summary
val notes: List<MidiNote>
val name: String?
val controlChanges: List<MidiControlChange>
val program: Int? General MIDI program number (0-based, so 0 == Acoustic Grand Piano). Written as a PROGRAM_CHANGE at tick 0 when non-null. See GeneralMidi.
val durationBeats: Double

MidiClip

data class MidiClip(val tracks: List<MidiTrackData> = emptyList(), val tempoMap: List<Pair<Double, Double>> = emptyList(), val ppq: Int = MIDI_DEFAULT_PPQ, val name: String? = null, val timeSignature: Pair<Int, Int> = 4 to 4)

A Field-native MIDI clip: some tracks of notes, a tempo map, a time signature and a PPQ.

Declaration Summary
val tracks: List<MidiTrackData>
val tempoMap: List<Pair<Double, Double>> Tempo changes, in beats. The pair is (beats, BPM). An empty map means "120 BPM throughout", and is written as an explicit 120 BPM event so that importers do not have to guess.
val ppq: Int
val name: String?
val timeSignature: Pair<Int, Int>
val notes: List<MidiNote>
val durationBeats: Double
fun bpmAt(beats: Double): Double BPM in force at beats.
fun beatsToSeconds(beats: Double): Double Seconds from the clip start to beats, integrating the tempo map.
val durationSeconds: Double
fun quantized(gridBeats: Double, strength: Double = 1.0, swing: Double = 0.0): MidiClip This clip with every track's notes moved onto a grid of gridBeats. See quantize.
fun transposed(semitones: Int): MidiClip This clip transposed by semitones.
fun toSequence(): Sequence Build a javax.sound.midi.Sequence for this clip.
fun writeTo(file: File): File Write this clip to file as a format-1 Standard MIDI File. Returns file.

GeneralMidi

object GeneralMidi

General MIDI program numbers, 0-based as the wire format has them.

Declaration Summary
const val ACOUSTIC_GRAND_PIANO
const val ELECTRIC_PIANO
const val CELESTA
const val MARIMBA
const val DRAWBAR_ORGAN
const val ACOUSTIC_GUITAR_NYLON
const val ACOUSTIC_BASS
const val VIOLIN
const val STRING_ENSEMBLE
const val TRUMPET
const val SOPRANO_SAX
const val FLUTE
const val SQUARE_LEAD
const val WARM_PAD
const val PERCUSSION_CHANNEL The GM channel reserved for percussion, 0-based (so channel 10 in 1-based DAW displays).
fun name(program: Int): String? The GM name for program program, or null if out of range.

MidiDevices.kt

Live MIDI device I/O, and offline rendering of a MidiClip to a WAV through the JDK's own software synthesizer.

MidiDevices enumerates what the JVM can see and opens a MidiOutput or MidiInput on it; MidiClip.playTo sends a clip to a device by name fragment. MidiClip.renderWav and renderToWav take the offline route instead, which needs no hardware at all.

Everything here has to survive a machine with no MIDI hardware, so every entry point either returns null or an empty list, or throws a message that names what was missing — midiRenderUnavailableReason is that check on its own, for asking before committing to a render. Nothing fakes a device.

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

Used in: SMPTE Timecode: LTC and MIDI Time Code

Top-level functions

Declaration Summary
fun midiRenderUnavailableReason(): String? Whether this JVM can render MIDI to audio offline, and if not, why not.
fun renderToWav(clip: MidiClip, file: File, tailSeconds: Double = 1.0, program: Int? = null): MidiRenderResult Render clip to a WAV at file using the JDK's Gervill software synthesizer, offline and faster than real time. Returns the measured result, or throws with a stated reason if rendering is unavailable — check midiRenderUnavailableReason first if you want to skip instead.
fun MidiClip.renderWav(file: File, tailSeconds: Double = 1.0, program: Int? = null): MidiRenderResult Render this clip to a WAV. See renderToWav.
fun MidiClip.playTo(nameFragment: String) Play this clip to a named MIDI output, opening and closing it around the play.
fun MidiClip.asSequence(): Sequence This clip as a javax.sound.midi.Sequence, for callers that want to drive a Sequencer.

MidiDeviceHandle

data class MidiDeviceHandle(val name: String, val vendor: String, val description: String, val version: String, val canReceive: Boolean, val canTransmit: Boolean, val implementation: String, internal val info: MidiDevice.Info)

One MIDI device the JVM can see.

Declaration Summary
val name: String
val vendor: String
val description: String
val version: String
val canReceive: Boolean True if Field can send messages to this device (it exposes receivers).
val canTransmit: Boolean True if Field can read messages from this device (it exposes transmitters).
val implementation: String The JDK implementation class, e.g. SoftSynthesizer, MidiOutDevice, RealTimeSequencer.
val isSoftware: Boolean True for the JDK's bundled software synthesizer and sequencer, rather than real hardware.
override fun toString(): String

MidiDevices

object MidiDevices

Enumeration and opening of live MIDI devices.

Declaration Summary
fun list(): List<MidiDeviceHandle> Every device MidiSystem reports, in its own order.
fun outputs(): List<MidiDeviceHandle> Devices Field can send to.
fun inputs(): List<MidiDeviceHandle> Devices Field can listen to.
fun hardware(): List<MidiDeviceHandle> Devices that are real ports rather than the JDK's built-in synthesizer and sequencer.
fun describe(): String A one-line-per-device report, for a box output or a test's skip reason.
fun findOutput(nameFragment: String): MidiDeviceHandle? The first output whose name contains nameFragment, case-insensitively.
fun findInput(nameFragment: String): MidiDeviceHandle? The first input whose name contains nameFragment, case-insensitively.
fun openOutput(handle: MidiDeviceHandle): MidiOutput Open handle for output. The caller owns the result and must close it.
fun openInput(handle: MidiDeviceHandle): MidiInput Open handle for input. The caller owns the result and must close it — but see MidiInput.

MidiOutput

class MidiOutput internal constructor(val handle: MidiDeviceHandle, private val device: MidiDevice, private val receiver: Receiver) : AutoCloseable

An open MIDI output. Send notes with noteOn/noteOff, or a whole clip with play.

Declaration Summary
val handle: MidiDeviceHandle
fun noteOn(pitch: Int, velocity: Int = 100, channel: Int = 0)
fun noteOff(pitch: Int, channel: Int = 0)
fun controlChange(controller: Int, value: Int, channel: Int = 0)
fun programChange(program: Int, channel: Int = 0)
fun send(message: javax.sound.midi.MidiMessage) Send any Java Sound message as-is (SysEx, MIDI Time Code, ...).
fun allNotesOff() Send an all-notes-off on every channel. Call this before closing, or a note hangs.
fun play(clip: MidiClip) Play clip to this output in real time, blocking until it finishes.
override fun close()

MidiInput

class MidiInput internal constructor(val handle: MidiDeviceHandle, private val device: MidiDevice) : AutoCloseable

An open MIDI input, usable as a Field control source. Poll lastValue for a controller, or install a callback with onMessage.

Declaration Summary
val handle: MidiDeviceHandle
val controllers: MutableMap<Pair<Int, Int>, Int> Latest value seen per (channel, controller), 0..127.
val lastNoteOn: MutableMap<Int, Pair<Int, Int>> Latest note-on seen per channel, as (pitch, velocity).
var messageCount: Long Count of messages seen since opening. Useful for asserting that a device really spoke.
fun knob(controller: Int, channel: Int = 0, default: Double = 0.0): Double The current value of controller on channel, normalised to 0..1 — the shape a knob on a controller driving a box property wants. Returns default until the controller has moved once, because MIDI controllers are stateless and report nothing until touched.
fun raw(controller: Int, channel: Int = 0): Int? Raw 0..127 value of controller on channel, or null if untouched.
fun onMessage(block: (MidiMessage, Long) -> Unit) Add a message callback. Called on the Java Sound MIDI thread, so keep it short.
override fun close()

MidiRenderResult

data class MidiRenderResult(val file: File, val seconds: Double, val sampleRate: Int, val peakAmplitude: Double, val rmsAmplitude: Double, val soundbankName: String?)

Result of renderToWav.

Declaration Summary
val file: File
val seconds: Double
val sampleRate: Int
val peakAmplitude: Double Peak absolute sample amplitude, 0.0..1.0. A silent render is 0.0.
val rmsAmplitude: Double Root-mean-square amplitude, 0.0..1.0.
val soundbankName: String?
val isSilent: Boolean

MidiPat.kt

The bridge between the pattern algebra and MidiClip, plus a piano roll drawn as FLines.

Two vocabularies have to be reconciled, and the pattern stack's spelling wins both times.

Cycles against beats. A PatPattern is a function of cycles, measured in PatQ: a rational over BigInteger, so a subdivision is exact however fine it gets. A MIDI file is quarter notes and ticks. beatsPerCycle bridges the two, defaulting to 4 so that one cycle is one bar of 4/4, which is what the mini-notation reads like. It is an Int because the import direction divides by ppq * beatsPerCycle: an integer denominator is what lets MidiClip.toPat rebuild every onset as an exact PatQ rather than a truncated one. Export does not keep that guarantee and cannot — MidiNote holds its position as Double quarter notes.

Velocity and channel are not in the Pat vocabulary. PatAlgebra.kt names the parameters the rest of the stack reads — gain for amplitude, conventionally 0..1, and note for MIDI pitch — and there is no velocity parameter and no channel. Rather than add two more that only MIDI would ever set, export reads velocity from gain when it is present, and from an explicit velocity annotation (0..127) when a caller wants wire-level control. Import writes both, so a round trip through the algebra keeps its dynamics either way; MidiRoundTripTest.a pattern exports to MIDI and comes back as a pattern is where that is pinned.

A clip read back in becomes a finite pattern. Querying past the last note yields nothing rather than looping, so reach for slow, fast or cat to place a clip inside a longer structure.

The piano roll is FLine geometry rather than an SVG string: one filled rectangle per note over a beat-and-bar grid, with a horizontal rule at every C to give the eye an octave reference the way a real roll's keyboard gutter does. Each note line carries its pitch, velocity, start beat and track index in properties, so a caller can hit-test a click back to the note it landed on. Field's y axis grows downward, so a higher pitch draws at a smaller y; reversing that yields an upside-down roll that still looks plausible.

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

Top-level functions

Declaration Summary
fun <A> PatPattern<A>.toMidiClip(cycles: Int = 1, bpm: Double = 120.0, beatsPerCycle: Int = MIDI_DEFAULT_BEATS_PER_CYCLE, velocity: Int = 100, channel: Int = 0, name: String? = null, ppq: Int = MIDI_DEFAULT_PPQ, program: Int? = null): MidiClip Render this pattern over cycles cycles into a MidiClip.
fun <A> PatPattern<A>.writeMidi(file: File, cycles: Int = 1, bpm: Double = 120.0, beatsPerCycle: Int = MIDI_DEFAULT_BEATS_PER_CYCLE, name: String? = null): File Write this pattern to file as a Standard MIDI File. Returns file.
fun MidiClip.toPat(beatsPerCycle: Int = MIDI_DEFAULT_BEATS_PER_CYCLE): PatPattern<String> This clip as a pattern, so a recorded or imported performance can be composed with the algebra.
fun readMidiPat(file: File, beatsPerCycle: Int = MIDI_DEFAULT_BEATS_PER_CYCLE): PatPattern<String> Read file as a Standard MIDI File and return it as a pattern.
fun PatTranscription.toMidiClip(bpm: Double = 120.0, name: String? = "transcription", ppq: Int = MIDI_DEFAULT_PPQ): MidiClip The notes a transcription found, as a MidiClip — the bridge PatTranscription did not have.
fun PatTranscription.writeMidi(file: File, bpm: Double = 120.0): File Write this transcription straight to a Standard MIDI File another DAW can open. Returns file.
fun MidiClip.toFLines(options: MidiRollOptions = MidiRollOptions()): List<FLine> This clip as a list of FLines: one filled rectangle per note, plus a beat/bar grid and a horizontal rule at each C.
fun Box.showMidiRoll(clip: MidiClip, channel: String = "lines", options: MidiRollOptions = MidiRollOptions(), w: Double = 900.0, h: Double = 320.0, fitToSize: Boolean = true) Draw this clip's piano roll into channel of this box.

Top-level values

Declaration Summary
const val MIDI_DEFAULT_BEATS_PER_CYCLE: Int Beats per cycle used by MidiClip.toPat and PatPattern.toMidiClip when none is given.

MidiRollOptions

data class MidiRollOptions(val pixelsPerBeat: Double = 44.0, val semitoneHeight: Double = 7.0, val originX: Double = 0.0, val originY: Double = 0.0, val showGrid: Boolean = true, val beatsPerBar: Int = MIDI_DEFAULT_BEATS_PER_CYCLE, val colorByVelocity: Boolean = true, val minimumNoteWidth: Double = 2.0)

Layout for MidiClip.toFLines. Sizes are in Field units, which for a box output read as pixels.

Declaration Summary
val pixelsPerBeat: Double
val semitoneHeight: Double
val originX: Double
val originY: Double
val showGrid: Boolean Draw a vertical line per beat, and a brighter one per bar.
val beatsPerBar: Int
val colorByVelocity: Boolean Colour notes by velocity rather than by track.
val minimumNoteWidth: Double

MidiSmokeTest.kt

Smoke cover for the Standard MIDI File surface: writes a clip, reads it back, exports a pattern, imports one, draws a roll, enumerates devices and opens a real output port.

It does not open a MIDI input, and cannot. Opening a CoreMIDI input spawns a non-daemon Java Sound MidiInDevice Thread that survives both Transmitter.close() and MidiDevice.close(), so a JVM that has opened one never exits naturally, and a jvm-main smoke test is required to exit naturally. The live input loopback is covered by MidiRoundTripTest instead, where Gradle tears the test JVM down regardless. See MidiInput.

Everything is written to a temp directory that is deleted on the way out, so a run leaves no files in the repository.

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

MidiSmokeTest

object MidiSmokeTest

Runs the Standard MIDI File smoke checks; main is the synth.midi-file.smoke entrypoint.

Declaration Summary
fun main(args: Array<String>) The synth.midi-file.smoke entrypoint. An optional first argument is a directory to write into; without one the run works in a temp directory and deletes it. Throws when a check fails, so the runner sees a non-zero exit.
fun runTests(outputDirectory: File? = null): Boolean Runs every check in turn and reports whether they all passed, printing Midi smoke tests passed on success and the first failure's stack trace otherwise.

PatSpatialHeadTrackedDocs.kt

Renders the head-tracked binaural clip on the spatial patterns page offline through Steam Audio, and checks that it repeats exactly and that leaning moves the level.

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

PatSpatialHeadTrackedDocs

object PatSpatialHeadTrackedDocs

The binaural clip on pat_spatial.md: HeadTrackedPatternDemo's four movements, through Steam Audio's HRTF, at MOVEMENT_SECONDS a movement.

Declaration Summary
const val MOVEMENT_SECONDS Seconds per movement on the page; the Gradle demo uses eight.
const val SR The demo's own rate.
const val LEAN_SWING_FLOOR Movement 4's loudest half-second over its quietest must reach this.
const val STILL_SWING_CEILING Movement 1's loudest half-second over its quietest must stay under this.
fun renderWav(name: String, outPath: String) Renders name (only head-tracked) to a stereo 16-bit WAV: the docs fence's entry point.
fun main(args: Array<String>)

PatSpatialPlacementDocs.kt

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

PatSpatialPlacementDocs

object PatSpatialPlacementDocs

The two placed, binaural clips on pat_spatial.md: the page's own patSpatial and showPatSpatial examples, through Steam Audio's HRTF, with a still listener.

Declaration Summary
const val SR patSpatial's default rate.
class Figure(val name: String, val seconds: Double, val cps: Double, val maxSources: Int, val pattern: () -> PatPattern<String>) One published clip: a pattern from the page, and how long to play it.
val FIGURES: List<Figure> The figures, in page order.
fun figure(name: String): Figure Looks a figure up by name, with a message that lists the alternatives.
fun render(name: String): FloatArray Renders figure name to interleaved stereo at SR.
fun renderWav(name: String, outPath: String) Renders name to a stereo 16-bit WAV: the docs fence's entry point.
fun main(args: Array<String>)

PatSpatialSoundDocs.kt

Two of the published clips on pat_spatial.md: the mono source, before Steam Audio places it.

This is the half of the chain that needs nothing uncommitted: the pattern through one DoughEngine, taken as mono. It is the exact buffer PatSpatialRoom copies into source.mono and hands to afield.audio.spatial.steamaudio.BinauralEffect. The placed, binaural output is PatSpatialHeadTrackedDocs, which needs Steam Audio's libphonon (./gradlew fetchSteamAudioNatives).

What the two clips are for

A single clip of a synth voice makes no claim. These two are the same notation, the same engine and the same gain, differing only in a tag that is not a spatial tag: the second adds lpf, which patSpatialEventsForArc forwards to Dough untouched. So everything audible between them is the forwarding rule, which is the reason to reach for this path over patSynth, whose voice has no lpf to forward to.

The spatial parameters are not in the mono source

Both published clips are peak-normalised, exactly as every other figure in these docs is, so neither of them can carry a claim about level. Three things about this path are measured on the pre-normalisation buffers instead:

  1. pan/0 is a lossless mono tap, not a downmix. Every event leaves patSpatialEventsForArc carrying pan/0, which puts all of an engine's energy in its left channel. The smoke test requires the right channel to peak below RIGHT_CHANNEL_CEILING, which is what lets PatSpatialRoom arraycopy the left channel rather than compute 0.5*(L+R), and why the source signal has full amplitude rather than half of it.
  2. The six spatial tags never reach Dough. Asserted on the event maps, not on the audio: spat, az, elev, dist, occl and air are consumed by this path and lpf is not.
  3. az is not in the signal. The same pattern rendered with az = 0 and with az = -70 produces a bit-identical mono source. The direction lives entirely in the effect, which is the part that needs the uncommitted binary.

The third says something about DoughEngine as well, and it is what makes these two clips publishable at all: two engines, separately booted and separately fed, agree to the bit. That is what DocsAudioProvenance requires of any producer, and a wasm runtime does not give it to you for free — so treat it as a property of this engine rather than of the platform.

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

PatSpatialSoundDocs

object PatSpatialSoundDocs

Renders the two pat_spatial.md source clips, asserts the three claims about them and writes the WAVs; main is the synth.pat-spatial.sound entrypoint.

Declaration Summary
const val SR Matching patSpatial's own default, and FieldAudioEngine.shared's rate on this machine.
const val CPS Cycles per second. Two, the rate sound_paths.md's comparison beat uses, so they line up.
const val CYCLES How many cycles. Four at CPS is two seconds of pattern.
const val GAIN Per-event gain, on top of each event's own velocity.
const val TAIL_SECONDS Seconds rendered past the last event, so the final note's release is in the clip.
const val PAGE The page these are published on. Its prose is checked against FIGURES, not trusted.
val WASM The wasm module, tracked in this repository — which is the whole reason this half renders.
val SPATIAL_TAGS The tags patSpatial consumes rather than forwards.
class Figure(val name: String, val pageLabel: String, val forwarded: Map<String, String>) One published clip.
val FIGURES: List<Figure> The two figures, in the order the page presents them.
fun figure(name: String): Figure Looks a figure up by name, with a message that lists the alternatives.
fun beat(azimuthDeg: Double = -70.0, extra: Map<String, String> = emptyMap()): PatPattern<String> The notation the figures play: a low line, and a faster one two octaves up.
const val NOTATION The mini-notation both figures play. See beat for the measurement that chose it.
const val SUSTAIN Multiplier on each event's own span, so a note is longer than its slot.
const val PROBE The drum spelling of the same rhythm, kept as a measurement rather than as a figure.
class Source(val mono: FloatArray, val right: FloatArray, val events: List<PatSpatialEvent>) What one offline render measured, alongside the samples it produced.
fun renderSource(pattern: PatPattern<String>): Source Renders one spat group's mono source offline — no device, no thread, no Steam Audio.
fun render(name: String): Source Renders figure name's mono source.
fun renderWav(name: String, outPath: String) Offline-renders figure name to a mono 16-bit WAV at outPath.
fun main(args: Array<String>) Renders both figures, asserts the three claims, and writes them.
const val RIGHT_CHANNEL_CEILING How much of the signal may appear in the right channel before pan/0 is not a tap.
const val FORWARDING_CENTROID_CEILING How far below the plain figure's centroid the lpf figure's has to sit to be the filter.

PetriSoundDocs.kt

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

PetriSoundDocs

object PetriSoundDocs

The clips on petri_nets.md: a page net's firings played through the pinned Tidal Dirt-Samples.

Declaration Summary
class Scene(val name: String, val listenFor: String, val source: String, val cycles: Int, val cps: Double) One page net as sound: its source, and how many cycles to play at what tempo.
val SCENES
fun scene(name: String): Scene Looks a scene up by name, with a message that lists the alternatives.
fun render(name: String): DirtRender.DirtRenderResult Renders name through the pinned Dirt-Samples, strictly.
fun renderWav(name: String, outPath: String) Offline-renders scene name to a stereo 16-bit WAV at outPath: the renderWav(name, path) contract the docs runner's audio="…" fence drives.
fun main(args: Array<String>)

PetriSparksDocs.kt

The sound on chain_petri_sparks.md: the page's Petri net, played from the pinned Dirt-Samples.

PetriSparksDocs holds the net source the page's boxes parse and renders its emitted pattern with patRenderStereoDirt. It runs as the synth.petri.sparks.sound FIELD_TEST.

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

PetriSparksDocs

object PetriSparksDocs

The sound on chain_petri_sparks.md: the page's Petri net, played from the pinned Dirt-Samples.

Declaration Summary
const val SR: Int Sample rate of every scene, shared with the other docs galleries.
val NET: String The page's net. Three rings on one grid and a place that counts kicks.
fun net(): PetriNet NET, parsed.
class Scene(val name: String, val listenFor: String, val cycles: Int, val cps: Double) One clip: how many cycles of the net, at what tempo.
val SCENES: List<Scene>
fun render(net: PetriNet, cycles: Int, cps: Double): PatSoundDocs.Stereo Plays cycles cycles of net's emitted sounds from the pinned Dirt-Samples, strictly.
fun renderWav(name: String, outPath: String) Offline-render scene name and write a stereo 16-bit PCM WAV to outPath.
fun main(args: Array<String>)

PrettyPrint.kt

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

SonicPi.kt

Sonic Pi v5 driven from a Field box, in owned mode.

sonicPi and Box.sonicPi are the box-facing entry points, both booting the one shared SonicPi on first use. That class carries the control API — running and stopping code, the mixer, recording, and the log and error feeds that come back into the box. The types around it model what the daemon sends: SonicPiLogMessage, SonicPiError, SonicPiFlash, SonicPiPorts and SonicPiVersion.

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

Used in: Sonic Pi

Top-level functions

Declaration Summary
fun sonicPi(): SonicPi The Sonic Pi this Field owns, booting it on first use.
fun sonicPi(code: String, workspace: String? = null): SonicPiLaunch Boot Sonic Pi if necessary and evaluate code in it.
fun Box.sonicPi(code: String, workspace: String? = null, lineOffset: Int = -1): SonicPiLaunch Run Sonic Pi code, get its errors back in this box, and tie the music to the box's lifecycle.

SonicPi

class SonicPi private constructor(private val install: SonicPiInstall, private val bootTimeoutMs: Long) : AutoCloseable, LiveGraphInspectable

Sonic Pi v5 driven from Field, in owned mode: Field spawns the Sonic Pi daemon itself and becomes the client the daemon expects, so there is no Sonic Pi window anywhere — a Field box is the buffer, and Sonic Pi's log and error reports come back into the box.

Declaration Summary
var verbose: Boolean Print Sonic Pi's log to stdout (and so into the box's output) as it arrives.
var reportErrors: Boolean Print errors as they arrive. On by default — a silent syntax error is the worst outcome.
var onLog: ((SonicPiLogMessage) -> Unit)?
var onError: ((SonicPiError) -> Unit)?
var onMessage: ((String, List<Any?>) -> Unit)? Raw feed of everything the spider server sends, for messages this class does not model.
var onFlash: ((SonicPiFlash) -> Unit)? Called for every line Sonic Pi reports as having triggered sound.
var closed: Boolean
val ports: SonicPiPorts
val isReady: Boolean True once the spider server has announced /spider/ready.
val isRunning: Boolean
fun awaitReady(timeoutMs: Long = 60_000L): Boolean Block until Sonic Pi is ready to accept code. Returns false on timeout.
fun run(code: String, workspace: String? = null, silent: Boolean = false): SonicPi Evaluate Sonic Pi code — the whole point of this class.
fun stopAll(): SonicPi
fun stop(jobId: Int): SonicPi
fun ping(id: String = "field4", timeoutMs: Long = 5_000L): Boolean Round-trip a /ping and wait for its /ack. The cheapest proof the token is accepted.
fun version(timeoutMs: Long = 5_000L): SonicPiVersion? Ask Sonic Pi its version, waiting for the reply.
fun volume(value: Double, silent: Boolean = false): SonicPi The fader after the limiter, 0.0–1.0. v5 rescaled this: v4's 0–5 range is gone.
fun drive(value: Double, silent: Boolean = false): SonicPi The gain before the limiter, 0.0–1.0 (0.5 is unity). New in v5.
fun startRecording(): SonicPi
fun stopRecording(): SonicPi
fun saveRecording(path: String): SonicPi
fun deleteRecording(): SonicPi
fun cue(address: String, args: List<Any?> = emptyList(), host: String = "127.0.0.1", port: Int = ports.oscCues): SonicPi Send an OSC cue to Sonic Pi's cue port.
fun cue(address: String, vararg args: Any?): SonicPi
fun reportTo(workspace: String, box: Box, lineOffset: Int = 0): SonicPi Route Sonic Pi's output for workspace into box.
fun stopReportingTo(workspace: String): SonicPi
fun activity(workspace: String): Map<Int, Long> Which lines have triggered sound, and how often, for workspace.
val flashes: List<SonicPiFlash>
fun lineAges(workspace: String, now: Long = System.nanoTime()): Map<Int, Long> How long ago, in milliseconds, each line of workspace last triggered sound.
fun activeLines(workspace: String, withinMs: Long = 500L): List<Int> The lines of workspace that triggered sound within the last withinMs.
fun jobIdFor(workspace: String): Int? The most recent Sonic Pi job id for workspace, if one has started.
fun isJobAlive(jobId: Int): Boolean
fun awaitJobStart(workspace: String, timeoutMs: Long = 15_000L): Int? Wait for workspace's next job to start, returning its id.
val log: List<SonicPiLogMessage>
val errors: List<SonicPiError>
fun messageCounts(): Map<String, Long> Addresses seen from the spider server, with how many of each — handy when something is quiet.
fun clearLog(): SonicPi
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
override fun toString(): String
override fun close()

SonicPiFlash

data class SonicPiFlash(val jobId: Int, val workspace: String, val line: Int)

One line reported as having triggered sound. See SonicPi.activity.

SonicPiPorts

data class SonicPiPorts(val daemon: Int, val guiListenToSpider: Int, val guiSendToSpider: Int, val scsynth: Int, val oscCues: Int, val token: Int)

The ports and token the Sonic Pi daemon allocates at boot and prints, once, to stdout.

Declaration Summary
val daemon: Int
val guiListenToSpider: Int
val guiSendToSpider: Int
val scsynth: Int
val oscCues: Int
val token: Int
override fun toString(): String

SonicPiInstall

data class SonicPiInstall(val home: File, val ruby: File, val daemon: File, val environment: Map<String, String> = emptyMap())

Where Sonic Pi is installed, and the files/environment that owned mode needs.

Declaration Summary
val home: File
val ruby: File
val daemon: File
val environment: Map<String, String> AppImages need the same library and executable search paths that their AppRun sets.

SonicPiLogLine

data class SonicPiLogLine(val style: Int, val text: String)

One styled line of Sonic Pi's log. Style 0 is Sonic Pi's own chatter, 1 is your puts.

SonicPiLogMessage

data class SonicPiLogMessage(val jobId: Int, val thread: String, val runtime: Double, val lines: List<SonicPiLogLine>)

Declaration Summary
val jobId: Int
val thread: String
val runtime: Double
val lines: List<SonicPiLogLine>
val text: String
override fun toString(): String

SonicPiError

data class SonicPiError(val jobId: Int, val message: String, val backtrace: String, val line: Int, val firstColumn: Int, val lastColumn: Int, val sourceLine: String, val syntax: Boolean)

Declaration Summary
val jobId: Int
val message: String
val backtrace: String
val line: Int
val firstColumn: Int
val lastColumn: Int
val sourceLine: String
val syntax: Boolean
override fun toString(): String

SonicPiVersion

data class SonicPiVersion(val version: String, val versionNumber: Int, val latestVersion: String?, val platform: String?)

Declaration Summary
val version: String
val versionNumber: Int
val latestVersion: String?
val platform: String?

SonicPiActivity.kt

Sonic Pi's per-line activity, shown in the editor.

Sonic Pi tells its client which line just triggered a note — /flash, from play, synth, sample, control, play_chord and the with_fx/live_loop block boundaries, tagged with the caller's line. That answers "what is making sound, and where in the code", so it belongs on the code rather than in a log.

Three things make this less trivial than painting the lines that flashed.

The rate is musical, not human. Two live_loops at a modest tempo produce something like sixteen flashes a second and each iteration reports the loop head, the play and the end. Repainting per flash would push a decoration set over the websocket faster than anyone can see, so sonicPiActivityDecorations is a pure function of recency that a caller samples at a sane rate — the launch loop's 50ms poll — rather than something the flash handler drives.

Lines have to cool off. A flash is an instant, not a duration, so a line that fired 40ms ago and one that fired 400ms ago are both "recent" and should not look identical; without a decay the display latches on and every line that has ever sounded stays lit.

And the line numbers need translating. Sonic Pi counts from line 1 of the source it was handed, which is the inside of a Kotlin triple-quoted string — see inferSonicPiLineOffset for why that offset is inferred rather than assumed to be zero.

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

SonicPiEditorExtension.kt

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

Both are static resources rather than strings built here: sonicpi.js is served to the browser as the AMD module bithero/sonicpi (see bootstrapMonaco.js) and sonicpi-vocabulary.json is fetched by it at install time. Reading them verbatim is what lets the node-probe smoke test exercise the code and data that ship, rather than a copy that can drift.

The vocabulary is generated by tools/generate_sonic_pi_vocabulary.py from the installed Sonic Pi; see that script for why it is extracted rather than written.

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

Top-level functions

Declaration Summary
fun sonicPiEditorExtensionJavascript(): String

SonicPiEditorExtension

object SonicPiEditorExtension

Reads the two shipped #sonicpi resources: the Monaco module, and the vocabulary it completes against.

Declaration Summary
const val id: String
fun javascript(): String
fun vocabularyJson(): String

SonicPiLaunch.kt

The handle Field holds while Sonic Pi is playing, and the registration that ties it to a box's lifecycle.

SonicPi.kt sends the code and returns a SonicPiLaunch naming the Sonic Pi job that resulted; this file is what makes returning one from a box mean anything. registerSonicPiLaunchReturnValue adds a handler to the return-value registry which runs a coroutine registered as one of the box's jobs, reports the box as running while Sonic Pi says the job is alive and this launch is still the box's current one, and stops the job on the way out. That stop runs NonCancellable, because the usual reason for reaching it is that the box was stopped and the stop still has to get to Sonic Pi.

awaitJobId settles which job a launch refers to: it waits for an id different from the one that was current when the code was sent, so a re-running box cannot mistake the job it just replaced for its own.

The handler polls the job every 50 ms, which is also the rate the sounding lines are sampled at to decorate the box while showActivity is set.

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

Top-level functions

Declaration Summary
fun registerSonicPiLaunchReturnValue() Teach Field that a SonicPiLaunch is something a box can be running.

SonicPiLaunch

class SonicPiLaunch internal constructor(val sonicPi: SonicPi, val workspace: String, private val priorJobId: Int?, val lineOffset: Int = 0, var showActivity: Boolean = true)

A Sonic Pi run that Field owns the way it owns any other launch.

Declaration Summary
val sonicPi: SonicPi The Sonic Pi this run was sent to, and the route back for everything asked of it afterwards.
val workspace: String The Sonic Pi buffer name this run is attributed to, and the name Sonic Pi uses when it says where an error happened.
val lineOffset: Int Box line on which the Sonic Pi source starts; see inferSonicPiLineOffset.
var showActivity: Boolean Highlight the lines that are sounding while this plays. See sonicPiActivityDecorations.
suspend fun awaitJobId(timeoutMs: Long = SONIC_PI_JOB_START_TIMEOUT_MILLIS): Int? The Sonic Pi job id for this launch, once /run/started has named it.
val jobId: Int? This run's Sonic Pi job id, or null until /run/started names one.
val isPlaying: Boolean Whether Sonic Pi still has this run's job alive.
fun activity(): Map<Int, Long> Lines that have triggered sound in this run. See SonicPi.activity.
fun stop() Stop this run's Sonic Pi job, leaving any other box's music alone.
override fun toString(): String One line naming the workspace, the job id, whether it is playing and the lines that have sounded so far. Read live, so printing one twice during a run gives two different answers.

SonicPiPatterns.kt

Field's pattern algebra as a sequencer for Sonic Pi's engine.

pat("bd [hh hh] sd") and the algebra around it (TidalPatterns.kt, PatAlgebra.kt, PatComposition.kt) keep musical structure exact in rational cycle space and can transform it live. SonicPi.kt owns a real Sonic Pi 5: 66 synths, 42 effects and 206 samples in the installed vocabulary, and a scheduler better than Field's own. This file is how a pattern reaches that engine without going through Sonic Pi's own language in a string literal, which the pattern algebra cannot see into.

There are two routes, good at different things, and both are here.

compile — turn a pattern into a Sonic Pi program.

sonicPiProgram(
    "drums" to pat("bd [hh hh] sd hh"),
    "bass" to pat("<e1 g1> ~ e2 ~").every(4) { it.rev() } on "tb303",
    cps = 0.5,
    cycles = 4
).source

gives you a live_loop per layer, with sleeps that are the pattern's own rational cycle positions. The result is ordinary Sonic Pi source: paste it into a #sonicpi region and edit it by hand, or hand it to Box.sonicPiPattern and hear it. Field's clock is not involved once it is running — Sonic Pi's scheduler is, which is the accurate one.

The tempo mapping that makes this readable is SonicPiProgram.bpm: use_bpm 60 * cps makes one Sonic Pi beat exactly one Field cycle, so every sleep in the emitted program is literally the cycle fraction the pattern computed. A third stays a third — Rational(1, 3) is emitted where a decimal would round, and a decimal where it would not.

drive — Field's clock sequences, Sonic Pi sounds.

pat("bd*4, [~ cp]*2").sonicPiLive(cps = 0.5)

One /run-code per event, dispatched by the same PatResponseSpec machinery that drives every other live pattern in Field, so .every, .degradeBy and a signal on gain(...) stay live while you edit. Costs a network hop of jitter per note; buys you a pattern you can change mid-bar.

Both return something a box can be running: compile returns a SonicPiLaunch, drive returns a PatResponseSpec. Either way the box stays lit while the music plays and stopping the box stops it.

Atoms become instruments through SonicPiInstruments, which resolves in this order: an explicit override, then a real sample name, then a real synth name, then a note (c4, e2, 60), then the drum-machine aliases (bd, sd, hh, cp, …). "Real" means in the installed vocabulary — sonicpi-vocabulary.json, extracted from the app by tools/generate_sonic_pi_vocabulary.py. An atom that resolves to nothing is reported in SonicPiProgram.unresolved rather than dropped, and a mistyped alias is a test failure rather than silence (synth.sonic-pi.patterns).

Parameters come from PatAlgebra's annotations, so with(gain(sine().range(0.2, 0.8))) works unchanged. Three of them are not pass-throughs and the difference matters:

  • gain → amp:, unchanged.
  • pan → pan:, rescaled: Field's pan is 0..1, Sonic Pi's is -1..1.
  • cutoff → cutoff: (synths) or lpf: (samples), converted: Field's cutoff is Hz, Sonic Pi's is a MIDI note number.

Any other annotation is passed through only if the vocabulary says the target accepts it — cutoff on :beep is not a Sonic Pi opt, so it is dropped and recorded in SonicPiProgram.droppedOpts instead of being sent for Sonic Pi to ignore.

Notes are emitted as MIDI numbers, not as :c4. Field's patNoteNumber defaults a bare c to octave 5 and Sonic Pi's :c is octave 4; emitting the number the pattern actually meant removes a whole class of off-by-an-octave that neither side would report.

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

Used in: Sonic Pi

Top-level functions

Declaration Summary
fun sonicPiSample(name: String): SonicPiVoice.Sample sonicPiSample("bd_tek") — a checked sample name, for SonicPiInstruments.mapping.
fun sonicPiNote(note: Int, synth: String? = null): SonicPiVoice.Note sonicPiNote(60, "tb303") — a checked synth name, for SonicPiInstruments.mapping.
fun PatPattern<String>.toSonicPi(cps: Number = 1.0, cycles: Int? = null, name: String = "field", instruments: SonicPiInstruments = SonicPiInstruments()): String Lower one pattern to a single-live_loop Sonic Pi program.
fun PatPattern<String>.toSonicPiProgram(cps: Number = 1.0, cycles: Int? = null, name: String = "field", instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiProgram Lower one pattern and keep everything the lowering learned — see SonicPiProgram.
fun sonicPiProgram(vararg layers: Pair<String, PatPattern<String>>, cps: Number = 1.0, cycles: Int? = null, instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiProgram Lower several named patterns to one Sonic Pi program, a live_loop per layer.
fun sonicPiProgram(vararg layers: SonicPiLayer, cps: Number = 1.0, cycles: Int? = null, instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiProgram Lower named layers, each free to name its own synth. See sonicPiProgram and on.
fun sonicPiFx(name: String, vararg opts: Pair<String, Number>): SonicPiFx A checked with_fx: sonicPiFx("reverb", "room" to 0.8, "mix" to 0.4).
infix fun SonicPiLayer.through(effect: SonicPiFx): SonicPiLayer pat("bd sd") on "beep" through sonicPiFx("echo") — add FX to a layer.
fun sonicPiLayer(name: String, pattern: PatPattern<String>, synth: String? = null, instruments: SonicPiInstruments? = null, fx: List<SonicPiFx> = emptyList()): SonicPiLayer A layer, spelled out.
infix fun Pair<String, PatPattern<String>>.on(synth: String): SonicPiLayer "bass" to pat("e1 ~ e2 ~") on "tb303" — this layer's notes play on synth.
infix fun Pair<String, PatPattern<String>>.using(instruments: SonicPiInstruments): SonicPiLayer "drums" to pat("bd sd") using myKit — this layer resolves its atoms with instruments.
fun Box.sonicPiPattern(pattern: PatPattern<String>, cps: Number = 1.0, cycles: Int? = null, name: String? = null, instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiLaunch Compile this pattern to Sonic Pi and play it, tied to the box.
fun Box.sonicPiPattern(vararg layers: Pair<String, PatPattern<String>>, cps: Number = 1.0, cycles: Int? = null, instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiLaunch Compile several named layers and play them, tied to the box. See sonicPiProgram.
fun Box.sonicPiPattern(vararg layers: SonicPiLayer, cps: Number = 1.0, cycles: Int? = null, instruments: SonicPiInstruments = SonicPiInstruments()): SonicPiLaunch Compile layers that may each name their own synth, and play them, tied to the box.
fun PatPattern<String>.sonicPiSource(cps: Number = 1.0, cycles: Int? = null, name: String = "field", instruments: SonicPiInstruments = SonicPiInstruments()): String The Sonic Pi source for a pattern, ready to paste into a #sonicpi region.
fun PatPattern<String>.sonicPiLive(cps: Number = 1.0, instruments: SonicPiInstruments = SonicPiInstruments(), lead: Number = 0.0, clock: PatClock? = defaultPatClock(), onSend: ((String) -> Unit)? = null): PatResponseSpec<String> Drive Sonic Pi from Field's clock, one /run-code per event.

Top-level values

Declaration Summary
val SONIC_PI_DRUM_ALIASES: Map<String, SonicPiVoice> The drum-machine names, resolved to real samples in the installed library.
const val SONIC_PI_LIVE_WORKSPACE: String The workspace live-driven one-shots run in, kept off the compile-mode workspaces.

SonicPiVocabulary

object SonicPiVocabulary

What the installed Sonic Pi can actually be asked to do.

Declaration Summary
val version: String by lazy The Sonic Pi the vocabulary was extracted from.
val synths: Set<String> by lazy
val fx: Set<String> by lazy
val sampleGroups: Map<String, List<String>> by lazy Sample group name (bd, drum, loop, …) to the samples in it.
val samples: Set<String> by lazy
val sampleOpts: Set<String> by lazy The opts sample accepts — amp, rate, lpf, hpf, beat_stretch, and forty more.
val commonPlayOpts: Set<String> by lazy The opts every synth accepts, whichever one is in force.
fun synthOpts(synth: String): Set<String> The opts synth accepts.
fun fxOpts(name: String): Set<String> The opts FX name accepts — :reverb has room and damp, :echo has phase and decay.
fun isSample(name: String): Boolean
fun isSynth(name: String): Boolean
fun isFx(name: String): Boolean
override fun toString(): String A one-line description, for a box that wants to prove the vocabulary loaded.

SonicPiVoice

sealed class SonicPiVoice

What one pattern atom turns into when Sonic Pi plays it.

Declaration Summary
data class Sample(val name: String) : SonicPiVoice() sample :bd_haus — a name from the installed sample library.
data class Note(val note: Int, val synth: String? = null) : SonicPiVoice() play 60 — a MIDI note, optionally after switching synth.
object Silent : SonicPiVoice() Nothing to play. Distinct from unresolved: this atom is a rest, and says so.

SonicPiInstruments

data class SonicPiInstruments(val overrides: Map<String, SonicPiVoice> = emptyMap(), val aliases: Map<String, SonicPiVoice> = SONIC_PI_DRUM_ALIASES, val defaultSynth: String = "beep", val defaultNote: Int = 60)

How an atom becomes a voice.

Declaration Summary
val overrides: Map<String, SonicPiVoice>
val aliases: Map<String, SonicPiVoice>
val defaultSynth: String The synth a bare note plays on, emitted as use_synth at the top of each live_loop.
val defaultNote: Int The note a bare synth-name atom plays.
fun resolve(atom: String): SonicPiVoice? Resolve one atom, or null if nothing in the installed vocabulary matches it.
fun mapping(vararg pairs: Pair<String, SonicPiVoice>): SonicPiInstruments Add or replace one atom's meaning: instruments.mapping("kick" to sonicPiSample("bd_tek")).

SonicPiPatternEvent

data class SonicPiPatternEvent(val layer: String, val at: PatQ, val voice: SonicPiVoice, val opts: List<Pair<String, String>>, val atom: String)

One thing Sonic Pi is asked to do, at one exact position in cycle space.

Declaration Summary
val layer: String The live_loop this belongs to.
val at: PatQ Cycles from the start of the loop body. Exact — this is the pattern's own number.
val voice: SonicPiVoice
val opts: List<Pair<String, String>> Rendered Sonic Pi opts, in emission order.
val atom: String The atom this came from, for pointing back at the pattern text.

SonicPiDroppedOpt

data class SonicPiDroppedOpt(val layer: String, val atom: String, val key: String, val why: String)

An annotation the target could not accept, kept so a dropped parameter is visible.

SonicPiUnresolvedAtom

data class SonicPiUnresolvedAtom(val layer: String, val atom: String, val count: Int)

An atom no part of the installed vocabulary matched.

SonicPiProgram

data class SonicPiProgram(val source: String, val events: List<SonicPiPatternEvent>, val cps: Double, val cycles: Int, val layers: List<String>, val unresolved: List<SonicPiUnresolvedAtom> = emptyList(), val droppedOpts: List<SonicPiDroppedOpt> = emptyList())

A Field pattern, lowered to a Sonic Pi program.

Declaration Summary
val source: String
val events: List<SonicPiPatternEvent>
val cps: Double
val cycles: Int
val layers: List<String>
val unresolved: List<SonicPiUnresolvedAtom>
val droppedOpts: List<SonicPiDroppedOpt>
val bpm: Double Sonic Pi beats per minute such that one beat is one Field cycle.
val secondsPerCycle: Double Seconds per cycle, which is what cps means at the other boundary.
fun onsetSeconds(): List<Double> Event onsets in seconds from the start of the loop, the way scheduled(cps) reports them.
override fun toString(): String

SonicPiLayer

data class SonicPiLayer(val name: String, val pattern: PatPattern<String>, val instruments: SonicPiInstruments? = null, val fx: List<SonicPiFx> = emptyList())

One live_loop: a name, a pattern, and optionally its own instrument set.

Declaration Summary
val name: String
val pattern: PatPattern<String>
val instruments: SonicPiInstruments?
val fx: List<SonicPiFx> FX to wrap this loop's body in, outermost first. See sonicPiFx.

SonicPiFx

data class SonicPiFx(val name: String, val opts: Map<String, Double> = emptyMap())

One with_fx around a layer's body.

Declaration Summary
override fun toString(): String

SonicPiReadEvent

data class SonicPiReadEvent(val layer: String, val at: PatQ, val kind: String, val name: String, val note: Int? = null, val opts: Map<String, String> = emptyMap())

One thing a Sonic Pi program was read as doing, and when.

Declaration Summary
val layer: String
val at: PatQ Beats from the top of the loop body — which, at use_bpm 60 * cps, is cycles.
val kind: String
val name: String Sample name for sample, the synth in force for play.
val note: Int?
val opts: Map<String, String>

SonicPiReading

data class SonicPiReading(val events: List<SonicPiReadEvent>, val loopPeriods: Map<String, PatQ>, val unparsed: List<String>, val fx: Map<String, List<SonicPiFx>> = emptyMap())

What SonicPiProgramReader made of a program: its events, and anything it could not read.

Declaration Summary
val events: List<SonicPiReadEvent>
val loopPeriods: Map<String, PatQ> Total sleep per loop, which for a well-formed loop is its period in cycles.
val unparsed: List<String> Lines that were neither blank, a comment, nor part of the emitted subset.
val fx: Map<String, List<SonicPiFx>> The with_fx blocks each loop opened, outermost first.

SonicPiProgramReader

object SonicPiProgramReader

Reads a generated program's text back into an event lattice.

Declaration Summary
fun read(source: String): SonicPiReading
fun parseRubyNumber(text: String): PatQ? A Ruby numeric literal from the emitted subset, exactly.

SonicPiPatternsSmokeTest.kt

Everything about lowering a Field pattern to Sonic Pi that can be checked with no Sonic Pi running — which, it turns out, is nearly everything worth checking.

The centre of this file is a differential oracle. For a corpus of patterns, the generated Sonic Pi text is parsed back by SonicPiProgramReader and its event lattice is compared against the pattern's own exact rational schedule, position by position, in PatQ — no doubles anywhere in the comparison. That is deliberate: the tempting version of this test compares SonicPiProgram.events against the pattern, which only proves the emitter can enumerate. Every failure that can actually reach a listener lives in the gap between that list and the text: a third rendered as 0.333333, a sleep delta that does not accumulate back to the position it came from, a missing pad at the end of the loop so the second bar arrives early, an opt attached to the wrong line. So the oracle reads the text.

The other half is the vocabulary. Sonic Pi answers a sample :bd_hauz by doing nothing at all — no error, no sound, and a live_loop that plays three of its four voices is very hard to debug from the sound. So every drum alias, every emitted instrument name and every emitted opt is checked against sonicpi-vocabulary.json, which was extracted from the installed app.

Needs no Sonic Pi install: the vocabulary is a committed resource. The parts that need a real engine are in afield.examples.sonicpi.SonicPiPatternProbe (sonicpi.pattern.timing).

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

SonicPiPatternsSmokeTest

object SonicPiPatternsSmokeTest

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

SonicPiSmokeTest.kt

Smoke tests for Field's Sonic Pi v5 bridge: a protocol test that runs without Sonic Pi installed (synth.sonic-pi.protocol), and a live test that boots a real Sonic Pi when one is present (synth.sonic-pi.live).

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

SonicPiProtocolSmokeTest

object SonicPiProtocolSmokeTest

Everything about the Sonic Pi v5 control protocol that can be checked without Sonic Pi installed: the daemon's port line, and the OSC encoding of an authenticated message.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

SonicPiLiveSmokeTest

object SonicPiLiveSmokeTest

Boots a real Sonic Pi and drives it, which is the only way to know any of this works.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

SoundPathMap.kt

Every route Field has from code to something audible, declared — and re-derived from the code.

The counts are the deliverable, so they live here as data rather than in a sentence somebody has to remember to update: SOURCE_COUNT ways to make samples, SINK_COUNT ways to get them out of the process, INPUT_COUNT ways to bring them in. Each is one SoundPath in PATHS, carrying its role, whether it can run offline, what has to be installed, the symbol it starts at, and the page that documents it — or null, when nothing does. Running this object as a program re-counts PATHS against all four declared totals and fails on disagreement.

The column that decides whether two paths can be used together is SoundPath.onSeam. Everything realtime meets at one signature, StereoRenderable.render, which AudioPatchSpec.Renderer repeats character for character. Two paths that both implement it compose for free; a path that does not implement it composes with nothing. SonicPi is the only one outside it, for a reason no wrapper reaches: it is a different process, and no sample ever enters this one. What the seam buys is clear in two earlier arrivals. Behind FaustVoiceBank are an event scheduler, a voice allocator and N WebAssembly instances in one GraalVM context, and reaching the speakers, the visualisers, the Link output and the box lifecycle cost it the five lines of FaustPolyNode. PatSynthBank is the pattern voice arriving the same way: a scheduler in front of the voice that already existed, and nothing else.

Why this is built like DocsAudioProvenance and not beside it

DocsAudioProvenance guards published assets: it scans the docs for audio= fences and re-renders each one. That is the right mechanism for "did a figure rot" and the wrong one for "did somebody add a path", because a new render path is invisible to it until a page publishes a clip from it. So this file takes the same three-step shape — declare, derive from the code, fail on disagreement — and points it at a different derivation: the MARKERS are literals that can only mean "a new way in or out of this process", and every file carrying one has to be attributed to a declared SoundPath.

A marker declares a file-to-path map rather than a bare allowlist because a literal turns up in the files that use a surface as well as in the one that is it: WavFloatArrayWriter.encode reaches both the inline-audio sink and its callers, and they belong to different paths. SELF is the sweep's one blind spot, and the audit turns it back into a check by requiring this file to spell every marker literal, so a literal that has drifted out of the tree cannot sit here matching nothing.

Why the fifth marker is the seam itself

The first four markers are all device calls or file writes, and a sample source need be neither. FaustVoiceBank — a Faust program played as an instrument by a pattern — carried none of them, so it sat in the tree undeclared for a month with SOURCE_COUNT reading six and the audit green, which is the one failure this file exists to prevent. The fifth marker is therefore the seam itself, override fun generateStereo(, which every StereoNode has to spell: a new realtime stereo source cannot arrive without a hit. It reported two findings on its first run — FaustVoiceBank, and DoughAudioSink, which is DoughEngine wrapped onto the seam by parkDough and had been declared off it since the day this file was written.

What it still cannot see is a source that is off the seam entirely: another SonicPi in another process, or another whole-buffer free function like patRenderStereo. A mono Signal is far too common a literal to mean anything.

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

Top-level functions

Declaration Summary
fun afield.document.graph.Box.showSoundPathComparison(cutoffHz: Double = SoundPathMap.COMPARISON_CUTOFF_HZ) One line for a box: the whole comparison, playable and plotted.

SoundPathMap

object SoundPathMap

The declared map, and the audit that re-derives it from the tree.

Declaration Summary
const val PAGE The page this map is written on. Its prose is checked against PATHS, not trusted.
const val SELF This file, excluded from the marker sweep — and the guard's one blind spot, stated as one.
enum class Role(val label: String) What a path does with samples.
enum class Timing(val label: String) Whether a path needs a clock and a device, or runs as fast as the CPU allows.
class SoundPath(val id: String, val role: Role, val timing: Timing, val onSeam: Boolean, val needs: String, val channels: String, val what: String, val file: String, val entryPoint: String, val page: String?) One route from code to sound, with everything a person choosing between them needs.
val PATHS: List<SoundPath> The map. Ten sample sources, five ways out, four ways in.
fun path(id: String): SoundPath Looks a path up by id, with a message that lists the alternatives.
const val SOURCE_COUNT How many distinct ways there are to make samples. Ten, replacing a bolded "two".
const val SINK_COUNT How many ways samples leave this process. Five, of which three are separate OpenAL contexts.
const val INPUT_COUNT How many ways samples arrive from outside. Four.
const val SEAM_COUNT How many declared paths meet AudioPatchSpec.Renderer.render. Thirteen of nineteen.
class Marker(val id: String, val literal: String, val meaning: String, val roots: List<String>, val declaredIn: Map<String, String>) A literal that can only mean "a new way in or out of this process", and where it may appear.
val MARKERS: List<Marker> Every marker, with the file-to-path attribution measured on the day each one was added.
fun filesCarrying(marker: Marker, repoRoot: File = File(".")): Set<String> Repo-relative paths of every file under a marker's Marker.roots that carries its literal.
fun selfCheck(repoRoot: File = File(".")): List<String> Marker literals that SELF does not spell the way it says it does.
fun markerDisagreements(repoRoot: File = File(".")): List<String> Every disagreement between MARKERS and the tree, phrased so the fix is obvious.
fun brokenCitations(repoRoot: File = File(".")): List<String> Declared files or symbols that are not there any more.
fun pageOmissions(repoRoot: File = File(".")): List<String> Paths the map claims but PAGE does not actually name.
const val COMPARISON_CUTOFF_HZ Cutoff every path in the comparison is asked for, in Hz.
fun dryBeat(): PatSoundDocs.Stereo The dry beat: patRenderStereoDirt through the real Tidal one-shots, unfiltered.
fun throughSignalGraph(dry: PatSoundDocs.Stereo, cutoffHz: Double = COMPARISON_CUTOFF_HZ): PatSoundDocs.Stereo dryBeat lowpassed by the Signal graph's own ChamberlinLowpass.
fun faustLowpassSource(cutoffHz: Double = COMPARISON_CUTOFF_HZ): String The Faust source the comparison compiles. A first-order lowpass on a stereo input, nothing else.
fun throughFaust(dry: PatSoundDocs.Stereo, cutoffHz: Double = COMPARISON_CUTOFF_HZ): PatSoundDocs.Stereo dryBeat lowpassed by a Faust program, compiled and run in this process with no audio device.
fun seamAgreementError(dry: PatSoundDocs.Stereo): Float Proof that the seam is one seam: the same gain, computed by the graph and by Faust.
class ComparisonFigure(val name: String, val pathId: String, val pageLabel: String, val render: () -> PatSoundDocs.Stereo) One published figure of the comparison, and which declared SoundPath made it.
val COMPARISON: List<ComparisonFigure> The three renders of one buffer, in the order the page presents them.
fun comparison(name: String): ComparisonFigure Looks a comparison figure up by name.
fun renderWav(name: String, outPath: String) Offline-renders comparison figure name to a stereo 16-bit WAV at outPath.
fun brightnessHz(samples: FloatArray, sampleRate: Int = PatSoundDocs.SR): Double Spectral centroid of a buffer, in Hz — the number that says "this got darker".
fun main(args: Array<String>) Prints the map, checks it against the tree, and renders the comparison.
const val BRIGHTNESS_DROP_CEILING A 900 Hz lowpass must take the spectral centroid down to a fraction of the dry buffer's.
const val CROSS_PATH_SIMILARITY_FLOOR Two different lowpasses of one buffer still correlate strongly.
const val SEAM_AGREEMENT_CEILING Signal * 0.5 and Faust *(0.5) on identical input must agree to float precision.

SoundRenderPathDocs.kt

The audio figures on sound.md, each declaring which voice made it.

SoundRenderPathDocs renders one pattern through patVoice and through the Tidal Dirt-Samples, and SoundProvenanceGuardTest checks every committed .wav against its declared voice.

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

SoundRenderPathDocs

object SoundRenderPathDocs

The figures behind public/docs/sound.md, and the reason that page can be trusted.

Declaration Summary
enum class Voice(val pageLabel: String) Which voice a figure is made of — declared by the figure, checked against the audio.
class Figure(val name: String, val title: String, val voice: Voice, val cycles: Int, val cps: Double, val pattern: () -> PatPattern<String>, val synth: PatVoiceSpec = patVoice(), val expectedUnmatched: Set<String> = emptySet()) One published audio figure: a pattern, a tempo, and the voice it claims to be made of.
fun theBeat(): PatPattern<String> The beat every A/B figure on the page uses, so the two voices are compared on one pattern.
val FIGURES: List<Figure>
fun figure(name: String): Figure Looks a figure up by name, with a message that lists the alternatives.
class Rendered(val figure: Figure, val audio: PatSoundDocs.Stereo, val result: DirtRender.DirtRenderResult?) A rendered figure: the audio, and what it turned out to be made of.
fun render(name: String): Rendered Renders name through the voice it declares.
fun standInReference(name: String, tokens: Set<String>? = null): PatSoundDocs.Stereo The stand-in render of a figure's pattern, whatever voice the figure declares.
fun sampleBackedReference(name: String): PatSoundDocs.Stereo The stand-in render of just the events the library is expected to play from samples.
fun fallbackReference(name: String): PatSoundDocs.Stereo The stand-in render of just the events expected to fall back to the synth voice.
fun tokensOf(figure: Figure): Set<String> Every distinct atom the figure's pattern schedules over its own length.
fun renderWav(name: String, outPath: String) Offline-renders figure name to a stereo 16-bit PCM WAV at outPath.
fun waveformSimilarity(a: PatSoundDocs.Stereo, b: PatSoundDocs.Stereo): Double How much two renders of the same pattern are the same waveform: normalised zero-lag cross-correlation of the two channels, worst channel wins.
class ProvenanceMeasurement(val figure: Figure, val toFullStandIn: Double, val toSampleBacked: Double, val toFallback: Double) What a rendered figure is actually made of, measured against three stand-in references.
fun measureProvenance(name: String, audio: PatSoundDocs.Stereo) Measures audio — a fresh render or a decoded committed .wav — against figure name.
fun main(args: Array<String>) Renders every figure to a temp dir and asserts the audio is real, and really the declared voice. Writes NO committed assets; pass an output dir as args[0] to regenerate those.
const val STAND_IN_SIMILARITY_FLOOR A figure that claims to be the stand-in must essentially be the stand-in.
const val SAMPLE_BACKED_SIMILARITY_CEILING The events a figure says came from samples must not sound like the synth.
const val FALLBACK_SIMILARITY_FLOOR A Voice.MIXED figure's fallback events must actually be audible in it.

SynthCookbookCompileDemo.kt

src/jvmMain/kotlin/afield/audio/synth/SynthCookbookCompileDemo.kt

ToneKotlinJs.kt

The typed Kotlin/JS view of Tone.js, the computer-music library loaded by the RemoteAR browser head, selected in a box with the // {{ tone }} profile.

The surface is described once with jsBinding. Both the compiler-facing external declarations and the completion model are generated from that one description, as with RemoteArKotlinJs, so the two cannot drift. Adding a Tone class or helper is one DSL call.

external object Tone maps to the global Tone: Tone.Synth() compiles to new Tone.Synth(), and Tone.now() to Tone.now(). Every name bound here is present in the vendored bundle vendor/tone/15.1.22/Tone.js under the RemoteAR extension resources. Distortion's constructor argument amount is not, so Distortion takes a dynamic options argument and exposes the real distortion property instead.

What is covered

  • the synth family and sources
  • the core effects
  • the Param / Signal value type
  • Transport scheduling, Loop and Sequence

Playback needs a user gesture in a real browser, so it is not checked headlessly. KotlinJsToneSurfaceSmokeTest compiles every bound name from Kotlin to JS against the generated declarations.

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

ToneKotlinJs

object ToneKotlinJs

The Tone.js binding: its profile id and the declarations generated for the compiler and for completion.

Declaration Summary
const val surfaceId: String
val declarations: String Compiler-facing external declarations, generated from binding.
val completionDeclarations: String JVM-analysis-safe model used by the out-of-process completion sidecar, generated from binding.

osc.kt

OSC in and out, wrapped around JavaOSC (com.illposed.osc) and driven from Field's main runloop. The page that teaches it is public/docs/osc.md.

The usual entry points are osc and oscClient, not the constructors: they hand back cached OscServer / OscClient instances (keyed by port, and by host+port) so that re-running a box during live editing reuses the existing socket instead of rebinding it. OscServer binds a UDP port on every interface (default 9000) in its constructor and starts listening immediately. Handlers registered with on are keyed by OSC address pattern and return a Generator. The handler lambda itself runs on JavaOSC's receive thread when a message arrives; the generator it returns is started, and resumed once, on the next service pass on the main thread, then resumed once per pass until it finishes — so a handler can span frames. Every yield hands back the message that started it. OscClient opens its outbound socket lazily on first send.

Handlers belong to the server, not to the box that registered them: stopping or re-running the box leaves them in place. Registering the same pattern again replaces the handler; a pattern the edited code no longer mentions keeps firing until remove is called for it.

Three things to know when debugging. A port another program already holds does not make the bind fail: JavaOSC sets SO_REUSEADDR, so both sockets bind and a program bound to 127.0.0.1 takes what is sent there. A handler that throws is reported as an error on the box that registered it, and on the console; a failed send is only printed, so a box that sends every frame keeps running. And OscServer has debug on by default, printing every incoming message to Field's console (not to the box).

src/jvmMain/kotlin/afield/io/osc/osc.kt — read the source

Used in: OSC and TUIO

Top-level functions

Declaration Summary
fun oscTimeTag(atMillis: Long): com.illposed.osc.argument.OSCTimeTag64 The OSC timetag for wall-clock time atMillis: NTP seconds since 1900 in the high 32 bits, the fraction of a second in the low 32. Built from the numbers directly because JavaOSC's own conversion from a Date counts from an epoch an hour away from the true one.
fun osc(port: Int = 9000): OscServer Get (and cache) an OSC server listening on port.
fun oscClient(host: String = "127.0.0.1", port: Int = 9000): OscClient Get (and cache) an OSC client targeting host:port.

OscServer

class OscServer(localPort: Int = 9000, autoStart: Boolean = true) : Closeable

Thin wrapper around JavaOSC's OSCPortIn that queues callbacks onto the main runloop.

Declaration Summary
data class Message(val address: String, val arguments: List<Any?>, val event: OSCMessageEvent)
var debug
val isListening: Boolean
fun startListening()
fun stopListening()
fun on(pattern: String, handler: (Message) -> Generator<Message, *>) Run handler for every message whose address matches pattern, replacing any handler already registered for that pattern. If the handler's lambda or its generator throws, the exception is shown as an error on the box that called on (and printed to the console); the generator is dropped and the handler stays registered for the next message.
fun on(pattern: String, runOwned: Boolean, handler: (Message) -> Generator<Message, *>) Register a handler, optionally releasing it on this run's stop, rerun or retirement. Cleanup removes only this registration, never a newer replacement or the shared socket. With runOwned false, the ordinary persistent registration semantics apply.
fun remove(pattern: String)
fun hasHandler(pattern: String): Boolean Whether a handler is registered for exactly pattern.
suspend fun run()
fun tick() Manual single tick; call from your main loop if you do not use run.
fun stopRunLoop()
val isClosed: Boolean True once close has run. osc never hands back a closed server; it opens a new one.
override fun close()

OscClient

class OscClient(remoteHost: String = "127.0.0.1", remotePort: Int = 9000, autoConnect: Boolean = false) : Closeable

Thin wrapper around JavaOSC's OSCPortOut for sending messages.

Declaration Summary
var debug
val isConnected: Boolean
fun connect()
fun disconnect()
fun send(address: String, arguments: List<Any?> = emptyList())
fun send(address: String, vararg arguments: Any?) Convenience vararg sender; converts arguments to a list and delegates to send.
fun sendBundle(messages: List<OSCMessage>, atMillis: Long? = null) Send messages as one bundle, for the receiver to act on at atMillis (wall-clock System.currentTimeMillis() time), or at once when it is null.
fun sendIn(millis: Long, address: String, vararg arguments: Any?) One message, in a bundle timetagged millis from now — sendBundle for the common case.
fun send(message: OSCMessage) Send a pre-built OSC message.
fun send(message: com.illposed.osc.OSCPacket) Send a pre-built OSC message or bundle.
override fun close()

OscServers

object OscServers

Cached per-port servers that are automatically driven on the main runloop.

Declaration Summary
fun get(port: Int = 9000): OscServer

OscClients

object OscClients

Cached per-host+port clients that open on first use and remain available across edits.

Declaration Summary
fun get(host: String = "127.0.0.1", port: Int = 9000): OscClient

PetriMusic.kt

Turns a Petri net into a PatPattern, so the whole pattern language applies to it.

A net is a pure function of time, and that is what lets it be a pattern. PatPattern is not a list of notes: it is a function from a time arc to the events inside it, and the combinators may call it with any arc, in any order, as many times as they like. rev() reads a cycle backwards, every(4) re-queries under a transform, and the scheduler walks forward one cycle at a time. A stateful sequencer cannot satisfy that contract. A Petri net with a deterministic conflict policy can, because PetriRun memoises an append-only trajectory: the answer for cycle 3 is the same object whether you ask before or after asking about cycle 9.

So nothing here reimplements a combinator:

val net = petri {
    place("kick", tokens = 1)
    t("hit",  dur = cyc(1, 16), emit = "bd") { from("kick"); to("hats") }
    t("back", dur = cyc(1, 16), emit = "sd") { from("hats"); to("kick") }
}

net.emits()                                  // PatPattern<String>
   .every(4) { it.fast(2) }                  // the rest of the language applies
   .jux { it.rev() }
   .respond(cps = 2.0) { println(it) }

Three ways from a net into the language

  1. Firings are events. pattern gives you PatPattern<PetriFiring>; emits narrows it to what each firing sounds. This is the short-term rhythm layer, where a net behaves like a drum machine.

  2. Tokens are patterns. A token's colour can be a whole PatPattern, so a place is a section and a transition is an edit. unfold plays whatever pattern a firing consumed across that firing's arc, which makes a slow net a song structure over the fast nets it schedules.

  3. Markings are control signals. signal turns a place's token count into a continuous PatPattern<Double>, optionally interpolated, so tension accumulating in a place can drive a filter cutoff: with cutoff(net.signal("tension").rangex(200, 8000)).

src/jvmMain/kotlin/afield/simulation/petri/PetriMusic.kt — read the source

Used in: Petri nets, Petri notation cheatsheet

Top-level functions

Declaration Summary
fun PetriRun.pattern(): PatPattern<PetriFiring> The net's firings as a pattern.
fun PetriNet.pattern(from: PetriMarking = initial): PatPattern<PetriFiring> The net's firings as a pattern, from a fresh PetriRun.
fun PatPattern<PetriFiring>.only(vararg transitions: String): PatPattern<PetriFiring> Keeps only the firings of the named transitions — one lane out of a multi-track net.
fun PatPattern<PetriFiring>.withoutTransitions(vararg transitions: String): PatPattern<PetriFiring> Drops the firings of the named transitions.
fun PatPattern<PetriFiring>.emits(): PatPattern<String> What each firing sounds, as text — the door to notes(), the Dirt sample vocabulary, and everything else in the language that speaks PatPattern<String>.
inline fun <reified T> PatPattern<PetriFiring>.emitsAs(): PatPattern<T> emits for a typed emit — emitsAs<Int>() for note numbers. Firings emitting anything else are dropped.
fun PetriNet.emits(from: PetriMarking = initial): PatPattern<String> The net's emitted sounds, straight from the net. The one-liner most nets want.
inline fun <reified T> PatPattern<PetriFiring>.consumed(): PatPattern<T> The colours a firing consumed, one event per firing that consumed a T.
inline fun <reified A> PatPattern<PetriFiring>.unfold(mode: PetriUnfold = PetriUnfold.NATURAL): PatPattern<A> Plays the pattern each firing consumed, across that firing's arc.
fun <A> petriUnfoldJoin(inners: PatPattern<PatPattern<A>>, mode: PetriUnfold): PatPattern<A> The join behind unfold, separated out so the reified wrapper stays small.
fun PetriRun.signal(place: String, interpolate: PetriInterp = PetriInterp.STEP): PatPattern<Double> A place's token count as a continuous signal.
fun PetriNet.signal(place: String, interpolate: PetriInterp = PetriInterp.STEP): PatPattern<Double> signal from a fresh run — convenient, but see PetriRun.pattern on sharing a run.
fun PetriRun.level(place: String, full: Int? = net.place(place)?.capacity, interpolate: PetriInterp = PetriInterp.STEP): PatPattern<Double> A place's occupancy as a 0..1 signal — the form the shaping operators expect.
fun PetriNet.level(place: String, full: Int? = place(place)?.capacity, interpolate: PetriInterp = PetriInterp.STEP): PatPattern<Double> level from a fresh run.
fun PetriRun.gate(place: String, atLeast: Int = 1): PatPattern<Double> A signal that is 1.0 while place holds at least atLeast tokens and 0.0 otherwise — a gate.
fun petriRing(name: String, pulses: Int, steps: Int, rotation: Int = 0, emit: Any? = name, build: PetriBuilder.() -> Unit = {}): PetriNet A Euclidean token ring: steps places in a circle, one token walking round them, and a sound at the pulses positions the Bjorklund algorithm picks out.
fun petriRingInto(into: PetriBuilder, name: String, pulses: Int, steps: Int, rotation: Int = 0, emit: Any? = name) Adds a Euclidean ring to a net you are already building. See petriRing.
fun petriSong(vararg sections: Pair<String, Pair<PatPattern<*>, Int>>, loop: Boolean = true): PetriNet A linear song form: one place per section, one transition between consecutive sections, each holding for its own number of cycles, looping at the end.
fun PetriNet.live(from: PetriMarking = initial, startAt: PatQ = PatQ.ZERO): PetriLive A live, pokeable instance of this net. See PetriLive.
fun PetriLive.play(cps: Number = 1.0, clock: afield.audio.synth.PatClock? = afield.audio.synth.musicClock(), ahead: Number = 0.0, dispatchOnMain: Boolean = true, callback: (PetriFiring) -> Unit = {}): afield.audio.synth.PatResponseSpec<Unit> Plays a live net in a box, on the beat.
fun petriTicks(step: PatQ = PatQ.of(1, 16)): PatPattern<Unit> A pure pattern of one silent event per step of a cycle — the metronome a live net is driven by.

PetriUnfold

enum class PetriUnfold

How unfold fits a pattern-valued token into the arc of the firing that consumed it.

Constants: SQUEEZE, NATURAL, RESTART.

PetriInterp

enum class PetriInterp

How signal fills in the space between the instants at which a place's token count changed.

Constants: STEP, LINEAR, SMOOTH.

PetriLive

class PetriLive internal constructor(val net: PetriNet, from: PetriMarking = net.initial, startAt: PatQ = PatQ.ZERO)

A net you can reach into while it is playing.

Declaration Summary
val net: PetriNet
var marking: PetriMarking The marking right now.
var time: PatQ How far the net has been advanced.
var clickRemoves: Boolean Whether clicking an occupied unbounded place in the visualizer takes a token out rather than putting another in. Off by default, because adding is the more common gesture.
val listeners: MutableList<(PetriFiring) -> Unit> Called for every firing, whether it came from the clock or from force.
fun onFiring(listener: (PetriFiring) -> Unit): () -> Unit Registers listener and returns a function that removes it again.
fun advanceTo(instant: PatQ): List<PetriFiring> Runs the net forward to instant, returning everything that fired on the way.
fun inject(place: String, colour: Any? = null, count: Int = 1): PetriLive Puts a token of colour into place, now.
fun remove(place: String, count: Int = 1): PetriLive Takes the oldest token out of place, if there is one.
fun clear(place: String): PetriLive Empties place.
fun force(transition: String): PetriFiring? Fires transition right now, whether or not the clock was going to, and returns the firing — or null if it is not enabled, in which case nothing happened.
fun reset(to: PetriMarking = net.initial): PetriLive Back to the marking it started from, at time zero.
fun frame(layout: PetriLayout = PetriLayout.AUTO, radius: Double = 240.0): PetriDrawing A frame for the visualizer: the marking now, and what it enables.
override fun toString(): String