«Command-line SC utility for MIDI clock out» by jamshark70
on 27 Dec'21 05:52 inWhen sending MIDI clock out at a fast tempo, there may be fewer than 10 ms between clock ticks. If you're sending the clock messages from the same sclang that is sequencing musical events, any unexpectedly long-running sclang activity could cause clock messages to be delayed, interfering with timing.
One solution is to run the MIDI clock in a separate sclang instance, and sync to the main sclang instance using LinkClock.
Command-line:
sclang path/to/thisUtility.scd deviceMatch tempo
Both parameters, after the code file path, are optional. You can change them in a GUI later.
'deviceMatch' is a partial string match for the MIDI output device. 'tempo' is a float, for BPM.
When you click the button next to the device menu, it will wait until just before the next bar line, and then send a MIDI clock start message, and start running ticks on the downbeat.
This has been battle-tested in a pub live jam, driving a Digitakt, for an hour without glitches.
Note that the MIDI '.connect' stuff is for Linux. You might need to tweak this for Windows or Mac.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
var window; var deviceMenu, devices, device, midiout; var tempoCtl, tempoCtlView, tempoWatcher; var clock, beatView, beatRoutine; var beatColor = Color.white; var pendingColor = Color.gray(0.5); var background = Color.gray(0.3); var vBack = Color.gray(0.25); var startB; var routine; var d = 1/24; // don't collide with main SC process (dammit) Archive.archiveDir = PathName.tmp; MIDIClient.init; // synchronous devices = MIDIClient.destinations.collect { |ep| ep.device + " : " ++ ep.name }; if(thisProcess.argv.size >= 1) { device = devices.detectIndex { |str| str.containsi(thisProcess.argv[0]) }; }; clock = LinkClock.new.latency_(s.latency); TempoClock.default = clock; if(thisProcess.argv.size >= 2) { clock.tempo = thisProcess.argv[1].asFloat / 60.0; }; midiout = MIDIOut(0); window = Window("MIDI Clock Out", Rect(800, 200, 500, 250)).front; window.background_(Color.gray(0.2)); window.layout = VLayout( HLayout( nil, deviceMenu = PopUpMenu(), startB = Button(), // stopB = Button(), nil ), View().fixedHeight_(60) .layout_(HLayout( nil, StaticText().string_("Tempo").stringColor_(Color.white), tempoCtlView = View().minWidth_(400), nil )), beatView = UserView().fixedHeight_(100) ); deviceMenu .items_(devices) .background_(vBack) .stringColor_(Color.white) .action_({ |view| if(device.notNil) { midiout.disconnect(device); }; device = view.value; midiout.connect(device); }); if(device.notNil) { deviceMenu.value_(device); midiout.connect(device); }; startB.states_([["stopped", Color.white, Color.gray(0.5)], ["RUNNING", Color.black, Color(0.7, 1, 0.7)]]) .action_({ |view| if(view.value > 0) { if(routine.notNil) { routine.stop }; routine = Routine { midiout.start; (thisThread.clock.nextBar - thisThread.beats).wait; loop { 23.do { |i| midiout.midiClock; d.wait; }; midiout.midiClock; (thisThread.clock.beats.ceil - thisThread.beats).wait; }; }.play(clock, quant: [-1, -0.5]); } { midiout.stop; routine.stop; routine = nil; }; }); tempoCtl = EZSlider(tempoCtlView, Rect(10, 0, 350, 40), "", [40, 240], initVal: clock.tempo * 60, labelWidth: 0, numberWidth: 45); tempoCtl.action_({ |view| clock.tempo = view.value / 60; }); tempoWatcher = SimpleController(clock) .put(\tempo, { defer { tempoCtl.value = clock.tempo * 60 } }); tempoCtl.numberView.stringColor_(Color.white).normalColor_(Color.white).background_(vBack); tempoCtl.sliderView.background_(vBack); // silly but I need to be sure MIDIClient init has finished { { tempoCtl.value = clock.tempo * 60}.defer(1) }.defer(1); beatView .background_(background) .drawFunc = { |view| var b = view.bounds.moveTo(0, 0).insetBy(10, 10); var bpb = clock.beatsPerBar; var beat = clock.beatInBar.round; var div = b.width / bpb; var dimen = Rect(div * 0.2, b.height * 0.2 + b.top, div * 0.6, b.height * 0.6); bpb.do { |i| Pen.color_( if(i <= beat) { beatColor } { pendingColor } ) .fillOval( dimen.moveBy(i * div, 0) ) }; }; beatRoutine = Routine { loop { { beatView.refresh }.defer(s.latency); 1.0.wait; } }.play(clock, quant: 1); window.onClose = { beatRoutine.stop; routine.stop; clock.stop; tempoWatcher.remove; if(thisProcess.platform.ideName != "scqt") { 0.exit }; };