Skip to content

Scripting

The Logic script is the per-project show-logic layer. One TypeScript file, full IntelliSense in Monaco, executed inside the engine on a sandboxed JavaScript runtime.

This is the meat of the docs. The pages under this section cover the API in the order you'd actually learn it:

  1. Widgets API, read and write Stage widgets from a script.
  2. MIDI input & output, receive CCs and notes from controllers, send notes and CCs out for LED feedback.
  3. OSC (beta), send and receive Open Sound Control.
  4. Hooks, the lifecycle events a script reacts to: onInit, onActivate, onDeactivate.
  5. Recipes & patterns, full-script examples pulled from real rigs.

A 30-second overview

typescript
// Every script gets these globals:
//   widget(name)          -> SwWidget (get / set / on('change'))
//   plugin(name)          -> SwPlugin (setParam / getParam / bypass)
//   midi.input(name)      -> on('cc' | 'note')
//   midi.output(name)     -> sendCC / sendNoteOn / sendNoteOff
//   osc                   -> on(address, fn) / send(host, port, address, ...)
//   onInit(fn)            -> run after eval, and on every hot reload
//   onActivate(fn)        -> after a rack goes live (fn gets the rack name)
//   onDeactivate(fn)      -> before a rack is torn down
//   setTimeout / setInterval / sleep -> timers
//   state                 -> a bag that survives hot reload
//   console.log(...)      -> emits into Diagnostics -> Engine log

const pianoVol = widget('PIANO_VOL');

pianoVol.on('change', (v) => {
  console.log(`piano volume now ${v.toFixed(2)}`);
});

onInit(() => {
  pianoVol.set(0.7);
});

Types

The runtime ships ambient TypeScript declarations so Monaco's IntelliSense knows exactly what each global returns. Hover any symbol to see its signature. The bundled declarations are the single source of truth for what is implemented, if it autocompletes, it exists.

Limitations

  • No eval, no dynamic import(), no Function constructor.
  • No file system, network, process, fetch, or XMLHttpRequest. The script lives in a sealed sandbox, the engine is its only surface to the rest of the host.
  • One script per project (v1). Multi-file authoring is on the roadmap, for now compose with TypeScript modules locally and paste in.
  • Handlers run on the engine's message thread, never the audio thread, so even a slow or hung handler cannot glitch audio. Each handler has a short (~1 ms) soft budget, overruns log a warning but still finish. Keep handlers short.
  • The runtime is capped at 32 MiB of memory and a 512 KiB stack, so runaway allocation or infinite recursion is contained rather than taking the engine down.
  • Persistent state lives in the state global (JSON-serialisable values, survives hot reload). See Hooks → state.

When NOT to use a script

The MIDI bindings on a widget already handle "hardware controller moves this knob". You only need a script when the gesture is conditional, computed, or stateful:

  • Press hardware Button A, set widget X to 1 and widget Y to 0 (radio- style switch across ungrouped widgets).
  • LED feedback to the controller mirroring widget state (the AKAI MIDI Mix example).
  • Tempo-relative timing (a setTimeout chain that recomputes its period each tick).

If the gesture is "knob moves parameter", no script is needed, just bind the widget in the inspector.

Start with Widgets API.

Proprietary software, used under the Stagewright Software Licence.