SynthDef crashes when reading from buffer with Phasor-based looper

Hi all,

I’m working on a SynthDef that reads from a buffer using a Phasor-based looping system. The idea is to define a loop start point and a loop length (in milliseconds), and have the Phasor read from that region inside the buffer.

The server crahes when i play around with the loopSize after starting an instance.

I’m new to SuperCollider, but experienced in other programming languages. But i can’t find the problem. Can someone help me debug this?

(
b = Buffer.readChannel(s, "sndfl.wav", channels: 1);
w = Buffer.loadCollection(s, Signal.hanningWindow(4096));
)
(
SynthDef(\sndflLooper, {
  arg buf, wbuf, speed = 1, loopSize = 100, loopStart = 0, bus = 0;

  // calculate loop size
  var bufSR = BufSampleRate.kr(buf);
  var bufFrames = BufFrames.kr(buf);
  //bufSizeInSecs = BufDur.kr(buf);
  var loopSizeInSecs = loopSize / 1000;
  var loopSizeInSamps = max(loopSizeInSecs * bufSR);
  // calculate phasor increment
  var phsIncr = (1 / loopSizeInSamps) * speed;

  // main phasor
  var phasor = Phasor.ar(0, phsIncr, 0, 1);
  var delta = HPZ1.ar(phasor);
  var startTrig = Impulse.ar(0);
  var trig = (startTrig + (delta < 0)) > 0;
  var pulseTrig = Trig1.ar(trig, SampleDur.ir);
  
  // set loop start and end points
  var startNew = loopStart * bufFrames;
  var start = Latch.ar(startNew, pulseTrig);
  var loopEndNew = loopStart + loopSizeInSamps;
  var loopEnd = Latch.ar(loopEndNew, pulseTrig);
  
  // read sndfl buf
  var sndflBufSig = BufRd.ar(1, buf, start + (phasor * (loopEnd - start)), loop: 1, interpolation: 4);

  // read wndw buf
  var sizeWndwBuf = BufFrames.kr(wbuf);
  var wndwBufSig = BufRd.ar(1, wbuf, phasor * sizeWndwBuf);

  var snd = sndflBufSig * wndwBufSig;
  Out.ar(bus, snd!2)
}).add;
)


~looper = Synth(\sndflLooper, [\buf, b, \wbuf, w, \loopSize, 100]);
~looper.set(\loopSize, 50);

I haven’t tested but what stands out is that the 4th arg of Phasor is in samples (frames), not in seconds so I think you need to do 1 * SampleRate.ir.

Maybe i get you wrong, but the 4th arg is describing the final value of the phasor? Since i need a phasor from 0 to 1, this needs to be 1?

It describes the final frame of the phasor. Try comparing these examples:

{ Phasor.ar(0, 1, 0, 1).poll; Silence.ar()}.play

{ Phasor.ar(0, 1, 0, 1 * s.sampleRate).poll; Silence.ar()}.play

Yes, but this is also a question of the Phasor increment value and since i do this:

  var loopSizeInSamps = loopSizeInSecs * bufSR;
  // calculate phasor increment
  var phsIncr = (1 / loopSizeInSamps) * speed;

It should be fine with a phasor range from 0 to 1?

Ah I see, I just tried you example now and it does work for me without crashing. I am not quite sure what you are wanting to do here, if I set the loopSize to the entire buffer (b.bufFrames), should it then play the whole sample (I used a different sample, but that should not matter)? In this case I don’t hear sound at all, I haven’t really checked the code yet, first I wanted to hear what you are trying to obtain.

loopSize expects a value in millieseconds. so, if you want to play the whole buffer you need to use b.duration * 1000. But then there is also the windowing function which is read once per phasor cycle, and with loop sizes this big you will have a lot of low sample values with a hanning window. this could explain the silence. The instrument is designed for smaller loops like under a second.

Could you play with different values for the loop size, under 2000ms and see if it’s crashing?

To be clear:

The crashing is happening at random times. it happens when i do ~looper.set(\loopSize, 50); with different values a several times.

Yes now I could get it to break. I don’t have time to check the math right now but in these cases using buffers, crashes are almost always due to indexing outside the buffer, so maybe check your math or put in some safeguards. When I did this safeguard I could not get it to break:

var sndflBufSig = BufRd.ar(1, buf, (start + (phasor * (loopEnd - start))).min(bufSR - 1), loop: 1, interpolation: 4);

However, some combos of setting the loopSize results in no sound and no sound no matter how I change the loopsize afterwards which probably has to do with your latching mechanism.

1 Like

startNew is not Audio rate, try using K2A.ar before pluggin into Latch.ar

1 Like

I guess this was it!

You can probably also just sample and hold the entire phase or the Bufrd, without having to convert to Audio rate and get rid of one of the Latch Ugens.

hey, you could check out this basic granulation attempt (make sure you got the latest update here for the necessary functions: A collection of functions for sub-sample accurate granulation and A collection of unit shapers).
The length of the buffer is normalized between 0 and 1.
You can change the looping segment with posLo and posHi.

Just for testing:

with \posLo, 0.50 and \posHi, 0.72:

