Processor API and ABI reference
Use the Processor API to run a compiled Onda processor directly in a native application or a
WebAssembly runtime. Your host owns storage, audio scheduling, and event delivery. Native objects
require the header
onda_processor_abi.h
and your platform linker; they do not require libonda.
For managed instance storage and checked native calls, use the C API. For browser audio playback, use the Web Audio adapter.
Build and host a processor
Compile a program to an object and its matching descriptor:
onda compile patch.onda --emit obj --output patch.o --meta-out patch.onda.json
Use a target spec or --target-triple when compiling for another platform. Link the object with your
application using that platform’s compiler and linker.
A host then:
- Reads the descriptor and verifies its versions and target.
- Allocates parameter and state storage with the reported sizes and alignments.
- Writes parameter defaults and prepares audio and external-buffer tables.
- Calls
onda_processor_initwithFULLand the current buffer bindings. - Calls
onda_processfor each block, checking its result before using the output. - Releases host-owned storage when processing is finished.
The direct native example includes a complete C host and link commands for Linux, macOS, and Windows. It reads sizes, offsets, and defaults from the descriptor rather than hard-coding them.
Versioned artifact descriptor
The paired JSON descriptor describes how to load and call the processor:
format: "onda-processor"andformat_versionfor the descriptor schema.abi_versionfor the logical entry-point and storage contract in this document.artifact_kind, currentlyrelocatable_objectorwebassembly_module.target, including the resolved triple, pointer width, byte order, pointer model, and calling convention.integration, including symbols that must survive integration and an artifact-specific profile.compile,exports,runtime, andmetadatafor specialization, symbol names, allocation, and the program interface.
Hosts must reject unknown descriptor or ABI versions. Use the descriptor from the same compilation as the object or module; storage layouts can differ between targets and compilations.
Logical processor interface
The signatures below use Ptr, an abstract pointer to host-owned storage. i32 arguments are
signed 32-bit values. Public LLVM entry points use the target’s C calling convention; complete core
WebAssembly modules use ordinary core-Wasm function calls.
onda_processor_init(
params: Ptr,
state: Ptr,
mode: InitMode,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
onda_process(
state: Ptr,
params: Ptr,
inputs: Ptr,
outputs: Ptr,
start_frame: i32,
frames: i32,
flags: i32,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
onda_event_N(
input: Ptr, // const EventInput*
params: Ptr,
state: Ptr,
buffers: Ptr,
buffer_frames: Ptr,
buffer_channels: Ptr,
buffer_sample_rates: Ptr,
output: Ptr,
) -> i32
Every packed event call requires an EventInput descriptor:
struct EventInput {
const uint8_t *payload;
uint32_t payload_bytes;
uint8_t *workspace;
uint32_t workspace_capacity_bytes;
};
It uses target C layout (16 bytes on wasm32, 32 bytes on native 64-bit targets). The descriptor itself is required even for an empty payload. Workspace starts at an eight-byte-aligned address; its capacity is provisioned by the host before realtime execution. Input, workspace, descriptor, state, parameters, and output storage are disjoint for the call. Native callers supply valid memory regions; wasm entry points also check regions against linear-memory bounds.
Before running a handler, the entry validates lengths, byte extents, exact input consumption,
alignment, and workspace capacity. Rejection returns status 2 with empty output batches and leaves
workspace and processor state unchanged. Accepted input is copied into aligned native tensors;
bools become zero or one and ranged integers are normalized before observation. Status 1 means
execution failed after entry and requires the usual instance recovery. Neither path allocates.
There is one onda_event_N for each declared event, in metadata order. Use exports in the
descriptor to find the entry points for the compiled processor.
Native relocatable objects also advertise one trusted onda_event_views_N entry per event through
the optional exports.event_views array. It has the same trailing arguments and result as
onda_event_N, but its first argument is a pointer to the schema-derived array below:
struct onda_processor_event_tensor_view {
const void *data;
int32_t element_count;
};
Each view is one contiguous, naturally aligned, native-endian primitive tensor in canonical
depth-first SoA leaf order. element_count counts primitive scalars; for a slice leaf it is the
slice length times that leaf’s fixed inner element count. The entry borrows every tensor only for
the synchronous call, resets call-scoped output, and neither allocates nor uses an event workspace.
It deliberately does not receive a tensor count or validate shape, alignment, canonical bool and
ranged-integer values, storage overlap, or the other processor pointers. Passing anything other
than the exact views and processor storage required by the paired descriptor is undefined behavior.
Use onda_event_N at trust boundaries.
This entry is a native-object capability, not part of the WebAssembly profiles. Native hosts can
derive its flat tensor contract once from the event schema with
onda_processor_abi::payload::PayloadPlan; JavaScript hosts can inspect the equivalent
PayloadPlan.tensorMetadata. The metadata records expose each leaf’s path, primitive encoding,
fixed shape, owning top-level parameter, slice status, and fixed element count.
InitMode has two values: PRESERVE_PINNED = 0 and FULL = 1. Use FULL before processing new
storage. It clears state and runs initialization. Use PRESERVE_PINNED only after successful full
initialization to rerun initialization while retaining pinned state and suspended tasks, unless
authored init code changes them.
Initialization reads the current external-buffer bindings. Pass null for output if you do not
collect initialization prints. Delegate publication is not allowed during initialization.
Initialization is not transactional: a failure may leave state partially changed.
All entry points return an execution status:
| Status | Meaning and host action |
|---|---|
0 (OK) |
The call completed successfully. |
1 (RUNTIME_SAFETY_FAILURE) |
Stop processing, discard partial audio, and recover with full initialization or snapshot restoration. |
2 (INPUT_REJECTED, events only) |
Correct the payload or workspace capacity and retry; processor state remains usable. |
Call-scoped execution output
For language semantics, see delegates and printing. The host output guide covers sizing, ownership, decoding, and presentation across host APIs.
output is null when the host consumes neither occurrence stream. Otherwise it points to two
independently nullable pointers followed by the generated call-local counter:
delegate_batch: Ptr
print_batch: Ptr
next_sequence: u32
Each present pointer addresses an independent caller-owned batch with this physical shape; native targets use native pointers and complete wasm32 modules use little-endian 32-bit linear-memory offsets:
storage: Ptr
capacity_bytes: u32
used_bytes: u32
record_count: u32
overflow_count: u32
Every supplied batch descriptor and non-null storage region is mutually disjoint and does not overlap any other region accessed through the entry-point call. This includes parameter and state storage, audio pointer tables and samples, external-buffer descriptor tables and samples, packed event input and workspace, native event tensor views, and the execution-output descriptor itself.
The fixed header of every contiguous record is three u32 values followed immediately by payload
bytes. A delegate record stores declaration-order delegate index, payload byte count, and call-local
sequence. Delegate payloads use the recursive schema described below, in little-endian wire order.
Record headers retain the artifact target’s byte order.
A print record stores log-site index, payload byte count, and the same call-local sequence. Its
payload contains only the site’s
primitive scalar arguments without padding: four bytes for f32/i32, eight for f64/i64, and
one zero-or-one byte for bool. metadata.source_files and metadata.log_sites supply labels,
source spans, lexical ownership, argument types, and fixed payload sizes. Scalar and header byte
order is the artifact target’s byte order.
For one fixed-shape occurrence, exact storage is the twelve-byte record header plus the descriptor’s
payload_size_bytes. A dynamic delegate has no exact pre-execution size; its descriptor reports
payload_min_size_bytes, including each slice’s four-byte length prefix but no slice elements.
There is no exact whole-batch size because occurrence counts, delegate selection, and slice lengths
may depend on runtime control flow. Capacity is a host policy, and overflow_count reports when it
was insufficient.
Before every init or process entry, the host resets the counters of each supplied batch and resets
next_sequence to zero. An input-event entry performs that reset itself before preflight, so
rejected input returns empty batches. Publications into either present batch consume the
shared counter. A complete record is appended only when it fits. Otherwise it is discarded whole
and that batch’s overflow counter saturates at u32::MAX; a later smaller record may still fit. Null output,
batch, or storage is neutral and does not count overflow. Generated execution failure clears
delegate results but retains print records and overflow already produced, because they may diagnose
the failure. Storage
is caller-owned, never allocated or retained by the processor, and is not part of snapshots. Hosts
that expose a combined log merge the two decoded batches by sequence; sequences have no meaning
across separate entry calls.
Pointer and target profiles
For a native LLVM object, Ptr is a real pointer with the width, byte order, data layout, and C ABI
selected by LLVM from the resolved target triple and target options. The application owns storage,
linking, symbol visibility, and any platform runtime dependencies.
For an LLVM WebAssembly relocatable object, Ptr is the target’s linear-memory offset type. A
wasm32 object therefore lowers Ptr to a 32-bit offset. The application linker owns final memory,
symbol export, and module policy.
For a complete wasm32 module emitted by Binaryen, Ptr is an unsigned i32 byte offset into the
module’s exported linear memory. The module exports memory and an immutable i32 __heap_base;
the host allocates at or above that address and grows memory before creating views. Memory growth
invalidates JavaScript typed arrays and DataView instances.
The target triple is a code-generation choice, not a different processor ABI. It selects such facts as instruction set, pointer size, C calling convention details, object representation, relocation model, and physical alignment. The descriptor records the resolved facts that an embedding host needs but does not replace a target SDK, sysroot, linker, or platform ABI documentation.
Relocatable-object integration
Onda emits relocatable objects; the application links them into an executable or module.
integration.required_symbols lists processor symbols the final link must retain.
integration.link_libraries, when present, lists required native libraries. Vector-math helpers
are included in the object; no separate SLEEF library is required. A destination toolchain and
platform math library may still be needed, such as -lm on Unix.
For native_relocatable_object, link the object with your application normally.
For webassembly_relocatable_object, create a module with no _start, retain the processor
exports, and export its linear memory. The profile declares no_entry: true and
export_memory: true; exact linker flags depend on your toolchain. A relocatable Wasm object
cannot be passed directly to WebAssembly.instantiate.
Direct native use
include/onda_processor_abi.h is the canonical C declaration of the current ABI entry points. An
application links the emitted object, allocates storage from the exact paired descriptor, builds the
input/output and external-buffer pointer tables, optionally prepares an
onda_processor_execution_output_t containing independently allocated delegate and print batches,
resets it immediately before entry, and calls onda_processor_init, onda_process, and any
onda_event_N functions directly. Native descriptors may additionally advertise
onda_event_views_N; onda_processor_event_tensor_view_t and
onda_processor_event_views_fn declare that trusted entry. No Onda runtime or compiler library is
required.
The application must reject descriptor/ABI versions it does not implement and must verify that the descriptor’s target, pointer width, byte order, and calling convention match the linked process. It also owns its audio-thread floating-point environment, scheduling, validation, snapshots, and final linker policy.
Complete core-WebAssembly integration
An artifact with kind webassembly_module is ready for WebAssembly.instantiate. Its integration
profile names its memory and __heap_base exports and declares any imports. Current complete
modules are self-contained and can be instantiated with an empty import object ({}).
The host still allocates processor storage and calls the entry points below. The Web API provides artifact validation and playback helpers.
Storage and initialization
The host allocates non-overlapping parameter and physical-state regions using the sizes and minimum
alignments in runtime. It initializes parameter defaults from program metadata and calls
onda_processor_init(params, state, FULL, buffers, buffer_frames, buffer_channels,
buffer_sample_rates, output) before processing. The four buffer tables describe the bindings
current for this initialization call and follow the same rules as process and event calls. Pass
null for output when initialization output is not consumed. Physical state uses the backend’s
selected target layout and is otherwise opaque.
Honor the descriptor’s reported alignment, including when cloning state. Do not infer physical storage size or padding from the declared Onda types.
Hosts may replace buffer descriptors between entry-point calls. The next init, event, or process call observes the replacement; rebinding alone does not execute initialization or recompute state previously derived from a buffer. Supplying bindings to init is intended for one-time preprocessing when the host already owns the source buffers before processing begins.
State-backed control outputs and persistent snapshot entries expose their physical offsets in the artifact descriptor. Scratch state is deliberately absent from snapshots.
Snapshot every entry in metadata.states, including entries with authored: false, so suspended
tasks can resume correctly. Use authored only when deciding which state to display to users.
Portable snapshots
The packed persistent-state snapshot is target-independent. The current snapshot format encodes persistent scalar elements in little-endian byte order, in metadata order, without physical padding or scratch state. It includes pinned authored roots and compiler-owned task frames. This is distinct from the target-native physical state image, which can use another byte order or alignment.
Restore begins with onda_processor_init(params, state, FULL, buffers, buffer_frames,
buffer_channels, buffer_sample_rates, NULL), using the current bindings, then overlays every
persistent entry from the packed snapshot. Restore initialization output is intentionally
suppressed. This resets instance scratch while preserving persistent state and task continuations.
A host converting between a big-endian physical target and the portable snapshot must encode
and decode each scalar according to metadata rather than copying physical bytes wholesale.
For state with integer_range metadata, normalize every restored element into the inclusive
min..=max interval using its declared clamp or Euclidean wrap mode. This also applies to every
element of a fixed array.
Audio ports
inputs and outputs point to tables of Ptr, one entry per flattened audio channel in metadata
order. Each channel points to a full compile-block scalar array. A null table is valid when the
corresponding flattened channel count is zero.
Audio scalar width belongs to the Onda interface. A platform adapter may convert its native audio format—for example, Web Audio f32 planar channels—to the declared scalar width. Such conversion is adapter behavior, not processor code generation.
Absent surfaces and null pointers
The paired descriptor is the authority for whether a pointer argument has storage behind it. A raw host passes null exactly when the corresponding surface is absent:
paramswhenruntime.param_size_bytesis zero;statewhenruntime.state_size_bytesis zero;inputsoroutputswhen the corresponding flattened metadata slot count is zero;- all four external-buffer table pointers when
metadata.buffersis empty; EventInput.payloadwhen its byte count is zero, and workspace when its required size is zero. TheEventInputdescriptor itself is always present.
A declared surface is not absent merely because the application does not use it. Every declared input/output slot requires valid compile-block storage. A non-empty buffer declaration list requires all four parallel tables. Each descriptor has positive frame/channel counts and a finite positive sample rate. A null sample pointer denotes an unbound buffer; generated code redirects reads to zero and discards writes. Give unbound buffers one frame, retain the declared channel count for exact-channel buffers, and use one channel for dynamic-channel buffers.
Segmented processing
An artifact is specialized for compile.block_size. A host may divide one logical compile block
into calls using start_frame and frames; the range must remain inside the compile block.
flags & 1isBEGIN_BLOCKand gates the program’s beginning-of-block work.flags & 2isEND_BLOCKand gates its end-of-block work.
The flags do not maintain a hidden cursor. Zero-frame calls are valid. A host whose callback size
differs from the compile block owns a cursor and splits callbacks at logical block boundaries. Audio
pointer tables continue to address complete compile-block storage; generated code derives logical
audio indices from start_frame.
Parameters, events, delegates, buffers, and control outputs
Parameter storage follows the paired descriptor’s offsets and scalar shapes. Every event and
delegate also carries a recursive schema: named parameters with scalar, primitive tuple, nominal
struct, fixed array, or top-level slice types. Fields include constant defaults and integer domains.
Use the schema to encode and decode host values.
Wire payloads are packed little-endian in parameter and depth-first field order. Every primitive
leaf is one contiguous tensor with outer array axes before inner field-array axes. A runtime slice
contributes one signed i32 logical length, then its field tensors; there is no padding or separate
length per struct field. Prepared workspace inserts scalar alignment padding and uses native byte
order. Its required size is computed by the shared payload planner, independently of wire size.
Use Rust’s onda_processor_abi::payload::PayloadPlan or JavaScript’s PayloadPlan to encode logical
values, calculate workspace sizes, and inspect tensor shapes. Prepare payloads and reserve storage
before realtime execution. See the C event API or
Web API for managed dispatch.
Every parameter has range_min_repr, range_max_repr, and
param_control. param_control is null for a parameter without a numeric host-control domain;
otherwise it contains:
scale:linearorlog;curve: an optional finite SuperCollider-stylelincurvevalue, mutually exclusive withscale = log;unit: optional display text;step_repr: the optional plain-domain step encoded in the declared scalar representation;step_count: the number of equal intervals between the inclusive endpoints.
Optional enumeration metadata describes scalar and fixed-array parameters/control outputs and
scalar leaves of event/delegate schemas. It contains a qualified name and
ordered members, each with a source name and display label. An enum’s member index is its
ordinary signed i32 representation. Its range is [0, members.length - 1]; enum parameters use
linear scale and unit step. A one-member enum has equal zero bounds and step_count: 0.
Enum values use i32 storage. Packed event preparation normalizes them through integer_range;
normalize them when restoring snapshots too.
The raw processor object does not export parameter conversion functions. Native hosts decode each
numeric control into the onda_processor_param_domain structure from
include/onda_processor_abi.h, whose header-only functions implement clamping, snapping, and
plain/normalized conversion without linking the Onda runtime. The structure carries the declared
scalar type so stepped floating-point grids are validated at their actual storage precision. The
reference generator in
examples/native/raw_processor_object emits decoded tables, indexed wrappers, typed reads, and
typed writes around that shared header implementation.
Parameter conversion rules
For a scalar numeric parameter, normalized-to-plain conversion is:
- Map NaN to zero and clamp the normalized input to
[0, 1]. - Return the exact range endpoint for normalized zero or one.
- If
curveis present, transformnwith the SuperCollider-stylelincurvemapping, then applymin + n * (max - min). Otherwise apply that linear mapping directly forlinear, or the overflow-safe equivalentexp(log(min) + n * (log(max) - log(min)))forlog. - Map a plain NaN to
min, then clamp the plain value to the inclusive range. - For a stepped domain, snap to
min + round((plain - min) / step) * stepand clamp again. - Convert to the declared scalar width when writing parameter storage.
Plain-to-normalized first performs the same plain clamping and step snapping, preserves exact
endpoints, then applies the inverse curved, linear, or logarithmic mapping. Boolean plain and
normalized host-control values use the threshold value >= 0.5 and store one byte containing zero
or one. Fixed parameter arrays retain their logical name, type, length, and per-element defaults.
Their numeric range and control metadata apply independently to each element, addressed in storage
at byte_offset + index * element_size_bytes. Unranged numeric parameters have no normalized
host-control domain.
Because the shared host-control surface uses binary64 values, an i64 control domain and its
range width are restricted to the exactly representable integer interval
[-9007199254740991, 9007199254740991]. This restriction does not apply to unranged i64
parameters written through their typed/raw storage representation.
External buffers use four parallel tables in declaration order:
buffers: writable sample pointers, with null entries denoting unbound buffers.buffer_frames: i32 frame counts.buffer_channels: i32 channel counts.buffer_sample_rates: f32 sample rates.
All four descriptor tables remain immutable for an entry-point call and do not overlap parameter, state, audio, or external-buffer sample storage. The non-overlap rule is between descriptor-table storage and the regions reached through non-null host storage pointers.
Fixed resource arrays occupy contiguous physical slots. metadata.buffer_arrays records each
logical group name, its first physical slot, and its length, so hosts can bind a whole bank without
parsing generated slot names. Each slot has its own metadata.buffers[first + slot].may_write
value. Supply writable sample memory whenever that value is true.
Samples use interleaved frame-major storage. Metadata declares scalar width, read/write access, and mono, static, or dynamic channel constraints. Every sample-rate entry is finite and positive, and every non-null pointer, frame count, and channel count denotes nonempty prepared storage. Null buffer entries use processor-owned zero and discard storage. Control outputs are state-backed values at declared physical offsets and may be observed between processor calls.
Numerical and failure behavior
Strict compilation preserves declared scalar widths, NaNs, signed zero, rounding behavior, and
one-rounding FMA semantics. Fast math is an explicit compilation policy recorded by the artifact.
Native floating-point control registers belong to the calling thread. Onda’s realtime hosts enable
x86 FTZ/DAZ before entering init, process, or event code to prevent subnormal feedback-state stalls;
a raw-object host that wants the same audio policy must configure its calling threads likewise.
Bounds checks, integer division, and other generated safety checks return
RUNTIME_SAFETY_FAILURE instead of trapping. Invalid host pointers, storage extents, or other
violations of the raw ABI remain outside generated-code recovery and can still trap or cause
undefined behavior. RUNTIME_SAFETY_FAILURE means execution may have partially changed state or
output; a host must stop using that processor state until full initialization or snapshot restoration
establishes valid state again, and audio hosts discard partial output. INPUT_REJECTED completes no
handler work, preserves processor state, returns empty output batches, and leaves the processor
usable after the host corrects its payload or workspace capacity.
Web Audio reference adapter
Use @onda-lang/webaudio to connect a
complete WebAssembly artifact to an AudioContext. It handles audio scheduling, parameter and
event messages, buffer bindings, and snapshots. Configure dynamic event capacity before playback;
oversized events are rejected rather than growing memory during rendering.
C header reference
The release SDK installs include/onda_processor_abi.h and this document together. The header is
self-contained and header-only except for the processor-specific onda_processor_init,
onda_process, generated onda_event_N, and optional native onda_event_views_N symbols supplied
by the compiled object. Include it from C or C++; no libonda linkage is required to call a
processor object.
The public declarations fall into four groups:
- ABI versions, execution results, initialization modes, and segmented-processing flags.
- Function-pointer signatures and the generated init, process, packed-event, and native event-view entry points.
- Caller-owned delegate, print, execution-output, occurrence, and cursor records.
- Inline batch iteration and parameter-domain validation/conversion helpers.
The inline batch iterators validate record boundaries before returning a payload view. Each next
call returns 1 for a record, 0 only at the exact end of a valid batch, and -1 for invalid or
malformed input. Sequential iteration with onda_processor_delegate_batch_next or
onda_processor_print_batch_next is linear in record count and constant-space. The random-access
convenience functions validate the complete batch and return 0 only when the requested index is
absent; repeated indexed iteration is therefore quadratic, so use a cursor when consuming a whole
batch.
All parameter conversion helpers are allocation-free and constant-time. Prepare and validate a domain once when constructing host controls rather than validating descriptor text on the audio thread.
All processor C functions
onda_process
onda_processor_batch_next_record
onda_processor_delegate_batch_next
onda_processor_delegate_batch_reset
onda_processor_execution_output_reset
onda_processor_float_grid_value_matches
onda_processor_init
onda_processor_integer_domain_value_is_valid
onda_processor_lincurve_normalized_to_unit
onda_processor_lincurve_unit_to_normalized
onda_processor_linear_plain_to_unit
onda_processor_linear_unit_to_plain
onda_processor_param_constrain_plain
onda_processor_param_domain_is_valid
onda_processor_param_normalized_to_plain
onda_processor_param_plain_to_normalized
onda_processor_print_batch_next
onda_processor_print_batch_reset