Audulus

Audulus 4 Manual

Welcome! This is where you'll find what you need to operate Audulus, the modular audio app that lets you create custom synthesizers, effects, and so much more.

Audulus circuit

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

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 with receive().

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.

LabelCommon useTypical meaning
AAudioA waveform used for sound or processing, often around -1 to 1.
GGateA 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.
MModulationA 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.
O1V/octAlso 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.

  • 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.

  • 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
ActionShortcut
New / Save⌘N / ⌘S
Cut / Copy / Paste⌘X / ⌘C / ⌘V
Select All⌘A
Undo / Redo⌘Z / ⇧⌘Z
Group selected objects⌘G
Open Module Library browserReturn
Zoom to Fit / Actual Size⌘F / ⌘0
Zoom In / Zoom Out⌘= / ⌘-
Show or hide Inspector⌘;
Exit a module⌘[
Toggle audioSpace
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

All 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

SignalRange
anyAny number and signal type
audio-1 to 1
gate0 or high (any non-zero positive 32-bit number)
hz0 to sampleRate/2
integerInteger values
midi valueIntegers 0 to 127
mod0 to 1
seconds0 to 2^32-1

Node Categories

There are 10 categories of nodes:

CategoryDescription
utilVarious utilities
mathMathematical and logical operations
meterSignal monitoring displays
midiMIDI utilities
modSignal level adjustment and analysis tools
dspDigital signal processing tools
synthEssential synthesis tools
moduleModule and submodule creation
polyPolyphonic signal management
switchSignal 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

ADC node

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

DAC node

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

Text node

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.

Text inspector panel

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:

CharacterUnicodeNameCharacterUnicodeName
±U+00B1Plus-Minus Sign×U+00D7Multiplication Sign
÷U+00F7Division Sign°U+00B0Degree Sign
¢U+00A2Cent Sign£U+00A3Pound Sign
¥U+00A5Yen Sign€U+20ACEuro Sign
©U+00A9Copyright Sign®U+00AERegistered Sign
™U+2122Trade Mark Sign§U+00A7Section Sign
U+00B6Pilcrow Sign·U+00B7Middle Dot
•U+2022Bullet…U+2026Horizontal Ellipsis
¼U+00BCOne Quarter½U+00BDOne Half
¾U+00BEThree Quarters⁄U+2044Fraction Slash
⅛U+215BOne Eighth⅜U+215CThree Eighths
⅝U+215DFive Eighths⅞U+215ESeven Eighths
∂U+2202Partial Differential∅U+2205Empty Set
∏U+220FN-Ary Product∑U+2211N-Ary Summation
−U+2212Minus Sign√U+221ASquare Root
∞U+221EInfinity∫U+222BIntegral
≈U+2248Almost Equal To≠U+2260Not Equal To
≤U+2264Less-Than or Equal≥U+2265Greater-Than or Equal
←U+2190Leftwards Arrow↑U+2191Upwards Arrow
→U+2192Rightwards Arrow↓U+2193Downwards Arrow
↔U+2194Left Right Arrow↕U+2195Up Down Arrow
↖U+2196North West Arrow↗U+2197North East Arrow
↘U+2198South East Arrow↙U+2199South West Arrow
¬U+00ACNot Sign‰U+2030Per Mille Sign
◊U+25CALozenge

Text size or color cannot be changed. The canvas node can do many manipulations with text that the text node cannot.

timer

Timer node

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

Zero cross node

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

Expr node

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.

Expr node inspector panel

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).

Expr node variables

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.

Expr error

Functions

FunctionDescription
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

OperatorDescriptionExample
+Additionx + y
-Subtractionx - y
*Multiplicationx * y
/Divisionx / y

Comparison Operators

OperatorDescriptionExample
>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

OperatorDescriptionExample
? :Conditional (if-then-else)x > y ? a : b returns a if x > y, otherwise b

sum

Sum node

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.

Sum node inspector
Sum node multiple inputs

product

Product node

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.

Product node inspector
Product node multiple inputs

random

Random node

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.

Random node inspector

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.

Meter negative

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

Meter inspector

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

Meter sizes

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

Meter poly

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.

Waveform range

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

Waveform mod vs audio

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.

Value inspector

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

Value precision

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

Value poly

light

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

Light on

rgb light

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

RGB light colors

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.

RGB light brightness

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

RGB light mix

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.

Scope circle

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).

Scope inspector

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.

Scope sizes

