Skip to content

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.

setNote({ pitch?, velocity?, startBeat?, lengthBeats? }): void

Emits one note into the render. Every field is optional.

FieldRequiredDefaultRange / unit
pitchoptional60 (C4)0..127 (MIDI note number, int)
velocityoptional1000..127 (7-bit, int)
startBeatoptionalCLIP_BEAT (current phase)not clamped (beat)
lengthBeatsoptional10.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 / velocity are rounded to an integer and then clamped. lengthBeats bottoms out at 0.001 — smaller values are raised to it (no silent notes).

CC, Pitch Bend and Channel Aftertouch all ride the same lane machinery. For editing them on screen, see CC / Pitch Bend / Aftertouch automation.

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.

FieldRequiredDefaultRange / unit
ccNumberrequired—0..127 (int)
beatoptionalCLIP_BEATnot clamped (beat)
valuerequired—0..127 (7-bit, int)
curveoptional"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" });
});
curveShape
"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 setCc on the same ccNumber accumulates into one lane’s event list. Different ccNumbers produce separate lanes.

setCcResolution(stepsPerBeat: number): void

Sets the onCc sampling grid resolution (how many ticks per beat).

ArgumentRequiredRange / unit
stepsPerBeatrequired1..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. Lower setCcResolution or shorten the clip.

setPitchBend({ beat?, value, curve? }): void

Emits one Pitch Bend point. A clip has at most one Pitch Bend lane.

FieldRequiredDefaultRange / unit
beatoptionalCLIP_BEATnot clamped (beat)
valuerequired—0..16383 (14-bit, center 8192)
curveoptional"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: 0 is fully bent down, not “no bend”. Pass 8192 to return to center.

setChannelAftertouch({ beat?, value, curve? }): void

Emits one Channel Aftertouch point. Like Pitch Bend, one lane per clip.

FieldRequiredDefaultRange / unit
beatoptionalCLIP_BEATnot clamped (beat)
valuerequired—0..127 (7-bit, int)
curveoptional"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 });
});

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.

FunctionCallback argumentCall count
onInit(fn)noneonce, 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(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(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(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(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 });
});

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: 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(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.

SituationReturn value
no lane for ccNumber / empty events0
beat before the first pointfirst point’s value (hold-back)
beat after the last pointlast point’s value (hold-forward)
several points on the same beatthe 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] sampleCc only reads the input ccLanes. Values written with setCc during the same render are not readable back (chicken and egg).

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(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) });
});

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: number

The beat for the current phase (onInit = 0, onBeat = grid position, onNote = that note’s startBeat, onCc = sampling grid position).

CLIP_TIME: number

CLIP_BEAT / bpm * 60, in seconds. The bpm is treated as a scalar (no tempo map).

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 });
});

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(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() from factory.js are lib functions built on Math.random(). They are not reproducible on a v1 clip, so reach for seededRandom() directly when you need determinism. See Helpers: numeric, random, modulation.

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 });
}
});

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.error are 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.

You rarely need these, but they help when troubleshooting.

  • Your code is compiled with new Function("__api", ...) and runs in a script context: this is the global object and arguments exists, but it is not module scope
  • __api and its __sync / __beat / __time are 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 version
  • CLIP_BEAT / CLIP_TIME are let bindings inside the wrapper. __sync() refreshes them right before each callback, so your closures see the value at call time
  • The id of setNote / setCc output is assigned with crypto.randomUUID()
  • setCc calls are bucketed by ccNumber in an internal Map, then each lane is returned sorted by ascending beat