Strategies to loop/timestretch audio samples in Patterns

Hi everyone,

I find very little documentation about playing live audio loops while live coding using patterns. I’m thinking about playing/slicing amen breaks, rhythmic samples with different BPMs, etc. As an example, see this tutorial. I know how to do that using other coding environments that depend on SuperCollider but I don’t see an obvious way to do it easily using JITLib/SCLang only (minimal code, maximum efficiency). I am sure that many different approaches are possible but I would like to learn more about them.

Hi,

you can take a look at miSCellaneous_lib’s Buffer Granulation tutorial. Chapter “2. Granulation driven by language”, Ex. 2a, covers time-stretching with Pbind. I’m using PLx proxy patterns in order to have easy GUI control. You could instead use Pdefn, live coding is possible with both variants – see “PLx suite” and “PLx and live coding with Strings”.
Concerning time-stretching in combination with Pbind: In Ex.2a, the position is calculated in the language, this might cause unwanted jumps when the region is changed. An alternative and maybe better – because more flexible – method is described in Ex.3b, where the position in the buffer is controlled by a separate LFO.
Concerning slicing/rolls, I’m adding some examples from my sound synthesis course which employ Pdup and need no extension installed. No time-stretching and live-coding yet built in here but that can easily be done with the techniques mentioned before.

Hope that helps, best

Daniel



// fixed-length Envelope

b = Buffer.read(s, Platform.resourceDir +/+ "sounds" +/+ "a11wlk01-44_1.aiff");

b.play


(
SynthDef(\playBuf_1, { |out, buf, att = 0.01, rel = 0.5, rate = 1,
	amp = 0.1, pos = 0.5, pan = 0|

	var sig = PlayBuf.ar(1, buf, rate * BufRateScale.kr(b), startPos: pos * BufFrames.kr(b));
	var env = EnvGen.ar(Env.perc(att, rel, amp), doneAction: 2);

	OffsetOut.ar(out, Pan2.ar(sig * env, pan))
}).add
)

(
// template
p = Pbind(
	\instrument, \playBuf_1,
	\buf, b,
	\dur, Pexprand(0.005, 0.5),
	// release depending on duration of Events
	\rel, Pkey(\dur) * 1.5,
	\rate, Pwhite(0.2, 2),
	\pos, Pwhite(0.3, 0.7),
	\pan, Pwhite(-0.8, 0.8),
	\amp, 2
);

// play with Pdup
q = Pdup(Pwhite(1, 4), p).play
)

q.stop

// chords

(
// template
p = Pbind(
	\instrument, \playBuf_1,
	\buf, b,
	\dur, Pexprand(0.005, 0.5),

	// release depending on duration of Events
	\rel, Pkey(\dur) * 1.5,
	\rate, Pwhite(0.5, 2) * [0.6, 0.75, 1, 1.3],
	\pos, Pwhite(0.3, 0.8),
	\pan, Pwhite(-0.8, 0.8) * [-1, 1],
	\amp, 1
);

// play with Pdup
q = Pdup(Pwhite(1, 4), p).play
)

q.stop


// sustained Envelope (Env.asr)


(
SynthDef(\playBuf_2, { |out, buf, att = 0.01, rel = 0.5, gate = 1, rate = 1,
	amp = 0.1, pos = 0.5, pan = 0|

	var sig = PlayBuf.ar(1, buf, rate * BufRateScale.kr(b), startPos: pos * BufFrames.kr(b));
	var env = EnvGen.ar(Env.asr(att, amp, rel), gate, doneAction: 2);

	OffsetOut.ar(out, Pan2.ar(sig * env, pan))
}).add
)

(
// template
p = Pbind(
	\instrument, \playBuf_2,
	\buf, b,
	\dur, Pexprand(0.005, 0.4),
	// release immer kurz
	\rel, 0.01,

	// legato determines when gate 0 is sent (default = 0.8)
	\legato, Pwhite(0.1, 1.7),

	\rate, Pwhite(0.2, 2) * Pfunc { [1, rrand(1.1, 1.7)] },
	\pos, Pwhite(0.3, 0.7),
	\pan, Pwhite(-0.8, 0.8) * [-1, 1],
	\amp, 1
);

// play with Pdup
q = Pdup(Pwhite(1, 4), p).play
)


q.stop

Hi! I don’t think that it entirely solves the problem. However, I’ve learned quite a lot about miSCellaneous_lib and I might use it for granulation! Impressive work, it sounds quite nice. It might be helpful to describe my specific setup a little more. I am hacking a very terse syntax for live improvisation with audio samples. My current non-functional audio looper is invoked like this:

// All the following is running in ProxySpace.push(s.boot)

// Starts looping the amen1 sound from sample library
~test == [sp: "amen1", nb: 0, db: 0, dur: 8];

// Adding a fixed lowpass filter on the given sound
~test.fx1(1, {arg in; LPF.ar(in, 1500)});

I am using quite a lot of operators (>>, ->, =>) added to NodeProxy to handle different flavours of Pbind (Pmono, simple sampling, loop sampling). Under the hood, this system is using a specific type of Event that handles sample allocation, defines the quant, deals with syntax shortcuts, etc.

/* Audio Looper (sample playback) */
== {
  arg pattern;
  pattern = EventShortener.findShortcuts(pattern);
  pattern = pattern ++ [\type, \buboLoopEvent];
  this[0] = Pbind(*pattern);
  this.quant = pattern[pattern.indexOf('dur') + 1];
  // fadeTime is important for loops
  this.fadeTime = 2;
  this.play;
  ^this
}