canvas

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

Canvas node

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

Keyboard node

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

Note send node

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

cc send

CC send node

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

Trigger node

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

Spline node

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.

Spline draw

The spline node is great for creating custom LFOs.

Spline LFO

mapper

Mapper node

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.

Mapper shapes

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

Mapper LFO
Mapper overshoot

env follow

Env follow node

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

ADSR node

The adsr node outputs an envelope when the gate input is held high.

Inputs:

  • a: Attack time
  • d: Decay time
  • s: Sustain level
  • r: 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.

ADSR cycle

DSP Nodes

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

unit delay

Unit delay node

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

Biquad node

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

Low-pass node

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

High-pass node

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

Delay line node

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

dc blocker

DC blocker node

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

sample rate

Sample rate node

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

resample

Resample node

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.

Resample inspector
Resample example

memory

Memory node

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

Memory inspector

DSP

DSP node

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

Osc node

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 from 0 to sampleRate/2 Hz
  • amp: 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.
Osc Hz and amp
Osc sync
Osc shape

phasor

Phasor node

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.

Phasor waveforms
Phasor sync
Phasor through-zero FM

sample & hold

Sample & hold node

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.

Sample & hold inspector
Sample & hold example

Module Nodes

Module nodes are important elements for creating modules and submodules.

module

Module node

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.

Module inspector

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.

Module move UI

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.

Module size and IO
Module modules and submodules
Module sequencer interior
Module counter interior
Module minisynth

input

Input node

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.

Input inspector

output

Output node

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.

Output inspector

knob

Knob node

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.

Knob inspector
Knob styles
Knob icons

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.

Knob wire

xy pad

XY pad node

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.

XY pad values

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

XY pad inspector

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

XY pad size

slider

Slider node

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.

Slider zeroed

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

Slider inspector
Slider colors

toggle

Toggle node

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.

Toggle inspector
Toggle colors

touch pad

Touch pad node

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.

Touch pad values

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

Touch pad inspector

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

Touch pad size

Poly Nodes

Poly nodes create and manage polyphonic signals.

combine

Combine node

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.

Combine inspector
Combine values

split

Split node

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.

Split inspector
Split values

poly mix

Poly mix node

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

channel index

Channel index node

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

Channel index inspector
Channel index values

channel count

Channel count node

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

Channel count values

Switch Nodes

Switch nodes route signals and control signal flow.

mux

Mux node

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.

Mux inspector

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.

Mux values

demux

Demux node

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.

Demux inspector
Demux values

spigot

Spigot node

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.

Spigot values
Spigot off and on
Spigot interrupt

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:

NodeLanguagePurpose
Lyte DSPLyteAudio and control signal processing; the recommended choice for new DSP work
DSPLuaAudio and control processing in existing Lua-based patches
CanvasLuaVector graphics, including controls, text, and visualizations
ShaderGLSLPixel 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

  1. 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.
  2. Connect an audio signal to input and connect output to a safe listening or monitoring destination.
  3. 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.

NameTypePurpose
input, output and other named ports[f32]Input and output sample buffers for the node.
framesi32Number of samples to process in the current block.
sampleRatef32Samples per second at the node's processing rate.
MAX_FRAMESi32Maximum 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.

  1. Add a Lyte DSP node and a Canvas node to the patch.
  2. Connect the Canvas node's [messages] input to an output of the Lyte DSP node. The output can still carry its normal signal.
  3. Call send(message) in the Lyte process block. In the Canvas Lua script, call receive() 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.

FunctionResult or argumentsPurpose
receive_midi()MidiMessageRead the next short MIDI 1.0 message as bytes.
send_midi(bytes)[u8] → boolSend one MIDI byte message.
receive_ump()UmpMessageRead the next Universal MIDI Packet (UMP).
send_ump(words, frame)[u32], i32 → boolSend 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 require clause. 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 of n samples beginning at start.
  • storage[j] = myValue stores a value in the node's persistent storage array.
  • send(myStringMessage) sends a string to a Canvas node. Connect the Canvas [messages] input to a DSP output and call receive() 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.

Canvas knob alignment
For precise alignment (like centering the knob graphic in this image), use Edit mode with the grid. In this case, below the code window in the inspector we set canvas width/height to 30×30, then in the Lua script we use `canvas_width` and `canvas_height` constants to center the graphic to the canvas window, then snap the canvas graphic in place.

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.

Canvas inspector

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.