class::LinkClock
categories::Scheduling>Clocks
summary::Scheduler synchronized with Ableton Link
related::Classes/TempoClock

Description::

LinkClock is a link::Classes/TempoClock:: based on link::https://www.ableton.com/en/link/##Ableton Link::, a protocol
that synchronizes phase (beat-within-bar) and tempo over a local network.

Subsection:: Link behavior

Link attempts, as much as possible, to allow applications to move in their own time while synchronizing:

list::
## Tempo: Any application can change the tempo at any time.
## Beat: Integer beats should sound at the same time in all applications.
## Phase (analogous to SuperCollider's link::Classes/TempoClock#-beatInBar::): A downbeat in one peer should correspond to a downbeat in other peers.
::

Link does not synchronize exact beat numbers. Two SuperCollider peers joining the same Link session, in 4/4 time, may be respectively at beats 4 and 16 (which are both barlines), but they should not be at beats 4.0 and 19.0.

Many Link-enabled applications (DAWs) can start and stop the entire timeline. SuperCollider clocks do not stop. A DAW, when starting to play, should lock into SuperCollider's phase within the bar. You can register functions to respond to other peers starting or stopping. (SuperCollider does not know which other peer started or stopped.)

Subsection:: Latency

Link coordinates the emphasis::sounding time:: of given beats.

All audio applications must prepare events, and audio processing, in advance, so that the signal hits the audio interface at the right time. SuperCollider uses OSC timestamps for this purpose, calculating timestamps from the current time in seconds + a teletype::latency:: offset.

To coordinate with other applications, then, the code::LinkClock:: must run earlier than the desired sounding time, by the same fixed offset. That is, it should have the same (positive) latency offset as the server:

code::
t = LinkClock.new.latency_(Server.default.latency);
::

Server messages should then be sent with the same amount of latency. Events generated by patterns automatically apply the server object's latency. For other messages, use link::Classes/Server#-makeBundle::.

With a positive latency, if you examine code::t.beats::, it will appear to be later than the beats shown in other applications. This is normal. The clock in SuperCollider must run early for the events to sound on time.

