Resetting start count with PulseDivider?

when using this approach and set the asynchronicity to 0 for synchronuous triggers:

(
var multiChannelPhase = { |numChannels, triggerRate, asynchronicity, overlap|
	
	var localFreq = triggerRate / numChannels;
	var localOverlap = numChannels / overlap;
	
	var minDuration = (2 ** asynchronicity) / localFreq;
	var maxDuration = (2 ** (-1 * asynchronicity)) / localFreq;
	var demand = Dunique(minDuration * ((maxDuration / minDuration) ** Dwhite(0, 1)));
	
	numChannels.collect{ |i|
		
		var duration = Duty.ar(demand, DC.ar(0), demand);
		var localPhase = LFSaw.ar(1 / duration, (1 - (i / numChannels + 0.5)) * 2).linlin(-1, 1, 0, 1);
		var localTrig = HPZ1.ar(localPhase) < 0;
		var hasTriggered = PulseCount.ar(localTrig) >= 1;
		
		localPhase * hasTriggered * localOverlap;
		
	}.reverse;
};

var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	var numChannels = 5;
	var tFreq = \tFreq.kr(100);
	var maxOverlap = min(\overlap.kr(1), numChannels);
	var windowRate = tFreq / maxOverlap;
	
	var windowPhases = multiChannelPhase.(numChannels, tFreq, \asynchronicity.kr(0), maxOverlap);
	
	hanningWindow.(windowPhases);
	
}.plot(0.2);
)

you get windows distributed across the 5 channels.

when setting overlap to overlapMax you get the desired behaviour for synchronous grains with overlapping windows:

(
var multiChannelPhase = { |numChannels, triggerRate, asynchronicity, overlap|
	
	var localFreq = triggerRate / numChannels;
	var localOverlap = numChannels / overlap;
	
	var minDuration = (2 ** asynchronicity) / localFreq;
	var maxDuration = (2 ** (-1 * asynchronicity)) / localFreq;
	var demand = Dunique(minDuration * ((maxDuration / minDuration) ** Dwhite(0, 1)));
	
	numChannels.collect{ |i|
		
		var duration = Duty.ar(demand, DC.ar(0), demand);
		var localPhase = LFSaw.ar(1 / duration, (1 - (i / numChannels + 0.5)) * 2).linlin(-1, 1, 0, 1);
		var localTrig = HPZ1.ar(localPhase) < 0;
		var hasTriggered = PulseCount.ar(localTrig) >= 1;
		
		localPhase * hasTriggered * localOverlap;
		
	}.reverse;
};

var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	var numChannels = 5;
	var tFreq = \tFreq.kr(100);
	var maxOverlap = min(\overlap.kr(5), numChannels);
	var windowRate = tFreq / maxOverlap;
	
	var windowPhases = multiChannelPhase.(numChannels, tFreq, \asynchronicity.kr(0), maxOverlap);
	
	hanningWindow.(windowPhases);
	
}.plot(0.2);
)

you can also set the asychrounous parameter to 1 and reduce the overlap to 1 to get asychronous grains. you see that some of the grains are already overlapping without touching the overlap parameter:

(
var multiChannelPhase = { |numChannels, triggerRate, asynchronicity, overlap|
	
	var localFreq = triggerRate / numChannels;
	var localOverlap = numChannels / overlap;
	
	var minDuration = (2 ** asynchronicity) / localFreq;
	var maxDuration = (2 ** (-1 * asynchronicity)) / localFreq;
	var demand = Dunique(minDuration * ((maxDuration / minDuration) ** Dwhite(0, 1)));
	
	numChannels.collect{ |i|
		
		var duration = Duty.ar(demand, DC.ar(0), demand);
		var localPhase = LFSaw.ar(1 / duration, (1 - (i / numChannels + 0.5)) * 2).linlin(-1, 1, 0, 1);
		var localTrig = HPZ1.ar(localPhase) < 0;
		var hasTriggered = PulseCount.ar(localTrig) >= 1;
		
		localPhase * hasTriggered * localOverlap;
		
	}.reverse;
};

var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	var numChannels = 5;
	var tFreq = \tFreq.kr(100);
	var maxOverlap = min(\overlap.kr(1), numChannels);
	var windowRate = tFreq / maxOverlap;
	
	var windowPhases = multiChannelPhase.(numChannels, tFreq, \asynchronicity.kr(1), maxOverlap);
	
	hanningWindow.(windowPhases);
	
}.plot(0.2);
)

