Early Preview · Kiwisonic and Kiwisynth are free during early preview. The shape of both products is still being figured out, and feedback is what drives them forward.

Script examples

All docs / Kiwisynth / Script examples

Kiwisynth manual

Script examples

A cookbook. Every entry on this page is one of the sample scripts that ship with the synth, in the script-samples folder and in the editor's sample picker. The sample files are the single source of truth: this page is generated from them, so what you read here is exactly what you can drop into the editor.

For the language and primitives, see the overview. For per-handler details, see the handlers page.

Each entry shows the snippet wrapped in the handler it belongs to, then explains what makes it worth writing as a script instead of a routing. A Globals entry is a declaration you place in the Globals scope; pair it with the handler that uses it.

Globals

Declarations that live in the Globals scope. A handler sample that reads hits, note_count, or a seed expects the matching declaration here.

Per voice counter

voice var hits = 0

A per-voice counter that survives across note events for one voice. The mod matrix has no user-defined state - it can only sum modulation sources. Use this kind of variable to track "how often has THIS note been hit" or "what stage is this voice in".

Global counter

var note_count = 0

A global counter shared across every voice and every event. The mod matrix has no way to count notes - this is script-only.

Deterministic randomness

seed 42

Pin the random seed so a generative script renders identically on every load and every offline bounce. Without a seed line the script is reproducible by default; use seed live to randomise per session.

on init

Runs once when the script loads. Prime global state or log a startup line.

One shot setup

on init
    log("script loaded")
end

on init runs ONCE, immediately after the script loads. It is the only event of its kind - useful for setup logging or for stamping a global var with a starting value derived from the patch.

on note

Fires per voice at note-on. Decisions that depend on which note was played and how hard.

Velocity threshold

on note
    if velocity > 100
        # ramp(paramPath, targetOffset, durationMs) glides the script's
        # offset to `targetOffset` over `durationMs`. The offset is added
        # to the parameter's base value - here, +4000 Hz on filter cutoff.
        ramp("filt_cutoff", 4000, 80)
    end
end

Discrete branch on velocity: the mod matrix can only SCALE velocity (continuous, linear). A script can act only above a threshold - here, only hard hits open the filter.

Pitch bucket

on note
    if pitch < 48                    # below C3 - bass register
        modulate("filt_reso", 0.3)
    elif pitch < 72                  # C3..B4 - mid register
        modulate("filt_reso", 0.1)
    end
end

Different behaviour per pitch range. The mod matrix has no notion of "if pitch is below 48" - it only multiplies a pitch-derived source. This sample adds resonance only to bass-register notes.

Pitch reference: 36 = C2 (bass), 48 = C3, 60 = C4 (middle C), 72 = C5, 84 = C6.

Bloom then decay

on note
    ramp("filt_cutoff", 4000, 80)    # open quickly: +4000 Hz over 80 ms
    after 200                        # 200 ms later...
        ramp("filt_cutoff", 0, 600)  # ...glide back to 0 over 600 ms
    end
end

Time-staged behaviour: open the filter NOW, then close it 200 ms later. after schedules a deferred block - the mod matrix has no concept of "do X, then later do Y" for a single voice.

Count hits

on note
    hits = hits + 1
    if hits % 3 == 0                 # every third hit
        modulate("master_vol", 0.2)
    end
end

