Skip to content
KODA
Reference

KODA Scripting Reference

Lists every function available to scripts along with code examples.

Introduction


Pre-release

This document is not finalized for publish and is still being worked on.

Conventions used in this reference

FunctionReturnsDescription
functionName(argType argName) ReturnType A description of what the function does, with range or unit information for numeric arguments and midpoints, or defaults where relevant.TipOptional note about parameter-accepting overloads or related functions.

How instrument scripts start

Nearly every instrument script opens the same way: an event initialize() that runs once when the instrument loads, switching on logging and setting how much of KODA's automatic note handling you want to keep.

event initialize()
{
    setLogEnabled(true);          // Without this, debug.log() output is silent
    setIgnoreMidiNotes(false);    // Make sure incoming MIDI notes still play
}
  • setLogEnabled(true) - logging is off by default, so debug.log() stays silent until you turn it on.
  • setIgnoreMidiNotes(false) - KODA keeps triggering notes for you. Use this when the script only listens: logging, GUI work, or modulation.
  • setIgnoreNoteOns(true) - KODA stops triggering notes so the script can start them itself with startNote(). setIgnoreMidiNotes(true) does the same for note-offs as well.

These flags live on the instrument, but the script owns them: every time a script loads or reloads, they reset to their defaults before initialize() runs, so the engine only keeps what the current version of the script actually sets. Remove a setIgnoreNoteOns(true) and reload - the engine goes back to handling note-ons. See MIDI input control for the full set.

No timing values yet

initialize() runs before any audio is flowing, so the sample rate, block size and tempo all read 0. Anything that turns time into samples reads 0 too: setDelayMS, setDelaySeconds, setLengthBeats, and the note length helpers. Nothing errors - most values just read zero. The exception is getBlockTimeSeconds(), which is computed from them and reads NaN there instead (see Avoiding NaN). Building a Pattern here is fine, though - patterns store beat positions as beats and only convert them to samples during playback.

If your setup needs them, do that part on the first process() call instead, where the numbers are real:

bool ready;

event process()
{
    if(!ready)
    {
        buildDelayTables();   // sample rate and tempo are real by now
        ready = true;
    }
}

Everything that doesn't depend on timing still belongs in initialize(): logging, MIDI flags, building UI, reading parameters.

Compressed examples

Since this opening block appears in nearly every example, the examples below often compress it onto a single line to save space: event initialize() { setIgnoreNoteOns(true); }. Both forms do the same thing, so write it whichever way you prefer.

Getting by name vs. by index

Two ways to fetch

Most getter functions that fetch a part of the instrument - getArticulation, getVariation, getGroup, getMic, getBus, getParameter, and others - accept either a name (string) or an index (int). Both forms return the same kind of object.

  • By name - More robust. If you reorder articulations, variations, or other parts of the instrument, scripts keep working because they reference the parts by what they're called, not where they sit.
  • By index - Less typing, and natural inside for loops where you'd iterate over every articulation, mic, etc. Index access skips the string lookup, but breaks if you reorder.
// By name - survives reordering
Articulation soft = getArticulation("Soft");
Mic close = getMic("Close");
// By index - concise in loops
event initialize()
{
    setLogEnabled(true);    // Without this, debug.log() output is silent

    for(int i = 0; i < getNumArticulations(); i++)
    {
        Articulation art = getArticulation(i);
        debug.log(art.getName());
    }
}

Note vs NoteData

Two related but distinct types describe a note at different points in its life:

  • NoteData - a template for a note before it plays. Carries pitch, velocity, envelope, timing, and routing. KODA hands one to your noteOn handler describing each incoming MIDI note. You can modify it freely, build new ones via createNoteData(), and trigger them with start() or startNote().
  • Note - a handle to a note that's currently playing. You get one back from startNote(). Use it to stop the playing note, or change its tuning, gain, pan, or trigger its release.

The typical flow is configure (NoteData) → start (returns a Note) → modify (Note):

event initialize() { setIgnoreNoteOns(true); }

event noteOn(NoteData note)  // NoteData is a template for a note to be played
{
    note.setAttack(0.05f);   // Set attack before note plays

    Articulation art = getCurrentArticulation();
    Note playing = art.startNote(note); // Trigger it, get back a Note handle.

    playing.setGain(0.8f);              // Modify the playing note
}

Once startNote() has fired, changes to the original NoteData have no effect on the playing note. They only describe the template. To affect a sounding note, always use its Note handle.

Two ways to call a function: with a dot or without

Same call, two ways

artic.startNote(note); can also be written as startNote(artic, note);

artic.setGain(0.5f); can also be written as setGain(artic, 0.5f);

Although written differently, they are identical.

Examples in this reference use dot syntax because it's the more common style in the Cmajor standard library and KODA codebase. If you prefer passing the object as the first argument, that form works everywhere too.

Using Parameter to skip using .getValue() (Overload Convenience)

Many setter functions that take a primitive (float, int, or bool) also accept a Parameter directly, saving you a .getValue() / .getIntValue() / .getBoolValue() call. The Parameter overload exists wherever you see a Tip line in a function's description.

// Both of these set the articulation's gain from the built-in Dynamics Parameter:
event Dynamics(Parameter parameter)
{
    Articulation art = getCurrentArticulation();

    art.setGain(parameter.getValue());   // Explicit unwrap
    art.setGain(parameter);              // Parameter overload - same result, less typing
}

For non-setter contexts (arithmetic, comparisons, logging, passing to functions without a Parameter overload) you'll always need an explicit .getValue():

//  Won't compile
if(parameter > 0.5f)
//  Works
if(parameter.getValue() > 0.5f)

Built-in Parameters

Every KODA instrument always carries these eight Parameters. They're created automatically - you don't need to add them in Figma - and you can react to them from a script with the standard event <ParameterName>(Parameter parameter) handler pattern without any extra UI setup. Their CC assignments are fixed. The CONTROL tab that will let users remap them is still to come.

Each has a range of 0.0-1.0 except where the table says otherwise. Note that the defaults are not all 0.0. Dynamics starts at 0.8, and BreathController starts at 1.0. If your script reads one of these before the user has touched it, that starting value is what you'll get. They appear in the order below in the app's Instrument Parameters table.

ParameterDefault CCDefaultDescription
Dynamics CC 1 0.8 Continuous expressive level - typically used to blend dynamic-layer Articulations or to control a Variation crossfader. Standard mod-wheel destination.
Vibrato CC 2 0.0 Vibrato depth or rate. Sometimes wired to a Modulator's LFO depth for scripted vibrato.TipThis is the one built-in that ships with IGNORE ticked in the Instrument Parameters table, so incoming CC 2 is muted until the author clears it. Setting the Parameter from a script is unaffected.
BreathController CC 3 1.0 Breath-controller position. Starts at 1.0 (fully open), not 0.0.
Expression CC 11 0.0 Continuous expression / volume-after-fader.
SustainPedal CC 64 0.0 Sustain pedal state, defaulting to pedal-up so a freshly loaded instrument never starts with sustain engaged. Conventionally treated as a switch above 0.5. To stop the pedal affecting notes at all, call setIgnoreSustainPedal(true).
PitchBend Pitch-bend msg 0.0 Pitch-bend wheel position. Range −1.0 (full down) to 1.0 (full up); 0.0 is centered. Driven by the dedicated MIDI pitch-bend message rather than a CC, so it has no CC number to remap.
Aftertouch - 0.0 Aftertouch pressure, normalized to 0.0-1.0. Fed by both channel pressure and polyphonic aftertouch. Whichever arrived last wins. Driven by the MIDI pressure message rather than a CC.
CurrentArticulation - 0 Index of the currently active Articulation, from 0 to the number of Articulations minus one. Controlled by articulation buttons in the UI or by script via setCurrentArticulation(...). It holds a whole-number index rather than a continuous control value, so it snaps between articulations instead of sliding through the values in between. Has no CC mapping.TipThis is the only built-in that isn't offered to the host as an automation slot. The other seven are.

Don't write scripts around the CC numbers themselves. You always address these by name - getParameter("Dynamics"), event Dynamics(Parameter parameter) - and the CC is just an input-routing detail handled outside your code. Once user remapping arrives, a script written this way keeps working with no changes.

How to use the script examples in this reference

Script examples throughout this reference use built-in Parameters like Dynamics, Expression, and SustainPedal so they copy-paste into any instrument and run without you building UI first.

It does make a few examples impractical - bypassing a distortion with the sustain pedal isn't something you'd normally do. Replace the built-in Parameter with one of your own, the Parameter behind your knob or slider, and rename the handler to match:

  • A button in place of SustainPedal
  • A knob or slider in place of a continuous CC like Expression
  • A menu in place of CurrentArticulation

Built-in helpers

Generic utility functions that don't belong to any specific type. Always in scope.

FunctionReturnsDescription
limit(Type value, Type min, Type max)TypeReturns value clamped to the inclusive range [min, max]. Works with any numeric type, inferred from the arguments. It returns the clamped value rather than changing value in place, so assign the result: x = limit(x, 0, 40);

Cmajor patterns you'll see

A few Cmajor language conventions that aren't part of the KODA API but show up regularly in scripts.

Safe array indexing with wrap<N> or .at()

When you index a fixed-size array with a plain int, the Cmajor compiler rejects the access unless the index is guaranteed to be in bounds. The standard fix is to cast the index via wrap<N>, which wraps the value round so it's always valid for an array of size N.

.at(index) does the same job in fewer characters: it takes a plain int and wraps out-of-range values the same way. You can assign through it as well as read from it.

float[8] table = (0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f);

int i = 12;
float v = table[wrap<8>(i)];   // 12 wraps into [0, 8) as 4, so v is 0.4
float w = table.at(i);         // same element, so w is 0.4 too

The difference is cost. wrap<N> guarantees the index is in range as part of its type, so the compiler emits no check at all. .at() checks on every access. Either is fine for occasional lookups; prefer wrap<N> in code that runs every block.

You'll see this most often in event process() when stepping through a precomputed table - for example, looking up an entry in a vibrato LFO waveform.

Fetching and calling need to be separate lines

Writing art.startNote(note) is shorthand for startNote(art, note), and the dot form needs a named variable on its left. So fetching something and calling a function on it need to be separate lines. Put both on one line and the compiler often can't work out which function you meant.

//  May fail to compile
Note n = getArticulation("Straight").startNote(note);
//  Fetch on one line, call on the next
Articulation art = getArticulation("Straight");
Note n = art.startNote(note);

Names you declare share the script with KODA's

Your variables and functions live in the same namespace as every function in this reference. Short, common words - transpose, start, set - are often already taken, and the compiler reports the collision as "The name is already in use", pointing at your declaration. The fix is a more specific name: transposeSemitones instead of transpose.

Instrument (Global)


This is your script's top-level entry point into the instrument. Everything you've built in the KODA app - the Articulations, Variations, Groups, Parameters, Modulators, Mics, and Buses - is reachable from here, alongside instrument-wide controls (volume, tuning, MIDI input filters) and host-supplied info (BPM, play state, time signature, sample rate, elapsed time).

When calling setInstrumentVolume(-6.0f) or setInstrumentTuning(2.0f), you're setting the same values as the instrument-wide volume and tuning controls in the KODA app's GUI.

These are always in scope. Call them from anywhere in your script - no setup, no object to fetch.

Logging

When logging, you'll need to write debug.log(...) to avoid a name collision with the built-in math function for logarithms.

Logging is disabled by default. Enable it once in event initialize() so messages reach the debug console.

Example: Log a note's position only when the transport is playing

event initialize()
{
    setLogEnabled(true);          // Without this, debug.log() output is silent
    setIgnoreMidiNotes(false);    // Make sure incoming MIDI notes still play
}

// Triggered every time the user plays a note
event noteOn(NoteData note)
{
    // Skip if the host transport isn't running (no DAW playback)
    if(getIsPlaying())
    {
        // Quarter notes elapsed since the start of the timeline (e.g. 2.5 = halfway through beat 3)
        debug.log("Note played at PPQ:", getppqPosition());
    }
}

Logging

FunctionReturnsDescription
setLogEnabled(bool enabled) void Enables or disables debug.log() output. Call once in event initialize() to enable.
debug.log(string message) void Logs a message to the debug console (requires setLogEnabled(true)).
debug.log<Type>(string message, Type value) void Logs a message followed by a value (any printable type) to the debug console.

Group Structure Section

This is where the Group Structure (Articulation / Variation / Group) hierarchy you've built in the KODA app becomes reachable from your script. Use these to grab a handle to whichever Articulation, Variation, or Group you want to act on.

