NOTAM Meetups: Spring 2025

phasor based sub-sample accurate granulation:

// Granular Synthesis is a type of Amplitude Modulational Synthesis.
// Think about applying a window function like an envelope to a carrier on a micro time scale.

// There are three types of granular synthesis:
// 1.) carrier is a single cycle waveform (e.g. pulsar, glisson, trainlet synthesis etc.)
// 2.) carrier is an audio file (e.g. buffer granulation)
// 3.) carrier is an arbitrary input signal, needs a circular buffer (e.g. grain delay)

// 1.1.) stateless window functions

// - stateless functions can be modulated at audio rate
// - the output of a stateless function only depends on the instantaneous input of the ramp signal
// - the ramp signal has to be linear and between 0 and 1

// the most basic granular window - the hanning window
// It is symmetrical and has a continious slope -
// perfect for overlapping grains without introducing amplitude modulational artifacts.

(
var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var unitGaussian = { |phase, index|
	var cosine = cos(phase * 0.5pi) * index;
	exp(cosine * cosine.neg);
};

{
	var skew = \skew.kr(0.5);
	var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
	var warpedPhase = triangle.(phase, skew);
	var hanning = unitHanning.(warpedPhase, skew);
	var gaussian = unitGaussian.(warpedPhase, \index.kr(2)) * hanning;
	[phase, warpedPhase];
	//[phase, warpedPhase, hanning];
	//[phase, warpedPhase, gaussian];
}.plot(0.021);
)

// 1.2.) sub-sample accurate granulation

// 1.2.1.) granulation starts with hard sync

// - the carrier waveform needs a phase reset
// - the carrier gets reset by the trigger derived from the scheduling phasor (hard sync)
// - to smooth out the phase reset a window function is applied to the carrier (windowed sync)

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

var accumSubSample = { |trig, subSampleOffset|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	accum + subSampleOffset;
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var eventPhase, eventSlope, eventTrigger;
	var subSampleOffset, accumulator;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainSlope, grainPhase;
	var carrier, grain;

	eventPhase = VariableRamp.ar(\tFreq.kr(100));
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

	subSampleOffset = getSubSampleOffset.(eventPhase, eventSlope, eventTrigger);
	accumulator = accumSubSample.(eventTrigger, subSampleOffset);

	windowSlope = Latch.ar(eventSlope, eventTrigger) / max(0.001, \overlap.kr(1));
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = hanningWindow.(windowPhase, \skew.kr(0.5));

	grainFreq = \freq.kr(440);
	grainSlope = grainFreq * SampleDur.ir;
	grainPhase = (grainSlope * accumulator).wrap(0, 1);

	carrier = sin(grainPhase * 2pi);

	grain = carrier * grainWindow;

	[windowPhase.wrap(0, 1), eventTrigger, grainPhase, carrier, grain];
}.plot(0.021);
)

// 1.2.2.) compare hard sync with no hard sync

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

var accumSubSample = { |trig, subSampleOffset|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	accum + subSampleOffset;
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var eventPhase, eventSlope, eventTrigger;
	var subSampleOffset, accumulator;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainSlope, grainPhase;
	var carrier, grain;

	eventPhase = VariableRamp.ar(\tFreq.kr(100));
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

	subSampleOffset = getSubSampleOffset.(eventPhase, eventSlope, eventTrigger);
	accumulator = accumSubSample.(eventTrigger, subSampleOffset);

	windowSlope = Latch.ar(eventSlope, eventTrigger) / max(0.001, \overlap.kr(1));
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = hanningWindow.(windowPhase, \skew.kr(0.5));

	grainFreq = \freq.kr(440);
	grainSlope = grainFreq * SampleDur.ir;
	grainPhase = (grainSlope * accumulator).wrap(0, 1);

	carrier = sin(grainPhase * 2pi);
	//carrier = SinOsc.ar(grainFreq);

	grain = carrier * grainWindow;

	grain = LeakDC.ar(grain);
	grain!2 * 0.1;

}.play;
)

