Playbuf for FM in GrainSin

hey, here i have two examples where i have been trying to use a custom envelope played with Playbuf to modulate the frequency of SinOsc in the first example and GrainSin in the second example.
Can i have GrainSin behave like the SinOsc example when using an unipolar envelope or a any other bipolar signal stored in a buffer with Playbuf to modulate its frequency, i cant hear or see any modulation at all in the second example? thanks.

EDIT: Maybe the rate scaling for both examples is wrong. i have no clue at all.

1.) Example with SinOsc:

// custom granular envelopes
(
~getCustomEnvs = {
	var tri = Env([0, 1, 0], [1, 1], \lin);
	var perc = Env.perc(0.01, 1, 1, -4);
	var customEnvs = [tri, perc];
	customEnvs.collect{|env| Buffer.sendCollection(s, env.discretize(2048), 1) };
};
~customEnvs = ~getCustomEnvs.();
)

(
SynthDef(\sinOsc, { |freqBuf|

	var maxOverlap = 4;

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainDur = \overlap.kr(1) / tFreq;

	var freq = \freq.kr(440);

	var freqEnv = PlayBuf.ar(
		numChannels: 1,
		bufnum: freqBuf,
		rate: freq * \mRatio.kr(2) * BufFrames.kr(freqBuf) * SampleDur.ir * BufDur.kr(freqBuf) / grainDur,
		trigger: trig,
		loop: 1,
	);

	var sig = Array.fill(maxOverlap, { |i|

		var localTrig, hasTriggered, phase, grainEnv;

		localTrig = PulseDivider.ar(trig, maxOverlap, i);
		hasTriggered = PulseCount.ar(localTrig) > 0;

		phase = Sweep.ar(localTrig, hasTriggered);
		grainEnv = IEnvGen.ar(Env(
			[0, 1, 0],
			[0.5, 0.5],
			\sin
		), phase / grainDur);

		sig = SinOsc.ar(freq * (1 + (freqEnv *  \index.kr(4))));

		sig = sig * grainEnv;

	}).sum;

	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.1));

	sig = LeakDC.ar(sig);
	Out.ar(\out.kr(0), sig);
}).add;
)

(
Pdef(\sinOsc,
	Pmono(\sinOsc,

		\freqBuf, ~customEnvs[1],

		\mRatio, 0.01,
		\index, 4,

		\freq, 440,

		\dur, 0.5,
		\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
		\overlap, 4.00,

		\amp, 0.10,
		\out, 0,
	)
).play;
)

2.) Example with GrainSin

(
SynthDef(\grainSin, { |freqBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainDur = \overlap.kr(1) / tFreq;

	var freq = \freq.kr(440);
	
	var freqEnv = PlayBuf.ar(
		numChannels: 1,
		bufnum: freqBuf,
		rate: freq * \mRatio.kr(2) * BufFrames.kr(freqBuf) * SampleDur.ir * BufDur.kr(freqBuf) / grainDur,
		trigger: trig,
		loop: 1,
	);

	var sig = GrainSin.ar(
		numChannels: 1,
		trigger: trig,
		dur: grainDur,
		freq: freq * (1 + (freqEnv * \index.kr(4))),
	);
	
	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.1));
	
	sig = LeakDC.ar(sig);
	Out.ar(\out.kr(0), sig);
}).add;
)

(
Pdef(\grainSin,
	Pmono(\grainSin,

		\freqBuf, ~customEnvs[1],

		\mRatio, 0.01,
		\index, 4,

		\freq, 440,

		\dur, 0.5,
		\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
		\overlap, 4.00,

		\amp, 0.10,
		\out, 0,
	)
).play;
)

Without trying the code (so, educated guess here), SC’s granular UGens typically sample-and-hold grain parameters at the moment of a trigger, so, frequency modulation within the grain would not be possible.

hjh

thanks :slight_smile:
this makes sense. so one more reason to go for the custom pulsedivider approach. where unfortunately I still having struggle to sequence different buffered windows (im speaking of symmetrical windows like hanning and triangle for example) without discontinuities, even with demand rate ugens and the Sample and Hold trick you have shared. The sequencing of different windows works without any problem with the SC Grain Ugens. Even with experimental window shapes like this no problem for GrainSin and unuseable with the SinOsc + Pulsedivider approach:

