GrainUtils - sub-sample accurate EventScheduler and dynamic VoiceAllocator

still work in progress, but you can check out the latest release here:

and test the current version with these plots and examples below (the SchedulerCycle is crisp now, phase starting at 0 and you still get an initial trigger).

EDIT: You can now search for “Event Scheduling” in the help browser and should find the first part of the guide there.

// ===== SHIFT REGISTER =====

(
{
	var trig = Impulse.ar(1000);
    var register = ShiftRegister.ar(
		trig: trig,
        chance: 1.0,
        length: 8,
        rotate: 1,
		reset: 0,
	);
	[
		register[\bit3],
		register[\bit8]
	];
}.plot(0.021).plotMode_(\plines);
)

///////////////////////////////////////////////////////////////////////////////////////

// ===== SCHEDULER CYCLE =====

(
{
	//var rate = 1000 * (2 ** (SinOsc.ar(50) * 1));
	var rate = 1000;
	var events = SchedulerCycle.ar(rate);
	[
		events[\phase],
		//events[\rate] / 1000,
		events[\trigger],
		//events[\subSampleOffset],
	];
}.plot(0.0021).plotMode_(\plines);
)

// random durations

(
var getSubDivs = { |rate, randomness|
	var subDiv = Ddup(2, (2 ** (Dwhite(-1.0, 1.0) * randomness))) / rate;
	Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};

{
	var subDiv, events;

	subDiv = getSubDivs.(\tFreq.kr(500), \randomness.kr(1));
	events = SchedulerCycle.ar(subDiv);

	events[\phase];

}.plot(0.021).plotMode_(\plines);
)

// sequence of durations

(
var getSubDivs = { |rate, arrayOfDurations, numOfDurations|
	var subDiv = Ddup(2, Dseq([Dser(arrayOfDurations, numOfDurations)], inf)) / rate;
	Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};

{
	var arrayOfDurations, numOfDurations, subDiv, events;

	arrayOfDurations = [5, 1, 2, 4];
	numOfDurations = 4;

	subDiv = getSubDivs.(\tFreq.kr(500), arrayOfDurations, numOfDurations);

	events = SchedulerCycle.ar(subDiv);

	events[\phase];

}.plot(0.021).plotMode_(\plines);
)

// euclidean durations (needs Dbjorklund2 from f0 plugins)

(
var getSubDivs = { |rate, numHits, numSize, offSet|
	var subDiv = Ddup(2, Dbjorklund2(numHits, numSize, offSet)) / rate;
	Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};

{
	var numHits, numSize, offSet, subDivs, events;

	numHits = \numHits.kr(5);
	numSize = \numSize.kr(8);
	offSet = \offSet.kr(0);

	subDivs = getSubDivs.(\tFreq.kr(500), numHits, numSize, offSet);

	events = SchedulerCycle.ar(subDivs);

	events[\phase];

}.plot(0.021).plotMode_(\plines);
)

///////////////////////////////////////////////////////////////////////////////////////

// ===== SCHEDULER BURST =====

// sequence of durations

(
var numOfSubDivs = 3;
var arrayOfSubDivs = [5, 1, 3].normalizeSum;

var getSubDivs = { |trig, arrayOfSubDivs, numOfSubDivs, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var subDiv = Ddup(2, Dseq(arrayOfSubDivs, numOfSubDivs)) * duration;
	Duty.ar(subDiv, trig, subDiv) * hasTriggered;
};

{
	var initTrigger, subDivs, events;

	//initTrigger = Impulse.ar(50);
	initTrigger = Trig1.ar(\trig.tr(1), SampleDur.ir);
	subDivs = getSubDivs.(initTrigger, arrayOfSubDivs, numOfSubDivs, \sustain.kr(0.02));

	events = SchedulerBurst.ar(
		trig: initTrigger,
		duration: subDivs,
		cycles: numOfSubDivs
	);

	events[\phase];

}.plot(0.041).plotMode_(\plines);
)

// sequence of durations with voice allocation (plot)

