SuperAPI AI Directory

m4l-gen

Design & creation

View repository

Updated 10-16 03:41

About this skill

gen~ / codebox inside Max for Live

gen~ turns an embedded Gen patcher into a fully compiled, sample-accurate DSP routine that runs on the audio thread. Inside M4L, reach for gen~ when:

  • You need single-sample feedback (filters with history, delay 1)
  • You need sample-accurate buffer reads/writes (granular, scrubbing)
  • You're writing a hand-coded oscillator where vector-rate MSP would alias or quantize timing
  • You need frequency-domain operations inside pfft~

Don't use gen~ for control-rate logic — that belongs in JS or message-domain Max objects.

1. codebox — the GenExpr text operator

A codebox inside a gen~ patcher holds GenExpr code. Its inlets/outlets are derived from the code: the compiler scans for inN / outN keywords where N is the index.

// single-pole lowpass (1-pole IIR)
Param cutoff(0.1, min=0.001, max=0.99);
History y1(0);
y1 = y1 + cutoff * (in1 - y1);
out1 = y1;

Wrap that codebox in a gen~ subpatcher with an in 1 and out 1 box and you have an audio-rate filter with a cutoff parameter accessible from outside.

2. GenExpr language essentials

  • Syntax is C / JavaScript-like.
  • Single-expression line: trailing ; optional. Multi-statement: required.
  • Comments: // and /* ... */.
  • I/O: in1, in2, …, out1 = …, out2 = …. Numbered, not named.
  • Multi-return: r, theta = cartopol(x, y); — unused returns are eliminated by the optimizer.
  • Control flow: if/else, for, while, break, continue. The compiler protects against infinite loops.
  • Custom functions: define before main statements.

Operators

Arithmetic: + - * / % Comparison: == != < > <= >= Logical: && || ! Bitwise: & | ^ ~ << >> Math functions: sin cos tan asin acos atan atan2 exp log log2 log10 sqrt pow abs sign min max clamp wrap fold mix lerp scale DSP-specific: phasor click train rate cycle noise delta change history delay sah peek poke sample lookup nearest wave splat

3. Declarations

Param — externally settable, persisted

Param name (default, min=lo, max=hi);

Each Param becomes an inlet / message-settable parameter on the wrapping gen~. From outside Max:

  • Send cutoff 0.5 to set
  • Connect a live.dial (with parameter_enable and valueof block) to a gen~ inlet labeled with the same param name to make it Live-automatable

Range constraints clamp inputs. Default is used at instantiation.

History — single-sample state

History y1(0);    // initial value 0
y1 = y1 + 0.01 * (in1 - y1);   // implicit Z⁻¹ — read y1, then assign new value, next sample reads the new value
out1 = y1;

History IS the Z⁻¹ register. Reading y1 returns the previous sample's value; writing updates it for the next sample. This is the unique GenExpr feature — MSP cannot do this with pow_pow_one~-style feedback at single-sample precision.

History is also externally settable/readable with the same naming rule as Param.

Delay — multi-sample delay line

Delay buf(48000);   // max delay in samples
buf.write(in1);
out1 = buf.read(samplerate * 0.25);  // 250 ms tap

buf.write(x), buf.read(samples), buf.read(samples, interpolation) where interpolation = linear, cubic, spline, cosine.

Buffer — reference an external buffer~

Buffer mybuf("kick");

out1 = peek(mybuf, phasor(1) * dim(mybuf), 0);   // play through "kick" buffer

Buffer operators: peek (interpolated read by sample frame), poke (write), sample (interpolated read by phase 0..1), lookup (table lookup), nearest, wave (resampled), splat (accumulating write).

dim(buf) = length in samples. channels(buf) = channel count.

Data — local storage (read/write at sample rate)

Data mybuf(512, 1);   // 512 samples, 1 channel
poke(mybuf, in1, in2);
out1 = peek(mybuf, in3);

Default Data(512, 1). 64-bit floats. Persists for the lifetime of the gen~ object.

4. Integration with the outer patcher

