Triggering action in SynthDef

I’m working on a sampler in SuperCollider, and I was wondering if there is a way to automatically trigger a Phasor after a certain amount of time. The idea is that I would play the attack portion of a sample, then loop part of it for sustain. I want to seamlessly move between the attack portion and the loop portion, and the most straightforward way to do that is to have the loop start (or reset) when the attack ends, with a little crossfade. I can always manually update the value of a trigger, but that’s annoying. I could also ignore the attack portion of the sample, but then it will sound less realistic. Any ideas?

Could you use Env with a loop node?

{
    EnvGen.kr(
        Env(
			[0, -1, 0.2, -1, 1,0.2], 
			1, 
			releaseNode: 4, 
			loopNode: 1 /*cannot be the last element*/
		) 
	)
}.scope

The actual looping is relatively simple using the reset arg of Trig or SetResetFF. The harder part is to loop without clicks caused by the discontinuity of the audio. I am not at the computer right now but I can post an example later of how to do looping of the sustain part of an env with cross fading

That’s a great idea - I hadn’t thought of that!

I can do crossfading. I just needed a way to trigger the event, and an envelope seems like a great idea.

Sharing my looper SynthDef (at least its core Function):

// Looper - v1 :
/**
* Reads a buffer from a point A to a point B repeatidly. When B is reached, restart at A with a crossfade.
*
* Parameters:
* @param bufnum : number of the buffer to play. 0 if omitted
* @param start : starting point. 0 if omitted or <0
* @param end : loop point. end of file if ommitted. Must be > `start`.
* @param loopat : point of restart reading. =`start` if omitted or >= `end` or <0
* The looper start at "start", reads the buffer until it reaches "end" and then restart reading the buffer at "loopat".
*
* @blend : time in *seconds* for the crossfade between the loop point and the loopat point.
*
* @param atk attack
* @param rel release
* @param amp
* @param pan
* @param out
*/

~looperCore = {
	// args from the SynthDef builder
	arg numChannels;

	// SynthDef specific arguments and parameters
	var blend=\blend.kr(0.1);
	var bufnum, start, end, loopat, realstart, realend;
	var atk=\atk.ir(0.1),rel=\rel.ar(0.1);

	var gate=\gate.ar(1);


	var idx1, gate1, sig1, rel1;
	var idx2, gate2, sig2, rel2;

	var trig1=DC.ar(0),trig2=DC.ar(0); // init at 0 by default
	var sig, env;

	var select;

	var trig0=Impulse.ar(0);

	// debug

	var poll=Impulse.ar(10);
	var elapsed=Sweep.ar(trig0,1);

	bufnum=\bufnum.ir(0);
	start=\start.kr(0).max(0);
	loopat=\loopat.kr(0).max(0);
	end=\end.kr(0);

	// si end <= start (ou omis) ==> en prend la fin du buffer
	end=Select.kr((end-start).max(0).sign,[BufFrames.kr(bufnum), end]);

	// si loopAt >= end start (ou omis) ==> on boucle à `start`
	loopat=Select.kr((end-loopat).max(0).sign,[start,loopat]);

	// La vrai fin, celle à partir de laquelle on va boucler
	realend=(end-(blend*BufSampleRate.kr(bufnum))).clip(start+1,end);

	// loop 1
	// .. buffer and cue points choice
	rel2=LocalIn.ar(1,0);
	trig1=rel2+Impulse.ar(0); // loop1's trigger is an initial trigger + the release of the loop2
	// .. le vrai début : `start` la 1ère fois, `loopat` les fois suivantes
	realstart=Select.ar(PulseCount.ar(trig1)-1,[K2A.ar(start),K2A.ar(loopat)]); // Pulse donne 1, 2, 3 à chaque trigger
	// realstart=start;

	// ..sweep and read
	idx1=Sweep.ar(trig1, BufSampleRate.kr(bufnum))+realstart;
	sig1=BufRd.ar(numChannels,  bufnum, idx1, 0);// 1: mono, 2: stereo

	// .. gate
	rel1=Trig1.ar(idx1-realend,0); // release when end cue point is reached
	gate1=SetResetFF.ar(trig1,rel1); // build a gate to be used in SelectX


	// loop 2
	trig2=rel1; // loop2's trigger is loop1's release
	// ..sweep and read
	idx2=Sweep.ar(trig2, BufSampleRate.kr(bufnum)) + loopat; // Démarre toujours à loopat
	sig2=BufRd.ar(numChannels, bufnum, idx2, 0);// 1: mono, 2: stereo

	// .. gate
	rel2=Trig1.ar(((1-gate1)*idx2)-realend,0); // release when end cue point is reached // ==> "(1-gate1)*idx2" : Limit analyse to the portion where loop2 is expected to play. Without this we face nonexpected starts
	gate2=SetResetFF.ar(trig2,rel2);


	// Poll.ar(rel2,idx2,"idx2 at rel2: ");

	LocalOut.ar([rel2]);

	sig=SelectX.ar(Lag.ar(1-gate1,blend),[sig1,sig2]);

	env=EnvGen.ar(Env.asr(atk,1,rel),gate,doneAction: 2);
	sig=sig*env;

	// return
	sig;


};