This is a design document for a centralized clock server system for the Santa Clara Laptop Orchestra. It describes a client-server protocol that allows for the centralized creation, control, and management of distributed network Clock objects for use in performance.
There should be an easy way to create an object similar to a TempoClock but that has synchronized beat, beatsPerBar, and bar values across all instances of that clock across the network, which we call clock cohorts. Keeping the beat counters in sync allows for distributed compositions with a high degree of structure, in that all clocks in the cohort will have the same concept of where they are in a piece, because of the synchronized beat count.
All members of a clock cohort are equal in that they can each set the tempo or modify other clock parameters, with the expectation that the changes will propagate to all other clocks in cohort automatically.
The SCLOrkClock singleton maintains a per-machine SCLOrkWire connection back to the SCLOrkClockServer, to provide and receive clock updates. Because the server is the authoritative time keeper for all changes in clock state, all times communicated back to the server are converted back into server time using the most recent synchronization approximations. In this section we detail the protocol for communication between client and server, followed by some typical scenarios.
The singleton SCLOrkClock code should check to ensure that the time synchronization service has already been started, or start it if it hasn't been, then checks for the viability of the SCLOrkWire connection back to the server. The singleton code maintains a map of cohort names as key and a list of clock instances as values, so any updates to state from the server can be propagated back to the individual clock instances. So this clock will be added to the map of cohort names, then the client code calls /clockRegister on the server, expecting a /clockUpdate callback on this cohort name as a response to registration.
Using the following as reported by the server:
tempo | Current tempo of the clock in beats per second. |
beatsPerBar | Number of beats in a bar. |
baseBarBeat | Beat count where beatsPerBar value was last changed, or 0 if never changed. |
beatAtLastTempoChange | Beat count where tempo was last changed, or 0 if never changed. |
serverTimeAtLastTempoChange | Server elapsed time count when tempo was last changed, or the start time of the clock if tempo never changed. |
baseBar | Bar count at last meter change. |
Assuming we have a means of computing local times relative to the provided server time, we can then compute the current clock state as follows:
To convert an inputBeat back to a local time elapsed seconds, or beats2secs, the formula is:
Note that this formula only works for inputBeat >= beatAtLastTempoChange
.
All immediate changes, like the first line in the example code above, take some time to propagate out and so will arrive at the other clocks in the clock cohort as a retroactive change, meaning that the applyAtTime field is likely to be in the past. Therefore all changes are time-based in a distributed clock, so are reported back to the server using the same mechanism, which is a call to the /clockChange command. Immediate changes don't actually change clock state, rather they will rely on server responding with a /clockUpdate change for some time in the recent past (essentially minus the round-trip-time between client and server).
The server has a central role in clock synchronization, but maintains no clocks of its own. Rather it maintains sufficient information that the clients can reconstruct the clock state from all reported information.
Clients should be able to request the names of all registered clock cohorts, requested detailed state information about any of the individual clock cohorts, and register to be informed of any further changes to the clocks. Given that the client and server time synchronization requires only one instance per computer, all clock cohorts can be managed by a per-machine singleton instance of client or server.
The server reports its current time when asked at /clockSyncGet by calling /clockSyncSet on the calling client on the supplied return port. The client keeps a rolling mean of the last several data points measuring difference between the client elapsed time and the reported server time, as well as the round-trip-time from request to responce. If the time diff is calculated on receipt at the client as:
Then the difference between local client time and server time is approximated by:
In this synthetic example the client is running consistently 5 minutes ahead of the server, and the round trip time between client and server is 1 minute:
localTime | serverTime | Event |
05:00 | 00:00 | Client send request for time to server, records local sending time. |
05:30 | 00:30 | Server receives request, notes server time, sends back to client. |
06:00 | 01:00 | Client receives server time, computes diff of 05:30, roundTripTime of 01:00, timeDiff of 05:00. |
label | cohortName | Name of clock cohort. If a cohort with this name already exists all following parameters are ignored. |
float64 | startTime | The client-estimated server start time of this clock. |
float64 | tempo | The initial tempo value of the clock. |
float64 | beatsPerBar | The beats per bar value of the clock, count of beats in an individual measure. |
Informs the server about the creation of a new individual clock within a cohort of the provided name. If the cohort is already registered the provided parameters are ignored, to prevent clocks joining an existing cohort with default parameters from clobbering the existing cohort parameters. If the cohort is new, the sever treats the provided data as authoritative. In either event, the server will respond to the registering client only with canonical clock state information by calling /clockUpdate on the client.
Called with no arguments, this is a request by an individual client to return the state of all registered cohorts with individual /clockUpdate return calls on the client.
label | cohortName | The name of clock cohort that this update applies to. |
float64 | applyAtTime | Server time that this change should be applied to the clock. |
float64 | tempo | Tempo value in beats per second. |
float64 | beatsPerBar | Number of beats in a bar. |
float64 | baseBarBeat | Beat that beatsPerBar was last changed, or 0 if never changed. |
float64 | beatAtLastTempoChange | Beat count at time of last tempo change, or 0 if never changed. |
float64 | timeAtLastTempoChange | Server time at last tempo change, or start time of clock cohort if never changed. |
float64 | barAtLastMeterChange | Bar count at last change to beatsPerBar. |