(
var numOfSubDivs = 4;
var arrayOfSubDivs = [5, 1, 8, 3].normalizeSum;

var getSubDivs = { |trig, arrayOfSubDivs, numOfSubDivs, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var subDiv = Ddup(2, Dseq(arrayOfSubDivs, numOfSubDivs)) * duration;
	Duty.ar(subDiv, trig, subDiv) * hasTriggered;
};

{
	var numChannels = 5;

	var initTrigger, subDivs, events, voices;

	//initTrigger = Impulse.ar(50);
	initTrigger = Trig1.ar(\trig.tr(1), SampleDur.ir);

	subDivs = getSubDivs.(initTrigger, arrayOfSubDivs, numOfSubDivs, \sustain.kr(0.02));

	events = SchedulerBurst.ar(
		trig: initTrigger,
		duration: subDivs,
		cycles: numOfSubDivs
	);
/*
	[
		events[\phase],
		events[\trigger],
		//events[\subSampleOffset]
	];
*/
	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / \overlap.kr(1),
		subSampleOffset: events[\subSampleOffset]
	);

	voices[\phases];

}.plot(0.041).plotMode_(\plines);
)

// sequence of durations with voice allocation (example)

(
var getSubDivs = { |trig, arrayOfSubDivs, numOfSubDivs, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var subDiv = Ddup(2, Dseq(arrayOfSubDivs, numOfSubDivs)) * duration;
	Duty.ar(subDiv, trig, subDiv) * hasTriggered;
};

SynthDef(\burst, {

	var numChannels = 8;

	var initTrigger, subDivs, numOfSubDivs, arrayOfSubDivs;
	var duration, events, voices;
	var grainPhases, grainWindows;
	var sigs, sig;

	initTrigger = Trig1.ar(\trig.tr(0), SampleDur.ir);
	duration = \sustain.kr(1);

	arrayOfSubDivs = \arrayOfSubDivs.kr(Array.fill(16, 1));
	numOfSubDivs = \numOfSubDivs.kr(16);

	subDivs = getSubDivs.(
		initTrigger,
		arrayOfSubDivs,
		numOfSubDivs,
		duration
	);

	events = SchedulerBurst.ar(
		trig: initTrigger,
		duration: subDivs,
		cycles: numOfSubDivs
	);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / \overlap.kr(1),
		subSampleOffset: events[\subSampleOffset]
	);

	grainWindows = ExponentialWindow.ar(
		voices[\phases],
		\windowSkew.kr(0.01),
		\windowShape.kr(0)
	);

	grainPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: \freq.kr(440),
		subSampleOffset: events[\subSampleOffset]
	);

	sigs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);

	sigs = sigs * grainWindows;

	sigs = PanAz.ar(2, sigs, \pan.kr(0));
	sig = sigs.sum;

	sig = sig * \amp.kr(-15).dbamp;

	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

	sig = LeakDC.ar(sig);
	sig = Limiter.ar(sig);
	Out.ar(\out.kr(0), sig);
}).add;
)

// Pmono

(
//var arrayOfSubDivs = Array.fill(12, { 2 ** rrand(-1.0, 1.0) } ).normalizeSum;
//arrayOfSubDivs.debug(\arrayOfSubDivs);

var arrayOfSubDivs = [0.090043825988599, 0.10094508851196, 0.059173625247572, 0.078758510686157, 0.057549414766475, 0.085870514810413, 0.10684511179581, 0.082087147023147, 0.1207103511832, 0.047718306079275, 0.12704484171827, 0.043253262189117];

Pdef(\burst,
	Pmono(\burst,

		\trig, 1,
		\legato, 0.8,
		\dur, 4,

		\freq, 440,
		\overlap, 1,

		\time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo },

		\arrayOfSubDivs, [arrayOfSubDivs],
		\numOfSubDivs, 12,

		\amp, -15,
		\out, 0,

	),
).play;
)


// Routines

(
//var arrayOfSubDivs = Array.fill(12, { 2 ** rrand(-1.0, 1.0) } ).normalizeSum;
//arrayOfSubDivs.debug(\arrayOfSubDivs);

var arrayOfSubDivs = [0.090043825988599, 0.10094508851196, 0.059173625247572, 0.078758510686157, 0.057549414766475, 0.085870514810413, 0.10684511179581, 0.082087147023147, 0.1207103511832, 0.047718306079275, 0.12704484171827, 0.043253262189117];

Routine({

	s.bind {

		~synth = Synth(\burst, [

			\trig, 0,

			\freq, 440,
			\overlap, 1,

			\amp, -15,
			\out, 0,

		]);

	};

	s.sync;

	loop {

		s.bind {

			~synth.set(
				\trig, 1,
				\sustain, 3.2,
				\arrayOfSubDivs, arrayOfSubDivs,
				\numOfSubDivs, 12,
			);

		};

		4.wait;

	};

}).play;
)

