Granular synth function in gen~ does not work with for loop but works without it
Hi all, long time no post.
I'm building a little granular synth with gen~, and I cannot seem to be able to build the grains using a function in a for-loop, while calling the same function 30 times (with a manually set instance number) works.
See the attached post.
So this works: out_left = 0;
out_right = 0;
l, r = make_grain(0, c, smpl, size, position, pch, spread, spray, stereo_spread);
out_left += l; d
out_right += r;
l, r = make_grain(1, c, smpl, size, position, pch, spread, spray, stereo_spread);
out_left += l;
out_right += r;
// ... and so on, times 30
But this doesn't: for(v=0;v<30;v+=1) {
q, w = make_grain(v, c, smpl, size, position, pch, spread, spray, stereo_spread);
out_leftt += q;
out_rightt += w;
}
If I turn the call in the for-loop into this...q, w = make_grain(0, c, smpl, 300000, position, 0.03, spread, spray, stereo_spread);
...I at least get some audible output. But setting the 0 back to v makes it go silent again, and the size and pitch parameters have to be set to very different value ranges than are otherwise defined somehow.
What am I missing?
I suspect the problem will be inside make_grain() -- it probably has some state such as a History, or some stateful operator such as a phasor() or cycle() or accum().
Here's a simple example to illustrate the root cause:for (i=0; i<10; i+=1) {
x = accum(y);
}
This code is ambiguous: Does this mean we have 10 accumulators, or does it mean we have one accumulator we call 10 times? Both are quite reasonable assumptions for different use cases.
The way gen~ interprets it, we have one accumulator that is called 10 times. (The reason why we opted for that interpretation is partly to prevent needing to allocate memory in the audio thread, which is generally a bad idea.)
It matters because accum() has an internal state -- the current running sum. If you had 10 accumulators you need 10 running sum states (10 numbers stored in memory). If you have 1 accumulator called 10 times, you only need 1 number stored in memory.
Translated to your case: in the for loop, you have 30 calls to make_grain(), not 30 copies of make_grain(). Whatever internal state you have in make_grain(), it's shared between all 30 loop iterations.
There's two ways to work around this. One is to just make 30 copies of your make grain patcher, or 30 calls to make_grain() as you did -- that ensures that the internal state of each one is distinct.
The other is to pass the state into the make_grain() function -- that may be more fiddly to implement, it depends on what make_grain() is doing.
(Well there's a third option too -- which we explored in Ch10 of the GO book, where each grain is created all at once and written into an output buffer/data -- that works well so long as the grains are very short.)
Cool, a response from Dr. Gen himself, thank you for the insights.
I would have never thought that there would be an internal difference between manually calling a function twice and doing that from a for-loop. Since I want to control the number of grains from outside of the gen code, the manual calling does not seem practical (or elegant).
See below for my make_grain(). From your explanation it seems the problem is with delta(), right?
make_grain(voice, gcount, buf, gsize, gposition, gpitch, gspread, gspray, gstereo_spread) {
trig = delta(gcount == voice) == 1;
icount = min( counter(1, trig), latch(gsize, trig) ); // counter resets when trig is nonzero. Latch passes gsize when trig is nonzero
pos = latch(gposition + (noise() * gspray), trig);
pos *= dim(buf);
sprd = noise() * gspread;
sprd = pow(2, sprd);
i = pos + icount * latch(gpitch, trig);
amp = hann(icount, latch(gsize, trig));
smp = peek(buf, i, boundmode="wrap", interp="cubic");
grain = amp * smp;
pan_left, pan_right = pan(gstereo_spread, trig);
grain_left = grain * pan_left;
grain_right = grain * pan_right;
return grain_left, grain_right;
}
If the internal state of delta() persists between iterations in the for-loop, trig is not going to be high as much as it needs to be.
So perhaps I could manually store the outcome of gcount == voice for each grain using a Data object with as many samples as there are grains, and then run my own delta operation?
I also looked at the ola gen example, but would have to modify it a bunch because I want to allow long grains while retaining responsiveness in shorter times than the grains may be long. Following your earlier comment about ola somewhere in the forum, I would need to make a scheduler that writes to the ola buffer in chunks instead of the entire grain.
All of the latch operators in that function are also stateful (they hold on to the previous value), and also the counter.
Since the latches are all driven by the trig, they are basically holding the per-grain parameters. The counter is just counting samples from the grain start, right? You could work around that by having a global counter (or just using the `elapsed` operator), and storing the grain start time.
So your Data would need to store all of these per-grain parameters, including the grain start time.
If you have, say 4 per-grain parameters, and up to 30 grains, you could store it all in a [data grainparams 30 4].
Then you can pull out all the parameters in a one-shot call like this:
make_grain(voice, buf, gstereo_spread) {
start, pos, gpitch, gsize = peek(grainparams, voice, channels=4);
icount = elapsed - start;
// etc.
}
Then you can have another function (start_grain()) that would configure the parameters for a new grain, including setting the start == elapsed, and writing into the data
poke(grainparams, elapsed, voice, 0);
poke(grainparams, position, gposition + noise() * gspray, 1);
poke(grainparams, elapsed, gpitch, 2);
// etc.
Hope that helps!
BTW a tip for debugging: use a buffer~ first so you can actually see what the data looks like. In the max patch, a [buffer~ grainparams @samps 4 30], and use [waveform~] objects to look at the data. In the gen~ patch, [buffer grainparams].
It's easy to then swap that out for [data grainparams 4 30] later once you know it is working ;-)
Thanks, this helps! I'll try implementing it and will report back for future generations.
There is a UI challenge that I have to deal with first - compared to the possibilities and workflow I'm used to for web-based UI design, Max leaves a lot to be desired in terms of customizability, layout functions and things like reuse of components or colors etc. But that's a different topic.