Script Overview

The Usine scripting language turns patches into programmable control surfaces, timing engines, and content generators. It extends Object Pascal with BrainModular-specific APIs so scripts can react to audio, MIDI, spatial data, and UI events across a workspace.

Script V2.0 introduces method-based access on parameters and Usine objects. If you are migrating from Script V1.x, review the compatibility notes to replace legacy helpers with their method equivalents.

Usine evaluates scripts at compile time, at the audio block rate, at the user interface refresh rate, and whenever an inlet changes. The sections below detail how to structure a script, which APIs are available, and how those APIs map to BrainModular Usine features such as racks, the Master Synchro panel, MIDI Matrix modules, and XY Draw views.

procedure Callback(N: Integer);
begin
  if N = transpose then Trace('Semitones', transpose.asInteger);
end;

Script Architecture

Init

Init runs when the script module loads or recompiles. Create every inlet/outlet and initialize state because Usine clears globals between compile passes.

procedure Init;
begin
  noteIn := CreateParam('MIDI In', ptMidi, pioInput);
  noteOut := CreateParam('MIDI Out', ptMidi, pioOutput);

  transpose := CreateParam('Transpose', ptDataFader, pioInput);
  transpose.Min(-24);
  transpose.Max(24);
  transpose.Format('%.0f');

  tick := CreateParam('Tick', ptButton, pioInput);
  tick.CallbackMode(cbImmediate);
end;

This example maps to MIDI In/Out modules and a tick button that can drive an arpeggiator inside a patch.

Destroy

Destroy fires when the module is deleted or recompiled. Free dynamic structures created in Init.

Inlets and outlets are released automatically.

procedure Destroy;
begin
  noteNames.Free;
  SetLength(activeNotes, 0);
end;

Process

Process executes every audio block (typically 3 ms). Keep it deterministic, avoid GUI calls, and push results into outlets.

procedure Process;
var msg: TMIDI;
    i: Integer;
begin
  for i := 0 to noteIn.Length - 1 do
  begin
    msg := noteIn.asMidi(i);
    msg.data1 := msg.data1 + transpose.asInteger;
    noteOut.asMidi(i, msg);
  end;
  noteOut.Length(noteIn.Length);
end;

This transforms incoming MIDI data before it enters downstream MIDI Transformer or Sequencer modules.

ProcessThread

ProcessThread runs in its own worker thread every 10 ms. Use it for heavy calculations that should not block the audio chain or polling external devices.

procedure ProcessThread;
var i: Integer;
begin
  for i := 0 to pointsCount - 1 do
  begin
    cachedX[i] := GetObjectFloat(pointNames[i]);
    SetObjectFloat(targetNames[i], cachedX[i]);
  end;
end;

Avoid MIDI manipulation here; the worker thread is not sample accurate.

ProcessIDLE

ProcessIDLE runs on the UI refresh clock (25–50 ms). Trigger Usine messages, update colors, and perform housekeeping that should stay off the realtime thread like GUI stuffs.

procedure ProcessIDLE;
begin
  SendUsineMsg('panel', 'refresh', 'custom-display');
  ModuleColor($FF3298DB);
end;

Callback

Callback executes when an inlet changes. The integer argument identifies the parameter that triggered the event, making this the most efficient place to respond to user interaction. That allows an optimization over constant running Process procedure: the code will run only if input changed.

The Callback will scan the inputs from 0 to n and can trig a specific code when input change is detected.

So if results are supposed to be computed only when input changed, prefer the callback to the process procedure. However if things need computations at a different block time that input changed, still need the process procedure.

For example an input change can set to 1 a parameter in callback, but if wanna set this param back to 0 next block, (like a 'has chg' behavior) the code to set back to 0 will occur in Process Procedure, unless it's triggered by an input.

You can use either Callback or Process Procedure, or booth simultaneously according to your needs.

By default Callback is fast, called immediately after the inlet has been modified in the audio thread. You can turn the Callback to slow for an inlet with the procedure : MyInlet.CallbackMode(cbMainThread). Slow callbacks are suitable for GUI modification and dynamic patching. Slow callbacks are provided at messages rate, typically every 25 or 50ms depending on the setup refresh speed.

