hey, i dont know whats up with Poll, but i have investigated the same topic in my thread for sub-sample accurate granulation
A segmented envelope like EnvGen
is divided into discrete time segments. The output depends on which segment is currently active. When beeing modulated at audio rate it can introduce discontinuities at segment boundaries and become unstable due to internal state transitions. For modulation i would recommend stateless functions which are driven by a ramp signal between 0 and 1. Their output only depend on the instantaneous input of the ramp signal.
The ways i know to achieve the desired behaviour for a continuous, linear ramp signal between 0 and 1 are:
1.) to derive a trigger from its own wrap to latch the modulation of its own rate
2.) to derive a trigger from its own wrap to increment a counter which looks up the next value in a list to be used as the next slope
When the specifications are linear ramps between 0 and 1 from a continuous phasor (no phase reset), its only possible with a single-sample feedback loop.
I have tried to model a solution i found in gen~ with different attempts:
Here is a little detour on the context for the approaches i have tried out (find some of them below, the last one is an attempt with LFO modulation. If the following requirements dont matter for your use case, one of these approaches might be working for you).
In the context of deriving sub-sample accurate events from a continuous ramp signal between 0 and 1, the following specifications have to be met:
The scheduling phasor (your clock) should be a continuous ramp between 0 and 1.
In this picture, you see that the scheduling phasor does have a value of non zero at the moment its wraps from 1 to 0. This value (the subsample offset) is calculated for each sample frame the scheduling phasor wraps to reset your accumulator to the sub sample offset, which then drives your grain window for sub-sample accurate granulation.
1.) Your scheduling phasor only has a non-zero value and the moment it wraps around if its not beeing reset to 0 by a trigger to calculate the sub-sample offset. Therefore you cannot reset it by a trigger.
2.) To have precise timing and distribute your linear phases between 0 and 1 correctly round robin accross the channels, the phase wrap of your scheduling phasor and the new slope derived from random periods have to be 100% in sync. The only way you can ensure this in SC, is to reset your scheduling phasor, which doesnt fullfill the first requirement.
3.) In SC calculations are done with 32bit floats, every attempt whichs needs to calculate the new slope of your scheduling phasor on a sample level (e.g. Duty
), will therefore cause floating point number errors which accumulate over time and give you nasty sounding aliasing (have tested that with no randomness for rates which divide your sample rate to perfect integers (works of course) and tried out the alternative to round your samples per period using .round
or .floor
which changes the pitch and isnt as sub-sample accurate as using an ordinary Phasor
for the case no randomness).
The attempts i have tried are:
- use
Duty
for random periods, derive a trigger from it withChanged
, latch the random periods with that trigger and resetSweep
where you plugin your random periods as an argument directly (doesnt fullfill the first requirement andSweep
currently has a bug not starting from sample count 0 and is therefore unrealiable for testing) - use
Duty
for random periods, derive a trigger from it withChanged
, latch the random periods with that trigger to be used as an argument forPhasor
without any reset ofPhasor
(doesnt fullfill the second requirement) - use
Duty
for random periods, derive a trigger from it withChanged
, latch the random periods with that trigger to be used as an argument forPhasor
and resetPhasor
with that trigger (for randomness > 0 this attempt is timing inaccurate and therefore doesnt distribute the phases correctly accross the channels and doesnt fullfill the first requirement). - use
Duty
for random periods, derive a trigger from it withChanged
, latch the random periods with that trigger and reset your accumulator multiplied by that random period (doesnt fullfill the first and the third requirement) - use
Duty
for random periods and to accumulate samples multiplied by your random periods to output linear ramps between 0 and 1 insideDuty
directly, either reset by the random periods themselves orTDuty
(doesnt fullfill the first and the third requirement) - use
Duty
for random periods and to accumulate samples multiplied by your random periods to output linear ramps between 0 and 1 insideDuty
directly, but not reset by a trigger but with modulus 1 (doesnt fullfill the second and the third requirement)
// random periods with duty, derived trigger latching the slope, into phasor (no phase reset)
(
var randomPeriods = { |rate, randomness|
var randomPeriod = Ddup(2, (2 ** (Dwhite(-1.0, 1.0) * randomness))) / rate;
Duty.ar(randomPeriod, DC.ar(0), 1 / randomPeriod);
};
var rampFromRandom = { |rate, randomness|
var initTrigger = Impulse.ar(0);
var randomPeriod = randomPeriods.(rate, randomness);
var trig = Changed.ar(randomPeriod) > 0 + initTrigger;
var slope = Latch.ar(randomPeriod, trig) * SampleDur.ir;
Phasor.ar(DC.ar(0), slope);
};
{
rampFromRandom.(\tFreq.kr(200), \randomness.kr(1));
}.plot(0.02);
)
// random periods with duty, derived trigger latching the slope, accumulating phasor with accum * slope (with phase reset)
(
var randomPeriods = { |rate, randomness|
var randomPeriod = Ddup(2, (2 ** (Dwhite(-1.0, 1.0) * randomness))) / rate;
Duty.ar(randomPeriod, DC.ar(0), 1 / randomPeriod);
};
var rampFromRandom = { |rate, randomness|
var initTrigger = Impulse.ar(0);
var randomPeriod = randomPeriods.(rate, randomness);
var trig = Changed.ar(randomPeriod) > 0 + initTrigger;
var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1));
var slope = randomPeriod * SampleDur.ir;
slope * accum;
};
{
rampFromRandom.(\tFreq.kr(200), \randomness.kr(1));
}.plot(0.02);
)
// random periods, durations and level with duty (with phase reset)
(
var rampFromRandom = { |rate, randomness|
var randomPeriod = Ddup(2, 2 ** (Dwhite(-1.0, 1.0) * randomness)) / rate;
Duty.ar(
// update every sample
dur: SampleDur.ir,
// reset every randomPeriod
reset: randomPeriod,
// "sample and hold" increment of (SampleDur.ir / randomPeriod) for every randomPeriod
// to increment for each sample by the sample count multiplied by slope modulus 1
level: Ddup(SampleRate.ir, SampleDur.ir / randomPeriod) * Dseries(0, 1) % 1
);
};
{
rampFromRandom.(\rate.kr(200), \randomness.kr(1));
}.plot(0.021);
)
// modulation of slope with an LFO, with Duty as a sample and hold, derived trigger latching a slope, into phasor (no phase reset)
(
var rampAndHold = { |rate|
var initTrigger = Impulse.ar(0);
var period = Duty.ar(1 / rate, DC.ar(0), rate);
var trig = Changed.ar(period) > 0 + initTrigger;
var slope = Latch.ar(period, trig) * SampleDur.ir;
Phasor.ar(DC.ar(0), slope);
};
{
var rateMod = 2 ** (SinOsc.ar(50) * \modDepth.kr(1));
var rate = \rate.kr(200) * rateMod;
rampAndHold.(rate);
}.plot(0.02);
)
With all the attemps from above which are not resetting the phase, you dont sync the wrapping of the ramp signal with the scheduling of the new slope value. These are parallel processes but should not be parallel but in succession (e.g. single-sample feedback loop). The trigger derived from the phasors wrap should determine when its time to pick a new slope value for its next cycle.
With these parallel attempts and without a phase reset you are often times wrapping the phasor, where the next slope value after the wrap is still the old slope value before the wrap and then the slope value is updated somewhere during the current cycle, which leads to a kink in slope for the Phasor (look at the plot, where you have the phasors wrap from 1 to 0 in red followed by the spike in blue of the single sample trigger dervied via Changed
from Duty
which updates the slope). This doesnt look like a big deal on the plot (its just one sample late), but in the case of deriving slopes and accumulating ramps from the scheduling phasor, which are reset by the derived trigger (like im doing for sub-sample accurate events), it entirely messes up the distribution of derived events.