Guide

Program custom modules

With a small script language you build your own modules for the Analog Modular Simulator: declare knobs, switches, jacks and LEDs, describe the signal processing per sample – and you have a module that sounds in the rack like any other.

For AI assistants: AGENTS.md describes the complete language with all functions, limits and examples – hand it to your assistant and it writes modules that run right away.

Download AGENTS.md

Quick start

A custom module is a text file with the extension .amsmod. You can write it in any text editor.

  1. Download the example fold.amsmod or copy the code below into a new file.
  2. Open the simulator and choose File → Load custom modules … (or “Load custom modules …” at the bottom of the module library).
  3. Choose the file. Every module appears as a preview with running LEDs – “Add” puts it into the rack and under “Custom modules” in the library.
// Wavefolder with a level meter – an example custom module for the Analog Modular Simulator.
module fold "Fold" hp 6 theme black {
  desc de "Wavefolder: faltet die Welle je nach FOLD mehrfach um – aus einem Sinus wird ein obertonreicher Klang."
  desc en "Wavefolder: folds the wave over itself depending on FOLD – a sine turns into a rich, buzzy tone."

  knob   amount "FOLD" 0..1 = 0.3
  knob   depth  "CV"   0..1 = 0.5 size s tip "CV amount"
  switch mode   "MODE" "SIN" "TRI" = 0
  in     x      "IN"   audio
  in     cv     "CV"   cv
  out    y      "OUT"  audio
  led    hot    red
  meter  level  "LEVEL"

  process {
    g = 1 + 4 * clamp(amount + cv / 10 * depth, 0, 1)
    s = x / AUDIO * g
    y = (mode == 0 ? sin(s * PI / 2) : fold(s, -1, 1)) * AUDIO
    hot = abs(y) > 4.5
    level = peak(y)
  }
}
Download fold.amsmod

File structure

A file contains one or more blocks module id "Name" { … }. Each block becomes a module of its own; the id (lowercase letters, digits, _) is its fixed identity, the name is printed on the panel.

Inside the block come the declarations – one per line –, custom functions (fn) and exactly one process { … }. Comments start with // or #. Inside parentheses and square brackets a line may wrap.

Reserved names

Keywords, functions and constants are reserved and cannot be names – so an input is not called voct, hz or gate. Besides all functions of the reference these are:

module desc knob switch button in out led meter state buffer const process fn for return if else and or not true false PI TAU E C4 AUDIO GATE ENV SR

Controls and jacks

Every declaration gets an id (its name in the code) and usually a label in quotes (at most 8 characters, usually upper case like on real hardware). [n] after the id turns it into an array of n equal elements – see Loops and arrays.

  • knob id "LABEL" min..max = start – rotary knob. The value has the knob’s unit and is smoothed. Options: log (logarithmic, min > 0), int (whole numbers, not smoothed), unit hz|s|ms|db|pct|v|st|bpm (tooltip display; pct expects 0..1), size s|m|l, tip "Tooltip", at x y, skin """<svg>…""".
  • switch id "TITLE" "A" "B" ["C"] = 0 – toggle switch with 2 or 3 positions; the value is 0, 1 or 2.
  • button id "LABEL" = off – latching button with LED; the value is a boolean (true/false).
  • in id "LABEL" audio|pitch|gate|cv – input jack, value in volts, 0 without a cable. The signal type only selects the preview’s test signal. With normal other an empty input reads another input.
  • out id "LABEL" audio|pitch|gate|env|mod|cv – output jack in volts. An output keeps its value until it is assigned again. The signal type shows in the plug tooltip; via a, b instead of a type passes on the type of inputs.
  • led id red|green|yellow|blue – LED, brightness 0..1 or a boolean; it glows on for about 150 ms so that short pulses are visible too.
  • meter id "LABEL" – level meter of six LEDs like on the Output module, value 0..1 – usually level = peak(signal).

State, buffers, constants

  • state id = 0 – a value that persists from sample to sample (number or boolean). Local variables, in contrast, only live within the current sample. As an array: state id[8] = 0 or with a list state id[] = [0, 1, 0, 1].
  • buffer id 500 ms – delay line for write and read/readi; at most four buffers, 8 seconds in total.
  • const id = 2 * PI – constant computed at compile time (numbers, constants and pure functions). As a table: const scale[] = [0, 2, 4, 7, 9].

The process block

process { … } runs once per sample – at 48 kHz that is 48,000 times per second. It consists of assignments (=, +=, -=, *=, /=, also to array elements a[i] = … and as x, y = f(…)), if … { } else { }, for i in a..b { } and write(buffer, value). Every module needs a fixed, bounded amount of computation per sample – that is why loops have a fixed number of steps.

A local variable is created by its first assignment and may only be read once it is assigned on every path – an if without else is not enough. Values across samples belong in a state.

There are two types: numbers and booleans. They never mix on their own. Conditions need booleans (x > 1, high(x)), arithmetic needs numbers: num(b) gives 0/1, gate(b) 0/10 V. An output always gets a number.

if and condition ? a : b compute only the chosen branch. and and or, in contrast, always evaluate both sides, so that rise(a) and rise(b) never misses an edge.

Like every module in the rack, a custom module sees its inputs one sample late – feedback through cables is allowed. Safe math: division by 0 gives 0, invalid values at outputs become 0, outputs are limited to ±100 V.

Operators

From strongest to weakest binding: parentheses and calls · ^ (power) · sign - + · * / % · + - · comparisons < <= > >= == != (not chainable) · not or ! · and or && · or or || · ? :.

Voltages and constants

Like in real Eurorack: audio ±5 V, envelopes 0 to 10 V, gates and triggers 0/10 V (a gate counts as on from 1 V and as off below 0.1 V), pitch 1 V per octave with 0 V = C4 (261.63 Hz).

