Early Preview · Kiwisonic, Kiwisynth, Kiwipulse and Kiwibass are free during early preview. Their shape is still being figured out, and feedback is what drives them forward.

Script handlers

All docs / Kiwisynth / Script handlers

Kiwisynth manual

Script handlers

A script reacts to events. Each event has its own handler, written as on <eventname> opening a block that closes with end. In Scoped mode the editor handles the on ... end wrapper for you; this page shows the assembled form.

Two handlers, note and release, run per voice. The rest run globally: there is one instance of the handler, not one per voice. Per-voice handlers see the voice's own voice var slots; global handlers do not.

Every handler can read the always-available clock variables bpm, time, and playing. The event-specific variables are listed under each handler.

For the language itself, the parameter primitives, and the editor, see the scripting overview.

on init

Fires once, on the first audio block after the script is applied (or the patch is loaded). Use it to log a startup line, prime a global counter from a tuning constant, or warm up state.

Bound variables: bpm, time, playing.

on init
    log("script loaded")
end

There is no matching shutdown handler. State that needs resetting is reset by re-applying the script.

on key

Fires on key-down, before a voice exists. It is the head of the note pipeline, ahead of the chord builder and the arpeggiator, so what this handler leaves behind is what those two act on.

Bound variables: pitch (0..127), velocity (0..127), channel, length, plus the clock variables.

length is how long the note is scheduled to last, in milliseconds, the same unit as play, ramp and after. Only a host that plays the note from a clip and says how long it is can give one. A key played live, or a note from a host that does not say, reads 0, so test length > 0 before treating a note as short. It is what was scheduled, not a promise: releasing the key still ends the note.

on key
    if length > 0 and length < 120
        velocity = velocity * 0.55
    end
end

pitch and velocity are writable here, and nowhere else. Assign to them and the whole instrument sees the new value: the chord builder, the arpeggiator, the voice, and MIDI Out.

on key
    pitch = scale_quantize(pitch)
    velocity = velocity * 0.8
end

skip() drops the key press so it makes no note. It drops the note and not the key: the instrument still knows you are holding it, and held_count() still counts it.

on key
    if pitch < 48
        skip()
    end
end

Writing a pitch outside 0..127 also drops the note. It is not clamped, because a wrong note at the edge of the keyboard is worse than silence.

This is a global handler, since there is no voice yet. A voice var is not available, and modulate from here writes the global lane, which moves every sounding voice rather than the note arriving. To shape the note that is arriving, use on note.

It does not fire for notes a script holds with hold(), or for the arpeggiator's own output. A note the script generates never comes back into the script.

on note

Fires per voice, at note-on, after the voice has been claimed but before audio for that note has rendered. Use it to make decisions that depend on which note was played and how hard.

It runs too late to change the pitch: the voice is already sounding it. on key is the handler that can.

Bound variables: pitch (0..127), velocity (0..127), channel (MIDI channel of the source note), length (as in on key), plus the clock variables. Every tone of a chord carries the length of the key that made it.

on note is the only handler where a voice var is initialised fresh. Inside its body, the voice variables are at their declared defaults; they keep their values across later on tick-style events only for this voice's lifetime.

on note
    if velocity > 100
        ramp("filt_cutoff", 4000, 80)
    end
end

A script-fired play(...) does not trigger another on note. The handler runs for notes received from the keyboard or the host only, not for notes the script itself starts.

on release

Fires per voice, at note-off. The voice is still alive at this point; it is in its release tail. Use it to schedule echoes, drop a parting modulation, or count releases per pitch.

Bound variables: pitch, channel, plus the clock variables. There is no velocity here because note-off does not carry release velocity in this version.

on release
    after 200
        play(pitch + 12, 50, 120)
    end
end

pitch is the note that was released, which is the same note the matching on note saw.

on tick

Fires globally, every 64 samples, and once more where a note starts between two of those. This is the tightest cadence a script can run at: about 750 times per second at 48 kHz, whatever the host's buffer size, so a script sounds the same in every project.

Bound variables: phase (0..1, the script clock's position within the current beat), index (tick counter since script load), time (seconds since load), plus the clock variables.

Use on tick to shape modulation that the LFOs cannot, or to advance a step sequencer that the arpeggiator does not cover.

on tick
    # Asymmetric pulse: 70% high, 30% low.
    if phase < 0.7
        modulate("filt_cutoff", 2000)
    else
        modulate("filt_cutoff", -1000)
    end
end

on tick is global. To do per-voice work on every tick, work from on note plus voice var plus after.

on beat

Fires globally on each musical beat boundary. The host's tempo and position drive the timing, so the handler is bar-locked across loops and bounces.

Bound variables: phase (0..1 within the beat at the moment the beat fires; usually zero, but non-zero if a block crosses a beat boundary), index (beat counter since load), plus the clock variables.

Use on beat for sequencers and step-counters. The handler is the right home for play calls that should land on the grid.

on beat
    # Two rhythms at once.
    if index % 4 == 0
        play(36, 110, 200)
    end
    if index % 6 == 0
        play(43, 80, 150)
    end
end

When the host is stopped, on beat does not fire. Beat counting is tied to host transport.

on cc(N)

Fires globally when a MIDI CC with number N arrives on any channel. N is part of the header: on cc(1) is the mod wheel, on cc(7) is channel volume, on cc(74) is the filter-cutoff CC. A script can have several on cc handlers, one per CC number.

Bound variables: value (0..1, the CC value normalised), channel, plus the clock variables.

Use on cc to turn a CC into something the mod matrix cannot do: fire a chord on a threshold, switch a discrete parameter, draw a non-linear curve.

on cc(1)
    if value > 0.5
        play(60, 100, 200)
        play(64, 100, 200)
        play(67, 100, 200)
    end
end

The CC also reaches the mod matrix in the normal way. The script handler runs in addition.

on pitchbend

Fires globally when the pitch wheel moves. Use it to draw a non-linear bend curve on top of the synth's built-in linear bend.

Bound variables: value (-1..+1, where 0 is centre), channel, plus the clock variables.

on pitchbend
    modulate("master_tune", value * value * value * 1200)
end

The synth's own pitch-bend handling still applies. The script's contribution adds on top, so a cube curve here gives a centre-soft, edge-aggressive bend without disabling the built-in one.

on aftertouch

Fires globally on channel aftertouch (the pressure value from many keyboards after a key is held). Polyphonic aftertouch is a separate modulation source in the mod matrix; this handler is the channel-pressure one.

Bound variables: value (0..1), channel, plus the clock variables.

on aftertouch
    if value > 0.85
        play(72, 60, 80)
    end
end

Aftertouch is a modulator in the mod matrix; turning it into a note trigger or a discrete switch is script territory.

on transport

Fires globally on the edges of the host transport: when the host starts playing or stops. Use it to reset script state at the start of a section, or to log when the host moved.

Bound variables: playing (true on a start edge, false on a stop edge), bpm.

on transport
    if playing
        note_count = 0
        log("transport started")
    end
end

on transport does not fire continuously while the host runs. It fires at the edges only. For "every block while the host runs", use on tick and read playing.