In my recent experiments, I’ve come across something I find strange and can’t quite understand.
It’s related in some way to issues I raised previously here, here and here.
In short, I need to instantiate a new TempoClock when required and thus start counting the bars and beats that have elapsed. I would also like to calculate the time in seconds that has elapsed since the ‘start’ of the new TempoClock, but it seems to me that this last piece of information is incorrect.
Here’s the code to replicate my issue:
// 1. define a "debug" task:
(
Tdef(\debug, {
{
"debugTask - bar: %\tbeat: %\ttime: %\n".postf(t.bar.asInteger, t.beatInBar.asInteger+1, t.seconds.asTimeString );
1.wait;
}.loop;
});
);
// 2. when needed start a new clock and start counting the bars/beat/time passing
(
t = TempoClock.new(110/60);
Tdef(\debug).stop.reset.play(t, quant:Quant(1, 0, 0));
)
// 3. when finished, stop the clock
(
Tdef(\debug).stop;
t.stop;
);
My output looks something like this:
debugTask - bar: 0 beat: 1 time: 00:00:26.147
debugTask - bar: 0 beat: 2 time: 00:00:26.693
debugTask - bar: 0 beat: 3 time: 00:00:27.238
debugTask - bar: 0 beat: 4 time: 00:00:27.784
debugTask - bar: 1 beat: 1 time: 00:00:28.329
debugTask - bar: 1 beat: 2 time: 00:00:28.875
debugTask - bar: 1 beat: 3 time: 00:00:29.420
debugTask - bar: 1 beat: 4 time: 00:00:29.966
debugTask - bar: 2 beat: 1 time: 00:00:30.511
debugTask - bar: 2 beat: 2 time: 00:00:31.056
debugTask - bar: 2 beat: 3 time: 00:00:31.602
debugTask - bar: 2 beat: 4 time: 00:00:32.147
debugTask - bar: 3 beat: 1 time: 00:00:32.693
debugTask - bar: 3 beat: 2 time: 00:00:33.238
debugTask - bar: 3 beat: 3 time: 00:00:33.784
debugTask - bar: 3 beat: 4 time: 00:00:34.329
debugTask - bar: 4 beat: 1 time: 00:00:34.875
And, as we can see, whilst the bars and beats restart correctly from 0, the elapsed time seems to be calculated not so much based on the t clock but on some other clock that has been running continuously since the evaluation of Tdef(\debug).
That’s correct – .seconds reports an absolute time measurement corresponding to SystemClock (absolute for the session).
If you need seconds relative to the start of the clock, use a variable to store the absolute time in seconds, and subtract.
Btw I’d also be careful about that .stop.reset.play – this can cause double-scheduling, if you didn’t stop the task first. Task tries to prevent this by printing a warning and not re-playing; I don’t remember whether Tdef has the same safeguard.
// 1. define a "debug" task:
(
Tdef(\debug, {
var start = t.seconds;
{
"debugTask - bar: %\tbeat: %\ttime: %\n".postf(t.bar.asInteger, t.beatInBar.asInteger+1, (t.seconds - start).asTimeString );
1.wait;
}.loop;
});
);
Thank you so much @jamshark70 for your explanation.
I relied on the documentation and thought I was using the method correctly, but as you’ve shown me, that clearly wasn’t the case.
So perhaps could we say that, in this case, the documentation is a bit ambiguous or, at any rate, not very comprehensive?
Instead, I wanted to ask you to elaborate on this issue regarding the possibility of double scheduling when using the .stop.reset.play methods on a Tdef.
I don’t think I fully understand the risk involved in using these methods this way, and if you could explain it to me—if you have the time and opportunity—I would appreciate it.
True – .seconds seems not to be explained very well, anywhere.
t = TempoClock.new;
u = TempoClock.new; // sometime later
[t.beats, u.beats, t.seconds, u.seconds]
-> [15.132709821, 7.1869653250001, 1313.119481536, 1313.119481536]
It’s intended that two TempoClocks could have different beats at the same time, depending on when they started, but if you poll both of them for seconds at the same time, the values should match.
I did make a couple of mistakes in that comment: 1/ Task and Tdef are not subject to double-scheduling (though Routine is); 2/ there’s no printed warning; 3/ to prevent double-scheduling, Task simply doesn’t reschedule at all in your test case; 4/ Tdef has some magic that does reschedule the player, but not the Tdef itself
I searched for a prior discussion of this, and I didn’t find it. So I’ll write it up again and bookmark this one for next time.
A key point about SC scheduling is that once something is scheduled, it cannot be removed from a clock. So how does anything stop? After .stop, it still wakes up as scheduled, but its state has been changed such that it will do nothing at that point (and by doing nothing, there is no future time to reschedule, so it simply drops off the clock and doesn’t come back).
We can do this manually by a scheduled function:
(
var i = 0;
~status = \running;
~reset = { i = 0 };
~func = {
if(~status == \running) {
[i, thisThread.beats].postln;
i = i + 1;
1 // wait until next time
} {
nil // non-numeric return = no rescheduling
}
};
TempoClock.sched(0, ~func);
)
Now, if you do ~status = \stopped: the function still runs! But it doesn’t print anything (because the if condition is false) and it doesn’t reschedule for the future. So it, well, stops.
Now let’s add a second code block that mimics .stop.reset.play.
(
var i = 0;
~status = \running;
~reset = { i = 0 };
~func = {
if(~status == \running) {
[i, thisThread.beats].postln;
i = i + 1;
1 // wait until next time
} {
nil // non-numeric return = no rescheduling
}
};
TempoClock.sched(0, ~func);
)
// prints:
-> TempoClock
[0, 4.444904582]
[1, 5.444904582]
[2, 6.444904582]
[3, 7.444904582]
(
~status = \stopped; // thread.stop
~reset.value; // .reset
// new TempoClock as in the example
t = TempoClock.new;
// note that .play implicitly does a sched(...)
t.sched(0, ~func); // .play
~status = \running; // .play also has to set the scheduled job's status
)
// prints:
-> running
[0, 0.0]
[1, 8.444904582]
[2, 1.0]
[3, 9.444904582]
[4, 2.0]
[5, 10.444904582]
[6, 3.0]
[7, 11.444904582]
~status = \stopped;
Notice after -> running that now there are two time bases: the original xx.4449 from the default TempoClock, and whole-number beats from the new clock (and the counter is interleaved between them).
That’s double-scheduling.
The problem stems from two status changes – it changes to \stopped, but before it wakes up on the old clock, it changes back to \running, and this \running status is valid on both clocks.
Routine shows the same behavior.
(
r = Routine {
var i = 0;
loop {
[i, thisThread.beats].postln;
i = i + 1;
1.wait;
}
}.play;
)
t = TempoClock.new;
r.stop.reset.play(t);
r.stop; t.stop;
Task protects against this, in .play, by checking whether they are already scheduled. If so, the new play request is invalid, and it’s silently dropped.
(
k = Task {
var i = 0;
loop {
[i, thisThread.beats].postln;
i = i + 1;
1.wait;
}
}.play;
)
[0, 4.722166786]
[1, 5.722166786]
[2, 6.722166786]
[3, 7.722166786]
[4, 8.722166786]
t = TempoClock.new;
k.stop.reset.play(t);
// counter resets, but it's clearly not using `t`'s beats
[0, 9.722166786]
[1, 10.722166786]
[2, 11.722166786]
k.clock === t // false! it never switched over to `t` at all
k.clock === TempoClock.default // true -- still on the old clock
k.stop; t.stop;
But Tdef has an additional layer of indirection: you talk to the Tdef; Tdef manages a PauseStream; .reset causes the old PauseStream to be dropped and a new one to be created, avoiding double-scheduling altogether.
(
Tdef(\resched, {
var i = 0;
loop {
[i, thisThread.beats].postln;
i = i + 1;
1.wait;
}
}).play;
)
[0, 11.0]
[1, 12.0]
[2, 13.0]
[3, 14.0]
[4, 15.0]
t = TempoClock.new;
Tdef(\resched).stop.reset.play(t);
[0, 0.0]
[1, 1.0]
[2, 2.0]
[3, 3.0]
[4, 4.0]
Tdef(\resched).clock === t // false
Tdef(\resched).player.clock === t // true
Tdef(\resched).stop; t.stop;
So you actually get away with it with Tdef, because it actually makes an all-new player (PauseStream), so it’s a different object being rescheduled.
So I hope I didn’t cause alarm – but, it isn’t safe to extrapolate from Tdef’s intuitive behavior and assume that Tasks and Routines behave the same, and it’s useful to understand why that is.