Trouble with polyrhythmic granular sampler

Hello all,

I am new to SC, and trying to build a polyrhythmic granular sampler. I feel like I have hit a conceptual wall and no number of tutorials or support documents is helping me get past it.

I am designing a sampler that uses at least two voice. I want the voices to synchronise and desynchronise using simple polyrhythmic ratios. e.g., voice 1 lasts 0.02ms and voice 2 lasts 0.03ms. However I also want these to be able to change according to a predefined sequence (or composition) and am trying to use patterns to achieve this. So for example voice 1 could change from 0.02ms to 0.04ms.

My first approach was to use BufRd.ar and have it controlled by Phasor.ar, allowing me to loop particular grains in the sample. The conceptual problem I hit with this is when I try to make changes to the length of the loop using Pbind it creates a new instance of Phasor, and I just start layering buffers on top of each other. Phasor obviously doesn’t have a doneAction, and this makes me feel that perhaps this is a misuse of this Ugen.

The second approach I have thought of it is not to loop at all but to use Line.ar instead, and use Pbind along with Pdup to create a rapid succession of Synthdefs. My concern is this somehow ‘feels’ wrong, and I wonder if it might create timing errors.

That leads me to my third design challenge which is when I have one voice functioning, how to make sure that it accurate synchronises with a second voice. The timing is essential in this design because both loops have to realign at 0 in their polyrhythmic cycles.

Here is a version of the code I have using the Phasor:

(
SynthDef.new (\voice, {
arg amp=1, out=0, buf, start, end, rate=1, dur;
var sig, ptr;
ptr = Phasor.ar(0, BufRateScale.kr(buf)*rate, start, end);
// ptr = Line.ar(0, end, dur, doneAction:2);
sig = BufRd.ar(2, buf, ptr);
sig = sig * amp;
Out.ar(out, sig);
}).add;
)

(
Pbind(
\instrument, \voice,
\buf, ~b0.bufnum,
\dur, Pseq([1, 1, 1, 1], 4),
\amp, 0.3,
\start, 0,
\end, Pseq([~b0.numFrames-1/32, ~b0.numFrames-1/64, ~b0.numFrames-1/128, ~b0.numFrames-1/246], 4),
\rate, 1).play;
);

Thank you in advance for any help, and thank you in general to the community who have created such an inspiring piece of software.

Dom

hey,
one observation:
i think the granulation part is missing in your SynthDef.
Phasor and BufRd in this configuration are just capable of playing back a soundfile.

What you want to do for granulation instead is: while Phasor reads through the soundfile from 0 to frames in the buffer with a specific posRate, to have a trigger signal, often times Impulse.ar(tFreq) which should produce a grain by applying a short envelope (often a hanning window) to the current segment of sound in the buffer. The duration of this short segment, the grain duration is often calculated by grainDur = overlap / tFreq. These short segments should then be played back at a specific rate.
Different settings for tFreq, rate and posRate introduce pitchshifting, timestretching and filtering of the soundfile while modulating these parameters can result in flanging, phasing and reverb effects. see this https://www.youtube.com/watch?v=n0Z3Cakh3Iw&t=306s

There are several approaches to do granular synthesis.
One of the more straight forward ones would be to use GrainBuf instead of BufRd.

You could have a look at the GrainBuf helpfile and especially this tutorial will get you started:

1 Like

Hi dietcv,

Thanks for your reply. Perhaps granular is not the ideal way to describe the sampler I’m trying to create, as I would also like the samples to become a size where they would not be considered grains e.g., 500ms. What I mean by granular is this sampler has the means to operate at the ‘Microsound’ level with a high level of timing accuracy.

I’ve been following all Eli’s tutorials which are fantastic.

Many thanks!

i think this would probably just a matter of choosing the appropriate values for controlling the Granulator.

i think to compose rhythmically on a microlevel you could for example either use Demand rate Ugens to form microrhytmical gestures often called streams or clouds and sequence these clouds with patterns on a macro level or for example use recursive Phrasing which also follows the logic of micro and macro structure.

Here you find some other strategies for Buffer Granulation

But maybe someone else has a more straight forward approach which suits your needs.

Ive also found this discussion Rhythmically skipping through a recording, audio probing

2 Likes

Hi dietcv,

Thanks again for such a helpful reply and the further resources you’ve provided. I’ve only had a chance to glance through them, but will study them in greater depth and process them in the coming days.

I might be able to refine my questions slightly as well, because I’m still seeking as simple a solution as possible.

  1. Is there a way to change the Phasor arguments (and the end in particular in this case) without instantiating another Phasor, and therefore creating layers? I have a feeling this question is fundamentally at odds with Supercollider’s architecture, and therefore the is a deeper misunderstanding in my conception in the working use of Pbind/Synthdefs.

  2. Is there a way to make multiple Pbinds synchronised so that the timing between them is locked together. E.g., a global clock governs them.

My current thinking is that the BufRd can work for me if I can accurately get the Phasor to change its “end” values. So for instance if I wanted 10 instances of a 0.03ms loop I just need to change the setting once after 0.3 seconds. Providing this is done accurately the only down side I can see to this is clicks.

I hope this makes sense, and thanks again to anyone reading this :slight_smile:

Yes, of course the parameters can be modulated. SuperCollider would be not at all usable if this were impossible.

To change the rate by a server command, see the set method for Node.

hjh

1 Like

One thing I’ll note - the Grain UGens work perfectly fine with large grains. It really is just triggered, windowed playback. Want 60 second grains with a custom envelope? GrainBuf can do that.

/*
Josh Parmenter
www.realizedsound.net/josh
*/

2 Likes

Thanks Jamshark,

I’ve used set previously, but not in the context of Pbind. This is what it causing the confusion for me. For the parameters to be changed automatically and using precise durations I am using Pbind. So my understanding is that the set messages must be triggered within Pbind to change the SynthDef. Yet I can’t find any examples that exactly match this criteria (e.g., each of these examples is somewhat different Pattern Guide 03: What Is Pbind | SuperCollider 3.11.2 Help) which makes me think my approach is wrong.

For instance in the example I shared about I would like each second to have the length of the buffer ‘end’ changed. I can’t work out how this would be coded, or if it’s possible in the way I have described.

Sorry if the question is basic, I’ve been using SC for under two weeks. I really love it, and want to get better, but feel there’s some misconception holding me back here.

Thank you everyone for your help and patience!

Thanks Josh, I didn’t realise it could handle such large Grains. I will take a look into it again. Helpful tip :+1:

Ah OK, I missed that.

There are a few issues here.

One is that looping a buffer segment by Phasor → BufRd is pretty much always an incorrect approach, because of clicks at the loop point. Everybody tries this at first, but it remains a fact that there is no sound in nature that transitions instantaneously – therefore, every sound that you produce electronically must have an envelope. This includes loop segments. No envelope at the transition = not quite right audio engineering.

IMO the minimum that is needed for looping looks like this:

s.boot;

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

(
a = { |bufnum, atk = 0.001, dcy = 0.001,
	start = 0,  // samples
	end = 44100,
	rate = 1,
	amp = 0.1|
	
	var bufsr = BufSampleRate.kr(bufnum);
	var loopFreq = bufsr / (end - start);
	var gate1 = LFPulse.ar(loopFreq * 0.5);  // 0 .. 1
	var gates = [gate1, 1 - gate1];
	var envelopes = EnvGen.ar(Env.asr(atk, 1.0, dcy, [-3, 3]), gates);
	var phases = (Sweep.ar(gates, rate * BufRateScale.kr(bufnum) * bufsr)
		+ start);
	
	var sig = (
		BufRd.ar(1, bufnum, phases, loop: 0, interpolation: 4)
		*
		envelopes
	).sum;
	
	(sig * amp).dup
}.play(args: [bufnum: b, start: 44100, end: 66150]);
)

But Josh’s suggestion might be easier.

~~

With Pbind in general, the first thing is to decide whether you want to spawn new synths for every event, or modify existing ones.

So the problem is that you’re using a new-synth Pbind when (I guess…?) you don’t want new synths.

Another problem with your Pbind usage is that the event must be able to stop the synth. Since you have designed your SynthDef with no overall volume envelope, the event can’t do that and you will get synth pile-up.

(
SynthDef(\oneLoop, { |out, gate = 1, bufnum, atk = 0.001, dcy = 0.001,
	start = 0,  // samples
	end = 44100,
	rate = 1,
	amp = 0.1|
	
	// pretty much every synth you ever write (except control rate)
	// will need to have an overall volume envelope like this;
	// get in the habit now. Do not omit this!
	var eg = EnvGen.kr(Env.asr(0.001, 1, 0.01), gate, doneAction: 2);
	
	var bufsr = BufSampleRate.kr(bufnum);
	var loopFreq = bufsr / (end - start);
	var gate1 = LFPulse.ar(loopFreq * 0.5);  // 0 .. 1
	var gates = [gate1, 1 - gate1];
	var envelopes = EnvGen.ar(Env.asr(atk, 1.0, dcy, [-3, 3]), gates);
	var phases = (Sweep.ar(gates, rate * BufRateScale.kr(bufnum) * bufsr)
		// oh my, there is an unfortunate detail here
		// don't feel like explaining it today
		+ Latch.ar(K2A.ar(start), gates));
	
	var sig = (
		BufRd.ar(1, bufnum, phases, loop: 0, interpolation: 4)
		*
		envelopes
	).sum;
	
	Out.ar(out, (sig * (amp * eg)).dup);
}).add;
)

(
p = Pmono(\oneLoop,
	\bufnum, b,
	\start, Pwhite(2000, 50000, inf),
	\end, Pkey(\start) + (Pwhite(1, 10, inf) * 4410)
).play;
)

hjh

3 Likes

Hi jamshark,

I can’t thank you enough for this response, and the code you have shared. This is precisely the problem I was facing logistically and conceptually…

I’m working through your code. It almost 100% makes sense to me, but I need to get adjusted to it a bit more. The tutorial series as well is great, and I wasn’t quite up to that point of the tutorial, so good to know it’s covered.

I think I’ve got to the heart of the matter now, though it will take a few days or weeks to really process it all. Thanks to everyone who has responded to this thread - it really made my week. Hopefully on my way now!