How to sync continuous synths with discrete synths emissions (using TempoClock/Routine)?

I’ve been building myself a simple sequencer. Instruments have a List containing their osc messages for every beat, and the sequencer is a TempoClock which calls a function : send the current bundle containing every osc message for the current beat, fetch the next bundle, then return 1 to keep looping.

However, I also have continuous synths, i.e. drones, that I’d like to also make vary in time. I’ve been thinking about this for quite some time, and I can’t really figure out what’s the best way to do so. For example, I’d like the frequency of the drone to slide from 440 to 880 between beat 5 and 9.

Maybe I’m wrong, but I suppose the TempoClock’s function is the trigger.

I think the best ‘musical’ solution would be to trigger a control bus at beat 5, that will play a Line.kr with the correct time so that it approximately ends at beat 9 (I’m definitely not looking for sample perfect accuracy here). But if I change the TempoClock’s tempo at beat 7, this won’t upate Line’s time setting, thus desynchronising. I suppose I could implement a function that retriggers a control bus every time I change the tempo, but that seems overcomplicated ?

Another solution would be to call a Routine, that changes the parameter so quickly that it appears linear. It would be triggered at beat 5, and would divide the target time in tiny increments, mapping the parameter change. Since it would be tied to the TempoClock, I suppose it would be keeping in sync (not perfectly but fine enough) if I change its tempo ?

I suppose some of you have been dealing with this for a long time, so if anyone has any solution or insight to share about this, this would help me a lot. Thank you.

If this could be ‘slide from the previous frequency to 880 over n seconds’,you can use VarLag, passing the value and the time as controls.

You’re right, Line isn’t going to do this.

I think VarLag probably won’t either. It starts a new leg segment when the target value changes. The requirement here is to change the amount of time without changing the target. So I think it won’t retrigger, instead, just continuing on.

I don’t have time to develop it right now, but, what if you generated a control value based on accumulating a rate of change? Taking a normalized slide for simplicity (you can easily map this on to 440 … 880 after the fact) – you want it to go from 0 to 1 (spanning 1 unit) in 4 beats, and based on tempo, you know how many seconds that is. So the per-sample increment is then 1 * beatsPerSec / beats * SampleDur.ir – at 120 bpm, that’s 1 * 2 / 16 = 1/8 * SampleDur.ir. Anyway once you have the rate of change, Integrator.ar that.

If the tempo changes, then this increment will also change, and the line would change slope midway through. An increase in tempo would raise the increment and reach the target faster. I got myself confused a bit there, but I think this is right.

You could clip it to get it to stop at the ending boundary. Or maybe better, InRange and FreeSelf.

hjh

Just testing this…

s.waitForBoot {
	x = { VarLag.kr(\target.kr, \time.kr, warp: \lin).poll }.play;
	s.sync;
	
	x.set(\target, 1, \time, 10);
	
	5.wait;
	'should now be 0.5'.postln;
	x.set(\time, 20);
	10.wait;
	'should now be 1'.postln;
}

… it works.
If you go this way you will need to keep a record of what time you started the transition and use that to calculate how long it should take now. A bit of a faff, but do-able.

Another approach, which has worked for me, is to schedule many small parameter increments on the Tempo clock itself so that it will always be in sync, even if the tempo changes.
If you use a small enough time resolution, such as 32 or 64 times per beat, the changes should hopefully sound close to continuous.
As a further stategy, you could always add a short Lag.kr to the synth control to smooth out any small bumps.

1 Like

It should also be noted that it works for warp: \lin only. Try warp: \sin (and .poll(2) to reduce post window activity):

UGen(EnvGen): 0
UGen(EnvGen): 0.00567413
... snip...
UGen(EnvGen): 0.418681
UGen(EnvGen): 0.496858
should now be 0.5        // 'k that's pretty close
UGen(EnvGen): 0.499479
UGen(EnvGen): 0.501756
... snip...
UGen(EnvGen): 0.729237
UGen(EnvGen): 0.748899
should now be 1          // this isn't
UGen(EnvGen): 0.768565
UGen(EnvGen): 0.788112

