# AGENTS.md – writing custom modules for the Analog Modular Simulator

This file is for AI coding assistants. It describes the complete script language for **custom modules** of the
Analog Modular Simulator (a virtual Eurorack in the browser, https://ams.eliasritter.de/). Follow it exactly and the
module file compiles and runs without changes. Human-readable guide: https://ams.eliasritter.de/en/docs/

## What you produce

- One plain-text file with the extension `.amsmod` (UTF-8). It contains **one or more** `module` blocks.
- The user loads it in the simulator via **File → Load custom modules …** (or the “Custom modules” section of the
  library). Every module is shown as a live preview and can be added to the patch.
- `process { … }` runs **once per audio sample** (48 kHz typical) inside a sandboxed virtual machine. Custom functions,
  arrays and loops with a fixed count are available (functions are inserted and loops unrolled when compiling); there
  is no recursion, no `while`/`break`, no strings at run time and no I/O. Execution time per sample is bounded (cost
  limit below) – a module may still be as complex as a dual function generator (see the example `integral`).

## File structure

```
// comments start with // or #
module <id> "<Name>" [hp <4–32>] [theme alu|black|cream|blue|wasp] [subtitle "<TEXT>"] {
  desc "<one text for all languages>"          // optional; or per language:
  desc de "<German text>"
  desc en "<English text>"
  face """<svg …>…</svg>"""                    // optional panel artwork (see Graphics)

  <declarations, one per line>

  fn <name>(<params>) {                         // optional custom functions (see below)
    <statements>
    return <expr>[, <expr> …]
  }

  process {
    <statements, one per line or separated by ;>
  }
}
```

- `<id>`: lowercase identifier `[a-z][a-z0-9_]*` – the stable identity of the module. Loading a file with the same id
  again **replaces** the module in the patch (instances keep position, knob values by id and cables by jack id).
- `"<Name>"`: shown on the panel (upper-cased) and in the library; max 20 characters and must fit the panel width.
- `hp`: panel width in HP (1 HP = 5.08 mm). Omit it to let the layout choose the smallest width that fits.
- Statements and declarations end at the end of the line (or `;`). Inside parentheses and square brackets a line
  break is allowed.
- Single-line blocks are fine: `process { q = gate(toggle(rise(c))) }`.

## Declarations

| Declaration | Meaning |
|---|---|
| `knob <id> "<LABEL>" <min>..<max> [= <default>] [log] [int] [unit hz\|s\|ms\|db\|pct\|v\|st\|bpm] [size s\|m\|l] [tip "<text>"] [at <x> <y>] [skin """<svg>"""]` | Rotary knob. Value in its own unit; smoothed (≈ 15 ms) unless `int`. `log` needs min > 0 and excludes `int`. Default = 0 if inside the range, else min. `pct` expects 0..1. |
| `switch <id> "<TITLE>" "<A>" "<B>" ["<C>"] [= <index>] [tip "…"] [at x y]` | 2 or 3 positions; value 0, 1 (, 2). Position texts ≤ 5 chars. |
| `button <id> "<LABEL>" [= on\|off] [tip "…"] [at x y] [skin """<svg>"""]` | Latching button with LED; value is a **boolean**. |
| `in <id> "<LABEL>" [audio\|pitch\|gate\|cv] [normal <otherInput>] [tip "…"] [at x y]` | Input jack, value in volts, 0 when unplugged. The signal hint only selects the preview test signal. `normal b`: when unplugged it reads input `b`. |
| `out <id> "<LABEL>" [audio\|pitch\|gate\|env\|mod\|cv \| via <in>, <in>] [tip "…"] [at x y]` | Output jack in volts; **keeps its value** until assigned again. Default signal type `cv`. |
| `led <id> [red\|green\|yellow\|blue] [at x y]` | LED, brightness 0..1 or a boolean; it glows on for ≈ 150 ms. |
| `meter <id> "<LABEL>" [at x y]` | Level meter (6 LEDs), value 0..1 – usually `peak(x)`. |
| `state <id> = <constant>` | Value that persists across samples (number or `true`/`false`). |
| `buffer <id> <length> s\|ms` | Delay line for `read`/`write`; at most 4 buffers, 8 s in total. |
| `const <id> = <constant expression>` | Compile-time constant (numbers, constants, pure functions; not `SR`). |

Labels are printed on the panel: max 8 characters (Eurorack style, usually upper case). `tip` texts (≤ 60) appear as
tooltips. Knobs, switches and buttons: at most 32 together; inputs ≤ 16, outputs ≤ 16, LEDs + meters ≤ 16.

**Arrays:** every declaration except `buffer` takes a length after the id: `knob pitch[8] "#" -2..2 = 0`,
`button on[8] "" = on`, `in g[4] "G#" gate`, `out var[4] "#" env`, `led l[8] green`, `state hist[4] = 0`,
`const scale[] = [0, 2, 4, 7, 9]` (`[]` = length of the list, `state` and `const` only).

- Length: a whole-number constant from 1 to the array limit below. Controls, jacks, LEDs and meters become single
  elements with the ids `<id>1 … <id>N` (these ids are also usable directly, e.g. in `normal`/`via`); they count
  towards the per-module limits.
- Labels: `#` is replaced by the element number (`"#"` → 1 … 8, `"G#"` → G1 … G4); without `#` every element gets
  the same label (`""` = none).
- Defaults: one value for all elements or a list with exactly N values (`knob`, `state`, `const`).
- Placement: `at x y step dx dy` puts element 1 at (x, y) and every further one offset by (dx, dy). `at` without
  `step` and `normal` are not allowed on arrays.

## Statements in `process`

- Assignment: `<name> = <expr>`, also `+=`, `-=`, `*=`, `/=`; array elements `<name>[<index>] = <expr>`. Targets:
  outputs, LEDs, meters, `state` variables and local variables. Knobs, switches, buttons, inputs, constants, buffers
  and loop variables are read-only.
- Several results of a custom function: `a, b = f(…)` (exactly as many names as results; `_` discards one).
- A **local variable** is created by its first assignment and gets that type. It must be assigned on **every path**
  before it is read. Locals do not survive to the next sample – use `state` for that.
- `if <bool> { … } else if <bool> { … } else { … }` – `else` may start on the next line.
- `for <i> in <a>..<b> { … }` – see Loops below.
- `write(<buffer>, <number>)` – the only call allowed as a statement.

## Custom functions

```
fn allpass(x, a) {                // 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) {
  if hard { return sign(v), abs(v) }
  else { return v, abs(v) }
}
```

- Declared inside the `module` block (anywhere next to the declarations), called like built-ins:
  `y = allpass(allpass(x, 0.3), 0.6)` gives two independent allpasses.
- Parameters: `a` (number), `a: bool`, `a = <constant>` (type from the default). Parameters with a default come last.
  Parameters may be reassigned inside the body.
- `return e` or `return e1, e2, …` only as the **last statement** of the body or of each branch of a final
  `if … else …`; every path must end with `return`, and all returns give the same number and types of values.
- `state` inside a function (top level of its body only) persists per call site – every call, also every loop step,
  has its own copy. Built-in functions with memory inside a function work the same way.
- A function reads everything of the module (knobs, jacks, outputs, `state`, constants, arrays, buffers via
  `read`) but **writes only its own** locals, parameters and `state` – return values and assign them in `process`.
  No `write()` inside functions.
- Functions may call other functions; recursion (also indirect) is an error. Functions are inserted at every call
  site when compiling: a call costs as much as its body.

## Loops and arrays

- `for i in a..b { … }` runs the body for i = a … b−1 (end excluded). `a` and `b` are whole-number constants
  (literals, `const`, constant expressions), at most 256 steps. The loop is unrolled when compiling: `i` is a
  constant in every step, so `arr[i]`, `if i == 0` and `connected(jack[i])` are resolved at compile time and only
  the taken branch of such an `if` is compiled. Every step is its own call site. No `break`/`while` – use a flag.
- `arr[i]` reads an element (index from 0). A run-time index (e.g. from `counter`) is rounded down and limited to
  0 … N−1; write `arr[n % N]` for wrap-around. A constant index outside the array is an error.
- `connected(<jack>)` takes an input or output jack (also `jack[k]` with a constant k). For outputs it tells whether
  a cable is plugged – use it for normalled busses: `t = connected(out1) ? 0 : out1`.

## Types and evaluation

- Two types: **number** and **boolean**. They never mix implicitly.
  - Conditions (`if`, `?:`), `and`/`or`/`not` and trigger arguments need booleans: write `x > 1` or `high(x)`.
  - Arithmetic needs numbers: convert with `num(b)` (0/1) or `gate(b)` (0/10 V).
  - Outputs need numbers (`y = gate(b)`, not `y = b`). LEDs accept both.
- `if`/`?:` evaluate only the chosen branch. `and`/`or` always evaluate **both** sides (no short-circuit), so
  `rise(a) and rise(b)` never misses an edge.
- **State per call site:** every place in the text that calls a stateful function (oscillators, triggers, envelopes,
  filters …) has its own state. Calling `phasor(2)` twice gives two independent oscillators. A call in a branch that
  is not executed pauses (its phase stands still).
- Safe math: `a/0 = 0`, `a%0 = 0`, `sqrt(x<0) = 0`, `log(x≤0) = log(1e-30)`, `%` is floored. Outputs are cleaned every
  sample (NaN/Inf → 0, limited to ±100 V).
- Cables delay by one sample (like every module), feedback loops are allowed.

## Operators (highest precedence first)

1. `( )` and calls
2. `^` (power, right-associative; `-x^2 = -(x^2)`)
3. unary `-` `+`
4. `*` `/` `%`
5. `+` `-`
6. `<` `<=` `>` `>=` `==` `!=` (not chainable: write `a < x and x < b`)
7. `not` `!`
8. `and` `&&`
9. `or` `||`
10. `c ? a : b` (right-associative)

## Voltages and constants

- Eurorack levels: audio ±5 V, envelopes 0…10 V, gates/triggers 0/10 V (a gate counts as high from 1 V, low below
  0.1 V), pitch 1 V per octave with 0 V = C4 (261.63 Hz).
- Built-in generators return **normalised** values (±1 or 0…1); scale at the jacks: `y = saw(f) * AUDIO`.
- Times in seconds (`pulse` in milliseconds), frequencies in Hz, pitch in V/Oct (`hz(v)`, `voct(f)`).
- Constants: `PI`, `TAU`, `E`, `C4` (261.6256), `AUDIO` (5), `GATE` (10), `ENV` (10), `SR` (sample rate at run time),
  `true`, `false`.

## Functions

### 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).

## Panel layout

- Automatic: knobs at the top, then switches and buttons, then LEDs and meters; inputs and outputs at the bottom
  (outputs last, on a dark box). Each row is centred; the free space lies between the top and bottom block.
- `at <x> <y>` places a control by hand (centre in mm, panel origin top-left, height 128.5 mm, width = HP × 5.08 −
  0.3 mm); arrays need `at <x> <y> step <dx> <dy>`. The automatic layout does **not** avoid hand-placed controls –
  place all of a section by hand or none. Hand-placed elements must stay inside the panel (below the title, y ≥
  ≈ 17.5 mm including the label above jacks) and must not overlap including their labels.
- If the controls do not fit, the error `layoutTooFull` suggests a wider HP.

## Graphics (panel artwork and caps)

- `face """<svg viewBox="0 0 W 128.5">…</svg>"""` draws artwork over the whole panel, below screws, title and
  controls. Use the panel size in mm as viewBox (W = HP × 5.08 − 0.3, e.g. 30 for 6 HP).
- `skin """<svg viewBox="…">…</svg>"""` on a `knob` or `button` replaces the drawn cap (knob: the scale and the
  pointer stay; button: the LED in the middle still shows the state). The graphic is fitted into the cap.
- `"""` starts a multi-line text; it may contain quotes and line breaks.
- 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`.
- No scripts, event attributes, `style`, `class`, `href`, images, filters or external `url(...)`. References only to
  your own IDs: `fill="url(#gradient)"`. IDs are made unique per module automatically. Max 32 KB per graphic.

## Reserved names

These cannot be used as identifiers: `module`, `desc`, `knob`, `switch`, `button`, `in`, `out`, `led`, `meter`, `state`, `buffer`, `const`, `process`, `fn`, `for`, `return`, `if`, `else`, `and`, `or`, `not`, `true`, `false`, `abs`, `sign`, `floor`, `ceil`, `round`, `frac`, `sqrt`, `exp`, `log`, `log2`, `exp2`, `sin`, `cos`, `tan`, `tanh`, `atan`, `atan2`, `min`, `max`, `clamp`, `mix`, `mod`, `pow`, `fold`, `wrap`, `softclip`, `hz`, `voct`, `semi`, `dbamp`, `ampdb`, `gate`, `num`, `connected`, `phasor`, `sine`, `tri`, `saw`, `square`, `noise`, `random`, `rise`, `fall`, `high`, `changed`, `prev`, `pulse`, `counter`, `every`, `toggle`, `latch`, `sah`, `timer`, `ad`, `adsr`, `lag`, `slew`, `smooth`, `peak`, `lp1`, `hp1`, `lp`, `bp`, `hp`, `notch`, `dcblock`, `read`, `readi`, `write`, `PI`, `TAU`, `E`, `C4`, `AUDIO`, `GATE`, `ENV`, `SR`.

## Limits

- Cost per sample (units, ≈ 2 ns): 1024
- Knobs, switches and buttons per module: 32
- Inputs per module: 16
- Outputs per module: 16
- LEDs and level meters per module: 16
- Buffers per module · total length: 4 · 8 s
- Program length (words): 4096
- State slots (functions with memory): 256
- Elements per array: 64
- Steps per loop · statements after unrolling and inserting: 256 · 20000
- Custom functions · parameters · return values: 64 · 16 · 8
- File size · lines: 128 KB · 5000
- Modules per file: 16
- Custom modules per patch: 32
- Characters for name · label · description: 20 · 8 · 160
- Width in HP: 4–32
- Size of one SVG graphic: 32 KB

## Error codes

Compiler messages are always English and give line and column. Codes:

- `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.
- `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.

## Checklist before you output a module file

1. Every module has `module <lowercase_id> "<Name>" { … }` with exactly one `process { … }`.
2. All identifiers are declared or assigned before use; no reserved names (e.g. do not name an input `voct`, `hz`,
   `gate`, `mix` or `saw` – those are functions).
3. Types: booleans only in conditions/trigger arguments; outputs get numbers (`gate(b)` for gates).
4. Locals read later are assigned on every path (`if` without `else` does not count) – otherwise use `state`.
5. Generators are normalised: multiply by `AUDIO` (±5 V) or `ENV`/`GATE` (10 V) at the outputs.
6. Labels ≤ 8 characters, name ≤ 20, descriptions ≤ 160, at most 3 switch positions.
7. Keep the work per sample small (filters cost more than arithmetic; a function costs its whole body at every call,
   a loop its body times the number of steps); the cost limit is listed above.
8. Rhythmic things should follow a clock input (`rise(clk)`), not a free-running oscillator.
9. Functions: `return` last (in every branch of a final `if`), no recursion, no writes to outputs or module `state`
   inside a function – return the values instead. Do not name variables like built-in functions (`peak`, `mix` …).
10. Arrays: index from 0, loop bounds `0..N` run N times; `at` on arrays needs `step`.

## Examples

### fold.amsmod

```
// 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)
  }
}
```

### clocktools.amsmod

```
// 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
  }
}
```

### artwork.amsmod

```
// 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)
  }
}
```

### drift.amsmod

```
// 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.amsmod

```
// 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.amsmod

```
// 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.amsmod

```
// 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.amsmod

```
// 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.amsmod

```
// 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.amsmod

```
// 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
  }
}
```
