I’m trying to create a Clock that advances on tick, rather than on time, like a MIDI clock. I would be calling the nudge method below from a Pfunc to advance the clock. My goal is to have a “leader” EventStreamPlayer that others could follow, so that I can be very loose with timing in the leader but still reliably trigger other things in sync as long as ticks align.
I have an implementation below based very heavily on MIDISyncClock from ddwMIDI, but I can’t seem to get this to work - it looks like it initializes OK, but then when I use it in a play, the events are never triggered. If I include a \dur value/pattern, then it blows off the clock completely and behaves as if I was using the default clock.
Note that I am really not au fait with classes so it’s likely I goofed the implementation. I also get the feeling I am not fulling groking how a Pbind/EventStreamPlayer actually uses the clock. Maybe I need a real Scheduler?
NudgeClock : Clock {
var queue, ticks;
new {
'calling nudgeClock new'.postln;
}
*new {
'calling nudgeClock *new'.postln;
^super.new.init()
}
play { |task, when|
'nudgeClock playing'.postln;
'nudgeClock playing task:'.postln;
task.postln;
'nudgeClock playing when'.postln;
when.postln;
this.sched(task, when)
}
init {
'calling nudgeClock init'.postln;
ticks = 0;
queue = PriorityQueue.new;
}
clear {
queue.clear;
}
sched { |delta, item|
"NudgeClock scheduling (enqueing)".postln;
"NudgeClock scheduling (enqueing) delta:".postln;
delta.postln;
"NudgeClock scheduling (enqueing) item:".postln;
item.postln;
"NudgeClock scheduling (enqueing) delta.():".postln;
delta.().postln;
queue.put(delta, item);
}
secs2beats {
^ticks
}
/* adapted from
https://github.com/jamshark70/ddwMIDI/blob/master/MIDISyncClock.sc */
nudge {
// last queue time used to detect empty queue
// next time used to detect whether it's item time
// save clock is some kind of threading
var lastQueueTime, nextTime, saveClock, task;
'calling nudgeClock nudge'.postln;
ticks = (ticks + 1).postln;
saveClock = thisThread.clock; // "should" be SystemClock
while {
lastQueueTime = queue.topPriority;
lastQueueTime.postln;
// if nil, queue is empty
lastQueueTime.notNil and: { lastQueueTime <= ticks }
} {
// perform the action, and check if it should be rescheduled
task = queue.pop;
thisThread.clock = this;
protect {
nextTime = task.awake(lastQueueTime, 0, this);
if(nextTime.isNumber) {
this.sched(nextTime, task, 0)
};
} {
thisThread.clock = saveClock;
};
};
}
}
Hm, I don’t see a code example for this, only for the class.
It’s also valuable to use a simple test, e.g.:
n = NudgeClock.new;
n.sched(2, { "bingbang".postln });
n.nudge;
n.nudge; // by hand until it fires
A couple of concrete issues:
Your sched method as it is now is like schedAbs for every other clock. sched is to schedule for a time that is n ticks later than now – that’s important and necessary for automatically rescheduling. sched should do queue.put(ticks + delta, item);. A whole lot of things will break because of this.
MIDISyncClock initializes ticks to -1, but you’re initializing to 0. I think you’ll have some unexpected behavior from that.
Ah, yes, good points. I did a pretty poor job hazarding a guess at what needed to change from your implementation. Both of those seemed to have come from points where I had realized I was missing something mid-implementation but failed to go back through the code to fix it.
Excellent idea on the test as well. I’ll go make those 2 easy changes and test it a bit. Thank you!
The reason why I initialized to -1 is that the first tick should process time point 0. So, why not initialize to 0 and increment the counter after processing the actions? If there’s an error in the user-scheduled actions – currently I’m using protect, which will stop executing upon error. In that case, time wouldn’t advance in the clock at all. (I suppose I could use try instead, but there are some known bugs with try so I’m a little nervous about that.)
Advancing the clock at the beginning of the “tick” cycle guarantees that the clock will advance, even in case of error.
Initializing to -1 means that the first “pre-increment” brings you to the desired time point 0.
I should probably add a comment to my code about this. When I reviewed my code this morning, I thought maybe it was a bug, but on second thought, it isn’t.
I made those changes, then when using as the argument to .play() in a Pbind, I got a binary operator '+' failed error on queue.put(ticks + delta, item). From playing around a bit, it looks like the delta here is Quant(0, nil, 0), where nil is the phase, and this (or, nil, anyway) is what appears to get passed to the + operator. As-is, this error is where NudgeClock stops doing anything.
This looks sketchy to me as I don’t really grok Quant and am really just guessing at which property to use, but it seems to work.
I also noticed a subtle bug (I think) in my code above. play was calling this.sched(task, when) but it looks like it should have been calling this.sched(when, task).
Hm, sched should accept a time point expressed only as a number – Quant is not appropriate here.
See TempoClock:play –
play { arg task, quant = 1;
this.schedAbs(quant.nextTimeOnGrid(this), task)
}
nextTimeOnGrid resolves a Quant to a concrete, numeric time point before passing it to schedAbs – the Quant itself never makes it through. (Note also that play should not call relative-timing sched – it should calculate an absolute time point and then schedule for that specific time point.)
So what you’re finding is consistent with the class library – if you try aTempoClock.sched(aQuant, something), you will get an error for that too. The fix is not to make your sched method work with quant – the fix is to make the play method handle the quant before it ever reaches sched.