/////////////////////////////////////////////////////////////////////////////////////

// ===== SCHEDULER CYCLE & VOICE ALLOCATOR =====

// plot multichannel grain frequencies

(
var multiChannelDwhite = { |triggers|
	var demand = Dwhite(-1.0, 1.0);
	triggers.collect{ |localTrig|
		Demand.ar(localTrig, DC.ar(0), demand)
	};
};

{
	var numChannels = 5;

	var reset, tFreqMD, tFreq;
	var overlapMD, overlap;
	var events, voices;
	var grainFreqMod, grainFreqs, grainPhases, grainWindows;
	var grainOscs, grains;

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

	tFreqMD = \tFreqMD.kr(0);
	tFreq = \tFreq.kr(400) * (2 ** (SinOsc.ar(50) * tFreqMD));

	overlapMD = \overlapMD.kr(0);
	overlap = \overlap.kr(5) * (2 ** (SinOsc.ar(50) * overlapMD));

	events = SchedulerCycle.ar(tFreq, reset);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / overlap,
		subSampleOffset: events[\subSampleOffset],
	);

	grainWindows = HanningWindow.ar(voices[\phases], \skew.kr(0.5));

	grainFreqMod = multiChannelDwhite.(voices[\triggers]);
	grainFreqs = \freq.kr(800) * (2 ** (grainFreqMod * \freqMD.kr(2)));

	grainPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: grainFreqs,
		subSampleOffset: events[\subSampleOffset]
	);

	grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);
	grains = grainOscs * grainWindows;

}.plot(0.041);
)

// play multichannel grain frequencies + panning

(
var multiChannelDwhite = { |triggers|
	var demand = Dwhite(-1.0, 1.0);
	triggers.collect{ |localTrig|
		Demand.ar(localTrig, DC.ar(0), demand)
	};
};

var multiChannelDseq = { |triggers, reset, arrayOfItems, numOfItems, repeatItem|
	var demand = Ddup(repeatItem, Dseq([Dser(arrayOfItems, numOfItems)], inf));
	triggers.collect{ |localTrig|
		Demand.ar(localTrig + reset, reset, demand)
	};
};

var multiChannelDxrand = { |triggers, reset, arrayOfItems, numOfItems, repeatItem|
	var demand = Ddup(repeatItem, Dxrand([Dser(arrayOfItems, numOfItems)], inf));
	triggers.collect{ |localTrig|
		Demand.ar(localTrig + reset, reset, demand)
	};
};

var tuning = Tuning.new((0..12) * (3.ratiomidi / 13), 3.0, "Bohlen-Pierce").ratios;
var degrees = { rrand(0, 12) } ! 8;
var ratios = degrees.collect{ |degree| tuning[degree] };

{
	var numChannels = 8;
	var numSpeakers = 2;

	var reset, tFreqMD, tFreq;
	var overlapMD, overlap;
	var events, voices, windowPhases, triggers;

	var grainFreqMod, grainFreqs, grainPhases, grainWindows;
	var grainOscs, grains, sig;
	var fmods, modPhases, pmods;
	var trans, octave, note, pan, demand;

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

	tFreqMD = \tFreqMD.kr(0);
	tFreq = \tFreq.kr(10) * (2 ** (LFDNoise3.ar(0.3) * tFreqMD));

	overlapMD = \overlapMD.kr(0);
	overlap = \overlap.kr(4) * (2 ** (LFDNoise3.ar(0.1) * overlapMD));

	events = SchedulerCycle.ar(tFreq, reset);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / overlap,
		subSampleOffset: events[\subSampleOffset],
	);

	grainWindows = HanningWindow.ar(
		phase: voices[\phases],
		skew: 0.03,
	);

	trans = multiChannelDxrand.(
		voices[\triggers],
		DC.ar(0),
		[0, 2, -2, 7, -5],
		5,
		1
	);

	octave = multiChannelDxrand.(
		voices[\triggers],
		DC.ar(0),
		[12, -12],
		2,
		1
	);

	grainFreqMod = multiChannelDseq.(
		voices[\triggers],
		DC.ar(0),
		ratios,
		\numOfItems.kr(8),
		\repeatItem.kr(2)
	);

	grainFreqs = (89 + trans + octave).midicps * grainFreqMod;

	grainPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: grainFreqs,
		subSampleOffset: events[\subSampleOffset]
	);

	grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);

	grains = grainOscs * grainWindows;

	pan = multiChannelDwhite.(voices[\triggers]);
	grains = PanAz.ar(
		numChans: numSpeakers,
		in: grains,
		pos: pan.linlin(-1, 1, -1 / numSpeakers, (2 * numSpeakers - 3) / numSpeakers);
	);
	sig = grains.sum;

	sig = LeakDC.ar(sig);
	sig * 0.1;
}.play
)