For finer control, use CallbackMode to target a specific thread — for example cbImmediate for callback immediate (in audio thread), cbMainThread for general purpose callbacks, cbGraphic for UI repaints, cbNetwork for OSC/TCP output, or cbAsync for heavy background work. See Parameter Creation and Layout for the full list of modes.

procedure Callback(N: Integer);
begin
  case N of
    transpose:
      Trace('Transpose semitones', transpose.asInteger);
    tick:
      if activeNotesCount > 0 then PlayNextNote;
  end;
end;

Include Files

Scripts can share common code between several script modules with the {$include} compiler directive (alias {$i}). The directive inserts the content of another file at the point where it appears, before compilation.

{$include my-library.pas}

procedure Init;
begin
  // functions declared in my-library.pas are available here
end;

The file name is resolved like any other media file: absolute paths are used as is, and relative names are searched in the patch folder, the workspace folder and the package folders. If the file still cannot be found, Usine opens the file locator.

Use {$include_once my-library.pas} to include a file only the first time it is encountered, which avoids duplicate declarations when several included files include the same shared library themselves.

Reserved Identifiers

Script identifiers are case insensitive and share a single namespace with every built-in declaration. A variable or function cannot reuse the name of a built-in type, function or constant — the compiler stops with Duplicate declaration. Typical traps: pFloat/pSingle (the pointer types PFloat/PSingle exist), Log, Null, Platform, TParameter, Trace. When in doubt, prefix your variables (myFloat, paramGain, ...).

Control Flow

Loops (for, while, repeat) support break and continue. Both accept an optional depth: break(2) exits two nested loops at once. Exit leaves the current procedure or function immediately.

for i := 0 to count - 1 do
begin
  if values.asArray(i) < 0 then continue;   // skip this element
  for j := i + 1 to count - 1 do
    if values.asArray(j) = values.asArray(i) then
      break(2);                             // leaves both loops
end;

Operators and Language Extensions

Compound assignment operators

The C-style compound assignments are enabled by default:

x += 2;      // x := x + 2
x -= 1;      // x := x - 1
x *= 0.5;    // x := x * 0.5
x /= n;      // x := x / n

Operator overloading

Scripts can define operators on their own record types, which keeps vector, 3D or color math readable:

type TPoint2D = record
  x, y : single;
end;

operator + (a, b: TPoint2D): TPoint2D;
begin
  Result.x := a.x + b.x;
  Result.y := a.y + b.y;
end;

// later: p := p1 + p2;

Swap, Default, New, Dispose

Procedure Description
procedure Swap(var A, B); Exchange the contents of two variables of the same type
function Default(T): T; Return the zero/default value of a type (e.g. Default(TPoint2D))
procedure New(var P: pointer-to-record); Allocate a record for a typed pointer
procedure Dispose(var P: pointer-to-record); Free a record allocated with New

label / goto

Standard Pascal labels are supported: declare them in a label section, mark a statement with name: and jump with goto name;.

procedure Process;
label retry;
var attempts : integer;
begin
  attempts := 0;
retry:
  attempts += 1;
  if (not TryStep) and (attempts < 3) then
    goto retry;
end;

goto only jumps within the current procedure. In almost every case a loop with break/continue reads better — keep goto for rare bail-out patterns.

Compiler Hints

The compiler reports non-blocking hints in the script console at every compilation:

Hint Meaning
Variable "x" not used A global or local variable is declared but never used
Parameter "x" not used A procedure/function parameter is never referenced in the body
Parameter "x" not set A var/out parameter is never assigned in the body
Result not set A function never assigns its Result
Method "x" is deprecated / is deprecated "msg" A call to a function marked deprecated (e.g. TParameter.FastCallback — use CallbackMode)
Method "x" is not implemented A call to a function marked unimplemented
Method "x" is experimental A call to a function marked experimental

Scripts can mark their own functions with the same directives, after the header:

procedure OldHelper; deprecated 'use NewHelper instead';
begin
  ...
end;

Every call to OldHelper then reports the hint, which makes it easy to phase out old code shared between scripts.