I would like to sequence different window types and also do FM with buffered signals. so i was not sure which approach might work best. I think none of these approaches can do both.
Is the rate scaling of the Playbuf in the first example at least correct, so it behaves like an Oscillator? it looks correct on the plot, but all these little mistakes add up along the way, so it would be nice if i could get a feedback on that. probably also better with sample and hold for sequencing of different buffers thanks :slight_smile:

One problem here is – to be honest, after multiple posts in this thread, and in other threads, I have no idea what you’re trying to do. I also have no idea what is represented in your plots. When the problem isn’t stated clearly, I have to limit the amount of time I’m willing to spend on it. Otherwise, threads just go on forever – “oh that’s nice, but now I want…”

For this post, I’d like to suggest that it might be more productive to work from known quantities, systematically, rather than hacking.

What do we know?

  • Trigger frequency
    • From this, we also know how much time passes between triggers = 1/tfreq
  • Overlap
    • From this, we know how long a single window should last = overlap / tfreq (seconds per window)

Let’s say we want to use the Sweep / BufRd approach. Sweep’s rate input is the increment per second. Usefully, given a rate, you can also predict how long it would take to reach 1.0 – rate = 5, time to reach 1 = 1/5 = 0.2.

We want Sweep to reach the end of the window function after overlap / tfreq seconds. If this is initially confusing, one way to simplify is to normalize the phase – declare, arbitrarily, that 0 is the beginning of the window and 1 is the end. (Note that we just identified a relationship between Sweep rate and time-to-reach-1!)

Rate should be “something over time.” The window duration overlap / tfreq is expressed as “time over something” – so then the rate would be proportional to the reciprocal of duration = tfreq / overlap. This rate will reach 1.0 in the desired window duration!

Sweep.ar(trigs, tfreq / overlap)

Then, as a practical matter, the rate needs to be suppressed until the first trigger happens. So that quotient would have to be multiplied by hasTriggered.

This gets you to 1.0, but you need to get to the end of the envelope buffer. So, multiply that by BufFrames.

So let’s test:

~plotDur = 1000/44100;

// and let's get 10 windows within that time
~trigFreq = 10 / ~plotDur;

