LinkClock + TempoBusClock = LinkBusClock

Hi everybody!

I’ve made a simple mix of LinkClock and TempoBusClock which allowed me to use a method called .makeLinkClock in a ProxySpace and then control the tempo of it throuth the link clock, or set it directly like p.clock.tempo_(110/60) and everything under the LinkClock, inside the ProxySpace p and outside SuperCollider will still be in sync.

It was super easy to do it, and I would like to make a pull request for it, because it is so convenient I’m sure more people out there would like to use it.

But compiling SuperCollider with LinkClock is optional and I guess it won’t be healthy to have something depending on it inside TempoBusClock and ProxySpace if one have opted out of having it.

So, what is to be done to check if LinkClock is in or out at the time of the compiling and then choosing the right version? Or there’s another better way of doing it?

best regards,
Gil

Yes: A better design would be to use a SimpleController to watch for tempo changes, and update the tempo bus based on that.

c = TempoClock.default;  // or TempoClock.new(whatever) or LinkClock

... wherever 'control' comes from...

~tempoWatcher = SimpleController(c)
.put(\tempo, {
	control.set(\fadeTime, 0.0, \tempo, c.tempo)
})
.put(\stop, {
	~tempoWatcher.remove;
});

TempoBusClock really shouldn’t inherit from TempoClock because the “is-a” relationship disallows any other kind of clock from being used.

hjh

Hi jamshark70
Thank you for your prompt response.

I tried to use SimpleController for what I’m trying to do (by ways of bad design) as you suggested but couldn’t figure out how.

What I trying to achieve is what we can see in the item c) connecting client and server tempo here and then being able to change p.clock.tempo, as in the example there, via LinkClock.

Thinking about this issue further, though –

Part of this depends on what you want to do. If you simply want the tempo available on the server side, then it’s easy – just make your own node proxy to store the tempo as a number. (This is what TempoBusClock does.)

TempoClock.default = l = LinkClock.new.latency_(s.latency).permanent_(true);

Ndef(\tempo, l.tempo);  // numeric proxy

(
var ctl;

ctl = SimpleController(l)
.put(\tempo, { Ndef(\tempo, l.tempo) })
.put(\stop, { ctl.remove });
)

Now anything in the server can get the tempo from Ndef(\tempo).kr(1) in a SynthDef, or Ndef(\tempo).bus.asMap in a pattern.

BUT that “example c” is running Impulses. This is nastier than you think.

Sync between server-side impulses and language-side clocks is already difficult. If it’s Impulse.ar, it would be OK against a TempoClock for awhile, but sound card clocks are usually not super-accurate, so you might experience some drift after a long time. Impulse.kr at 44.1 kHz drifts out of sync within a few minutes (because 1 second does not divide evenly into an integer number of control blocks).

Sync against a LinkClock will be even less accurate because LinkClock makes continual micro-adjustments to stay in sync with other peers. You gain the ability to play in time with other computers, but lose any hope of sample accuracy.

A TempoBusClock strategy vs LinkClock is not enough to ensure sync, then.

What I would do is to drive impulses from the language side. If there’s a new pulse-train synth every beat, then the micro-adjustments remain micro and it should sound pretty smooth.

Roadbumps: 1. The click synth needs to be sure to reset the control bus back to 0 before it releases (–> Delay1). 2. The synth arg name can’t be tempo because this is reserved in the event system. 3. The click nodeproxy needs to initialize at control rate, because without this, it assumes that every pattern played on the proxy will be audio rate.

So this is a case where you might get the basic principle right, but there are a bunch of other things waiting to trip you up.

s.boot;

TempoClock.default = l = LinkClock.new.latency_(s.latency);

// no need to re-evaluate this if you did it already in the earlier example
(
var ctl;

Ndef(\tempo, l.tempo);  // numeric proxy

ctl = SimpleController(l)
.put(\tempo, { Ndef(\tempo, l.tempo) })
.put(\stop, { ctl.remove });
)

(
SynthDef(\krClick, { |out, beatsPerSec = 1, beats = 1, subdiv = 4|
	var click = Impulse.kr(beatsPerSec * subdiv);
	var count = PulseCount.kr(click);
	FreeSelf.kr(Delay1.kr(count >= (subdiv * beats)));
	Out.kr(out, click);
}).add;
)

// It turns out, if I don't initialize the proxy
// to a kr signal, then the Pbind will be assumed to be ar
// and that will make the clicks unavailable
Ndef(\click, { DC.kr(0) }).quant_(1).clock_(l);

Ndef(\click, Pbind(
	\instrument, \krClick,
	\beatsPerSec, Ndef(\tempo).bus.asMap,
	\dur, 1
));

Ndef(\boop, {
	var trig = Ndef(\click).kr(1);
	var eg = Decay2.kr(trig, 0.01, 0.1);
	var osc = SinOsc.ar(TExpRand.kr(200, 800, trig));
	(osc * eg * 0.5).dup
});

Ndef(\boop).play;

Ndef(\boop).stop;

I tested this against another Link peer, where the other one is changing tempo, and sync is tight.

What this version doesn’t give you on the server side is information about the metrical position of each click. Unfortunately, I’ve already spent more time than I expected on this, so I don’t quite have time to do that today.

hjh

3 Likes

That is brilliant!

I didn’t expect to learn that much when I started this conversation.

I don’t need this for my use a.t.m. I’m good to go!
If anyone out there do need it, have already a good starting point.

Thank you for your time.

Gil