Clock advance on tick, a la MIDI?

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.

hjh

1 Like

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!

Cool.

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.

hjh

2 Likes

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.

So, I rewrote sched like so:

	sched { |delta, item|
		if(delta.class == Quant, { delta = delta.timingOffset; });
		queue.put(delta + ticks, item);
	}

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.

hjh