(
{
	var overlap = 4;
	var trig = Impulse.ar(~trigFreq);
	var trigs = PulseDivider.ar(trig, overlap, (0 .. overlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	
	var phase01 = Sweep.ar(trigs, (~trigFreq/overlap) * hasTriggered);
	var phase = phase01 * BufFrames.kr(b);
	
	var envs = BufRd.ar(1, b, phase);
	
	envs.sum  // so we can see the composite shape
}.plot(duration: ~plotDur);
)

triangular-overlap

And, after the ramp-up, it does stabilize on a flat value. (It’s 2.0 because, with overlap = 4, there are two pairs of envelopes, and each pair stabilizes at 1.0.)

Then we can take this as a proven methodology to overlap windows in a stable way. This does not guarantee that every window shape will overlap-add to a flat value, but using this method, you can be sure that the windows are timed and scaled correctly. Therefore, if you try a different window shape and find discontinuities or amplitude modulation, then it must be the fault of the window shape and not of the windowing logic.

At this point, I think I will need to withdraw from this general topic. I’ve already taken quite a bit of time on it.

hjh

hey, my deep apologies.

i was going trough the source code of the nuPG and was trying to implement some of its functionalities with buffered FM and experimental windows for pulsar synthesis because unfortunately its not working on a windows maschine, where im currently at.
This ran quickly over my head and I discovered some misunderstandings of language and server side sequencing and granular synthesis on my side along the way.

thank you very much for your time, i deeply appreciate all your help.
I was trying my best to be as clear as possible and to present my questions the best way i could possibly do, but there is obviously room for adjustment.
i will be trying to be as clear and condensed as possible in the future with my questions so other users could help if they like to.

i will be going trough your explanations and your example.

EDIT: sequencing of two different buffered windows with patterns which are not causing any amplitude modulation or discontinuities by themselves, are still leading to discontinuities i will try to find a way out of this.

thanks a lot!

I should also apologize for being a bit grumpy in the morning.

There are a few known methodologies for problems like this, where there are a lot of moving parts. Off the top of my head, some relevant ones are:

  • Simplify the test case as much as possible (eliminate as many points of potential failure as you can).

  • Any components that can be isolated, try to debug them in isolation. (My example this morning isolates the grain envelopes.)

  • Incremental testing: Change (or delete) one thing at a time until broken behavior gets better, or working behavior breaks. Then you know that whatever you changed is the thing that made the difference.

hjh

hey, dont worry :slight_smile:

i will keep these in tips in mind.

There seem to be some build in functionalities inside the SCs Grain Ugens, which make it possible to play small overlaps, high trigger rates and asymetrical window shapes without any bigger problem, i took this for granted and already build some Pdefs on top of these functionalities, which are not easily reproduceable with the PulseDivider approach.

In the nuPG source code are some GrainBuf / TGrains + freqPlayBuf combinations for FM, so i thought this would work. probably some things ive overlooked.

So right now Im left with either having the possibility of more extreme settings with the Grain Ugens without buffered signals for FM or do FM with complex buffered signals and a hanning window for the envelope and no possibility of sequencing them.

Ive been trying to implement a BufRd crossfade for envBuf and freqBuf to be able to sequence two different buffers for each of them with Demand Ugens. But as far as i was able to implement it, its quite CPU demanding and works semi well.

And beside that the asymetrical window shapes even without any sequencing sound way more distorted with this approach compared to the Grain Ugens which can handle them.

thanks :slight_smile:

just one discovery:

you can do FM with GrainSin when you bind the trigger rate to the fundamental:

(
{ 
	var freq = \freq.kr(440);
	var trig = Impulse.ar(freq);
	var grainDur = \overlap.kr(1) / freq;
	var sig, fmod;
	
	fmod = SinOsc.ar(5);
	
	sig = GrainSin.ar(
		numChannels: 1, 
		trigger: trig, 
		dur: grainDur, 
		freq: freq * (1 + (fmod * \index.kr(5)))
	); 
	
	sig = LeakDC.ar(sig);

	sig !2 * 0.1;
}.play;
)

OK, one other quick thing – if you’re checking the behavior of envelopes in SC’s built-in granular UGens, you’re probably looking at two things at the same time: the envelope, multiplied by audio. That means, with respect to the envelopes, you don’t actually have any idea what you’re seeing. So you can’t draw a valid conclusion.

For the purpose of “debugging with isolation,” you can look at the envelopes only by filling the audio buffer with 1.0. Then you will get the sum of the envelopes * 1.0.

What you’ll find when you do this is that, for an arbitrary window function, you have no guarantee of a flat sum.

b = Buffer.alloc(s, 88200, 1, { |buf| buf.fillMsg(0, buf.numFrames, 1) });

// linear percussive envelope
c = Buffer.sendCollection(s, Env([0, 1, 0], [0.005, 0.995], \lin).discretize(2048), 1);

(
{
	var tfreq = 200;
	var overlap = 4;
	var trig = Impulse.ar(tfreq);
	var dur = overlap / tfreq;
	GrainBuf.ar(2, trig, dur, b, pos: 0.5, envbufnum: c)
}.plot(duration: 0.05)
)

This plot ramps up and then stabilizes on a sawtooth pattern.

OK, let’s do the same with the Sweep approach.

~plotDur = 0.05;

~trigFreq = 200;

(
{
	var overlap = 4;
	var trig = Impulse.ar(~trigFreq);
	var trigs = PulseDivider.ar(trig, overlap, (0 .. overlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	
	var phase01 = Sweep.ar(trigs, (~trigFreq/overlap) * hasTriggered);
	var phase = phase01 * BufFrames.kr(c);
	
	var envs = BufRd.ar(1, c, phase);
	
	envs.sum  // so we can see the composite shape
}.plot(duration: ~plotDur);
)

Same shape. So there’s no magic.

Now, I’m not sure if this is what you mean by “discontinuity” but this is an example of what I said earlier: if you deviate away from the ideal case of a Hann (or triangular) window with an even overlap factor, then the result will lose smoothness.

That is, if you want to sequence envelope shapes, then you’re not going to get a flat envelope sum.

hjh

thanks for the dispel of magic :slight_smile:

ive tested to sequence hanning / triangular and hanning / perc with patterns, that only works with Grain Ugens.
For smoothing out the sequencing one probably needs a solution for crossfading between buffers.
My assumption would be that Grainbuf does this automatically.

Can you post an example of this? At this point, the only thing I can discern is that you’re dissatisfied with some of the results, but it isn’t clear to me exactly what is dissatisfactory.

AFAICS the enveloping logic should be the same between the grain UGens and the Sweep/BufRd approach, because, if the problem spec is to produce f grains per second with overlap g, then there’s only one right answer: it’s a deterministic problem with a deterministic result. (Even if you choose envelope shapes randomly, a particular sequence of them is still a deterministic problem and there is only one correct way to render it. If there is a significant difference between the two implementations, then one or both of them must be faulty. That’s the reason for my interest tbh – if there’s a bug, then it’s worth finding and fixing. Now, what I found is that my Sweep version and GrainBuf do not show a significant difference. So at this point, my best guess is that your SynthDef version has a problem somewhere.)

I’d guess that it doesn’t, because the time scale of a grain is so small that differences in envelope shape are likely to have a relatively small perceptual impact. Crossfading would produce even more minute variations, with even less perceptual impact.

I looked back at one of your earlier posts, saying something about simple waveforms with complex envelopes. So then the amplitude modulation from the envelopes is the whole point… why bother with complex envelopes if they’re just going to flatten out? Thus “discontinuities” must be referring to something else, but lacking a description or an example, I couldn’t go much further.

hjh

here is an example of the sequencing comparision:

(
// custom granular envelopes
~getCustomEnvs = {
	var tri = Env([0, 1, 0], [1, 1], \lin);
	var perc = Env.perc(0.01, 1, 1, -4);
	var customEnvs = [tri, perc];
	customEnvs.collect{|env| Buffer.sendCollection(s, env.discretize(2048), 1) };
};
~customEnvs = ~getCustomEnvs.();
)

1.) GrainSin

(
SynthDef(\grainSin, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainDur = \overlap.kr(1) / tFreq;
	
	var sig = GrainSin.ar(
		numChannels: 1,
		trigger: trig,
		dur: grainDur,
		freq: \freq.kr(440),
		envbufnum: envBuf
	);
	
	sig = Pan2.ar(sig,\pan.kr(0), \amp.kr(0.50));
	Out.ar(\out.kr(0), sig);
}).add;
)


// sequencing of two envelope shapes with GrainSin
(
x = Pmono(\grainSin,
	
	\freq, 440,
	
	\bufIndex, Pdup(4, Pseq([0, 1], inf)),
	\envBuf, Pfunc{ |ev| ~customEnvs[ev[\bufIndex]] },
	
	\dur, 0.1,
	\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
	\overlap, 1.00,
	
	\amp, 0.5,
	\out, 0,
).play;
)

x.stop;

2.) PulseDivider

(
SynthDef(\pulseDivider, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var maxOverlap = 4;
	
	var trigs = PulseDivider.ar(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	var phase = Sweep.ar(trigs, (tFreq / \overlap.kr(1)) * hasTriggered);
	var grainEnv = BufRd.ar(1, envBuf, phase * BufFrames.kr(envBuf));
	
	var sig = SinOsc.ar(\freq.kr(440));
	
	sig = (sig * grainEnv).sum;
	
	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.10));
	Out.ar(\out.kr(0), sig);
}).add;
)