when you now increase the overlap you get the discontinuity in the phase.

i think this could be solved by using one random sequence where every new phase gets a new channel instead of having numChannels with its own random sequence . Then overlap could be calculated in relation to the local duration. but i have no idea how to do that. You also cannot use trigger masking because you create the phase first and then the triggers and not the other way around.

yes, in my example i’m also all the time bumping into discontinuos phases when overlap set to max. it also becomes strikingly more apparent with harmonic materials/ continuous tones.
i’m not sure i can provide any useful suggestion to your case, also given i use a quite different approach/syntax. but when you think of a new channel for every phase, does it possibly mean you’d need more channels available in a synth than are actually used/played at a given moment?


with the implementation right now you have numChannel times LFSaw OScs with a separate random Duty sequence and an offset.
i think for overlap behaving correctly you would have to caculate the duration between 1 and 6, 2 and 7 etc. and better use one Duty where every new phase gets distributed to a new channel instead of having 5 separete random phases.

somewhat optimizing my gating example from before, it is possible to skip the first trigger after a reshuffling of the start positions using Pulsecount and reset, which lessens the envelope artifacts, but they still occur nonetheless.

({
	var n=8;

	var freq=80;

	var btrig=Impulse.ar(freq!n);

	var division = n;
	
	var reset=PulseDivider.ar(btrig,freq/4);

	var start = TIRand.ar(0,n-1,reset).poll;   //dynamically reshuffling start position of triggers causes clicks in envelope

	//	var start = (1..n);                  //linear sequence triger offset doesnt interrupt envelope
	
	var rcount = Demand.ar(btrig,reset, Dseries(0, 1, inf));

	var rtrig=Trig1.ar(BinaryOpUGen('==', ((rcount+start+1)%(division+1)), 0), SampleDur.ir);

	var init= n.collect{|i| PulseCount.ar(rtrig.at(i),reset.at(i))>0};

	SinOsc.ar(0,Sweep.ar(init*rtrig,freq/n).clip(0,1)*1pi);

}.plot(1))

the only thing that comes to my mind for having durations and triggers coincide is to read them from a buffer filled with prepared random durations and read them from there. like this any triggered event would be of known duration.

yeah, but thats probably a bit limiting.
I think you can calculate the phases first and get the triggers then. will have a look at a possible multichannel solution for the approach i have been sharing.

hi @dietcv,

curious to hear whether you eventually managed to come up with a satisfying workaround for asynchronous grains w overlap as discussed above?

hey, this is a really tricky problem when the core of the overlapping granulation is the round robin method.

(
var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, chan);
	};
};

var multiChannelPhase = { |triggers, windowRate|
	triggers.collect{ |localTrig, i|
		var hasTriggered = PulseCount.ar(localTrig) > 0;
		var localPhase = Sweep.ar(localTrig, windowRate[i] * hasTriggered);
		localPhase * (localPhase < 1);
	};
};

{
	var numChannels = 5;

	var tFreq, reset, trig, durations, triggers, legato, overlap, maxOverlap;
	var windowRates, windowPhases, randomness;

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

	// random durations for trigger and legato sequence
	randomness = \randomness.kr(1);
	durations = Dunique(2 ** (2 * randomness * Dwhite(0, 1) - randomness));

	// create triggers from sequence of durations
	trig = TDuty.ar(durations / tFreq, reset);

	// distribute triggers round robin across the channels
	triggers = multiChannelTrigger.(numChannels, trig);

	// trigger legato sequence by multichannel triggers
	legato = Demand.ar(triggers, reset, durations);

	// multiply overlap by sequence of durations to adjust phase per trigger
	overlap = \overlap.kr(1) * legato;

	// TO DO: calculate maximum overlap per trigger
	// maxOverlap = ....

	// add max(0.001, ...) to prevent division through zero until first trigger has arrived on each channel
	windowRates = tFreq / max(0.001, overlap);
	windowPhases = multiChannelPhase.(triggers, windowRates);

	IEnvGen.ar(Env([0, 1, 0.8, 0], [0.25, 0.5, 0.25]), windowPhases);

}.plot(0.1);
)

