Onda Language Guide
This guide is the main reference for writing Onda programs. It is organized as a learning path: start with a small patch, learn the top-level program shape, then move through executable code, data, functions, structs, processors, graphs, generics, and modules.
Contents
- A First Patch
- Source Files
- Top-Level Program Surface
- Executable Sections
- Types and Values
- Statements and Expressions
- Constants and Compile-Time Code
- Functions with
def - Structs
- Processors with
proc - Graphs
- Generics and Compile-Time Parameters
- Modules, Namespaces, and
use - Reference Notes
1. A First Patch
An Onda file describes an audio processor. This patch exposes one host parameter, creates persistent state, computes one value per block, and writes one output sample at a time.
params:
freq = 440.0 {20.0, 20000.0}
init:
phase = 0.0
block:
incr = freq * TWO_PI / SR
sample:
phase = phase + incr
if phase > TWO_PI:
phase = phase - TWO_PI
out1 = sin(phase)
The main parts are:
| Part | Meaning |
|---|---|
params |
Host-visible control values. |
init |
Setup code and persistent state. |
block |
Code that runs once per host block. |
sample |
Code that runs once per sample. |
out1 |
A numbered audio output. |
The rest of the language grows from this model. You describe a processor’s surface, write code at the rate where it should run, and use functions, structs, processors, and graphs when the patch becomes reusable or larger.
2. Source Files
Onda supports indentation syntax and brace syntax. These two programs are equivalent:
outs:
out1
sample:
out1 = 0.0
outs {
out1
}
sample {
out1 = 0.0
}
Basic source rules:
- Statements can be separated by newlines or
;. - Line comments start with
#. - Names are introduced before they are used.
- Top-level declarations are processed in lexical order.
import module/pathloadsmodule/path.onda.include "path.onda"orinclude "path.on"inserts another file by quoted path.
Top-level forms:
| Form | Purpose |
|---|---|
ins, inputs |
Host input ports. |
params, kins |
Host-visible parameters. |
outs, outputs |
Audio-rate output ports. |
kouts |
Block-rate control output ports. |
buffers |
External host-bound buffers. |
events, event |
Host-triggered event handlers. |
init |
Setup and persistent state. |
block |
Per-block code. |
sample |
Per-sample code. |
graph |
Declarative signal routing. |
const, const def |
Compile-time values and helpers. |
def |
Runtime helper functions. |
struct |
Nominal data types. |
proc, processor |
Reusable DSP processors. |
namespace |
Qualified declaration groups and integer templates. |
use, pub use |
Unqualified lookup imports and re-exports. |
3. Top-Level Program Surface
The program surface is the set of values the host can connect to the processor:
inputs, params, outputs, buffers, and events. This chapter covers the top-level
forms. Processor-specific versions of the same ideas are introduced later in
Processors with proc.
Inputs
ins declares input ports. inputs is an alias.
ins:
in1
side: f64
stereo: f32[2]
Shorthand forms:
ins 2
ins<f64>:
left
right
meter: f32
Rules:
- Omitted input types default to
f32, or to the section default inins<T>. ins Nexpands toin1..inN.Ncan be a compile-time integer expression.- If
inNis used without aninsblock, that input is implicitly created asf32. - If a count and explicit list are both present, they must match exactly.
- Scalar inputs can have defaults and ranges:
freq = 440.0 {20.0, 20000.0}. - A single range value only specifies the max range:
freq = 440.0 {20000.0}. - Fixed-size array inputs can have defaults, and array literal defaults must match the declared length.
Explicitly declared homogeneous inputs can be indexed:
const N = 4
ins N
outs N
sample:
for i in 0..N:
outs[i] = ins[i] * 0.5
ins[i] is 0-based and runtime indices are clamped. Implicit inputs created by
using in1, in2, and so on cannot be dynamically indexed.
Parameters
params declares host-visible control parameters.
params:
gain = 1.0
mode: i32 = 0
spread: f32[2] = [0.25, 0.75]
freq = 440.0 {20.0, 20000.0}
At the top level only, kins is an alias for params.
kins:
cutoff = 1200.0
resonance = 0.5
Rules:
- Omitted param types without defaults become
f32. - Omitted param types with defaults infer from the default.
gain = 0.5becomesf32;mode = 0becomesi32.- Scalar params can have ranges. Array params cannot have ranges.
params Nexpands toparam1..paramN; top-levelkins Nexpands tokin1..kinN.- Top-level code may declare either
paramsorkins, not both. - Top-level
paramNorkinNusage can implicitly create params up to that ordinal. - Top-level params are readable in executable code but are not writable from top-level event handlers.
Explicitly declared homogeneous params can be indexed directly:
params 4
sample:
out1 = params[0] + params[1]
params, kins, and dynamic param views are not first-class arrays. Use direct
params[i] or kins[i] access in block or sample code rather than assigning,
slicing, passing, returning, or storing the whole surface.
Outputs
outs declares sample-rate audio outputs. outputs is an alias.
outs:
out1
stereo: f32[2]
kouts declares block-rate control outputs.
kouts:
rms: f32
peak: f32
Shorthand forms:
outs 2
kouts<f32> 4
outs<f64>:
left
right
Rules:
- Omitted output types default to
f32, or to the section default. outs Nexpands toout1..outN;kouts Nexpands tokout1..koutN.- Using
outNwithout anoutsblock implicitly creates a sample-ratef32output. - Using
koutNwithout akoutsblock implicitly creates a block-ratef32control output. - Top-level
outsandkoutsnames must be disjoint. - Numbered
outNnames are audio outputs; usekoutNfor numbered control outputs. outs[i] = expris valid in sample-rate code when explicit outputs form one scalar type.kouts[i] = expris valid in block-rate code when explicit control outputs form one scalar type.- Dynamic output indices are 0-based and clamped.
External Buffers
buffers declares host-bound buffers.
buffers:
src: buffer[f32]
bus: buffer[f32[2]]
any_bus: buffer[f32[]]
Inside a buffers block, shorthand element forms are accepted:
buffers:
mono: f32
stereo: f32[2]
dyn: f32[]
Shorthand forms:
buffers 2
buffers[f32]:
delay
scratch
Buffer access and metadata:
sample:
mono0 = src[0]
left0 = bus[0][0]
n = src.len()
c = bus.chans()
sr = src.samplerate()
Non-clamping access exists as both methods and free functions. Despite the
historical unsafe_ spelling, these operations trap on an invalid index; only
compiler-proven accesses may be unchecked in MIR:
sample:
a = src.unsafe_read(i)
src.unsafe_write(i, a)
b = bus.unsafe_read2(ch, i)
bus.unsafe_write2(ch, i, b)
Rules:
buffers Nexpands tobuf1..bufN.- Explicit declarations and count shorthand cannot currently be mixed in one
buffersblock. - Runtime binding validates element type and channel constraints.
- A bound buffer always has a non-null pointer, positive frame and channel counts, and a finite positive sample rate. Hosts must not process while a declared buffer is unbound.
- Binding with a zero sample rate unbinds the buffer; the pointer and dimensions are ignored.
- Primitive buffer slices are supported with the same slice syntax as arrays.
Top-Level Events
Top-level events are host-triggered handlers. They are useful for musical gestures, one-shot control changes, and stateful commands.
init:
freq_state = 440.0
amp_state = 0.0
gate = false
events:
note_on(freq_hz = 440.0, amp = 1.0):
freq_state = freq_hz
amp_state = amp
gate = true
note_off():
gate = false
Singular event sugar:
event bang():
gate = true
This is equivalent to an events: block with one event. Singular event ...
declarations and an events: block can be mixed in the same owner.
Supported top-level event parameter types:
- Primitive scalars.
- Fixed-size primitive arrays:
T[N]. - Read-only primitive slices:
T[].
Rules:
- Event params without explicit types default to
f32. - Defaults work for scalar and fixed-size array params.
- Fixed-array and slice params are read-only in handlers.
- Top-level events run immediately on the audio thread.
- Handlers cannot write inputs, outputs, or top-level params.
- Top-level handlers may write only existing top-level state rooted in
init. - Unknown top-level event indices are ignored at runtime.
- A known top-level event with the wrong payload size is a runtime error.
- Top-level host events with slice params use payload layout
i32 lenfollowed by contiguous element bytes.
4. Executable Sections
Executable sections determine when code runs. Learn them in this order:
init creates state, sample produces or processes audio, and block wraps
sample code with per-block work.
init
init runs when an instance is created or reset. It creates persistent state
and usually constructs structs and processors.
init:
phase = 0.0
gain = 0.5
taps: f32[8]
Typical uses:
- Create persistent scalar state.
- Create arrays and tuples.
- Construct structs.
- Construct proc instances.
- Perform one-time setup.
Section default scalar types are supported:
init<f64>:
phase = 0.0
last = 0.0
Rules:
- A fresh top-level scalar assignment in
initintroduces persistent owner state. - Assigning to an already visible state symbol updates that state.
constdeclarations are allowed insideinit.- Declaration order is lexical.
- A fresh assignment inside nested control flow in
initis local to that flow, not persistent state.
init:
if ready:
tmp = 1.0
else:
tmp = 2.0
carried = tmp
Here tmp is local to the init flow, while carried becomes persistent state.
sample
sample is the per-sample executable scope. It is the most direct way to write
audio-rate code.
sample:
out1 = in1 * gain
Rules:
- Fresh assignments in
samplecreate locals. sampledoes not introduce new persistent owner state.returnis valid indefbodies, not in top-levelsample.- Input/output surfaces are available in
sample.
Oversampled sample
Once a normal sample block is clear, you can oversample it with sample N:.
sample 4:
out1 = tanh(in1 * 8.0)
Rules:
- Supported factors are
1,2,4,8,16,32,64,128,256, and512. sample:is equivalent tosample 1:.- The factor can be any compile-time integer expression resolving to a supported factor.
- Audio input reads are interpolated across oversample substeps.
- Params are control-rate boundaries and are held within the base sample.
- Outputs are filtered and decimated back to the base rate.
SRinside oversampled code is the effective sample rate.HOST_SRand its aliases always mean the host sample rate.BSremains the logical host block size.
Generated signal code runs at the rate of the scope that evaluates it. For example, a host-rate oscillator feeding an oversampled distortion proc is evaluated once per host sample, then interpolated into the distortion proc. An oscillator evaluated inside the oversampled scope runs at the oversampled rate.
block
block runs once per host audio block. It is useful when a value should be
computed once per block rather than once per sample.
block:
incr = freq * TWO_PI / SR
sample:
phase = phase + incr
if phase > TWO_PI:
phase = phase - TWO_PI
out1 = sin(phase)
You can think of a block with audio outputs as three regions:
- Block-pre statements before the nested
sample. - The nested per-sample
sample. - Block-post statements after the nested
sample.
Rules:
- With sample-rate outputs, a
blocksection must include a nestedsample. - Top-level statements before nested
sampleare block-pre code. - Statements after nested
sampleare block-post code. - Fresh top-level assignments in block-pre introduce block-carried owner state visible to later
sampleand block-post code. - Fresh top-level assignments in block-post are visible only after that point.
- Fresh nested assignments inside
if,for, andwhilestay local. blockandsampleare mutually exclusive withgraphin the same owner.
kouts programs and processors use block without a nested sample, because
control outputs are block-rate values.
5. Types and Values
Primitive types:
f32f64i32i64bool
Compound types:
| Type | Example | Notes |
|---|---|---|
| Fixed array | f32[8] |
Length is compile-time. |
| Slice | f32[] |
Read-only or writable view depending on source and call usage. |
| Tuple | (f32, i32) |
Anonymous fixed-length heterogeneous value. |
| Buffer | buffer[f32], buffer[f32[2]], buffer[f32[]] |
Host-bound external data. |
| Struct | Voice, Box<f32> |
Nominal data type declared with struct. |
| Proc | Gain, Sine<f64> |
Stateful processing unit declared with proc. |
Numeric Literals and Casts
Numeric literals and pure numeric constant expressions begin without a source
machine width. During semantic analysis they retain the widest supported
literal representation until a concrete numeric context selects f32, f64,
i32, or i64.
A concrete context can come from an annotation, a function parameter or return
type, another concretely typed operand, an interface/state/array element type,
or generic specialization at a call site. Conversion happens once at that
boundary. Runtime arithmetic then executes at the selected width; Onda does not
silently evaluate an f32 expression through f64 intermediates.
When no context exists, first assignment uses Onda defaults:
sample:
x = 0.5 # f32
n = 5 # i32 when it fits, otherwise i64
Pure numeric expressions adapt directly to their surrounding context:
sample:
narrow: f32 = 0.0
wide: f64 = 0.0
a = narrow + 0.1 # f32 addition
b = wide + 0.1 # f64 addition
c = 0.1 # no context, so f32
Builtin constants such as TWO_PI have an f64 standalone type, but a pure
compile-time expression such as freq * TWO_PI / SR can convert directly into
an f32 context. This does not create an f64 runtime calculation followed by
an f32 truncation.
Use an explicit annotation or cast when wider runtime evaluation is intended:
sample:
wide = f64(narrow) * 0.1
count = i64(0)
Builtin Constants and Functions
Builtin constants:
| Constant family | Names | Type |
|---|---|---|
| Pi | PI, pi |
f64 |
| Two pi | TWO_PI, TWOPI, two_pi, twopi |
f64 |
| Effective sample rate | SAMPLE_RATE, SAMPLERATE, SR, sample_rate, samplerate |
f32 |
| Host sample rate | HOST_SR, HOST_SAMPLE_RATE, HOST_SAMPLERATE, host_sample_rate, host_samplerate |
f32 |
| Block size | BLOCK_SIZE, BLOCKSIZE, BS, block_size, blocksize |
i32 |
Builtin functions include:
sin cos tan tanh atan atan2 exp log sqrt pow abs fabs
floor ceil round trunc min max fma
Arrays and Slices
Fixed-size arrays can be state or locals:
init:
taps: f32[8]
sample:
coeffs = [0.5, 0.25, 0.125]
out1 = coeffs[0]
Primitive array and buffer slices use Python-style syntax:
sample:
a = buf[:]
b = buf[2:]
c = buf[:-1]
d = buf[1:-2]
Rules:
- Slice forms are
a[:],a[start:],a[:end], anda[start:end]. - Negative bounds are supported.
- Slice expressions lower to primitive slice views of type
T[]. - Buffer slicing also yields
T[]. - Struct-element arrays are not sliceable in the current implementation.
Writable slice assignment is statement-only:
sample:
values[1:-1] = 0.5
dst[:] = src[:]
Scalar fill writes the full target slice. Slice copy writes
min(dst_len, src_len) elements. Overlapping slice copies behave as if copied
through a temporary. Event payload arrays and slices are read-only.
Tuples
Tuples are anonymous fixed-length heterogeneous values.
sample:
pair = (1.0, 2.0)
mixed = (1.0, 42, true)
out1 = pair[0]
Rules:
- Type syntax is
(T1, T2, ...). - Maximum arity is 16.
- Nested tuples are not currently supported.
- Tuple element access uses compile-time integer indices.
- Tuple destructuring is supported:
(a, b) = (10.0, 20.0). - Tuples can be locals,
initstate,defparams and returns, and struct fields.
6. Statements and Expressions
Assignment and Declarations
First assignment infers a type:
sample:
x = 0
y = 0.0
Explicit declarations pin the type:
sample:
x: i64 = 0
Assigning to an existing visible symbol updates it. Assigning to a new symbol introduces a symbol according to the storage rules of the current scope.
Operators
Supported operators:
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, % |
| Comparisons | ==, !=, <, <=, >, >= |
| Logical | !, &&, || |
| Bitwise integer | ~, &, |, ^, <<, >> |
Bitwise operators accept i32 and i64. Mixed i32 and i64 operands widen
to i64. >> is an arithmetic right shift.
Control Flow
Supported forms:
if x > 0.0:
y = x
elif x < 0.0:
y = -x
else:
y = 0.0
for i in 0..8:
sum = sum + taps[i]
for i in 0..=8:
sum = sum + f32(i)
for i @ -1 in 10..0:
dst[i] = src[i]
loop 8:
sum = sum + taps[_]
while sum < 1.0:
sum = sum + 0.1
Rules:
for i in A..BexcludesB;for i in A..=BincludesB.@ STEPdefaults to1;@ 0is invalid.- Descending loops use a negative step.
loop Nis shorthand forfor _ in 0..N.- Loop variables are local to the loop body.
- Fresh symbols created inside loops do not escape the loop.
breakandcontinueare supported in loops.returnis valid indefbodies, not in top-levelsample.
7. Constants and Compile-Time Code
Use const for compile-time values:
const MaxVoices = 8
const Hop: i32 = BLOCK_SIZE / 2
const Scale: f32[3] = [0.5, 1.0, 2.0]
const MoreScale: f32[] = [0.25, 0.5, 1.0, 2.0]
Rules:
const NAME = exprandconst NAME: T = exprare supported.exprmust be compile-time evaluable.- Primitive const arrays are supported at top level and namespace scope.
const NAME: T[N] = [ ... ]declares a fixed-size const array.const NAME: T[] = exprinfers the concrete array length from the initializer.- Inferred-length const array initializers can be literals, existing const arrays, const-array slices, or array-returning
const defcalls. - Untyped scalar const declarations remain contextual compile-time numerics and preserve the widest supported literal representation until each use site selects a concrete scalar type.
- A typed const fixes its scalar type at the declaration. An untyped pure numeric const may
specialize directly to
f32in one context andf64in another. - Once a numeric expression is concretely typed, every runtime operation uses that width and observes that type’s normal rounding semantics. Use an explicit cast to request wider evaluation.
- Reassignment, forward references, recursion, and mutual recursion are rejected.
const def declares compile-time helper functions:
const def ramp() -> f32[4]:
values: f32[4]
for i in 0..4:
values[i] = f32(i) * 0.25
return values
const Ramp: f32[4] = ramp()
const def rules:
- Every
const defmust declare an explicit return type. - Params support primitive scalars, fixed-size primitive arrays, typed primitive slices such as
f32[], and untyped slices[]. - Typed slice params accept compile-time arrays of any length with the matching element type.
- Untyped slice params accept compile-time arrays of any length and primitive element type.
- Slice params support indexed reads and
.len(), but not indexed writes. - Array-returning bodies can use local fixed primitive arrays, indexed local-array reads/writes,
if,for,loop,return, pure builtin math, and calls to earlier visible const defs. - Scalar-returning const defs can be used by scalar const declarations.
- Fixed-array-returning const defs can be used by const array declarations.
Const arrays and const slices can be passed to ordinary runtime def array
params when the callee treats the param as read-only. Writes through the param,
aliases, unsafe_write, or forwarding to a mutable callee make the param
mutable and reject const-array arguments.
8. Functions with def
def declares reusable runtime functions.
def wrap_phase(p, upper = TWO_PI):
if p > upper:
return p - upper
return p
Supported features:
- Positional arguments.
- Named arguments.
- Default values.
- Early return.
- Optional explicit return type annotations with
->. - Multi-line argument lists with an optional trailing comma.
- Method-style sugar for ordinary defs:
x.clamp01()rewrites toclamp01(x).
Examples:
def wrap_phase(p, upper = TWO_PI) -> f32:
if p > upper:
return p - upper
return p
def pair(x: f32, y: i32) -> (f32, i32):
return (x, y)
Return rules:
- A
defcan return a primitive scalar. - A
defcan return a tuple of primitive scalars. - A value-returning
defmust return a value on every reachable path. A return nested only in afororwhileloop is not sufficient because the loop may execute zero times. - Explicit annotations can use primitive scalars, tuples of primitive scalars, and generic primitive placeholders belonging to the current generic owner.
- Returning structs, arrays, or buffers is not supported.
- Return checking follows ordinary assignment rules: exact match and implicit widening are allowed; narrowing requires an explicit cast.
- Runtime def call graphs must be acyclic. Direct and mutual recursion are rejected because they do not provide a statically bounded realtime workload.
Top-level def bodies are lexical-local. Top-level runtime symbols such as
inputs, outputs, params, buffers, and init state are not in scope unless
passed explicitly.
Generic Defs
Runtime defs can declare type parameters:
def id<T>(x: T) -> T:
return x
def pair<T>(x: T, y: i32) -> (T, i32):
return (x, y)
The compiler monomorphizes generic defs from their call sites. Type arguments can often be inferred:
sample:
a = id(0.5) # T inferred as f32
b = id<f64>(1.0) # T provided explicitly
Rules:
- Generic def type args are restricted to
f32,f64,i32, andi64. boolis not allowed as a generic def type arg.- Generic type params can appear in scalar params, array params, buffer element params, locals, casts, and supported return annotations.
const defcannot declare type parameters.
Parameter Kinds
def params support:
- Primitive scalars.
- Explicit struct types.
- Typed arrays such as
arr: f32[]. - Untyped arrays such as
arr: []. - Typed buffers such as
buf: buffer[f32]. - Bare buffers such as
buf: buffer. - Generic struct and proc parameters specialized at the call site.
- Typed tuple params such as
p: (f32, i32). - Untyped tuple params inferred from the call site.
- Untyped structural params inferred from field and method use.
def sum(arr: f32[]):
total = 0.0
for i in 0..arr.len():
total = total + arr[i]
return total
def first(arr: []):
return arr[0]
def read_first(buf: buffer):
return buf[0]
Untyped parameters can be specialized structurally:
struct A:
x: f32
struct B:
x: f32
def read_x(s):
return s.x
read_x can be called with both A and B; the compiler specializes it from
the concrete argument shape and the field access in the body.
Overloads
Top-level defs and struct methods can be overloaded by arity and parameter types.
def sat(x: f32):
return x
def sat(x: f64):
return f32(x)
Resolution rules:
- Exact typed match wins first.
- If no exact typed match exists, numeric widening candidates may be used.
- Explicit typed params outrank generic or duck-typed params.
- Generic or duck-typed params outrank untyped params.
- Default arguments participate in overload matching.
- Return type is not part of overload selection.
- Equally valid candidates are a semantic error.
Proc-local defs are not overloadable. Runtime defs may still be generic with
syntax such as def id<T>(x: T) -> T; those generic defs are specialized from
their call sites.
9. Structs
struct declares nominal data types with fields and methods.
struct Voice:
phase: f32
sig: f32
def tick(self, hz):
self.phase = self.phase + hz * TWO_PI / SR
self.sig = sin(self.phase)
Supported features:
- Field defaults.
- Methods.
- Overloaded methods.
- Tuple fields.
- Generic structs.
Construction:
init:
a = Voice()
b: Voice = Voice()
c: Voice
Rules:
selfmust be the first method parameter.- Methods can read and write struct fields through
self. - Typed struct declarations are
init-only. - Declaration-only form such as
c: Voicedesugars to default-constructor initialization. - For generic structs, typed declarations require explicit type args when the type is still generic.
Indexed Struct-Array Field Access
For arrays of data structs, one inline field-access dot is supported:
sample:
gain = voices[i].level
tap = voices[i].taps[j]
Accepted forms:
base[idx].fieldbase[idx].field[fidx]
Deeper inline chains are rejected:
base[idx].field.otherbase[idx].field[fidx].other
Use an intermediate alias for deeper access:
sample:
v = voices[i]
gain = v.settings.level
Proc arrays use their own indexed forms such as voices[i].gain,
voices[i](...), and voices[i].note_on(...).
10. Processors with proc
proc is Onda’s reusable stateful processing unit. processor is an alias.
Everything in this chapter builds on the top-level sections introduced earlier,
but scoped to a reusable child processor.
proc Gain:
ins:
in1
params:
g = 1.0
outs:
out1
sample:
out1 = in1 * g
A proc can contain:
insparamseventsandeventbuffersoutsorkoutsinitblocksamplegraph- Proc-local
defhelpers
In practice, init and events are optional, and a proc normally has one
execution body: sample, block, or graph.
Proc Inputs, Params, Outputs, and Buffers
Proc sections use the same surface syntax as the top level, with these differences:
kinsis not valid inside a proc; proc parameter sections are alwaysparams.- A processor declares either
outsorkouts, not both. koutsprocessors useblockwith no nestedsample, cannot declareins, and cannot declaregraph.- Proc constructor arguments for params and buffers are named-only.
- Proc inputs are bound by positional proc call args or named input args.
proc StereoGain:
ins:
input: f32[2]
params:
gain: f32[2] = [1.0, 1.0]
outs:
out: f32[2]
sample:
out[0] = input[0] * gain[0]
out[1] = input[1] * gain[1]
Constructing and Calling Procs
Proc instances are usually created in init:
init:
g = Gain(g = 0.5)
Call and access forms:
sample:
y = g(in1) # single-output scalar sugar
z = g(in1).out1
g.g = 0.25
out1 = g.out1
Rules:
- Positional proc call args bind inputs only.
- Named call args can bind inputs or params.
- Named param args store the clamped param value before the call runs.
- Generic procs specialize on construction.
- Multiple proc calls in one expression are evaluated in source order.
- Named param args are not supported inside logical
&&/||expressions orwhileconditions. - For
koutsprocs, usekout1or named control outputs.
Pinned Params
Use pin when a proc param should be initialized and updated only through that
proc’s controlled code path.
proc Filter:
params:
pin cutoff = 1000.0
pin q = 0.707
Pinned params:
- Can be set by the constructor.
- Can be set by the builtin proc
init(...)event. - Can be read or assigned by the owning proc’s own
init,sample,block,event, or proc-localdefbodies. - Cannot be accessed directly from outside through
child.cutoff,child.cutoff = ...,child.coeffs[i],child.coeffs[i] = ..., orchild(cutoff = ...). - Cause external dynamic
child.params[i]access to be rejected for that child proc.
pin is a reserved keyword. It is only valid as a proc-param prefix.
Param Update Hooks
A primitive scalar proc param can bind a proc-local update hook with
=> hook_name.
proc Voice:
params:
freq = 440.0 {20.0, 20000.0} => update_freq
init:
phase_inc = 0.0
def update_freq():
phase_inc = freq / SR
sample:
out1 = 0.0
Hook rules:
- The hook target must be a zero-parameter proc-local
defin the same proc. - The hook must have no explicit return type and no
return. - Hooks run after the param store and range clamp.
- Construction and builtin
init(...)run hooks after the procinitbody, in param declaration order. - Hooks are immediate per-param reactions; they are not batched.
- Hooks may read owner params, update init-rooted state, and assign named params on child procs.
- Hooks cannot assign owner params, inputs, outputs, child proc I/O or internal state, child dynamic
params[i], or call child events. - If a proc has bound params, dynamic
params[i] = ...assignments are rejected; assign the named param instead.
Use hooks for single-param derived state. Use an explicit proc event or setter when several params should rebuild shared state once.
Proc Events
Proc events are receiver-only commands called on a proc instance. They are useful for reset, note, trigger, and setter style APIs.
proc Env:
params:
amp = 0.0
event note_on(v: f32):
amp = v
sample:
out1 = amp
Proc-event rules:
- Calls use receiver syntax such as
voice.note_on(...). - Proc-event calls are statements, not expressions.
- Unqualified calls never resolve to proc events.
- A proc cannot call its own event handler as an internal subroutine; put shared logic in a proc-local
def. - Proc handlers may write proc state rooted in
initand proc params. - Proc handlers cannot write inputs or outputs.
- Generic proc events can use generic primitive placeholders such as
T,T[N], andT[].
Every proc also gets a reserved builtin init(...) event. It mirrors the proc
params in declaration order, assigns provided values into params, reruns that
proc instance’s init, then runs bound param hooks. Omitted args use defaults.
voice.init(0.5)
voice.init(gain = 0.5)
voices[i].init(freq = 220.0, amp = 0.1)
Proc-Local Defs
Processors can declare private helper defs that implicitly see proc state.
proc Filter<T>:
ins<T> 1
outs<T> 1
init:
state: T = 0.0
coeff: T = 0.5
def reset_state():
state = T(0.0)
def apply(x: T):
state = state + (x - state) * coeff
return state
event reset():
reset_state()
sample:
out1 = apply(in1)
Rules:
- Proc-local defs are private to the enclosing proc.
- They can be called from proc
init,block,sample,events, and other proc-local defs. - They can read and write proc state directly, without
self. - They support params, defaults, named args, and returns like normal defs.
- Recursive and mutually recursive proc-local defs are rejected.
- Proc-local defs are not overloadable.
Proc Arrays
Arrays of proc instances are supported in init.
init:
voices: Voice[4] = Voice()
Supported forms:
- Literal array construction:
voices: Voice[2] = [Voice(), Voice()]. - Broadcast constructor sugar:
voices: Voice[4] = Voice(). - Compile-time capacity expressions in the array length.
Indexed proc-array operations:
sample:
voices[i](freq)
out1 = voices[i].out1
voices[i].gain = 0.5
voices[i].note_on(220.0)
Rules:
- Runtime indices are clamped to the valid slot range.
- Aliasing such as
v = voices[i], thenv(...), is supported. - Proc-array buffer refs are refreshed on the safe
process_checkedpath. - A proc cannot directly instantiate its own type in its own state.
If the proc defines a block section, indexed proc-array calls use active-slot
block-hook semantics: block-pre runs lazily on the first () call to that slot
in the current block, and block-post runs once at block end for each called slot.
Plain slot retrieval does not trigger hooks.
11. Graphs
graph gives you a declarative way to wire processor instances and signal flow.
proc GainProc:
params:
gain = 1.0
sample:
out1 = in1 * gain
init:
p = GainProc()
graph:
in1 >> p.in1
3.0 >> p.gain
p.out1 >> out1
import std/osc
params:
freq = 220.0 {20.0, 20000.0}
mod = 100.0 {0.0, 1000.0}
init:
sine = std::osc::Sine()
graph:
@sample freq + sine.out1 * mod >> sine.freq
sine.out1 >> out1
Supported edge forms:
src >> dst
dst << src
@block src >> dst
@sample src >> dst
src >>[expr] dst
src >> { a, b }
{ a, b } << src
Rules:
graphis mutually exclusive withsampleandblockin the same owner.initmay be used withgraph.- Proc instances used as graph nodes are typically created in
init. - Unannotated edges targeting proc params default to
@block. - Unannotated edges targeting other destinations default to
@sample. @samplecan override the default@blockbehavior for proc param destinations.- Delayed edges use
>>[expr]or<<[expr]. - Delay expressions must be compile-time nonnegative integers.
- Delayed edges are sample-rate only.
- Each destination has one writer.
- Fan-out is allowed.
- Cycles are rejected unless a positive sample delay breaks the cycle.
- Proc nodes are stepped implicitly according to graph reachability and topological order.
- Inspect lowering with
onda compile <file> --dump-graph.
Current graph sources include:
- Top-level inputs and params.
- Proc outputs.
- Proc-array slot outputs.
- Array literals such as
[a, b]. - Indexed reads, sliced reads, and whole-array reads.
- Arithmetic and logical expressions built from supported graph sources.
- Element-wise array expressions when the final shape matches the destination.
Current legal destinations include:
- Top-level outputs.
- Proc inputs.
- Proc params.
- Proc-array slot inputs and params.
Type and scheduling rules:
- Graph edges use strict shape matching.
- Scalar-to-fixed-array broadcast is allowed.
- Proc inputs, params, and outputs are legal graph endpoints.
- Bare proc instances and proc-array slots can route into destination sets.
- Destination sets zip by output order when counts match.
- Single-output procs broadcast to destination sets.
- Otherwise, mismatched bundles are semantic errors.
Current graph limits:
- User-defined function calls and proc calls are not supported inside graph source expressions.
- Array-constructor expressions such as
f32[2](...)are not graph sources. - Graph event propagation syntax does not exist; use ordinary
eventsoreventdeclarations.
Example with proc bundles:
graph:
reverb >> { out1, out2 }
voices[0] >> { left, right }
12. Generics and Compile-Time Parameters
Onda has two complementary compile-time generic mechanisms:
- Type generics on
structandproc, such asT. - Integer namespace params, such as
N, for counts, sizes, and arity.
Type Generics
Generic structs and procs are monomorphized. The compiler creates a concrete specialized copy for each type combination your program uses.
struct Pair<T>:
a: T
b: T
proc OnePole<T>:
ins<T> 1
outs<T> 1
init:
state: T = 0.0
sample:
state = state + (in1 - state) * 0.1
out1 = state
Specialization:
init:
a = Pair<f32>()
b = Pair<f64>()
lp = OnePole() # unresolved constructor type params default to f32
hp = OnePole<f64>()
Rules:
- Generic type args are restricted to
f32,f64,i32, andi64. boolis not allowed as a generic type arg.- Unresolved generic type params in declaration and type positions are errors.
- For untyped constructor assignments only, unresolved constructor type params default to
f32. T(expr)rewrites to the bound primitive cast.T[]is valid for method anddefarray params where a primitive slice is valid.- Typed generic locals such as
x: T = ...are supported in executable scopes.
Def Monomorphization
Runtime defs are specialized from call sites. This applies both to explicit
generic defs such as def id<T>(x: T) -> T and to polymorphic parameter shapes
that do not need a named type parameter:
- Untyped arrays such as
arr: []. - Bare buffers such as
buf: buffer. - Generic struct and proc params supplied by concrete arguments.
- Untyped tuple params inferred from tuple literals.
- Untyped structural params inferred from field or method usage.
def first(arr: []):
return arr[0]
def id<T>(x: T) -> T:
return x
sample:
a = [1.0, 2.0]
b = [1, 2]
x = first(a)
y = first(b)
z = id<f64>(1.0)
Integer Namespace Params
Namespace template params are compile-time integers.
namespace DSP<Channels = 2>:
proc Gain<T>:
ins<T> Channels
outs<T> Channels
params:
gains: T[Channels]
sample:
for i in 0..Channels:
outs[i] = ins[i] * gains[i]
Use namespace integer params in:
- Fixed array sizes such as
T[N]. - Section counts such as
ins N,outs N,params N, andbuffers N. - Loop bounds.
- Compile-time expressions and namespace
assert(...)checks.
Instantiate inline or through aliases:
namespace Stereo = DSP<2>
init:
g = Stereo::Gain<f32>(gains = [0.5, 0.25])
13. Modules, Namespaces, and use
Imports
Use import to load another module:
import reverb
import std/osc
import std/filter
Rules:
import module/pathresolves asmodule/path.onda.- Each imported file is imported once.
- Built-in std modules are available under
std/.... - Imported files are declaration-only:
const,struct,def,proc,namespace, anduse. std/preludeis auto-imported during semantic analysis.
Current std modules include:
std/prelude std/math std/random std/complex
std/osc std/filter std/env std/delay std/data std/lookup
std/fft std/convolution std/gain std/levels std/mix
std/noise std/pitch std/smoothing
std/prelude currently imports std/math, std/lookup, and std/random.
Includes
include inserts another source file by quoted path:
include "shared/reverb.onda"
include "shared/util.on"
Rules:
- The path must be quoted.
- The path must end in
.ondaor.on. - Use
/path separators.
Namespaces
namespace groups declarations under a qualified path.
namespace my::dsp:
def sat(x):
return clamp(x, -1.0, 1.0)
sample:
out1 = my::dsp::sat(in1)
Namespace-local consts and nested namespaces are supported:
namespace Config:
const MaxVoices = 8
Templated namespaces take compile-time integer params with defaults:
namespace FFT<N = 256>:
assert(N > 0)
assert((N & (N - 1)) == 0)
Rules:
- Namespace template params require defaults.
- Args support positional and named forms.
- Args are normalized as
i32(...)at compile time. - Namespace-local
assert(expr)performs compile-time checks. <>is used for namespace instantiation, generic specialization, and section default type modifiers.[]is used for arrays, indexing, slices, and buffer/channel forms.
Namespace aliases:
namespace D = std::data<SR, 1>
init:
a = std::data<SR, 1>::Data<f64>()
b = D::Data<f64>()
Use Declarations
use brings namespace members into unqualified lookup. It does not load
modules; use import first when the target lives in another module.
import std/math
import std/random
import std/fft
use std::math
use std::random::Rng
use std::fft<512> as fft512
use std::fft<1024>::FFT as FFT1024
pub use std::lookup
init:
rng = Rng<f32>()
a = fft512::FFT<f32>()
b = FFT1024<f32>()
sample:
out1 = clamp(in1, -1.0, 1.0)
Forms:
use Namespacebrings direct declarations in that namespace into unqualified lookup.use Namespace::Symbolbrings one declaration into unqualified lookup.use Namespace as Aliascreates a namespace alias.use Namespace::Symbol as Aliascreates a symbol alias.pub use ...re-exports the use declaration through imports.
Rules:
asapplies only to the wholeusedeclaration.useis allowed at top level and insidenamespace.- Plain top-level
useis private to the source file where it appears. - Imported files expose only
pub usedeclarations to the importing file. - Fully qualified paths always work.
- Explicit
usecollisions are errors at unqualified use sites; qualify the name to disambiguate.
14. Reference Notes
Section Shorthands
| Section | Count shorthand | Default type shorthand | Generated names |
|---|---|---|---|
ins |
ins N |
ins<f64>: |
in1..inN |
params |
params N |
params<i32>: |
param1..paramN |
kins |
kins N |
kins<i32>: |
kin1..kinN |
outs |
outs N |
outs<f64>: |
out1..outN |
kouts |
kouts N |
kouts<f32>: |
kout1..koutN |
buffers |
buffers N |
buffers[f32]: |
buf1..bufN |
Section counts can use compile-time integer expressions, ordinary const values, and namespace integer template params.
Dynamic Surfaces
Direct indexed access is supported for explicitly declared homogeneous surfaces:
ins[i]
outs[i] = x
kouts[i] = x
params[i]
kins[i]
child.params[i]
These views are not first-class arrays. Do not assign, slice, pass, return, or
store ins, outs, kouts, params, kins, or child proc dynamic views.
Input/output surfaces are block/sample-bound. inN, outN, koutN, declared
I/O arrays, and synthetic I/O views cannot be read, written, passed, returned,
or stored from init, event, or top-level def bodies.
Aliases and Reserved Names
inputsaliasesins.outputsaliasesouts.processoraliasesproc.- Top-level
kinsaliasesparams. - Control-flow keywords are reserved:
if,elif,else,for,in,while,loop,break,continue,return, andassert. inseparates the loop variable from its range infor i in A..B; use names such asinputfor ports and variables.import,include,use,as,pub, andpinare reserved for their declaration and modifier syntax.trueandfalseare reserved boolean literals.- Numbered
outNnames are audio outputs; usekoutNfor numbered control outputs.
Common Current Limits
- Proc-local defs are not overloadable.
- Returning structs, arrays, or buffers from runtime
defis unsupported. - Struct-element arrays are not sliceable.
graphsource expressions cannot call user-defined functions or procs.graphhas no event-routing syntax.