hey, while creating the GrainDelay
, i have figured out a way to implement a dynamic VoiceAllocator
. This comes in handy for server side polyphony (especially for granulation), instead of using the round-robin method (increment a counter for every received trigger and distribute the next voice to the next channel), the VoiceAllocator
finds a channel which is currently free and schedules the next event on that channel, if no channels are available the event gets dropped.
You can grab the latest release here
The EventScheduler
has two arguments, a triggerRate
and reset
and outputs:
- a derived trigger on output[0]
- a linear ramp between 0 and 1 on output[1]
- a derived and latched rate in hz on output[2] output[1]
- a derived sub-sample-offset on output[3] output[2]
its own rate is sampled and held for every ramp cycle, which means we make sure the internal ramps are linear and between 0 and 1, while beeing modulated.
The VoiceAllocator
has four arguments, numChannels
, a trigger
, a rate
and a sub-sample offset
and outputs:
- an array of sub-sample accurate phases
- an array of triggers
This setup then allows you to:
- modulate the trigger rate of the
EventScheduler
, without distorting its phase and calculate thesub-sample offset
for one of its outputs - distribute the events across the channels via the
VoiceAllocator
and make sure the channel where you distribute your next voice to, is currently free.
These things in combination then enable:
- trigger frequency modulation for audio ratchets without distorting the phase
- overlapping grains while the events have different durations without distorting the phase
Currently the interface is a bit convoluted, but i hope i can think this through (try out adjusting the numChannels
, and look at the plot when adjusting tFreqMD
and overlapMD
):
(
{
var numChannels = 5;
var reset, tFreqMD, tFreq;
var overlapMD, overlap;
var events, voices, phases, triggers;
var sig;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreqMD = \tFreqMD.kr(2);
tFreq = \tFreq.kr(400) * (2 ** (SinOsc.ar(50) * tFreqMD));
overlapMD = \overlapMD.kr(0);
overlap = \overlap.kr(1) * (2 ** (SinOsc.ar(50) * overlapMD));
events = EventScheduler.ar(tFreq, reset);
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[0],
rate: events[2] / overlap,
subSampleOffset: events[3],
);
phases = voices[0..numChannels - 1];
triggers = voices[numChannels..numChannels * 2];
phases;
}.plot(0.041);
)
In the context of granular synthesis this setup allows you to generate windowPhases
to drive an arbitrary stateless window function (have a look here) from your multichannel phase output of VoiceAllocator
, while using the sub-sample offset output of EventScheduler
and the multichannel trigger output of VoiceAllocator
to accumulate or integrate your grainPhases
which are driving your carrier oscillator.
The reason we cant put this all together in one Ugen is, that we want to be able to use the multichannel windowPhases
from VoiceAllocator
to be able to drive modulators used for FM of the multichannel grain frequencies and we additionally need the multichannel trigger from VoiceAllocator
and the subsample offset from EventScheduler
to pass it to the grainPhase
accumulator / integrator to reset its phase and add the sub-sample offset.
Here is a still bit convoluted test example, where i have been adding the multichannel accumulator for the grainphase manually in sc:
(
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 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 sig;
var grainFreqMod, grainFreqs, grainSlopes, grainPhases, sigs;
var grainWindows;
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 ** (SinOsc.ar(0.3) * overlapMD));
events = EventScheduler.ar(tFreq, reset);
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[0],
rate: events[2] / overlap,
subSampleOffset: events[3],
);
windowPhases = voices[0..numChannels - 1];
triggers = voices[numChannels..numChannels * 2 - 1];
grainWindows = HanningWindow.ar(windowPhases, \skew.kr(0.1));
grainFreqMod = multiChannelDwhite.(triggers);
grainFreqs = \freq.kr(440) * (2 ** (grainFreqMod * \freqMD.kr(2)));
grainSlopes = grainFreqs * SampleDur.ir;
grainPhases = (grainSlopes * multiChannelAccumulator.(triggers, Latch.ar(events[3], triggers))).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;
)
If you have additional ideas let me know