Trying to re-create the Max rate~ object

I’m trying to write something that works like the rate~ object in Max in order to create a phasor synced system. This is what i’ve come up with so far (based on a Max patch found here), and it almost works, but the two phasors in this example are not in sample correct sync (obviously). I suspect i need to compensate for block sizes introduced by the Delays and LocalIns and LocalOuts, possible roundoff errors or similar? Or are there better ways to implement this in SC? Oh, and I need everything to be audio rate.

(
x = SynthDef(\rate, {
	var sr, bs, mstPhasor, slvPhasor, delta, prevSmp, rate, a, b;
	sr = SampleRate.ir;
	bs = BlockSize.ir;
	mstPhasor = Phasor.ar(Impulse.ar(1), 1 / sr);
	rate = Latch.ar(\rate.ar(1), mstPhasor < 0.5) * bs;
	prevSmp = Delay1.ar(mstPhasor);
	delta = mstPhasor - prevSmp;
	slvPhasor = Select.ar(delta < 0, [delta, Delay1.ar(delta)]);
	slvPhasor = slvPhasor * rate;
	slvPhasor = Delay1.ar(slvPhasor) + LocalIn.ar(1);
	LocalOut.ar(slvPhasor);
	slvPhasor = slvPhasor % 1.0;
	a = Saw.ar(300 + (mstPhasor * 300), 0.1);
	b = Saw.ar(300 + (slvPhasor * 300), 0.1);
	Out.ar(0, [a, b]);
}).play;
)
x.set(\rate, 2);

Added a Latch on the rate change to keep the rate changes in sync. It’s still drifting though.

I am not sure I understand what your code is trying to do, and I don’t know what ~rate does, but wouldn’t this do what you describe?

phasorB = (phasorA * factor).wrap(0, 1)

The original rate~ object time stretches or time compress a phasor. So, i guess this works for scaling factors that are <1 (and divide the rate by powers of 2) but not >1 because they would restart phasorB from 0 when phasorA restarts.

you can get the slope of your original ramp, run an accumulator and subdivide by a factor and wrap between 0 and 1. Your subdivided phase can now be slower then your original phasor, but might get out of sync.

(
var rampToSlope = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	delta.wrap(-0.5, 0.5);
};

{
	var phase, slope, trigger, accumulator, phaseDiv;
	
	phase = (Phasor.ar(0, \rate.kr(100) * SampleDur.ir) - SampleDur.ir).wrap(0, 1);
	slope = rampToSlope.(phase);
	
	accumulator = Duty.ar(SampleDur.ir, 0, Dseries(0, 1));
	
	phaseDiv = (slope * \ratio.kr(0.5) * accumulator).wrap(0, 1);
	
	[phase, phaseDiv]

}.plot(0.021)
)

Using the factor and wrap, you could derive any number of phasors from a non-resetting master phase that continuously increments, unless you run the system for a very long time (days?). Using your original sonification above, it would look like:

(
Server.default.waitForBoot({
    play {
        var phase;
        var derivedSlow, derivedUnity, derivedFast;
        var a, u, b;

        phase = Phasor.ar(DC.ar(1), SampleRate.ir().reciprocal(), (60 * 60 * 24), inf); // starting 24 hours in seems fine

        derivedUnity = (phase * 1).wrap(0, 1);
        derivedSlow = (phase * (1/8)).wrap(0, 1);
        derivedFast = (phase * 2).wrap(0, 1);

        phase.poll();

        a = Saw.ar(300 + (derivedSlow * 300), 0.1);
        u = Saw.ar(300 + (derivedUnity * 300), 0.1);
        b = Saw.ar(300 + (derivedFast * 300), 0.1);

        Out.ar(0, Splay.ar([a, u, b]));
    };
});
)

Hm. Yes, i actually started out this project with a Sweep as a master, which i guess is similar to a Phasor with inf as end value. The thing i’m trying to do here is a modular system of phasors where i can build a tree of sub-phasors (which i used to do quite often during my Max days using rate~). So, what i should’ve told you is that the slavePhasor in my example need to be able to work as master to other sub-phasors and i guess they all need to have a range between 0-1.

Thanks! But why is it getting out of sync? And why can’t we have sample accuracy here?

Do you have the book on gen~ “generating sound & organizing time” ?
There it is said: “Unlike the ramp multiplication we explored earlier in this chapter, we can work with divisions that are longer than the input ramp without beeing reset. However, even with simple divisions, with this patch, there is no guarantee that the output ramps cycle will remain phase-synchronized with the input ramp. Even if they start synchronized, modulations to the ratio can cause them to drift”