s.freqscope;

// 1.2.3.)  grain duration depending on grain frequency

// - the grain duration can depent on the grain frequency instead of the trigger frequency
// - high grain frequencies then lead to short grain durations and vice versa
// - This creates a cheap physical modelling effect!

// We can still add an overlap factor to adjust the grain duration to compensate for that effect

// There are two design problems with this approach:
// 1.) There is no fixed maximum overlap value
// The maximum overlap possible depents on the ratio between the grain frequency and the trigger frequency.

// 2.) You barely get overlapping grains
// The grain frequency is often multiple times higher then the trigger frequency,
// so the grain durations are really short and the grains dont overlap.
// This is not very versatile and mostly interesting for ratchets.

// 1.2.3.1.) the plot

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

var accumSubSample = { |trig, subSampleOffset|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	accum + subSampleOffset;
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var eventPhase, eventSlope, eventTrigger;
	var subSampleOffset, accumulator, overlap, maxOverlap;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainSlope, grainPhase;
	var carrier, grain;

	eventPhase = VariableRamp.ar(\tFreq.kr(50));
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

	grainFreq = \freq.ar(400);
	grainSlope = grainFreq * SampleDur.ir;

	subSampleOffset = getSubSampleOffset.(eventPhase, eventSlope, eventTrigger);
	accumulator = accumSubSample.(eventTrigger, subSampleOffset);

	overlap = \overlap.kr(4);
	maxOverlap = min(overlap, Latch.ar(grainSlope / eventSlope, eventTrigger));

	windowSlope = Latch.ar(grainSlope, eventTrigger) / max(0.001, maxOverlap);
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = hanningWindow.(windowPhase, \skew.kr(0.5));

	grainPhase = (grainSlope * accumulator).wrap(0, 1);
	carrier = sin(grainPhase * 2pi);

	grain = carrier * grainWindow;

	[windowPhase.wrap(0, 1), grainWindow, carrier, grain];
}.plot(0.04);
)

// 1.2.3.2) listen to pulsar ratchets

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	delta.wrap(-0.5, 0.5);
};

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

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

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

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

var easingToLinear = { |x, shape, easingFunc|
	var mix = shape * 2;
	easingFunc * (1 - mix) + (x * mix);
};

var linearToEasing = { |x, shape, easingFunc|
	var mix = (shape - 0.5) * 2;
	x * (1 - mix) + (easingFunc * mix);
};

var lerpEasing = { |x, shape, easingFuncA, easingFuncB|
	Select.ar(BinaryOpUGen('>', shape, 0.5), [
		easingToLinear.(x, shape, easingFuncA),
		linearToEasing.(x, shape, easingFuncB)
	]);
};

var pseudoExponentialIn = { |x, coef = 13|
	(2 ** (coef * x) - 1) / (2 ** coef - 1)
};

var pseudoExponentialOut = { |x|
	1 - pseudoExponentialIn.(1 - x);
};

var exponentialLerp = { |x, shape|
	var easeOut = pseudoExponentialOut.(x);
	var easeIn = pseudoExponentialIn.(x);
	lerpEasing.(x, shape, easeOut, easeIn);
};