A gen~ subpatcher receives signals through in N boxes and emits via out N boxes inside the gen patcher (these are gen-specific, not the Max inlet/outlet classes).

  • Audio signals flow through in/out.
  • Param values are settable via messages or by connecting a signal to a matching param inlet — Max routes cutoff 0.5 to the codebox's Param cutoff.
  • History initial values can be reset by sending history y1 0 to the gen~.

Driving Params from live.dial

[live.dial @parameter_longname cutoff @parameter_mmin 0.001 @parameter_mmax 0.99]
              |
              v
[gen~ filter @t cutoff]   ;; --- or send via a [s cutoff] / [r cutoff] inside gen

Or directly via a message connected to the gen~ inlet labeled cutoff. Inside the gen patcher, the Param cutoff(...) declaration creates that inlet automatically.

5. mc.gen~ — multichannel

mc.gen~ runs the same GenExpr body in N parallel voices, one per channel of the wrapping object. Used for polyphonic synthesis without poly~:

[mc.gen~ @chans 8 voice]

Each channel has its own History state. Configure voice count via @chans attribute or chans <n> message.

mc.live.gain~ etc. integrate with Live 11+ multichannel routing.

6. pfft~ integration

For frequency-domain DSP:

[pfft~ fft-patch 2048 4]   ;; FFT size 2048, overlap 4

Inside the pfft~ subpatcher, you can use gen~ for sample-accurate per-bin processing. The bin streams carry real/imag pairs.

7. When NOT to use gen~

  • Control-rate logic (parameter scaling, MIDI processing) — use JS or Max objects.
  • Per-block DSP that vanilla MSP handles fine (a delay~ cascade, cycle~ + *~).
  • LiveAPI traffic — gen~ has no access to it.

8. Common GenExpr mistakes

  1. Forgetting History for feedback — writing y = y * 0.5 inside a codebox without declaring History y won't compile.
  2. Using Param names that don't show up on the gen~ object — they appear as message destinations and as inlet labels visible in the gen~ Inspector; check.
  3. Floating-point comparison — never if (x == 0.0); use if (abs(x) < 1e-6).
  4. Loop bounds at compile time — for (i = 0; i < n; i += 1) requires n to be a constant or Param; not all expressions are allowed.
  5. Buffer name collisions — a Buffer declaration like Buffer kick("kick") references the buffer~ named "kick" in Max; if a buffer with that name doesn't exist, the gen~ silently does nothing.
  6. No floats below 0 or above 1 for peek phase argument when using sample(buf, phase) — phase is 0..1, not 0..length.
  7. out1 not assigned — gen~ emits silence. Always set every declared output.
  8. samplerate inside expressions — samplerate is a built-in constant (samples per second) for use in calculations.

9. Worked example: stereo allpass reverb

// Schroeder-style allpass
Param feedback(0.7, min=0, max=0.95);
Param dryWet(0.5, min=0, max=1);

Delay ap1(2048);
Delay ap2(2048);
Delay ap3(2048);
Delay ap4(2048);

ap_size = [347, 113, 37, 59];  // sample delays

x = in1;
y = x;

History h1(0);
v = h1 + (-feedback) * y;
ap1.write(v);
y = ap1.read(ap_size[0]) + feedback * v;
h1 = y;

// ...repeat for ap2..4

out1 = mix(in1, y, dryWet);
out2 = mix(in2, y, dryWet);

Drive feedback and dryWet from live.dials in the outer patcher's presentation mode.

10. Lifecycle

  • gen~ recompiles on edit. There's no visible "compile" step — just save.
  • Compilation errors appear in the Max Console with line numbers in the codebox.
  • gen~ patchers can be exported to C++ (gen.exportcode) for use as a standalone DSP plugin or VST/AU — but this is outside M4L's scope.

11. Recommended workflow

  1. Sketch the DSP in MSP first if possible; only move to gen~ for the parts that genuinely need single-sample feedback or sample-accurate buffer access.
  2. Wrap the gen~ in a bpatcher subpatch so the params are clearly visible from the outer patch.
  3. Expose every Param as a live.dial/live.numbox in the outer patcher (see m4l-live-api).
  4. Test with extreme values — gen~ doesn't auto-protect from blowups; add clamp/tanh saturation where feedback paths can grow.
  5. Freeze the device before distribution so the embedded gen patcher travels with the .amxd.