The built-in generators return normalised values (±1 or 0…1) – volts only come in at the jack: y = saw(f) * AUDIO. Times are in seconds (pulse in milliseconds), frequencies in Hz.

Constants: PI, TAU, E, C4 (261.6256), AUDIO (5), GATE and ENV (10), SR (sample rate at run time), true, false.

Functions

Arguments with a default may be omitted. num means a number, bool a boolean, buffer and jack the name of a buffer or a jack (input or output).

Math

abs(x: num) → num
Absolute value.
sign(x: num) → num
Sign: −1, 0 or 1.
floor(x: num) → num
Round down.
ceil(x: num) → num
Round up.
round(x: num) → num
Round (halves away from 0).
frac(x: num) → num
Fractional part x − floor(x), always 0…1.
sqrt(x: num) → num
Square root; negative values give 0.
exp(x: num) → num
e to the power of x (x at most 80).
log(x: num) → num
Natural logarithm; values ≤ 0 count as 1e-30.
log2(x: num) → num
Logarithm to base 2.
exp2(x: num) → num
2 to the power of x.
sin(x: num) → num
Sine (radians).
cos(x: num) → num
Cosine (radians).
tan(x: num) → num
Tangent (radians), limited to ±1,000,000.
tanh(x: num) → num
Soft saturation, exactly ±1 from |x| = 3.
atan(x: num) → num
Arc tangent.
atan2(y: num, x: num) → num
Angle of the point (x, y) in −π…π.
min(a: num, b: num) → num
Smaller value.
max(a: num, b: num) → num
Larger value.
clamp(x: num, lo: num, hi: num) → num
Limit the value to lo…hi.
mix(a: num, b: num, t: num) → num
Crossfade: a + (b − a) · t (t = 0 → a, t = 1 → b).
mod(a: num, b: num) → num
Remainder of the division, floored (positive for positive b even with negative numbers).
pow(a: num, b: num) → num
Power a^b; a negative base with a fractional exponent is mirrored.
fold(x: num, lo: num = -5, hi: num = 5) → num
Wavefolding: folds x back and forth between lo and hi.
wrap(x: num, lo: num = 0, hi: num = 1) → num
Wrap-around: x runs in a circle within lo…hi.
softclip(x: num, lim: num = 5) → num
Soft limiting to ±lim (linear up to 80 %).

Conversion

hz(v: num) → num
Pitch in V/Oct → frequency in Hz (0 V = C4), the same tuning as the VCO.
voct(f: num) → num
Frequency in Hz → V/Oct.
semi(v: num) → num
Round to semitones (1/12 V).
dbamp(db: num) → num
Decibels → gain factor.
ampdb(x: num) → num
Gain factor → decibels.
gate(b: bool) → num
Boolean → 0 or 10 V (for gate outputs).
num(b: bool) → num
Boolean → 0 or 1.
connected(jack: jack) → bool
Is a cable plugged into this jack? Works for inputs and outputs – an unpatched output can feed an internal bus like outputs 1–4 of the Calculus.

Oscillators and randomness

phasor(hz: num, reset: bool = false) → num
Phase 0…1 running hz times per second (clock-stable in f64); reset sets it to 0 (hard sync).
sine(hz: num, reset: bool = false) → num
Sine oscillator ±1.
tri(hz: num, reset: bool = false) → num
Triangle oscillator ±1.
saw(hz: num, reset: bool = false) → num
Sawtooth ±1, band-limited (PolyBLEP); reset = hard sync.
square(hz: num, pw: num = 0.5, reset: bool = false) → num
Square ±1 with pulse width pw (0.02…0.98), band-limited; reset = hard sync.
noise() → num
White noise ±1.
random(trig: bool) → num
Random value 0…1, new on every trig.

Triggers, gates, counters

rise(x: num) → bool
Rising edge: true exactly in the sample in which x rises above 1 V (Schmitt trigger 1 V / 0.1 V).
fall(x: num) → bool
Falling edge: true exactly in the sample in which x drops below 0.1 V.
high(x: num) → bool
Level with hysteresis: on from 1 V, off below 0.1 V.
changed(x: num) → bool
True if x changed since the previous sample.
prev(x: num) → num
Value of x in the previous sample.
pulse(trig: bool, ms: num = 5) → bool
Pulse of ms milliseconds from trig (retriggerable) – for trigger outputs and LEDs.
counter(up: bool, reset: bool = false, n: num = 0) → num
Counts up on every up, reset sets it to 0; with n ≥ 1 it wraps around 0…n−1.
every(trig: bool, n: num) → bool
True on the 1st, (n+1)th, (2n+1)th … trigger – a clock divider.
toggle(trig: bool) → bool
Flips between false and true on every trigger (flip-flop).
latch(set: bool, reset: bool) → bool
Becomes true on set and false on reset (reset wins).
sah(x: num, trig: bool) → num
Sample & hold: takes x on every trigger and holds the value.
timer(trig: bool) → num
Seconds since the last trigger.

Envelopes and smoothing

ad(trig: bool, a: num, d: num) → num
Envelope 0…1: rises in a seconds, falls in d seconds; a new trigger starts from the current level.
adsr(gate: bool, a: num, d: num, s: num, r: num) → num
Envelope 0…1 with a gate (boolean): attack, decay, sustain (0…1), release – times for the full swing.
lag(x: num, s: num) → num
Smooth following with time constant s seconds (one-pole low-pass) – glide, smoothing.
slew(x: num, up: num, down: num = up) → num
Limits the change to up or down volts per second (linear glide).
smooth(x: num) → num
Smoothing over 15 ms against zipper noise.
peak(x: num) → num
Peak value for level meters: |x|/5, rises at once, falls in about 300 ms.

Filters

