13 KiB
BeamDSP Language Reference
BeamDSP is a domain-specific language for writing audio processing scripts in Lightningbeam. Scripts are compiled to bytecode and run on the real-time audio thread with guaranteed bounded execution time and constant memory usage.
Quick Start
name "Simple Gain"
category effect
inputs {
audio_in: audio
}
outputs {
audio_out: audio
}
params {
gain: 1.0 [0.0, 2.0] ""
}
process {
for i in 0..buffer_size {
audio_out[i * 2] = audio_in[i * 2] * gain;
audio_out[i * 2 + 1] = audio_in[i * 2 + 1] * gain;
}
}
Save this as a .bdsp file or create it directly in the Script Editor pane.
Script Structure
A BeamDSP script is composed of header blocks followed by a process block. All blocks are optional except name, category, and process.
name "Display Name"
category effect|generator|utility
inputs { ... }
outputs { ... }
params { ... }
state { ... }
ui { ... }
process { ... }
name
name "My Effect"
Sets the display name shown in the node graph.
category
category effect
One of:
effect— Processes audio (has inputs and outputs)generator— Produces audio or CV (outputs only, no audio inputs)utility— Signal routing, mixing, or other utility functions
inputs
Declares input ports. Each input has a name and signal type.
inputs {
audio_in: audio
mod_signal: cv
}
Signal types:
audio— Stereo interleaved audio (2 samples per frame: left, right)cv— Mono control voltage (1 sample per frame, NaN when unconnected)
outputs
Declares output ports. Same syntax as inputs.
outputs {
audio_out: audio
env_out: cv
}
params
Declares user-adjustable parameters. Each parameter has a default value, range, and unit string.
params {
frequency: 440.0 [20.0, 20000.0] "Hz"
gain: 1.0 [0.0, 2.0] ""
mix: 0.5 [0.0, 1.0] ""
}
Format: name: default [min, max] "unit"
Parameters appear as sliders in the node's UI. They are read-only inside the process block.
state
Declares persistent variables that survive across process calls. State is zero-initialized and can be reset.
state {
phase: f32
counter: int
active: bool
buffer: [44100]f32
indices: [16]int
clip: sample
}
Types:
| Type | Description |
|---|---|
f32 |
32-bit float |
int |
32-bit signed integer |
bool |
Boolean |
[N]f32 |
Fixed-size float array (N is a constant) |
[N]int |
Fixed-size integer array (N is a constant) |
sample |
Loadable audio sample (stereo interleaved, read-only in process) |
State arrays are allocated once at compile time and never resized. The sample type holds audio data loaded through the node's UI.
ui
Declares the layout of controls rendered below the node in the graph editor. If omitted, a default UI is generated with sliders for all parameters and pickers for all samples.
ui {
sample clip
param frequency
param gain
group "Mix" {
param mix
}
}
Elements:
| Element | Description |
|---|---|
param name |
Slider for the named parameter |
sample name |
Audio clip picker for the named sample state variable |
group "label" { ... } |
Collapsible section containing child elements |
process
The process block runs once per audio callback, processing all frames in the current buffer.
process {
for i in 0..buffer_size {
audio_out[i * 2] = audio_in[i * 2];
audio_out[i * 2 + 1] = audio_in[i * 2 + 1];
}
}
Types
BeamDSP has three scalar types:
| Type | Description | Literal examples |
|---|---|---|
f32 |
32-bit float | 1.0, 0.5, 3.14 |
int |
32-bit signed integer | 0, 42, 256 |
bool |
Boolean | true, false |
Type conversions use cast syntax:
int(expr)— Convert float to integer (truncates toward zero)float(expr)— Convert integer to float
Arithmetic between int and f32 promotes the result to f32.
Variables
Local variables
let x = 1.0;
let mut counter = 0;
Use let to declare a local variable. Add mut to allow reassignment. Local variables exist only within the current block scope.
Built-in variables
| Variable | Type | Description |
|---|---|---|
sample_rate |
int |
Audio sample rate in Hz (e.g., 44100) |
buffer_size |
int |
Number of frames in the current buffer |
Inputs and outputs
Input and output ports are accessed as arrays:
// Audio is stereo interleaved: [L0, R0, L1, R1, ...]
let left = audio_in[i * 2];
let right = audio_in[i * 2 + 1];
audio_out[i * 2] = left;
audio_out[i * 2 + 1] = right;
// CV is mono: one sample per frame
let mod_value = mod_in[i];
cv_out[i] = mod_value;
Input arrays are read-only. Output arrays are write-only.
Parameters
Parameters are available as read-only f32 variables:
audio_out[i * 2] = audio_in[i * 2] * gain;
State variables
State scalars and arrays are mutable and persist across calls:
state {
phase: f32
buffer: [1024]f32
}
process {
phase = phase + 0.01;
buffer[0] = phase;
}
Control Flow
if / else
if phase >= 1.0 {
phase = phase - 1.0;
}
if value > threshold {
audio_out[i * 2] = 1.0;
} else {
audio_out[i * 2] = 0.0;
}
for loops
For loops iterate from 0 to an upper bound (exclusive). The loop variable is an immutable int.
for i in 0..buffer_size {
// i goes from 0 to buffer_size - 1
}
for j in 0..len(buffer) {
buffer[j] = 0.0;
}
The upper bound must be an integer expression. Typically buffer_size, len(array), or a constant.
There are no while loops, no recursion, and no user-defined functions. This is by design — it guarantees bounded execution time on the audio thread.
Operators
Arithmetic
| Operator | Description |
|---|---|
+ |
Addition |
- |
Subtraction (binary) or negation (unary) |
* |
Multiplication |
/ |
Division |
% |
Modulo |
Comparison
| Operator | Description |
|---|---|
== |
Equal |
!= |
Not equal |
< |
Less than |
> |
Greater than |
<= |
Less than or equal |
>= |
Greater than or equal |
Logical
| Operator | Description |
|---|---|
&& |
Logical AND |
|| |
Logical OR |
! |
Logical NOT (unary) |
Built-in Functions
Trigonometric
| Function | Description |
|---|---|
sin(x) |
Sine |
cos(x) |
Cosine |
tan(x) |
Tangent |
asin(x) |
Arc sine |
acos(x) |
Arc cosine |
atan(x) |
Arc tangent |
atan2(y, x) |
Two-argument arc tangent |
Exponential
| Function | Description |
|---|---|
exp(x) |
e^x |
log(x) |
Natural logarithm |
log2(x) |
Base-2 logarithm |
pow(x, y) |
x raised to power y |
sqrt(x) |
Square root |
Rounding
| Function | Description |
|---|---|
floor(x) |
Round toward negative infinity |
ceil(x) |
Round toward positive infinity |
round(x) |
Round to nearest integer |
trunc(x) |
Round toward zero |
fract(x) |
Fractional part (x - floor(x)) |
Clamping and interpolation
| Function | Description |
|---|---|
abs(x) |
Absolute value |
sign(x) |
Sign (-1.0, 0.0, or 1.0) |
min(x, y) |
Minimum of two values |
max(x, y) |
Maximum of two values |
clamp(x, lo, hi) |
Clamp x to [lo, hi] |
mix(a, b, t) |
Linear interpolation: a*(1-t) + b*t |
smoothstep(edge0, edge1, x) |
Hermite interpolation between 0 and 1 |
Array
| Function | Description |
|---|---|
len(array) |
Length of a state array (returns int) |
CV
| Function | Description |
|---|---|
cv_or(value, default) |
Returns default if value is NaN (unconnected CV), otherwise returns value |
Sample
| Function | Description |
|---|---|
sample_len(s) |
Number of frames in sample (0 if unloaded, returns int) |
sample_read(s, index) |
Read sample data at index (0.0 if out of bounds, returns f32) |
sample_rate_of(s) |
Original sample rate of the loaded audio (returns int) |
Sample data is stereo interleaved, so frame N has left at index N*2 and right at N*2+1.
Comments
// This is a line comment
let x = 1.0; // Inline comment
Line comments start with // and extend to the end of the line.
Semicolons
Semicolons are optional statement terminators. You can use them or omit them.
let x = 1.0; // with semicolons
let y = 2.0
audio_out[0] = x + y
Examples
Stereo Delay
name "Stereo Delay"
category effect
inputs {
audio_in: audio
}
outputs {
audio_out: audio
}
params {
delay_time: 0.5 [0.01, 2.0] "s"
feedback: 0.3 [0.0, 0.95] ""
mix: 0.5 [0.0, 1.0] ""
}
state {
buffer: [88200]f32
write_pos: int
}
ui {
param delay_time
param feedback
param mix
}
process {
let delay_samples = int(delay_time * float(sample_rate)) * 2;
for i in 0..buffer_size {
let l = audio_in[i * 2];
let r = audio_in[i * 2 + 1];
let read_pos = (write_pos - delay_samples + len(buffer)) % len(buffer);
let dl = buffer[read_pos];
let dr = buffer[read_pos + 1];
buffer[write_pos] = l + dl * feedback;
buffer[write_pos + 1] = r + dr * feedback;
write_pos = (write_pos + 2) % len(buffer);
audio_out[i * 2] = l * (1.0 - mix) + dl * mix;
audio_out[i * 2 + 1] = r * (1.0 - mix) + dr * mix;
}
}
Sine Oscillator
name "Sine Oscillator"
category generator
outputs {
audio_out: audio
}
params {
frequency: 440.0 [20.0, 20000.0] "Hz"
amplitude: 0.5 [0.0, 1.0] ""
}
state {
phase: f32
}
ui {
param frequency
param amplitude
}
process {
let inc = frequency / float(sample_rate);
for i in 0..buffer_size {
let sample = sin(phase * 6.2831853) * amplitude;
audio_out[i * 2] = sample;
audio_out[i * 2 + 1] = sample;
phase = phase + inc;
if phase >= 1.0 {
phase = phase - 1.0;
}
}
}
Sample Player
name "One-Shot Player"
category generator
outputs {
audio_out: audio
}
params {
speed: 1.0 [0.1, 4.0] ""
}
state {
clip: sample
phase: f32
}
ui {
sample clip
param speed
}
process {
let frames = sample_len(clip);
for i in 0..buffer_size {
let idx = int(phase) * 2;
audio_out[i * 2] = sample_read(clip, idx);
audio_out[i * 2 + 1] = sample_read(clip, idx + 1);
phase = phase + speed;
if phase >= float(frames) {
phase = 0.0;
}
}
}
CV-Controlled Filter (Tone Control)
name "Tone Control"
category effect
inputs {
audio_in: audio
cutoff_cv: cv
}
outputs {
audio_out: audio
}
params {
cutoff: 1000.0 [20.0, 20000.0] "Hz"
resonance: 0.5 [0.0, 1.0] ""
}
state {
lp_l: f32
lp_r: f32
}
ui {
param cutoff
param resonance
}
process {
for i in 0..buffer_size {
let cv_mod = cv_or(cutoff_cv[i], 0.0);
let freq = clamp(cutoff + cv_mod * 5000.0, 20.0, 20000.0);
let rc = 1.0 / (6.2831853 * freq);
let dt = 1.0 / float(sample_rate);
let alpha = dt / (rc + dt);
let l = audio_in[i * 2];
let r = audio_in[i * 2 + 1];
lp_l = lp_l + alpha * (l - lp_l);
lp_r = lp_r + alpha * (r - lp_r);
audio_out[i * 2] = lp_l;
audio_out[i * 2 + 1] = lp_r;
}
}
LFO
name "LFO"
category generator
outputs {
cv_out: cv
}
params {
rate: 1.0 [0.01, 20.0] "Hz"
depth: 1.0 [0.0, 1.0] ""
}
state {
phase: f32
}
ui {
param rate
param depth
}
process {
let inc = rate / float(sample_rate);
for i in 0..buffer_size {
cv_out[i] = sin(phase * 6.2831853) * depth;
phase = phase + inc;
if phase >= 1.0 {
phase = phase - 1.0;
}
}
}
Safety Model
BeamDSP scripts run on the real-time audio thread. The language enforces safety through compile-time restrictions:
- Bounded time: Only
for i in 0..Nloops with statically bounded N. Nowhileloops, no recursion, no user-defined functions. An instruction counter limit (~10 million) acts as a safety net. - Constant memory: All state arrays have compile-time sizes. The VM uses a fixed-size stack (256 slots) and fixed locals (64 slots). No heap allocation occurs during processing.
- Fail-silent: If the VM encounters a runtime error (stack overflow, instruction limit exceeded), all outputs are zeroed for that buffer. Audio does not glitch — it simply goes silent.
File Format
BeamDSP scripts use the .bdsp file extension. Files are plain UTF-8 text. You can export and import .bdsp files through the Script Editor pane or the node graph's script picker dropdown.