FunctionReturnsDescription
getArticulation(int index)
getArticulation(string name)
Articulation Gets an Articulation by 0-based index or by name (names are defined in the instrument's structure).
getArticulations() Articulation[] Returns an array of all articulations in the instrument.
getNumArticulations() int Returns the total number of articulations in the instrument.
getCurrentArticulation() Articulation Returns the currently active articulation (controlled by the CurrentArticulation parameter).
setCurrentArticulation(int index)
setCurrentArticulation(string name)
setCurrentArticulation(Articulation articulation)
void Sets the current articulation by 0-based index, name, or an existing Articulation object.

Parameters & Modulators

The Parameters you've defined in Figma and the Modulators wired up in the KODA app's Modulators panel become reachable from your script here. Use these to grab a handle to the Parameter or Modulator you want to read, set, or react to.

FunctionReturnsDescription
getParameter(string name) Parameter Gets a Parameter by name. The name matches what was defined in Figma.
getParameters() Parameter[] Array of every Parameter currently registered in the engine. Iterate to scan them all.
getModulator(int index)
getModulator(string name)
Modulator Gets a Modulator by 0-based index or by name.

Mics

The mic mixer you've configured in the KODA app becomes reachable from your script here. Use these to grab a handle to a specific Mic so you can adjust its gain, pan, solo, or mute from your script.

FunctionReturnsDescription
getMic(int index)
getMic(string name)
Mic Gets a Mic by 0-based index or by name.

Convolution IRs

Your script can read the library's impulse-response .wav files - the same list the Convolution effect shows in its own dropdown - and use them to autopopulate a front-end menu of IRs. They live in the library's IR Samples folder. The index matches Convolution::File, so a menu item's value can route straight to setInsertParameter(slot, Convolution::File, index).

FunctionReturnsDescription
getNumConvolutionFiles() int Returns the number of IR .wav files found in the library's IR Samples folder (0 if the folder is absent or empty). Useful for for-loops that build a menu of every available IR.
getConvolutionFileName(int index) string Returns the display name (extension stripped) of the IR at the given 0-based index (0getNumConvolutionFiles() - 1), in the same order as the Convolution effect's dropdown. Returns "" if the index is out of range.

Example: Autopopulate a menu with the library's IRs

(Three things have to be in place before this example will run: a Menu widget named mnuConvolution in your Figma UI, a Convolution insert in slot 0 of the master bus, and at least one .wav in the library's IR Samples folder.)

// Runs once when the instrument loads.
event initialize()
{
    createConvolutionMenu("mnuConvolution");   // must match the Menu widget's name
}

// Build a front-end dropdown from the library's "IR Samples" folder.
Component createConvolutionMenu(string name)
{
    Component menu = getComponent(name);
    menu.clearItems();
    menu.addItem("None", -1);   // -1 clears the IR (pass-through)

    for (int i = 0; i < getNumConvolutionFiles(); i++)
        menu.addItem(getConvolutionFileName(i), i);

    return menu;
}

// The menu value maps directly to the Convolution insert's File parameter.
event mnuConvolution(Parameter parameter)
{
    Bus master = getMasterBus();
    master.setInsertParameter(0, Convolution::File, parameter.getValue());   // slot 0 of the master bus
}

Instrument output

The instrument-wide volume, pan and tuning controls in the KODA app are exposed here. Use these to adjust the global volume, pan or tuning from a script.

FunctionReturnsDescription
setInstrumentGain(float gain) void Sets the global instrument gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity.
setInstrumentVolume(float decibels) void Sets the global instrument volume in decibels - the same value, and the same range, as the master volume control in the KODA app. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.TipAlso accepts a Parameter directly: setInstrumentVolume(myVolumeKnob).
setInstrumentPan(float pan) void Sets the global instrument pan. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center. This is an offset that combines with the instrument Amplifier's own pan knob rather than replacing it.TipAlso accepts a Parameter directly: setInstrumentPan(myPanKnob).
setInstrumentTuning(float semitones) void Sets the global instrument tuning in semitones. 0.0 = no change. No hard clamp; typical range -24 to +24.TipAlso accepts a Parameter directly: setInstrumentTuning(myTuneKnob).

MIDI input control

Filter or block specific kinds of incoming MIDI messages at the instrument level. These flags reset to their defaults whenever the script loads or reloads, then the script's initialize() re-asserts whatever it sets - so a call removed from the script never lingers in the engine.

FunctionReturnsDescription
setIgnorePitchBend(bool ignore) void Sets whether to ignore incoming pitch-bend messages. true = ignore, false = process normally.
setIgnoreSustainPedal(bool ignore) void Sets whether to ignore sustain pedal messages. true = ignore, false = process normally.
setIgnoreKeyRange(bool ignore) void Sets whether to ignore the playable key range filter. true = play notes outside the playable range; false = filter them out.
setIgnoreNoteOns(bool ignoreNoteOns) void Sets whether the instrument ignores incoming note-on events.
setIgnoreNoteOffs(bool ignoreNoteOffs) void Sets whether the instrument ignores incoming note-off events.
setIgnoreMidiNotes(bool ignore) void Sets whether the instrument ignores both note-on and note-off events. Shortcut for calling setIgnoreNoteOns + setIgnoreNoteOffs together.
setNoteOffCancelsDelayedNotes(bool cancel) void Sets whether a note-off cancels notes that are still in their delay phase (haven't started yet).
Taking manual control of routing

When your script routes notes itself - picking an articulation per note, applying script-side modulation, or generating notes programmatically - call setIgnoreNoteOns(true) in event initialize(). Without it, every incoming MIDI note triggers twice: once through the instrument's default routing and again through your startNote() call.

setIgnoreNoteOffs works the same way for releases. Most scripted instruments want both. setIgnoreMidiNotes(true) turns them on together in one call.

Both flags belong to the instrument, not to the script that set them. They are saved with the instrument and restored on load, so a flag switched on by one script stays on after you replace that script with another. KODA's default script template sets setIgnoreNoteOns(true), so most instruments start out ignoring MIDI notes.

Instrument runtime state

Read-only queries about what your instrument is doing right now.

FunctionReturnsDescription
getNumActiveNotes() int Returns the number of currently active notes (across all articulations).
isNoteHeld(int noteNumber) bool Returns true if the given MIDI note number is currently held (key down). Tracks host MIDI only. Script-triggered notes are not counted.
getHeldNotes() HeldNotes Returns the current set of held MIDI notes as a HeldNotes value. Tracks host MIDI only. Script-triggered notes are not counted.

Transport state

Read-only queries about the host's current transport state and audio configuration. Note: although you access this from the instrument, the data is being passed in from the host DAW.

FunctionReturnsDescription
getBPM() float64 Returns the host transport's current BPM (beats per minute).
getppqPosition() float64 Returns the host transport's current position in PPQ (pulses per quarter note).
getTimeSignature() TimeSignature Returns the host transport's current time signature as a TimeSignature struct (numerator + denominator).
getTimeSignatureNumerator() float64 Returns the time signature numerator only (e.g. 4.0 in 4/4 time).
getIsPlaying() bool Returns true if the host transport is currently playing.
getIsRecording() bool Returns true if the host transport is currently recording. Not every DAW reports this. Hosts that don't will return false even while recording.
getIsStandalone() bool Returns true if KODA is running as a standalone application (not loaded as a plugin in a DAW).
getBlockSize() int Returns the audio processing block size for the current block, in samples. Note: this can vary block-to-block depending on the host; don't assume it's constant.
getSampleRate() float64 Returns the internal sample rate in Hz (e.g. 48000.0).

System & environment

Read-only info about the host environment and machine.

FunctionReturnsDescription
getIsRealTime() bool True in a real-time audio context; false during offline/bounce render.
getWrapperType() string Plugin format/wrapper (e.g. "Standalone", "AU", "VST3").
getHostType() string Host/DAW name.
getOperatingSystem() string OS name.
getCpuArchitecture() string CPU architecture (e.g. "arm64", "x86_64").
getCoreCount() int Logical CPU cores.
getPhysicalCoreCount() int Physical CPU cores.
getMemorySizeMB() int Total system RAM in MB.
getUserLanguage() string User's language.
getUserRegion() string User's region.
getBlockTimeSeconds() float64 Duration of one audio block in seconds (block size ÷ sample rate).

Time elapsed & beats

Time since the script started running, and helpers for sample-counts until the next beat or bar. Reach for the samples version for sample-accurate scheduling (it combines naturally with getBeat(), getNextBeat(), and getBlockSize()); reach for the seconds version for logging or human-readable duration checks.

FunctionReturnsDescription
getTimeElapsedSamples() int64 Returns the total time elapsed since the script started, in samples.
getTimeElapsedSeconds() float64 Returns the total time elapsed since the script started, in seconds.
getBeat() float64 Returns the length of one beat in samples (at the current host BPM and sample rate).
getNextBeat() int Returns the number of samples remaining until the next beat boundary.
getNextBar() int Returns the number of samples remaining until the next bar boundary.

Note length helpers

Length of musical note durations in samples, based on the host BPM (and time signature for bars). Useful for scheduling delays and lengths in rhythmic units.

FunctionReturnsDescription
getBar() float64 Length of one bar in samples (depends on time signature and BPM).
getWholeNote() float64 Length of a whole note in samples.
getHalfNote() float64 Length of a half note in samples.
getHalfNoteTriplet() float64 Length of a half-note triplet in samples.
getQuarterNote() float64 Length of a quarter note in samples.
getQuarterNoteTriplet() float64 Length of a quarter-note triplet in samples.
get8thNote() float64 Length of an 8th note in samples.
get8thNoteTriplet() float64 Length of an 8th-note triplet in samples.
get16thNote() float64 Length of a 16th note in samples.
get16thNoteTriplet() float64 Length of a 16th-note triplet in samples.
get32ndNote() float64 Length of a 32nd note in samples.
get32ndNoteTriplet() float64 Length of a 32nd-note triplet in samples.

Example: Print the host's sample rate and tempo

initialize() runs before the first block of audio, so anything the host supplies - sample rate, BPM, time signature, transport position - is still 0 there. Read it from process() or noteOn() instead. The flag below keeps it to a single log line.

bool loggedHostInfo;

event initialize() { setLogEnabled(true); }   // Without this, debug.log() output is silent

// Runs once per block of 64 samples, after the host has supplied its info
event process()
{
    if (!loggedHostInfo)
    {
        debug.log("Sample rate:", getSampleRate());
        debug.log("BPM:", getBPM());
        loggedHostInfo = true;
    }
}

Parameter


The Parameters you've created in Figma - knobs, sliders, buttons, etc. - become reachable from your script here. Use these to grab a handle to whichever Parameter you want to read, set, or react to.

  • Access a parameter inside its specific callback: event <ParameterName>(Parameter parameter)
  • Or a generic callback that fires when any parameter changes: event parameterUpdated(Parameter parameter)
  • Or, for use outside the callback, create a global Parameter via getParameter("Name").
Parameter expression = getParameter("Expression");   // Expression is a built-in parameter. Swap in your own.
float currentExpression = expression.getValue();

Reading values

(Where multiple functions are listed in the below tables, they do the same thing.)

FunctionReturnsDescription
getValue()
get()
float Returns the Parameter's current value as a float.
getIntValue()
toInt()
int Returns the Parameter's current value rounded to the nearest integer.
getBoolValue()
isTrue()
bool Returns true if the Parameter's current value is greater than 0.5, otherwise false.
getValueNormalized() float Returns the Parameter's current value normalized to the 0.0-1.0 range, based on its min and max.
getValueInverted(float& value) void Writes the inverted-normalized value (max − normalized × range) into the supplied reference.

Range & identity

FunctionReturnsDescription
getId() Identifier Returns the Parameter's unique identifier. Use to compare against another Parameter's ID (e.g. inside event parameterUpdated to identify which Parameter changed, though the named-event form event <ParameterName>(...) usually avoids the need for this check).
getName() string Returns the Parameter's name as defined in Figma. Returns "Invalid Parameter" if the ID is out of range.
getMinValue() float Returns the Parameter's minimum value as set in Figma. Returns 0.0 if the ID is invalid.
getMaxValue() float Returns the Parameter's maximum value as set in Figma. Returns 1.0 if the ID is invalid.
getDefaultValue() float Returns the Parameter's default value as set in Figma. Returns 0.0 if the ID is invalid.
getRange() float Returns the Parameter's range (max − min).
idIsValid() bool Returns true if the Parameter's ID falls within the valid range.
isEqualTo(Parameter other) bool Returns true if this Parameter has the same ID as other. Useful inside event parameterUpdated when you need to tell which Parameter changed.

Setting values

FunctionReturnsDescription
setValue(float newValue)
setValue(int newValue)
setValue(bool newValue)
void Sets the Parameter's value, clamped to its min/max range. Bool true = 1.0, false = 0.0.NoteCalling setValue() from within the Parameter's own parameterUpdated handler is not allowed (logs a warning and is ignored).

Reacting to parameter changes

To run code when a Parameter's value changes, write a named event handler. The handler's name must match the Parameter's name (the string passed to getParameter). KODA sends each change to its own handler automatically.

The event runs once per change, with the new value already on the parameter. Read it with parameter.getValue() / parameter.getIntValue() / parameter.getBoolValue(), or pass the parameter directly to any setter that accepts a Parameter overload (see Parameter Convenience).

Example: Using the Expression parameter to control current articulation volume

event Expression(Parameter parameter)
{
    Articulation art = getCurrentArticulation();  // Get handle to current articulation
    art.setGain(parameter); // Set the volume of the articulation
}

Articulation, Variation & Group


Every KODA instrument's samples are organized into a three-level container hierarchy:

  • Articulation - the top-level grouping, typically a name for a playing style ("Sustain", "Staccato", "Pizzicato"). Switching articulations via the keyswitch UI changes which Articulation plays, as well as the elements contained within.
  • Variation - a mid-level container inside an Articulation. Used for things like dynamic layers (pp / mf / ff), vibrato vs. non-vibrato, or alternate takes.
  • Group - the bottom-level container inside a Variation. Where the actual sample mappings live. The level you'd reach to manipulate the insert FX chain attached to those samples.

All three containers share most of the same functions - identity / state, gain / pan, tuning / transposition, sample loading, note triggering, and per-container parameters, so they're documented together below. Three subsections at the end cover the per-type extras: navigating a parent's children (Articulation, Variation) and the insert FX chain (Group).

You can use the structure however you like. That said, structuring your articulations in the Articulations section will give KODA automatic awareness of what articulations are available and what the user currently has selected, which lets you harness some automatic functionality for free. Similarly, placing your release tails on the Group level allows you to use built-in release tail trigger functionality.

Get a handle via getCurrentArticulation(), getArticulation(), articulation.getVariation(), variation.getGroup(), or articulation.getGroup() (which jumps from Articulation directly to Group, using whichever Variation is currently active).

Example: Log which articulation received each note

event initialize()
{
    setLogEnabled(true);          // Without this, debug.log() output is silent
    setIgnoreMidiNotes(false);    // Make sure incoming MIDI notes still play
}

// Fires every time a note is played
event noteOn(NoteData note)
{
    // Reach into the currently-active articulation and log its name
    Articulation art = getCurrentArticulation();
    debug.log("Note routed to articulation:", art.getName());
}

Example: Alter current articulation volume/pan/tuning with sustain pedal

event initialize()
{
    setLogEnabled(true);          // Without this, debug.log() output is silent
    setIgnoreMidiNotes(false);    // Make sure incoming MIDI notes still play
}

// Fires whenever the sustain pedal moves (built-in, MIDI CC 64 by default)
event SustainPedal(Parameter parameter)
{
    Articulation art = getCurrentArticulation();

    if(parameter.isTrue())      // pedal down
    {
        art.setVolume(-6.0f);       // 6 dB below unity
        art.setPan(-0.5f);          // half left
        art.setTuningCents(50.0f);  // a quarter-tone sharp

        debug.log("Trimmed:", art.getName());
    }
    else                        // pedal up: back to normal
    {
        art.setVolume(0.0f);        // unity
        art.setPan(0.0f);           // centered
        art.setTuningCents(0.0f);   // no offset

        debug.log("Restored:", art.getName());
    }
}
Shared setter pattern

Most setter functions on Articulation, Variation, and Group (gain, pan, tuning variants, transposition, active child, insert parameters) accept either a primitive value or a Parameter directly. See the Parameter convenience at the top of this reference.

Identity & state

FunctionReturnsDescription
getName() string Returns the container's name as defined in the instrument (e.g. "Sustain").
getIndex() int Returns the container's 0-based index within its parent. (Articulation's parent is the instrument itself.)
isIdValid() bool Returns true if this handle refers to a real container in the instrument. Use as a null check before calling functions on the handle.
validate() void Logs whether this handle is valid (and its name if so) to the console. Useful while debugging name lookups.
getTypeName() string Returns the type name as a string ("Articulation", "Variation", or "Group"). Useful for generic logging.

Gain & pan

FunctionReturnsDescription
setGain(float gain) void Sets gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity.
setVolume(float decibels) void Sets volume in decibels - the same value, and the same range, as the volume knob on this item in the KODA app. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.TipAlso accepts a Parameter directly: art.setVolume(myVolumeKnob).
setPan(float pan) void Sets pan position. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center. Clamped.
getGain() float Returns the current gain. Range: 0.0-4.0.
getPan() float Returns the current pan position. Range: -1.0-1.0.

Tuning & transposition

Tuning shifts the pitch of a sample. Transposition offsets the position of the note to trigger a different sample.

FunctionReturnsDescription
setTuningSemitones(float semitones) void Sets tuning in semitones. 0.0 = no change. No hard clamp; typical range -24 to +24.
setTuningCents(float cents) void Sets tuning in cents. 100 cents = 1 semitone; 0.0 = no change.
setTuningFactor(float speedRatio) void Sets tuning as a speed/frequency ratio. 1.0 = no change; 2.0 = one octave up; 0.5 = one octave down.
setTransposition(int transposition) void Sets transposition in semitones by changing the note number played (not by pitch-shifting). 0 = no change.
getTuningSemitones() float Returns the current tuning in semitones. 0.0 = no change.
getTransposition() float Returns the current transposition in semitones. 0.0 = no change.

Sample loading/purging

Load or purge this container's samples from memory. Useful for templating instruments that hold many articulations but only need a few in memory at once.

FunctionReturnsDescription
loadSamples() void Loads this container's samples into memory if they aren't already.
purgeSamples() void Unloads this container's samples from memory (frees RAM). Re-trigger loadSamples() before playing again.

Keyboard highlight

Overlay this container's mapped range - the exact set of MIDI notes covered by every zone beneath it (across all child Variations and Groups) - on the GUI piano, tinted with a chosen Colour. Handy for showing the user which keys an Articulation, Variation, or Group responds to, e.g. when a menu switches the active Variation. This overlay is transient and independent of the articulation's own mapped-range bar (the DISPLAY checkbox in the KEY RANGES module, addressable from a script as the "Playable Range" name via hideKeyRange()).

FunctionReturnsDescription
showPlayableRange(Colour colour) void Highlights every note this container covers on the GUI piano in colour. Several containers can be highlighted at once. Each keeps its own colour, and when two overlap on a note the most recently shown one wins, so call order acts as draw order (show a small, informative range last to keep it on top of full-keyboard layers). Remove with hidePlayableRange() or clearPlayableRanges().
hidePlayableRange() void Removes this container's playable-range highlight from the GUI piano. No-op if it isn't currently shown.

Playing notes

Trigger and stop notes scoped to this container. Notes started on an Articulation flow through its active Variation (and that Variation's active Group); notes started on a Variation flow through its active Group; notes started on a Group play that Group's samples directly.

FunctionReturnsDescription
createNoteData() NoteData Returns a fresh NoteData struct with this container's indices pre-filled - ready to be customized (velocity, note number, envelope, etc.) and passed to startNote().
startNote(NoteData note)
startNote(int note, float velocity)
Note Starts a note on this container and returns a Note handle you can use to modify or stop the note while it plays. The simple form takes a MIDI note number (0-127) and velocity (0.0-1.0); the NoteData form lets you pre-configure envelope, gain, pan, and routing first.
stopNote(NoteData note) Note Stops the matching note (sends it into its release envelope).
stopAllNotes() void Stops every active note belonging to this container.

Parameters & properties

Each container can carry its own set of named parameters (numeric values) and properties (typed values) defined alongside it in the instrument. Both are authored in the GROUP VARIABLES section of the Setup tab, where properties are labelled Constants. Use these to attach per-container settings without polluting the global parameter list - and see Using Constants (properties) and parameters to make templates for the pattern that builds on them.

FunctionReturnsDescription
getParameterValue(string name) float Returns the container's named parameter value as a float. Returns 0.0 if not found.
getParameterIntValue(string name) int Returns the container's named parameter as an integer (cast from float).
getParameterBoolValue(string name) bool Returns the container's named parameter as a bool (true if value > 0.5).
getProperty(string name) string Returns the container's named property as a string. Returns "" if not found.
getIntProperty(string name) int Returns the container's named property as an integer.
getFloatProperty(string name) float Returns the container's named property as a float.
getBoolProperty(string name) bool Returns the container's named property as a bool.

Variations & active variation Articulation only

Access an Articulation's child Variations, and control which Variation new notes route to.

FunctionReturnsDescription
getVariation(int index)
getVariation(string name)
Variation Gets a child Variation by 0-based index or by name.
getNumVariations() int Returns the number of Variations under this Articulation. Useful for for-loops that iterate over every Variation.
getGroup(int index) Group Gets a Group by index from the Articulation's currently-active Variation (skips one level of the hierarchy).
setActiveVariation(int index)
setActiveVariation(string name)
void Sets the active Variation by 0-based index or by name. Subsequent notes routed through this Articulation will play from the chosen Variation.
setAllActive() void Makes all Variations active simultaneously (notes will layer through every Variation).
getActiveVariation() int Returns the index of the currently-active Variation. Returns -1 if all are active.

Example: Crossfade between vibrato and non-vibrato

// Fires whenever Vibrato changes (built-in, MIDI CC 2 by default).
// (Assumes the current articulation has Variations named "NonVibrato" and "Vibrato".)
event Vibrato(Parameter parameter)
{
    Articulation art = getCurrentArticulation();
    Variation nonVib = art.getVariation("NonVibrato");
    Variation vib   = art.getVariation("Vibrato");

    float blend = parameter.getValue();

    // 0.0 = full NonVibrato, 1.0 = full Vibrato, in-between = blended
    nonVib.setGain(1.0f - blend);
    vib.setGain(blend);
}

Example: Manually trigger a release-tail Variation when a note is released

The script takes over note handling so it can decide which Variation sounds and when. Left to itself, KODA sends each note-on to every Variation in the articulation, so the tail would sound on the way down as well as the way up.

// (Assumes the current articulation has Variations named "Sustain" and "ReleaseTail".)

float[128] noteOnVelocity;   // remembers how hard each key was struck

event initialize() { setIgnoreMidiNotes(true); }   // Override automatic note handling so we can implement in script

event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    Variation sus = art.getVariation("Sustain");

    noteOnVelocity.at(note.getNoteNumber()) = note.getVelocity();   // Stash it for the release tail
    sus.startNote(note);
}

// Fires when the user lifts a key
event noteOff(NoteData note)
{
    Articulation art = getCurrentArticulation();
    Variation sus = art.getVariation("Sustain");
    Variation tail = art.getVariation("ReleaseTail");

    sus.stopNote(note);   // Stop sustain manually

    // A note-off carries release velocity, so swap in the note-on velocity
    note.setVelocity(noteOnVelocity.at(note.getNoteNumber()));
    tail.startNote(note); // Trigger release tail manually
}

Because the script starts the note, it also has to stop it - nothing releases it otherwise. The noteOnVelocity array lives at module level so it persists between the two events, and .at() is used because a plain int index into a fixed-size array won't compile (see Cmajor patterns you'll see).

Release tails without a script

Setting a Group's trigger type to Release does this natively: the group stays silent at note-on and sounds at note-off, using the recorded note-on velocity, with its own Release Level and sustain-pedal modes. Reach for the script when you need something that setting can't express - picking between several tails, or skipping the tail depending on how long the key was held.

Groups & active group Variation only

Access a Variation's child Groups, and control which Group new notes route to.

FunctionReturnsDescription
getGroup(int index)
getGroup(string name)
Group Gets a child Group by 0-based index or by name.
getNumGroups() int Returns the number of Groups under this Variation. Useful for for-loops that iterate over every Group.
setActiveGroup(int index)
setActiveGroup(string name)
void Sets the active Group by 0-based index or by name. Subsequent notes routed through this Variation will play from the chosen Group.
setAllActive() void Makes all Groups active simultaneously (notes will layer through every Group).
getActiveGroup() int Returns the index of the currently-active Group. Returns -1 if all are active.

Insert FX Group only

Each Group can host a chain of insert FX (compressor, EQ, reverb, etc.) defined in the instrument. These functions let you modulate individual insert parameters or bypass entire inserts at runtime.

FunctionReturnsDescription
setInsertParameter(int insertIndex, int paramIndex, value) void Sets a parameter on an insert FX. insertIndex: 0-based insert slot. paramIndex: 0-based parameter on that insert. value: float, int, or bool depending on the parameter type.TipAlso accepts a Parameter directly: group.setInsertParameter(0, 0, myKnob).
setInsertBypassed(int insertIndex, bool bypassed) void Bypasses (or un-bypasses) an entire insert FX slot. true = bypassed, false = active.TipIf you're wiring this to an "Enabled" button (where true means the user wants the effect on), invert the value: setInsertBypassed(0, !param.getBoolValue()).

Example: Darken soft notes with a velocity-driven filter cutoff

// (Assumes first insert of the Group is a Filter effect.)

event initialize() { setIgnoreMidiNotes(false); }   // Make sure incoming MIDI notes still play

// Fires every time a note is played
event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    Group group = art.getGroup(0);

    // Lower-velocity notes get a lower cutoff (darker tone, mimicking a softer note)
    float cutoffHz = 200.0f + note.getVelocity() * 10000.0f; // Adjust to taste, depending on the instrument
    group.setInsertParameter(0, Filter::Cutoff, cutoffHz);
}