{

	var numChannels = 5;

	var flux, tFreqMod, tFreq;
	var eventPhase, eventTrigger, eventSlope;
	var triggers, subSampleOffsets, accumulator;
	var overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;
	var grainFreq, grainSlope, grainPhases;
	var carriers, grains, sig;

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

	tFreqMod = SinOsc.ar(1, 1.5pi).lincurve(-1, 1, 0.1, 3.0, 2) * SinOsc.ar(0.5, 0.75pi).linlin(-1, 1, 0.1, 1.0);
	tFreq = \tFreq.kr(300) * tFreqMod * flux;

	eventPhase = VariableRamp.ar(tFreq);
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

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

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

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

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

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

	grainFreq = \freq.ar(1200);
	grainFreq = grainFreq * flux;
	grainSlope = grainFreq * SampleDur.ir;

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

	overlap = \overlap.kr(4);
	maxOverlap = min(overlap, numChannels * Latch.ar(grainSlope / eventSlope, triggers));

	windowSlopes = Latch.ar(grainSlope, triggers) / max(0.001, maxOverlap);
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases, \skew.kr(0.01));

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

	grainPhases = exponentialLerp.(windowPhases, \shape.kr(0.03));
	grainPhases = (grainPhases * maxOverlap).wrap(0, 1);

	carriers = sin(grainPhases * 2pi);
	grains = carriers * grainWindows;

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

	sig!2 * 0.1;

}.play;
)

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

// 1.3.) multichannel expansion / polyphony (the round-robin method)

// 1.3.1.) handle grain durations longer then the trigger period

// if the current grain duration is longer then the trigger period the ramp doesnt reach 1
// and therefore doesnt read through the entire window function
// we have to setup multichannel expansion / polyphony
// e.g. to play a three note chord on the piano you need three fingers,
// to play three grains simultaneously you need three channels!

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

var accumSubSample = { |trig, subSampleOffset|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	accum + subSampleOffset;
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var eventPhase, eventSlope, eventTrigger;
	var subSampleOffset, accumulator, overlap;
	var windowSlope, windowPhase, grainWindow;

	eventPhase = VariableRamp.ar(\tFreq.kr(50));
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

	subSampleOffset = getSubSampleOffset.(eventPhase, eventSlope, eventTrigger);
	accumulator = accumSubSample.(eventTrigger, subSampleOffset);

	windowSlope = Latch.ar(eventSlope, eventTrigger) / max(0.001, \overlap.kr(2));
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = hanningWindow.(windowPhase, \skew.kr(0.5));

	[windowPhase.wrap(0, 1), grainWindow];
}.plot(0.041);
)

// 1.3.2.) overlap grains when the grain duration is longer then the trigger period

// create a multichannel trigger based on PulseDivider for overlapping grains (round-robin method)
// the round-robin method increments a counter by 1 when receiving a trigger
// and distributes the next event to next channel and wraps around at the last channel

// - the next grain can already start before the current one has finished without truncation
// - the maximum polyphony / overlap is specified by numChannels (fixed with SynthDef evaluation)

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, numChannels - 1 - chan);
	};
};
/*
var multiChannelTrigger = { |numChannels, trig|
	var count = Demand.ar(trig, DC.ar(0), Dseries(0, 1));
	numChannels.collect{ |chan|
		trig * BinaryOpUGen('==', (count + (numChannels - 1 - chan) + 1) % numChannels, 0);
	};
};
*/
var getSubSampleOffset = { |phase, slope, trig|
	var sampleCount = phase / slope;
	Latch.ar(sampleCount, trig);
};

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

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var numChannels = 5;

	var tFreqMod, modDepth, tFreq;
	var eventPhase, eventTrigger, eventSlope;
	var triggers, subSampleOffsets, accumulator, overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;

	tFreq = \tFreq.kr(500);

	eventPhase = VariableRamp.ar(tFreq);
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

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

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

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

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

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

	overlap = \overlap.ar(1);
	maxOverlap = min(overlap, numChannels); // maximum polyphony

	windowSlopes = Latch.ar(eventSlope, triggers) / max(0.001, Latch.ar(maxOverlap, triggers));
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases, \skew.kr(0.5));

}.plot(0.021);
)

// 1.3.3.) overlapping grains with durations of unequal lengths

// the round-robin method doesnt know about the state of its channels (busy or not)
// if the grains are of unequal lenghts e.g modulation of trigger frequency,
// we cant know about the maximum possible overlap per channel in advance

