GrainBuf Dur and Env Parameters, Why?

What sense should it make to have the possibility of defining an envelope for each grain in GrainBuf, where you could basically define the time each grain should take, and also having the dur parameter as well? Take the example of the help file:

(
var winenv;

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff");
// a custom envelope
winenv = Env([0, 1, 0], [0.5, 0.5], [8, -8]);
z = Buffer.sendCollection(s, winenv.discretize, 1);

SynthDef(\buf_grain_test, { |out, gate = 1, amp = 1, sndbuf, envbuf|
    var pan, env, freqdev;
    // use mouse x to control panning
    pan = MouseX.kr(-1, 1);
    env = EnvGen.kr(
        Env([0, 1, 0], [1, 1], \sin, 1),
        gate,
        levelScale: amp,
        doneAction: Done.freeSelf);
    Out.ar(out,
        GrainBuf.ar(2, Impulse.kr(10), 0.1, sndbuf, LFNoise1.kr.range(0.5, 2),
            LFNoise2.kr(0.1).range(0, 1), 2, pan, envbuf) * env)
}).add;
)

what happens to the winenv duration of 1 second (also can’t be heared) when I have a grain dur of 0.1 seconds?

The grain envelope duration will be scaled by whatever factor is needed so that the output envelope matches the grain duration.

There’s really no other logical way to handle it.

hjh

I can’t understand your answer! Please elaborate a bit more…

Granular Synthesis is per definition a short segment of audio with a window function applied to it. Often times the window function is a hanning window which is symmetrical and has a continuous slope which makes it perfect for overlapping without creating any amplitude modulation artefacts. If you dont specify a custom window for GrainBuf its using its default hanning window and applies it to every grain but you are free to use every shape you want and pass a custom envelope / window function to GrainBuf. i hope i got the question right.

I know all of this, my question was why there is a need for a grain dur parameter, when I can specify the grain duration using an envelope!

in the example you have two envelopes. one which is the grain window which is applied to every grain and the main envelope which is a applied to the output of Grainbuf.

Just rephrasing what @jamshark70 said (although I think it was clear).

GrainBuf’s envbufnum’s duration will be ignored.
It will be scaled so that the overall time is dur.

In fact, you can’t.

As an experiment, let’s take three different envelopes, all with two segments of equal lengths (i.e. dividing the time in half). The first envelope is your envelope = 1 second. The second is 1 millionth of a second, and the third is 1 million seconds.

Then we will do the standard practice for preparing an envelope buffer – discretize – and plot these results together.

(
[
    Env([0, 1, 0], [0.5, 0.5], [8, -8]),
    Env([0, 1, 0], [0.0000005, 0.0000005], [8, -8]),
    Env([0, 1, 0], [500000, 500000], [8, -8])
].collect { |env|
    env.discretize.as(Array)
}
.lace(1024*3)
.plot(numChannels: 3);
)

… and the three look the same (even though the long envelope is a trillion times longer than the shortest).

.discretize stretches or squeezes the envelope to fit in the number of requested samples. At that moment, the envelope’s duration is lost forever. GrainBuf has no idea about 1 second or other duration in the Env. What you’re left with is an abstract shape which preserves the ratios between the segments’ times. (That is, if you repeat the experiment with times [1, 3], and [0.000001, 0.000003], and [1000000, 3000000], they will all discretize to segments where the first takes 1/4 of the size, and the second takes 3/4.)

Having, now, an abstract, “timeless” envelope shape, GrainBuf applies graindur to it by scaling the shape to match the grain time. We can visualize this by granulating audio content = 1.0 and making sure there’s no overlap between grains, so that each grain is (audio = 1.0) * (env shape). Here, the envelope shape gets longer with each successive grain.

// weirdo grain shape
e = Env([0, 1, 0.1, 0.6, 0], [1, 3, 1, 5], \sin);
e.plot;

~envBuf = Buffer.sendCollection(s, e.discretize, 1);
~unityBuf = Buffer.alloc(s, 40000, 1, completionMessage: { |buf| buf.fillMsg(0, 40000, 1) });

(
{
	var trig = Impulse.ar(50);  // should be 5 in the plot
	var dur = PulseCount.ar(trig) * 0.004;
	GrainBuf.ar(1, trig,
		dur: dur,
		sndbuf: ~unityBuf,
		envbufnum: ~envBuf
	)
}.plot(duration: 0.1);
)

One might also imagine that the envbuf’s number of samples determines the grain duration. That would be inflexible – different durations per grain is a reasonable thing to support. You wouldn’t want to create a different buffer for every grain duration you’d ever want.

hjh