Insert parameters are addressed by index, but each effect publishes named constants for its own - Filter::Cutoff, Filter::Resonance, and so on - which read better and survive an effect gaining parameters.

Note

Be aware, the insert changes are applied per-group and not per-note. So in the above example, a user playing a chord with varying velocities will get a cutoff set by the last note played.

Example: Bypass a distortion with the sustain pedal

// Holding the sustain pedal bypasses the distortion on the group's FX insert.
// (Assumes a distortion in the first insert slot of the current articulation's group.)
event SustainPedal(Parameter parameter)
{
    Articulation art = getCurrentArticulation();
    Group group = art.getGroup(0);

    // Pedal pressed = bypass on, pedal released = bypass off
    group.setInsertBypassed(0, parameter.getBoolValue());
}
Technical sidenote

Articulation, Variation, and Group all share the same underlying state representation internally - which is why their gain / pan / tuning functions look identical.

Group is the last level of the AVG hierarchy as exposed via these functions. At the engine level, Groups contain Zones (one per note/velocity region), and Zones contain Voices - alternate samples used for round-robin playback. Zone selection and round-robin cycling are normally handled by the engine automatically. Round robins can be manually overridden per-note via NoteData.forceRoundRobinIndex(); Zones themselves aren't reachable from scripts.

NoteData


A NoteData describes a note before it plays - its pitch, velocity, envelope, timing, and which articulation / variation / group / mic / bus it routes through. KODA hands one to your noteOn and noteOff handlers describing each incoming MIDI note, and you can also build your own by calling createNoteData() on an Articulation, Variation, or Group.

Once a NoteData is configured, trigger it with noteData.start() to play it through its current routing (see Triggering below). A NoteData is just a value. Changing it after the note has started has no effect on the playing note (use the returned Note handle for that).

Always blank a NoteData before you use it

A NoteData you declare yourself starts out full of zeros - zero volume, zero tuning - so it plays nothing, silently. Blanking it sets those values to "inherit from the Group" instead. Three ways to get a usable note:

How you get itRoutingWhen to use it
initialize()
(after declaring one)
not set The everyday case. Play it with art.startNote(note) and the Articulation supplies the routing.
a copy of a note
you were handed
copied Echoes and doubled layers. A note from noteOn is already blanked, but you inherit its pitch and velocity too.
createNoteData()
on an Articulation,
Variation, or Group
set for you When the note carries its own destination - queuing it in a Pattern, or starting it with note.start().

Example: Build a note from scratch

// Holding the sustain pedal plays middle C on the current articulation.

Note playing;                       // Handle to the note, so we can stop it again

event SustainPedal(Parameter parameter)
{
    Articulation art = getCurrentArticulation();

    if(parameter.isTrue())          // Pedal down: build a note and play it
    {
        NoteData note;              // Build a note from scratch
        note.initialize();          // Blank it, or it plays silently
        note.setNoteNumber(60);     // Middle C
        note.setMidiVelocity(100);

        playing = art.startNote(note);
    }
    else                            // Pedal up: stop it
    {
        playing.stop();
    }
}
Pre-release: missing getters

NoteData has setters for most of its fields, but several don't yet have matching getters. If you can't find a getter for a value you've set, the function is on the to-do list. For values you set yourself, the workaround is to keep a copy in module-level state.

Example: Add an octave-up echo to every note

Note[128] echoes;   // One handle per MIDI note, so each echo can be stopped

event initialize() { setIgnoreMidiNotes(false); }   // Original note pressed by user is handled automatically

event noteOn(NoteData incoming)
{
    Articulation art = getCurrentArticulation();

    // Build a copy of the incoming note, an octave higher and half as loud.
    // KODA still triggers the original note normally.
    NoteData echo = art.createNoteData();
    echo.setNoteNumber(incoming.getNoteNumber() + 12);
    echo.setVelocity(incoming.getVelocity() * 0.5f);

    echoes.at(incoming.getNoteNumber()) = art.startNote(echo); // Start echo manually and store note handle
}

// Fires when the user lifts a key
event noteOff(NoteData incoming)
{
    Note echo = echoes.at(incoming.getNoteNumber()); // Retrieve note handle
    echo.stop(); // End echo manually
}

Pitch & velocity

Set or read the note's pitch (note number, tuning offsets, transposition) and velocity. Velocity has two unit conventions exposed as separate function pairs - float (0.0-1.0) and MIDI integer (0-127) - that read and write the same underlying value.

FunctionReturnsDescription
setNoteNumber(int note) void Sets the MIDI note number. Range: 0-127 (clamped).
getNoteNumber() int Returns the MIDI note number (before transposition).
getNoteNumberWithTransposition() int Returns the effective MIDI note that will be played (note number + transposition).
getNoteName() string Returns the note name as a string (e.g. "C3", "F#2"). Middle C (MIDI 60) is "C3".
transpose(int interval) void Sets transposition to interval semitones (replaces, doesn't accumulate).
transposeToNote(int note) void Sets transposition so the note plays as note (e.g. transpose every incoming note to middle C).
getTransposition() int Returns the current transposition in semitones.
setVelocity(float velocity) void Sets velocity as a float. Range: 0.0-1.0 (clamped).
setMidiVelocity(int velocity) void Sets velocity from a MIDI value. Range: 0-127.
getVelocity() float Returns velocity as a float (0.0-1.0).
getMidiVelocity() int Returns velocity as a MIDI integer (0-127).
setTuningSemitones(float semitones) void Sets fine-tuning in semitones (continuous, not quantised to MIDI notes).
setTuningCents(float cents) void Sets fine-tuning in cents. 100 cents = 1 semitone.
setTuningRatio(float ratio) void Sets tuning as a frequency ratio (2.0 = one octave up, 0.5 = one octave down).
setPitchShift(float semitones) void Shifts the note's pitch without changing its playback speed or sample timbre, using the time-stretch engine. 0.0 = no shift. Distinct from setTuning*, which retunes by changing the sample's playback rate (faster/slower, with a shifted timbre).

Gain & pan

FunctionReturnsDescription
setGain(float gain) void Sets gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity. Clamped.
setVolume(float decibels) void Sets the note's volume in decibels. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.TipAlso accepts a Parameter directly: note.setVolume(myVolumeKnob).
getGainDecibels() float Returns the current gain in decibels.
setPan(float pan) void Sets pan position. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center. Clamped.

Delay, length & stretch

Control where in the sample to start playback (sample start), how long to delay before triggering, and how time-stretched it is.

FunctionReturnsDescription
setDelaySamples(int delaySamples) void Sets the delay before the note starts, in audio samples.
setDelayMS(float delayMS) void Sets the delay before the note starts, in milliseconds.
setDelaySeconds(float delaySeconds) void Sets the delay before the note starts, in seconds.
setLengthMS(float lengthMS) void Schedules the note to release itself this many milliseconds after it starts. 0 = no scheduled release. Absolute time - a later tempo change doesn't rescale it.
setLengthSeconds(float lengthSeconds) void Schedules the note's release, in seconds.
setLengthSamples(int lengthSamples) void Schedules the note's release, in audio samples.
setLengthBeats(float64 lengthInBeats) void Schedules the note's release a number of beats after it starts, converted using the tempo at the moment of the call - so call it in the same handler that fires the note. Reads 0 in initialize(). For a gate that keeps tracking tempo inside a Pattern, use addNote's length argument instead.
setSampleStart(float sampleStartMS) void Sets the starting offset within the source sample, in milliseconds. 0 = play from the beginning. Milliseconds are used rather than samples because the triggered voice may run at a different sample rate than the engine. In Normal (streaming) playback the start can reach at most the group's Start Range, which is the window preloaded for every voice - set Start Range to at least the furthest start the script uses. In Sampler and TimeStretch modes the whole sample is in memory and any start is honoured.
setSampleStartMS(float sampleStartMS) void Alias of setSampleStart - sets the starting offset within the source sample, in milliseconds.
setStretchRatioByLength(float stretchRatio) void Sets time-stretch as a length ratio. 1.0 = no change; 2.0 = twice as long.
setStretchRatioBySpeed(float speedRatio) void Sets time-stretch as a speed ratio. 1.0 = no change; 2.0 = twice as fast (half as long).
getTimeElapsedSeconds() float For notes already playing, returns the time elapsed since the note started, in seconds.

Envelope

Override the note's envelope stages individually, or apply a whole envelope at once via setEnvelope(). Curve exponents take values in the range 0.5-2.0: 0.5 = convex (fast start, slow finish), 1.0 = linear, 2.0 = concave (slow start, fast finish).

FunctionReturnsDescription
setPreDelay(float preDelay) void Sets pre-delay before the envelope begins, in seconds.
setAttack(float attack) void Sets attack time in seconds. 0.0 = instant.
setAttack(float attack, float curve) void Sets attack time and curve exponent together.
setHold(float hold) void Sets hold time in seconds (after attack, before decay begins).
setDecay(float decay) void Sets decay time in seconds.
setDecay(float decay, float curve) void Sets decay time and curve exponent together.
setSustain(float sustain) void Sets sustain level. Range: 0.0 (silence) to 1.0 (full level). Clamped.
setRelease(float release) void Sets release time in seconds.
setRelease(float release, float curve) void Sets release time and curve exponent together.
setEnvelope(Envelope envelope) void Applies an entire envelope (all stages and curves) at once.
scheduleRelease(float timeToRelease, float releaseTime, float releaseCurve) void Schedules an automatic release a fixed time after the note starts. Older helper - prefer a length setter plus setRelease(), which do the same job as separate calls.

Behavior flags

FunctionReturnsDescription
setOneShot(bool oneShot) void Toggles one-shot mode. true = note plays through to completion regardless of MIDI note-off. false = note respects note-off.
forceRoundRobinIndex(int rr) void Overrides automatic round-robin cycling and plays the specified round-robin index for this note. rr: 0-based index into the available round robins.

Routing

Direct the note through a specific articulation, variation, group, mic, or bus. The setTarget(...) overloads are the most ergonomic. They set the relevant indices in one call. For low-level control, use the individual index setters; indices of -1 mean "no override - use the engine's default routing".

FunctionReturnsDescription
setTarget(Articulation articulation)
setTarget(Variation variation)
setTarget(Group group)
void Routes the NoteData through the given Articulation, Variation, or Group. Indices below the target's hierarchy level are cleared (e.g. passing a Variation sets the articulation and variation indices but clears the group index).
setArticulationIndex(int index) void Low-level: routes through the Articulation at this 0-based index.
setVariationIndex(int index) void Low-level: routes through the Variation at this 0-based index.
setGroupIndex(int index) void Low-level: routes through the Group at this 0-based index.
setMicIndex(int index) void Forces the note through a specific Mic (by 0-based index).
setBusIndex(int index) void Routes the note's output to a specific Bus (by 0-based index).
setLayerIndex(int index) void Sets the velocity layer index used when looking up the sample.
getGroupType() GroupType Returns the most specific routing level set on this NoteData: GroupType::Group, ::Variation, ::Articulation, or ::Invalid if no routing is set.
getArticulation() Articulation Returns the Articulation this NoteData routes through.
getVariation() Variation Returns the Variation this NoteData routes through.
getGroup() Group Returns the Group this NoteData routes through.

Triggering

Convert a configured NoteData into a playing note. The note plays through the routing set on the NoteData (via setTarget(...) or inherited from the incoming noteOn event).

FunctionReturnsDescription
initialize() void Blanks the note so it inherits the Group's envelope, volume and tuning. Call it right after declaring a NoteData. See Always blank a NoteData before you use it.
startNote()
start()
Note Triggers this NoteData and returns a Note handle you can use to modify or stop the note while it plays. Both read the routing off the note itself, so the note must already know its destination - from createNoteData() or setTarget(...). To play a note that has no routing of its own, pass it to an Articulation, Variation, or Group instead: art.startNote(note).

Example: Make soft notes longer and breathier

event initialize() { setIgnoreNoteOns(true); }  // Override automatic note-on handling

event noteOn(NoteData note)
{
    if(note.getVelocity() < 0.4f)
    {
        // Slow swell, gentle release for soft notes (curve 2.0 = slow start / fast end)
        note.setAttack(0.5f, 2.0f);
        note.setRelease(2.0f, 2.0f);
    }

    // Use the routing already on the incoming note - no need to fetch the articulation
    note.start();
}

Note


A Note is a handle to a currently-playing note. You get one back from startNote() on an Articulation, Variation, or Group, and you use it to modify or stop that specific note while it's still sounding.

Every Note function returns a bool: true if the note was still active and the change was applied, false if the note had already finished. That return value lets you safely call functions on a Note without first checking isActive().

Example: Apply a longer release to every incoming note

event initialize() { setIgnoreNoteOns(true); }  // Override automatic note-on handling

event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    Note playing = art.startNote(note);

    // Stretch the release out to 2 seconds with a slightly concave curve (slow start)
    playing.setRelease(2.0f, 1.5f);
}

