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
FramesSigLoadOptions
| Declaration | Summary |
|---|---|
val minimumCodePointLimit: Int |
|
val codePointLimitPadding: Int |
|
val defaultSmallVectorWidth: Int |
FramesSigMetadata
FramesSigProduct
sealed interface FramesSigProduct
| Declaration | Summary |
|---|---|
val path: String |
|
val frameCount: Int |
|
fun sampleAt(timeSeconds: Double, metadata: FramesSigMetadata): FramesSigTimedSample |
FramesSigFloatProduct
| Declaration | Summary |
|---|---|
override val path: String |
|
val values: FloatArray |
|
override val frameCount: Int |
|
override fun sampleAt(timeSeconds: Double, metadata: FramesSigMetadata): FramesSigTimedSample |
FramesSigFloatVectorProduct
| 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
| Declaration | Summary |
|---|---|
val path: String |
|
val index: Int |
|
val timeSeconds: Double |
|
val values: FloatArray |
|
val value: Float? |
FramesSigProductInfo
| Declaration | Summary |
|---|---|
val path: String |
|
val frameCount: Int |
|
val vectorWidth: Int |
|
val startTimeSeconds: Double |
|
val endTimeSeconds: Double |
FramesSigDocument
| 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
| Declaration | Summary |
|---|---|
val requireManifest: Boolean |
|
val requireRowAlignment: Boolean |
Sound2VecManifest
Sound2VecClip
| 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
Sound2VecDataset
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
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
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
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
Samples remain at the input rate; no normalization, clipping or resampling is applied.
| Declaration | Summary |
|---|---|
val peak: Float |
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
Offline process boundary with durable binary caching and a bounded timeout; call async from a box.
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
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
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
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
The opcodes a Csound box can complete, with their signatures, as libcsound itself lists them.
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
Banks built as arrays of the scalar models: ResonatorBankArray holds one Resonator per frequency and TrackingResonatorBankArray one TrackingResonator, each updated sample by sample.
TrackingResonatorBankArray
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
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
| 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
Scalar resonator reference implementation.
TrackingResonator
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
ResonatorBank
Kotlin port of the "resonate" EMA resonator bank from noFFT.
TrackingResonatorBank
A phase-tracking resonator bank.
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
SimpleSpatialAudioEngine
Simple spatial audio engine using LWJGL OpenAL.
SoundBuffer
| 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)
ReverbSettings
EaxReverbSettings
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
Durable storage for a TrackingResonatorBank analysis.
TrackingPartial
| Declaration | Summary |
|---|---|
val frame: Int |
|
val resonator: Int |
|
val naturalFrequency: Float |
|
val frequency: Float |
|
val amplitude: Float |
|
val power: Float |
|
val phase: Float |
TrackingResonatorTrack
| 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
| 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
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
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
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
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
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
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
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
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
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
Precomputes the probe-to-probe visibility graph that PathEffect searches at runtime.
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
Precomputes reflection data -- impulse responses, or reverb decay times -- at each probe.
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
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
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
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
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.
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
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
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
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
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
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
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
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
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
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
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
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
Names a particular piece of baked data within a ProbeBatch.
ProbeGenerationParams
How probes should be distributed within a region.
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
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
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
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
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
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
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
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
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
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
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
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
| 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
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
One Ableton Live set, as read from an .als file by read.
AbletonTrack
| 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
| 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
Constants: Audio, Midi.
AbletonClipLoop
AbletonSampleRef
| Declaration | Summary |
|---|---|
val relativePath: String? |
|
val absolutePath: String? |
|
val defaultDurationSamples: Long? |
|
val defaultSampleRate: Double? |
|
val originalFileSize: Long? |
|
val originalCrc: Long? |
|
val defaultDurationSeconds: Double? |
AbletonSampleFileMapper
| 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
| 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
| Declaration | Summary |
|---|---|
val track: AbletonTrack |
|
val arrangementBeat: Double |
|
val arrangementSeconds: Double |
|
val bands: AbletonVolumeBands |
|
val clips: List<AbletonClipVolume> |
|
val rms: Double |
AbletonClipVolume
| 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
| Declaration | Summary |
|---|---|
val windowSeconds: Double |
|
val hopSeconds: Double |
|
val lowCutoffHz: Double |
|
val highCutoffHz: Double |
|
val fftSize: Int |
AbletonSampleVolumeAnalysis
| 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
| Declaration | Summary |
|---|---|
fun analysisFor(sourceFile: File): AbletonSampleVolumeAnalysis |
|
fun clear() |
AbletonPitchEstimate
| Declaration | Summary |
|---|---|
val midiPitch: Int |
|
val velocity: Int |
|
val startSeconds: Double |
|
val endSeconds: Double |
|
val channel: Int |
|
val frequencyHz: Double |
AbletonClipPitch
| Declaration | Summary |
|---|---|
val track: AbletonTrack |
|
val clip: AbletonClip |
|
val sourceFile: File |
|
val sourceSeconds: Double |
|
val pitch: AbletonPitchEstimate |
AbletonTrackPitch
| Declaration | Summary |
|---|---|
val track: AbletonTrack |
|
val arrangementBeat: Double |
|
val arrangementSeconds: Double |
|
val pitch: AbletonPitchEstimate |
|
val clips: List<AbletonClipPitch> |
AbletonSamplePitchAnalysis
| Declaration | Summary |
|---|---|
val sourceFile: File |
|
val midiFile: File? |
|
val sourceLastModified: Long |
|
val sourceLength: Long |
|
val notes: List<AbletonPitchEstimate> |
|
fun dominantPitchAtSeconds(sourceSeconds: Double): AbletonPitchEstimate? |
AbletonSamplePitchAnalysisCache
| Declaration | Summary |
|---|---|
fun analysisFor(sourceFile: File): AbletonSamplePitchAnalysis |
|
fun dominantPitchAtSeconds(sourceFile: File, sourceSeconds: Double): AbletonPitchEstimate? |
|
fun clear() |
AbletonAutomationEnvelope
| 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
AbletonAutomationEvent
| Declaration | Summary |
|---|---|
val id: String |
|
val time: Double |
|
val value: AbletonAutomationValue |
|
val curve: AbletonAutomationCurve? |
AbletonAutomationValue
sealed class AbletonAutomationValue
AbletonAutomationCurve
| Declaration | Summary |
|---|---|
val control1X: Double |
|
val control1Y: Double |
|
val control2X: Double |
|
val control2Y: Double |
|
fun xToY(x: Double): Double |
AbletonTempoMap
| 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
AbletonSetFLineDebugOptions
AbletonSetFLineDebugOptionsBuilder
class AbletonSetFLineDebugOptionsBuilder
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
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
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, andDirtRenderResult.usedSamplesisfalse, 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
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
| 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
| 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
| 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
DirtSamplePlayer
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
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
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.
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
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
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
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.jsthat detects Node would callrequire('fs')and die), createDSPFactorystill returns its wasm indataas an embind vector rather thancode,- 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
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
|
fun runTests(): Boolean |
FaustSoundDocs.kt
src/jvmMain/kotlin/afield/audio/synth/FaustSoundDocs.kt — read the source
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
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
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
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
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
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
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
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
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
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
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
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
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
Layout for MidiClip.toFLines. Sizes are in Field units, which for a box output read as pixels.
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
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:
pan/0is a lossless mono tap, not a downmix. Every event leavespatSpatialEventsForArccarryingpan/0, which puts all of an engine's energy in its left channel. The smoke test requires the right channel to peak belowRIGHT_CHANNEL_CEILING, which is what letsPatSpatialRoomarraycopythe left channel rather than compute0.5*(L+R), and why the source signal has full amplitude rather than half of it.- The six spatial tags never reach Dough. Asserted on the event maps, not on the audio:
spat,az,elev,dist,occlandairare consumed by this path andlpfis not. azis not in the signal. The same pattern rendered withaz = 0and withaz = -70produces 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
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
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
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
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.
SonicPiFlash
data class SonicPiFlash(val jobId: Int, val workspace: String, val line: Int)
One line reported as having triggered sound. See SonicPi.activity.
SonicPiPorts
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
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
| Declaration | Summary |
|---|---|
val jobId: Int |
|
val thread: String |
|
val runtime: Double |
|
val lines: List<SonicPiLogLine> |
|
val text: String |
|
override fun toString(): String |
SonicPiError
| 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
| 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
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
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) orlpf:(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
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
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
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
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
One thing Sonic Pi is asked to do, at one exact position in cycle space.
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
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
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
One thing a Sonic Pi program was read as doing, and when.
SonicPiReading
What SonicPiProgramReader made of a program: its events, and anything it could not read.
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
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
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
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/Signalvalue type Transportscheduling,LoopandSequence
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
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
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
Cached per-port servers that are automatically driven on the main runloop.
| Declaration | Summary |
|---|---|
fun get(port: Int = 9000): OscServer |
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
-
Firings are events.
patterngives youPatPattern<PetriFiring>;emitsnarrows it to what each firing sounds. This is the short-term rhythm layer, where a net behaves like a drum machine. -
Tokens are patterns. A token's colour can be a whole
PatPattern, so a place is a section and a transition is an edit.unfoldplays whatever pattern a firing consumed across that firing's arc, which makes a slow net a song structure over the fast nets it schedules. -
Markings are control signals.
signalturns a place's token count into a continuousPatPattern<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
PetriUnfold
How unfold fits a pattern-valued token into the arc of the firing that consumed it.
Constants: SQUEEZE, NATURAL, RESTART.
PetriInterp
How signal fills in the space between the instants at which a place's token count changed.
Constants: STEP, LINEAR, SMOOTH.
PetriLive
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 |