Getting started
Getting Started
Introduction
Audulus is a modular synthesizer and visual programming environment for iOS, iPadOS, and macOS. Connect nodes and modules to build instruments, effects, sequencers, and other audio tools. Audulus runs standalone or as an AUv3 plug-in in a host that supports AUv3.
Getting Help
- Email: support@audulus.com
- Discord: Join the Audulus Discord community for real-time help
- Forum: forum.audulus.com
Modular Audio and Visual Programming
Modular audio on a computer has a history of its own, shaped by visual programming and digital signal processing. Audulus also grows from an interest in modular synthesis and Eurorack, and from the creativity and spirit of exploration in those communities.
In the Audulus patchbay, you build by connecting nodes with virtual cables. The connections make signal flow visible: you can follow a sound from its source through processing to an output, or connect a control signal and see how it changes the result.
This way of programming has a hands-on quality. Move a node, make a connection, and hear or see the result immediately. That sense of cause and effect invites experimentation, whether you are exploring an idea or building a complete instrument.
Device Compatibility
Audulus runs on iPhone, iPad, and Mac. Patches can sync across your devices via iCloud. Interface layout and performance vary by device.
Core Concepts
Patches, Nodes, and Modules
A patch is an Audulus document containing connected nodes and modules. A patch can play notes, process audio, generate patterns, or combine these tasks.
A node is a building block with a particular job: an oscillator generates a waveform, a delay node stores a signal, and a math node performs a calculation. Nodes can have inputs, outputs, controls, and settings. All modules are made from nodes. A module groups nodes behind an interface of its own. Think of it as a block of visual code: its internal connections define what it does, while its ports and controls let you use it in a larger patch. A module inside another module is called a submodule.
The Patchbay
The patchbay is the black workspace where you arrange and connect nodes and modules to build a patch. It follows an infinite patchbay model, giving your creations room to grow beyond the visible screen while making their structure and signal flow visible.
Signals, graphics, and messages
Audulus patches work with three kinds of information:
- Audio signals are streams of numbers processed at the audio sample rate, used for sound and for control values such as pitch, gates, and modulation.
- Graphics make a patch visible and interactive and update at the graphics frame rate. Canvas uses Lua for vector graphics; Shader uses GLSL for pixel graphics.
- String messages carry text or encoded data from a DSP node to a Canvas node. Lyte sends a message with
send(); the Canvas Lua script reads it withreceive().
Audulus also receives and sends MIDI. MIDI nodes and Lyte DSP can respond to MIDI events or send them to instruments and hosts. Lyte can process both MIDI 1.0 messages and MIDI 2.0 Universal MIDI Packets. MIDI events and Canvas messages are separate from the numbers carried by signal ports.
Audio signals and port labels
Audulus signal ports carry 32-bit floating-point values. The same kind of signal can represent an audible waveform, pitch, a gate, or a changing control value; its meaning depends on how a patch uses it. These common labels and ranges appear on modules throughout Audulus. They reflect familiar modular synthesis practices and make connections easier to understand, but they are an arbitrary convention—not enforced signal types or required labels.
| Label | Common use | Typical meaning |
|---|---|---|
| A | Audio | A waveform used for sound or processing, often around -1 to 1. |
| G | Gate | A value indicating a state or event, such as a held note. A rising edge can trigger an envelope or advance a sequencer; gate height can also carry velocity. |
| M | Modulation | A value that controls a parameter, such as an envelope controlling a VCA or an LFO moving a filter. Many controls use 0 to 1; modulation can use other ranges and change at audio rate. |
| O | 1V/oct | Also called voct or octave pitch. In Audulus, 0 is A440, 1 is 880 Hz, and -1 is 220 Hz. Adding 1 raises pitch one octave; 1/12 raises it one semitone. A common range is -5 to 5, spanning ten octaves. The scale makes musical intervals equal steps while frequency in hertz doubles with each octave. |
Some Audulus ports accept Hz or freq (frequency in hertz). To convert a 1V/oct (O) signal to hertz, use 2^pitch * 440, where pitch is the octave value.
A wire connects an output to an input. One output can feed several inputs; each input accepts one connection. Use a math node or mixing module to combine signals before an input. Check port descriptions for the expected units and ranges, and start at a low listening level when experimenting.
Graphics and string messages
Canvas draws vector graphics and can respond to signals at its input ports. Shader produces pixel graphics. Both can provide visual feedback in a patch.
Audio processing runs at the audio sample rate, while Canvas draws at the graphics device’s frame rate. The send() / receive() message stream runs at roughly ten times the graphics frame rate and can transfer large batches of string data. Canvas receives and processes those messages into graphics, such as a waveform drawn from encoded audio samples. Audio itself continues through signal wires. See Sending messages to Canvas for the connection and a complete example.
Signal Flow
Patches are often easiest to read from left to right: sources, processing, then outputs. Wires can travel in any direction; signals flow from the connected output to the input.
For example, an oscillator can feed a filter, the filter can feed a VCA, and an envelope can control the VCA's amplitude. A keyboard or sequencer can control the oscillator's pitch and trigger the envelope. The dac node sends the resulting signal to the available audio outputs; use adc to bring in audio when the app or host provides an input.
CPU and Performance
Audio must be processed in time for each block. If a patch takes too long, you may hear clicks, crackles, or dropouts. The CPU display helps you see the processing load as you build. Timing or profiling mode can show the relative processing time of nodes and modules; turn it off when you finish inspecting the patch, since profiling can itself affect performance.
If a patch runs heavily, compare its load before and after a change, reduce unnecessary work or polyphony, or simplify costly parts. The multi-core audio processing setting can help some patches, but its benefit depends on the patch and device. Buffer size also affects the balance between latency and the time available to process audio.
User Interface
Choose nodes from the node menu or modules from the Module Library browser. Arrange and connect them on the patchbay, and select an object to see its settings in the Inspector. Controls can vary by device and host.
iPhone and iPad
Opening and Managing Patches
Open a patch from the document browser, or create a new one with the add button. Use the browser's editing controls to organize, rename, duplicate, or delete documents. The standalone app can request microphone access when you need audio input; a patch that only generates audio does not need microphone input.
Use the navigation control to return to the document browser. Inside a module, it first returns to the containing patch.
Navigating and Editing the Patchbay
- Pan: Drag with two fingers to move around the workspace.
- Zoom: Pinch to zoom in or out. Double-tap the patchbay to fit the patch in view.
- Add an item: Open the patchbay's Create menu to choose a node, or use the Module Library browser to add a module.
- Select and move: Tap a node or module, then drag it. Use the selection controls to work with several objects.
- Connect: Drag from an output port to an input port, then release when the connection is ready. To disconnect, drag the wire away from its input.
The toolbar provides undo and redo, grid snapping, the module UI edit lock, timing/profiling, audio on/off, settings, and the Inspector. Controls may be arranged differently on iPhone and iPad. Selection exposes actions such as Cut, Copy, Delete, and Group.
Inspector and Module Library Browser
Select a node or module and open the Inspector to adjust its available properties. Depending on the selection, these can include port names, ranges, appearance, scripts, or other node-specific settings. A module's own controls can also be used directly on its face.
Search the Module Library by name or tag, then choose a module to add it to the patch. Open a module to see its contents; use the exit control to return to its parent.
Performance
Performance mode locks the screen view and enables multitouch on Touch Pad nodes. Touch Pad multitouch is inactive outside Performance mode.
macOS
Opening and Managing Patches
Use the document browser or File → Open to open a patch, and File → New to start one. Audulus 4 patch documents use the .audulus4 extension. Use File → Save to save your work; the File menu also provides document-management commands such as Duplicate. Multiple documents can be open at once.
Navigating and Editing the Patchbay
- Pan: Hold Option and drag the patchbay.
- Zoom: Hold Control and scroll with the mouse. Double-click the patchbay or use View → Zoom to Fit to see the patch.
- Add an item: Right-click the patchbay to access the node menu. Press Return to open the Module Library browser, where you can search or browse for modules.
- Select and move: Click a node or module and drag it. Shift-click to add to a selection, or drag on the patchbay to select a group.
- Connect: Drag from an output port to an input port. To disconnect, drag the wire away from the input.
Right-click a selection for context-sensitive actions. Grouping selected objects creates a module that can be opened and edited. Select an object and open the Inspector to see its properties and node-specific settings.
Useful Keyboard Shortcuts
| Action | Shortcut |
|---|---|
| New / Save | ⌘N / ⌘S |
| Cut / Copy / Paste | ⌘X / ⌘C / ⌘V |
| Select All | ⌘A |
| Undo / Redo | ⌘Z / ⇧⌘Z |
| Group selected objects | ⌘G |
| Open Module Library browser | Return |
| Zoom to Fit / Actual Size | ⌘F / ⌘0 |
| Zoom In / Zoom Out | ⌘= / ⌘- |
| Show or hide Inspector | ⌘; |
| Exit a module | ⌘[ |
| Toggle audio | Space |
Custom Modules
Add your own module or patch files to the user modules folder to make them available in the Module Library browser.
Modules
Modules are groups of nodes. They can be simple utilities, complete synthesizers, effects, sequencers, or anything else you build in Audulus. Module nodes include inputs and outputs, knobs, sliders, and other controls you can use to create an interface.
Creating your own modules
To make a module, select the nodes you want to group and choose Group. You can also start with a module node and build inside it. Add input and output nodes to connect the module to the rest of your patch, and use controls such as knobs and sliders to make it playable. Expose controls and displays on the module's face, arrange them in UI editing mode, and use the Inspector to name and describe your creation.
Within a patch, you can copy and paste a module to reuse its construction. Use submodules to organize a large module into smaller pieces. See the module node for more on its settings and interface.
The Module Library
The Module Library contains built-in modules and your own saved modules. Browse or search for instruments, effects, sequencers, MIDI tools, mixers, and more.
Save a module to your User Library as an .audulus4 patch file to find and load it in other patches. You can also share your creations with the Audulus community.
Double-click or double-tap a non-Pro library module to explore and modify its contents.
The Pro Module series consists of in-app purchases built using Lyte and precompiled for speed. Pro Modules are designed to be used through their controls and connections; their internal construction cannot be edited.
Library categories include Building, Collection, Effect, Input-Output, Meter, MIDI, Mixing, Modulation, Sequencer, Utility, VCA, VCF, VCO, and Visual.
Nodes
Introduction to Nodes

Everything in Audulus is built with nodes. There are 53 nodes in total.
Nodes are packets of code that perform specific functions. A node may have inputs, outputs, both, or neither.
Inputs are on the left side of nodes and are colored blue. Outputs are on the right side of nodes and are colored red.
Nodes can be moved around the patchbay but cannot be rotated.
Connecting Nodes
You connect nodes by dragging a wire from an output to an input. You cannot drag a wire from an input to an output. One output can connect to as many inputs as needed.
You disconnect nodes by unhooking a wire from an input. Wires cannot be disconnected from outputs. An input accepts only one wire. To connect multiple wires to one input, use an add node to combine the signals first.
Wires can go from any output to any input in any direction: up, down, left, or right. However, the overall signal flow in Audulus moves from left to right.
Signals
Nodes send and receive signals through wires. Every signal is a number. Although all signals are numbers, they fall into several categories defined by their range, unit, and usage.
Signal Types
| Signal | Range |
|---|---|
any | Any number and signal type |
audio | -1 to 1 |
gate | 0 or high (any non-zero positive 32-bit number) |
hz | 0 to sampleRate/2 |
integer | Integer values |
midi value | Integers 0 to 127 |
mod | 0 to 1 |
seconds | 0 to 2^32-1 |
Node Categories
There are 10 categories of nodes:
| Category | Description |
|---|---|
util | Various utilities |
math | Mathematical and logical operations |
meter | Signal monitoring displays |
midi | MIDI utilities |
mod | Signal level adjustment and analysis tools |
dsp | Digital signal processing tools |
synth | Essential synthesis tools |
module | Module and submodule creation |
poly | Polyphonic signal management |
switch | Signal routing tools |
Some nodes count as outputs, such as value, light, and dac nodes. If a node is not connected to an output, it will not be evaluated.
Util Nodes
Util nodes provide essential utilities for getting audio in and out of Audulus, labeling patches, timing events, and analyzing signals.
adc

The adc node brings audio and CV into Audulus from external sources like microphones, DAWs, or hardware modular synth CV inputs.
The adc node outputs an audio signal with incoming signals clamped between -1 and 1. While often used for audio input, it can also pass gate signals from a modular synth.
The adc node can access the first 16 inputs of any class-compliant audio interface.
dac

The dac node sends audio and CV out of Audulus to external outputs like headphones, speakers, DAWs, or hardware modular synth CV outputs.
The dac node accepts an audio signal input with outgoing signals clamped between -1 and 1. While often used for audio output, it can also pass gate signals to a modular synth.
The dac node can access the first 16 outputs of any class-compliant audio interface.
Best practice: Use a dc blocker node before a dac node unless you explicitly need to send a mod or gate signal to an external instrument.
text

The text node has no inputs or outputs. It's used for labeling and commenting within patches.
Click or tap the node and open the inspector panel to type in the text field. Text will appear inside the node.

The text node text area box can be resized by tapping or clicking the blue ball that appears when selecting the node, then dragging left or right.
Available Characters
The text node supports the following characters from the Anodina-Regular font.
Standard Characters
These can be typed directly:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9
! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ \ { | } ~
Space: (spacebar)
Extended Math & Special Characters
To use these characters, copy and paste them directly, or use the Unicode code in apps that support Unicode input:
| Character | Unicode | Name | Character | Unicode | Name |
|---|---|---|---|---|---|
| ± | U+00B1 | Plus-Minus Sign | × | U+00D7 | Multiplication Sign |
| ÷ | U+00F7 | Division Sign | ° | U+00B0 | Degree Sign |
| ¢ | U+00A2 | Cent Sign | £ | U+00A3 | Pound Sign |
| ¥ | U+00A5 | Yen Sign | € | U+20AC | Euro Sign |
| © | U+00A9 | Copyright Sign | ® | U+00AE | Registered Sign |
| ™ | U+2122 | Trade Mark Sign | § | U+00A7 | Section Sign |
| ¶ | U+00B6 | Pilcrow Sign | · | U+00B7 | Middle Dot |
| • | U+2022 | Bullet | … | U+2026 | Horizontal Ellipsis |
| ¼ | U+00BC | One Quarter | ½ | U+00BD | One Half |
| ¾ | U+00BE | Three Quarters | ⁄ | U+2044 | Fraction Slash |
| ⅛ | U+215B | One Eighth | ⅜ | U+215C | Three Eighths |
| ⅝ | U+215D | Five Eighths | ⅞ | U+215E | Seven Eighths |
| ∂ | U+2202 | Partial Differential | ∅ | U+2205 | Empty Set |
| ∏ | U+220F | N-Ary Product | ∑ | U+2211 | N-Ary Summation |
| − | U+2212 | Minus Sign | √ | U+221A | Square Root |
| ∞ | U+221E | Infinity | ∫ | U+222B | Integral |
| ≈ | U+2248 | Almost Equal To | ≠ | U+2260 | Not Equal To |
| ≤ | U+2264 | Less-Than or Equal | ≥ | U+2265 | Greater-Than or Equal |
| ← | U+2190 | Leftwards Arrow | ↑ | U+2191 | Upwards Arrow |
| → | U+2192 | Rightwards Arrow | ↓ | U+2193 | Downwards Arrow |
| ↔ | U+2194 | Left Right Arrow | ↕ | U+2195 | Up Down Arrow |
| ↖ | U+2196 | North West Arrow | ↗ | U+2197 | North East Arrow |
| ↘ | U+2198 | South East Arrow | ↙ | U+2199 | South West Arrow |
| ¬ | U+00AC | Not Sign | ‰ | U+2030 | Per Mille Sign |
| ◊ | U+25CA | Lozenge |
Text size or color cannot be changed. The canvas node can do many manipulations with text that the text node cannot.
timer

The timer node outputs elapsed time in seconds since its last reset. The node resets on the rising edge of an incoming gate signal.
The timer output increases smoothly from second to second with functional precision of 1/sampleRate seconds.
zero cross

The zero cross node counts the time between two zero-crossings and outputs that value as a hz signal. To be counted as a zero-crossing, the signal must either pass through 0 or equal 0 at some point during its cycle.
Math Nodes
Math nodes are among the most powerful and versatile nodes in Audulus. They perform mathematical operations, logic, and basic programming functions.
expr

The expr node is the most versatile node, with 40 operators and functions that can be combined in many ways. It performs math and basic programming operations.
Double-clicking on any input creates an expr node.
Enter equations into the inspector panel without an = sign. You can drag and drop functions into the text field or type them directly.

Entering letters or words creates variables as inputs. Variables are case-sensitive (x is not the same as X). Spaces and underscores are not allowed. For long variables, use camelCase (e.g., thisIsALongVariable).

Errors
White Cables
If a math expression miscalculates and the output is harmful to the audio signal, the cables will turn white, and your audio will likely stop processing, requiring you to restart Audulus (and rarely, but possibly, causing you to have to restart your device to get audio back).
If an expression results in NaN (Not a Number), the node displays the 'nan' error message and the cables turn white. If an expression results in inf (infinitely large number), the node displays the 'inf' error message and the cables turn white.
Error Warnings
If the expr node is empty, or if the syntax is incorrect, Inspector will report a parse error underneath the expression text box, and further down a warning, Selected nodes contain errors. This error will also report in the module containing the expr node with the error.

Functions
| Function | Description |
|---|---|
sin(x) | Sine of x (in radians) |
cos(x) | Cosine of x (in radians) |
tan(x) | Tangent of x (in radians) |
asin(x) | Arcsine of x (returns radians) |
acos(x) | Arccosine of x (returns radians) |
atan(x) | Arctangent of x (returns radians) |
atan2(y, x) | Two-argument arctangent (returns angle from positive x-axis to point (x,y)) |
sinh(x) | Hyperbolic sine of x |
cosh(x) | Hyperbolic cosine of x |
tanh(x) | Hyperbolic tangent of x |
asinh(x) | Inverse hyperbolic sine of x |
acosh(x) | Inverse hyperbolic cosine of x |
atanh(x) | Inverse hyperbolic tangent of x |
pow(x, y) | x raised to the power of y (x^y) |
exp(x) | e raised to the power of x (e^x) |
exp2(x) | 2 raised to the power of x (2^x) |
sqrt(x) | Square root of x |
abs(x) | Absolute value of x |
floor(x) | Largest integer less than or equal to x (rounds down) |
ceil(x) | Smallest integer greater than or equal to x (rounds up) |
fract(x) | Fractional part of x (x - floor(x)) |
mod(x, y) | Modulo operation (remainder of x divided by y) |
min(x, y) | Returns the smaller of x and y |
max(x, y) | Returns the larger of x and y |
clamp(x, a, b) | Clamps x between a and b (returns a if x < a, b if x > b, otherwise x) |
step(x, edge) | Returns 1 if x >= edge, otherwise 0 |
mix(x, a, b) | Linear interpolation between a and b based on x (returns a when x=0, b when x=1) |
smoothstep(a, b, x) | Smooth interpolation between 0 and 1 when x is between a and b |
isinf(x) | Returns 1 if x is infinite, otherwise 0 |
isnan(x) | Returns 1 if x is NaN (Not a Number), otherwise 0 |
Arithmetic Operators
| Operator | Description | Example |
|---|---|---|
+ | Addition | x + y |
- | Subtraction | x - y |
* | Multiplication | x * y |
/ | Division | x / y |
Comparison Operators
| Operator | Description | Example |
|---|---|---|
> | Greater than (returns 1 if true, 0 if false) | x > y |
< | Less than (returns 1 if true, 0 if false) | x < y |
>= | Greater than or equal (returns 1 if true, 0 if false) | x >= y |
<= | Less than or equal (returns 1 if true, 0 if false) | x <= y |
= or == | Equality (returns 1 if equal, 0 if not equal) | x == y |
Ternary Conditional Operator
| Operator | Description | Example |
|---|---|---|
? : | Conditional (if-then-else) | x > y ? a : b returns a if x > y, otherwise b |
sum

The sum node adds two or more signals together.
In the inspector panel, specify how many channels to add (maximum: 256). The sum node can be expanded to have multiple inputs.


product

The product node multiplies two or more signals together.
In the inspector panel, specify how many channels to multiply (maximum: 256). The product node can be expanded to have multiple inputs.


random

The random node outputs a new random number between 0 and 1 for every sample, producing white noise.
The seed value can be changed in the inspector panel. Changing the seed is necessary when you have two or more random nodes in a patch and want them to produce different random number sequences.

Meter Nodes
Meter nodes display information about signals, providing visual feedback and debugging capabilities.
meter
The meter node displays signal amplitude. The input works in a range of -1 to 1, though 0 to 1 displays the same as 0 to -1.

In the inspector panel, resize the meter by altering the height and width parameters. It can also be exposed.

If both W and H values are 0, the node defaults to standard size. The node can be resized but not rotated.

When you wire a poly signal to a meter node, it automatically creates one meter per channel.

waveform
The waveform node displays signals changing over time. It has a fixed 5-second window and moves from left to right.
Since it works with the audio range, the top limit is 1, the center is 0, and the bottom is -1.

The waveform node works best with low-frequency signals. For audio-rate signals, use a scope node.

value
The value node displays the current value of any signal.
In the inspector panel, set precision from 0 to 0.0000 and choose alignment: Left, Right, or Center. The value node can also be exposed.

Changing precision is useful for UI displays. For example, a sequencer's current step display should be a whole number, not a decimal.

When you wire a poly signal to a value node, it automatically creates one value display per channel.

light
The light node turns on when the incoming signal is high (any non-zero positive number). Otherwise, it stays off.

rgb light
The rgb light node has three inputs (0 to 1): red r, green g, and blue b.

Each input uses a mod signal. When the signal is 0, the light is off. When the signal is 1, the light is fully on.

Colors can be mixed by sending different values to each input.

scope
The scope node visualizes high-frequency waveforms, phase relationships, and Lissajous curves that cannot be seen with the waveform node.
A cursor draws a dot with color {r, g, b} at coordinates (x, y), where both x and y range from -1 to 1. The point (0, 0) is the center of the display.

The s variable measures line persistence, where 0 shows only the current cursor position and 1 makes the line persist infinitely. Values between 0 and 1 cause the line to fade gradually.
In the scope node's inspector panel, change both the Size and resolution (Image Size).

The default size is W = 200 and H = 200 with an image size of W = 512 and H = 512. For a crisp, clean line, the Image Size should be at least double the Size parameter.

canvas
The canvas node draws vector graphics with Lua using Audulus's VGER graphics library.

See Programming Canvas for the editor, drawing functions, and messaging.
shader
The shader node draws pixel graphics with a GLSL fragment shader. See Programming Shaders for an overview.
MIDI Nodes
MIDI nodes send and receive MIDI signals in and out of Audulus, connecting to external controllers, hardware, and DAWs.
keyboard

The keyboard node receives input from MIDI keyboards, DAWs, and other MIDI input devices.
It outputs a Hz value for the note and a gate signal for note on/off. The gate height represents velocity.
note send

The note send node sends MIDI note, gate, and velocity data from Audulus to external instruments.
cc send

The cc send node sends MIDI CC values from Audulus to external instruments.
When the trigger input goes high, the signal at the value input is sent to the specified CC number.
trigger

The trigger node outputs a gate when its button is pressed. The button can be clicked, tapped, or assigned to an external hardware MIDI signal in MIDI mapping mode.
Mod Nodes
Mod nodes manipulate signal levels, create custom response curves, and extract amplitude envelopes.
spline

The spline node allows you to draw a line using two or more breakpoints. xand y ranges in the graph are 0 to 1. For example, if you connect a knob (also 0 to 1) to the spline input port, turning the knob will advance through the spline graph and output the current y value.
Add breakpoints by double-clicking or tapping on the blank field inside the node. Once created, they can be dragged anywhere within the field.

The spline node is great for creating custom LFOs.

mapper

The mapper node changes the response of an incoming modulation signal based on the curve drawn in the node's field.
The coordinate system determining the output is (in, line). The bottom left of the field is (0, 0) and the top right is (1, 1). The output of the node is the y value of the line given an x input.

The mapper node is excellent for creating custom LFOs and overshoot effects.


env follow

The env follow node extracts an envelope from an incoming audio signal and transforms it into a modulation signal. It does this by taking the absolute value of the incoming signal and applying a small amount of filtering to smooth transitions between samples.
adsr

The adsr node outputs an envelope when the gate input is held high.
Inputs:
a: Attack timed: Decay times: Sustain levelr: Release time
As long as the gate input is high, the envelope output increases from 0 to gate height over a seconds, decreases to the s level over d seconds, then once the gate goes low, decreases from the s level back to 0 over r seconds.

DSP Nodes
DSP (Digital Signal Processing) nodes are essential for creating filters and manipulating signals at a low conceptual level.
unit delay

The unit delay node delays an incoming signal by one sample. It also explicitly marks where a feedback delay occurs within a feedback loop.
If a unit delay is not inserted within a feedback loop, Audulus will guess where to place it. This is usually acceptable, but in some cases like analog-modeling filters, you need to be explicit about feedback delay placement.
biquad

The biquad node allows you to create all types of biquadratic filters.
Calculate the coefficient inputs separately from the node using expr nodes.
low-pass

The low pass node is a simple non-resonant 6dB/oct low-pass filter. The alpha input is the smoothing factor, where 1 = no filtering and 0 = maximum filtering.
high-pass

The high pass node is a simple non-resonant 6dB/oct high-pass filter. The alpha input is the smoothing factor, where 1 = maximum filtering and 0 = no filtering.
delay line

The delay line node delays an incoming signal by a set number of seconds, determined by the value at the time input.
dc blocker

The dc blocker node removes any DC offset from an audio signal.
sample rate

The sample rate node outputs the current sample rate in Hz.
resample

The resample node lets you run a portion of a patch at a higher sample rate (supersampling).
In the inspector panel, choose from different multiples of the base sample rate from 1× to 16×. Higher sample rates use more CPU time.
Note: the resample node should always be accompanied by a 'closing' resample node to convert back to 1x sample rate at the end of the section you wish to resample.


memory

The memory node can read, write, and store audio and control signals. Information is stored as a series of samples.

DSP

The DSP node processes audio with Lua. For new audio programming, use Lyte DSP; the Lua node remains useful for existing patches. See Lua DSP for an overview.
Lyte DSP
The Lyte DSP node processes audio and control signals using Lyte, a language designed for audio DSP in Audulus. See Programming Lyte DSP to get started.
Synth Nodes
Synth nodes are low-level building blocks specific to audio synthesis.
osc

The osc node outputs one of four anti-aliased elemental waveforms as audio: sine, square, triangle, and saw. Select the waveform on the node's panel.
The osc node is intended as an audio-rate oscillator and is band-limited using the miniBLEP method. Using it as an LFO can lead to unpredictable behavior with overshooting caused by the band-limiting.
Inputs:
Hz: Controls oscillator speed from0tosampleRate/2Hzamp: Controls the amplitude of the oscillator waveform.sync: Resets the phase of the waveform on the rising edge of an incoming gate or audio signal (hard sync).shp: Changes waveform shape — for square waves, controls pulse width from 50% to 100%; for saw waves, transforms into a supersaw shape. Modulate this port in the 0 to 1 range.



phasor

The phasor node outputs a cycling ramp from 0 to 2π with 64-bit internal precision.
A phasor ramp is a tradition timebase for creating cycling waveforms. For example, sin(phasor) outputs a sinewave at the freq rate.
The sync input resets the phasor on the rising edge of a gate or audio signal.
Positive Hz values at the freq input run the phasor from 0 to 2π, while negative Hz values reverse the direction, running from 2π to 0. This bi-directional behavior allows for things like through-zero FM.



sample & hold

The sample & hold node samples a value at its input on the rising edge of a gate signal at the trigger input and holds that value at its output until triggered again.
Think of it as a single-sample memory node -- select Save Data in the inspector panel to allow the held value to persist across patch saves.


Module Nodes
Module nodes are important elements for creating modules and submodules.
module

The module node lets you encase nodes inside of a UI.
In the inspector panel, you have options to name the module, define its size, whether or not it is exposed, when it is exposed if it should hide its inputs and outputs, and a field to enter a description of what it does.

Exposed elements within a module node will appear automatically on the UI. You can move them around by clicking or tapping the lock icon to enter Edit Modules mode.

There are two classes of things you can create with the module nodes: modules and submodules.
The difference between them is that modules are meant to be interacted with while submodules are like custom nodes used to create modules.





input

The input node brings signals into a module node. It must be placed inside the module node.
It can be renamed in the inspector panel.

output

The output node sends signals out of a module node. It must be placed inside the module node.
It can be renamed in the inspector panel.

knob

The knob node modifies other nodes and outputs a 0 to 1 mod signal. This signal can be scaled and shaped by expr, mapper, and spline nodes.
In the inspector panel, directly set the Value, change the Style, Icon, color, and MIDI CC and Channel.


Wires can be attached directly to knobs. When attached, a blue circle appears at the center. The input signal is clamped to the 0 to 1 mod signal range.

xy pad

The xy pad node outputs two mod signals that correspond to the (x, y) coordinates of where you click or touch. This point is marked by a colored dot.

In the inspector panel you can change the Size of the node and its color.

You can make the node smaller, larger, or rectangular.

slider

The slider node is used to modify other nodes. It outputs a 0 to 1 mod signal. This signal can be scaled and shaped by expr, mapper, and spline nodes.
When the slider node is zeroed (turned all the way down) a square appears in the center of the small circle.

In the inspector panel you can change the color of the slider to blue, green, or red, and assign MIDI CC and Channel.


toggle

The toggle node outputs a 0 or 1 gate signal. When the switch is set to the left, the output is 0, and when it's set to the right, it is 1.
In the inspector panel you can set the color of the switch as blue, green, or red.


touch pad

The touch pad node outputs two mod signals that correspond to the (x, y) coordinates of where you click or touch. This point is marked by a blue circle.

In the inspector panel you can change the Size of the node and the number of Channels (for iOS multitouch).

You can make the node smaller, larger, or rectangular.

Poly Nodes
Poly nodes create and manage polyphonic signals.
combine

The combine node takes 2 to 256 mono signals and combines them into one polyphonic signal.
In the inspector panel, set the number of channels.


split

The split node takes a poly signal and splits it into 1 to 256 mono signals.
In the inspector panel, set the number of channels.


poly mix

The poly mix node takes a poly signal and mixes it down to a mono signal.
channel index

The channel index node creates a signal with 1 to 256 channels, where each channel is an integer index number indexed from 0.


channel count

The channel count node outputs the number of channels in a signal as an integer.

Switch Nodes
Switch nodes route signals and control signal flow.
mux

The mux node routes one of 2 to 256 inputs to a single output according to the sel value, which is indexed from 0.
In the inspector panel, set the number of inputs.

When the sel value is ≥ 0 but < 1, the 0 input is sent to the output. When sel is ≥ 1 but < 2, the 1 input is sent to the output, and so on.

demux

The demux node routes one signal to 2 to 256 outputs according to the sel value, which is indexed from 0.
In the inspector panel, set the number of outputs.


spigot

The spigot node turns off processing for all nodes to its left in the signal path that aren't connected to an output node like a meter or dac.
When the active input is 0, nothing passes and processing is halted. When the active input is high, the signal passes and everything processes normally.



Programming in Audulus
Audulus lets you write code inside nodes when you need a custom audio process or visual display. Each programming node has a different job:
| Node | Language | Purpose |
|---|---|---|
| Lyte DSP | Lyte | Audio and control signal processing; the recommended choice for new DSP work |
| DSP | Lua | Audio and control processing in existing Lua-based patches |
| Canvas | Lua | Vector graphics, including controls, text, and visualizations |
| Shader | GLSL | Pixel graphics and visual effects |
A node's Inspector provides its code editor and settings. Programs can use node inputs to respond to signals from the rest of the patch; DSP nodes produce signals, while Canvas and Shader nodes produce graphics.
Programming Lyte DSP
Lyte is a statically typed language designed for audio DSP in Audulus. Use the Lyte DSP node to build oscillators, filters, effects, sequencers, utilities, and other audio or control processes. It is the recommended choice for new DSP programs and is substantially faster than the older Lua DSP node.
Lua's dynamic typing and runtime work add overhead to audio processing, and allocations and garbage collection can make execution less predictable. Lyte has static types, no garbage collector or heap allocation, and compile-time checks for array and slice access. Its compiler can produce native code where supported; the execution backend depends on the platform.
This section teaches Lyte in the context of an Audulus node. For a wider language tour, more examples, and compiler details, see Further Lyte resources.
Your first Lyte DSP node
- Add a Lyte DSP node to a patch and open its Inspector. The editor starts with a working pass-through program and comments about messaging and MIDI.
- Connect an audio signal to
inputand connectoutputto a safe listening or monitoring destination. - The program below is the essential part of the template. It copies the incoming signal to the output unchanged:
init {}
process {
for i in 0 .. frames {
output[i] = input[i]
}
}
init runs when the node initializes. Include it even when empty. process runs once for each block of samples. frames is the number of samples in the current block; for i in 0 .. frames visits indices from 0 through frames - 1. The upper end of the range is excluded. input[i] reads one sample and output[i] writes one sample.
If you are coming from Lua DSP, note that Lyte buffer indices start at 0; Lua DSP indices start at 1.
Ports and globals
Use the Inspector to add and name input and output ports. Audulus makes each port available to the program as a slice of f32 samples with the same name. The default ports are input and output. Ports, block information, and storage are supplied by Audulus; do not declare them yourself.
| Name | Type | Purpose |
|---|---|---|
input, output and other named ports | [f32] | Input and output sample buffers for the node. |
frames | i32 | Number of samples to process in the current block. |
sampleRate | f32 | Samples per second at the node's processing rate. |
MAX_FRAMES | i32 | Maximum block size, useful for declaring temporary arrays. |
storage | [f32] | Persistent host storage supplied by Audulus. |
Read input samples only for the current block and write each output sample the program is responsible for. A processing block can contain fewer than MAX_FRAMES samples, so loop to frames, not MAX_FRAMES. For large DSP buffers such as delay lines, use a dedicated global array sized for the task; storage is host-provided persistent node storage.
A gain control
Add an input port named gain in the Inspector. Connect an audio signal to input and a control signal to gain:
init {}
process {
for i in 0 .. frames {
output[i] = input[i] * gain[i]
}
}
A gain of 1.0 passes the input unchanged, 0.5 halves it, and 0.0 silences it. Reading gain[i] lets the control change on every sample. Audulus does not automatically limit the output to the range -1.0 to 1.0; large signals can overload later stages.
Values, types, and control flow
Use let for a binding that does not change and var for a variable you assign again. Lyte often infers a type from the initial value, or you can write it explicitly:
let scale = 0.5
var level: f32
level = scale * 2.0
f32 is the usual type for audio samples and DSP calculations; i32 is a signed integer, bool is true or false, and u8 and u32 are unsigned types useful for MIDI. Integer and floating-point values are distinct types. Use as for an explicit conversion, such as note as i32 or 60 as u8.
Use if and else for decisions, for for a range of indices, and while when repeating until a condition changes. Conditions must be Boolean; compare a numeric signal with a value, for example gate[i] > 0.0. Comments begin with //. The language guide covers functions, structs, arrays, slices, and the rest of the language in more depth.
State and sample rate
A local variable inside process is created for that call. Declare a global var outside process when a value must carry over to the next block. This is how an oscillator remembers its phase or a filter remembers a previous sample. Use init for initialization that depends on sampleRate.
This oscillator uses input as a frequency in hertz. Keep the frequency between 0 and half the sample rate for this simple example; connect a modest output level before listening.
var phase: f32
var inv_sr: f32
init {
inv_sr = 1.0 / sampleRate
}
process {
for i in 0 .. frames {
let hz = input[i]
output[i] = sin(phase * 6.28318530)
phase = phase + hz * inv_sr
if phase >= 1.0 {
phase = phase - 1.0
}
}
}
phase represents a position in a cycle from 0 to just under 1. The program advances it by frequency divided by sample rate for each sample and wraps it at the end of the cycle. The global preserves the phase across blocks. Computing 1.0 / sampleRate in init avoids repeating that division for each sample.
Audio rate and control rate
Audio-rate work can respond to every sample: read input[i], update the state, and write output[i] inside the sample loop. Use this for audio, audio-rate modulation, or events whose sample position matters.
Some values can be updated less often. For example, a slow display or a parameter that is intended to be constant across a block may use one value for the block, then fill or calculate the output. This can save CPU, but sampling a changing input only at the beginning of a block loses intermediate changes and may add stepping or timing error. Choose the rate according to the signal's required accuracy, and measure CPU use in the patch. Check that the block is nonempty before reading a fixed index such as input[0].
Arrays, slices, and bounds
A fixed-size array owns its elements, for example var buffer: [f32; 1024]. A slice such as [f32] is a view into existing elements; Audulus ports and storage are slices. Lyte checks that array and slice accesses are safe. The usual for i in 0 .. frames port loop uses the bounds supplied by Audulus.
If the compiler cannot prove an index is valid, add a guard that establishes its range, or restructure the loop. For helper functions that write to a slice, require can express the bounds the caller must prove:
set_value(values: [f32], index: i32, value: f32)
require index >= 0 && index < values.len {
values[index] = value
}
A require clause is a precondition checked at the call site, not a substitute for deciding what to do with an invalid index. Array size, slice length, and the current frames count are different things; use the appropriate limit for the data being accessed.
Sending messages to Canvas
Lyte DSP and Canvas can exchange data through Audulus's message connection. The Lyte DSP node sends string messages; the Canvas node receives them in its Lua script. This is useful for passing display state, text, or arrays of values without creating a separate signal port for each value.
- Add a Lyte DSP node and a Canvas node to the patch.
- Connect the Canvas node's
[messages]input to an output of the Lyte DSP node. The output can still carry its normal signal. - Call
send(message)in the Lyteprocessblock. In the Canvas Lua script, callreceive()to read the messages.
For a simple complete example, use this Lyte DSP program. It passes audio through and sends a message roughly once per second:
var elapsed: f32
init {}
process {
for i in 0 .. frames {
output[i] = input[i]
}
elapsed = elapsed + (frames as f32)
if elapsed >= sampleRate {
send("hello from Lyte")
elapsed = elapsed - sampleRate
}
}
In the connected Canvas node, use this Lua script to retain and draw the most recently received message:
last_message = last_message or "Waiting for a message"
local messages = receive()
if messages then
for i = 1, #messages do
last_message = messages[i]
end
end
text(last_message, theme.text)
send() accepts a string or a character buffer and returns true when the message is queued. receive() returns a Lua table of strings when messages are available, or nil when there are none. The table uses Lua's 1-based indices. Canvas scripts run repeatedly, so retain any display state you need between draws; the example initializes last_message only if it does not already exist.
Each call to send() supplies one message; Audulus does not prescribe the contents. The two scripts can agree on their own format, such as comma-separated numbers, and the Canvas script can parse each received string. Messages update the display asynchronously and are not audio samples or sample-accurate events. Send only when the display needs an update, and check the return value if a failed queue operation matters to your design.
See Programming Canvas for drawing functions and Canvas settings.
MIDI input and output
Audulus provides MIDI functions directly to Lyte DSP nodes. Call them from process; no imports or declarations are needed. MIDI messages are events, with frame offsets identifying their positions in the current audio block.
| Function | Result or arguments | Purpose |
|---|---|---|
receive_midi() | MidiMessage | Read the next short MIDI 1.0 message as bytes. |
send_midi(bytes) | [u8] → bool | Send one MIDI byte message. |
receive_ump() | UmpMessage | Read the next Universal MIDI Packet (UMP). |
send_ump(words, frame) | [u32], i32 → bool | Send one UMP at a frame in the current block. |
Reading MIDI 1.0. A MidiMessage contains count: i32, frame: i32, and bytes: [u8; 3]. count is the number of valid bytes, or 0 when no messages remain. Check the count before reading data bytes: not all messages have three bytes. This complete program drains the input and remembers the most recently received note-on note on channel 0:
var last_note: i32
init {}
process {
for i in 0 .. frames {
output[i] = input[i]
}
var msg = receive_midi()
while msg.count > 0 {
if msg.count == 3 {
if msg.bytes[0] as i32 == 144 && msg.bytes[2] as i32 > 0 {
last_note = msg.bytes[1] as i32
}
}
msg = receive_midi()
}
}
A status byte of 144 (0x90) is note-on on channel 0; middle C is note 60. MIDI channels occupy the low four status bits, so channel 1 has status 145. A note-on with velocity zero is conventionally a note-off. The example stores a note number; it does not generate sound or forward the event. To make an audio process react at the event's precise position, use its frame while processing that block.
receive_midi() translates MIDI 2.0 input to MIDI 1.0 and does not deliver SysEx. Use UMP when the original MIDI 2.0 packet or SysEx7 data is needed.
Sending MIDI 1.0. The following sends a note-on on channel 0 for note 60 at velocity 100:
send_midi([144 as u8, 60 as u8, 100 as u8])
Call it when an event occurs, rather than unconditionally in every block. A note generator also needs a note-off, such as [128 as u8, 60 as u8, 0 as u8]. send_midi returns whether the message was handed to the output; it has no frame argument.
MIDI 2.0 and UMP. A UmpMessage contains count: i32, frame: i32, and words: [u32; 4]. count is the number of valid words, or 0 when the input is exhausted. The UMP reader preserves MIDI 2.0 packet data; legacy MIDI input is wrapped as MIDI 1.0 UMP. The MIDI 1.0 and UMP readers advance independently, so forwarding both can duplicate events.
This complete example passes incoming UMP messages through at their original frame offsets:
init {}
process {
for i in 0 .. frames {
output[i] = input[i]
}
var msg = receive_ump()
while msg.count > 0 {
send_ump(msg.words, msg.frame)
msg = receive_ump()
}
}
send_ump reads the packet length from its type and ignores trailing words, so the whole four-word array can be passed directly. The frame is relative to the current block and is clamped into it. A false return can indicate an unavailable output, an incomplete or unsupported packet, or a packet that cannot be translated for a MIDI 1.0 destination.
To construct a MIDI 2.0 note-on, place this fragment in process under an event trigger:
var w0 = (4u << 28) | (9u << 20) | (0u << 16) | (60u << 8)
var w1 = 32768u << 16
send_ump([w0, w1], 0)
This creates a MIDI 2.0 channel-voice note-on in group 0, channel 0, for note 60 at frame 0. 32768 is approximately half of the 16-bit velocity range. The u suffix makes the words u32; shifts and masks place and extract UMP fields.
Troubleshooting and performance
- No output: check the connections, port names,
init {}, and that every output sample is written. The default pass-through is a useful starting point. - Type error: check whether a value is
f32,i32, or unsigned. Use an explicit cast when a conversion is intended. - Bounds error: check the range of the index and add a guard or a suitable
requireclause. Don't assume the checker knows a fact that is only implicit in the program's design. - Clicks or unexpected timing: check where state changes occur and whether an input or MIDI event needs sample-accurate handling. Reducing an audio-rate control to one value per block changes its behavior.
- High CPU use: first make the program work correctly, then identify work that can safely run less often or be calculated once in
init. Measure the result in the patch. Avoid frequent logging and Canvas messages in audio loops.
Further Lyte resources
- Lyte DSP Quick Start — practical DSP examples, including oscillators, filters, delay, and sequencing.
- Learning Lyte — a broader introduction to Lyte syntax, language features, and common errors.
- Lyte repository — the primary source for the language, compiler, tools, and development documentation, including editor and language-server setup.
Repository documentation follows the development version and can differ from the Lyte version in your installed Audulus build. Features such as MIDI depend on the version of Audulus you are using.
Lua DSP
The original DSP node uses Lua to process audio and control signals. It remains available for existing patches, but Lyte DSP has effectively superseded it for new work: Lyte is faster, designed for audio processing, and the focus of ongoing DSP development and documentation.
A Lua DSP script defines a process(frames) function. Audulus calls it for each block and supplies inputs and outputs as global arrays. The default program copies the input to the output:
function process(frames)
for i = 1, frames do
output[i] = input[i]
end
end
frames is the number of samples in the block. Lua DSP buffers use 1-based indexing, so this loop processes samples 1 through frames. Add and name ports in the Inspector to use other input and output arrays in the script.
The node also provides these helpers and facilities:
fill(output, value)fills an output buffer with a constant, useful for block-rate control signals.fill_range(output, value, start, n)fills a range ofnsamples beginning atstart.storage[j] = myValuestores a value in the node's persistentstoragearray.send(myStringMessage)sends a string to a Canvas node. Connect the Canvas[messages]input to a DSP output and callreceive()in the Canvas script to read the latest messages.
Existing Lua DSP scripts continue to use Lua syntax and indexing. Lyte is a separate language, so moving a script to a Lyte DSP node requires translating its code.
Programming Canvas
The Canvas node uses Lua and Audulus's VGER library to draw vector graphics: shapes, lines, text, gradients, and custom controls. These are the same graphics tools Audulus uses for elements of the patchbay.
[messages] port: When you load a canvas node, you'll see a glowing gradient circle and a [messages] port. This port receives string messages from DSP or Lyte DSP nodes: the audio script calls send(), and the Canvas script reads them with receive(). This is useful for drawing large chunks of data faster than would otherwise be possible, such as audio waveforms or data that would require many ports.
Create your own input ports in the inputs field at the top of the Inspector Panel. Enter each variable name separated by a space.
Code Editor: Use the code editor to write your Lua script and draw vector graphics in the canvas area.

Available Functions: Below the code editor are all available custom Lua functions for canvas node. Use these as reference, or drag and drop them directly into your code from the inspector panel—you'll see code editing input fields for available parameters that you can tab through.
paint = color_paint { r, g, b, a }
paint = linear_gradient( {start_x, start_y}, {end_x, end_y}, {r, g, b, a}, {r, g, b, a}, glow)
paint = radial_gradient( {center_x, center_y}, inner_radius, outer_radius, {r, g, b, a}, {r, g, b, a}, glow)
fill_circle( {x, y}, radius, paint)
stroke_circle( {x, y}, radius, width, paint)
stroke_arc( {x, y}, radius, width, rotation, aperture, paint)
stroke_segment( {ax, ay}, {bx, by}, width, paint)
fill_rect( {min_x, min_y}, {max_x, max_y}, corner_radius, paint)
stroke_rect( {min_x, min_y}, {max_x, max_y}, corner_radius, width, paint)
stroke_bezier( {ax, ay}, {bx, by}, {cx, cy}, width, paint) -- stroke a quadratic bezier
text("hello world!", {r,g,b,a})
min, max = text_bounds("hello world!")
text_box("lorem ipsum...", break_row_width, {r,g,b,a})
min, max = text_box_bounds("lorem ipsum...", break_row_width)
move_to {x, y} -- moves the pen for path fills
line_to {x, y} -- adds a line to the path
quad_to({bx, by}, {cx, cy}) -- adds a quadratic bezier segment to the path
fill(paint) -- fills the current path
translate {tx, ty} -- translates coordinate system
scale {sx, sy} -- scales coordinate system
rotate(theta) -- rotates coordinate system
save() -- saves transform state
restore() -- restores transform state
canvas_width -- width of drawing area
canvas_height -- height of drawing area
theme.text -- text color
theme.grooves -- black background color
theme.azureHighlight -- azure highlight color
theme.azureHighlightDark -- dark azure highlight color
theme.azureHighlightBackground -- azure background color
theme.greenHighlight -- green highlight color
theme.greenHighlightDark -- dark green highlight color
theme.greenHighlightBackground -- green background color
theme.redHighlight -- red highlight color
theme.redHighlightDark -- dark red highlight color
theme.redHighlightBackground -- red background color
Options: Below the functions are options to change the canvas node Size and enable Save Data to save globals across patch loads.

Programming Shaders
The Shader node uses GLSL, the OpenGL Shading Language, to draw pixel graphics. A fragment shader calculates the color of each pixel, making it useful for procedural images and effects. Edit the shader in the node's Inspector and use inputs to make the image respond to the patch.
The default shader uses three input values for red, green, and blue, then writes the resulting color to outColor:
layout(location = 0) out vec4 outColor;
layout(binding = 0) uniform Inputs {
float r;
float g;
float b;
} inputs;
uniform sampler2D previousFrame;
void main()
{
outColor = vec4(inputs.r, inputs.g, inputs.b, 1.0);
}
The alpha value 1.0 makes the result fully opaque. The template also declares previousFrame for shaders that use the preceding image.
