Gen Memory is Weird

Published on 17/10/2025

Back to previous

(From a traditional programming perspective at least)

Gen is MaxMsp's low-level sub-language designed for audio and video processing. It compiles to C++ that can be run directly on the CPU and GPU, instead of inside Max's interpreting scheduler environment thingy.

In Gen (the DSP variant at least), to store a value you'll most often need to use a [history] or [delay] operator, or write values into an externally allocated buffer using something like [poke]. These memory operations have special syntax in GenExpr (the text variant of gen code, as opposed to the visual patching), they look kinda like object declarations in C-like languages, implying they're allocating memory and doing something special even if you're not looking at the exported C++ code too closely to know what that is.

Note: In notes like this, I always write the text used for visual patching objects inside square brackets, so they're distinguishable from the text used in GenExpr code.

Take this implementation of an integrator, shown in both patch form and the auto-generated GenExpr:

History history_1(0);
add_2 = in1 + history_1;
out1 = add_2;
history_1_next_3 = fixdenorm(add_2);
history_1 = history_1_next_3;

The [history] operator is created with History history_1(0) on the first line, and its value is updated with the assignment operator on the final line. Max inserts an implicit [fixdenorm] in some situations to prevent potential floating point issues, if you're wondering what that's about.

Here's similar code, but using a [delay] operator instead of a [history]:

Delay delay_1(1);
tap_2 = delay_1.read(8);
add_3 = in1 + tap_2;
out1 = add_3;
delay_1.write(add_3);

There's now a read and write method instead of just treating the delay "object" like a value, which I think is because [delay] supports multiple taps (read-writable locations inside the buffer), and you'd need syntax to pick which one. But, where did 8 on the same line come from? Any non-zero value here also implies that the one sample max delay length is also being ignored, huh? Random appearances of 4/8/16 in places like this I usually assume is some internal power of 2 based DSP optimization stuff. Also, no [fixdenorm] here I guess?

What is common to both of these operators though is that they're defined similarly using syntax like History history_1(0); and Delay delay_1(1);, which resembles creating OOP style objects in other languages. The [history] and [delay] operators both store values across iterations of a running patch, so I guess it makes sense to denote them with object-like syntax.

But this is totally inconsistent!

This GenExpr will produce a sine wave at 440 Hz:

out = cycle(440);

See anything weird about that? There's no Cycle cycle(frequency) syntax, you just call the cycle function. Internally though, [cycle] at least needs some state to track the current phase of the oscillator, but I guess here that's just implied?

This is super weird to me as a C-like language creature.

On one hand, it's neat because the result of the patch can be a single expression that maps 1-to-1 with visual elements in the patch. Take this example where I'm just summing 4 sine waves, its generated GenExpr simplifies exactly to a text representation of the tree, which is nice:

cycle_1, cycleindex_2 = cycle(110);
cycle_3, cycleindex_4 = cycle(220);
add_5 = cycle_1 + cycle_3;
cycle_6, cycleindex_7 = cycle(330);
cycle_8, cycleindex_9 = cycle(440);
add_10 = cycle_6 + cycle_8;
add_11 = add_5 + add_10;
out1 = add_11;

Simplified:

out1 = cycle(110) + cycle(220) + cycle(330) + cycle(440);

This would be fine, a quirk of the language design. Just learn that every time you use one of these stateful operators, either in a patch or in GenExpr, that it's abstracting some state allocation and management away from you. You can treat it like an abstract self contained signal processor, very Fausty, nice.

But this isn't at all how Gen actually works! Unlike Faust, Gen isn't a purely functional language, we have C-style for and while loops!

out_sum = 0;
for (i = 0; i < 10; i += 1) {
    out_sum += cycle(110 + i * 10);
}
out = out_sum;

If you were the Gen compiler, how would you interpret this?

The first option produces arguably an unintended result, as the stored phase needed to generate the intended wave shapes are being mashed together in a nonsensical way. However, the second option would require somehow counting the number of times the loop could run. Given that you can put any expressions in there, that feels a bit solve-the-halting-problem-y (impossible to write code to do).

Gen seems to go with option A. Which leads to really counterintuitive results and tutorials teaching "oh yeah I want to use a loop here but it doesn't work for some reason so just copy and paste the code inside 16 times."

That sine wave addition loop above, when pasted into a [codebox] and exported to C++ code, looks like this:

Generated object declarations:

SineCycle __m_cycle_1;
SineData __sinedata;

Generated loop code, runs for every sample:

t_sample out_sum = 0;
int i = 0;
while (i < 10) {
  __m_cycle_1.freq(110 + i * 10);
  out_sum = (out_sum + __m_cycle_1(__sinedata));
  i = i + 1;
}
*(__out1++) = out_sum;

I've cleaned up this code, abbreviated it, and removed some infinite loop tests, but you can see only one instance of the internal SineCycle object is created and used. You can't even create an array of these objects, because the GenExpr doesn't have syntax for declaring or accessing arrays.

Maybe Gen is just unique, I can't think of other languages that work like this? It guess it makes sense to try keep things simple for artists and musicians (MaxMsp's intended demographic). It has started to become a limitation I keep running into though, because unsurprisingly [cycle] isn't the only operator with behavior like this.

I don't really have a solution to end on, my workaround is to just prototype in C++ upfront lately when I know I'll be dealing with iterative algorithms and skip visual patching. Maybe I'll look into implementing my own abstractions in Gen itself that can read/write to slots in a shared buffer instead of storing individual pieces state, which would maybe get around the lack of array support?

Hopefully in the meantime if anyone stumbles onto this while dealing with the frustration of trying to decipher these mechanics for yourself, you can at least know that it's not just you, and I hope this helped clarify how Gen works.