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);
)