// We can additionally multiply maxOverlap with 2 ** modDepth.neg
// This makes sure grains of unequal length (modulation of trigger frequency) doesnt cause any truncation
// The tradeoff however is that the maximum possible overlap is small for high modulational indices.

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

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

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

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

{
	var numChannels = 5;

	var tFreqMod, modDepth, tFreq;
	var eventPhase, eventTrigger, eventSlope;
	var triggers, subSampleOffsets, accumulator, overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;

	modDepth = \modMD.kr(1);
	tFreqMod = 2 ** (SinOsc.ar(\modMF.kr(50)) * modDepth);
	tFreq = \tFreq.kr(500) * tFreqMod;

	eventPhase = VariableRamp.ar(tFreq);
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

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

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

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

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

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

	overlap = \overlap.ar(1);
	maxOverlap = min(overlap, 2 ** modDepth.neg * numChannels); // maxOverlap tradeoff

	windowSlopes = Latch.ar(eventSlope, triggers) / max(0.001, Latch.ar(maxOverlap, triggers));
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases, \skew.kr(0.5));

}.plot(0.021);
)

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

// 1.4.) extending the setup

// - exchange carrier or window for BufRd, OscOS or stateless window functions etc.
// - add sequencing and modulation
// - add Frequency Modulation, Phase Modulation, Phase Shaping etc.
// - add Panning
// - add multichannel fx

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

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

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

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

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

{
	var numChannels = 5;

	var tFreqMod, modDepth, tFreq;
	var eventPhase, eventTrigger, eventSlope;
	var triggers, subSampleOffsets, accumulator, overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;
	var grainFreqMod, grainFreqs, grainSlopes, grainPhases, sigs;

	modDepth = \modMD.kr(0);
	tFreqMod = 2 ** (SinOsc.ar(\modMF.kr(50)) * modDepth);
	tFreq = \tFreq.kr(200) * tFreqMod;

	eventPhase = VariableRamp.ar(tFreq);
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

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

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

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

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

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

	overlap = \overlap.ar(3);
	maxOverlap = min(overlap, 2 ** modDepth.neg * numChannels);

	windowSlopes = Latch.ar(eventSlope, triggers) / max(0.001, Latch.ar(maxOverlap, triggers));
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases, \skew.kr(0.5));

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

	grainFreqMod = multiChannelDwhite.(triggers);
	grainFreqs = \freq.kr(800) * (2 ** (grainFreqMod * \freqMD.kr(2)));
	grainSlopes = grainFreqs * SampleDur.ir;
	grainPhases = (grainSlopes * accumulator).wrap(0, 1);

	sigs = sin(grainPhases * 2pi);
	sigs * grainWindows;

}.plot(0.041);
)

(
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 rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = phase - history;
	delta.wrap(-0.5, 0.5);
};

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

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

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

var multiChannelAccumulator = { |triggers, subSampleOffsets|
	triggers.collect{ |localTrig, i|
		accumulatorSubSample.(localTrig, subSampleOffsets[i]);
	};
};

var triangle = { |phase, skew|
	var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
		phase / skew,
		1 - ((phase - skew) / (1 - skew))
	]);
	Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};

var unitHanning = { |phase|
	1 - cos(phase * pi) * 0.5;
};

var hanningWindow = { |phase, skew|
	var warpedPhase = triangle.(phase, skew);
	unitHanning.(warpedPhase);
};

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