// play multichannel grain frequencies + panning 2

(
var multiChannelDwhite = { |triggers|
	var demand = Dwhite(-1.0, 1.0);
	triggers.collect{ |localTrig|
		Demand.ar(localTrig, DC.ar(0), demand)
	};
};

{
	var numChannels = 8;
	var numSpeakers = 2;

	var reset, tFreq;
	var events, voices, windowPhases;
	var grainFreqMod, grainFreqs, grainPhases, grainWindows;
	var grainOscs, grains, sig, pan;

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

	events = SchedulerCycle.ar(tFreq, reset);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / \overlap.kr(0.5),
		subSampleOffset: events[\subSampleOffset],
	);

	grainWindows = HanningWindow.ar(
		phase: voices[\phases],
		skew: \skew.kr(0.03)
	);

	grainFreqMod = multiChannelDwhite.(voices[\triggers]);
	grainFreqs = \freq.kr(800) * (2 ** (grainFreqMod * \freqMD.kr(2)));

	grainPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: grainFreqs,
		subSampleOffset: events[\subSampleOffset]
	);
	grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);

	grains = grainOscs * grainWindows;

	pan = multiChannelDwhite.(voices[\triggers]);
	grains = PanAz.ar(
		numChans: numSpeakers,
		in: grains,
		pos: pan.linlin(-1, 1, -1 / numSpeakers, (2 * numSpeakers - 3) / numSpeakers);
	);
	sig = grains.sum;

	sig = LeakDC.ar(sig);
	sig = sig * 0.1;
}.play;
)

// demonstration of multichnanel FM & PM

(
var multiChannelDwhite = { |triggers|
	var demand = Dwhite(-1.0, 1.0);
	triggers.collect{ |localTrig|
		Demand.ar(localTrig, DC.ar(0), demand)
	};
};

{
	var numChannels = 8;

	var reset, tFreqMD, tFreq;
	var overlapMD, overlap;
	var events, voices, windowPhases, triggers;

	var grainFreqMod, grainFreqs, grainPhases, grainWindows;
	var grainOscs, grains, sig;
	var fmods, modPhases, pmods;

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

	tFreqMD = \tFreqMD.kr(2);
	tFreq = \tFreq.kr(10) * (2 ** (SinOsc.ar(0.3) * tFreqMD));

	overlapMD = \overlapMD.kr(0);
	overlap = \overlap.kr(1) * (2 ** (LFDNoise3.ar(0.1) * overlapMD));

	events = SchedulerCycle.ar(tFreq, reset);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: events[\rate] / overlap,
		subSampleOffset: events[\subSampleOffset],
	);

	grainWindows = HanningWindow.ar(
		phase: voices[\phases],
		skew: \skew.kr(0.05)
	);

	grainFreqMod = multiChannelDwhite.(voices[\triggers]);
	grainFreqs = \freq.kr(440) * (2 ** (grainFreqMod * \freqMD.kr(1)));

	fmods = ExponentialWindow.ar(
		phase: voices[\phases],
		skew: \pitchSkew.kr(0.03),
		shape: \pitchShape.kr(0)
	);

	grainPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: grainFreqs * (1 + (fmods * \pitchMD.kr(0))),
		subSampleOffset: events[\subSampleOffset]
	);

	modPhases = RampIntegrator.ar(
		trig: voices[\triggers],
		rate: grainFreqs * \pmRatio.kr(1.5),
		subSampleOffset: events[\subSampleOffset]
	);
	pmods = SinOsc.ar(DC.ar(0), modPhases * 2pi);

	grainPhases = (grainPhases + (pmods * \pmIndex.kr(1))).wrap(0, 1);
	grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);

	grains = grainOscs * grainWindows;

	grains = PanAz.ar(2, grains, \pan.kr(0));
	sig = grains.sum;

	sig = LeakDC.ar(sig);
	sig = sig * 0.1;
}.play
)

// binding grain duration to grain frequency (pulsar synthesis) with phase shaping

