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.