// sequencing of two shapes with pulsedivider
(
y = Pmono(\pulseDivider,
	
	\freq, 440,
	
	\bufIndex, Pdup(4, Pseq([0, 1], inf)),
	\envBuf, Pfunc{ |ev| ~customEnvs[ev[\bufIndex]] },
	
	\dur, 0.1,
	\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
	\overlap, 1.00,
	
	\amp, 0.1,
	\out, 0,
).play;
)

y.stop;

probably ive made a mistake with the pulsedivider approach, ive turned the amplitude down in the second example to match more or less the same loudness.

EDIT: whats also interesting is that the pulse of the percussive envelope is irregular with the first example.

this is something ive picked up in a talk by @marcin_pietruszewski at https://youtu.be/yNF9eoRS60I?t=2623
where he is presenting the nuPG and wanted to try out by myself. The envelope he is using in the end “hairy leg” is this one for example:

1 Like

I think it’s this: all grain parameters should be sampled-and-held at the start of the grain. It is not valid to change the envBuf mid-grain, but that is a possibility here. If the envBuf control input changes, it should take effect for the next grain, but never in the middle of a currently playing grain.

var grainEnv = BufRd.ar(1, Latch.kr(envBuf, trigs), phase * BufFrames.kr(envBuf));

In fact, I did mention Latch before, but that’s been forgotten in your SynthDef.

