Custom envelope for grains - clips

Hi guys,
I have a question about a strange artifact I’m facing working with granular synthesis and custom buffer envelopes.

I will try to explain it with my (simplified) code.
First let’s create the synth definition

(
SynthDef(\grain_player_test, {
	|
	out=0, gate=1, amp=0.9, pan=0.0,
	atk=5, dcy=0.2, sus=0.7, rel=5,
	buf, rate=1, length=1, density=5, envbuf=(-1)
	|
	var sig, env;
	var trigger = Impulse.kr( density );
	var pos = 0.5 + TRand.kr(-0.35, 0.35, trigger);
	env = EnvGen.kr(Env.adsr(atk, dcy, sus, rel), gate, doneAction:2);

	sig = GrainBuf.ar(
		1, // number of channels 
		trigger,
		length,
		buf,
		rate,
		pos,
		2,
		envbufnum:envbuf
	);
	
	sig = LeakDC.ar(sig);
	sig = sig * env * amp;

	Out.ar(out, Pan2.ar(sig, pan));
}).add;
);

and loading any soft, continuos, harmonic mono samples you have;

~buf_pad = Buffer.read(s, "/path/to/my/pad/sample.wav");

Then instantiate the synth, you should hear a sterophonic granular/drone-sort-of sound.


(
x = Synth(\grain_player_test, [
	\out, 0, \gate, 1, \amp, 0.9,
	\buf, ~buf_pad,
	\rate, 1, \pan, 0.0,
	\atk, 5, \dcy, 0.2, \sus, 0.7, \rel, 5,
	\density, 1,
	\length, 10
])
);

As you can see we are using the default envbuf (Hann envalope) here. The result is fine!

Now, create a custom buffer envelope with an initial sharp edge and a closing soft ramp

~winenv = Env([1,0],[1]).discretize(n:1024);
~buf_env = Buffer.loadCollection(s, Array.newFrom( ~winenv ) );
// you can plot it to see its shape
~winenv.plot;
~buf_env.plot;

And now use this envelope to shape the individual grains (let’s separate them a bit, and also make them shorter to better understand the glitch phenomenon I was talking about):

x.set(\length, 1, \density, 0.5, \amp, 0.9, \envbuf, ~buf_env)

If you hear what I do, you will perceive a clip at the beginning of each grain (which makes perfect sense since we have a sharp shape on the rising edge of the envelope) but, why is there also a clip at the end of the grain from the moment that we have a smooth shape this side?

I’ve done also another experiment with a different envelope (this time it has a triangular shape):

~winenv = Env([0,1,0],[1,1]).discretize(n:1024);
~buf_env = Buffer.loadCollection(s, Array.newFrom( ~winenv ) );

x.set(\length, 1, \density, 0.5, \amp, 0.9, \envbuf, ~buf_env)

This time the sound is perfectly smooth.

Why?
Is there something I’m not doing the correct way?
Does the dimensione of the buffer has somthing to do with it? In case so, is there a suggested dimensione for the buffer?

Aside from this: it that the best way to convert an envelope to a buffer the one I’m using here?

Thank you so mush for you support
regards
na

This code bit isolates a single grain’s envelope shape as produced by GrainBuf, or grain contents (depending on the srcBufFill function).

(
var srcBufSize = 2000;

// use this one to see the envelope shape
var srcBufFill = { |buf| buf.fillMsg(0, 2000, 1.0) };

// use this one to see a sinusoid under the envelope
// var srcBufFill = { |buf| buf.sine1Msg((0 ! 24) ++ [1], asWavetable: false) };

s.waitForBoot {
	var c = CondVar.new;
	var unityBuf, envBuf, recBuf;
	var result;

	unityBuf = Buffer.alloc(s, 2000, 1, srcBufFill);
	envBuf = Buffer.sendCollection(s, Env([1, 0], [1]).discretize(1024), 1);
	recBuf = Buffer.alloc(s, 512, 1);
	
	s.sync;
	
	a = {
		var sig = GrainBuf.ar(
			1, Impulse.ar(0),
			dur: recBuf.duration * 0.7,
			sndbuf: unityBuf,
			pos: 0.2,
			envbufnum: envBuf
		);
		RecordBuf.ar(sig, recBuf, loop: 0, doneAction: 2);
		Silent.ar(1);
	}.play;
	a.onFree({ c.signalOne });
	c.wait;
	
	recBuf.getToFloatArray(action: { |data|
		result = data;
		c.signalOne;
	});
	c.wait;
	
	result.plot;
	
	unityBuf.free; envBuf.free; recBuf.free;
};
)

But I don’t see the problem: neither plot shows any discontinuity.

I also tried your synthdef with my own smooth-sound audio file, as well as with a sine wave:

b = Buffer.alloc(s, 22050, 1, { |buf| buf.sine1Msg((0 ! 400) ++ [1], asWavetable: false) });

No end-of-grain click.

So I can’t guess what you’re hearing/seeing. Perhaps try recording it, and zooming in using Audacity, to look visually for discontinuities.

hjh

Actually I did the test wrong… now I see/hear it.

My guess is that the index reading the envelope buffer might be overshooting by a small amount.

Generally grains are considered to begin with silence and end with silence. If you begin with silence, then the overshoot is silent… so why not like this?

e = Buffer.sendCollection(s, Env([0, 1, 0], [0.001, 0.999]).discretize(1024), 1);

hjh

1 Like

Thank you @jamshark70 for your support and thank your for double checking on the thing.
I perfectly understand your point and, in fact, your approach works!

The fact is that I don’t want to completely get rid of the glitch: in particular I only want the initial glitch, that’s why, in my original code, I had that descending ramp shape.

In my mind this should have created the effect I wanted but, apparently not (also it doesn’t seem to be related to the grain interpolation, nor the envbuf dimension)!

I think I should use something different from the GrainBuf Ugen :frowning:
Do you have any suggestion on how to do this in a different way?

I think the starting point here is – what is granular synthesis?

It’s a cloud of overlapping grains.

Then… what is a grain? It’s a segment of audio under an envelope.

  • Segment of audio: There are all kinds of ways to handle that. Assuming that the audio source is a buffer, the most likely UGen would be PlayBuf.
  • Envelope: Could be EnvGen, could be a function saved in a buffer, could be… anything.

Then you need to parcel out triggers to handle the overlap.

There’s a neat trick with PulseDivider to spread the grain triggers out across several parallel signal chains. Parallel chains can overlap. The maximum amount of overlap has to be hardcoded into the SynthDefs structure, but that shouldn’t be a big problem.

(
a = {
	// use something of your own with PlayBuf here
	var sig = SinOsc.ar((2..6) * 110);
	
	var trig = Impulse.kr(4);
	
	// here we get 5 parallel signal chains
	var trigs_x5 = PulseDivider.kr(trig, 5, (0..4));

	// here, use whatever you want to get your envelope shape
	var envs_x5 = EnvGen.kr(Env.perc(0.01, 0.97), trigs_x5);
	
	((sig * envs_x5).sum * 0.1).dup
}.play;
)

In that PulseDivider overlap design, you could substitute your own buffer readers in place of the sine waves, and your own click-on-start envelope.

GrainBuf and TGrains make it easier to do with granular synthesis, but if SC didn’t have them, you would still be able to do granular synthesis by going back to the fundamentals.

hjh

2 Likes

Thank you @jamshark70 ,
It always takes me some time to test and familiarize with new code, Ugens, other users approaches.
It seems to work for me, this is actually what I was looking for :slight_smile:

You are right, that’s one of the many wonderful aspects of SC!