With this approach for example you can create asynchronous grains (evaluate the plot several times, note that overlap is set to 1) but the problem is to calculate the maximum possible overlap per channel.

You can visualise the problem if you have a look at the following plot.

Lets put asynchronicity aside for a moment and lets say we have a specific number of channels predefined in the SynthDef, in this case 5 and you would have a number of events which differ for each measure. In the plot you see the first measure has 6 events [3, 1, 2, 2, 1, 3] and the second measure has 7 events [3, 1, 2, 2, 1, 2, 1]. To calculate the maximum overlap possible per channel you have to sum all the events until the next event happens in that specific channel. In the case of the plot you have a first event 3 then 1, then 2, then 2 and then 1 until the next event happens in this channel. So the maximum overlap for the first channel in the first measure would be 3 + 1+ 2 + 2 + 1. The problem here is that measures and number of channels are independent from each other and you see that already for the calculation of the maximum overlap for the third channel you have to take into account an event which is happening in the next measure and is therefore not available yet. For random created durations / asynchronous grains you dont have to wait for the next measure that the problem comes up, its already present with the next event happing. You would have to calculate all the events which would happen for the time your synth is running in advance to calculate the max overlap and prevent phase distortion at all moments in time.

I have figured out one approximation for the plotted example:

(
var rampRotate = { |phase, offset|
	(phase - offset).wrap(0, 1);
};

var rampFromBPM = { |bpm, beatsPerMeasure, reset|
	var beatsPerSec = bpm / 60;
	var measureRate = beatsPerSec / beatsPerMeasure;
	var measurePhase = Phasor.ar(reset, measureRate * SampleDur.ir);
	rampRotate.(measurePhase, SampleDur.ir);
};

var rampToSlope = { |phase|
	var delta = Slope.ar(phase) * SampleDur.ir;
	delta.wrap(-0.5, 0.5);
};

var rampToTrig = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	var sum = (phase + history);
	var trig = (delta / sum).abs > 0.5;
	Trig1.ar(trig, SampleDur.ir);
};

var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, numChannels - 1 - chan);
	};
};

var getSubSampleOffset = { |phase, trig|
	var slope = rampToSlope.(phase);
	var sampleCount = phase - (slope < 0) / slope;
	Latch.ar(sampleCount, trig);
};

var accumulatorSubSample = { |trig, subSampleOffset|
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1));
	accum + subSampleOffset;
};

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		var hasTriggered = PulseCount.ar(localTrig) > 0;
		var localAccum = accumulatorSubSample.(localTrig, subSampleOffsets[i]);
		localAccum * hasTriggered;
	};
};

