Lets figure out where the difference between these two approaches is.
Lets start with subsample accurate events from phasors for now (this really took me some time to think through, so we can discuss the other approach in the upcoming days and then finally come to your questions haha).
For scheduling events with continous waveforms you need a carrier phase which should reset to zero whenever your scheduling phase completes a cycle. An implementation of this naive hard sync causes a jump in the phase of the carrier whenever the scheduler completes a cycle. This jump in phase leads to a hard edge in the carrier waveform and therefore causes harsh aliasing noise. The first step of getting rid of this hard edge whenever the scheduler forces the carrier to wrap around is to fade out the carrier waveform at the moment the sync happens by applying a window function. This is called windowed sync and is the basic concept of granulation.
In the context of granulation you have a grain scheduler, a phasor going from 0 to 1 and you derive triggers from it to schedule your grain events by calculating the delta of your phasors slope to figure out when the absolute delta is above a certain threshold (the moment the grain scheduler wraps) to reset your carrier phase and applying a window function to fade out the carrier waveform at exactly that moment to smooth out the hard edge the reset causes.
The ideal scheduling phasors wrap (red) happens somewhere between one sample frame and the next (grey region). Calculating the scheduling phasors slope by taking its sample values from the last sample frame and its current sample frame (pink) and to look if this delta is above a certain threshold derives a trigger from the scheduling phasor (blue) which resets the carrier phase exactly to zero at the actual scheduling phasors wrap (marked by the red arrow) and schedules a window function starting from zero at exactly that moment (the dashed line from the book is a mistake IMO). But within that sample frame, where the actual scheduling phasor wraps the sample value is slightly above zero, it deviates from zero by a small amount (orange) and the ideal scheduling phasors wrap happens with a fractional offset from the actual scheduling phasors wrap (green).
The carrier and the window function (dotted line) should have a value of zero at the ideal scheduling phasors wrap to be perfectly aligned with the ideal phasor (red). The actual phasors wrap (marked with the red arrow) should therefore not reset the carrier and the window function to zero but to the fractional sub-sample offset (orange).
Without adding the subsample offset the window function starts from zero at the scheduling phasors wrap.
When adding the subsample offset the value of the window function at the scheduling phasors wrap is equal to the subsampleoffset
In the context of granulation, resetting the accumulator to the sub-sample offset, works perfectly to prevent aliasing for high trigger rates.
(
var rampToSlope = { |phase|
var history = Delay1.ar(phase);
var delta = (phase - history);
delta.wrap(-0.5, 0.5);
};
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 getSubSampleOffset = { |phase, trig|
var slope = rampToSlope.(phase);
var sampleCount = phase - (slope < 0) / slope;
Latch.ar(sampleCount, trig);
};
var accumulatorSubSample = { |trig, subSampleOffset|
var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1));
accum + subSampleOffset;
};
{
var triggerFreq = \triggerFreq.kr(2000);
var eventPhase = Phasor.ar(DC.ar(0), triggerFreq * SampleDur.ir);
var eventSlope = rampToSlope.(eventPhase);
var eventTrigger = rampToTrig.(eventPhase);
var subSampleOffset = getSubSampleOffset.(eventPhase, eventTrigger);
var accumulator = accumulatorSubSample.(eventTrigger, subSampleOffset); // <-- sub-sample accurate
//var accumulator = accumulatorSubSample.(eventTrigger, 0); // <-- naive version
var windowSlope = Latch.ar(eventSlope, eventTrigger) / max(0.001, \overlap.kr(1));
var windowPhase = (windowSlope * accumulator).clip(0, 1);
var window = 1 - cos(windowPhase * 2pi) / 2;
var grainSlope = \grainFreq.kr(4000) * SampleDur.ir;
var grainPhase = (grainSlope * accumulator).wrap(0, 1);
var grain = sin(grainPhase * 2pi);
var sig = grain * window;
sig = LeakDC.ar(sig);
sig!2 * 0.1;
}.play;
)