tFreq, overlap and the SinOsc frequency should also be Latched.

hjh

okay, thanks sample-and-hold got lost along the way.

unfortunately now with this implementation i get only one envelope:

(
SynthDef(\pulseDivider, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var maxOverlap = 4;
	var grainRate = tFreq / \overlap.kr(1);
	
	var trigs = PulseDivider.ar(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	var phase = Sweep.ar(trigs, Latch.kr(grainRate, trigs) * hasTriggered);
	var grainEnv = BufRd.ar(1, Latch.kr(envBuf, trigs), phase * BufFrames.kr(envBuf));
	
	var sig = SinOsc.ar(Latch.kr(\freq.kr(440), trigs));
	
	sig = (sig * grainEnv).sum;
	
	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.10));
	Out.ar(\out.kr(0), sig);
}).add;
)

// sequencing of two shapes with pulsedivider
(
y = Pmono(\pulseDivider,
	
	\freq, 440,
	
	\bufIndex, Pseq([1, 0], inf),
	\envBuf, Pfunc{ |ev| ~customEnvs[ev[\bufIndex]] },
	
	\dur, 0.1,
	\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
	\overlap, 1.00,
	
	\amp, 0.1,
	\out, 0,
).play;
)

y.stop;

It turns out that there is a not-quite-fully-solvable problem with this approach.

It’s a good case of isolating components for testing. You would never discover the cause only by listening to the result. The only way to debug this is to look at all four channels – what is happening with the components going into the envelopes.

“.plot” won’t work with Pmono (which is essential to your case). So, we also have to take apart plotting – create a buffer, and RecordBuf instead of Out. Also, it doesn’t make sense to pass envelopes by themselves out to the hardware, so let’s create a bus too.

~recBuf = Buffer.alloc(s, s.sampleRate, 4);
~recBus = Bus.audio(s, 4);

Four channels because, to debug this type of problem, you need to make sure each channel is doing the right thing.

s.boot;

(
// custom granular envelopes
~getCustomEnvs = {
	var tri = Env([0, 1, 0], [1, 1], \lin);
	var perc = Env.perc(0.01, 1, 1, -4);
	var customEnvs = [tri, perc];
	customEnvs.collect{|env| Buffer.sendCollection(s, env.discretize(2048), 1) };
};
~customEnvs = ~getCustomEnvs.();
)

~recBuf = Buffer.alloc(s, s.sampleRate, 4);
~recBus = Bus.audio(s, 4);

(
SynthDef(\pulseDividerRec, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var maxOverlap = 4;
	var grainRate = tFreq / \overlap.kr(1);
	
	var trigs = PulseDivider.ar(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	var phase = Sweep.ar(trigs, Latch.kr(grainRate, trigs) * hasTriggered);
	var grainEnv = BufRd.ar(1, Latch.kr(envBuf, trigs), phase * BufFrames.kr(envBuf));

	RecordBuf.ar(grainEnv, ~recBuf, loop: 0);
}).add;
)


// run the Pmono for at least 1 sec
// save time and trouble: use Pfindur to stop it automatically
(
y = Pfindur(1, Pmono(\pulseDividerRec,
	
	\freq, 440,
	
	\bufIndex, Pseq([1, 0], inf),
	\envBuf, Pfunc{ |ev| ~customEnvs[ev[\bufIndex]] },
	
	\dur, 0.1,
	\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
	\overlap, 1.00,
	
	\amp, 0.1,
	\out, ~recBus,
)).play;
)

