Sweeping through a Buffer in sync with Tempo

Hey. Kind of new to SuperCollider, sorry if the question is silly.

I’m experimenting with granular synthesis in order to read and mangle a loop contained in a buffer.

Basically, the idea is to read the buffer with a BufGrain, sweep through the buffer by moving the pos parameter and add some noise to the pos for some jitter. Here is what I’ve got so far:

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

SynthDef(\kali, { |out=0, buf, tempo=1, rate=1, scale=1, density=10, scatter=0, mul=1, add=0|
	Out.ar(out,
		Pan2.ar(
			BufGrain.ar(
				trigger: Impulse.kr(density),
				dur: 1/density * 2,
				sndbuf: buf,
				rate: rate,
				pos: Sweep.kr(Impulse.kr(tempo/(scale*4)), tempo/(scale*4)) + PinkNoise.kr(mul:scatter),
				mul: mul,
				add: add,
			)
		)
	)
}).add;

a = Synth(\kali, [
	\buf, b,
	\rate, 1,
	\density, 10,
	\scatter, 0,
	\tempo, 1,
	\scale, 2,
	\rate, 1,
]);
)

This code does the trick, but here is the thing: the loop playback should be synchronized with a global tempo, and reading here and there, I suspect my synth will drift out of sync sooner or later.

I think there is an approach by moving the “sweep” code out of the synth, and pass it through a parameter pos, like this:

SynthDef(\grain, { |out=0, buf, pos=0, rate=1, density=10, scatter=0, mul=1, add=0|
	Out.ar(out,
		Pan2.ar(
			BufGrain.ar(
				trigger: Impulse.kr(density),
				dur: 1/density * 2,
				sndbuf: buf,
				rate: rate,
				pos: PinkNoise.kr(mul:scatter, add:pos),
				mul: mul,
				add: add,
			)
		)
	)
}).add;

Now, here is where I’m stuck: how do I generate a stream of pos value in sync with a TempoClock? I guess it must be something with Routine or Task, but I’m having troubles wrapping my head around this one…

I’d recommend keeping the “phasor” that moves the playback position around on the server and just retriggering it from the client, so you’d keep the Sweep ugen and replace the Impulse that’s triggering it now with a control:

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

    SynthDef(\kali, { |out=0, buf, t_trig, tempo=1, rate=1, scale=1, density=10, scatter=0, mul=1, add=0|
	Out.ar(out,
            Pan2.ar(
                BufGrain.ar(
                    trigger: Impulse.kr(density),
                    dur: 1/density * 2,
                    sndbuf: buf,
                    rate: rate,
                    pos: Sweep.kr(t_trig, tempo/(scale*4)) + PinkNoise.kr(mul:scatter),
                    mul: mul,
                    add: add,
                )
            )
        )
    }).add;
)

Then you’d create the Synth as before:

(
    a = Synth(\kali, [
	\buf, b,
	\rate, 1,
	\density, 10,
	\scatter, 0,
	\tempo, 1,
	\scale, 2,
	\rate, 1,
    ]);
)

There are several ways to set the \t_trig control from the client. One of the simplest would be a Routine:

(
    r = Routine.new({
        inf.do({
            a.set(\t_trig, 1);
            1.yield // interval in beats between triggers
        })
    }).play(quant: 1) // begin triggering on beat, play on TempoClock.default unless another Clock is specified
)

r.stop // stop retriggering

I hope that gives you enough to get started with. Two other quick notes:

  1. The name t_trig is a special form that creates a “trigger rate” Control. See the SynthDef helpfile for more information. As an alternative to declaring t_trig in the arg list, you can create a NamedControl directly in the body of the SynthDef function like this: \trig.tr. There’s a great explanation of why you might want to do this in this thread: NamedControl: a better way to write SynthDef arguments, but feel free to stick with whatever approach works for you.

  2. As I said above, there are many different ways of sending triggers (and of setting the other parameters of your Synth) from the client. If you want to use Events and Patterns to control this or any other Synth, you’ll need to watch out for certain reserved Symbols (including 'tempo' and 'scale'!) that have special properties in Events and can cause issues if you use them as control names in a SynthDef. These are listed in Event’s helpfile.