{
	var numChannels = 5;

	var tFreqMod, modDepth, tFreq;
	var eventPhase, eventTrigger, eventSlope;
	var triggers, subSampleOffsets, accumulator, overlap, maxOverlap;
	var windowSlopes, windowPhases, grainWindows;
	var grainFreqMod, grainFreqs, grainSlopes, grainPhases;
	var sigs, sig;

	modDepth = \modMD.kr(2);
	tFreqMod = 2 ** (LFDNoise3.ar(\modMF.kr(0.3)) * modDepth);
	tFreq = \tFreq.kr(10) * tFreqMod;

	eventPhase = VariableRamp.ar(tFreq);
	eventSlope = rampToSlope.(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = rampToTrig.(eventPhase);

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

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

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

	// create a multichannel accumulator with sub-sample accuracy
	accumulator = multiChannelAccumulator.(triggers, subSampleOffsets);

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

	overlap = \overlap.ar(4);
	maxOverlap = min(overlap, 2 ** modDepth.neg * numChannels);

	windowSlopes = Latch.ar(eventSlope, triggers) / max(0.001, Latch.ar(maxOverlap, triggers));
	windowPhases = (windowSlopes * accumulator).clip(0, 1);
	grainWindows = hanningWindow.(windowPhases, LFDNoise3.ar(0.3).linlin(-1, 1, 0.01, 0.99));

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

	grainFreqMod = multiChannelDwhite.(triggers);
	grainFreqs = \freq.kr(440) * (2 ** (grainFreqMod * \freqMD.kr(2)));
	grainSlopes = grainFreqs * SampleDur.ir;
	grainPhases = (grainSlopes * accumulator).wrap(0, 1);

	sigs = sin(grainPhases * 2pi);
	sigs = sigs * grainWindows;

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

	sig!2 * 0.1;

}.play;
)


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

// 1.5.) phase shaping vs. FM

(
var eventData = { |rate|

	var eventPhase, eventSlope, eventTrigger;

	eventPhase = VariableRamp.ar(rate);
	eventSlope = ~grainFunctions.helperFunctions[\rampToSlope].(eventPhase);

	eventPhase = Delay1.ar(eventPhase);
	eventTrigger = ~grainFunctions.helperFunctions[\rampToTrig].(eventPhase);

	(
		phase: eventPhase,
		slope: eventSlope,
		trigger: eventTrigger
	);

};

{
	var tFreq, events;
	var subSampleOffset, accumulator;
	var windowSlope, windowPhase, grainWindow;
	var grainFreq, grainFreqMD, grainSlope, grainPhaseA, grainPhaseB;
	var freqWindow, grainFreqA, grainFreqB;

	tFreq = 100;
	events = eventData.(tFreq);

	subSampleOffset = ~grainFunctions.helperFunctions[\subSampleOffset].(
		events[\phase],
		events[\slope],
		events[\trigger]
	);

	accumulator = ~grainFunctions.helperFunctions[\accumSubSample].(
		trig: events[\trigger],
		subSampleOffset: subSampleOffset
	);

	windowSlope = events[\slope] / max(0.001, \overlap.kr(1.0));
	windowPhase = (windowSlope * accumulator).clip(0, 1);
	grainWindow = windowPhase < 1;

	grainFreq = \freq.kr(400);

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

	freqWindow = ~unitShapers.windowFunctions[\exponential].(
		windowPhase,
		\pitchSkew.kr(0),
		\pitchShape.kr(1)
	);

	grainFreqA = grainFreq * (1 + (freqWindow * \grainFreqMD.kr(14)));
	grainSlope = grainFreqA * SampleDur.ir;

	grainPhaseA = ~grainFunctions.helperFunctions[\rampSubSample].(
		trig: events[\trigger],
		slope: grainSlope,
		subSampleOffset: subSampleOffset
	);
	grainPhaseA = grainPhaseA.wrap(0, 1);

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

	grainPhaseB = ~unitShapers.lerpingFunctions[\exponential].(
		windowPhase,
		\phaseShape.kr(0)
	);
	grainPhaseB = grainPhaseB * (grainFreq / max(0.001, windowSlope * SampleRate.ir));
	grainPhaseB = grainPhaseB.wrap(0, 1);

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

	//[grainPhaseA, grainPhaseB];
	[sin(grainPhaseA * 2pi) * grainWindow, sin(grainPhaseB * 2pi) * grainWindow];

}.plot(0.021);
)
4 Likes