~recBuf.plot;

sc-env-one-channel

Oddly, at this point, only the fourth channel is active.

That’s definitely not right. (So, this part of troubleshooting depends on knowing what is the expected result – which is, that the triggers are distributed, round robin, among the four channels, and each trigger produces an envelope within its channel. We aren’t seeing that, hence, there’s a bug, hence, dig deeper.)

So, back up a layer, to phase – change to RecordBuf.ar(phase, ~recBuf, loop: 0); and run the test again.

sc-phase-one-channel

… and the phase is active only in the fourth channel, and only one every four triggers. (So the repeated envelopes are coming from BufRd loop = 1 – actually that’s another bug – you don’t want BufRd to loop!)

But if you check trigs and hasTriggered this way, they are behaving correctly.

Pull out the Sweep rates, and run it again:

(
SynthDef(\pulseDividerRec, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var maxOverlap = 4;
	var grainRate = tFreq / \overlap.kr(1);
	
	var trigs = PulseDivider.ar(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.ar(trigs) > 0;
	var rates = Latch.kr(grainRate, trigs) * hasTriggered;
	var phase = Sweep.ar(trigs, rates);
	var grainEnv = BufRd.ar(1, Latch.kr(envBuf, trigs), phase * BufFrames.kr(envBuf));

	RecordBuf.ar(rates, ~recBuf, loop: 0);
}).add;
)

Only the 4th channel rises to 10.

At this point, I was stumped for a while, until realizing that you have audio rate triggers and kr Latches. If a trigger occurs in the middle of a control block (there’s a 63/64 probability of this!) then Latch.kr won’t respond.

So when using ar inputs, Latch needs to be fully ar, all the way (including every input).

But – and here’s the bad news – BufRd grabs its buffer number only at control rate. To sequence envelope buffers, the buffer number needs to switch exactly at the moment of the trigger, which might occur in the middle of a control block.

So, with this approach, you would be limited to control rate triggers. This might be acceptable if you reduce the block size: 64/44100 is about 1.45 ms, but 16/44100 is about 0.36 ms, where jitter would be undetectable to the ear.

(
SynthDef(\pulseDividerRec, { |envBuf|

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.kr(tFreq);
	var maxOverlap = 4;
	var grainRate = tFreq / \overlap.kr(1);
		
	var trigs = PulseDivider.kr(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.kr(trigs) > 0;
	var rates = Latch.kr(grainRate, trigs) * hasTriggered;
	var phase = Sweep.ar(trigs, rates);
	var envBufNums = Latch.kr(envBuf, trigs);
	var grainEnv = BufRd.ar(1, envBufNums, phase * BufFrames.kr(envBuf), loop: 0);

	RecordBuf.ar(grainEnv, ~recBuf, loop: 0);
	// envBufNums.poll(trigs);
}).add;
)

sc-env-kr-trigs-ok

Syncing Impulse.kr with the language clock is likely to be a problem, so I’d suggest passing in the trigger from the pattern.

(
SynthDef(\pulseDivider, { |envBuf|

	var trig = \trig.tr(0);
	var tFreq = \tFreq.kr(1);
	var maxOverlap = 4;
	var grainRate = tFreq / \overlap.kr(1);
	
	var trigs = PulseDivider.kr(trig, maxOverlap, (0 .. maxOverlap-1));
	var hasTriggered = PulseCount.kr(trigs) > 0;
	var rates = Latch.kr(grainRate, trigs) * hasTriggered;
	var phase = Sweep.ar(trigs, rates);
	var envBufNums = Latch.kr(envBuf, trigs);
	var grainEnv = BufRd.ar(1, envBufNums, phase * BufFrames.kr(envBuf), loop: 0);
	
	var sig = SinOsc.ar(Latch.kr(\freq.kr(440), trigs));
	
	sig = (sig * grainEnv).sum;
	
	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.10));
	Out.ar(\out.kr(0), sig);
}).add;
)