Thanks a lot, that was very helpful! the t_trig special naming is a real gotcha …

I agree! That’s why I recommended NamedControl – I think that arg lists can get awkward quickly once you’ve got a lot of controls of different rates in a SynthDef.

yes, I’m reading the NamedControl thread and it looks definitely cleaner.

If you don’t need your time values to be continuous or very accurate, you can just use a pattern to set the value directly into a bus, and then read that bus on the server:

(
~ktime = ~ktime ?? { Bus.control(s, 2) };

Pdef(\time, Pbind(
	\tempo, 1.1,
	\dur, 1/4,      // whatever granularity you want to set your clock time...
	\time, Pseries(0, Pkey(\dur)),
	\buffer, ~ktime,
	\play, {
		~buffer.set(~time.postln);
	}
)).play;

Ndef(\test, {
	SinOsc.ar(In.kr(~ktime).poll * 50);
}).play;
)

If you DO need it to be continuous and accurate, it’s harder… the TempoClock and your audio clock are, by their nature, not perfectly synchronized. They WILL drift over longer periods of time, so you have to account for this. Here’s something I adapted from a slightly more crude version I had floating around - this was a little tricky, but should provide an Ndef on the server that is accurately synced with the times provided by a pattern (in this case, coming directly from TempoClock via Ptime). I hope the comments make this example somewhat clear, but please feel free to ask questions!

(
~time = ~time ?? { Bus.audio(s, 2) };

SynthDef(\setTime, {
	var time, tempo, offset;
	
	// Interpret each as a single sample trigger
	time = T2A.ar(\time.tr(-999), 0);
	tempo = T2A.ar(\tempo.tr(0), 0);
	
	// remove our synth as soon as possible
	Line.kr(0, 1, ControlDur.ir, doneAction:2);

	// OffsetOut, so we have perfect-ish time accuracy inside the block size....
	OffsetOut.ar(\out.ir, [time, tempo]);
}).add;


Ndef(\tempoClock, {
	var time, tempo, changed, serverTime;
	
	// Read time and tempo triggers
	#time, tempo = In.ar(~time, 2);

	// Interpret non-zero tempo as a trigger..
	changed = tempo > 0;
	
	// Latch time and tempo when triggered
	time = Latch.ar(time, changed);
	tempo = Latch.ar(tempo, changed);
	
	// Time is always based on last time, counting up with a phaser
	time = time + Phasor.ar(
		changed,
		tempo * SampleDur.ir,
		0, inf,
	);
	
	// Calculate the same time purely using the audio clock (e.g. no TempoClock updates)
	// This is likely to drift over long periods of time
	serverTime = Phasor.ar(
		Trig.ar(changed, inf),
		tempo * SampleDur.ir,
		0, inf
	);
	
	time.poll(8, label:"tempo time");
	serverTime.poll(8, label:"server time");
	((serverTime - time) * SampleRate.ir).poll(8, label:"drift samples".padLeft(11));
	
	time;
});

Pdef(\time, Pbind(
	\instrument, \setTime, 
	\group, Ndef(\tempoClock).group, 
	\addAction, \addBefore,
	\out, ~time,
	
	\dur, 1/4,    		// effectively: how often do we resync the clock
	\tempo, 1,    		// changing this should work - even dynamically!
	\time, Ptime()
)).play;

~buffer = Buffer.read(Server.default, Platform.resourceDir +/+ "sounds/a11wlk01.wav", action:{
	Ndef(\audioTest, {
		[1,1] * BufRd.ar(1, ~buffer, Ndef(\tempoClock).ar(1) * SampleRate.ir, loop:1)
	}).play
});

)



1 Like

thanks for your answer, this is clearly touching on concepts I’m not familiar with … yet. I’ll spend some time dissecting all that!