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;
};