KODA Scripting Reference
Lists every function available to scripts along with code examples.
Introduction
This document is not finalized for publish and is still being worked on.
Conventions used in this reference
| Function | Returns | Description |
|---|---|---|
| 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, sodebug.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 withstartNote().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.
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.
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
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
forloops 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 yournoteOnhandler describing each incoming MIDI note. You can modify it freely, build new ones viacreateNoteData(), and trigger them withstart()orstartNote().Note- a handle to a note that's currently playing. You get one back fromstartNote(). 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
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.
| Parameter | Default CC | Default | Description |
|---|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| limit(Type value, Type min, Type max) | Type | Returns 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.
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
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
|
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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
|
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).
| Function | Returns | Description |
|---|---|---|
| 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 (0 … getNumConvolutionFiles() - 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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). |
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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.)
| Function | Returns | Description |
|---|---|---|
|
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
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
|
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());
}
}
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
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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()).
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
|
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).
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.
| Function | Returns | Description |
|---|---|---|
|
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.
| Function | Returns | Description |
|---|---|---|
| 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.
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());
}
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 it | Routing | When 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();
}
}
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.
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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).
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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".
| Function | Returns | Description |
|---|---|---|
|
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).
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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. |
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.
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.
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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.
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
| Function | Returns | Description |
|---|---|---|
| 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 Variable | Type | Description |
|---|---|---|
| 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;
}
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);
}
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).
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);
}
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
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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()). |
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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| getLeft() | float | Left channel, linear (see range above). |
| getRight() | float | Right channel, linear (see range above). |
| getLeftDecibels() | float | Left channel in dB (floored at -180). |
| getRightDecibels() | float | Right channel in dB (floored at -180). |
| getPeak() | float | Max of left/right, linear (see range above). |
| getPeakDecibels() | float | Peak in dB (floored at -180). |
| getAverage() | float | Mean of left/right, linear (see range above). |
| getAverageDecibels() | float | Average 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);
}
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
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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).
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);
}
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
| Function | Returns | Description |
|---|---|---|
| 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).
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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
| Function | Returns | Description |
|---|---|---|
| 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).
| Function | Returns | Description |
|---|---|---|
| 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));
}
| Function | Returns | Description |
|---|---|---|
| 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. |
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.
| Function | Returns | Description |
|---|---|---|
| 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.
| Function | Returns | Description |
|---|---|---|
| setBounds(Bounds bounds) | void | Sets position and size from a Bounds. |
| setRotation(float radians) | void | Rotates about the center; angle in radians (2π = full turn), 0.0 = none. |
| setScale(float scale) | void | Uniform scale about the center; 1.0 = original. |
| setScale(float scaleX, float scaleY) | void | Non-uniform scale per axis; 1.0 = original on that axis. |
| bringToFront() | void | Raises to the front of its sibling z-order. |
| getName() | string | Returns the Component's control ID. |
Text formatting
For Label, value-edit and text-edit Components.
| Function | Returns | Description |
|---|---|---|
| setText(float value) | void | Sets text to a float (2 decimal places). |
| setText(float value, int numDecimals) | void | Sets text to a float with N decimals. |
| setText(int value) | void | Sets text to an integer. |
| clearText() | void | Clears the text. |
| setSuffix(string suffix) | void | Appends a unit suffix to displayed text (e.g. " Hz"). |
| setUnitType(int unitType) | void | Formats 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) | void | Restricts typeable characters on a text-edit (empty string restores default). |
| setFont(Font font) | void | Sets the font from a Font. |
Effects & sprites
Attach visual effects and sprite sheets to a Component.
| Function | Returns | Description |
|---|---|---|
| addEffect(Effect effect) | void | Adds a visual effect (see Effect). |
| removeEffects() | void | Removes all effects. |
| setSprite(string path, int spriteWidth, int spriteHeight, int numFrames) | void | Attaches a sprite sheet (per-frame size + frame count). |
| setSprite(SpriteSheet sprite) | void | Attaches a sprite sheet from a SpriteSheet. |
Containers & event routing
Nest Components and route their input to script event handlers.
| Function | Returns | Description |
|---|---|---|
| addComponent(Component child) | void | Re-parents a child into this (panel) Component so it moves and scales with it. Call on the parent: panel.addComponent(child). |
| sendMouseEvents(int identifier) | void | Routes 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) | void | Routes 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.
| Function | Returns | Description |
|---|---|---|
| addItem(string name, int value) | void | Adds an item (display label + user int reported on selection). |
| clearItems() | void | Removes all items. |
| setListSelection(int index) | void | Selects a ListItemBox item by index (−1 clears). |
| clearMenuSelection() | void | Clears a Menu's selection (same as setMenuSelection(−1)). |
| setItemSize(int width, int height) | void | Per-item pixel size. |
| setItemTextFont(Font font) | void | Item text font. |
| setItemTextBounds(Bounds bounds) | void | Item text layout bounds within each item. |
| setItemTextColour(Colour colour, int state) | void | Item text colour per state; MenuItemState:: Normal=0, Hover=1, Selected=2. |
| setMenuBackgroundColour(Colour colour) | void | Menu drop-down background. |
| setMenuOutlineColour(Colour colour) | void | Menu drop-down border colour. |
| setMenuOutlineThickness(float thickness) | void | Menu 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.
It is not synced to the actual GUI framerate, so don't rely on it for precise timing.
| Function | Returns | Description |
|---|---|---|
| setViewRefreshRate(int hz) | void | Sets the viewRefresh rate; clamped 12-60 Hz (default 24). Only the instrument-level (root) script's setting takes effect. |
| getViewRefreshRate() | int | Returns 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).
| Function | Returns | Description |
|---|---|---|
| createButton(string name, Bounds bounds) | Component | Creates a button. |
| createPanel(string name, Bounds bounds) | Component | Creates a panel; use as a container (addComponent) or a paint surface. |
| createRotarySlider(string name, Bounds bounds) | Component | Creates a rotary slider (knob). |
| createHorizontalSlider(string name, Bounds bounds) | Component | Creates a horizontal slider. |
| createVerticalSlider(string name, Bounds bounds) | Component | Creates a vertical slider. |
| createLabel(string name, Bounds bounds, string text) | Component | Creates a text label with initial text. |
| createXYPad(string name, Bounds bounds, string parameterX, string parameterY) | Component | Creates an XY pad bound to two parameters by name (one per axis). |
| createMenu(string name, Bounds bounds) | Component | Creates a drop-down menu; populate with addItem(). |
| createListItemBox(string name, Bounds bounds) | Component | Creates a scrollable list; populate with addItem(), track with setListSelection(). |
| createSearchField(string name, Bounds bounds) | Component | Creates a text/search entry field. |
| createStateDisplay(string name, Bounds bounds) | Component | Creates a state-display widget. |
| createAnimation(string name, Bounds bounds) | Component | Creates 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);
}
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.
| Function | Returns | Description |
|---|---|---|
| colours::fromRGB(int r, int g, int b) | Colour | Opaque colour from 8-bit channels (each 0-255). |
| colours::fromRGBA(int r, int g, int b, int a) | Colour | From 8-bit channels plus alpha (each 0-255). An overload takes a float alpha 0.0-1.0. |
| colours::fromRGB(int hex) | Colour | From a 24-bit hex RGB literal (e.g. 0xFF7F00), opaque. |
| colours::fromHex(int64 hex) | Colour | From 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) | Colour | From 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) | Colour | Opaque colour from HSB components (each 0.0-1.0). fromHSBA adds an alpha argument. |
| colours::fromHSL(float h, float s, float l) | Colour | Opaque 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.
| Function | Returns | Description |
|---|---|---|
| getAlpha() / getRed() / getGreen() / getBlue() | int | Channel value 0-255. |
| getAlphaFloat() / getRedFloat() / getGreenFloat() / getBlueFloat() | float | Channel value 0.0-1.0. |
| getHue() / getSaturation() / getBrightness() | float | HSB component 0.0-1.0. |
Transforms
Each transform returns an immutable copy; the original colour is unchanged.
| Function | Returns | Description |
|---|---|---|
| withAlpha(float a) | Colour | Copy with alpha set (0.0-1.0). An int overload withAlpha(int a) takes 0-255. |
| withMultipliedAlpha(float m) | Colour | Copy with the current alpha multiplied by m. |
| brighter(float amt) | Colour | Copy blended toward white (0.0 = unchanged, 1.0 = white). |
| darker(float amt) | Colour | Copy blended toward black (0.0 = unchanged, 1.0 = black). |
| withHue(float h) / withSaturation(float s) / withBrightness(float b) | Colour | Copy with one HSB component replaced (each 0.0-1.0). |
| interpolatedWith(Colour other, float t) | Colour | Linear blend; t 0.0 = this colour, 1.0 = other. |
| overlaidOn(Colour background) | Colour | Blends 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.
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.
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 Variable | Type | Description |
| x | float | X coordinate in pixels. |
| y | float | Y coordinate in pixels. |
| Bounds - a position + size rectangle | ||
|---|---|---|
| Member Variable | Type | Description |
| x | float | X position of the top-left corner. |
| y | float | Y position of the top-left corner. |
| width | float | Width in pixels. |
| height | float | Height in pixels. |
Ellipse
An ellipse shape. Draw it with fill(ellipse, …) and/or stroke(ellipse, …), passing a Colour or a Gradient.
| Member Variable | Type | Description |
|---|---|---|
| x | float | X position of the ellipse's bounding box. |
| y | float | Y position of the ellipse's bounding box. |
| width | float | Bounding-box width (full diameter on the x-axis). |
| height | float | Bounding-box height (full diameter on the y-axis). |
| strokeWidth | float | Outline 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 Variable | Type | Description |
|---|---|---|
| x | float | X position of the top-left corner. |
| y | float | Y position of the top-left corner. |
| width | float | Width in pixels. |
| height | float | Height in pixels. |
| strokeWidth | float | Outline thickness in pixels, used by stroke(). |
| cornerRadius | float | Radius 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 Variable | Type | Description |
|---|---|---|
| x | float | X position of the arc's bounding box. |
| y | float | Y position of the arc's bounding box. |
| width | float | Bounding-box width. |
| height | float | Bounding-box height. |
| startAngle | float | Start angle in radians. 0 = 3 o'clock; positive = clockwise. |
| endAngle | float | End angle in radians. |
| strokeWidth | float | Outline 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 Variable | Type | Description |
| strokeThickness | float | Stroke thickness in pixels (for strokePath()). |
| jointStyle | int | How stroked corners join: JointStyle::mitered = 0, curved = 1, beveled = 2. |
| endCapStyle | int | How stroked ends are capped: EndCapStyle::butt = 0, square = 1, rounded = 2. |
| Build functions | ||
|---|---|---|
| Function | Returns | Description |
| 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).
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 Variable | Type | Description |
| type | int | GradientType::Linear / Radial / Angular / Diamond. Default 0 = Linear. |
| x1 | float | X of the gradient start point (center for Radial). |
| y1 | float | Y of the gradient start point (center for Radial). |
| x2 | float | X of the gradient end point (edge for Radial). |
| y2 | float | Y of the gradient end point (edge for Radial). |
| Functions | ||
|---|---|---|
| Function | Returns | Description |
| 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 Variable | Type | Description |
|---|---|---|
| image | string | Image identifier - the name of the asset to draw. |
| x | float | X position of the top-left corner. |
| y | float | Y position of the top-left corner. |
| width | float | Display width in pixels. |
| height | float | Display 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 Variable | Type | Description |
|---|---|---|
| text | string | The string to render. |
| font | string | Font family name (e.g. "Inter"). Must be available to the instrument. |
| x | float | X position of the text's bounding box. |
| y | float | Y position of the text's bounding box. |
| width | float | Bounding-box width (text wraps if it overflows). |
| height | float | Bounding-box height. |
| fontSize | float | Font size in points. |
| fontColour | Colour | Text colour (ARGB). |
Font
A font spec for label-style widgets, passed to setFont() / setItemTextFont().
| Font - a label/value font spec | ||
|---|---|---|
| Member Variable | Type | Description |
| name | string | Font family name (e.g. "Inter"). |
| style | string | Style name: "Regular", "Bold", "Italic", "Bold Italic", etc. |
| height | float | Point height. A value ≤ 0 keeps the widget's current height. |
| justification | int | Text alignment; Justification:: constants: Keep=0 (no change), Left=1, Center=2, Right=3, plus combined TopLeft=4 … BottomRight=12 (CenterLeft=7, Centered=8, CenterRight=9). |
| kerning | float | Extra inter-character spacing factor (0.0 = default). |
| colour | Colour | Text 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 Variable | Type | Description |
| type | int | Effect 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). |
| radius | float | Blur/spread radius in pixels. |
| tint | Colour | Shadow/glow colour (DropShadow/Glow) or tint. |
| offsetX | float | Horizontal shadow offset in pixels (DropShadow). |
| offsetY | float | Vertical 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;.
| Function | Returns | Description |
|---|---|---|
| fill(Rectangle r, Colour|Gradient) | bool | Records a filled rectangle. Same overloads for Ellipse and Arc. Returns false if the canvas is full. |
| stroke(Rectangle r, Colour|Gradient) | bool | Records a stroked rectangle. Same overloads for Ellipse and Arc. Returns false if the canvas is full. |
| fillPath(Path p, Colour|Gradient) | bool | Records a filled path. Returns false if the canvas is full. |
| strokePath(Path p, Colour|Gradient) | bool | Records a stroked path. Returns false if the canvas is full. |
| paint(Text text) | bool | Records text. Returns false if the canvas is full. *** Not implemented yet: draws a placeholder and ignores the Text struct. |
| paint(Image image) | bool | Records an image. Returns false if the canvas is full. |
| clear() | void | Clears 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 Variable | Type | Description |
| path | string | Image asset name. |
| width | int | Per-frame width in pixels. |
| height | int | Per-frame height in pixels. |
| frames | int | Total 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.
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 Variable | Type | Description |
|---|---|---|
| x | int | X position of the mouse relative to the Component's origin. |
| y | int | Y position of the mouse relative to the Component's origin. |
| mods | int | Bitfield of held modifier keys. Compare with MouseModifiers::Command, ::Shift, ::Alt, or use the isCommandDown() / isShiftDown() / isAltDown() helpers below. |
| rightClick | int | Non-zero when the press is a right-click. Prefer isRightClick() / isLeftClick(). |
| type | MouseEventType | What kind of mouse action this is. One of Down, Up, Enter, Exit, DoubleClick, StartDrag, Drag, DragEnter, DragExit, Drop. |
| identifier | int | Identifier used internally to associate the event with its source. Most scripts can ignore this. |
Modifier-key helpers
| Function | Returns | Description |
|---|---|---|
| isCommandDown() | bool | true if Command (⌘ on macOS, Ctrl on Windows) was held during the event. |
| isShiftDown() | bool | true if Shift was held. |
| isAltDown() | bool | true if Alt / Option was held. |
Event-type helpers
| Function | Returns | Description |
|---|---|---|
| isLeftClick() | bool | true on a left-button press (Down event, non-right-click). |
| isRightClick() | bool | true on a right-button press (Down event, right-click). |
| isDown() | bool | true if type is Down. |
| isUp() | bool | true if type is Up. |
| isEnter() | bool | true if the mouse entered the Component's bounds. |
| isExit() | bool | true if the mouse left the Component's bounds. |
| isDoubleClick() | bool | true on a double-click. |
| isStartDrag() | bool | true when a drag operation starts. |
| isDrag() | bool | true if this is a drag event (mouse moved while a button is held). |
| isDragEnter() | bool | true when a drag enters the Component's bounds. |
| isDragExit() | bool | true when a drag leaves the Component's bounds. |
| isDrop() | bool | true 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.
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 Variable | Type | Description |
|---|---|---|
| keyCode | int | Raw key code. Compare with constants in the Qwerty namespace (e.g. Qwerty::SpaceBar, Qwerty::Enter, Qwerty::UpArrow) or use the helpers below. |
| mods | int | Bitfield of held modifier keys. Use isCommandDown() / isShiftDown() / isAltDown(). |
| identifier | int | Identifier used internally to associate the event with its source. |
Key helpers
| Function | Returns | Description |
|---|---|---|
| getKeyCode() | int | Returns the raw key code. |
| isSpaceBar() | bool | true if the event fired from the space bar. |
| isEnter() | bool | true if the event fired from Enter / Return. |
| isTab() | bool | true if the event fired from Tab. |
| isDelete() | bool | true if the event fired from the Delete key. |
| isBackspace() | bool | true if the event fired from Backspace. |
| isUpArrow() | bool | true if the event fired from the up-arrow key. |
| isDownArrow() | bool | true if the event fired from the down-arrow key. |
| isLeftArrow() | bool | true if the event fired from the left-arrow key. |
| isRightArrow() | bool | true if the event fired from the right-arrow key. |
| isCharacterKey(string character) | bool | true if the event matches the printable character passed in (e.g. "a", "$", " "). |
Modifier-key helpers
| Function | Returns | Description |
|---|---|---|
| isCommandDown() | bool | true if Command / Ctrl was held during the keypress. |
| isShiftDown() | bool | true if Shift was held. |
| isAltDown() | bool | true 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.
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 Variable | Type | Description |
|---|---|---|
| value | float | The value carried by the event. |
| identifier | int | Identifier used internally to associate the event with its source. |
Functions
| Function | Returns | Description |
|---|---|---|
| getValue() | float | Returns the event's value member variable. |
| getIdentifier() | int | Returns 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, thenaddKeyRange()draws it andremoveKeyRange()/clearKeyRanges()take it down. These are transient overlays owned by the script (matched byname); 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 theKeyRange.nameyou pass (every match, at instrument and articulation scope). Onlynameis 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.
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 Variable | Type | Description |
|---|---|---|
| startNote | int | First MIDI note in the range (inclusive). Range: 0-127. |
| endNote | int | Last MIDI note in the range (inclusive). Range: 0-127. |
| name | string | Display name shown on or near the highlighted region. |
| colour | Colour | Highlight colour shown on the GUI piano. Build with a colours:: constructor or use a named constant (e.g. colours::orange). |
| tooltip | string | Tooltip 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().
| Function | Returns | Description |
|---|---|---|
| addKeyRange(KeyRange range) | void | Draws 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) | void | Removes the script-owned range with this name. |
| clearKeyRanges() | void | Removes every script-owned range. Ranges authored in your instrument are left alone. |
| showKeyRange(KeyRange range) | void | Makes 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) | void | The 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) | void | Paints 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() | void | Clears every key painted by setKeyHeld(). |
| clearPlayableRanges() | void | Removes 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() | void | Reserved - not currently honoured by KODA (the event is ignored). Use clearKeyRanges() and hideKeyRange(). |
| setPlayableRange(int startNote, int endNote[, Colour colour]) | void | Reserved - 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.
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.
| Function | Returns | Description |
|---|---|---|
| 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. |
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
| Function | Returns | Description |
|---|---|---|
| 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. |
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
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.
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.