Resetting start count with PulseDivider?

i think you need a continuous phase.
Ive once tried to convert this into a multi channel random trigger / phase with no luck. maybe we can work on this together.

(
var randomTrigPhase = { |triggerRate = 10, randomness = 1|
	var minDuration = (2 ** randomness) / triggerRate;
	var maxDuration = (2 ** (-1 * randomness)) / triggerRate;
	var demand = Dunique(minDuration * ((maxDuration / minDuration) ** Dwhite(0, 1)));
	var duration = Duty.ar(demand, DC.ar(0), demand);
	var phase = Phasor.ar(DC.ar(0), (1 / duration) / SampleRate.ir, 0, 1);
	(phase: phase, trigger: HPZ1.ar(phase) < 0);
};

{
	var rate = 100;
	randomTrigPhase.(rate, 1).phase;
}.plot(0.1);
)

hi @dietcv,
thanks for chiming in! a continuous phase in what sense you mean?

i think in your example Sweep is receiving a trigger while going from 0 to 1 causing the discontinuity in the phase because the duration between one trigger and the next is unknown.

with this example you get at least a continiuous phase with random triggers to drive the hanning window (evaluate it several times). but its not multichannel with distributed phases / windows accross channels:

(
var randomTrigPhase = { |triggerRate = 10, randomness = 1|
	var minDuration = (2 ** randomness) / triggerRate;
	var maxDuration = (2 ** (-1 * randomness)) / triggerRate;
	var demand = Dunique(minDuration * ((maxDuration / minDuration) ** Dwhite(0, 1)));
	var duration = Duty.ar(demand, DC.ar(0), demand);
	var phase = Phasor.ar(DC.ar(0), (1 / duration) / SampleRate.ir, 0, 1);
	(phase: phase, trigger: HPZ1.ar(phase) < 0);
};

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

{
	var rate = 100;
	var phase = randomTrigPhase.(rate, 1).phase;
	hanningWindow.(phase);
}.plot(0.1);
)

im not sure if this is helping it just reminded me of some issue with multichannel random triggers and continiuous phases i have been trying to solve.

im not yet sure if i’m able to transfer this to my usage case, but what is avoiding a multichannel expansion?

with my example the phase is calculated first and then you can get the triggers by HPZ1 which solves the unknown duration issue.
When using Pulsedivider and Sweep to distribute the triggers / phases round robin across the channels how i would like to (to disable the randomness and mask the triggers by demand sequences of 0s and 1s), its the other way around first the triggers are calculated and then plugged into Sweep to get the phases. I just have no idea how to go about that. but when solved this could be used for your use case with Pulsedivider as well i guess to get random multichannel triggers and continiuous phases.

ok, i see. i unfortunately don’t have an immediate suggestion for this also, but it sounds like it could pave a way for many usages surely!

alternatively for my example im pondering a compromise (too avoid too many calculations of phase e.g.) with a gate that blocks sweep from receiving new triggers until the envelope is closed, but this is still an unimplemented, abstract thought.
ideally of course, a smartly set up sweep just neatly triggers and closes envelopes, no matter what random sequence is fed;)

yes i think all the custom made granular stuff with better abilities for modulation per grain. especially FM/PM or AM per grain. i have been either using FM/PM with a stateless index window per grain or a frequency window per grain.
right now:
by using synchronous triggers by Impulse / Pulsedivider / Sweep you get continiuous grains.
by using synchronous triggers by Impulse / Pulsedivider / Sweep masked by demand sequences with 0s and 1s you get semi synchronous / asynchronous grains
by using asynchronous triggers and Pulsedivider you get a discontinuity in the phase.

i think Sweep just starts right away not caring of the first trigger so you have to make sure the first trigger has arrived with PulseCount to start Sweep. Additionally every Sweep gets interrupted by a new trigger not caring if it has reached its end destination or not which is causing the discontinuity.

Yes, i was wondering though if possibly with Gate.ar one could engineer a way in which retriggers only occur under the condition of Sweeps phase being 0, by actively ignoring retriggers while an envelope is open/ non-zero.

This was what i was envisioning “gating” the sweep input. While it avoids retriggering in the middle of an envelope, it effectively skips triggers and so basically creates a much more discontinuous trigger stream. The randomization does work, but overall not a satisfying solution to the problem unfortunately!

({
	var n=8;
	
	var freq=80;
	
	var btrig=Impulse.ar(freq);
	
	var division = n;
	
	var start = TIRand.ar(0,n-1,PulseDivider.ar(btrig,n)!8);   //dynamically reshuffling start position of triggers causes clicks in envelope
	
	var linstart = (1..n);                                       //linear sequence triger offset
	
	var count = Demand.ar(btrig,0, Dseries(0, 1, inf));
	
	var trig=Trig1.ar(BinaryOpUGen('==', ((count+start+1)%(division)), 0), SampleDur.ir);
	var lintrig=Trig1.ar(BinaryOpUGen('==', ((count+linstart+1)%(division)), 0), SampleDur.ir);
	
	var init= PulseCount.ar(trig)>0;
	
	init*SinOsc.ar(0,Sweep.ar(trig*lintrig,freq/n).clip(0,1)*1pi);
	
}.plot(1))

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!