Lifecycle

FunctionReturnsDescription
isActive() bool Returns true if the note is still playing (not yet released to silence).
stop() bool Triggers the note's release stage using its currently-configured envelope. Returns true if the note was active when stopped.
getTypeName() string Returns the type name as a string ("Note"). Useful for generic logging.
Pattern

To fade an active note out with a custom timing - for legato slurs, voice stealing, or any "active fade-out from script" - pair setRelease(lengthSeconds, curveExponent) with stop(). The first call overrides the envelope; the second triggers the fade using those new settings.

Calling stop() alone uses whatever release was configured when the note started. Calling setRelease() alone changes what will happen when the note is later released (e.g. when the user lifts the key), but doesn't itself produce sound.

Per-note properties

Adjust gain, tuning, length, time-stretch, pan, and release on a single note while it's playing. Each function returns true if the change landed (the note was still active), false if the note had already finished.

Absolute setters

These setters overwrite the property's current value rather than adding to it. Each call replaces what the previous call set, so if two modulators want to influence the same property in the same audio block - e.g. a legato pitch glide running alongside a vibrato LFO - sum their contributions in script and apply the total with a single call.

FunctionReturnsDescription
setGain(float gain) bool Sets the note's gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity.
setVolume(float decibels) bool Sets the playing note's volume in decibels. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.
setTuningSemitones(float semitones) bool Sets the note's tuning offset in semitones. 0.0 = no change.
setTuningCents(float cents) bool Sets the note's tuning offset in cents. 100 cents = 1 semitone.
setTuningRatio(float ratio) bool Sets tuning as a frequency ratio. 1.0 = no change; 2.0 = one octave up.
setPitchShift(float semitones) bool Shifts the playing note's pitch without changing its playback speed or sample timbre, using the time-stretch engine. 0.0 = no shift. Distinct from setTuning*, which retunes by changing the sample's playback rate.
setLength(float lengthSeconds) bool Sets the total length of the note in seconds. No hard clamp.
setStretch(float stretch) bool Sets the time-stretch factor. 1.0 = no change; > 1.0 = slower playback; < 1.0 = faster playback.
setStretchRatioByLength(float stretchRatio) bool Sets time-stretch as a length ratio (2.0 = twice as long).
setStretchRatioBySpeed(float speedRatio) bool Sets time-stretch as a speed ratio (2.0 = twice as fast).
setPan(float pan) bool Sets the note's pan position. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center.
setRelease(float lengthSeconds, float curveExponent) bool Configures the note's release envelope. lengthSeconds: release time (0.0 = instant). curveExponent: range 0.5-2.0; 0.5 = convex, 1.0 = linear, 2.0 = concave.NoteThis only updates the envelope settings. It does not trigger the release. The new settings take effect when the note is released, either by the user lifting the key or by an explicit stop() call.

Insert FX

FunctionReturnsDescription
setParameter(int insertIndex, int paramIndex, float value) bool Sets a parameter on the note's insert FX chain. insertIndex: 0-based insert slot. paramIndex: 0-based parameter on that insert. value: float value (range depends on the insert parameter).

Example: Vary release length by velocity

event initialize() { setIgnoreNoteOns(true); }  // Override automatic note-on handling

event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    Note playing = art.startNote(note);

    // Scale the release straight off velocity: harder hits ring out longer
    float releaseSeconds = note.getVelocity() * 3.0f;    // velocity is 0.0-1.0, so 0.0-3.0 seconds
    playing.setRelease(releaseSeconds, 1.0f);            // 1.0 = a linear fade
}

HeldNotes


A HeldNotes is a snapshot of every MIDI note the user is currently holding down - useful for chord recognition, arpeggiator-style logic, or any script behavior that needs to know what's pressed at a given moment. Get one via the free function getHeldNotes(); for a quick single-note query, use isNoteHeld(int) instead.

HeldNotes stores up to 128 note numbers. The size member variable tells you how many are currently held.

Host MIDI only

HeldNotes tracks notes the user is physically holding on a keyboard or sending from the host DAW. Notes you trigger from a script via startNote() do not register as held. Only incoming MIDI does.

Example: Detect chords by counting held notes

// Logs to the debug console when three or more notes are pressed at once.

event initialize()
{
    setLogEnabled(true);          // Without this, debug.log() output is silent
    setIgnoreMidiNotes(false);    // Make sure incoming MIDI notes still play
}

event noteOn(NoteData note)
{
    HeldNotes held = getHeldNotes();

    if(held.size >= 3)
        debug.log("Chord detected, notes:", held.size);
}

Functions

FunctionReturnsDescription
isEmpty() bool Returns true if no notes are currently held.
getHeldNote(int index) int Returns the MIDI note number at the given 0-based index. Notes are ordered lowest to highest, not in the order they were pressed. Returns -1 when the index is past the last held note; a negative index is treated as 0.
visit<Type>(Type& visitor) void Generic-visitor iteration. Calls visitor.processHeldNote(int) for every held note. Pass any struct that defines a processHeldNote(int) function.

Member Variables

Member VariableTypeDescription
size int Number of notes currently held.

Time-varying modulation


The script-side API has no built-in glide, fade, or ramp helpers for active notes. There's no glideTo(target, durationSec) or fadeOutOver(durationSec). Instead, you interpolate values yourself in event process() and call Note's per-note setters (setTuningSemitones, setGain, etc.) each audio block. This pattern is the standard answer to "how do I make X change smoothly over time".

The process() event

event process() fires once per audio block (typical block size 64 samples ≈ 1.3 ms at 48 kHz). Use it to advance any time-varying state - pitch glides, fade-outs, scripted LFOs, parameter sweeps - and apply the result to active notes.

event process()
{
    // runs every block - keep work small and predictable
}

Use process() for anything you can hear, and viewRefresh() for anything you can see. viewRefresh() fires at 12-60 Hz, far too slow to step a glide smoothly, and its frame timing isn't guaranteed, so you can't measure elapsed time from it. process() gives you both: a fast rate, and an exact time step you can calculate.

Block-delta seconds

To advance a glide, fade, or any other value that changes over time, you need to know how much time has passed between block calls. Divide the block size (samples) by the sample rate:

event process()
{
    float blockSec = float(getBlockSize()) / float(getSampleRate());
    // blockSec ≈ 0.00133 at 48 kHz / 64-sample blocks

    // ... advance an elapsed-time counter, look up a position, etc.
}

Example: 200 ms pitch glide

Animate a Note's tuning offset linearly from a starting value down to zero over a fixed duration. The state lives at module level so it persists across block calls.

Note currentNote;                        // Handle to the note currently gliding
bool gliding;                            // True while a glide is in progress
float glideElapsedSec;                   // Seconds since this glide started
const float kGlideDurationSec = 0.2f;    // Glide length. Portamento usually sits around 0.1 - 0.3
const float kGlideSemitones = -1.0f;     // Glide distance. Negative starts below pitch, positive above

event initialize() { setIgnoreNoteOns(true); }  // The script plays the note, not KODA's default routing

event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    currentNote = art.startNote(note);   // Keep the handle so we can retune the note while it sounds

    // Start off-pitch by kGlideSemitones, then glide up to natural pitch
    currentNote.setTuningSemitones(kGlideSemitones);
    glideElapsedSec = 0.0f;
    gliding = true;
}

event process()                          // Runs once per audio block
{
    if(!gliding)                         // Nothing to do between glides
        return;

    float blockSec = float(getBlockSize()) / float(getSampleRate());   // How much time one block covers
    glideElapsedSec += blockSec;

    float ratio = min(glideElapsedSec / kGlideDurationSec, 1.0f);      // 0.0 at the start, 1.0 when finished
    currentNote.setTuningSemitones(kGlideSemitones * (1.0f - ratio));  // Shrink the offset to zero as ratio rises

    if(ratio >= 1.0f)
        gliding = false;
}
One note at a time

A single Note member variable holds one note, so playing a second note before the first finishes hands the glide to the new one and leaves the previous note stuck at whatever tuning it had reached. To glide chords or overlapping notes, keep a Note per MIDI note number in an array and give each one its own elapsed-time counter.

Combining glide and vibrato on the same note

The scripted glide and vibrato below are both intended to change the same note's tuning. The problem is that note setters are absolute, so each call replaces the last one. If the two wrote their own values, you'd hear only whichever ran last. The solution is to add their offsets into one total instead, and apply that in a single call.

Note currentNote;
bool gliding;
float glideElapsedSec;
bool vibratoActive;
float vibratoPhase;                          // Kept between blocks so the sine stays continuous
const float kGlideDurationSec = 0.2f;
const float kGlideSemitones = -1.0f;
const float kVibratoHz = 5.0f;               // Vibrato speed in cycles per second
const float kVibratoDepthSemitones = 0.15f;  // Roughly a 15 cent wobble either side

event initialize() { setIgnoreNoteOns(true); }  // The script plays the note, not KODA's default routing

event noteOn(NoteData note)
{
    Articulation art = getCurrentArticulation();
    currentNote = art.startNote(note);

    glideElapsedSec = 0.0f;                  // Start both modulators from scratch on every note
    gliding = true;
    vibratoPhase = 0.0f;
    vibratoActive = true;
}

event process()
{
    float blockSec = float(getBlockSize()) / float(getSampleRate());

    float totalSemitones = 0.0f;             // Each modulator adds its offset here

    if(gliding)
    {
        glideElapsedSec += blockSec;
        float ratio = min(glideElapsedSec / kGlideDurationSec, 1.0f);
        totalSemitones += kGlideSemitones * (1.0f - ratio);
        if(ratio >= 1.0f) gliding = false;
    }

    if(vibratoActive)
    {
        // Phase counts whole cycles, so scale it by twoPi for sin().
        // twoPi is a Cmajor built-in constant, and a float64, so cast it to keep the math in float
        vibratoPhase += kVibratoHz * blockSec;
        totalSemitones += kVibratoDepthSemitones * sin(vibratoPhase * float(twoPi));
    }

    // One call carries both offsets
    currentNote.setTuningSemitones(totalSemitones);
}
Per-note state lives in the script

If you want to remember something extra about a note - its vibrato phase, a glide timer, a custom envelope counter - there's nowhere to attach that data to the Note itself. Instead, declare your variables at the top of the script (outside any event handler) so they persist between calls.

For a single-voice instrument, plain variables are enough. See the glide example above. For polyphonic instruments, use an array with one slot per MIDI note (size 128) or one slot per voice the script keeps track of.

Bus


A Bus is one of the audio routing destinations in your instrument - the master bus, a mic bus, or an FX bus. It's the level you'd reach to adjust output gain, route signal between buses, or toggle an insert FX on a shared FX chain rather than per-Group.

Buses are also how a signal becomes visible to the user. Each one can appear as a channel strip in the plugin's Mix tab with its own fader, pan, solo and mute buttons, and meters, so anything you want the user to mix has to live on a bus.

Get a Bus via the free functions getMasterBus() or getBus() (by name or 0-based index).

Sends are still filling out

getSend(int index) and getSend(string name) both return a Send, not a Bus. A Send can set pan, solo, mute, output index, and its insert FX parameters and bypass, but it has no gain control yet.

Set send levels on the Bus that's sending: bus.setSendLevel(int sendIndex, float level) or bus.setSendLevelDecibels(int sendIndex, float decibels), up to 8 send slots.

Send levels only work on the buses you see in the mixer. Setting one on the bus from getMasterBus() is accepted but never heard, since nothing is tapped from the master bus.

Example: Drive a reverb send from a MIDI controller

// Fires when Expression changes (built-in, MIDI CC 11 by default).
// Repurposed here as a reverb-send amount for portable testing.
// (Assumes a reverb sitting on the first send bus.)
event Expression(Parameter parameter)
{
    Bus main = getBus("Main");   // The first bus in the mixer, called "Main" by default

    // Send a portion of that bus's output to the first send bus (send index 0)
    main.setSendLevel(0, parameter);
}
Shared setter pattern

Most setter functions on Bus (gain, pan, bypass, solo, mute, send level, insert parameters, bypassInsert) accept either a primitive value or a Parameter directly. See the Parameter convenience at the top of this reference.

Identity & state

FunctionReturnsDescription
isIdValid() bool Returns true if this Bus refers to a real bus in the instrument. Use as a null check before calling functions on the Bus.
validate() void Logs whether this Bus is valid (and its index if so) to the console.
getTypeName() string Returns the type name as a string ("Bus"). Useful for generic logging.

Gain & pan

FunctionReturnsDescription
setGain(float gain) void Sets the Bus's gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity.
setVolume(float decibels) void Sets the Bus's volume in decibels - the same value, and the same range, as the bus fader in the KODA app's mixer. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.TipAlso accepts a Parameter directly: bus.setVolume(myVolumeKnob).
setPan(float pan) void Sets pan position. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center.

Bypass, solo & mute

Toggle the Bus's playback state. These mirror the bypass, solo, and mute buttons users see on each bus in the plugin's mixer.

FunctionReturnsDescription
setBypass(bool bypass) void Bypasses (or un-bypasses) the entire Bus. true = bypassed (Bus disabled), false = active. This is also how you enable and disable mics: bypassing a Bus drops the preloaded samples of every mic routed to it.
setSolo(bool solo) void Solos the Bus. While any bus is soloed, only soloed buses are audible.
setMute(bool mute) void Mutes the Bus. true = silenced, false = audible.

Send levels

Each Bus can send a portion of its signal to the instrument's send buses (up to 8 of them). Use this to drive parallel effects like reverbs and delays.