Increment a per-voice counter on each note. Requires a matching voice var hits = 0 in Globals (one slot per voice; persists for that voice's lifetime).

Use the counter to drive behaviour the engine cannot - e.g. "every third hit on this voice is louder".

Relative pitch layer

on note
    if velocity > 100                # only on hard hits
        # play(pitch, velocity, durationMs) - softer than the main voice
        # so the layer sits underneath rather than fighting it.
        play(pitch + 7, 70, 250)
    end
end

Add a one-shot harmony layer a perfect fifth above whatever key was just played. The mod matrix can transpose oscillators by a fixed interval, but it cannot spawn an extra voice at a relative pitch - that requires a script.

pitch is the bound variable from the note-on, so pitch + 7 (semitones) tracks the performance instead of sitting on a fixed note. Useful for parallel-fifth leads, octave doublers (+ 12), bass sub-octave layers (- 12), or chord stabs on accented hits.

Random pan scatter

on note
    modulate("osc1_pan", (random() - 0.5) * 1.6)
end

Scatter every note to a different point in the stereo field. A per-note random offset is something the mod matrix cannot do - an S&H LFO is global and time-driven, not re-rolled per voice.

osc1_pan is -1 (hard left) .. +1 (hard right); (random() - 0.5) * 1.6 spreads notes across roughly 80% of the width, kept off the extremes.

on release

Fires per voice at note-off, while the voice is still in its release tail.

Post release echo

on release
    after 200
        # play(pitch, velocity, durationMs) - pitch+12 is one octave up
        # from the released note, velocity 50 = soft, 120 ms duration.
        play(pitch + 12, 50, 120)
    end
end

Trigger another note 200 ms AFTER the user lifts the key. The synth's own release behaviour cannot fire a fresh note - only the script can.

Darken the tail

on release
    ramp("filt_cutoff", -3000, 500)
end

Round off the release: as soon as the key is lifted, glide this voice's filter cutoff down so the tail fades darker than it played. The release envelope shapes level, not timbre - only a script can tie a cutoff move to note-off for the same voice.

Cutoff offsets are in Hz; -3000 over 500 ms pulls the brightness out gradually as the note rings down.

on tick

Fires once per audio block, the tightest cadence a script runs at. Shape modulation the LFOs cannot.

Stepped wavetable

on tick
    modulate("osc1_wt_pos", floor(phase * 4) / 4)
end

Quantize the wavetable position to 4 discrete frames per beat. The mod matrix and built-in LFOs always interpolate smoothly between values - only a script can write a stepped, non-smooth target.

floor(phase * 4) is 0, 1, 2 or 3 across the beat. Dividing by 4 gives osc1_wt_pos values 0.0, 0.25, 0.5, 0.75 - a 4-step staircase.

Asymmetric pulse

on tick
    if phase < 0.7                   # first 70% of the beat
        modulate("filt_cutoff", 2000)
    else
        modulate("filt_cutoff", -1000)
    end
end

Asymmetric square: 70% of each beat the cutoff is up, 30% down. The built-in LFO square shape is fixed at 50/50 duty - only a script can shape its own pulse width like this.

Filter cutoff offsets are in Hz (raw parameter units).

Trance gate

on tick
    if phase > 0.5
        modulate("master_vol", -1)
    else
        modulate("master_vol", 0)
    end
end

An eighth-note volume gate: full level for the first half of every beat, silenced for the second. The classic "trance gate" chop. A gate LFO could approximate it, but here the duty is a number you read and edit directly.

phase runs 0..1 across the beat. modulate latches and holds across blocks, so the open half must restore 0 explicitly - otherwise the -1 offset (which pulls master_vol to its 0 floor) would stick shut.

on beat

Fires on each musical beat boundary. The home for sequencers and play calls that land on the grid.

Polyrhythm

on beat
    if index % 4 == 0
        play(36, 110, 200)
    end
    if index % 6 == 0
        play(43, 80, 150)
    end
end

Two independent rhythms running simultaneously - a kick every 4 beats and a snare every 6. The arpeggiator can only play ONE rhythm at a time; a script can interleave any number.

Evolving pattern

on beat
    play(60 + (index % 12), 90, 100)
end

A pattern that changes WITH the bar - pitch climbs one semitone per beat. Neither the arpeggiator nor the motif player can derive note choice from a running global counter like this.

Random octave bass

on beat
    play(36 + floor(random() * 13), 100, 160)
end

Fire a random bass note inside one octave on every beat. With the default fixed seed the sequence is identical on every playback, so a bounce and a live take agree - generative but reproducible. The arpeggiator and motif player follow a written pattern; this invents one from the RNG.

36 is C2; floor(random() * 13) adds 0..12 semitones (a full octave).

on cc

Fires when a Control Change arrives. Turn a controller into a trigger or a discrete switch the mod matrix cannot reach.

Wheel triggers chord

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

Crossing the wheel above half fires a chord. A CC in the mod matrix can only modulate - it cannot fire notes. Only a script can turn an incoming MIDI CC into a note trigger.

Discrete switch

on cc(1)
    set("filt_type", floor(value * 6))
end

CC selects a discrete filter type (LP12 / LP24 / HP / ...). The mod matrix never targets stepped/discrete parameters - only continuous ones. set is script-only and the right tool for switch parameters.

on pitchbend

Fires on the pitch wheel. Reshape the wheel response beyond the built-in linear bend.

Cubic bend

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

A non-linear pitchbend curve: gentle near the centre, aggressive at the extremes. The synth's built-in bend is strictly linear; the mod-matrix bend source is also linear.

on aftertouch

Fires on channel pressure. Turn pressure into a trigger instead of a modulator.

Pressure triggers note

on aftertouch
    if value > 0.85
        # play(pitch, velocity, durationMs) - pitch 72 = C5, vel 60 = soft,
        # 80 ms before the synth's normal release takes over.
        play(72, 60, 80)
    end
end

Hard pressure (above 85% of max) fires a one-shot note. Aftertouch in the mod matrix can only act as a modulator; only a script can use it to TRIGGER a note when it crosses a threshold.

on transport

Fires on a host play/stop edge. React to the transport and reset script- owned state.

Counter reset

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

Reset the shared counter when the host hits play. Pair this with the "Global counter" Globals sample, which declares var note_count = 0. The arpeggiator and motif player react to transport but cannot reset script-owned state - only the script can.

Determinism

By default two listens to the same patch playing the same notes produce the same audio. The script clock counts samples since load, not wall-clock time, and random() is seeded from a fixed patch seed, so an offline bounce and a live take agree. The generative samples above (random() bass, scattered pan, index-driven patterns) are reproducible for this reason; add seed live in Globals only when you want a patch to vary between takes.