lp1(x: num, hz: num) → num
First-order low-pass (6 dB/octave).
hp1(x: num, hz: num) → num
First-order high-pass (6 dB/octave).
lp(x: num, hz: num, res: num = 0) → num
Second-order low-pass (state variable, 12 dB/octave); res 0…1 emphasises the cutoff.
bp(x: num, hz: num, res: num = 0) → num
Second-order band-pass.
hp(x: num, hz: num, res: num = 0) → num
Second-order high-pass.
notch(x: num, hz: num, res: num = 0) → num
Second-order notch filter.
dcblock(x: num) → num
Removes DC (high-pass at 20 Hz).

Delay

read(buf: buffer, s: num) → num
Value of the buffer from s seconds ago (interpolated, at least 1 sample, at most the buffer length).
readi(buf: buffer, n: num) → num
Value of the buffer from n whole samples ago.
write(buf: buffer, x: num)
Writes x into the buffer (as a statement of its own, once per sample).

State per call

Functions with memory – oscillators, edges, counters, envelopes, filters – have their own state at every place in the text. Two calls sine(220) are two independent oscillators. The same holds for every call of a custom function and every step of a loop – there is nothing to manage.

If such a call sits in a branch that is not running, it pauses: the phase stands still, the counter does not count. Anything that must keep running belongs into a variable before the if.

Custom functions

With fn name(a, b) { … return value } you bundle calculations that occur more than once. Functions sit in the module block next to the declarations and may call other functions, but not themselves. When compiling, every function is inserted at its call site – a call costs as much as its body, nothing more.

fn allpass(x, a) {               // own state per call site
  state x1 = 0
  state y1 = 0
  y = a * (x - y1) + x1
  x1 = x
  y1 = y
  return y
}

fn split(v, hard: bool = false) { // two results, a default value
  if hard { return sign(v), abs(v) }
  else { return v, abs(v) }
}

process {
  y = allpass(allpass(x, 0.3), 0.6)   // two independent allpasses
  s, _ = split(y)                     // _ discards a value
}

Parameters are numbers; x: bool makes one a boolean, x = 0.5 gives it a default (parameters with a default come last). Parameters may be changed inside the body. return a, b gives several values, which x, y = f(…) takes over; _ discards one. return stands at the end of the function or at the end of every branch of its last if.

state in the body gives every call site a stored value of its own – that is how you build your own building blocks such as an allpass or an integrator that behave like built-in functions. A function reads everything in the module (knobs, jacks, state, constants) but only changes its own values: it returns results, and process assigns them. The example Integral rebuilds the Calculus this way.

Loops and arrays

An array is a row of values of the same kind: state hist[4] = 0 (or with a list), const scale[] = [0, 2, 4, 7, 9] (length from the list) – and in the same way knobs, switches, buttons, jacks, LEDs and level meters. knob pitch[8] # … creates eight knobs with the IDs pitch1 to pitch8; a # in the label becomes the number, and with at x y step dx dy they sit at a fixed spacing.

const N = 8
knob   pitch[N] "#" -2..2 = 0 unit v size s at 8 30 step 9.3 0   // pitch1 … pitch8, labels 1 … 8
button on[N]    ""  = on               at 8 44 step 9.3 0
led    l[N]     green                  at 8 52 step 9.3 0
state  hist[4] = 0
const  scale[] = [0, 2, 4, 7, 9]

process {
  n = counter(rise(clk), false, N)
  cv = pitch[n]                      // index at run time
  for i in 0..N { l[i] = n == i }    // i = 0 … 7, unrolled when compiling
}

Elements are read and written with a[i], counted from 0. The index may come about at run time, for example from a counter – it is rounded down and limited to the valid range (write a wrap-around yourself: a[n % N]). For connected(jack[i]) it must be known when compiling.

for i in 0..N { … } repeats the body for i = 0 to N − 1. The bounds are fixed whole numbers (at most 256 steps); the loop is unrolled when compiling, and i is a constant in every step. Every step is a call site of its own – oscillators or filters inside run independently. There is no break or while: the computation per sample is always fixed.

Panel layout

The panel is laid out automatically: knobs at the top, below them switches and buttons, then LEDs and level meters; inputs and outputs sit at the bottom (outputs last, on a dark field). Every row is centred.

With at x y an element sits exactly there (centre in millimetres from the top-left corner; the panel is 128.5 mm high and HP × 5.08 − 0.3 mm wide). For arrays, at x y step dx dy places the first element and every further one offset by dx/dy. The automatic layout does not avoid fixed positions – so place a section entirely by hand or not at all.

If the controls do not fit, the compiler reports layoutTooFull with a suggestion for the width.

Custom artwork

You can design the panel and the caps yourself with SVG. face draws over the whole panel – below screws, title and controls; the panel size in millimetres makes a good viewBox (for 6 HP e.g. 0 0 30 128.5). skin on a knob or button replaces the drawn cap (the scale and the pointer or the LED stay).