Error Handling

Scripts support structured error handling with try..except..end and try..finally..end blocks. Both forms can be combined in a single try..except..finally..end statement.

except runs only when an error occurs in the protected block (for example a division by zero or an array access out of bounds). finally runs in every case, even when the protected block exits early — use it to release resources.

procedure Callback(N: Integer);
begin
  if N = trigger then
  try
    result.asFloat(numerator.asFloat / denominator.asFloat);
  except
    result.asFloat(0);  // fallback value on error
    Trace('calculation failed');
  end;
end;
var list: TStringList;
begin
  SLCreate(list);
  try
    SLLoadFromFile(list, fileName.asString);
    Trace(list.Count);
  finally
    SLFree(list);       // always executed, even on error
  end;
end;

Inside an except block, raise; re-raises the current error so it is reported by Usine as usual after the script has done its own cleanup.

Scripts can also raise their own errors with RaiseException, with a plain message or with a standard TIFException code (erCustomError, erOutOfRange, erDivideByZero, ...). ExceptionToString builds the corresponding message text.

if denominator.asFloat = 0 then
  RaiseException(erCustomError, 'denominator must not be zero');

The on E: Exception do typed clause is not supported: the except block catches every error, and the error message itself is not accessible from the script.

Execution Context Constants

BlocSize

BlocSize returns the number of samples in the current audio block. Use it to scale per-block accumulations or convert counts to milliseconds.

procedure Process;
begin
  rms := rms + sqr(input.asFloat) / BlocSize;
end;

BlocDuration

BlocDuration exposes the duration in milliseconds of the current audio block.

tempoLag := tempoLag + BlocDuration * 10.0;

RefreshSpeed

RefreshSpeed is the interval in milliseconds of the UI refresh loop. Tie slower automation to this cadence when running inside ProcessIDLE.

SampleRate

SampleRate holds the current audio sampling rate configured in the setup panel.

samplesPerBeat := Round((60 / hostTempo) * SampleRate);

Platform

Platform is a TPlatform enum that identifies the active operating system and hardware.

Type Description
plWindowsIntel Windows on x86/x64 processors.
plWindowsARM Windows on ARM processors.
plMacOSIntel macOS on Intel processors.
plMacOSARM macOS on Apple Silicon/ARM processors.
plLinuxIntel Linux on x86/x64 processors.
plLinuxARM Linux on ARM processors.
plIOSARM iOS/iPadOS on ARM processors.
plAndroidARM Android on ARM processors.
if Platform in [plWindowsIntel, plWindowsARM] then
  Trace('Running on Windows')
else
  Trace('Running on other platform');

Messaging and Diagnostics

Use these helpers to monitor script behavior and communicate with other BrainModular subsystems such as the Master Synchro panel, browser, or timeline.

Procedure Description
procedure Trace(S: string; Value: variant); Output a debug message combining a label and a value (variant type) to the Usine trace panel
procedure Trace(S: string); Output a text message to the Usine trace panel
procedure Trace(I: Integer); Output an integer value to the Usine trace panel
procedure Trace(F: Single); Output a floating-point value to the Usine trace panel

Trace

Trace is overloaded for strings, integers, floats, and variants. It writes to the Usine panel and to the script editor log.

Trace('Active voices', voiceCount.asInteger);
Trace('Tempo', hostTempo);
Trace('BlocSize');

Module Styling

ModuleColor

Sets the module tint in the patch so custom scripts integrate visually with other modules.

procedure Init;
begin
  ....
  ModuleColor($FF8E44AD);
end;

Transport Commands

Procedure Description
function GetTimecodePos: double; Gets the current Usine timecode position (in seconds or beats, depending on context)
procedure SetTimecodePos(pos: double); Sets the current Usine timecode position to the specified value

SetTimecodePos

procedure SetTimecodePos(pos: double);

Sets the current Usine timecode position to the specified value.

if jumpButton.asInteger = 1 then
  SetTimecodePos(musicTime);

GetTimecodePos

function GetTimecodePos: double;

Gets the current Usine timecode position.

  trace(GetTimecodePos);

more about scripts

version 7.0.250121

Edit All Pages