(
var lfo = {

	var measurePhase = Phasor.ar(DC.ar(0), \rate.kr(0.5) * SampleDur.ir);
	var stepPhase = (measurePhase * \stepsPerMeasure.kr(2)).wrap(0, 1);

	var measureLFO = HanningWindow.ar(measurePhase, \skewA.kr(0.75));
	var stepLFO = GaussianWindow.ar(stepPhase, \skewB.kr(0.5), \index.kr(1));

	stepLFO * measureLFO;
};

{
	var numChannels = 8;

	var reset, flux, tFreqMod, tFreq, windowRatio;
	var events, voices, windowPhases, triggers;
	var grainFreq, grainPhases, grainWindows;
	var grainOscs, grains, sig;

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

	flux = LFDNoise3.ar(\fluxMF.kr(1));
	flux = 2 ** (flux * \fluxMD.kr(0.5));

	tFreqMod = lfo.().linlin(0, 1, 1, 50);
	tFreq = \tFreq.kr(20) * flux * tFreqMod;

	grainFreq = \freq.kr(1200) * flux;
	windowRatio = \windowRatio.ar(5); // this has to be audio rate, we will latch that later

	events = SchedulerCycle.ar(tFreq, reset);

	voices = VoiceAllocator.ar(
		numChannels: numChannels,
		trig: events[\trigger],
		rate: grainFreq / windowRatio, // grain duration depending on grainFreq scaled by windowRatio
		subSampleOffset: events[\subSampleOffset],
	);

	grainWindows = HanningWindow.ar(
		phase: voices[\phases],
		skew: \skew.kr(0.01)
	);

	// phase shaping for a frequency trajectory per grain:
	// using normalized windowPhases into UnitCubic,
	// then scaling to number of cycles by windowRatio before wrapping between 0 and 1
	// important to latch windowRatio per trigger here!!!
	grainPhases = UnitCubic.ar(voices[\phases], \shape.kr(0.45));
	grainPhases = (grainPhases * Latch.ar(windowRatio, voices[\triggers])).wrap(0, 1);

	grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);

	grains = grainOscs * grainWindows;

	grains = PanAz.ar(2, grains, \pan.kr(0));
	sig = grains.sum;

	sig = LeakDC.ar(sig);
	sig = sig * 0.1;
}.play;
)

//////////////////////////////////////////////////////////////////////////////////

// ===== UNIT SHAPERS =====

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	UnitKink.ar(phase, \skew.kr(0.25));
}.plot(0.02);
)

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	UnitTriangle.ar(phase, \skew.kr(0.5));
}.plot(0.02);
)

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	UnitCubic.ar(phase, \index.kr(0.5));
}.plot(0.02);
)

// ===== WINDOW FUNCTIONS =====

// warped hanning window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	HanningWindow.ar(phase, \skew.kr(0.5));
}.plot(0.02);
)

// warped raised cosine window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	RaisedCosWindow.ar(phase, \skew.kr(0.5), \index.kr(5));
}.plot(0.02);
)

// warped gaussian window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	GaussianWindow.ar(phase, \skew.kr(0.5), \index.kr(5));
}.plot(0.02);
)

// warped trapezoidal window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	TrapezoidalWindow.ar(phase, \skew.kr(0.5), \width.kr(0.5), \duty.kr(1));
}.plot(0.02);
)

// warped tukey window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	TukeyWindow.ar(phase, \skew.kr(0.5), \width.kr(0.5));
}.plot(0.02);
)

// warped exponential window

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	ExponentialWindow.ar(phase, \skew.kr(0.5), \shape.kr(0));
}.plot(0.02);
)

// ===== INTERP FUNCTIONS =====

// linear interpolation of quintic in and quintic out

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);

	var sigA = JCurve.ar(phase, \shapeA.kr(0));
	var sigB = JCurve.ar(phase, \shapeB.kr(0.5));
	var sigC = JCurve.ar(phase, \shapeC.kr(1));

	[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)

// linear interpolation of quintic sigmoid and quintic seat

(
{
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);

	var sigA = SCurve.ar(phase, \shapeA.kr(0), \inflectionA.kr(0.25));
	var sigB = SCurve.ar(phase, \shapeB.kr(0.5), \inflectionB.kr(0.50));
	var sigC = SCurve.ar(phase, \shapeC.kr(1), \inflectionC.kr(0.75));

	[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)
1 Like