{
	var numChannels = 5;

	var triggers, overlap, maxOverlap;
	var windowSlopes, windowPhases;
	var reset, arrayOfValues, valuesPerRow, seqOfRows, rowSeqSize, eventDurations, legato;

	var subSampleOffsets, eventTrigger, stepSlope, stepPhase, eventPhase, stepsPerMeasure;
	var measurePhase, stepIndex, stepOffset, stepTrigger, measureIndex, measureTrigger;
	var accumulator, eventIndex, plotScale, eventsPerMeasure, index;

	plotScale = 100;
	reset = \reset.tr(0);

	arrayOfValues = [
		[ 12 ],
		[ 6, 6 ],
		[ 6, 3, 3 ], // <-- index 2
		[ 3, 3, 3, 3 ],
		[ 3, 3, 2, 1, 3 ],
		[ 3, 1, 2, 2, 1, 3 ], // <-- index 5
		[ 3, 1, 2, 2, 1, 2, 1 ], // <-- index 6
		[ 2, 1, 1, 2, 2, 1, 2, 1 ],
		[ 2, 1, 1, 2, 2, 1, 1, 1, 1 ],
		[ 2, 1, 1, 2, 1, 1, 1, 1, 1, 1 ],
		[ 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
		[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
	];
	stepsPerMeasure = arrayOfValues.size;

	seqOfRows = [2, 5, 6];
	rowSeqSize = seqOfRows.size;

	// measure phase & trigger
	measurePhase = rampFromBPM.(\bpm.kr(120 * plotScale), \beatsPerMeasure.kr(4), reset);
	measureTrigger = rampToTrig.(measurePhase);

	// step phase, slope, trigger
	stepPhase = (measurePhase * stepsPerMeasure).wrap(0, 1);
	stepTrigger = rampToTrig.(stepPhase);
	stepSlope = rampToSlope.(stepPhase);

	// create sequence of measure indices
	measureIndex = Demand.ar(measureTrigger, reset, Dseq([Dser(seqOfRows, rowSeqSize)], inf));
	eventsPerMeasure = measureIndex + 1;

	// calculate stepOffset of each event for flattened triangle shaped matrix
	stepOffset = measureIndex * eventsPerMeasure / 2;

	// calculate stepIndex (each measure has measureIndex + 1 items)
	stepIndex = Dseq([Dseries(0, 1, eventsPerMeasure)], inf);

	// index into the matrix with Dswitch1 and duplicate each event
	eventDurations = Ddup(2, Dswitch1(arrayOfValues.flat, stepOffset + stepIndex));
	eventDurations = Demand.ar(stepTrigger, reset, Ddup(eventDurations, eventDurations));

	// create event triggers from sequence of event durations and masking step triggers
	eventIndex = Demand.ar(stepTrigger, reset, Dseq([Dseries(0, 1, eventDurations)], inf));
	eventTrigger = stepTrigger * Demand.ar(stepTrigger, reset, Dswitch1([1] ++ (0 ! (16 - 1)), eventIndex));

	// distribute triggers round robin across the channels
	triggers = multiChannelTrigger.(numChannels, eventTrigger);

	// calculate sub-sample offset per multichannel trigger
	subSampleOffsets = getSubSampleOffset.(stepPhase, triggers);

	// trigger legato sequence by multichannel trigger
	legato = Demand.ar(triggers, reset, eventDurations);

	// multiply overlap by sequence of durations to adjust phase per trigger
	overlap = \overlap.kr(5) * legato;

	// calculate maximum overlap per trigger
/*	
	maxOverlap = numChannels.collect{ |chan|
		Demand.ar(triggers, reset, Dswitch1(arrayOfValues.flat, (Dseries(0, 1, inf) + chan % eventsPerMeasure) + stepOffset));
	}.sum;
*/
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

	// have a look at the calculation of maxOverlap here:
	windowSlopes = Latch.ar(stepSlope, eventTrigger) / min(overlap, legato + (numChannels - 1));
	//windowSlopes = Latch.ar(stepSlope, eventTrigger) / min(overlap, maxOverlap);
	windowPhases = (windowSlopes * accumulator).clip(0, 1);

	IEnvGen.ar(Env([0, 1, 0], [0.5, 0.5], \lin), windowPhases);

}.plot(0.08);
)

If you calculate the maximum overlap with legato + (numChannels - 1) you can make sure that the phases wont get distorted but you cant take full avantage of the maximum overlap possible for the channels you have defined to begin with, in this case 5.

I guess this round robin method is not the best way to go about that problem. Im currently working on a better approach to tackle this problem in gen~. Where you have a condition testing if a specific channel is currently busy and a single-sample feedback loop which then distributes the next event which should be scheduled to another channel indenpendent of the legato values for the current event. So you can set the maximum polyphony with the number of channels but independent of the legato of events. Therefore you dont have to calculate the maximum overlap possible per channel which enables overlapping asynchronous grains.

Wow, this one issue really is a headbreaker, thanks for the example and thorough explanation.
The complexity of the workaround you present does show that one would probably need a wholly different approach as this specific synth architecture doesn’t really manage the problem well, (as it even is quite a hassle to have maximum overlap dependent on the asynchronicity itself).
I can’t accept that there’s no simpler way to go at it, albeit i don’t yet see a good way forward, and round robin does present many other advantages for granular synthesis, but it’s a shame one can’t (yet) bring it all under one hood… Maybe and hopefully your work with gen~ can eventually bring some new insight for sc too!

1 Like

Im trying to enlarge the possibilities for micro sound on a per grain basis in sc for about 2 years now. Will keep you in the loop with my upcomings In gen :slight_smile:

1 Like

Really appreciate your thoughts and contributions so far, keep my fingers crossed for the upcoming studies & thanks for the dedication & sharing!