FunctionReturnsDescription
setSendLevel(int sendIndex, float level) void Sets the send level on this Bus. sendIndex: 0-based index into the instrument's send buses (up to 8). level: typically 0.0 (no send) to 1.0 (unity send); no hard clamp.
setSendLevelDecibels(int sendIndex, float decibels) void Same as setSendLevel but in decibels - the same value, and the same range, as the send control in the KODA app. Range: -96 dB (send off) to +12 dB; 0 dB is unity. Anything at or below -96 dB turns the send fully off.TipAlso accepts a Parameter directly: bus.setSendLevelDecibels(0, mySendKnob).

Insert FX

Like Groups, each Bus can host a chain of insert FX defined in the instrument. These functions let you modulate individual insert parameters or bypass entire inserts at runtime.

FunctionReturnsDescription
setInsertParameter(int insertIndex, int paramIndex, float value) void Sets a parameter on an insert FX. insertIndex: 0-based insert slot. paramIndex: 0-based parameter on that insert. value: the new value as a float. For switch-type parameters use 0.0f / 1.0f; to bypass a whole insert, use bypassInsert below.TipAlso accepts a Parameter directly: bus.setInsertParameter(0, 0, myKnob).
bypassInsert(int insertIndex, bool bypassed) void Bypasses (or un-bypasses) an entire insert FX slot on this Bus. true = bypassed, false = active.TipIf you're wiring this to an "Enabled" button (where true means the user wants the effect on), invert the value: bypassInsert(0, !param.getBoolValue()).
Technical sidenote

Bus uses bypassInsert(index, bypassed) where Group uses setInsertBypassed(index, bypassed). The two do the same conceptual thing - toggle an insert slot - but the function names differ. setInsertParameter is spelled the same on both.

Example: Bypass a reverb on the master bus

// Fires when SustainPedal changes (built-in, MIDI CC 64 by default).
// Repurposed here as a reverb-bypass toggle for portable testing.
// (Assumes a reverb at insert slot 0 of the master bus.)
event SustainPedal(Parameter parameter)
{
    Bus master = getMasterBus();

    // Pass the button state straight to the insert's bypass flag
    master.bypassInsert(0, parameter);
}

Output levels

Read a bus's current output meter. getLevels() returns a Levels snapshot, refreshed once per audio block. Read it in event viewRefresh() or event process() to drive meters.

FunctionReturnsDescription
getLevels() Levels Current output levels of this bus (also callable as the free function getLevels(myBus)).

Levels

A read-only snapshot of a bus's output meter, returned by bus.getLevels(). Linear values run from 0.0 (silence) to ~3.98 (+12 dB), with 1.0 at unity (0 dB). Decibel values are floored at -180 dB, so a silent bus reads exactly -180.

FunctionReturnsDescription
getLeft()floatLeft channel, linear (see range above).
getRight()floatRight channel, linear (see range above).
getLeftDecibels()floatLeft channel in dB (floored at -180).
getRightDecibels()floatRight channel in dB (floored at -180).
getPeak()floatMax of left/right, linear (see range above).
getPeakDecibels()floatPeak in dB (floored at -180).
getAverage()floatMean of left/right, linear (see range above).
getAverageDecibels()floatAverage in dB (floored at -180).

Mic


A Mic represents one of the microphone positions in your instrument - the same mics users see in the plugin's mic mixer. Mics are defined at the clip level: when you import samples, the Tokenizer identifies a Signal (Mic) token in each filename (e.g. Close, Room, Overheads), and the unique set across all clips becomes the instrument's mic list. Each mic becomes an object you can address by name or index.

Get a Mic via the free functions getMic(string name) or getMic(int index).

Mic functions cover level and position: gain, pan, solo, and mute. To enable or disable a mic entirely, bypass the Bus it's routed to, which drops the preloaded samples of every mic on that bus.

Example: Tie a mic's gain to a MIDI controller

// Fires when BreathController changes (built-in, MIDI CC 3 by default).
// Repurposed here as close-mic gain for portable testing. It starts at 1.0,
// so the mic sits at full gain until the controller is moved.
// (Assumes a mic named "Close" in the instrument.)
event BreathController(Parameter parameter)
{
    Mic close = getMic("Close");   // Rename this to any mic you want to control

    // Pass the knob value straight to the mic's gain
    close.setGain(parameter);
}
Shared setter pattern

Every setter on Mic (gain, gain in dB, pan, solo, mute) accepts either a primitive value or a Parameter directly. See the Parameter convenience at the top of this reference.

Identity & state

FunctionReturnsDescription
isIdValid() bool Returns true if this Mic refers to a real mic in the instrument. Use as a null check before calling functions on the Mic.
validate() void Logs whether this Mic is valid (and its index if so) to the console.
getTypeName() string Returns the type name as a string ("Mic"). Useful for generic logging.

Gain & pan

FunctionReturnsDescription
setGain(float gain) void Sets the Mic's gain (linear). Range: 0.0 (silence) to 4.0 (~+12 dB); 1.0 is unity.
setVolume(float decibels) void Sets the Mic's volume in decibels - the same value, and the same range, as that mic's fader in the KODA app's mic mixer. Range: -96 dB (silence) to +12 dB; 0 dB is unity. Anything at or below -96 dB is exact silence.TipAlso accepts a Parameter directly: mic.setVolume(myVolumeKnob).
setPan(float pan) void Sets pan position. Range: -1.0 (full left) to 1.0 (full right); 0.0 is center.

Solo & mute

These mirror the solo and mute buttons users see for each mic in the plugin's mic mixer. To enable or disable a mic, bypass its Bus - see the note at the top of this section.

FunctionReturnsDescription
setSolo(bool solo) void Solos the Mic. While any mic is soloed, only soloed mics are audible.
setMute(bool mute) void Mutes the Mic. true = silenced, false = audible.

Example: Toggle a mic on/off from a MIDI controller

// Fires when SustainPedal changes (built-in, MIDI CC 64 by default).
// Repurposed here as a Room-mic on/off toggle for portable testing.
event SustainPedal(Parameter parameter)
{
    Mic room = getMic("Room");   // Rename this to any mic you want to control

    // Pedal down silences the Room mic, pedal up brings it back
    room.setMute(parameter);
}

Modulator


A Modulator represents an LFO or envelope defined in your instrument - the same modulators users see attached to parameters in the plugin's modulation panel. From a script, you can adjust the modulator's shape (LFO frequency, waveform, depth) or envelope (attack, decay, sustain, release) at runtime, and enable or bypass it.

Get a Modulator via the free functions getModulator(string name) or getModulator(int index).

Depth chain

A modulator's effective output is the product of three things: its raw signal (LFO waveform output or envelope shape), the script-side depth amount (setLFODepth for LFOs, setEnvelopeSustain for envelopes), and the routing amount set in the modulation panel in the KODA app when the modulator was attached to its destination.

The script's depth is a normalized 0..1 multiplier. The absolute cents, semitones, or dB applied to the target come from the routing amount. If you've called setLFODepth(1.0) and still hear no modulation, open the modulation panel, find the modulator's attachment to its destination parameter, and check that the routing amount isn't zero.

Example: Tie vibrato depth to a MIDI controller

// Dynamics controls modulator depth (built-in, MIDI CC 1 by default).
// (Assumes a Modulator named "Vibrato" in the instrument.)
event Dynamics(Parameter parameter)
{
    Modulator vibrato = getModulator("Vibrato");

    // 0.0 = no vibrato, 1.0 = full vibrato
    vibrato.setLFODepth(parameter);
}
Shared setter pattern

Every setter on Modulator (enable, all LFO controls, all envelope controls) accepts either a primitive value or a Parameter directly. See the Parameter convenience at the top of this reference.

Enable & bypass

FunctionReturnsDescription
setEnabled(bool enabled) void Enables or bypasses the Modulator. true = enabled (active), false = bypassed (modulator has no effect).

LFO controls

These shape the LFO's behavior - rate, waveform, phase, depth, and whether the LFO runs globally (shared across all notes) or per-note (each note starts a fresh phase).

FunctionReturnsDescription
setLFOFrequency(float frequency) void Sets the LFO frequency in Hz. 0.0 = stopped. Typical range 0.01 to 100 Hz; no hard clamp.
setLFOWaveform(int waveform) void Sets the LFO waveform by index: Sine = 0, Triangle = 1, Saw = 2, Square = 3, SampleHold = 4, Drift = 5. SampleHold is the app's "Sample & Hold" (random steps); Drift glides smoothly through the same random sequence.
setLFOPhaseOffset(float phaseOffset) void Sets the LFO phase offset (normalized). Range: 0.0 to 1.0 (wraps); 0.5 = half cycle.
setLFOBipolar(bool bipolar) void Sets whether the LFO output is bipolar. true = -1 to 1 (centered on 0), false = 0 to 1 (centered on 0.5).
setLFOGlobal(bool global) void Sets whether the LFO is global or per-note. true = global (all notes share phase), false = per-note (each note has independent phase).
setLFODepth(float depth) void Sets the LFO depth. Range: 0.0 (no modulation) to 1.0 (full routing-defined range). The absolute cents/semitones/dB are set by the routing in the editor. This just scales them.

Envelope controls

These shape the envelope's pre-delay, attack-hold-decay-sustain-release stages, and their curve exponents.

FunctionReturnsDescription
setEnvelopePreDelay(float preDelay) void Sets pre-delay in seconds. 0.0 = no delay. Typical range 0.0-10.0 seconds; no hard clamp.
setEnvelopeAttack(float attack) void Sets attack time in seconds. 0.0 = instant. Typical range 0.01-10.0 seconds.
setEnvelopeAttackCurve(float attackCurve) void Sets the attack curve exponent. Range: 0.5-2.0. 0.5 = convex (fast start, slow finish), 1.0 = linear, 2.0 = concave (slow start, fast finish).
setEnvelopeHold(float hold) void Sets hold time in seconds. 0.0 = no hold. Typical range 0.0-10.0 seconds.
setEnvelopeDecay(float decay) void Sets decay time in seconds. 0.0 = instant. Typical range 0.0-10.0 seconds.
setEnvelopeDecayCurve(float decayCurve) void Sets the decay curve exponent. Same conventions as attack curve.
setEnvelopeSustain(float sustain) void Sets the sustain level. Range: 0.0 (silence) to 1.0 (full level).
setEnvelopeRelease(float release) void Sets release time in seconds. 0.0 = instant. Typical range 0.0-10.0 seconds.
setEnvelopeReleaseCurve(float releaseCurve) void Sets the release curve exponent. Same conventions as attack curve.

Example: Switch LFO waveform with the current articulation

// Switching the current articulation number also changes the LFO wave shape
// (in your own instrument, swap in the Parameter behind a menu widget)
// 0 = Sine, 1 = Triangle, 2 = Saw, 3 = Square, 4 = SampleHold, 5 = Drift
// (Assumes an LFO Modulator named "Tremolo" in the instrument.)
event CurrentArticulation(Parameter parameter)
{
    Modulator tremolo = getModulator("Tremolo");

    tremolo.setLFOWaveform(parameter);
}

Component


A Component is a handle to a single GUI element from your instrument's UI - a knob, button, menu, label, panel, animation, etc. In a Figma project, components appear in the left panel as layers marked in purple. From a script you can toggle its visibility, move it, set its value, bind it to a parameter, paint shapes on it, or drive its animation.

Get a Component via the free function getComponent(string controlID). The controlID is the name set on the element in Figma.

Example: Show or hide a panel from an on/off Parameter

// Fires when SustainPedal changes (built-in, MIDI CC 64 by default).
// Repurposed here as a panel visibility toggle for portable testing.
// (Assumes a Component named "MixerPanel" in the instrument UI.)
event SustainPedal(Parameter parameter)
{
    Component mixerPanel = getComponent("MixerPanel");

    mixerPanel.setVisible(parameter.getBoolValue());
}

Visibility & position

FunctionReturnsDescription
setVisible(bool visible) void Shows or hides the Component. true = visible, false = hidden.
setX(int x) void Sets the Component's x position relative to its parent, in design-space units - the same coordinates as the layout canvas, so the position holds as the window is resized.
setY(int y) void Sets the Component's y position relative to its parent, in design-space units.
setXInView(int x) void Sets the Component's x position relative to the whole view rather than to its parent. Useful for a Component nested in a Panel via addComponent, which would otherwise be positioned panel-relative. Leaves y unchanged.
setYInView(int y) void Sets the Component's y position relative to the whole view rather than to its parent. Leaves x unchanged.
setPositionInView(int x, int y) void Sets both axes at once, relative to the whole view. Coordinates are in the same design-space units as the layout canvas and setViewSize, so the position holds as the window is resized. For a Component parented directly to the view this matches setX/setY.
setBoundsInView(Bounds bounds)
setBoundsInView(int x, int y, int width, int height)
void View-relative setBounds: position and size together, both measured in the view. Under a scaled ancestor the Component ends up the requested size as seen in the view.
setAlpha(float alpha) void Sets the Component's opacity. Range: 0.0 (fully transparent) to 1.0 (fully opaque).
setEnabled(bool enabled) void Enables or disables interaction. true = interactive, false = grayed-out and unresponsive.

Value, text & state

FunctionReturnsDescription
setValue(float value) void Sets a numeric value on the Component. Valid range depends on the bound parameter or element type.
setText(string text) void Sets the text content (for labels, text fields, etc.).
setToggleState(bool state) void Sets the toggle state for buttons or switches. true = on/down/active, false = off/up/inactive.
setMenuSelection(int selection) void Sets the selected item on a menu by 0-based index.
setNumColumns(int columns) void Sets the number of columns for grid-style Components. Must be a positive integer.

Binding

Connect a Component to data sources at runtime: a UI parameter (so the component reflects and controls that parameter) or a panel variant (for Figma multi-variant panels).

FunctionReturnsDescription
setParameter(string parameterName) void Binds the Component to a UI Parameter by name. The Component will now reflect and control that parameter's value.
setPanelVariant(string variantName) void Switches a panel Component to a named Figma variant.

Painting

