what brilliant stuff!!
![]()
I couldnāt find enough impulse to implement it myself. This is super-cool
classic pulsar synthesis example with additional envelope compensation and phase shaping using these tools:
(
{
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 = 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) * flux * tFreqMod;
grainFreq = \freq.kr(1200) * flux;
windowRatio = \windowRatio.ar(4); // this has to be audio rate, we will latch that later
events = EventScheduler.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 SCurve,
// then scaling to number of cycles by windowRatio before wrapping between 0 and 1
// important to latch windowRatio per trigger here!!!
grainPhases = SCurve.ar(voices[\phases], \shape.kr(1), \inflection.kr(1));
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;
)
One important thing to note here is that the number of channels used for distributing the grains across the channels and which are available via VoiceAllocator are not equal to number of speakers one might want to use for panning the grains across an array of speakers. Thats because of the smart distribution on to free channels to make sure we are not scheduling any grains on channels which are currently busy, while providing a possibility for maximum overlap grain density.
I think one would have to map the individual grains to speaker channels.
The best i could come up with for panning is to distribute each grain to another channel using this channelMask function. But thats not a general panning solution for n-number of speakers for an LFO for example. You can open the s.meter and see each grain distributed to another potential āspeakerā channel with the example below. Maybe someone else could supply a general solution here, im not really good with calculating the channel positions for PanAz. Would be really appreciated ![]()
Just know about scztt-core/PanArray.sc at master Ā· scztt/scztt-core Ā· GitHub
Im currently working on some additional stuff for our repository, will update soon.
(
var channelMask = { |triggers, numSpeakers = 2|
var arrayOfPositions = Array.series(numSpeakers, -1 / numSpeakers, 2 / numSpeakers).wrap(-1.0, 1.0);
var channelPos = Dswitch1(arrayOfPositions, Dseq([Dseries(0, 1, numSpeakers)], inf));
triggers.collect{ |localTrig|
Demand.ar(localTrig, DC.ar(0), channelPos);
};
};
{
var numChannels = 8;
var numSpeakers = 4;
var reset, tFreq;
var events, voices, windowPhases;
var grainFreq, grainPhases, grainWindows;
var grainOscs, grains, sig, chanMask;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreq = \tFreq.kr(2);
grainFreq = \freq.kr(440);
events = EventScheduler.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)
);
grainPhases = RampIntegrator.ar(
trig: voices[\triggers],
rate: grainFreq,
subSampleOffset: events[\subSampleOffset]
);
grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);
grains = grainOscs * grainWindows;
chanMask = channelMask.(voices[\triggers], numSpeakers);
grains = PanAz.ar(numSpeakers, grains, chanMask * \panMax.kr(1));
sig = grains.sum;
sig = LeakDC.ar(sig);
sig = sig * 0.1;
}.play;
)
s.meter;
okay, i guess i have figured it out while looking at an old sc users thread:
(
{
var numChannels = 8;
var numSpeakers = 4;
var reset, tFreq;
var events, voices, windowPhases;
var grainFreq, grainPhases, grainWindows;
var grainOscs, grains, sig, chanMask;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreq = \tFreq.kr(2);
grainFreq = \freq.kr(440);
events = EventScheduler.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)
);
grainPhases = RampIntegrator.ar(
trig: voices[\triggers],
rate: grainFreq,
subSampleOffset: events[\subSampleOffset]
);
grainOscs = SinOsc.ar(DC.ar(0), grainPhases * 2pi);
grains = grainOscs * grainWindows;
grains = PanAz.ar(
numChans: numSpeakers,
in: grains,
pos: MouseX.kr(0, 1).linlin(0, 1, -1 / numSpeakers, (2 * numSpeakers - 3) / numSpeakers);
);
sig = grains.sum;
sig = LeakDC.ar(sig);
sig = sig * 0.1;
}.play;
)
s.meter;
I will reset the repository with some Major changes I have already implemented and some which Are yet to come for an architecture which is Modular and also understandable in the Next week, I guess I have Figured something out which might work in the Long run, when wanting to add different types of Event Generators. I will also add some helpfiles and try to Integrate my notam guides in there for some background Information about sub sample accurate scheduling and Voice allocation.
Im currently not sure if we should split the ramp scheduler and the EventData into two separate Ugens. This could potentially cause some confusion. But otherwise i would need to make the ShiftRegister output rate, trigger and sub-sample offset beside the 3-bit and 8-bit output instead of the phase to follow the same architecture. I think having both the EventData and the EventScheduler Ugen is even more confusing.
I have also implemented an attempt for oneshot ramps triggered from the language, which could be subdivided by a sequence of durations for each measure, which needs a different kind of trigger detection inside EventData. One could maybe provide a flag 0: cycle and 1: burst for this.
This would then mean SchedulerCycle outputtig ramps into EventData with flag 0: cycle and SchedulerBurst outputting ramps into EventData with flag 1: burst. Then ShiftRegister should be renamed to SchedulerRungler or something into EventData with flag 0: cycle.
Otherwise we could have EventSchedulerCycle outputting rate, trigger and sub-sampleoffset and EventSchedulerBurst outputting rate, trigger and sub-sampleoffset and EventSchedulerRungler or something. Im currently not really satisfied with these long names.
One downside of splitting the ugens is that the slope and trigger calculation needs the delta = current - last, which means the whole output is delayed by 1-sample. When the ramp generation is in the same ugen as the data, we can run this in the constructor so it starts with an initial slope and trigger. I dont think that 1-sample delay is that big of a problem, but if one then would use the ramp output of the schedulers directly, then this might cause problems.
Iāve been experimenting with various ideas with GrainUtils since its release, and personally, I didnāt find this confusing to have both EventScheduler and EventData. This separation reminds me of the Duty and Demand ugens. It gives sense of modularity. I also like the naming. I do understand the complexity of this design choice tho
hey, thanks ![]()
The problem is that for the RampBurst Ugen you would need a different type of EventData Ugen.
The reason for that is, that you cant use the same trigger detection logic we use for cycling ramps (we dont want an end of burst trigger at the end of the last ramp).
To prevent that trigger from happening you could either:
1.) increment a counter for each subdivision and then use and bool trigger = trigDetect.process(m_phase) && m_triggerLast; where m_triggerLast = m_subdivIndex < cycles; but then you additionally have to pass the number of cycles to the EventData Ugen
2.) or you can use a scaled and then stepped version of our RampBurst Ugen into the EventData Ugen with a different trigger dection logic, but then the output of the RampBurst Ugen cant be normalized between 0 and 1, which i dont like.
Both of these options are not very elegant. For these reasons i guess i will implement one version SchedulerCycle and SchedulerBurst. These then output triggers, rates, subsampleOffset and maybe the ramp itself.
Im currently banging my head against the wall to get the initial trigger right for the SchedulerBurst, currently its 1-sample late. I will then probably implement the ShiftRegister either without the feedback so it just expects a trigger and then just outputs 3-bit and 8-bit or outputs trigger, rate, subsampleoffset, 3-bit, 8-bit and renamed to SchedulerRungler (these multiple outputs are so convoluted).
still a prototype, but it works ![]()
(
var numOfSubDivs = 3;
var arrayOfSubDivs = [4, 1, 7].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 = 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[\rate] / 1000,
//events[\subSampleOffset]
];
}.plot(0.021).plotMode_(\plines);
)
phase starts from 0 and we get an initial trigger:
and no end of burst trigger:
You can then use Pmono or Routines and subdivide your āmeasure phaseā by an array of normalized subdivisions to get audio rate triggers, rates and subsampleoffsets and plug those into the VoiceAllocator.
// Pmono
(
var arrayOfSubDivs = Array.fill(12, { 2 ** rrand(-1.0, 1.0) } ).normalizeSum;
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;
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;
)
i have also decided to get rid of the feedback loop for the ShiftRegister, otherwise we get too many outputs. I think thats cleaner. I will include a more Benjolin/BlippoBox type of Ugen in my DualOscOS repository soon.The new interface will look like this:
(
{
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);
)
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]);
)
Currently we have these UGens:
For the Event System:
SchedulerCycleSchedulerBurstVoiceAllocatorRampIntegrator
we have these window functions:
HanningWindowRaisedCosWindowGaussianWindowTrapezoidalWindowTukeyWindowExponentialWindow
we have these unit shapers:
UnitKinkUnitTriangleUnitCubic
and these interpolating easing functions:
JCurveSCurve
and the ShiftRegister
The only thing which needs a bit more work is the SchedulerBurst, everything else is tested, refined and works as intended. I will additionally add helpfiles for all the Ugens and merge the GrainDelay with the next release when i reset the repository for a clean start.
Currently we also have one guide called āEvent Schedulingā which is an updated version of my Notam guide. All the explanations in there build towards the SchedulerCycle UGen in the end, which outputs all the necessary data (triggers, ramps, rates and sub-sample offsets) for the user and solves the modulation of the trigger rate problem, which is not possible with any UGen from the core library. I really hope this makes it more user friendly to use.
I will also add an updated version of the Notam āGranulation Guideā which leads then nicely to the VoiceAllocator and together with the āEvent Scheduling Guideā gives all the necessary background information about polyphonic server side sequencing and sub-sample accurate granulation.
You can get some nice shepard tones with these ugens, which is just glisson synthesis with a frequency trajectory per grain (mess with the freqModOffset for some different flavours). The point here is that you dont need a dedicated UGen like Shepard for doing this stuff its just the same principles applied and you have more freedom to get this and also other sounds from your Synthdef:
(
var multiChannelPhase = { |numChannels, rate, phaseOffset|
var localRate = rate / numChannels;
numChannels.collect{ |i|
var localPhase;
localPhase = Phasor.ar(DC.ar(0), localRate * SampleDur.ir);
(localPhase + (1 - (i / numChannels * phaseOffset)) - SampleDur.ir).wrap(0, 1);
};
};
SynthDef(\shepard, {
var numChannels = 30;
var reset, tFreq, events, voices;
var grainFreqs, grainPhases, grainWindows, modPhases;
var grains, sig;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreq = \tFreq.kr(1, spec: ControlSpec(1, 500, \exp));
events = SchedulerCycle.ar(tFreq, reset);
voices = VoiceAllocator.ar(
numChannels,
events[\trigger],
events[\rate] / \overlap.kr(30),
events[\subSampleOffset]
);
grainWindows = HanningWindow.ar(
phase: voices[\phases],
skew: \windowSkew.kr(0.01),
);
modPhases = multiChannelPhase.(numChannels, \freqMF.kr(0.8), \freqModOffset.kr(1));
grainFreqs = \freq.kr(440) * (2 ** (modPhases * 2 - 1 * \freqMD.kr(5)));
grainPhases = RampIntegrator.ar(
trig: voices[\triggers],
rate: grainFreqs,
subSampleOffset: events[\subSampleOffset]
);
grains = SinOsc.ar(DC.ar(0), grainPhases * 2pi);
grains = grains * grainWindows;
grains = PanAz.ar(2, grains, \pan.kr(0));
sig = grains.sum;
sig = sig * \amp.kr(-35).dbamp;
sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));
sig = LeakDC.ar(sig);
Out.ar(\out.kr(0), sig);
}).add;
)
(
Routine({
s.bind {
Synth(\shepard, [
\tFreq, 80,
\overlap, 30,
\freqMF, 0.8,
\freqMD, 5,
\freq, 440,
\freqModOffset, 2.931,
\amp, -35,
\out, 0,
]);
};
}).play;
)
Would really like to see what others make with these tools,
feel free to share what you are creating ![]()
I have reset the repository for a clean start. Now the GrainDelay is here as well, additionally i have made some constructor prime magic happening for the SchedulerBurst. We get an initial trigger and yet phase starts from 0 and no end of burst trigger as you want from this approach.
(
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],
events[\trigger]
];
}.plot(0.041).plotMode_(\plines);
)
You can also now find a PlanckWindow in there with adjustable index between 0 and 1 for the taper.
(
{
var phase = Phasor.ar(DC.ar(0), 50 * SampleDur.ir);
PlanckWindow.ar(phase, \skew.kr(0.5), \width.kr(0), \index.kr(0));
}.plot(0.02);
)
The only thing which is missing now are the helpfiles and the one guide, will work on this as well.
added RampAccumulator as well, here its beeing used for buffer granulation:
(
SynthDef(\grains, { |sndBuf|
var numChannels = 8;
var reset, events, voices;
var tFreqMod, tFreq;
var overlapMod, overlap;
var posRateMod, posRate;
var grainRateMod, grainRate;
var grainWindows, accumulator, grainPhases, pos;
var sigs, sig;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreqMod = LFDNoise3.ar(\tFreqMF.kr(1));
tFreq = \tFreq.kr(1) * (2 ** (tFreqMod * \tFreqMD.kr(0)));
events = SchedulerCycle.ar(tFreq, reset);
overlapMod = LFDNoise3.ar(\overlapMF.kr(0.3));
overlap = \overlap.kr(1) * (2 ** (overlapMod * \overlapMD.kr(0)));
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[\trigger],
rate: events[\rate] / overlap,
subSampleOffset: events[\subSampleOffset],
);
grainWindows = HanningWindow.ar(
phase: voices[\phases],
skew: \windowSkew.kr(0.5),
);
///////////////////////////////////////////////////////////////////////////////////
posRateMod = LFDNoise3.ar(\posRateMF.kr(0.3));
posRate = \posRate.kr(1) * (1 + (posRateMod * \posRateMD.kr(0)));
pos = Phasor.ar(
trig: DC.ar(0),
rate: posRate * BufRateScale.kr(sndBuf) * SampleDur.ir / BufDur.kr(sndBuf),
start: \posLo.kr(0),
end: \posHi.kr(1)
);
pos = Latch.ar(pos, voices[\triggers]) * BufFrames.kr(sndBuf);
///////////////////////////////////////////////////////////////////////////////////
grainRateMod = LFDNoise3.ar(\grainRateMF.kr(0.3));
grainRate = \grainRate.kr(1) * (2 ** (grainRateMod * \grainRateMD.kr(0)));
accumulator = RampAccumulator.ar(
trig: voices[\triggers],
subSampleOffset: events[\subSampleOffset]
);
grainPhases = Latch.ar(grainRate, voices[\triggers]) * accumulator;
///////////////////////////////////////////////////////////////////////////////////
sigs = BufRd.ar(
numChannels: 1,
bufnum: sndBuf,
phase: grainPhases + pos,
loop: 1,
interpolation: 4
);
sigs = sigs * grainWindows;
sigs = PanAz.ar(2, sigs, \pan.kr(0));
sig = sigs.sum;
sig = sig * \amp.kr(-25).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;
)
~sndBuf = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
Synth(\grains, [
\tFreq, 100,
\tFreqMF, 0.3,
\tFreqMD, 0,
\overlap, 8,
\overlapMF, 0.3,
\overlapMD, 0,
\windowSkew, 0.5,
\grainRate, 1,
\grainRateMF, 0.3,
\grainRateMD, 0,
\posRate, 1.0,
\posRateMF, 0.3,
\posRateMD, 0,
\posLo, 0,
\posHi, 1,
\sndBuf, ~sndBuf,
\amp, -15,
]);
)
I have updated the GrainUtils with the guide on granulation Release GrainUtils v1.0.0 Ā· dietcv/GrainUtils Ā· GitHub
These two guides are trying to explain why we need continuous, linear ramps between 0 and 1 even when we want to modulate our trigger frequency of our scheduling phasor (Event Scheduling) and dynamic voice allocation for overlapping grains of unequal lengths (Voice Allocation).
Im not a teacher i hope that my explanations make sense, if you feel lost with some aspects of these feel free to reach out. The individual help files for the classes will follow.
You can now find both guides via:
Sub-Sample Accurate Granulation - Event Scheduling
Sub-Sample Accurate Granulation - Voice Allocation
I have updated the release, now you can find a helpfile for SchedulerCycle, SchedulerBurst and VoiceAllocator. These could potentially be refined with some cross references but its a start ![]()


