Lightningbeam/lightningbeam-ui/beamdsp/BEAMDSP.md

614 lines
13 KiB
Markdown

# 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..N` loops with statically bounded N. No `while` loops, 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.