Artwork goes into a multi-line text between """ and """. It is checked: only shapes, groups, colour gradients and text are allowed – no scripts, event attributes, styles, classes, images, filters or external links. References to your own IDs (fill=url(#gradient)) are allowed; IDs are made unique per module.

Allowed elements

g path rect circle ellipse line polyline polygon text tspan defs linearGradient radialGradient stop clipPath

Allowed attributes

x y x1 y1 x2 y2 cx cy r rx ry fx fy width height d points fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-dasharray stroke-dashoffset stroke-miterlimit opacity transform id offset stop-color stop-opacity gradientUnits gradientTransform spreadMethod clip-path clip-rule clipPathUnits font-size font-weight font-style text-anchor dominant-baseline letter-spacing visibility

// Custom artwork: front panel (face) and knob cap (skin) as SVG – a starting point for your own design.
module glow "Glow" hp 6 theme black {
  desc de "Abschwächer mit eigener Frontplatte: Farbverlauf, Ring und eine eigene Knopfkappe – Vorlage für eigene Grafiken."
  desc en "Attenuator with custom panel art: colour gradient, ring and a custom knob cap – a template for your own artwork."

  face """
  <svg viewBox="0 0 30 128.5">
    <defs>
      <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stop-color="#3a2150"/>
        <stop offset="1" stop-color="#10223a"/>
      </linearGradient>
    </defs>
    <rect width="30" height="128.5" fill="url(#bg)"/>
    <circle cx="15" cy="25.8" r="10" fill="none" stroke="#b97a4b" stroke-width="0.4" stroke-dasharray="1 1"/>
  </svg>
  """

  // The automatic layout puts the first large knob at the top centre (y ≈ 25.8 mm) – the ring sits around it.
  knob level "LEVEL" 0..1 = 0.7 size l skin """
  <svg viewBox="-10 -10 20 20">
    <circle r="10" fill="#b97a4b"/>
    <circle r="7.2" fill="#3a2150" stroke="#f0dcc4" stroke-width="0.8"/>
  </svg>
  """

  in    x   "IN"  audio
  out   y   "OUT" audio
  meter lvl "LEVEL"

  process {
    y = x * level
    lvl = peak(y)
  }
}

Load, replace, share

File → Load custom modules … opens a window: choose a file (on a computer you can also drop it in), a short loading spinner, then every module of the file appears with its finished panel and a live preview – LEDs and level meters show how it reacts to test signals; below it says what share of a CPU core the module needs at most. For each module you decide: add it or not.

If the patch already contains a module with the same id, the button reads “Replace”: all instances get the new version and keep their position, knob values (via the knobs’ id) and cables (via the jacks’ id).

Custom modules belong to the patch: they are saved with it and shared with the patch file. In the library they are listed under “Custom modules”; via ⋯ or right-click they can be saved as .amsmod, replaced from a file or removed.

On a phone everything works the same way: choose a file, look at the preview, add. Some file managers append .txt – such files are read as well.

Error messages

When the compiler finds an error, the window shows it right away – always as an English sentence with line and column and the marked excerpt. Syntax errors concern the whole file; errors in one module only that module, the others can still be added.

Every error code has an entry here; in the window a click on the code leads straight to it.

  • fileTooLarge

    The file is larger than max KB.

  • fileTooManyLines

    The file has more than max lines.

  • fileNotText

    The file is not a text file – please save it as UTF-8.

  • fileEmpty

    The file contains no module.

  • fileTooManyModules

    A file may contain at most max modules.

  • lexBadChar

    Unexpected character “char”.

  • lexBadNumber

    “text” is not a valid number.

  • lexUnclosedString

    The text is not closed – the closing quotation mark is missing.

  • lexBadString

    Texts must not contain control characters.

  • syntaxExpected

    Expected expected, found found.

  • syntaxUnexpected

    Unexpected: found.

  • syntaxTopLevel

    Outside a module only “module …” is allowed (found: found).

  • syntaxNoProcess

    The module has no “process { … }” block.

  • syntaxDuplicateProcess

    A module has exactly one “process” block.

  • syntaxUnclosedBlock

    The block is not closed – “}” is missing.

  • syntaxChainedCompare

    Comparisons cannot be chained – join them with “and”, e.g. “a < x and x < b”.

  • syntaxNesting

    Nested too deeply (at most max levels).

  • syntaxStatement

    A statement is expected here (assignment, “if”, “for”, “return” or “write”), found: found.

  • declDuplicate

    “name” is already declared.

  • declDuplicateModule

    The module ID “name” appears more than once in the file.

  • declReserved

    “name” is a reserved word and cannot be a name.

  • declBadId

    The module ID “name” may only contain lowercase letters, digits and _ and must start with a letter.

  • declEmptyName

    The module name must not be empty.

  • declIdTooLong

    Names have at most max characters.

  • declTextTooLong

    The text is too long (at most max characters).

  • declBadRange

    Invalid range: the minimum must be less than the maximum (with “log” both above 0, not combined with “int”).

  • declBadDefault

    The default value is outside the range.

  • declBadOption

    The option “name” is unknown or not allowed here (on arrays “at” needs “step”; “normal” is not possible there).

  • declDuplicateOption

    The positions of a switch must differ.

  • declBadTheme

    Unknown theme “name” (alu, black, cream, blue, wasp).

  • declBadHp

    The width must be a whole number from min to max HP.

  • declBadSignal

    Unknown signal type “name”.

  • declBadColor

    Unknown LED colour “name” (red, green, yellow, blue).

  • declBadUnit

    Unknown unit “name” (hz, s, ms, db, pct, v, st, bpm).

  • declBadSize

    Unknown knob size “name” (s, m, l).

  • declBadNormal

    “name” is not a suitable input.

  • declBadSwitch

    A switch has 2 or 3 positions.

  • declTooMany

    Too many what (at most max).

  • declBufferTooLong

    Buffers are at most max s long in total.

  • declNotConst

    Only a constant expression is allowed here (numbers, constants, pure functions).

  • declDuplicateDesc

    The description is given twice.

  • declDuplicateFace

    The panel artwork (“face”) is given twice.

  • svgSyntax

    The SVG graphic is not well-formed (at “detail”).

  • svgElement

    The SVG element “name” is not allowed (only shapes, groups, gradients and text).

  • svgAttribute

    The SVG attribute “name” is not allowed (no scripts, styles, classes or links).

  • svgValue

    The value of “name” is not allowed (references only to own IDs such as url(#gradient)).

  • svgTooLarge

    The SVG graphic is too large (at most max KB).

  • nameUnknown

    Unknown name “name”.

  • callUnknown

    Unknown function “name”.

  • callArity

    “name” expects min to max arguments, not got.

  • callArgType

    Argument index of “name” must be expected.

  • callNeedsBuffer

    “name” needs a buffer (“buffer”).

  • callNeedsInput

    “name” needs the name of an input or output jack as its argument.

  • callUnusedResult

    The result of “name” is not used – assign it to a variable.

  • callNoValue

    “name” returns no value and can only be used as a statement.

  • assignReadonly

    “name” cannot be assigned – knobs, inputs, constants, buffers, functions and loop variables are read-only.

  • assignTypeChange

    “name” already has a different type (number or boolean).

  • useBeforeAssign

    “name” is read before it is assigned on every path – use “state” for values across samples.

  • typeNeedBool

    A boolean is expected here, e.g. “x > 1” or “high(x)”.

  • typeNeedNum

    A number is expected here – convert booleans with “num(b)” or “gate(b)”.

  • typeBoolToOut

    Output “name” needs a voltage – “gate(b)” turns a boolean into 0/10 V.

  • fnRecursive

    The function “name” calls itself (directly or through other functions) – recursion is not allowed.

  • fnReturnOutside

    “return” is only allowed inside a function.

  • fnReturnNotLast

    “return” must be the last statement of the function or of a branch of its last “if”.

  • fnMissingReturn

    Not every path through “name” ends with “return”.

  • fnReturnMismatch

    All “return” statements of “name” must give the same number of values of the same types.

  • fnResultCount

    The number of results does not match: “name” gives count, so assign exactly that many names (a, b = name(…)).

  • fnWritesOutside

    A function can only change its own values and state – return the value and assign “name” in process.

  • fnStatePlacement

    “state” belongs at module level or at the top level of a function body.

  • fnParamOrder

    Parameters with a default value come last – “name” needs one as well.

  • loopBadRange

    A loop needs whole-number constant bounds a..b with a < b and at most max steps.

  • arrayNotArray

    “name” is not an array.

  • arrayNeedsIndex

    “name” is an array – use an element such as “name[0]”.

  • arrayIndexRange

    Index index is outside “name” (0 to max).

  • arrayIndexConst

    Here the index of “name” must be known when compiling (a number, a constant or a loop variable).

  • arrayBadLength

    The length of “name” must be a whole-number constant from 1 to max.

  • arrayListLength

    The list for “name” has got values, but the array has len.

  • limitCode

    The program is too long (at most max words).

  • limitRegisters

    Too many values at once (at most max registers).

  • limitState

    Too many stateful calls (at most max state slots).

  • limitCost

    The module is too expensive (cost of at most max units per sample).

  • limitExpand

    Functions and loops expand to too much code (more than max statements).

  • layoutTooFull

    The controls do not fit on hp HP (suggestion: suggest HP).

  • layoutOverlap

    “a” and “b” overlap.

  • layoutOutside

    “name” lies outside the usable area.

  • layoutTitle

    The module name does not fit on hp HP.

Notes (do not block)

  • warnUnusedControl

    “name” is never read.

  • warnOutputNeverSet

    Output “name” is never assigned.

  • warnUnusedVar

    “name” is assigned but never read.

  • warnBufferNeverWritten

    Buffer “name” is never written.

  • warnUnusedFunction

    The function “name” is never called.

Limits and performance

So that no module can disturb the sound, each has a fixed upper limit of computation per sample. One unit is roughly one simple calculation; filters and envelopes cost more. The limit is enough for dozens of filters or a whole module like the Calculus including oversampling (example Integral) – the window shown when loading tells you what share of a CPU core a module needs at most. How many such modules run at once depends on the device.

Cost per sample (units, ≈ 2 ns)1024
Knobs, switches and buttons per module32
Inputs per module16
Outputs per module16
LEDs and level meters per module16
Buffers per module · total length4 · 8 s
Program length (words)4096
State slots (functions with memory)256
Elements per array64
Steps per loop · statements after unrolling and inserting256 · 20000
Custom functions · parameters · return values64 · 16 · 8
File size · lines128 KB · 5000
Modules per file16
Custom modules per patch32
Characters for name · label · description20 · 8 · 160
Width in HP4–32
Size of one SVG graphic32 KB

Examples

All examples can be downloaded and loaded directly – they cover the function groups and make good templates.

Fold – wavefolder with level meterMath, switch, LED and level meter: the sine is folded several times depending on FOLD.
Download
// Wavefolder with a level meter – an example custom module for the Analog Modular Simulator.
module fold "Fold" hp 6 theme black {
  desc de "Wavefolder: faltet die Welle je nach FOLD mehrfach um – aus einem Sinus wird ein obertonreicher Klang."
  desc en "Wavefolder: folds the wave over itself depending on FOLD – a sine turns into a rich, buzzy tone."

  knob   amount "FOLD" 0..1 = 0.3
  knob   depth  "CV"   0..1 = 0.5 size s tip "CV amount"
  switch mode   "MODE" "SIN" "TRI" = 0
  in     x      "IN"   audio
  in     cv     "CV"   cv
  out    y      "OUT"  audio
  led    hot    red
  meter  level  "LEVEL"

  process {
    g = 1 + 4 * clamp(amount + cv / 10 * depth, 0, 1)
    s = x / AUDIO * g
    y = (mode == 0 ? sin(s * PI / 2) : fold(s, -1, 1)) * AUDIO
    hot = abs(y) > 4.5
    level = peak(y)
  }
}
Divider and Toggle – two modules in one fileClock divider with reset and a flip-flop: edges, counters, pulses and gates.
Download
// Two clock tools in one file – every `module` block becomes a module of its own.

module divider "Divider" hp 4 {
  desc de "Taktteiler: gibt jeden n-ten Takt als Trigger weiter (÷2 bis ÷8), RST beginnt von vorn."
  desc en "Clock divider: passes on every n-th clock as a trigger (÷2 to ÷8), RST starts over."

  knob div   "DIV" 2..8 = 4 int
  in   clk   "CLK" gate
  in   reset "RST" gate
  out  q     "OUT" gate
  led  act   yellow

  process {
    t = rise(clk)
    n = counter(t, rise(reset), div)
    fire = t and n == 1
    q = gate(pulse(fire, 10))
    act = pulse(fire, 60)
  }
}

module toggle "Toggle" {
  desc de "Flipflop: jeder Takt schaltet den Ausgang um – halbes Tempo mit 50 % Tastverhältnis, INV liefert das Gegenteil."
  desc en "Flip-flop: every clock flips the output – half tempo at 50 % duty cycle, INV gives the opposite."

  in  clk "CLK" gate
  out q   "Q"   gate
  out nq  "INV" gate
  led on  green

  process {
    s = toggle(rise(clk))
    q = gate(s)
    nq = gate(not s)
    on = s
  }
}
Glow – custom panel and knob capTemplate for your own artwork: SVG colour gradient as panel, custom cap on the large knob.
Drift – gliding random voltageRandomness on every clock or at a free rate, smoothed softly.
Download
// Random, smoothly gliding control voltage – brings filters, pitch or effects to life.
module drift "Drift" hp 4 {
  desc de "Zufallsspannung: springt bei jedem Takt (ohne Takt in freier Rate) auf einen neuen Wert und gleitet weich dorthin."
  desc en "Random voltage: jumps to a new value on every clock (without a clock at a free rate) and glides there smoothly."

  knob rate  "RATE"  0.05..10 = 0.5 log unit hz
  knob glide "GLIDE" 0..2 = 0.3 unit s
  knob range "RANGE" 0..10 = 5 unit v
  in   clk   "CLK"   gate
  out  y     "OUT"   cv
  led  tick  yellow

  process {
    ph = phasor(rate)
    wrapped = ph < prev(ph)
    t = connected(clk) ? rise(clk) : wrapped
    y = lag(random(t) * 2 - 1, glide) * range
    tick = pulse(t, 40)
  }
}
Quantizer – semitones and pentatonicRound pitch, hold it with a trigger, a gate on every new note.
Download
// Quantizer: snaps a control voltage to semitones or the major pentatonic scale.
module quant "Quantizer" {
  desc de "Quantisierer: rundet V/Oct auf Halbtöne oder die Dur-Pentatonik; mit Trigger hält er den Ton bis zum nächsten Takt."
  desc en "Quantizer: rounds V/Oct to semitones or the major pentatonic scale; with a trigger it holds the note until the next clock."

  switch scale "SCALE" "CHR" "PEN" = 0
  in     x     "IN"   pitch
  in     clk   "TRIG" gate
  out    y     "OUT"  pitch
  out    g     "GATE" gate

  process {
    s = semi(x)
    // Pentatonic: round the semitone within the octave down to 0, 2, 4, 7 or 9.
    oct = floor(s)
    n = round((s - oct) * 12)
    pen = n >= 9 ? 9 : n >= 7 ? 7 : n >= 4 ? 4 : n >= 2 ? 2 : 0
    q = scale == 0 ? s : oct + pen / 12
    y = connected(clk) ? sah(q, rise(clk)) : q
    g = gate(pulse(changed(y), 5))
  }
}
LPG – low-pass gateOne envelope opens filter and volume at the same time.
Download
// Low-pass gate: one envelope opens filter and volume together – the typical "plonk".
module lpg "LPG" hp 4 {
  desc de "Low-Pass-Gate: Jeder Trigger lässt Lautstärke und Helligkeit gemeinsam ausklingen – wie ein angeschlagenes Instrument."
  desc en "Low-pass gate: each trigger lets volume and brightness decay together – like a struck instrument."

  knob decay "DECAY" 0.02..3 = 0.4 log unit s
  knob tone  "TONE"  0..1 = 0.6
  in   x     "IN"    audio
  in   trig  "TRIG"  gate
  out  y     "OUT"   audio
  led  env   green

  process {
    e = ad(rise(trig), 0.002, decay)
    cutoff = 80 * exp2(e * (2 + tone * 6))
    y = lp(x, cutoff, 0.1) * e
    env = e
  }
}
Chorus – modulated delayBuffer, write and read with a slow sine.
Download
// Chorus: a slowly wavering delay doubles the sound.
module chorus "Chorus" hp 6 {
  desc de "Chorus: mischt das Signal mit einer Kopie, deren Verzögerung ein langsamer Sinus schwanken lässt – breit und schwebend."
  desc en "Chorus: mixes the signal with a copy whose delay is swayed by a slow sine – wide and shimmering."

  knob   rate  "RATE"  0.05..3 = 0.4 log unit hz
  knob   depth "DEPTH" 0..1 = 0.5
  knob   blend "MIX"   0..1 = 0.5
  in     x     "IN"    audio
  out    y     "OUT"   audio
  buffer line 50 ms

  process {
    write(line, x)
    d = 0.012 + 0.008 * depth * sine(rate)
    y = mix(x, read(line, d), blend)
  }
}
Osc – oscillator with hard syncBand-limited saw and square with V/Oct and sync.
Download
// Oscillator with saw, square with pulse width and hard sync – band-limited (PolyBLEP).
module osc "Osc" hp 6 {
  desc de "Oszillator: Sägezahn und Rechteck (Pulsbreite per Regler), V/Oct-Eingang und Hard-Sync – bandbegrenzt, ohne Aliasing."
  desc en "Oscillator: saw and square (pulse width by knob), V/Oct input and hard sync – band-limited, without aliasing."

  knob tune "TUNE"  -2..2 = 0 unit v tip "Coarse tune (octaves)"
  knob pw   "PW"    0.05..0.95 = 0.5 unit pct
  in   pitch "V/OCT" pitch
  in   sync "SYNC"  gate
  out  sw   "SAW"   audio
  out  sq   "SQR"   audio

  process {
    f = hz(pitch + tune)
    r = rise(sync)
    sw = saw(f, r) * AUDIO
    sq = square(f, pw, r) * AUDIO
  }
}
Steps – 8-step sequencerArrays of knobs, buttons and LEDs, an index at run time and a loop.
Download
// Steps: an 8-step sequencer – arrays of knobs, buttons and LEDs, a runtime index and a loop.
module steps "Steps" hp 16 {
  desc de "8-Schritt-Sequencer: Jeder Takt geht einen Schritt weiter, Taster schalten Schritte stumm, LEN kürzt die Folge, RST beginnt von vorn."
  desc en "8-step sequencer: every clock moves one step on, buttons mute steps, LEN shortens the sequence, RST starts over."

  const N = 8
  // Arrays: pitch1 … pitch8, on1 … on8, l1 … l8 – "#" in a label becomes the step number, "at … step" spaces them.
  knob   pitch[N] "#" -2..2 = [0, 0.25, 0.583, 0.25, 1, 0.583, 0.333, 0.75] unit v size s at 8 30 step 9.3 0
  button on[N]    ""  = on at 8 44 step 9.3 0
  led    l[N]     green    at 8 52 step 9.3 0
  knob   len "LEN" 1..N = N int at 40.5 70
  in     clk "CLK"  gate  at 14 100
  in     rst "RST"  gate  at 28 100
  out    cv  "CV"   pitch at 53 100
  out    g   "GATE" gate  at 67 100
  state  n = 0                        // current step (0 = step 1)
  state  armed = false                // the first clock after start or RST plays step 1

  process {
    t = rise(clk)
    if rise(rst) {
      n = 0
      armed = false
    } else if t {
      if armed { n = (n + 1) % len } else { armed = true }
    }
    cv = pitch[n]                     // index at run time
    g = gate(high(clk) and on[n])
    for i in 0..N { l[i] = n == i }   // unrolled: eight assignments
  }
}
Integral – the Calculus as a scriptA complex module: custom functions with their own state, an f64 integrator with oversampling, a loop over the bus and connected() on outputs.
Download
// Integral: a two-channel function generator written as a script – the built-in Calculus rebuilt with custom
// functions, per-call state, loops, arrays and connected() on outputs.
//
// Channel 1 and 4 are the same function "channel" called twice; each call has its own state. The core is a
// voltage-dependent integrator in f64 that carries the remaining time over segment ends (exact cycle length up to
// audio rate) and runs twice per sample with a half-band filter (anti-aliasing). Known differences to the built-in
// module: RISE, FALL and RESP are smoothed like every knob, there is no idle shortcut (the load is always the worst
// case) and the half-band filter computes in f64.
module integral "Integral" hp 20 theme black {
  desc de "Funktionsgenerator als Skript: zwei Kanäle (Hüllkurve, LFO, Slew bis Audiorate), zwei Offsets, SUM/OR/INV – ein Nachbau des Calculus."
  desc en "Function generator as a script: two channels (envelope, LFO, slew up to audio rate), two offsets, SUM/OR/INV – the Calculus rebuilt."

  // Channel 1 (left)
  in     sig1   "SIGNAL" cv   at 8 25
  in     trig1  "TRIG"   gate at 20 25
  button cycle1 "CYCLE"       at 10 37
  led    cyc1   red           at 17 37
  knob   rise1  "RISE" 0.0005..180 = 0.3 log unit s size l at 26 40
  knob   fall1  "FALL" 0.0005..180 = 0.3 log unit s size l at 26 58
  knob   resp1  "RESP" 0..1 = 1 / 3 unit pct tip "Response: log – linear – exp" at 26 75
  in     rcv1   "RISE"  cv   at 8 48.5
  in     bcv1   "BOTH"  cv   at 8 60
  in     fcv1   "FALL"  cv   at 8 71.5
  in     cin1   "CYCLE" gate at 8 83

  // Channel 4 (right, mirrored)
  in     sig4   "SIGNAL" cv   at 93.3 25
  in     trig4  "TRIG"   gate at 81.3 25
  button cycle4 "CYCLE"       at 91.3 37
  led    cyc4   red           at 84.3 37
  knob   rise4  "RISE" 0.0005..180 = 0.3 log unit s size l at 75.3 40
  knob   fall4  "FALL" 0.0005..180 = 0.3 log unit s size l at 75.3 58
  knob   resp4  "RESP" 0..1 = 1 / 3 unit pct tip "Response: log – linear – exp" at 75.3 75
  in     rcv4   "RISE"  cv   at 93.3 48.5
  in     bcv4   "BOTH"  cv   at 93.3 60
  in     fcv4   "FALL"  cv   at 93.3 71.5
  in     cin4   "CYCLE" gate at 93.3 83

  // Channels 2 and 3, attenuverters 1–4 and the bus
  in     in2    "IN 2" cv at 40 25
  in     in3    "IN 3" cv at 61.3 25
  knob   att[4] "#" -1..1 = [1, 0, 0, 1] unit pct at 50.65 36 step 0 14.5
  out    unity1 "UNITY" env  at 9 99.5
  led    u1p    green        at 17.2 95
  led    u1n    red          at 17.2 100
  out    var[4] "#"     env  at 25 99.5 step 14 0
  led    u4p    green        at 84.1 95
  led    u4n    red          at 84.1 100
  out    unity4 "UNITY" env  at 92.3 99.5
  out    eor    "EOR"   gate at 9 113.5
  led    eorl   yellow       at 17.2 111.5
  out    orout  "OR"    cv   at 32 113.5
  out    sum    "SUM"   cv   at 48 113.5
  led    sump   green        at 56 109
  led    sumn   red          at 56 114
  out    inv    "INV"   cv   at 64 113.5
  led    eocl   yellow       at 84.1 111.5
  out    eoc    "EOC"   gate at 92.3 113.5

  const LIN  = 1 / 3              // linear mark of RESP
  const EXPO = 2 / 3              // exponential mark
  const IEXP = 1 / 2.085          // curve constants: log ≈ 2×, exp ≈ 0.5×, hyper-exp ≈ 0.3× the linear time
  const IHYP = 1 / 3.3
  const MAXR = 10 / 50e-6         // fastest segment: 10 V in 50 µs

  // Linear up to ±10 V, then soft up to ±10.5 V (supply rails).
  fn rail(x) {
    ax = abs(x)
    return ax <= 10 ? x : sign(x) * (10 + 0.5 * tanh((ax - 10) * 2))
  }

  // First-order allpass – every call keeps its own state.
  fn allpass(x, a) {
    state x1 = 0
    state y1 = 0
    y = a * (x - y1) + x1
    x1 = x
    y1 = y
    return y
  }

  // Two samples at double rate (mid first) → one sample: polyphase half-band.
  fn halfband(mid, cur) {
    return 0.5 * (allpass(allpass(cur, 0.07986643), 0.54532365) + allpass(allpass(mid, 0.28382935), 0.8344119))
  }

  // Rate factor at voltage v: slower near the top for log, faster for exp.
  fn response(v, curve) {
    a = abs(v) + 1
    if abs(curve - LIN) < 0.0001 { return 1 }
    else if curve <= LIN {
      lg = 3 / a
      return lg + (1 - lg) * (curve * 3)
    } else if curve <= EXPO { return 1 + (a * IEXP - 1) * ((curve - LIN) * 3) }
    else { return a * IEXP + (a * a * IHYP - a * IEXP) * ((curve - EXPO) * 3) }
  }

  // Integrate `left` seconds towards the apex (rising) or the signal; the time left at a segment end carries over.
  fn advance(v, up: bool, apex, sig, left, rup, rdn, curve, cyc: bool) {
    done = false
    for k in 0..3 {
      if not done {
        target = up ? apex : sig
        d = target - v
        if up and d <= 0 { up = false }
        else if d == 0 {
          if cyc {
            up = true
            apex = 8
          } else { done = true }
        } else {
          rising = d > 0
          rate = (rising ? rup : rdn) * response(v, curve)
          need = abs(d) / rate
          if need > left {
            v += (rising ? rate : -rate) * left
            done = true
          } else {
            v = target
            left -= need
            if up { up = false }
            else if not rising and cyc {
              up = true
              apex = 8
            }
            done = left <= 0
          }
        }
      }
    }
    return v, up, apex
  }

  // One function channel: unity output, end-of-rise state and cycling.
  fn channel(sig, trig, trise, tfall, curve, cbtn: bool, cin, rcv, fcv, both) {
    state v = 0
    state up = false
    state apex = 10
    state rising = false
    state cgate = false
    state sprev = 0
    cgate = cgate ? cin > 2 : cin >= 2.5
    cyc = cbtn or cgate
    if rise(trig) and not rising {
      up = true
      apex = cyc ? 8 : 10
    }
    b = exp2(clamp(both, -10, 10))
    rup = min(10 / max(trise, 0.00001) * max(1 - 0.11 * rcv, 0.12) * b, MAXR)
    rdn = min(10 / max(tfall, 0.00001) * max(1 - 0.11 * fcv, 0.12) * b, MAXR)
    c = clamp(curve, 0, 1)
    dt = 0.5 / SR
    mid = 0.5 * (sig + sprev)
    sprev = sig
    v, up, apex = advance(v, up, apex, mid, dt, rup, rdn, c, cyc)
    vmid = v
    v, up, apex = advance(v, up, apex, sig, dt, rup, rdn, c, cyc)
    rising = up or sig > v
    return rail(halfband(vmid, v)), not rising and v > 0.01, cyc
  }

  process {
    u1, hi1, cy1 = channel(sig1, trig1, rise1, fall1, resp1, cycle1, cin1, rcv1, fcv1, bcv1)
    u4, hi4, cy4 = channel(sig4, trig4, rise4, fall4, resp4, cycle4, cin4, rcv4, fcv4, bcv4)
    unity1 = u1
    unity4 = u4
    eor = gate(hi1)
    eoc = gate(not hi4)
    var[0] = rail(u1 * att[0])
    var[1] = rail((connected(in2) ? in2 : 5) * 2 * att[1])
    var[2] = rail((connected(in3) ? in3 : 2.5) * 2 * att[2])
    var[3] = rail(u4 * att[3])
    // Bus: only channels whose variable output is not patched.
    bus = 0
    top = 0
    for k in 0..4 {
      t = connected(var[k]) ? 0 : var[k]
      bus += t
      top = max(top, t)
    }
    s = rail(bus)
    sum = s
    inv = -s
    orout = top
    cyc1 = cy1
    cyc4 = cy4
    eorl = hi1
    eocl = not hi4
    u1p = u1 / 8
    u1n = -u1 / 8
    u4p = u4 / 8
    u4n = -u4 / 8
    sump = s / 10
    sumn = -s / 10
  }
}

AGENTS.md for AI assistants

AGENTS.md describes the language completely and concisely: structure, all declarations, type rules, every function with its signature, limits, error codes, a checklist and all examples. It is generated from the compiler with every update and therefore always current.

Hand it to your AI assistant together with your idea (“a bitcrusher with rate and bits knobs”) – out comes an .amsmod file you can load right away.

Download AGENTS.md