Draw shapes, text, or images onto a Component at runtime. The vector shapes (Ellipse, Rectangle, Arc, Path) are drawn with fill(...) and stroke(...): each takes the shape plus a Colour or a Gradient. fill fills the shape; stroke outlines it (using the shape's strokeWidth / strokeThickness). They are separate operations. To both fill and outline a shape, call fill and then stroke. Image and Text are drawn with paint(...). Every function draws behind the Component's children; the matching ...Over(...) form draws in front of them.

Not every Component type can be painted on. Paint onto a plain Component (no type set) or a panel; buttons, sliders, labels and animations work too. A menu authored in Figma, a level meter, a table, and an XY pad draw themselves instead, and discard script paint silently - no error, nothing drawn.

Coordinates are measured from the Component's own top-left corner, in the same units as your Figma layout, so a drawing keeps its place and proportions as the plugin is resized. A script can't read a Component's size back, so the numbers you paint with have to match the size the Component has in Figma. Keeping that size in a constant at the top of the script, as below, saves hunting for stray numbers when the layout changes.

What you paint stays on the Component until you clear it. If you repaint every frame from viewRefresh(), call clearCanvas() first. Otherwise each frame's drawing piles on top of the last and the Component fills up with old artwork. The usual shape is clear, then draw:

Example: Vector-painted pulse that follows the master level

// A pulse that grows and shifts from green to red with the master bus level.
// (Assumes a plain component named "LevelGlow", 100 x 100 px in Figma.)

const float kSize = 100.0f;      // must match the component's size in Figma (square)
const float kFloorDb = -48.0f;   // the level that maps to the smallest circle
const float kFallRate = 0.15f;   // how fast the circle shrinks back: 0 = frozen, 1 = instant

float displayLevel;              // 0..1, kept between frames so the fall is smooth

event initialize()
{
    setViewRefreshRate(60);      // the fastest UI rate KODA allows
}

event viewRefresh()
{
    Bus master = getMasterBus();
    Levels levels = master.getLevels();

    // Work in decibels. Linear gain spends most of its range close to silence,
    float db = levels.getPeakDecibels();                          // -180 (silence) .. 0 (unity)
    float target = limit((db - kFloorDb) / -kFloorDb, 0.0f, 1.0f);   // -48..0 dB becomes 0..1

    // Meter ballistics: jump straight up to peaks, then ease back down
    if(target > displayLevel)
        displayLevel = target;
    else
        displayLevel = displayLevel + (target - displayLevel) * kFallRate;

    Component glow = getComponent("LevelGlow");
    glow.clearCanvas();          // wipe last frame, or the circles stack up

    // Centered circle, growing from 10% to 100% of the component's width
    float diameter = kSize * (0.1f + displayLevel * 0.9f);
    float inset = (kSize - diameter) * 0.5f;
    Ellipse disc = (inset, inset, diameter, diameter, 0.0f);      // last member is strokeWidth

    // Hue sweeps green -> yellow -> red as the level rises; alpha brightens with it
    float hue = 0.33f * (1.0f - displayLevel);   // 0.33 = green, 0.16 = yellow, 0.0 = red
    glow.fill(disc, getColourHSBA(hue, 0.9f, 1.0f, 0.25f + displayLevel * 0.75f));
}
FunctionReturnsDescription
fill(Rectangle r, Colour|Gradient) void Fills a rectangle with a solid colour or a gradient (behind its children). Same overloads exist for Ellipse and Arc.
stroke(Rectangle r, Colour|Gradient) void Outlines a rectangle (line width = the shape's strokeWidth) with a colour or gradient. Same overloads exist for Ellipse and Arc.
fillPath(Path p, Colour|Gradient) void Fills a Path with a colour or gradient (behind its children).
strokePath(Path p, Colour|Gradient) void Strokes (outlines) a Path with a colour or gradient, using its strokeThickness / jointStyle / endCapStyle.
paint(Image image) void Paints an image onto the Component (behind its children).
paint(Text text) void Paints text onto the Component (behind its children). *** Not implemented yet: draws the placeholder word "Text" and ignores the Text struct. Use a Label Component and setText instead.
paint(Canvas canvas) void Paints a pre-recorded Canvas of paint actions onto the Component. Build a Canvas once, then reuse it across components.
clearCanvas() void Wipes everything previously painted on this Component. Call it at the top of viewRefresh() before redrawing, or each frame's artwork accumulates on the last.
setImageBacked(bool imageBacked) void false (the default) redraws as vectors, staying crisp at any resolution. true keeps the pixels between frames, so you can build an image up over time - a fading trail, for instance. Call before painting.
fillOver / strokeOver() void Same as fill / stroke but draws in front of the Component's children (Rectangle, Ellipse, Arc). Paths have fillPathOver / strokePathOver; Image/Text/Canvas have paintOver(...).
clearCanvasOver()
setImageBackedOver(bool imageBacked)
void The in-front-of-children versions of clearCanvas and setImageBacked. The two layers are kept separately, so clearing one leaves the other alone. A Component painted on both sides needs both calls to clear fully.
Technical sidenote

The behind-children and in-front-of-children layers hold independent drawings and independent image-backed settings. On a widget with no children the two look identical, so the choice only matters for containers like Panel, Menu, and ListItemBox.

Animation

For Components that hold animated content (e.g. Lottie or sprite-sheet animations from Figma), these functions control playback.

FunctionReturnsDescription
start() void Starts the animation playing.
pause() void Pauses the animation at its current frame.
stop() void Stops the animation.
reset() void Resets the animation to its first frame.
reverse(bool reversed) void Sets the playback direction. true = play backwards, false = play forwards.
setFrameRate(int fps) void Sets the animation frame rate in frames per second. Typical: 24-60 fps.
setFrame(int frame) void Jumps to a specific frame (0-based).
setLooping(bool isLooping) void Sets whether the animation loops. true = loop continuously, false = play once and stop.

Example: Drive an animation with a toggle button

// Fires when SustainPedal changes (built-in, MIDI CC 64 by default).
// Repurposed here as an animation play/pause toggle for portable testing.
// (Assumes an animated Component named "Spinner" in the instrument UI.)
event SustainPedal(Parameter parameter)
{
    Component spinner = getComponent("Spinner");

    if(parameter.getBoolValue())
        spinner.start();
    else
        spinner.pause();
}

Bounds, rotation & scale

Position, orient and scale a Component after creation.

FunctionReturnsDescription
setBounds(Bounds bounds)voidSets position and size from a Bounds.
setRotation(float radians)voidRotates about the center; angle in radians ( = full turn), 0.0 = none.
setScale(float scale)voidUniform scale about the center; 1.0 = original.
setScale(float scaleX, float scaleY)voidNon-uniform scale per axis; 1.0 = original on that axis.
bringToFront()voidRaises to the front of its sibling z-order.
getName()stringReturns the Component's control ID.

Text formatting

For Label, value-edit and text-edit Components.

FunctionReturnsDescription
setText(float value)voidSets text to a float (2 decimal places).
setText(float value, int numDecimals)voidSets text to a float with N decimals.
setText(int value)voidSets text to an integer.
clearText()voidClears the text.
setSuffix(string suffix)voidAppends a unit suffix to displayed text (e.g. " Hz").
setUnitType(int unitType)voidFormats the value with a unit; UnitType:: constants: None=0, FrequencyHz=1 (auto Hz/kHz above 1000), Decibels=2 (shows "-inf" at/below −60 dB), Percent=3, Milliseconds=4, Pan=5 (100L..C..100R), NoteName=6.
setAllowedCharacters(string characters)voidRestricts typeable characters on a text-edit (empty string restores default).
setFont(Font font)voidSets the font from a Font.

Effects & sprites

Attach visual effects and sprite sheets to a Component.

FunctionReturnsDescription
addEffect(Effect effect)voidAdds a visual effect (see Effect).
removeEffects()voidRemoves all effects.
setSprite(string path, int spriteWidth, int spriteHeight, int numFrames)voidAttaches a sprite sheet (per-frame size + frame count).
setSprite(SpriteSheet sprite)voidAttaches a sprite sheet from a SpriteSheet.

Containers & event routing

Nest Components and route their input to script event handlers.

FunctionReturnsDescription
addComponent(Component child)voidRe-parents a child into this (panel) Component so it moves and scales with it. Call on the parent: panel.addComponent(child).
sendMouseEvents(int identifier)voidRoutes this Component's mouse events to a script event handleMouse(MouseEvent mouseEvent) handler; read mouseEvent.getIdentifier() to tell components apart. (Attach to Panel/Label/Default widgets. Sliders and buttons consume their own mouse events.)
sendGuiEvents(int identifier)voidRoutes value/selection changes to a script event handleGuiEvent(GuiEvent guiEvent) handler; read guiEvent.getIdentifier() and guiEvent.getValue().

Menu & list items

For Menu and ListItemBox Components.

FunctionReturnsDescription
addItem(string name, int value)voidAdds an item (display label + user int reported on selection).
clearItems()voidRemoves all items.
setListSelection(int index)voidSelects a ListItemBox item by index (−1 clears).
clearMenuSelection()voidClears a Menu's selection (same as setMenuSelection(−1)).
setItemSize(int width, int height)voidPer-item pixel size.
setItemTextFont(Font font)voidItem text font.
setItemTextBounds(Bounds bounds)voidItem text layout bounds within each item.
setItemTextColour(Colour colour, int state)voidItem text colour per state; MenuItemState:: Normal=0, Hover=1, Selected=2.
setMenuBackgroundColour(Colour colour)voidMenu drop-down background.
setMenuOutlineColour(Colour colour)voidMenu drop-down border colour.
setMenuOutlineThickness(float thickness)voidMenu drop-down border thickness in pixels.

The viewRefresh() event

event viewRefresh() fires once per UI "frame" at a configurable rate (default 24 Hz). Use it to update painted UI and animated readouts.

Note

It is not synced to the actual GUI framerate, so don't rely on it for precise timing.

FunctionReturnsDescription
setViewRefreshRate(int hz)voidSets the viewRefresh rate; clamped 12-60 Hz (default 24). Only the instrument-level (root) script's setting takes effect.
getViewRefreshRate()intReturns the current viewRefresh rate in Hz.

Creating Widgets at Runtime


Beyond binding to widgets you laid out in Figma, a script can create widgets at runtime (e.g. in event initialize()). Each create function returns a Component handle you keep and drive like any other. Positions and sizes use a Bounds (x, y, width, height in pixels).

FunctionReturnsDescription
createButton(string name, Bounds bounds)ComponentCreates a button.
createPanel(string name, Bounds bounds)ComponentCreates a panel; use as a container (addComponent) or a paint surface.
createRotarySlider(string name, Bounds bounds)ComponentCreates a rotary slider (knob).
createHorizontalSlider(string name, Bounds bounds)ComponentCreates a horizontal slider.
createVerticalSlider(string name, Bounds bounds)ComponentCreates a vertical slider.
createLabel(string name, Bounds bounds, string text)ComponentCreates a text label with initial text.
createXYPad(string name, Bounds bounds, string parameterX, string parameterY)ComponentCreates an XY pad bound to two parameters by name (one per axis).
createMenu(string name, Bounds bounds)ComponentCreates a drop-down menu; populate with addItem().
createListItemBox(string name, Bounds bounds)ComponentCreates a scrollable list; populate with addItem(), track with setListSelection().
createSearchField(string name, Bounds bounds)ComponentCreates a text/search entry field.
createStateDisplay(string name, Bounds bounds)ComponentCreates a state-display widget.
createAnimation(string name, Bounds bounds)ComponentCreates an animation widget; drive with the Animation functions (start/pause/setFrame/…).

The name argument is the new Component's control ID (used later with getComponent).

A Component made this way arrives with no artwork, so it is invisible until you give it one, either by attaching a sprite sheet with setSprite or by painting on it. It is also inert until you connect it to something: setParameter links a button to a Parameter, so clicking it drives that Parameter and your handler for it runs. The example below does both.

Example: Build a Play button in the script

// Draws a rectangular green button in the upper left of the GUI
// (Assumes a Parameter named "PlayToggle" in the instrument.)

const float kButtonWidth = 240.0f;
const float kButtonHeight = 80.0f;

Component playButton;      // kept at script scope so later events can repaint it

event initialize()
{
    playButton = createButton("Play", Bounds(10.0f, 10.0f, kButtonWidth, kButtonHeight));
    playButton.setParameter("PlayToggle");   // clicks now drive that Parameter
    paintButton(false);                     // draw the "off" look right away
}

// Fires whenever PlayToggle changes, including the click that changed it
event PlayToggle(Parameter parameter)
{
    paintButton(parameter.isTrue());
}

// Paint coordinates are local to the button, so they start at 0, 0
void paintButton(bool playing)
{
    playButton.clearCanvas();

    // Rectangle members are x, y, width, height, strokeWidth, cornerRadius
    Rectangle body = (0.0f, 0.0f, kButtonWidth, kButtonHeight, 0.0f, 6.0f);
    playButton.fill(body, playing ? colours::limegreen : colours::dimgrey);
}
Bounds are in your UI's own coordinates

The x, y, width and height you pass are in the same coordinate space as your Figma layout, and KODA scales them with the window. On a wide layout, Bounds(10, 10, 80, 30) is a small control in the top-left corner, not a button that fills the screen.

Colour


Colour is the strong ARGB colour type used across the paint API, replacing the raw int32 values used in earlier builds. It has a single member variable value (int32, packed 0xAARRGGBB), but you normally build colours with the colours:: constructors and read or change them with the functions below rather than touching value directly.

Construction

The colours:: constructors are free functions that each return a Colour.

FunctionReturnsDescription
colours::fromRGB(int r, int g, int b)ColourOpaque colour from 8-bit channels (each 0-255).
colours::fromRGBA(int r, int g, int b, int a)ColourFrom 8-bit channels plus alpha (each 0-255). An overload takes a float alpha 0.0-1.0.
colours::fromRGB(int hex)ColourFrom a 24-bit hex RGB literal (e.g. 0xFF7F00), opaque.
colours::fromHex(int64 hex)ColourFrom a 32-bit ARGB literal. Use the _L suffix so an alpha ≥ 0x80 doesn't overflow int32 (e.g. 0xFFFFA500_L).
colours::fromFloat(float r, float g, float b, float a)ColourFrom float channels (each 0.0-1.0). A 3-arg overload omits alpha and returns an opaque colour.
colours::fromHSB(float h, float s, float b)ColourOpaque colour from HSB components (each 0.0-1.0). fromHSBA adds an alpha argument.
colours::fromHSL(float h, float s, float l)ColourOpaque colour from HSL components (each 0.0-1.0). fromHSLA adds an alpha argument.

There are also named constants such as colours::white, colours::black, colours::red, colours::orange, colours::dodgerblue, and colours::transparent, along with the full CSS/JUCE colour-name set, each a const Colour.

Channels & HSB

Read individual channels as 8-bit integers or normalized floats, or pull out HSB components.

FunctionReturnsDescription
getAlpha() / getRed() / getGreen() / getBlue()intChannel value 0-255.
getAlphaFloat() / getRedFloat() / getGreenFloat() / getBlueFloat()floatChannel value 0.0-1.0.
getHue() / getSaturation() / getBrightness()floatHSB component 0.0-1.0.

Transforms

Each transform returns an immutable copy; the original colour is unchanged.

FunctionReturnsDescription
withAlpha(float a)ColourCopy with alpha set (0.0-1.0). An int overload withAlpha(int a) takes 0-255.
withMultipliedAlpha(float m)ColourCopy with the current alpha multiplied by m.
brighter(float amt)ColourCopy blended toward white (0.0 = unchanged, 1.0 = white).
darker(float amt)ColourCopy blended toward black (0.0 = unchanged, 1.0 = black).
withHue(float h) / withSaturation(float s) / withBrightness(float b)ColourCopy with one HSB component replaced (each 0.0-1.0).
interpolatedWith(Colour other, float t)ColourLinear blend; t 0.0 = this colour, 1.0 = other.
overlaidOn(Colour background)ColourBlends this colour over a solid background, using its alpha.
// Build a base colour, then derive a translucent, brighter variant.
Colour base    = colours::fromRGB(96, 192, 128);
Colour lighter = base.brighter(0.3f);
Colour overlay = lighter.withAlpha(0.5f);

Paint Primitives


Paint primitives are plain data structs that describe shapes, text, or images to draw onto a Component. The vector shapes (Ellipse, Rectangle, Arc, Path) carry only geometry. Colour is supplied at draw time: construct the shape, set its member variables, then pass it to fill(shape, colour) / stroke(shape, colour) (or fillPath / strokePath for a Path), passing a Colour or a Gradient. Image and Text are drawn with paint(image) / paint(text).

Unlike most KODA types, paint primitives don't expose getters or setters. You read and write their member variables directly. All coordinates are in pixels relative to the Component's origin. All sizes are in pixels.

Colour format

Colours are passed to fill / stroke (and used inside a Gradient's stops) as the strong Colour type. Build one with a colours:: constructor (e.g. colours::fromRGB(96, 192, 128) or colours::fromHex(0xFF60C080_L)) or use a named constant (e.g. colours::aliceblue, colours::black). Use colours::transparent (or any colour with zero alpha) for an invisible draw. See the Colour section for channel getters and transforms.

Target Component setup

Painting onto a Component requires specific widget setup in your instrument's UI. Not every Component type will render paint actions, and Panel-type Components belong to visibility groups managed by the panel-group system. Examples and recipes for setting up a paintable Component will appear here once that workflow is documented. For now, refer to the Component section for known-good binding patterns.

Point & Bounds

Lightweight position and size structs used as building blocks (notably inside Path).

Point - a 2D coordinate
Member VariableTypeDescription
xfloatX coordinate in pixels.
yfloatY coordinate in pixels.
Bounds - a position + size rectangle
Member VariableTypeDescription
xfloatX position of the top-left corner.
yfloatY position of the top-left corner.
widthfloatWidth in pixels.
heightfloatHeight in pixels.

Ellipse

An ellipse shape. Draw it with fill(ellipse, …) and/or stroke(ellipse, …), passing a Colour or a Gradient.

Member VariableTypeDescription
xfloatX position of the ellipse's bounding box.
yfloatY position of the ellipse's bounding box.
widthfloatBounding-box width (full diameter on the x-axis).
heightfloatBounding-box height (full diameter on the y-axis).
strokeWidthfloatOutline thickness in pixels, used by stroke().

Rectangle

Same as Ellipse, plus a corner-radius member variable for rounded rectangles. Draw with fill(rectangle, …) / stroke(rectangle, …).

Member VariableTypeDescription
xfloatX position of the top-left corner.
yfloatY position of the top-left corner.
widthfloatWidth in pixels.
heightfloatHeight in pixels.
strokeWidthfloatOutline thickness in pixels, used by stroke().
cornerRadiusfloatRadius for rounded corners in pixels. 0.0 = square corners.

Arc

A circular arc swept between two angles. Useful for knob indicators, ring meters, and pie-style readouts. Draw with fill(arc, …) / stroke(arc, …).

Member VariableTypeDescription
xfloatX position of the arc's bounding box.
yfloatY position of the arc's bounding box.
widthfloatBounding-box width.
heightfloatBounding-box height.
startAnglefloatStart angle in radians. 0 = 3 o'clock; positive = clockwise.
endAnglefloatEnd angle in radians.
strokeWidthfloatOutline thickness in pixels, used by stroke().

Path

A multi-segment vector outline built from straight lines and Bézier curves. A Rectangle or Ellipse is described by setting its member variables - x, y, width, height. A Path isn't: you build it by calling its segment functions, then draw it with the Component's fillPath(path, colour|gradient) or strokePath(path, colour|gradient).

Build and draw as many Paths as you like in one function. Each draw call carries that path's own points, so paths never interfere with one another, and calling fillPath and then strokePath on the same Path fills it and then outlines it.

Member Variables
Member VariableTypeDescription
strokeThicknessfloatStroke thickness in pixels (for strokePath()).
jointStyleintHow stroked corners join: JointStyle::mitered = 0, curved = 1, beveled = 2.
endCapStyleintHow stroked ends are capped: EndCapStyle::butt = 0, square = 1, rounded = 2.
Build functions
FunctionReturnsDescription
startNewSubPath(float x, float y) void Begins a new sub-path at (x, y) without drawing a line to it. Call before the first segment of a disconnected shape.
lineTo(float x, float y) void Adds a straight line from the current point to (x, y).
quadraticTo(float controlX, float controlY, float x, float y) void Adds a quadratic Bézier curve to (x, y) using one control point.
cubicTo(float control1X, float control1Y, float control2X, float control2Y, float x, float y) void Adds a cubic Bézier curve to (x, y) using two control points.
close() void Closes the current sub-path by drawing a line back to its start point.
clear() void Removes all segments so the Path can be rebuilt.

Example: Paints a triangle onto a Component

// Paints a triangle onto a plain Component named "Canvas" (100 x 100 px in Figma).
event initialize()
{
    Path triangle;
    triangle.strokeThickness = 2.0f;
    triangle.startNewSubPath(10.0f, 10.0f);
    triangle.lineTo(90.0f, 10.0f);
    triangle.lineTo(50.0f, 80.0f);
    triangle.close();

    Component canvas = getComponent("Canvas");
    canvas.strokePath(triangle, colours::red);
}

Gradient

A multi-stop colour gradient. Pass a Gradient in place of a Colour to any fill / stroke / fillPath / strokePath to fill or outline a shape with a gradient. Build it by setting the type and endpoints, then adding up to 16 colour stops with addStop(position, colour).

Gradient types

Set type to one of GradientType::Linear (0), GradientType::Radial (1), GradientType::Angular (2), or GradientType::Diamond (3). Only Linear and Radial render today. Angular and Diamond are reserved for a future JUCE version and currently draw nothing (a one-time warning is logged to the script console). For Linear the gradient runs along the axis from (x1,y1) to (x2,y2); for Radial it runs from the center (x1,y1) out to (x2,y2). Coordinates are in pixels relative to the Component's origin.

Member Variables
Member VariableTypeDescription
typeintGradientType::Linear / Radial / Angular / Diamond. Default 0 = Linear.
x1floatX of the gradient start point (center for Radial).
y1floatY of the gradient start point (center for Radial).
x2floatX of the gradient end point (edge for Radial).
y2floatY of the gradient end point (edge for Radial).
Functions
FunctionReturnsDescription
addStop(float position, Colour colour) void Adds a colour stop at position (0.0 = start, 1.0 = end). Up to 16 stops; extra calls are ignored.
clear() void Removes all stops so the Gradient can be rebuilt.

Example: Paints a gradient-filled rectangle

// Fills a plain Component named "Canvas" (100 x 100 px in Figma) with a 3-stop linear gradient.
event initialize()
{
    Rectangle r = (0.0f, 0.0f, 100.0f, 100.0f, 0.0f, 0.0f);   // x, y, width, height, strokeWidth, cornerRadius

    Gradient g;
    g.type = GradientType::Linear;
    g.x1 = 0.0f; g.y1 = 0.0f;       // top
    g.x2 = 0.0f; g.y2 = 100.0f;     // bottom
    g.addStop(0.0f, colours::dodgerblue);
    g.addStop(0.5f, colours::white);
    g.addStop(1.0f, colours::black);

    Component canvas = getComponent("Canvas");
    canvas.fill(r, g);
}

Image

Paints an image (looked up by name) into a rectangular region. The image must already be available to the instrument (typically exported alongside the Figma UI).

Member VariableTypeDescription
imagestringImage identifier - the name of the asset to draw.
xfloatX position of the top-left corner.
yfloatY position of the top-left corner.
widthfloatDisplay width in pixels.
heightfloatDisplay height in pixels.

Text

Paints a string of text. Use for runtime-generated labels, value readouts, or status indicators where the text changes based on parameter values.

*** Not implemented yet. Painting a Text draws a placeholder and ignores every member below. For text that changes at runtime, use a Label Component and setText.

Member VariableTypeDescription
textstringThe string to render.
fontstringFont family name (e.g. "Inter"). Must be available to the instrument.
xfloatX position of the text's bounding box.
yfloatY position of the text's bounding box.
widthfloatBounding-box width (text wraps if it overflows).
heightfloatBounding-box height.
fontSizefloatFont size in points.
fontColourColourText colour (ARGB).

Font

A font spec for label-style widgets, passed to setFont() / setItemTextFont().

Font - a label/value font spec
Member VariableTypeDescription
namestringFont family name (e.g. "Inter").
stylestringStyle name: "Regular", "Bold", "Italic", "Bold Italic", etc.
heightfloatPoint height. A value ≤ 0 keeps the widget's current height.
justificationintText alignment; Justification:: constants: Keep=0 (no change), Left=1, Center=2, Right=3, plus combined TopLeft=4BottomRight=12 (CenterLeft=7, Centered=8, CenterRight=9).
kerningfloatExtra inter-character spacing factor (0.0 = default).
colourColourText colour. An alpha of 0 keeps the current colour.

Effect

A visual effect added to a Component via addEffect().

Effect - a component visual effect
Member VariableTypeDescription
typeintEffect kind; EffectType:: constants: Blur=0 (blurs the component's own content), BackgroundBlur=1 (frosted-glass blur of what's behind it), DropShadow=2 (offset shadow), Glow=3 (centered halo).
radiusfloatBlur/spread radius in pixels.
tintColourShadow/glow colour (DropShadow/Glow) or tint.
offsetXfloatHorizontal shadow offset in pixels (DropShadow).
offsetYfloatVertical shadow offset in pixels (DropShadow).

Canvas

A Canvas records a list of paint actions you build once and then draw onto one or more Components with paint(Canvas) / paintOver(Canvas). Use it to avoid re-issuing the same paint calls per component. Declare one with Canvas myCanvas;.

FunctionReturnsDescription
fill(Rectangle r, Colour|Gradient)boolRecords a filled rectangle. Same overloads for Ellipse and Arc. Returns false if the canvas is full.
stroke(Rectangle r, Colour|Gradient)boolRecords a stroked rectangle. Same overloads for Ellipse and Arc. Returns false if the canvas is full.
fillPath(Path p, Colour|Gradient)boolRecords a filled path. Returns false if the canvas is full.
strokePath(Path p, Colour|Gradient)boolRecords a stroked path. Returns false if the canvas is full.
paint(Text text)boolRecords text. Returns false if the canvas is full. *** Not implemented yet: draws a placeholder and ignores the Text struct.
paint(Image image)boolRecords an image. Returns false if the canvas is full.
clear()voidClears all recorded actions.

Example: Paints a prebuilt Canvas onto a Component

// Builds the artwork once, then draws it onto a plain Component named "Canvas" (100 x 100 px in Figma).
event initialize()
{
    Canvas artwork;

    Rectangle r = (0.0f, 0.0f, 100.0f, 100.0f, 0.0f, 0.0f);   // x, y, width, height, strokeWidth, cornerRadius
    artwork.fill(r, colours::dodgerblue);

    Component canvas = getComponent("Canvas");
    canvas.paint(artwork);
}

SpriteSheet

Describes a sprite sheet passed to setSprite() for animated Components.

SpriteSheet - a sprite-sheet definition
Member VariableTypeDescription
pathstringImage asset name.
widthintPer-frame width in pixels.
heightintPer-frame height in pixels.
framesintTotal number of frames in the sheet.

MouseEvent


A MouseEvent is passed to your script when the user interacts with a Component that has a mouse event declared. You handle one by writing event <Name>(MouseEvent mouseEvent) { ... }, where <Name> matches the event name set on the Component in Figma. The event tells you where the mouse was (x, y), which modifier keys were held, and what kind of mouse action occurred.

Target widget setup

In the current build, only Panel widgets forward mouse events to script handlers. Other widget types (Slider/Knob, XYPad, Table, Button) consume their clicks for their own behavior without forwarding, and Label / Animation / Default-type widgets don't intercept clicks at all by default. To make event <Name>(MouseEvent ...) fire reliably, attach the "MouseEvent" property to a Panel with "Floating Window": "True" in your Figma export. A worked example will appear here once the source-side fix lands that lets other widget types forward MouseEvents alongside their own handling.

Member Variables

Member VariableTypeDescription
xintX position of the mouse relative to the Component's origin.
yintY position of the mouse relative to the Component's origin.
modsintBitfield of held modifier keys. Compare with MouseModifiers::Command, ::Shift, ::Alt, or use the isCommandDown() / isShiftDown() / isAltDown() helpers below.
rightClickintNon-zero when the press is a right-click. Prefer isRightClick() / isLeftClick().
typeMouseEventTypeWhat kind of mouse action this is. One of Down, Up, Enter, Exit, DoubleClick, StartDrag, Drag, DragEnter, DragExit, Drop.
identifierintIdentifier used internally to associate the event with its source. Most scripts can ignore this.

Modifier-key helpers

FunctionReturnsDescription
isCommandDown()booltrue if Command (⌘ on macOS, Ctrl on Windows) was held during the event.
isShiftDown()booltrue if Shift was held.
isAltDown()booltrue if Alt / Option was held.

Event-type helpers

FunctionReturnsDescription
isLeftClick()booltrue on a left-button press (Down event, non-right-click).
isRightClick()booltrue on a right-button press (Down event, right-click).
isDown()booltrue if type is Down.
isUp()booltrue if type is Up.
isEnter()booltrue if the mouse entered the Component's bounds.
isExit()booltrue if the mouse left the Component's bounds.
isDoubleClick()booltrue on a double-click.
isStartDrag()booltrue when a drag operation starts.
isDrag()booltrue if this is a drag event (mouse moved while a button is held).
isDragEnter()booltrue when a drag enters the Component's bounds.
isDragExit()booltrue when a drag leaves the Component's bounds.
isDrop()booltrue when a dragged item is dropped on the Component.

KeyEvent


A KeyEvent is passed to your script when a keyboard key is pressed while a Component is focused. Handle one by writing event <Name>(KeyEvent keyEvent) { ... }, where <Name> matches the event name set in Figma.

Feature not yet end-to-end verified

In the current build, KeyEvent handlers don't have a verified delivery path. Every widget type we tested either doesn't intercept keyboard input at the script-reachable level (Label, Animation, Default), or it intercepts at an inner JUCE child (TextEdit's inner editor, Slider's inner slider) which prevents the script's keyPressed hook from firing.

The script-side declaration syntax (event <Name>(KeyEvent keyEvent)) is documented above, and the kdui side ("KeyEvent": "<Name>" property on a Component) compiles cleanly. A worked example will appear here once the source-side delivery path is verified for a specific widget type.

Member Variables

Member VariableTypeDescription
keyCodeintRaw key code. Compare with constants in the Qwerty namespace (e.g. Qwerty::SpaceBar, Qwerty::Enter, Qwerty::UpArrow) or use the helpers below.
modsintBitfield of held modifier keys. Use isCommandDown() / isShiftDown() / isAltDown().
identifierintIdentifier used internally to associate the event with its source.

Key helpers

FunctionReturnsDescription
getKeyCode()intReturns the raw key code.
isSpaceBar()booltrue if the event fired from the space bar.
isEnter()booltrue if the event fired from Enter / Return.
isTab()booltrue if the event fired from Tab.
isDelete()booltrue if the event fired from the Delete key.
isBackspace()booltrue if the event fired from Backspace.
isUpArrow()booltrue if the event fired from the up-arrow key.
isDownArrow()booltrue if the event fired from the down-arrow key.
isLeftArrow()booltrue if the event fired from the left-arrow key.
isRightArrow()booltrue if the event fired from the right-arrow key.
isCharacterKey(string character)booltrue if the event matches the printable character passed in (e.g. "a", "$", " ").

Modifier-key helpers

FunctionReturnsDescription
isCommandDown()booltrue if Command / Ctrl was held during the keypress.
isShiftDown()booltrue if Shift was held.
isAltDown()booltrue if Alt / Option was held.

GuiEvent


A GuiEvent is passed to your script when a GUI control with a declared GuiEvent fires - typically used by widgets to send a value plus an identifier. Handle one by writing event <Name>(GuiEvent guiEvent) { ... }, where <Name> matches the event name declared in Figma.

Required UI declaration

Before event <Name>(GuiEvent ...) will compile, the same name must be declared in your Figma export. Add a "GuiEvent": "<Name>" property to a Component in your kdui. Without the declaration, the script compiler reports Event handler '<Name>' does not match an event input.

identifier is there so one handler can tell several Components apart, and it only carries a value when a script tags the Component with sendGuiEvents(component, identifier). Components declared through the Figma property alone report 0.

For a Menu, value is the selected item's own value, which is not always its position. Items defined in Figma are numbered from 1 in list order, so there the two match. Items added from a script with addItem(name, value) report whatever value you gave them.

Example: React to a menu selection

// Clears a menu, populates new items, and logs the selected item's value
// (Assumes a Menu named "MyMenu" in your kdui, with property "GuiEvent" set to "MenuChoice".)
event initialize()
{
    setLogEnabled(true);       // Without this, debug.log() output is silent

    Component menu = getComponent("MyMenu");
    menu.clearItems();          // drop the items defined in Figma first
    menu.addItem("Alpha", 100);
    menu.addItem("Beta", 250);
}

event MenuChoice(GuiEvent guiEvent)
{
    debug.log("Menu item value:", int(guiEvent.getValue()));   // 100 or 250, not 1 or 2
}

Member Variables

Member VariableTypeDescription
valuefloatThe value carried by the event.
identifierintIdentifier used internally to associate the event with its source.

Functions

FunctionReturnsDescription
getValue()floatReturns the event's value member variable.
getIdentifier()intReturns the event's identifier member variable.

KeyRange


A KeyRange describes a span of MIDI notes with a name, colour, and tooltip - used to highlight regions of the GUI piano. There are two kinds of range a script can work with:

  • Script-drawn ranges - construct a KeyRange, then addKeyRange() draws it and removeKeyRange() / clearKeyRanges() take it down. These are transient overlays owned by the script (matched by name); they are cleared on every recompile and never saved with the instrument.
  • Ranges authored in the KEY RANGES module (Setup page) - showKeyRange() / hideKeyRange() toggle the visibility of an existing range whose display name matches the KeyRange.name you pass (every match, at instrument and articulation scope). Only name is read; the other members are ignored. The per-articulation mapped range (the drawn-on-demand span of every note the articulation's zones cover) answers to the name "Playable Range", so a script can hide or show it the same way.
Two different jobs

addKeyRange() creates a new highlight from your KeyRange, colour and all. removeKeyRange() and clearKeyRanges() manage the ranges you made this way.

showKeyRange() and hideKeyRange() do something else entirely: they toggle the visibility of a range already authored in your instrument, matched by name. Every other member variable is ignored, so calling showKeyRange() on a range your UI doesn't already contain does nothing at all.

Example: Highlight keys C3 to B3 on startup

event initialize()
{
    KeyRange octave3;
    octave3.startNote = 60;          // C3
    octave3.endNote = 71;            // B3
    octave3.name = "Octave 3";
    octave3.colour = colours::cornflowerblue;
    octave3.tooltip = "Middle C through B above";

    addKeyRange(octave3);          // script-drawn overlay

    KeyRange mapped;
    mapped.name = "Playable Range";
    hideKeyRange(mapped);           // hide every articulation's mapped range
}

Member Variables

Member VariableTypeDescription
startNoteintFirst MIDI note in the range (inclusive). Range: 0-127.
endNoteintLast MIDI note in the range (inclusive). Range: 0-127.
namestringDisplay name shown on or near the highlighted region.
colourColourHighlight colour shown on the GUI piano. Build with a colours:: constructor or use a named constant (e.g. colours::orange).
tooltipstringTooltip text shown when the user hovers over the highlighted region.

Functions

Highlight regions of the GUI piano. All of these are also available on the Keyboard struct returned by getKeyboard().

FunctionReturnsDescription
addKeyRange(KeyRange range)voidDraws a script-owned range on the GUI piano, on top of the ranges authored in your instrument (keyed by range.name; adding the same name again replaces it).
removeKeyRange(KeyRange range)voidRemoves the script-owned range with this name.
clearKeyRanges()voidRemoves every script-owned range. Ranges authored in your instrument are left alone.
showKeyRange(KeyRange range)voidMakes every KEY RANGES-module range whose display name equals range.name visible in PLAY mode; every other member variable is ignored, and nothing happens if no authored range has that name. "Playable Range" targets each articulation's mapped range. Also available as show(KeyRange). To create a range from a script, use addKeyRange().
hideKeyRange(KeyRange range)voidThe opposite of showKeyRange(). Also available as hide(KeyRange). Both are recorded in the instrument (they survive a save), so prefer addKeyRange() for transient highlights.
setKeyHeld(int note, bool held)voidPaints a key as held on the GUI piano without sounding it - e.g. to mirror a keyswitch from event CurrentArticulation, since keyswitch keys are consumed before noteOn and never reach the script.
clearHeldKeys()voidClears every key painted by setKeyHeld().
clearPlayableRanges()voidRemoves every mapped-range highlight shown via a container's showPlayableRange() (all Articulations, Variations, and Groups at once). Distinct from clearKeyRanges(), which clears script-drawn KeyRange overlays.
clearAllKeyRanges()voidReserved - not currently honoured by KODA (the event is ignored). Use clearKeyRanges() and hideKeyRange().
setPlayableRange(int startNote, int endNote[, Colour colour])voidReserved - not currently honoured by KODA (the event is ignored). The playable span is derived from the mapped zones; see Keyboard highlight.

Pattern


A Pattern is a note sequencer that lives in your script: fill it with NoteData events placed at beat positions, give it a length in beats, and it plays them back - once, or looping. Everything about a Pattern is musical time: positions, the loop length, and note gates are all beats, converted to samples against the live host tempo as the notes fire. Tempo sync isn't a mode you enable - it's what a Pattern is. Change the DAW tempo mid-loop and the pattern follows, with no reference tempo to declare anywhere.

Declare Patterns at the top level of the script (they're big - see Large object scope), and give each one its per-block pulse by calling update() from event process(). That call is the one piece of wiring every Pattern needs.

The smallest useful Pattern is a one-note repeater - hold a key and it retriggers every beat:

Pattern repeater;

event initialize() { setIgnoreNoteOns(true); }

event noteOn (NoteData note)
{
    repeater.setTarget(getCurrentArticulation());   // route fired notes to the current articulation
    repeater.repeat(note, 1.0);                      // loop the held note every beat
}

event noteOff (NoteData note) { repeater.stop(); }

event process() { repeater.update(); }

Building

Add events with addNote() - each call copies the NoteData you pass in, so one NoteData variable can be reused to lay down every step (see the melody example below). A Pattern holds up to 1024 events; additions beyond that are silently ignored.

Read at fire time

A Pattern bakes nothing in when you add notes. The length, looping flag, transposition and target are consulted at the moment each note fires, so the order you call setters in doesn't matter - and you can change any of them while the pattern is playing. The melody example below re-keys a running loop by calling setTransposition() from noteOn.

This is also why building a pattern in initialize() is safe even though the tempo still reads 0 there: beats are stored as beats and only become samples during playback.

FunctionReturnsDescription
addNote(NoteData note, float64 beat) void Adds a copy of the note at a beat position. Valid positions run from 0 up to (but not including) the pattern length - an event at or beyond the length never fires.
addNote(NoteData note, float64 beat, float64 lengthInBeats) void As above, and releases the note lengthInBeats beats after it starts. The gate is converted to time against the tempo in effect when the note fires, and it lands even when it falls past the loop wrap.
addNote(NoteData note) void Adds the note at beat 0.
setLengthInBeats(float64 lengthInBeats) void Sets the pattern length - the loop point - in beats.
setLooping(bool looping) void true = wrap at the pattern length and keep going. false = play once, then stop (releasing anything still sounding).
setTransposition(int semitones) void Transposes every note the pattern fires. Applied at fire time - change it mid-loop to re-key what's already queued.
setTarget(Articulation target) void Routes every fired note to the given container. Also accepts a Variation or Group. Set it once, before or after adding notes. Without a target, each note keeps the routing it was created with (e.g. from createNoteData()).
clearTarget() void Removes the target; fired notes fall back to their own routing.
setInitialOffset(int offsetSamples) void Extra delay in audio samples added to every fired note.
clear() void Removes all events and rewinds to beat 0. Length, looping, target and transposition are kept.
Three kinds of note length

A note inside a pattern can get its length three ways. addNote's lengthInBeats argument is musical and keeps tracking tempo - the gate is resolved when the note fires, every time it fires. setLengthBeats() on the NoteData is musical but a snapshot - converted to absolute time at the moment you call it. setLengthMS() / setLengthSeconds() are absolute wall-clock lengths - a fixed 200 ms pluck stays 200 ms at any tempo.

The pattern never touches a length the note already carries - it only writes one when you pass addNote a gate argument.

Playback

FunctionReturnsDescription
start() void Rewinds to beat 0 and starts playback.
start(int initialOffsetSamples) void As above, with an extra sample delay applied to every fired note.
startLooping(float64 lengthInBeats) void setLengthInBeats() + setLooping(true) + start() in one call.
repeat(NoteData note, float64 lengthInBeats) void Clears the pattern, adds the note at beat 0, and starts looping every lengthInBeats beats - the one-line note repeater.
repeat(NoteData note, float64 lengthInBeats, float64 gateInBeats) void As above, releasing each hit gateInBeats beats after it starts.
stop() void Stops playback, rewinds, and sends a note-off for everything the pattern may have started.
update() void The per-block pulse - call it from event process(). Fires the events that fall inside the current audio block, sample-accurately; does nothing while the pattern is inactive.
isActive() bool true between start() and stop(). A non-looping pattern stops itself when it reaches the end.
isLooping() bool Returns the looping flag.
getLengthInBeats() float64 Returns the pattern length in beats.
getPlayPositionBeats() float64 Current playback position in beats - handy for driving UI.
Events past the loop point

An event at or beyond the pattern length stays in the queue but never fires. If you shrink the length below an existing event's position, that step simply goes silent until the length grows past it again - nothing is removed.

Example: A melody sequencer

One NoteData lays down every step, and the pattern re-keys itself to whatever key is held. Because positions and gates are beats, this is safe to build in initialize() and rescales to any host tempo:

Pattern melody;

const int kRoot = 60;   // the melody is written around middle C

event initialize()
{
    setIgnoreNoteOns(true);

    var art = getCurrentArticulation();
    NoteData step = art.createNoteData();   // a blank note, routed to this articulation
    step.setMidiVelocity(100);

    // note number            position (beats)  length (beats)
    step.setNoteNumber(60);  melody.addNote(step, 0, 0.5);
    step.setNoteNumber(64);  melody.addNote(step, 1, 0.5);
    step.setNoteNumber(67);  melody.addNote(step, 2, 0.5);
    step.setNoteNumber(64);  melody.addNote(step, 3, 0.5);

    melody.setLengthInBeats(4);
    melody.setLooping(true);
}

event noteOn (NoteData note)
{
    melody.setTransposition(note.getNoteNumber() - kRoot);   // play it in the held key
    melody.start();
}

event noteOff (NoteData note) { melody.stop(); }

event process() { melody.update(); }

Explicit note-off events are also possible for cases where an off shouldn't be tied to its own on: add a note with setIsNoteOff(true) at any beat before the pattern length and it fires as a note-off. For ordinary melodic gates, prefer the length argument - it stays correct across the loop wrap.

Saving & Restoring State


This section is currently under construction.

Advanced Topics


You can script KODA without anything in this section. These topics start to matter as scripts grow - more data, code you want to reuse across instruments, and the failure modes that only show up at scale. Each topic stands alone, so dip in as needed.

Passing by value vs reference

Assigning a value or passing it to a function makes a copy - structs and arrays included. Changes to the copy never reach the original, which is also why editing a NoteData after starting the note has no effect (see Note vs NoteData).

To let a function change what the caller passed in, mark the parameter with &:

//  By value - boost() changes its own copy
event initialize() { setLogEnabled(true); }

void boost (float value) { value += 0.2f; }

event noteOn (NoteData note)
{
    float v = 0.5f;
    boost(v);
    debug.log("v", v);   // still 0.5
}
//  By reference - boost() changes the caller's v
event initialize() { setLogEnabled(true); }

void boost (float& value) { value += 0.2f; }

event noteOn (NoteData note)
{
    float v = 0.5f;
    boost(v);
    debug.log("v", v);   // now 0.7
}

& works in one place only: parameters being passed into a function. A function cannot return a reference - returning always hands back a copy - and there are no reference variables. So a function that should give the caller a changed value either returns the copy for the caller to assign, or writes into a & parameter as above. Copying is fine for small values; for big ones, see the next topic.

Large object scope

Declare big arrays and structs at the top level of the script, not inside functions. Top-level variables live for the lifetime of the instrument and are shared by every function; function locals are copied around and limited in size. Pass a big object to your own helper with & so the helper works on it in place:

// A big lookup table: declared at the top level, filled in place through a & parameter
float[1024] table;                     // top level - lives with the instrument

void fill (float[1024]& t)             // & - fills the caller's table, no copy
{
    for (wrap<1024> i)
        t[i] = float(i) / 1023.0f;
}

event initialize()
{
    setLogEnabled(true);
    fill(table);
    debug.log("table.at(512)", table.at(512));   // 0.500489
}

Index lookup tables with wrap<N> or .at() as described in Cmajor patterns you'll see.

Generics (functions, namespaces)

A generic function is written once and works for every type you call it with - the compiler builds a version per type. Put <Type> after the name, then use Type as a placeholder. The built-in limit() works this way, which is why it accepts int and float alike.

// One generic function serving two types: int and float
event initialize() { setLogEnabled(true); }

Type highest<Type> (Type a, Type b)
{
    static_assert (Type.isPrimitive, "highest() needs a numeric type");
    return a > b ? a : b;
}

event noteOn (NoteData note)
{
    debug.log("int", highest(3, 7));                        // 7
    debug.log("float", highest(note.getVelocity(), 0.5f));   // at least 0.5
}

static_assert is a check that runs at compile time: if its condition is false, compilation stops and its message becomes the error. Here it's optional but worth the keystrokes - without it, calling the function with a type it can't handle fails with a confusing compiler error instead of your own message. Namespaces can be parameterised the same way - see the Cmajor language documentation.

Designing your own structs

A struct groups related values under one name, and every member variable starts at zero - there are no member initialisers, so a struct needs no constructor. If a member should start at some other value, set it in initialize(). Functions declared inside the struct refer to it as this, and calling them follows the same two spellings as everything else (see dot or no dot).

If you're coming from an OOP language, a struct is the closest thing to a class, with one big difference up front: there is no inheritance. A struct cannot extend another, so build bigger structs by composition - one struct as a member of the next. Otherwise the shape is familiar: member variables plus the functions that work on them, called with dot syntax. Two smaller differences: a struct is a plain value - assigning or passing one copies it (see Passing by value vs reference), where a class object in most languages is shared by reference - and there is no constructor, so every struct starts zeroed, every time.

// A struct that accumulates a running average across noteOn calls
struct VelocityStats
{
    float total;
    int   count;

    void  add (float v) { this.total += v; this.count++; }
    float average()     { return this.count == 0 ? 0.0f : this.total / float(this.count); }
}

VelocityStats stats;                    // top level, zero-initialised

event initialize() { setLogEnabled(true); }

event noteOn (NoteData note)
{
    stats.add(note.getVelocity());
    debug.log("average velocity", stats.average());
}

Name your struct anything except State - that name opts into the save and restore machinery described in Saving & Restoring State.

Using Constants (properties) and parameters to make templates

Constants let one script serve many instruments: instead of hard-coding numbers, the script reads named values authored per-instrument in the GROUP VARIABLES section of the Setup tab. The script side calls them properties - the property getters on Articulation, Variation, and Group read them by name. Despite the name, a Constant has nothing to do with the const keyword: it is constant from the script's point of view because only the instrument author can change it, and your script reads it into an ordinary variable.

A name that isn't authored on the instrument reads as "", 0, or false rather than erroring. That silence is what lets one script load everywhere, but it means a template should check and log its fallbacks, so an instrument builder can see what's missing. Read Constants once in initialize() and keep the values in top-level variables:

// Read a per-instrument Constant (property) with a logged fallback when it's missing
int transposeSemitones;                 // read once at startup

event initialize()
{
    setLogEnabled(true);

    Articulation art = getCurrentArticulation();

    if (art.getProperty("Transpose") == "")
        debug.log("No 'Transpose' Constant - using 0");

    transposeSemitones = art.getIntProperty("Transpose");
    debug.log("transposeSemitones", transposeSemitones);
}

Avoiding infinite loops

Everything your script does - noteOn, process, every event - runs inside KODA's audio engine, so a loop that never exits doesn't just stall the script, it freezes all of KODA. The compiler rejects loops it can prove never exit (a bare loop {} fails with "The function contains at least one infinite loop"), but it cannot catch a loop whose exit depends on a value that never arrives. Floats are the classic trap: stepping by 0.1f never lands exactly on 1.0f, so a != test stays true forever.

//  Compiles cleanly, then freezes KODA on the first note
event noteOn (NoteData note)
{
    float t = 0.0f;
    while (t != 1.0f)      // never exactly 1.0
        t += 0.1f;
}
//  Bounded by construction
event noteOn (NoteData note)
{
    float t = 0.0f;
    loop (10)               // runs exactly 10 times
        t += 0.1f;
}

Prefer loops that are bounded by construction: the counted loop (n) form, for loops with fixed bounds, and < comparisons rather than != when the condition involves a float.

No safety net

Nothing stops a runaway loop. It freezes audio, meters, and the whole KODA window on the spot - the app can't even quit normally, and force-quitting it is the only way out. Test loops in the standalone app with nothing unsaved.

Avoiding NaN

NaN - "not a number" - is what float maths produces when an operation has no answer, like 0.0f / 0.0f or the square root of a negative. It spreads: any arithmetic that touches a NaN yields NaN, and every comparison against NaN is false, so range checks and clamps wave it through, and a != loop condition holding a NaN never exits (see Avoiding infinite loops).

The place scripts usually meet one: timing functions inside initialize(), before any audio is flowing. getBlockTimeSeconds() divides two values that are still zero there and reads NaN. Guard with isnan() before a value like that reaches a setter:

// Where NaN shows up, and how to stop it before it reaches a setter
event initialize()
{
    setLogEnabled(true);
    debug.log("block time", getBlockTimeSeconds());   // logs nan - no audio yet
}

event noteOn (NoteData note)
{
    float64 blockTime = getBlockTimeSeconds();

    if (isnan(blockTime))
        return;                                       // never let NaN reach a setter

    debug.log("block time", blockTime);               // real value, e.g. 0.001333
}

An unguarded NaN fails silently: fed into setInstrumentGain, the instrument simply goes quiet - no error, no log message, just no sound until the script is fixed and the instrument reloaded. If a value could ever come from a division, check the denominator or check the result with isnan() (and isinf() for its cousin) before using it.