(Because the clock and server have separate latency settings, it is possible to adjust timing relative to other applications. If you find, for instance, that SuperCollider is sounding slightly late, you can increase the LinkClock's latency slightly, moving the clock earlier, without affecting messaging latency. Ideally, this should not be necessary, but inter-application coordination can be fiddly.)

This does have an impact on tempo changes. SuperCollider receives tempo changes from other peers only after they are sent, which is probably slightly late for our clock. In practice, it means that events falling within the latency gap may be slightly out of sync, but sync should recover very quickly. (So, Link is not ideal for music involving frequent tempo or meter changes.)

Subsection:: beatsPerBar and quantum

For every moment in time, Ableton Link knows the beat, time in seconds, and emphasis::phase::. Phase is measured relative to a emphasis::quantum::. Link peers sharing the same quantum will synchronize barlines.

For instance, assume quantum = 4. Peer A is currently at beat 33. Peer B joins the network. Peer A's last barline was beat 32, so phase = 1. Peer B's beat counter should arrange to have the same phase: in practice, it will start with -3.

table::
## strong::Phase:: || strong::Peer A beats:: || strong::Peer B beats:: || Notes
## 0 || 32 || -- ||
## 1 || 33 || -3 || Link calculates -3 for B
## 2 || 34 || -2 ||
## 3 || 35 || -1 ||
## 0 || 36 || 0 || Barline (synced)
::

If you change the quantum, Link does not guarantee that beats will keep the same duration. Beats may be shorter or longer, to try to preserve phase sync.

Therefore, you should emphasis::not:: change the quantum in the middle of a performance. Ableton recommends to set the quantum just before beginning. Then, you can change SuperCollider's code::beatsPerBar:: as needed, without changing the quantum, so that Link's phase reference remains consistent.

SuperCollider peers may optionally synchronize barlines and all meter changes by using link::Classes/LinkClock#-enableMeterSync::. This is independent of Link's quantum, and is safe to use mid-performance. See link::Classes/MeterSync::.

Subsection:: Notifications

A LinkClock's state may be changed by other connected Link peers. LinkClock uses dependant notifications to relay the new state to other interested objects.

table::
## strong::Notification:: || strong::Event::
## code::\tempo:: || Tempo changed
## code::\meter:: || The clock's code::beatsPerBar:: changed
## code::\linkStart:: || An external peer started playback
## code::\linkStop:: || An external peer stopped playback
## code::\numPeers:: || The number of connected peers changed
## code::\resynced:: || After link::#-enableMeterSync::, a code::\resynced:: notification is sent whenever a link::Classes/MeterSync#-resyncMeter:: call finishes successfully. One Boolean argument is passed: code::true:: if other SuperCollider peers were found, code::false:: if not.
::

(Note that Link does not synchronize meter across peers; this notification is inherited from TempoClock. link::Classes/LinkClock#-isSyncingMeter:: is a SuperCollider-specific way to synchronize meter; it is not part of the Link protocol.)

The most convenient way to register to receive a notification is link::Classes/SimpleController::. See the examples below.

Classmethods::

method::new
Creates a new instance of LinkClock.

argument:: tempo
The initial link::#-tempo#tempo::. Defaults to code::1::.

argument:: beats
The time in beats, corresponding to the reference time. Default to code::0::.

argument:: seconds
The reference time in seconds. See link::Classes/TempoClock#*new::.

argument:: queueSize
The storage size of the scheduling queue. See link::Classes/TempoClock#*new::.

discussion::
If an existing Link session is found on the local network, the object connects
to it and use its properties: the code::tempo:: argument is discarded in favor
of the session tempo, and the code::beat:: argument will be adjusted to ensure
proper beat and phase synchronization across all peers.
If SuperCollider is the first to join, a new Link session is locally
created and initialized with the constructor arguments.

method::newFromTempoClock
Creates a new instance of LinkClock derived from a link::Classes/TempoClock::.

argument::clock
The link::Classes/TempoClock:: used to create the LinkClock.

discussion::
The LinkClock link::#-tempo#tempo::, link::#-beats#beats::,
link::#-seconds#seconds:: and link::#-beatsPerBar#beatsPerBar:: values are
set to those of the TempoClock. The TempoClock is then stopped and all of its
tasks are rescheduled by the newly created LinkClock, so that they are now
synchronized with the Link session.

Instancemethods::

private:: prInitFromTempoClock, prSetQuantum, prStart, prTempoChanged, prStartStopSync, prNumPeersChanged, setMeterAtBeat, setTempoAtBeat, setTempoAtSec

method:: latency
Gets or sets the number of seconds of OSC messaging latency for which the code::LinkClock:: should account. In general, this should be set to match the server object's latency.

argument:: lat
A Float.

method:: tempo
Sets or gets the current session tempo at the current logical
time. Note that the tempo may be changed at any time by another peer;
the LinkClock broadcasts a code::\tempo:: notification in this case
(see link::Classes/LinkClock#Notifications::).

method:: beats
Sets or gets the current logical time in beats. If you are trying to set
the beats, Link may adjust your given value to maintain sync with other peers.

method:: numPeers
Gets the number of peers connected to the current Link session. When
peers join or leave the session, the LinkClock broadcasts a
code::\numPeers:: notification (see
link::Classes/LinkClock#Notifications::).

method:: quantum
Gets or sets Link's internal quantum (see above, link::#beatsPerBar and quantum::). Normally this should be done at the beginning of a performance, or not at all. It is risky to change quantum during a performance. Changing meter locally is the same as in TempoClock: link::Classes/TempoClock#-beatsPerBar::.

argument:: quantum
An Integer or Float.

method:: enableMeterSync
Activates SuperCollider-barline sync by creating a link::Classes/MeterSync:: object internally. (If already enabled, no new object will be created.)

argument:: id
Optional: An integer ID, uniquely identifying this instance. If not provided, one will be chosen randomly.

argument:: ports
Optional: An array of port numbers, to which barline-sync messages will be sent.

returns:: The LinkClock instance (to support chaining configuration methods, e.g. code::l = LinkClock.new.latency_(s.latency).enableMeterSync::, in which case you want code::l:: to be the clock object). To get access to the MeterSync object, use link::#-getMeterSync::.

method:: disableMeterSync
Remove all barline-sync objects.

method:: isSyncingMeter
returns:: A Boolean, code::true:: if barline sync is active, code::false:: if not. (If you create the barline sync object independently, this answer is likely to be incorrect.)

method:: getMeterSync
returns:: The MeterSync object previously created by link::#-enableMeterSync::.

note:: It is possible, though redundant and not recommended, to create multiple barline-sync objects by doing code::clock.enableMeterSync:: and code::m = SCClockMeterSync(clock)::. code::getMeterSync:: has access emphasis::only:: to the object created by code::clock.enableMeterSync::. Therefore, it is recommended to use code::clock.enableMeterSync:: in all cases. ::

Examples::

code::
(
s.waitForBoot({
	SynthDef(\click, { |freq=400, amp=0.2|
		var sig = SinOsc.ar(freq, 0.5pi) *
			Env.perc(0.001,0.06).ar(Done.freeSelf, levelScale: amp);
		OffsetOut.ar(0, sig.dup)
	}).add;
});
)

// create a synchronized clock
l = LinkClock(1).latency_(Server.default.latency);

// GUI to watch status
(
var win = Window("LinkClock", Rect(200, 200, 500, 100)).front,
peersBox, tempoBox, barsBox, beatsBox,
font = Font.default.copy.size_(32),
boldFont = font.boldVariant,
controller, task;

win.layout = HLayout(
	StaticText().font_(font).string_("Peers:"),
	peersBox = NumberBox().font_(boldFont).align_(\center).fixedWidth_(80),
	StaticText().font_(font).string_("Tempo:"),
	tempoBox = NumberBox().font_(boldFont).align_(\center).fixedWidth_(120),
	StaticText().font_(font).string_("Now:"),
	barsBox = NumberBox().font_(boldFont).align_(\center).fixedWidth_(80),
	beatsBox = NumberBox().font_(boldFont).align_(\center).fixedWidth_(80)
);

[peersBox, barsBox, beatsBox].do { |view| view.enabled_(false) };

tempoBox.action = { |view| l.tempo = view.value / 60 };
tempoBox.value = l.tempo * 60;
peersBox.value = l.numPeers;

task = Routine {
	var bars, beats;
	loop {
		bars = l.bar;
		beats = l.beatInBar;
		{
			barsBox.value = bars;
			beatsBox.value = beats;
		}.defer(l.latency);
		1.0.wait;
	}
}.play(l, quant: 1);

controller = SimpleController(l)
.put(\tempo, {
	defer { tempoBox.value = l.tempo * 60 }
})
.put(\numPeers, {
	defer { peersBox.value = l.numPeers }
})
.put(\stop, { defer { win.close } });

win.onClose = { task.stop; controller.remove };
)

// now launch a program using Ableton Link to test synchronization

// patterns automatically apply server latency,
// so this should be in sync
p = Pbind(\instrument, \click, \freq, Pseq([900, Pn(400,3)], inf)).play(l, quant:4);

// In Tasks, you should handle latency yourself:
(
r = Task {
	loop {
		s.makeBundle(s.latency, {
			Synth(\click, [freq: exprand(500, 1200)])
		});
		1.wait;
	}
}.play(l, quant: 4);
)

// changing the tempo in another application should affect tempo in SC
// changing the tempo in SC should affect every connected application
l.tempo = 90/60;

// start/stop notifications
// the remote peer should enable start/stop sync
(
q = nil;
c = SimpleController(l)
.put(\linkStart, {
	if(q.isNil) {
		q = Pbind(
			\degree, Pn(Pseries(0, 1, 8), inf),
			\dur, 0.25
		).play(l, quant: -1);
	}
})
.put(\linkStop, {
	q.stop;
	q = nil;
})
.put(\stop, { c.remove });  // clean up if clock stops
)

p.stop;
r.stop;
l.stop;
::
