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.mdQuick start
A custom module is a text file with the extension .amsmod. You can write it in any text editor.
- Download the example
fold.amsmodor copy the code below into a new file. - Open the simulator and choose File → Load custom modules … (or “Load custom modules …” at the bottom of the module library).
- 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.amsmodFile 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
Header and texts
After module id "Name" you may add the width hp 4 to hp 32 (without it the layout picks the smallest width that fits), the look theme alu, black, cream, blue or wasp and a subtitle subtitle "TEXT".
desc "Text" describes the module in the library and in the preview. With desc de "…" and desc en "…" there is one text per language; the simulator shows its current language.
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;pctexpects 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. Withnormal otheran 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, binstead 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 – usuallylevel = 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] = 0or with a liststate id[] = [0, 1, 0, 1].buffer id 500 ms– delay line forwriteandread/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
loandhi. 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
hztimes per second (clock-stable in f64);resetsets 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
msmilliseconds fromtrig(retriggerable) – for trigger outputs and LEDs. counter(up: bool, reset: bool = false, n: num = 0) → num- Counts up on every
up,resetsets it to 0; withn≥ 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
setand false onreset(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
aseconds, falls indseconds; 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
sseconds (one-pole low-pass) – glide, smoothing. slew(x: num, up: num, down: num = up) → num- Limits the change to
upordownvolts 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);
res0…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
sseconds ago (interpolated, at least 1 sample, at most the buffer length). readi(buf: buffer, n: num) → num- Value of the buffer from
nwhole 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.
fileTooLargeThe file is larger than max KB.
fileTooManyLinesThe file has more than max lines.
fileNotTextThe file is not a text file – please save it as UTF-8.
fileEmptyThe file contains no module.
fileTooManyModulesA file may contain at most max modules.
lexBadCharUnexpected character “char”.
lexBadNumber“text” is not a valid number.
lexUnclosedStringThe text is not closed – the closing quotation mark is missing.
lexBadStringTexts must not contain control characters.
syntaxExpectedExpected expected, found found.
syntaxUnexpectedUnexpected: found.
syntaxTopLevelOutside a module only “module …” is allowed (found: found).
syntaxNoProcessThe module has no “process { … }” block.
syntaxDuplicateProcessA module has exactly one “process” block.
syntaxUnclosedBlockThe block is not closed – “}” is missing.
syntaxChainedCompareComparisons cannot be chained – join them with “and”, e.g. “a < x and x < b”.
syntaxNestingNested too deeply (at most max levels).
syntaxStatementA statement is expected here (assignment, “if”, “for”, “return” or “write”), found: found.
declDuplicate“name” is already declared.
declDuplicateModuleThe module ID “name” appears more than once in the file.
declReserved“name” is a reserved word and cannot be a name.
declBadIdThe module ID “name” may only contain lowercase letters, digits and _ and must start with a letter.
declEmptyNameThe module name must not be empty.
declIdTooLongNames have at most max characters.
declTextTooLongThe text is too long (at most max characters).
declBadRangeInvalid range: the minimum must be less than the maximum (with “log” both above 0, not combined with “int”).
declBadDefaultThe default value is outside the range.
declBadOptionThe option “name” is unknown or not allowed here (on arrays “at” needs “step”; “normal” is not possible there).
declDuplicateOptionThe positions of a switch must differ.
declBadThemeUnknown theme “name” (alu, black, cream, blue, wasp).
declBadHpThe width must be a whole number from min to max HP.
declBadSignalUnknown signal type “name”.
declBadColorUnknown LED colour “name” (red, green, yellow, blue).
declBadUnitUnknown unit “name” (hz, s, ms, db, pct, v, st, bpm).
declBadSizeUnknown knob size “name” (s, m, l).
declBadNormal“name” is not a suitable input.
declBadSwitchA switch has 2 or 3 positions.
declTooManyToo many what (at most max).
declBufferTooLongBuffers are at most max s long in total.
declNotConstOnly a constant expression is allowed here (numbers, constants, pure functions).
declDuplicateDescThe description is given twice.
declDuplicateFaceThe panel artwork (“face”) is given twice.
svgSyntaxThe SVG graphic is not well-formed (at “detail”).
svgElementThe SVG element “name” is not allowed (only shapes, groups, gradients and text).
svgAttributeThe SVG attribute “name” is not allowed (no scripts, styles, classes or links).
svgValueThe value of “name” is not allowed (references only to own IDs such as url(#gradient)).
svgTooLargeThe SVG graphic is too large (at most max KB).
nameUnknownUnknown name “name”.
callUnknownUnknown function “name”.
callArity“name” expects min to max arguments, not got.
callArgTypeArgument 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.
callUnusedResultThe 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.
typeNeedBoolA boolean is expected here, e.g. “x > 1” or “high(x)”.
typeNeedNumA number is expected here – convert booleans with “num(b)” or “gate(b)”.
typeBoolToOutOutput “name” needs a voltage – “gate(b)” turns a boolean into 0/10 V.
fnRecursiveThe 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”.
fnMissingReturnNot every path through “name” ends with “return”.
fnReturnMismatchAll “return” statements of “name” must give the same number of values of the same types.
fnResultCountThe number of results does not match: “name” gives count, so assign exactly that many names (a, b = name(…)).
fnWritesOutsideA 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.
fnParamOrderParameters with a default value come last – “name” needs one as well.
loopBadRangeA 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]”.
arrayIndexRangeIndex index is outside “name” (0 to max).
arrayIndexConstHere the index of “name” must be known when compiling (a number, a constant or a loop variable).
arrayBadLengthThe length of “name” must be a whole-number constant from 1 to max.
arrayListLengthThe list for “name” has got values, but the array has len.
limitCodeThe program is too long (at most max words).
limitRegistersToo many values at once (at most max registers).
limitStateToo many stateful calls (at most max state slots).
limitCostThe module is too expensive (cost of at most max units per sample).
limitExpandFunctions and loops expand to too much code (more than max statements).
layoutTooFullThe controls do not fit on hp HP (suggestion: suggest HP).
layoutOverlap“a” and “b” overlap.
layoutOutside“name” lies outside the usable area.
layoutTitleThe module name does not fit on hp HP.
Notes (do not block)
warnUnusedControl“name” is never read.
warnOutputNeverSetOutput “name” is never assigned.
warnUnusedVar“name” is assigned but never read.
warnBufferNeverWrittenBuffer “name” is never written.
warnUnusedFunctionThe 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 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 |
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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.