The buboLoopEvent is defined like this:

      Event.addEventType(\buboLoopEvent, {
        arg server;
        if (~sp.notNil && ~nb.notNil, {
          ~sp = ~sp ?? 'default';
          ~nb = ~nb ?? 0;
          ~buf = Bank(~sp)[~nb % Bank(~sp).paths.size];
          if (Bank(~sp).metadata[~nb % Bank(~sp).size][\numChannels] == 1) {
              ~instrument = \looperMono;
          } {
              ~instrument = \looperStereo;
          };
        });
        ~type = \note;
        currentEnvironment.play;
      });

This is very hacky but more than enough for simple beat/rhythm oriented improvisations. I can manually deal with more complex use cases by using the regular SC syntax. The final piece in the equation is the SynthDef itself, borrowed and adapted from the tutorial mentioned above:

z = SynthDef(\looperStereo,
      {arg out, buf = 0;
          var sig,env ;
          sig = Mix.ar(
            PlayBuf.ar(
              2,
              buf,
              BufRateScale.ir(buf) * ((BufFrames.ir(buf) / s.sampleRate) * p.clock.tempo / \dur.kr(8)
            ),
            1,0,doneAction:2)
          );
          env = EnvGen.ar(
            Env.linen(
              0.0,
              \dur.kr,0
            ),doneAction:2
          );
          sig = sig * env;
          sig = sig * \amp.kr(-6.dbamp);
          OffsetOut.ar(out,Pan2.ar(sig,\pan.kr(0)));
  }).add;

I can play my amen breaks and the playback speed is determined by dur. So far, so good. However, there are some problems that make it unusable for live improv:

  • audio overlaps: the head and tail of the sample are somehow overlapping.
  • fadeTime can be quite ugly. It would be better to just cut the playback and start with a fresh sound, keeping the playback position between audio samples.

EventShortener and Bank are unknown, maybe they contained in a Quark or defined by yourself ?
So it’s difficult giving a concrete helping advice for your specific setup at that point.

Another option for breakbeats is retriggering a PlayBuf, which would avoid this issue. You can retrigger an envelope at the same time:

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff");

(
SynthDef(\retrig, {
	arg out = 0, buf = 0,  minStartPos = 0, maxStartPos = 9, 
		att = 0.01, gate = 1, rel = 1,
		rate = 1, amp = 0.5, loop = 1,
		rhy = #[0.4, 0.2, 0.2];
	var sig, trig, env, start, startPos = NamedControl(\startPos, 0 ! 10);
	
	trig = TDuty.kr(Dseq(rhy, inf));
	
	start = Demand.kr(
		trig,
		0,
		Dswitch1(startPos, Diwhite(minStartPos, maxStartPos))
	);
	env = EnvGen.ar(Env.asr(att, releaseTime: rel), trig);
	sig = PlayBuf.ar(
		numChannels: 1,
		bufnum: buf,
		startPos: start * BufFrames.ir(b) / BufDur.ir(b),
		rate: BufRateScale.kr(buf) * rate * [1, 1.001],
		trigger: trig,
		loop: loop
	);
	sig = sig * amp * env;
	Out.ar(out, sig);
}).add
)


(
x = Synth(\retrig, [	
	buf: b, 
	amp: 5.dbamp, 
	rate: 1, 
	att: 0.01,
	rel: 0.5,
	minStartPos: 1,
	maxStartPos: 7,
	startPos: (1..10) / 4,
	rhy: [2, 1, 1] / 10
]);
)

x.free


Hi, I have made some progress with some help from an experienced user :slight_smile: I have no overlaps anymore. Just need to get the slicing / jumping feature working and it’ll be alright!

I have not included them here because they are non-essential parts. Bank is just managing buffers automatically (loading a sample collection to be available globally) and EventShortener turns i into instrument, etc… The only thing to know is that I can summon buffers using Bank('amen1')[0]. The buboEvent type can also help with splitting that weird looking expression into [sp: 'amen1', nb: 0].

Here is the updated SynthDef, with… indeed, a new envelope!

SynthDef(\looperStereo,
      {
        arg out;
        var sig, env, index;
        index = Select.kr(\direction.kr(1) > 0, [\index.kr(0) + 1, \index.kr]);
        sig = PlayBuf.ar(
          2,
          \buf.kr(0),
          (BufRateScale.kr(\buf.kr) * (BufSamples.kr(\buf.kr)
          / \slices.kr(1) / BufSampleRate.kr(\buf.kr)) / \time.kr * \direction.kr) / 2,
          1, BufSamples.kr(\buf.kr) * (index / \slices.kr), doneAction: 0
        );
        env = EnvGen.ar(
          Env.asr(0.01, 1, 0.01), \gate.kr(1), doneAction: 2
        );
        sig = sig * env;
        sig = sig * \amp.kr(-6.dbamp);
        OffsetOut.ar(out,Pan2.ar(sig,\pan.kr(0)));
  }).add;

This new definition can handle playback direction and can probably handle jumping and slicing (slices and index) but I was not able to play with these arguments in a creative way while improvising.

The remaining issues I have are mostly tied to the EventStream and JITLib:

  • avoiding weird fade-in/fade-out when an expression is re-evaluated (setting fadeTime and quant is not enough). I would basically like to kill the synth to start fresh when the expression is re-evaluated. I probably need to come up with some id or reference system.
  • still some overlaps when playing too much with dur and quant (old playback still running, new sample starting on top of it).

EDIT: currently trying your SynthDef. Very interesting!