// sequencing of two shapes with pulsedivider
(
y = Pmono(\pulseDivider,
	
	\freq, 440,
	
	\trig, 1,  // not optional
	\bufIndex, Pseq([1, 0], inf),
	\envBuf, Pfunc{ |ev| ~customEnvs[ev[\bufIndex]] },
	
	\dur, 0.4,
	\tFreq, Pfunc{ |ev| ev[\dur].reciprocal },
	\overlap, 0.5,
	
	\amp, 0.1,
	\out, 0,
).play;
)

y.stop;

Indeed, this was a hard one, crashing up against a hidden assumption in the buffer UGens (that you won’t switch buffers very often). I assumed you could switch buffers freely :flushed:

But with a small block size, you might get acceptable results. That is, there’s a trade-off: sample accurate triggers or more control over the signal being granulated, but at present, there’s no way to have both at the same time. (Edit: After stepping away from the computer, I realized you could also have one BufRd per envelope shape times max overlaps, and multiplex the triggers to the right BufRd for this cycle. Then there is no need to change the bufnum for any of the BufRd units = kr problem gone. But I’m out of time for today, won’t be able to write that code just now.)

hjh

thank you for the detailed explanation and the ideas for troubleshooting.

okay, i understand that.

okay, this sounds like a really smart solution, which i think im not able to implement by myself.

thanks you very much, so much to learn in here :slight_smile:

ive thought about this once more without beeing able to implement the multiplex approach. but if I still need to sample and hold the grain parameters one would not be able to implement frequency modulation per grain or? I think what im after is buffer sequencing for the grain envelope and frequency modulation per grain via buffered signals if possible also sequenceable. sorry that this is just coming out one after the other, its a complicated topic for me.

ive been trying to work on the multiplex approach and can sequence both shapes now without any discontinuities. EDIT: its also looking good on the plot:

is this correct and what could be adjusted? using \trig.tr for the correct timing is bothering me a bit because you cant additionally modulate tFreq with an LFO then.

(
// custom granular envelopes
~getCustomEnvs = {
	var tri = Env([0, 1, 0], [1, 1], \lin);
	var perc = Env.perc(0.01, 1, 1, -4);
	var customEnvs = [tri, perc];
	customEnvs.collect{|env| Buffer.sendCollection(s, env.discretize(2048), 1) };
};
~customEnvs = ~getCustomEnvs.();
)

(
SynthDef(\pulseDivider, {

	var trig = K2A.ar(\trig.tr(0));
	var tFreq = \tFreq.kr(1);
	//var trig = Impulse.ar(tFreq);
	var grainRate = tFreq / \overlap.kr(1);

	var maxOverlap = 4;
	var envBufNums = 4;
	var envBufs = \envBufs.kr(Array.fill(envBufNums, 0));

	var sig = Array.fill(maxOverlap, { |i|

		var localTrig, hasTriggered;
		var phase, grainEnvs, grainEnv;

		localTrig = PulseDivider.ar(trig, maxOverlap, i);
		hasTriggered = PulseCount.ar(localTrig) > 0;

		phase = Sweep.ar(localTrig, grainRate * hasTriggered);

		grainEnvs = envBufs.collect { |envBuf|
			BufRd.ar(1, envBuf, phase * BufFrames.kr(envBuf), loop: 0, interpolation: 4)
		};

		grainEnv = Select.ar(\localEnvBufIndex.kr(1), grainEnvs);

		sig = SinOsc.ar(\freq.kr(440));

		sig = sig * grainEnv;

	}).sum;

	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.10));
	Out.ar(\out.kr(0), sig);
}).add;
)