If you dont modulate the rate of your main ramp or the division / multiplication of your subramps you might be fine. The patch from the book has a sync logic to sync the derived ramps to the main ramp when the ratio changes by an significant amount by detecting a proportional change above a certain threshold. This issue has nothing to do with the programming language you are using.

1 Like

You can output both the phasor and the phase from each derived stage and sync further derived sub stages from the phase and not the phasor, like so:

(
Server.default.waitForBoot({
    var derive;

    derive = { |phaseIn, factor|
        var phasor, phaseOut;

        phaseOut = phaseIn * factor;
        phasor = phaseOut.wrap(0, 1);

        [phasor, phaseOut]
    };

    play {
        var phase;
        var derivedAPhasor, derivedAPhase;
        var derivedBPhasor, derivedBPhase;

        phase = Phasor.ar(DC.ar(1), SampleRate.ir().reciprocal(), 0, inf);

        #derivedAPhasor, derivedAPhase = derive.(phase, 3);
        #derivedBPhasor, derivedBPhase = derive.(derivedAPhase, 2);

        a = Saw.ar(300 + (derivedAPhasor * 300), 0.1);
        b = Saw.ar(300 + (derivedBPhasor * 300), 0.1);

        Out.ar(0, [a, b]);
    };
});
)
1 Like

Sounds like an interesting book. I don’t have that. Will check it out.
I have tried implementing a sync logic that resets the phase using a Latch and subtraction. It seems the resets causes slight offsets to the sub-ramps though. I will have to investigate this further.

Thanks! This might work as a basis for what i’m trying to do i think. But is there a way to compensate for the offset caused by a rate change? I can’t figure out what’s happening here because the offset is different with every change it seems (looking at my phase oscilloscope).

(
Server.default.waitForBoot({
    var derive;

    derive = { |phaseIn, factor|
        var phasor, phaseOut;

        phaseOut = phaseIn * factor;
        phasor = phaseOut.wrap(0, 1);

        [phasor, phaseOut]
    };

    x = play {
        var phase;
        var derivedAPhasor, derivedAPhase;
        var derivedBPhasor, derivedBPhase;

        phase = Phasor.ar(DC.ar(1), SampleRate.ir().reciprocal(), 0, inf);

        #derivedAPhasor, derivedAPhase = derive.(phase, 1);
        #derivedBPhasor, derivedBPhase = derive.(derivedAPhase, \rate.ar(1));

        a = Saw.ar(300 + (derivedAPhasor * 300), 0.1);
        b = Saw.ar(300 + (derivedBPhasor * 300), 0.1);

        Out.ar(0, [a, b]);
    };
});
)
x.set(\rate, 0.5);
x.set(\rate, 1);

Here you see the go.ramp.div abstraction from the book which is comparable to the rate~ object in max.

The most important point is when to sync and what to sync to.
For implementing the go.latchsync abstraction you need a single sample feedback loop.

1 Like

Maybe you could do something with Select and Changed.

Great! Thank you!
I tried this, which kind of works, if you ignore the constant offset that i mention above:
rate = Latch.ar(\rate.ar(1), mstPhasor < 0.5);

have you considered using a different function to get triggers from ramps then using mstPhasor < 0.5?
This will not give you a single sample trigger on the phasors wrap, but a gate signal:

(
{
	var phase = Phasor.ar(0, 100 * SampleDur.ir);
	[phase, phase < 0.5];
}.plot(0.02);
)

grafik

One option could be to start your phase one sample earlier and use this rampToTrig function:

(
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 phase = (Phasor.ar(0, 100 * SampleDur.ir) - SampleDur.ir).wrap(0, 1);
	var trig = rampToTrig.(phase);
	[phase, trig];
}.plot(0.02);
)


The phasor should start one sample earlier, because the calculation for getting the delta is based on the current and the last sample. If your phase starts at 0, the rampToTrig function will still output a trigger but its one sample late from the phasors wrap. Here i have zoomed in on the phasors wrap and you see that the trigger is aligned with the phasors wrap if you start your phase one sample earlier.

grafik

Another option is to use HPZ1.ar(phase) < 0 and add an additional initial trigger with Impulse.ar(0)
This caused me some inaccurancy if you additionally calculate the slope with last and current sample using Delay1 and accumulate new phases by using that slope. They are one sample off from the main ramp, so i decided to base both the trigger and the slope calculation on Delay1 and start my main ramp one sample earlier. But using HPZ1 with an additional initial trigger might work for you as well.

(
{
	var phase = Phasor.ar(0, 100 * SampleDur.ir);
	var trig = HPZ1.ar(phase) < 0 + Impulse.ar(0);
	[phase, trig];
}.plot(0.02);
)
2 Likes

This is very useful stuff indeed. Thanks for sharing!