(
SynthDef(\grains, { |sndBuf|

	var numChannels = 8;

	var reset, tFreqMD, tFreqMod, tFreq, events;
	var subSampleOffsets, triggers, accumulator;
	var overlapMod, overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;
	var posRate, posRateMod, pos;
	var rate, rateMod, grainPhases;
	var panMF, panMod, pan;
	var sigs, sig;

	reset = Trig1.ar(\reset.tr(0), SampleDur.ir);

	tFreqMD = \tFreqMD.kr(0, spec: ControlSpec(0, 2));
	tFreqMod = LFDNoise3.ar(\tFreqMF.kr(1, spec: ControlSpec(0.1, 5)));

	tFreq = \tFreq.kr(1, spec: ControlSpec(1, 1000, \exp));
	tFreq = tFreq * (2 ** (tFreqMod * tFreqMD));

	events = ~grainFunctions.eventsCircular[\events].(tFreq, reset);

	///////////////////////////////////////////////////////////////////////////////////

	// distribute triggers round-robin across the channels
	triggers = ~grainFunctions.multiChannel[\trigger].(numChannels, events[\trigger]);

	// calculate sub-sample offset per multichannel trigger
	subSampleOffsets = ~grainFunctions.helperFunctions[\subSampleOffset].(
		events[\phase],
		events[\slope],
		triggers
	);

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = ~grainFunctions.multiChannel[\accumSubSample].(triggers, subSampleOffsets);

	///////////////////////////////////////////////////////////////////////////////////

	overlap = \overlap.kr(1, spec: ControlSpec(0.125, numChannels));
	overlapMod = ~grainFunctions.multiChannel[\dwhite].(triggers);
	overlap = overlap * (2 ** (overlapMod * \overlapMD.kr(0, spec: ControlSpec(0, 1))));

	maxOverlap = min(overlap, 2 ** tFreqMD.neg * numChannels);

	windowSlopes = Latch.ar(events[\slope] / max(0.001, maxOverlap), triggers);
	windowPhases = (windowSlopes * accumulator).clip(0, 1);

	///////////////////////////////////////////////////////////////////////////////////

	grainWindows = ~unitShapers.windowFunctions[\gaussian].(
		windowPhases,
		\windowSkew.kr(0.5, spec: ControlSpec(0, 1)),
		\windowIndex.kr(0, spec: ControlSpec(0, 5)),
	);

	///////////////////////////////////////////////////////////////////////////////////

	posRate = \posRate.kr(1, spec: ControlSpec(0.125, 4));
	posRateMod = SinOsc.ar(\posRateMF.kr(1, spec: ControlSpec(0.01, 1)));
	posRate = posRate + (posRate * posRateMod * \posRateMD.kr(0, spec: ControlSpec(0, 2)));

	pos = Phasor.ar(
		trig: DC.ar(0),
		rate: posRate * BufRateScale.kr(sndBuf) * SampleDur.ir / BufDur.kr(sndBuf),
		start: \posLo.kr(0, spec: ControlSpec(0, 1)),
		end: \posHi.kr(1, spec: ControlSpec(0, 1))
	);

	///////////////////////////////////////////////////////////////////////////////////

	rate = \rate.kr(1, spec: ControlSpec(0.125, 4));
	rateMod = SinOsc.ar(\rateMF.kr(1, spec: ControlSpec(0.01, 1)));
	rate = rate * (2 ** (rateMod * \rateMD.kr(0, spec: ControlSpec(0, 2))));

	///////////////////////////////////////////////////////////////////////////////////

	grainPhases = Latch.ar(rate, triggers) * accumulator;
	grainPhases = grainPhases + (Latch.ar(pos, triggers) * BufFrames.kr(sndBuf));

	sigs = BufRd.ar(
		numChannels: 1,
		bufnum: sndBuf,
		phase: grainPhases,
		loop: 1,
		interpolation: 4
	);

	sigs = sigs * grainWindows;

	///////////////////////////////////////////////////////////////////////////////////

	// panning
	panMF = \panMF.kr(0.3, spec: ControlSpec(0.01, 1));
	panMod = { |phase|
		SinOsc.ar(panMF, phase * pi)
	};

	pan = ~unitShapers.helperFunctions[\modScaleBipolar].(
		modulator: panMod.(0.5),
		value: \pan.kr(0.5, spec: ControlSpec(0, 1)),
		amount: \panMD.kr(0, spec: ControlSpec(0, 1)),
		direction: \full
	);

	sigs = PanAz.ar(2, sigs, pan.linlin(0, 1, -0.5, 0.5));
	sig = sigs.sum;

	///////////////////////////////////////////////////////////////////////////////////
	
	sig = sig * ~unitShapers.helperFunctions[\modScaleBipolar].(
		modulator: SinOsc.ar(\ampMF.kr(5, spec: ControlSpec(1, 10))),
		value: \amp.kr(-25, spec: ControlSpec(-35, -5)).dbamp,
		amount: \ampMD.kr(0, spec: ControlSpec(0, 1)),
		direction: \down
	);

	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

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

~buffer = //... load your sample here ...

(
Synth(\grains, [

	\tFreq, 1000,
	\tFreqMF, 0.1,
	\tFreqMD, 0,
	
	\overlap, 8,
	\overlapMD, 0,

	\windowSkew, 0.5,
	\windowIndex, 0,

	\rate, 1,
	\rateMF, 0.1,
	\rateMD, 0,

	\posRate, 1.0,
	\posRateMF, 1,
	\posRateMD, 0,

	\posLo, 0.50,
	\posHi, 0.72,

	\sndBuf, ~buffer,
	
	\pan, 0.5,
	\panMF, 0.1,
	\panMD, 0,

	\amp, -10,
	\ampMF, 0.3,
	\ampMD, 0,
	
	\out, 0,

]);
)