… though my earlier recollection was also incorrect. I thought it would ignore time changes without a target change at the same time. It does respond to set(\time, 20), but differently from the VarLag unit that’s created for a linear ramp.

  • VarLag recognizes that half the time has elapsed, so, it uses half of the requested 20 seconds.
  • The EnvGen that’s used for other warps triggers a new segment, going from the current halfway value toward the prior target, but using the entire requested time, so the \sin warp reaches only 0.75 after 10 seconds.

For a linear ramp, you can get the second behavior by writing curvature: 0 instead of warp: \lin.

Which behavior is correct? Both are justifiable.

One takeaway from this is that it’s important to spec out the semantics of changing the segment time midstream.

hjh

1 Like

This problem bugged me and it was a little harder than I though (assuming linear ramp as @jamshark70 mentioned)…

Here is an object (env thing) that keeps the state.

~mkSynthArgLerper = { |synth, targetName, timeName, initalValue|
	var obj = ();
	
	obj[\__beats2secsRelative] = {|beats|
		thisThread.clock.beats2secs(thisThread.clock.beats + beats) - thisThread.clock.seconds
	};
	obj[\start] = { |target, beats|
		obj[\__target] = target;
		obj[\__total_beats] = beats;
		obj[\__remaining_beats] = beats;
		obj[\__start_beat] = thisThread.beats;
		
		// only tempo clock lets you get the tempo, defaults to 60bpm if some other kind of clock.
		obj[\__tempo] = try {thisThread.clock.tempo} {1}; 
		
		synth.set(
			targetName, obj[\__target], 
			timeName, obj[\__beats2secsRelative].(obj[\__remaining_beats])
		)
	};
	
	obj[\update] = { 
		var cur_tempo = try {thisThread.clock.tempo} {1}; 
		if(cur_tempo != obj[\__tempo]) {
			obj[\__tempo] = cur_tempo;
			obj[\__remaining_beats] = obj[\__total_beats] - (thisThread.beats - obj[\__start_beat]);
			
			if(obj[\__remaining_beats] > 0) {
				synth.set(timeName, obj[\__beats2secsRelative].(obj[\__remaining_beats]))
			}
		}
	};
	
	obj[\start].(initalValue, 0);
	obj;
};

It is initalised by passing in the synth instance, the name of the target, the name of the time control, and the init value.

~myControlLerper = ~mkSynthArgLerper.(
   ~synth, 
   \myControl, 
   \myControlTime, 
   initalValue: 0
);

Then you call…

~myControlLerper[\start].(target: 10, beats: 10);

… to start the transition, calling…

~myControlLerper[\update].();

… every update tick.

I think it works reasonably well.

Again, this only works on linear ramps, but you could easily add an extra .lincurve after the VarLag, passing the previous starting value and target value.

Here is a use case where the tempo changes.

~clock = TempoClock.new(60/60);

Routine({
	~synth = { 
		VarLag.kr(\myControl.kr, \myControlTime.kr, warp: \lin).poll
	}.play;

	s.sync;
	
	~myControlLerper = ~mkSynthArgLerper.(~synth, \myControl, \myControlTime, initalValue: 0);

	~myControlLerper[\start].(target: 10, beats: 10);
	
	// update over 5 beats
	~startBeat = thisThread.beats;
	while { thisThread.beats - ~startBeat < 5 } {
		~myControlLerper[\update].();
		0.1.wait;
	};
	
	'waited for 5 seconds (5 beats at 60bpm), result should be 5'.postln;
	
    ~clock.tempo = 30 / 60;

	~myControlLerper[\start].(target: 0, beats: 20);

	~startBeat = thisThread.beats;
	while { thisThread.beats - ~startBeat < 10 } {
		~myControlLerper[\update].();
		0.1.wait;
	};
	'waited for 20 seconds (10 beats at 30bpm), result should be 2.5'.postln;
	
}).play(~clock);