Host API
Everything on this page is injected directly by runRenderCode (the host). It is loaded before libs/codes/*.js, and neither your code nor a lib can shadow it.
The signatures, ranges, units and sample code on this page are checked against the in-app contract (hostApiSpec) in CI. If the runtime and this page disagree, the build fails. Every sample runs as-is with no lib enabled — copy, paste, Render.
euclid() / scale() / lfo() / clamp() and friends are not host API: they come from factory.js (a lib). See Helpers.
Note output
Section titled “Note output”setNote()
Section titled “setNote()”setNote({ pitch?, velocity?, startBeat?, lengthBeats? }): void
Emits one note into the render. Every field is optional.
| Field | Required | Default | Range / unit |
|---|---|---|---|
pitch | optional | 60 (C4) | 0..127 (MIDI note number, int) |
velocity | optional | 100 | 0..127 (7-bit, int) |
startBeat | optional | CLIP_BEAT (current phase) | not clamped (beat) |
lengthBeats | optional | 1 | 0.001 or greater (beat) |
id is assigned automatically with crypto.randomUUID(). Calling it twice with the same pitch and beat records two separate notes.
Minimal
onInit(() => { setNote({ pitch: 60 });});Practical — layer an octave above every source note at velocity 70:
onNote((note) => { setNote({ ...note }); setNote({ ...note, pitch: note.pitch + 12, velocity: 70 });});[!CAUTION]
pitch/velocityare rounded to an integer and then clamped.lengthBeatsbottoms out at0.001— smaller values are raised to it (no silent notes).
Channel message output
Section titled “Channel message output”CC, Pitch Bend and Channel Aftertouch all ride the same lane machinery. For editing them on screen, see CC / Pitch Bend / Aftertouch automation.
setCc()
Section titled “setCc()”setCc({ ccNumber, beat?, value, curve? }): void
Emits one CC point. Repeated calls with the same ccNumber are merged into a single lane and sorted by ascending beat.
| Field | Required | Default | Range / unit |
|---|---|---|---|
ccNumber | required | — | 0..127 (int) |
beat | optional | CLIP_BEAT | not clamped (beat) |
value | required | — | 0..127 (7-bit, int) |
curve | optional | "linear" | curve kind (below) |
Minimal
onInit(() => { setCc({ ccNumber: 11, beat: 0, value: 64 });});Curves (P-2) — curve shapes the segment that ends at this point (how the value travels from the previous point to this one):
onInit(() => { setCc({ ccNumber: 1, beat: 0, value: 0 }); setCc({ ccNumber: 1, beat: 4, value: 100, curve: "scurve" });});curve | Shape |
|---|---|
"linear" | straight line (default) |
"hold" | holds A, then steps to B’s value at B.beat |
"exp" | ease-in (slow then fast, t**2.7) |
"expInv" | ease-out (fast then slow, the mirror of exp) |
"scurve" | smoothstep (flat tangents at both ends, symmetric) |
[!TIP] Repeated
setCcon the sameccNumberaccumulates into one lane’s event list. DifferentccNumbers produce separate lanes.
setCcResolution()
Section titled “setCcResolution()”setCcResolution(stepsPerBeat: number): void
Sets the onCc sampling grid resolution (how many ticks per beat).
| Argument | Required | Range / unit |
|---|---|---|
stepsPerBeat | required | 1..256 (onCc calls / beat) |
If you never call it, the resolution is 16 (≈ 1/64 note ≈ 31 ms @120 bpm). It only takes effect when called at top level or inside onInit; calls from onBeat / onNote / onCc are silently ignored (the grid is snapshotted just before the onCc sweep).
onInit(() => setCcResolution(32));
onCc(({ phase }) => { const value = 64 + 32 * Math.sin(phase * 2 * Math.PI); setCc({ ccNumber: 1, value });});[!CAUTION] When resolution × clip length exceeds 32,768 ticks, the render returns
Runtime error (onCc): grid sweep would emit ... ticks, exceeding cap. LowersetCcResolutionor shorten the clip.
setPitchBend()
Section titled “setPitchBend()”setPitchBend({ beat?, value, curve? }): void
Emits one Pitch Bend point. A clip has at most one Pitch Bend lane.
| Field | Required | Default | Range / unit |
|---|---|---|---|
beat | optional | CLIP_BEAT | not clamped (beat) |
value | required | — | 0..16383 (14-bit, center 8192) |
curve | optional | "linear" | same curve kinds as setCc |
Valid curve values are "linear" / "hold" / "exp" / "expInv" / "scurve".
onInit(() => { setPitchBend({ beat: 0, value: 16000 }); setPitchBend({ beat: 1, value: 8192, curve: "exp" });});[!CAUTION]
value: 0is fully bent down, not “no bend”. Pass8192to return to center.
setChannelAftertouch()
Section titled “setChannelAftertouch()”setChannelAftertouch({ beat?, value, curve? }): void
Emits one Channel Aftertouch point. Like Pitch Bend, one lane per clip.
| Field | Required | Default | Range / unit |
|---|---|---|---|
beat | optional | CLIP_BEAT | not clamped (beat) |
value | required | — | 0..127 (7-bit, int) |
curve | optional | "linear" | same curve kinds as setCc |
Valid curve values are "linear" / "hold" / "exp" / "expInv" / "scurve".
onInit(() => { setChannelAftertouch({ beat: 0, value: 0 }); setChannelAftertouch({ beat: 0.5, value: 100 }); setChannelAftertouch({ beat: 1, value: 0 });});Callback registration
Section titled “Callback registration”Register any number of callbacks per phase. They run in registration order and their return values are ignored. Throwing aborts that phase (results from earlier phases are kept). See Lifecycle for what survives an error.
| Function | Callback argument | Call count |
|---|---|---|
onInit(fn) | none | once, at the start of the render |
onBeat(fn) | (beat: number) | clip length × stepsPerBeat (default 4/beat) |
onNote(fn) | (note: MidiNote) | number of source notes |
onCc(fn) | (ctx: { beat, time, phase }) | clip length × CC resolution (default 16/beat, setCcResolution()) |
onInit()
Section titled “onInit()”onInit(fn: () => void): void
Runs once at the start of the render. The right place to build a whole clip in one pass.
onInit(() => { setNote({ pitch: 60 });});onBeat()
Section titled “onBeat()”onBeat(fn: (beat: number) => void): void
Sweeps the step grid defined by stepsPerBeat, in time order. The beat argument is the position from the start of the clip.
onBeat((beat) => { if (beat % 1 === 0) { setNote({ pitch: 36, startBeat: beat, lengthBeats: 0.25 }); }});onNote()
Section titled “onNote()”onNote(fn: (note: MidiNote) => void): void
Hands you each source note from the piano roll, in ascending startBeat order.
onNote((note) => { setNote({ ...note }); setNote({ ...note, pitch: note.pitch + 12, velocity: 70 });});onCc()
Section titled “onCc()”onCc(fn: (ctx: { beat: number; time: number; phase: number }) => void): void
Sweeps the fixed grid set by setCcResolution, whether or not the clip has input CC lanes. ctx.phase is beat / CLIP_LENGTH clamped to 0..1 — handy for one-cycle LFOs.
onInit(() => setCcResolution(32));
onCc(({ phase }) => { const value = 64 + 32 * Math.sin(phase * 2 * Math.PI); setCc({ ccNumber: 1, value });});Input data
Section titled “Input data”events
Section titled “events”events: MidiNote[]
The source notes placed in the piano roll tab. The clip’s clip.notes at render time, handed over read-only.
interface MidiNote { id: string; pitch: number; // 0..127 velocity: number; // 0..127 startBeat: number; lengthBeats: number;}Ordering follows the edit order under the legacy contract v1 (only onNote’s argument is sorted by startBeat). Under contract v2 the array is canonically ordered, so reassigned IDs or a different editing order no longer change it.
onInit(() => { console.log("source notes:", events.length);});ccLanes
Section titled “ccLanes”ccLanes: ClipCcLane[]
The source channel-message lanes stored on the clip (drawn in the piano roll tab, or recorded from MIDI input).
interface ClipCcLane { id: string; messageType?: "cc" | "pitchBend" | "channelAftertouch"; // defaults to "cc" ccNumber: number; // 0..127 (unused for PB / CA) events: MidiCcEvent[]; // [{ id, beat, value, curve? }, ...]}onInit(() => { const expr = ccLanes.find((lane) => lane.ccNumber === 11); console.log(expr ? "CC11 events: " + expr.events.length : "no CC11");});sampleCc()
Section titled “sampleCc()”sampleCc(ccNumber: number, beat: number): number
Finds the input lane for ccNumber (0..127) and returns its linearly interpolated value at beat (a float — rounding and clamping are the caller’s call). The semantics match the Rust engine’s playback interpolation, so reading with sampleCc, adding movement and writing back with setCc keeps the numbers and what you hear in agreement.
| Situation | Return value |
|---|---|
no lane for ccNumber / empty events | 0 |
beat before the first point | first point’s value (hold-back) |
beat after the last point | last point’s value (hold-forward) |
several points on the same beat | the last one in array order |
onCc(({ beat, phase }) => { const base = sampleCc(7, beat); const wobble = 10 * Math.sin(phase * 8 * Math.PI); setCc({ ccNumber: 7, value: Math.min(127, Math.max(0, base + wobble)) });});[!CAUTION]
sampleCconly reads the inputccLanes. Values written withsetCcduring the same render are not readable back (chicken and egg).
samplePitchBend()
Section titled “samplePitchBend()”samplePitchBend(beat: number): number
Linearly interpolates the input Pitch Bend lane at beat. With no lane (or an empty one) it returns 8192 (no bend) — the one place the fallback is not 0, because 0 means fully bent down.
onCc(({ beat }) => { const bend = samplePitchBend(beat); setCc({ ccNumber: 1, value: Math.abs(bend - 8192) / 64 });});sampleAftertouch()
Section titled “sampleAftertouch()”sampleAftertouch(beat: number): number
Linearly interpolates the input Channel Aftertouch lane at beat. Returns 0 when the lane is absent or empty.
onCc(({ beat }) => { setCc({ ccNumber: 74, value: sampleAftertouch(beat) });});Time globals
Section titled “Time globals”All of these are clip-local, with 0 at the start of the clip. They are not bar numbers, PPQ, or transport position. For how they are refreshed, see Lifecycle.
CLIP_BEAT
Section titled “CLIP_BEAT”CLIP_BEAT: number
The beat for the current phase (onInit = 0, onBeat = grid position, onNote = that note’s startBeat, onCc = sampling grid position).
CLIP_TIME
Section titled “CLIP_TIME”CLIP_TIME: number
CLIP_BEAT / bpm * 60, in seconds. The bpm is treated as a scalar (no tempo map).
CLIP_LENGTH
Section titled “CLIP_LENGTH”CLIP_LENGTH: number
Clip length in beats. Constant for the whole render.
onInit(() => { console.log("clip length:", CLIP_LENGTH, "beats"); console.log("start:", CLIP_BEAT, "beat /", CLIP_TIME, "sec");});
onNote((note) => { const isLast = note.startBeat >= CLIP_LENGTH - 1; setNote({ ...note, velocity: isLast ? 120 : note.velocity });});Randomness
Section titled “Randomness”seededRandom()
Section titled “seededRandom()”seededRandom(): number
A [0, 1) pseudo-random number from mulberry32. Reproducible.
The default seed depends on the contract version. The legacy v1 hashes “code body + source notes + loaded libs”; v2 derives it from the canonicalized musical input, so reassigning IDs or reordering does not change the seed.
onBeat((beat) => { const pitch = 60 + Math.floor(seededRandom() * 12); setNote({ pitch, startBeat: beat, lengthBeats: 0.25 });});seed()
Section titled “seed()”seed(n: number | string): void
Overrides the seed. Strings are hashed into a 32-bit integer seed.
onInit(() => { seed("variation-A"); console.log("first draw:", seededRandom());});Math: typeof Math
The standard Math members. One difference: under contract v2, Math.random() draws from the seeded stream, making it reproducible like seededRandom() (v1 keeps the ordinary Math.random()). Every member other than Math.random is the standard implementation.
onBeat((beat) => { const pitch = 60 + Math.floor(Math.random() * 12); setNote({ pitch, startBeat: beat, lengthBeats: 0.25 });});[!IMPORTANT]
random()/randomInt()/pick()/chance()/weighted()/shuffle()/urn()/drunk()fromfactory.jsare lib functions built onMath.random(). They are not reproducible on a v1 clip, so reach forseededRandom()directly when you need determinism. See Helpers: numeric, random, modulation.
Project macros
Section titled “Project macros”macros
Section titled “macros”macros: Readonly<Record<string, number>>
A snapshot of the Project Macros (macro name → value). It is read-only and never moves during a render; writing to it throws in strict mode.
An undefined macro name reads as undefined, so supply a fallback with ??.
onInit(() => { const density = macros.density ?? 4; for (let i = 0; i < density; i += 1) { setNote({ pitch: 48, startBeat: i, lengthBeats: 0.25 }); }});Logging
Section titled “Logging”console
Section titled “console”console.log(...args: unknown[]): void
Arguments are stringified and shown in the Code tab console.
- strings pass through as-is
- everything else goes through
JSON.stringify(circular references and functions end up as[object Object]and friends) console.warn/console.errorare not provided
onNote((note) => { console.log("note:", note.pitch, "@", note.startBeat);});[!TIP] Heavy logging slows the Code tab down. Delete the calls once you are done debugging.
Implementation notes
Section titled “Implementation notes”You rarely need these, but they help when troubleshooting.
- Your code is compiled with
new Function("__api", ...)and runs in a script context:thisis the global object andargumentsexists, but it is not module scope __apiand its__sync/__beat/__timeare host-internal wiring. They are not public API, so they appear neither in completion nor in this reference — code that touches them will break in a future versionCLIP_BEAT/CLIP_TIMEareletbindings inside the wrapper.__sync()refreshes them right before each callback, so your closures see the value at call time- The
idofsetNote/setCcoutput is assigned withcrypto.randomUUID() setCccalls are bucketed byccNumberin an internalMap, then each lane is returned sorted by ascendingbeat