// sequencing of two shapes with pulsedivider
(
y = Pmono(\pulseDivider,

	\trig, 1,
	\freq, 440,

	\localEnvBufIndex, Pseq([0, 1], inf),

	\bufIndex, [0, 1],

	\envBufs, Pfunc{ |ev|
		~customEnvs[ev[\bufIndex]]
	}.collect(`_),

	\dur, 0.5,
	\tFreq, 1 / Pkey(\dur),
	\overlap, 0.5,

	\amp, 0.1,
	\out, 0,
).play;
)

y.stop;

Forgot to chime in on this – if the triggers are tr or kr, then you could just use the simple BufRd approach (because the buffer number can be changed at controller).

The whole point of running all of them and Select-ing at audio rate was to support audio rate triggers. So you should be able to reintroduce trig = Impulse.ar(tFreq);.

Note that this comment – “Syncing Impulse.kr with the language clock is likely to be a problem” – applies to control rate, but it is much less of a problem for audio rate!

I guess you’ve taken two solutions and conflated them – since BufRd’s buf input is control rate only, then there are two directions to go:

  1. All kr – kr triggers, kr bufnum control etc.
    • In this case, multiplexing is not necessary! So if you’re putting multiplex logic into this approach, then it’s wasting effort.
    • For sync, triggers should be received from outside. (Why? 44100/64 is not an integer, so at 44.1 kHz, Impulse.kr will drift.)
  2. Or: kr tFreq, with Impulse.ar for the triggers.
    • This approach needs multiplexing.
    • With Impulse.ar, trigger timing is likely to be more accurate than at kr, so there is no point to \trig.tr here.
    • You could also choose envelope bufnums using Demand units.

hjh

hey, thanks for you reply, these are good news.

ive swapped the \trig.tr vs Impulse.ar again, without the Demand Ugens its still sliding out of time.
So i would like to implement the Demand switching of buffers:

(
// custom granular envelopes
~getCustomEnvs = {
	var tri = Env([0, 1, 0], [1, 1], \lin);
	var perc = Env.perc(0.01, 1, 1, -4);
	var customEnvs = [tri, perc];
	customEnvs.collect{|env| Buffer.sendCollection(s, env.discretize(2048), 1) };
};
~customEnvs = ~getCustomEnvs.();
)

(
SynthDef(\pulseDivider, {

	var tFreq = \tFreq.kr(1);
	var trig = Impulse.ar(tFreq);
	var grainRate = tFreq / \overlap.kr(1);

	var maxOverlap = 4;
	var envBufs = \envBufs.kr(#[1,1,1,1]);

	var sig = Array.fill(maxOverlap, { |i|

		var localTrig, hasTriggered;
		var phase, grainEnvs, grainEnv, envBufSelect;

		localTrig = PulseDivider.ar(trig, maxOverlap, i);
		hasTriggered = PulseCount.ar(localTrig) > 0;

		phase = Sweep.ar(localTrig, grainRate * hasTriggered);

		grainEnvs = envBufs.collect { |envBuf|
			BufRd.ar(1, envBuf, phase * BufFrames.kr(envBuf), loop: 0, interpolation: 4)
		};

		envBufSelect = Demand.ar(trig, 0, Dseq([Dser(\envBufSelect.kr(#[1,1,1,1]), \envBufMod.kr(2))], inf));
		grainEnv = Select.ar(envBufSelect, grainEnvs);

		//grainEnv = Demand.ar(trig, 0, Dswitch1(grainEnvs, \envBufSelect.kr(0)));

		sig = SinOsc.ar(\freq.kr(440));

		sig = sig * grainEnv;

	}).sum;

	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.10));
	Out.ar(\out.kr(0), sig);
}).add;
)

(
y = Pmono(\pulseDivider,

	\freq, 440,

	\envBufSelect, [0, 1],
	\envBufIndex, [0, 1],
	\envBufs, Pfunc{ |ev| ~customEnvs[ev[\envBufIndex]] }.collect(`_),

	\dur, 0.5,
	\tFreq, 1 / Pkey(\dur),
	\overlap, 0.5,

	\amp, 0.1,
	\out, 0,
).play;
)

My first attempt works without any timing issues (taken trig instead of localTrig here, otherwise you get maxOverlaps times the first buffer and then maxOverlaps times the second Buffer).

Is it also possible to implement it with Dswitch1 or an easier to use solution?
The first solution is a bit complicated to handle, you have to track \envBufSelect, \envBufMod and also \envBufs and you are also fixed to a specific Dseq sequence you wouldnt be able to repeat the first buffer x-times and then the second buffer x-times like you normally could like this: Pdup(4, Pseq([0, 1], inf)) if you like to.

thanks