Date.localTime as UGen?

Hey there,

for a project I need a version of Date.localTime.rawSeconds on the server. As I understand, scsynh should have some representation of the machine’s localTime to handle OSCMessages, right? Is there any way of accessing it on the Server? Or would I need to write an UGen for it?

(I basically would like to use the output of rawSeconds as a ramp in my system)

Warmly,
moritz

Is there any way of accessing it on the Server?

There isn’t.

(I basically would like to use the output of rawSeconds as a ramp in my system)

Are you sure you really need the system time? Why can’t you use sample time (= Sweep)?

Thank you for your answer! I would like to use the current date/time as a (simple) timebase for a longform installation/composition. Maybe a Sweep/Phasor initialised with sclang’s Date.localTime.rawSeconds could do the trick for now :slight_smile:

I see!

Maybe a Sweep/Phasor initialised with sclang’s Date.localtime.rawSeconds could do the trick for now

Keep in mind that the sample time, as measured by Sweep, is not the same as the system time (= NTP time). The audio clock and the system clock will eventually drift apart over time.

But there is another, bigger problem: scsynth represents audio signals as 32-bit floating point numbers. The larger the number, the smaller the precision of the fractional part. The precision is determined by the size of the mantissa, which for 32-bit floats is 23 bits (not counting the sign bit). In fact, if the number is greater than 2^23 (8388608), there will be no fractional part at all. The current value of Date.localtime.rawSeconds is about 1748813790. A 32-bit floating point number can only represent such a time with an accuracy of about 3 minutes(!)

In other words: you can’t really work with absolute time stamps in scsynth.

2 Likes

1748813790 / 8388608 = 208.4748 which is longer than 3 minutes, sure – but integer precision extends above 2^23, so the interval is (I think) more like 104 seconds – still useless as a timer, of course.

((2**24) + (-4 .. 4)).collect { |double| Float.from32Bits(double.as32Bits) }
.do(_.postln); ""

16777212.0
16777213.0
16777214.0
16777215.0 -- still have full int precision
16777216.0
16777216.0 -- oopsy, now only even-int precision
16777218.0
16777220.0
16777220.0

@schmolmo to extend relative times in the server, a phasor can trigger an accumulator:

// edit: doesn't actually need the feedback loop -- simpler:
a = {
	var samps = Phasor.ar(0, 1, 0, 60 * SampleRate.ir);
	var trig = HPZ1.ar(samps) < 0.0;
	var minutes = FOS.ar(trig, 1, 0, 1);
	SinOsc.ar((minutes * 25) + 100, 0, 0.1).dup
}.play;

Now you’ve got minutes (up to 16 million) and a sample-accurate position within each minute.

hjh

1 Like

Thank you so much for the replies! Of course this a problem, somehow it’s hard to wrap my head around the 32-bit floating point representation in scsynth.
@jamshark70 thank you for the example! Do I understand correctly and HPZ1.ar gives me the Phasor wraparound points and F0S.ar works as the accumulator?
I guess I’ll have to find a good (human-readable) pair of time-values to store date values, e.g. one value that is hours since 1970 and another value that stores seconds (exact to 1 millisecond) since the last hour.

Is there any way to measure this clock drift? :slight_smile:

P.S. Trying out the example worked really well as a very calm timer while doing my e-mails :wink:

Not that I’m aware of.

One thing you could try: send the current logical system time (SystemClock.seconds + an offset calculated at start-up) at regular intervals (e.g. 1 second) to the Server as scheduled OSC bundles. In the Synth, ramp to the new value over that same interval. (To adjust for the delay, you would need to add the ramp time and the Server latency to the timestamp value.)

1 Like

Close – HPZ1 will be +0.5 most of the time (given a Phasor incrementing by 1 each time) and a larger negative value when it wraps around. HPZ1.ar(samps) < 0 produces exactly one sample of 1.0 at the wrap point. Then the First-Order Section totals those wraparound points… or PulseCount would do it too.

Counting up for an hour, you’ll certainly run into floating point rounding error. Phasor counts per sample, not per ms; 3600 seconds x 44100 samples/second = 158760000, which needs 28 mantissa bits (and you’ve got only 24, including the implicit one).

You could use two levels of accumulator: Phasor on a one minute cycle, one accumulator that would roll over after 60 minutes, and that rollover triggers an hour counter.

hjh

1 Like

Ah, yes, you’re right! I guess these three might be a good option:

// 1: days since 1970
log(365*2500) /// ... 14 bits, good enough for a very long time
// 2: seconds Since Midnight
log2(60*60*24) // ... 17 bits
// 3: a Sample Counter synced to the seconds-counter

This example achieves it for now:

(
s.makeBundle(s.latency, {
	{
		var rawSeconds = Date.localtime.rawSeconds + s.latency;
		var daysSince1970 = (rawSeconds / 86400);
		var secondsSinceMidnight = (rawSeconds % 86400);
		var samps = Phasor.ar(0.0, 1.0, 0, SampleRate.ir) + ((rawSeconds%1.0)*SampleRate.ir);
		daysSince1970 = Phasor.ar(0.0, 1/(SampleRate.ir*86400), 0, inf) + daysSince1970;
		secondsSinceMidnight = (Phasor.ar(0, SampleRate.ir.reciprocal, 0, 86400) + secondsSinceMidnight) % 86400;
		[secondsSinceMidnight, samps]
	}.plot(4, separately:true);
});
)

Comparing scheduled sclang-scheduled Synth’s that play an impulse with the Phasor-based scheduling:

SynthDef("dirac", { arg out, amp=0.1;
	var u;
	u = Impulse.ar(0);
	FreeSelf.kr(u);
	Out.ar(out, u * amp);
}).add;
)

(
Tdef(\scheduler, {
	var delta;
	loop {
		delta = absdif(Date.localtime.rawSeconds%1, 1);
		s.makeBundle(delta, {Synth(\dirac, [\out, 1, \amp, 0.5]) });
		1.wait
	}
	
}).play;

s.makeBundle(s.latency, {
	{
		var delta = (Date.localtime.rawSeconds%1.0) + s.latency;
		var phasor = Phasor.ar(0.0, 1.0, 0, SampleRate.ir) + (delta*SampleRate.ir);
		HPZ1.ar(phasor % SampleRate.ir) < 0.0;
	}.play
})
)

This gives me a ~5msec difference

Which should be good for now :~)

Thank you for the help!

var rawSeconds = Date.localtime.rawSeconds + s.latency;

Note that this samples the system time at the exact time this function is called, which introduces jitter. I would recommend to use the logical system time instead, i.e. the time the current Routine has been scheduled for.

I would only call Date.localtime.rawSeconds once during startup to calculate the offset:

// startup:
~clockOffset = Date.localtime.rawSeconds - SystemClock.seconds;

// in Routine:
(
s.makeBundle(s.latency, {
    var rawSeconds = SystemClock.seconds + ~clockOffset + s.latency;
    // etc.
1 Like