I have been trying to implement the bandlimited saw with hardsync from the GO book in SC.
Its mostly about calculating the subsample offset of the naive phasors wrap. Some things to consider with this approach are:
The phasors ramp in discrete time has no infinite resolution. The resolution is determined by the sample rate, which is an approximation of the ideal phasor in continous time.
In the moment the phasor wraps around it doesnt make a big jump from 1 to 0 but instead moves continuously from 1 to 0 over one sample. This transition does not perfectly align with the sample rate and therefore doesnt perfectly reaches 0. These slight deviations add up with every cycle and the higher the phasors frequency the bigger the deviations for every cycle. These create irregular harmonic periods and therefore introduce unrelated frequency content into the phasors sound.
The simple idea for bandlimiting in this case is instead of trying to synthesize an ideal phasor, to synthesize a function that already has the single-sample slope requirement embedded into it. This function should be independent from the sample points and is used to place the slope precisely centered on the ideal phasors transition point rather than the sampling grid.
To do that you calculate the values of the ideal phasor one-half a sample frame before and after it has a transition and the subsample location of the ideal transition relative to the sampling grid to get the ramp between these two corner points and replace the naive phasor when it is in its “wrapping frame” with a “replacement phasor” taken from these calculations and then you add hard sync
the patch looks like this:
for a frequency of 2000 hz without hard sync it sounds like this:
raw phasor:
antialiased:
thats my current attempt, there is no switch Ugen in SC so i have used Select. The output is anti-aliased somehow but not as good as the audio examples from the gen patch above. if you have any further ideas let me know
(
var rampToSlope = { |phase|
var delta = Slope.ar(phase) * SampleDur.ir;
delta.wrap(-0.5, 0.5);
};
var rampRotate = { |phase, offset|
(phase - offset).wrap(0, 1);
};
var getSubSampleOffset = { |phase|
var slope = rampToSlope.(phase);
var sampleCount = phase - (slope < 0) / slope;
var trig = Trig1.ar((sampleCount < 1) * slope, SampleDur.ir);
var subSample = Latch.ar(sampleCount, trig);
(subSample: subSample, trig: trig);
};
var accumulatorSubSample = { |trig, subSampleOffset|
var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1));
accum + subSampleOffset;
};
{
var rate, slope, syncPhasor, syncTrig, subSampleOffset, subSampleOffsetAndTrig, accumulator;
var corePhasor, loopSubSample, mix, beforeTrans, afterTrans;
var trig, replaceBefore, replaceAfter, sig;
rate = \rate.kr(205);
slope = rate * SampleDur.ir;
syncPhasor = Phasor.ar(DC.ar(0), \syncRate.kr(60) * SampleDur.ir);
subSampleOffsetAndTrig = getSubSampleOffset.(syncPhasor);
subSampleOffset = subSampleOffsetAndTrig[\subSample];
syncTrig = subSampleOffsetAndTrig[\trig];
accumulator = accumulatorSubSample.(syncTrig, subSampleOffset);
corePhasor = (slope * accumulator).wrap(0, 1);
loopSubSample = corePhasor.wrap(-0.5, 0.5) + (slope * 0.5) / slope;
mix = Select.ar(syncTrig, [loopSubSample, subSampleOffset]);
beforeTrans = rampRotate.(corePhasor, mix * slope);
afterTrans = rampRotate.(corePhasor, mix - 1 * slope);
trig = (mix >= 0) * (mix < 1);
replaceBefore = Select.ar(trig, [corePhasor, beforeTrans]);
replaceAfter = Select.ar(trig, [corePhasor, afterTrans]);
sig = LinXFade2.ar(replaceBefore, replaceAfter, mix.clip(0, 1));
//sig = sig * 2 - 1;
sig = LeakDC.ar(sig);
sig!2 * 0.03;
}.play;
)
that beeing said i think most of this stuff is better done in C++
EDIT: keeping track of the subsample offset and resetting the phasor not to 0 but to the subsample offset location reduces aliasing for higher rates quite efficienctly, see my other thread:
granulation of a sine carrier with a hanning window and a trigger rate 1000 hz without subsample offset:
granulation of a sine carrier with a hanning window and a trigger rate 1000 hz with subsample offset:
thats one benefit of having a source of time which has a continuous slope like a phasors ramp, so you know at every time exactly what slope you have instead of having triggers without knowing when they happen or when the last time was they happened.