Trigger Pbind / patterns with external clock?

hi,
i am trying to sync pbind / patterns with a external midi clock (hardware).
i was not able to get any tight results without drifts with MIDISyncClock http://quark.sccode.org/ddwMIDI/Help/MIDISyncClock.html

(
~myPattern = Pbind(
    \intrument, \default,
    \dur, 1,
    \amp, 1
).play(MIDISyncClock, quant:4);
)

my attempts to sync and control TempoClock.tempo by MIDIdef.sysrt (with the calculated delta ms of the ticks)
where very prone to other language or gui events and less reliable than MIDISyncClock.

but using the external midi ticks (96th) directly as a clock works quite well.

(
~myPattern = Pbind(
    \intrument, \default,
    \dur, 1,
    \amp, 1
).asStream;
)
(
var count = 0;
MIDIdef.sysrt(\mymidiclock, {arg ...args;
    switch(args[2],
        8, {// midi ticks
            if((count%12==0), { // speedlimit: 96th/24 = 4th
                ~myPattern.next(Event.new).play           
            });
            count = count +1;
        }
    );
});
)

i would like to be able to switch between internal and external clock and keep the full functionality of pbind / patterns like, the timing, Ppar etc…
is there another way to clock patterns without converting them to streams ?

thanks in advance for any hints

MIDISyncClock is my work.

I’m afraid it isn’t very well tested because I don’t own an external device that sends MIDI ticks. I have tested it in the past by sending MIDI clock messages from another sclang instance. In principle there’s no reason why it should be any worse than reading ticks directly.

Is it 96 ppq? Or 96 pulses per bar?

Few things off the top of my head:

  • “Using Event Streams with MIDISyncClock” – this section is no longer necessary. You don’t have to make these hacks anymore. I changed MIDISyncClock some months ago to make that unnecessary. (sccode.org has an old version of the help file.) https://github.com/jamshark70/ddwMIDI/commit/bb3c90c32f5999d74cefd87fdd4cbb6d7139b71b

  • The new, SCDoc-format help file adds a note: “Set the server’s latency as low as possible. MIDI clock messages assume the receiving device will play instantaneously; there is no compensation for OSC messaging latency.” s.latency is 200 ms by default, so, if you haven’t explicitly set latency, every event is probably 200 ms late. You might try s.latency = nil.

  • The new version of the help file also recommends to initialize MIDIClient and MIDISyncClock first, and only after that starting the clock source. MIDI ticks do not send any position within the bar. The only way to be sure barlines are synced is if MIDISyncClock is listening for start/stop/tick messages before the external clock actually sends them.

hjh

i really had an old version of your quark. i installed it for some reasons by hand.

it runs now much much better. thank you !
with buffersize set to 64 and s.latency set to zero i measure something like 9ms latency

there is one error i experience:
Pdef’s throw an “Message ‘timeToNextBeat’ not understood.” error.

(
SynthDef(\clicks,{|freq=5000|
	Out.ar(0,
		SinOsc.ar(3000!2, 0, 0.5) * EnvGen.kr(Env.perc(0.001,0.01),doneAction: 2)
	);
}).add
)
(
Pdef(\test2,
	Pbind(\instrument, \clicks,\dur, 1/2)
)
)
Pdef(\test2).play(MIDISyncClock, quant: 1)
Pdef(\test2).stop

