# Syncing GrainBuf with an LFO that modulates its playback rate

I want to modulate the playback rate of a buffer without changing the pitch (i.e. time-stretching). So I am playing it through GrainBuf, and using a SinOsc as an LFO to modulate the phasor rate. This is simple enough, but the tricky part is that I want the period of the LFO to exactly match the period of the modulated buffer so that when it plays in a loop, it always slows down and speeds up at the exact same parts of the buffer. If you run this code, you will hear the effect that I’m going for:

``````b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

(
SynthDef(\bufstretch, {|buf|
var bufdur, maxrate, minrate, lfo_period,
phasor_rate, phasor, graintrig, graindur, sig;
bufdur = BufDur.ir(buf); // 4.28329
maxrate = 4.0;
minrate = 0.1;
lfo_period = 2.09;
// I found the correct lfo_period through trial and error.
// Can it be calculated as a function of maxrate, minrate, and bufdur?
phasor_rate = SinOsc.ar(lfo_period.reciprocal, pi/2).range(minrate, maxrate);
phasor = Sweep.ar(1, phasor_rate / bufdur);
graintrig = Impulse.ar(\trigrate.kr(20));
graindur = \trigrate.kr.reciprocal * \overlap.kr(2);
sig = GrainBuf.ar(2, graintrig, graindur, buf, BufRateScale.ir(buf), phasor);
Out.ar(0, sig);
)

Synth(\bufstretch, [buf: b]);
``````

You can see in my code that I have hard-coded `lfo_period = 2.09`, which I arrived at through trial and error. This is the approximate duration of the buffer after it has undergone one period of modulation. But I would like to be able to calculate `lfo_period` as a function of `maxrate`, `minrate`, and `bufdur` so that I can change these values and still maintain perfect sync between the buffer and LFO. I think it is possible to do so with some calculus, but unfortunately my calculus skills are extremely rusty. Does anyone know how to solve this?

1 Like

I think I figured out a solution. I use `Integrator` in combination with `Sweep` to find the average of all phasor_rate values and assign this to the variable `integrated_mean`. I figured the correct `lfo_rate` should be `integrated_mean / bufdur`, so I use `LocalOut`/`LocalIn` to feedback this value to `lfo_rate`. I didn’t think it would work, but it does!

``````b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

(
SynthDef(\bufstretch, {|buf|
var bufdur, maxrate, minrate, lfo_rate, integrated_mean, phasor_avg,
phasor_rate, phasor_rate_inv, phasor, graintrig, graindur, sig;
bufdur = BufDur.ir(buf); // 4.28329
maxrate = \maxrate.ir(4.0);
minrate = \minrate.ir(0.1);
lfo_rate = LocalIn.ar(1).poll(label: "calculated lfo rate");
phasor_rate = SinOsc.ar(lfo_rate, \phase.kr(pi/2)).range(minrate, maxrate);
phasor_rate_inv = phasor_rate.linlin(minrate, maxrate, maxrate, minrate);
phasor_avg = (phasor_rate + phasor_rate_inv) / 2;
integrated_mean = Integrator.ar(phasor_avg) / Sweep.ar(0, SampleRate.ir);
LocalOut.ar(integrated_mean / bufdur);
phasor = Sweep.ar(1, phasor_rate / bufdur);
graintrig = Impulse.ar(\trigrate.kr(20));
graindur = \trigrate.kr.reciprocal * \overlap.kr(2);
sig = GrainBuf.ar(2, graintrig, graindur, buf, BufRateScale.ir(buf), phasor);
Out.ar(0, sig);
)

// some tests. These work well:
Synth(\bufstretch, [buf: b]);
Synth(\bufstretch, [buf: b, maxrate: 5.0, minrate: 0.5, phase: pi]);
Synth(\bufstretch, [buf: b, maxrate: 3.0, minrate: 0.2, trigrate: 100]);

// doesn't sound in sync with fast rates...
Synth(\bufstretch, [buf: b, maxrate: 15.0, minrate: 4.0, phase: 3pi/2]);

// ...unless you also use a very fast trigrate in the GrainBuf:
Synth(\bufstretch, [buf: b, maxrate: 15.0, minrate: 4.0, phase: 3pi/2, trigrate: 200]);
``````

Now the only issue I have is that `maxrate` and `minrate` can’t be changed on the fly because `Integrator` would still use previous values to calculate the mean. Unless it is possible to reset `Integrator` when a new value is received for `maxrate` or `minrate`? Is this possible? I’m still interested in an analytical/calculus solution as well, but this is looking like some kind of differential equation or something beyond my pay grade.

You have to reset both the numerator and denominator of the integrated mean (the `Integrator` and the `Sweep`). Resetting the integrator is accomplished by setting the integration coefficient to zero using a trigger, and `Sweep` already has a reset argument.

``````(
SynthDef(\bufstretch, {|buf|
var bufdur, maxrate, minrate, changed, lfo_rate, integrated_mean, phasor_avg,
phasor_rate, phasor_rate_inv, phasor, graintrig, graindur, sig;
bufdur = BufDur.ir(buf); // 4.28329
maxrate = \maxrate.kr(4.0);
minrate = \minrate.kr(0.1);
changed = Changed.kr(maxrate) + Changed.kr(minrate) / 2;
lfo_rate = LocalIn.ar(1).poll(label: "calculated lfo rate");
phasor_rate = SinOsc.ar(lfo_rate, \phase.kr(pi/2)).range(minrate, maxrate);
phasor_rate_inv = phasor_rate.linlin(minrate, maxrate, maxrate, minrate);
phasor_avg = (phasor_rate + phasor_rate_inv) / 2;
integrated_mean = Integrator.ar(phasor_avg, 1 - changed) / Sweep.ar(changed, SampleRate.ir);
LocalOut.ar(integrated_mean / bufdur);
phasor = Sweep.ar(1, phasor_rate / bufdur);
graintrig = Impulse.ar(\trigrate.kr(20));
graindur = \trigrate.kr.reciprocal * \overlap.kr(2);
sig = GrainBuf.ar(2, graintrig, graindur, buf, BufRateScale.ir(buf), phasor);
Out.ar(0, sig);
)

x = Synth(\bufstretch, [buf: b]);
x.set(\maxrate, 2);
x.set(\minrate, 0.6);
``````
4 Likes

Ah, I totally forgot about `Changed`. That works perfectly! Thanks so much for your help

One catch here is that Integrator’s reset trigger is control rate, not audio rate. An audio rate trigger occurring in the middle of a control block will not reset it.

If you need an audio rate reset:

``````var reset_trig = /* some audio-rate trigger */;
var inverse_trig = reset_trig <= 0;

// audio-rate resettable integrator
FOS.ar(phasor_avg, inverse_trig, DC.ar(0), inverse_trig)
``````

For FOS or to update its coefficients at audio rate, all inputs must be audio rate. If `reset_trig` is ar, then `inverse_trig` will also be ar.

hjh

2 Likes

Thank you, James! It definitely finds the “correct” `lfo_rate` much more quickly when using `FOS.ar` and audio rate controls. Here is my updated version:

``````b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");

(
SynthDef(\bufstretch, {|buf|
var bufdur, maxrate, minrate, lfo_rate, phasor_avg,
reset_trig, inverse_trig, integral, integrated_mean,
phasor_rate, phasor_rate_inv, phasor, graintrig, graindur, sig;
bufdur = BufDur.ir(buf);
maxrate = \maxrate.ar(4.0);
minrate = \minrate.ar(0.1);
reset_trig = Changed.ar(maxrate) + Changed.ar(minrate) / 2;
inverse_trig = reset_trig <= 0;
lfo_rate = LocalIn.ar(1).poll(label: "calculated lfo rate");
phasor_rate = SinOsc.ar(lfo_rate, \phase.kr(pi/2)).range(minrate, maxrate);
phasor_rate_inv = phasor_rate.linlin(minrate, maxrate, maxrate, minrate);
phasor_avg = (phasor_rate + phasor_rate_inv) / 2;
integral = FOS.ar(phasor_avg, inverse_trig, DC.ar(0), inverse_trig);
integrated_mean = integral / Sweep.ar(reset_trig, SampleRate.ir);
LocalOut.ar(integrated_mean / bufdur);
phasor = Sweep.ar(1, phasor_rate / bufdur);
graintrig = Impulse.ar(\trigrate.kr(20));
graindur = \trigrate.kr.reciprocal * \overlap.kr(2);
sig = GrainBuf.ar(2, graintrig, graindur, buf, BufRateScale.ir(buf), phasor);
Out.ar(0, sig);
this is so great. tried it out with some piano phrases 