if i evaluate Pdef(\test2 a second time while playing it throws that error.

Pbindef’s behave like expected.

Pbindef(\test1,\instrument, \clicks,\dur, 1/2);
Pbindef(\test1).play(MIDISyncClock, quant: 4);
Pbindef(\test1).stop;

here is the complete error message

ERROR: Message 'timeToNextBeat' not understood.
RECEIVER:
class MIDISyncClock (0x107daa0) {
  instance variables [19]
    name : Symbol 'MIDISyncClock'
    nextclass : instance of Meta_MIDISysDataDispatcher (0x16e4820, size=19, set=5)
    superclass : Symbol 'Object'
    subclasses : nil
    methods : nil
    instVarNames : nil
    classVarNames : instance of SymbolArray (0x107db60, size=15, set=3)
    iprototype : nil
    cprototype : instance of Array (0x107dbe0, size=15, set=4)
    constNames : nil
    constValues : nil
    instanceFormat : Integer 0
    instanceFlags : Integer 0
    classIndex : Integer 615
    classFlags : Integer 0
    maxSubclassIndex : Integer 615
    filenameSymbol : Symbol '/root/.local/share/SuperCollider/downloaded-quarks/ddwMIDI/MIDISyncClock.sc'
    charPos : Integer 176
    classVarIndex : Integer 130
}
ARGS:
   Float 1.000000   00000000 3FF00000

PROTECTED CALL STACK:
	Meta_MethodError:new	0xffb160
		arg this = DoesNotUnderstandError
		arg what = nil
		arg receiver = MIDISyncClock
	Meta_DoesNotUnderstandError:new	0xffc320
		arg this = DoesNotUnderstandError
		arg receiver = MIDISyncClock
		arg selector = timeToNextBeat
		arg args = [ 1 ]
	Object:doesNotUnderstand	0xb13c20
		arg this = MIDISyncClock
		arg selector = timeToNextBeat
		arg args = nil
	EventPatternProxy:constrainStream	0x12afd20
		arg this = Pdef('test2')
		arg stream = a Routine
		arg newStream = a Routine
		arg inval = (  )
		arg cleanup = an EventStreamCleanup
		var delta = nil
		var tolerance = nil
		var quantBeat = 0
		var catchUp = nil
		var deltaTillCatchUp = nil
		var forwardTime = nil
		var quant = 1
	EventPatternProxy:embedInStream	0x12af960
		arg this = Pdef('test2')
		arg inval = (  )
		arg embed = true
		arg default = nil
		var outval = ( 'instrument': clicks, 'msgFunc': a Function, 'dur': 0.5, 'amp': 0.1,
  'server': localhost, 'sustain': 0.4, 'isPlaying': true, 'freq': 261.6255653006, 'hasGate': false,
  'id': [ 1209 ] )
		var count = 0
		var pat = a Pbind
		var test = true
		var resetTest = nil
		var stream = a Routine
		var cleanup = an EventStreamCleanup
	Routine:prStart	0x1591a60
		arg this = a Routine
		arg inval = (  )

CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Routine>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Object:doesNotUnderstand
		arg this = <instance of Meta_MIDISyncClock>
		arg selector = 'timeToNextBeat'
		arg args = [*1]
	EventPatternProxy:constrainStream
		arg this = <instance of Pdef>
		arg stream = <instance of Routine>
		arg newStream = <instance of Routine>
		arg inval = <instance of Event>
		arg cleanup = <instance of EventStreamCleanup>
		var delta = nil
		var tolerance = nil
		var quantBeat = 0
		var catchUp = nil
		var deltaTillCatchUp = nil
		var forwardTime = nil
		var quant = 1
	EventPatternProxy:embedInStream
		arg this = <instance of Pdef>
		arg inval = <instance of Event>
		arg embed = true
		arg default = nil
		var outval = <instance of Event>
		var count = 0
		var pat = <instance of Pbind>
		var test = true
		var resetTest = nil
		var stream = <instance of Routine>
		var cleanup = <instance of EventStreamCleanup>
	Routine:prStart
		arg this = <instance of Routine>
		arg inval = <instance of Event>
^^ The preceding error dump is for ERROR: Message 'timeToNextBeat' not understood.
RECEIVER: MIDISyncClock

That is very good to hear! I had run some reasonably acceptable tests, but hadn’t gotten as far as a real-world test with external hardware, so I wasn’t sure how it would actually fare.

That’s a quick fix – I just pushed that method into the https://github.com/jamshark70/ddwMIDI.git repository. Let me know if it works.

hjh

it works !
thank you very much.this is helping me a lot

Hello,

It seems that MIDISyncClock is broken none of the example works on 3.11.1

I get this error

MIDISyncClock
ERROR: Message '+' not understood.12:51:22.026 
Receiver	nil
Selector	+ 
Args	Integer 0
Object:doesNotUnderstand
Args
this	nil
selector	Symbol '+'
args	nil
Meta_MIDISyncClock:*nextTimeOnGrid
Args
this	MIDISyncClock
quant	Integer 4
phase	Integer 0
Vars
offset	nil
Meta_MIDISyncClock:*play
Args
this	MIDISyncClock
task	
▶ a Function
when	
▶ Quant Quant(-1, nil, 0)
EventStreamPlayer:play
Args
this	
▶ an EventStreamPlayer
argClock	MIDISyncClock
doReset	false
quant	
▶ Quant Quant(-1, nil, 0)

Make sure to follow the procedure in the help file exactly:

hjh

Here’s a quick video walk-through for MIDISyncClock. I should probably do a proper video tutorial with narration.

hjh

3 Likes

Hello James,
I just saw your video on YouTube about MIDISyncClock and I think problem is coming from my hardware. I have a old dsi evolver and apparently MIDISyncClock is not compatible with it cause it work fine with other sources. But Bitwig for example is taking evolver clock with no issues…

The error happened when I lunch this simple test patern after having followed you instructions:

Pbind(\dur,1).play(MIDISyncClock, quant: -1);

log is :

ERROR: binary operator '-' failed.
RECEIVER:
   nil
ARGS:
   Float 171.795471   8008E8A1 40657974
   nil

PROTECTED CALL STACK:
	Meta_MethodError:new	0x11938c100
		arg this = BinaryOpFailureError
		arg what = nil
		arg receiver = nil
	Meta_DoesNotUnderstandError:new	0x11938e0c0
		arg this = BinaryOpFailureError
		arg receiver = nil
		arg selector = -
		arg args = [ 171.795471208, nil ]
	Object:performBinaryOpOnSomething	0x1180dbf40
		arg this = nil
		arg aSelector = -
		arg thing = 171.795471208
		arg adverb = nil
	Meta_MIDISyncClock:seconds	0x119a3c3c0
		arg this = MIDISyncClock
	a FunctionDef	0x119a36a80
		sourceCode = "<an open Function>"
	Function:prTry	0x119d75c80
		arg this = a Function
		var result = nil
		var thread = a Thread
		var next = nil
		var wasInProtectedFunc = false
	
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of BinaryOpFailureError>
	Nil:handleError
		arg this = nil
		arg error = <instance of BinaryOpFailureError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of BinaryOpFailureError>
	Object:throw
		arg this = <instance of BinaryOpFailureError>
	Function:protect
		arg this = <instance of Function>
		arg handler = <instance of Function>
		var result = <instance of BinaryOpFailureError>
	< FunctionDef in Method Meta_MIDISyncClock:initClass >
		arg data = nil
		var lastTickDelta = 0.019698707000003
		var lastQueueTime = 288.0
		var nextTime = 171.795006719
		var task = <instance of Function>
		var tickIndex = nil
		var saveClock = <instance of Meta_SystemClock>
	Meta_MIDISyncClock:tick
		arg this = <instance of Meta_MIDISyncClock>
		arg index = 8
		arg data = nil
	Meta_MIDIIn:doSysrtAction
		arg this = <instance of Meta_MIDIIn>
		arg src = 1499883921
		arg index = 8
		arg val = nil
^^ The preceding error dump is for ERROR: binary operator '-' failed.
RECEIVER: nil

MIDISyncClock.tempo is working normally tho

Ok so after some testing it happens to work, sometimes. I think I will follow your advice and try to use Link for a better stability. Thanks for your help

When the error occurs, it’s because the internal variable startTime is not initialized.

startTime is initialized when MIDISyncClock receives a “start” message from the clock source.

So, if it works sometimes and breaks other times, it means that sometimes it’s getting the start message and other times, it isn’t.

One of the key points in the video is that the start message is the only way to be sure that the clock messages are beginning on the downbeat.

I could change the class so that the error isn’t fatal. But that would change the error into “silent failure” – then you would still have a question for me, but it would be “MIDISyncClock seems to be playing in tempo but the bars don’t line up with the hardware sequencer.” So as a coder, I have to make a decision which is worse: errors annoy you, but doing the wrong thing would both annoy you and confuse you.

I do see a mistake in the code and I’ll fix that shortly. I think I can also make it so that a missing start message is not fatal. But I’ll print a very large warning in the post window explaining that, if it’s receiving a clock tick without a start message first, then the startup sequence from the video has not been done correctly.

hjh

OK, done. https://github.com/jamshark70/ddwMIDI/commit/8cd3b982de72c9edb2bc2ef7a0dd15db07894d37

As noted, with this change, MIDISyncClock should now run even if it didn’t get a start message. This is good for supporting devices that don’t send “start” (they all should, but who knows…?). This is bad because: Previously, initializing incorrectly produced an error you couldn’t ignore. Now, it will “seem” to work but it’s very likely to be out of sync. I expect, a couple months later, there will be a question “I can’t get it to play in time.” (To which the answer will be: Follow the startup procedure carefully, every time.)

hjh

1 Like