{
   "labels" : [
      "midi",
      "sync",
      "clock"
   ],
   "code" : "var window;\r\n\r\nvar deviceMenu, devices, device, midiout;\r\n\r\nvar tempoCtl, tempoCtlView, tempoWatcher;\r\n\r\nvar clock, beatView, beatRoutine;\r\nvar beatColor = Color.white;\r\nvar pendingColor = Color.gray(0.5);\r\nvar background = Color.gray(0.3);\r\n\r\nvar vBack = Color.gray(0.25);\r\n\r\nvar startB;\r\n\r\nvar routine;\r\n\r\nvar d = 1/24;\r\n\r\n// don't collide with main SC process (dammit)\r\nArchive.archiveDir = PathName.tmp;\r\n\r\nMIDIClient.init;  // synchronous\r\n\r\ndevices = MIDIClient.destinations.collect { |ep| ep.device + \" : \" ++ ep.name };\r\nif(thisProcess.argv.size >= 1) {\r\n\tdevice = devices.detectIndex { |str| str.containsi(thisProcess.argv[0]) };\r\n};\r\n\r\nclock = LinkClock.new.latency_(s.latency);\r\nTempoClock.default = clock;\r\nif(thisProcess.argv.size >= 2) {\r\n\tclock.tempo = thisProcess.argv[1].asFloat / 60.0;\r\n};\r\n\r\nmidiout = MIDIOut(0);\r\n\r\nwindow = Window(\"MIDI Clock Out\", Rect(800, 200, 500, 250)).front;\r\nwindow.background_(Color.gray(0.2));\r\n\r\nwindow.layout = VLayout(\r\n\tHLayout(\r\n\t\tnil,\r\n\t\tdeviceMenu = PopUpMenu(),\r\n\t\tstartB = Button(),\r\n\t\t// stopB = Button(),\r\n\t\tnil\r\n\t),\r\n\tView().fixedHeight_(60)\r\n\t.layout_(HLayout(\r\n\t\tnil,\r\n\t\tStaticText().string_(\"Tempo\").stringColor_(Color.white),\r\n\t\ttempoCtlView = View().minWidth_(400),\r\n\t\tnil\r\n\t)),\r\n\r\n\tbeatView = UserView().fixedHeight_(100)\r\n);\r\n\r\ndeviceMenu\r\n.items_(devices)\r\n.background_(vBack)\r\n.stringColor_(Color.white)\r\n.action_({ |view|\r\n\tif(device.notNil) {\r\n\t\tmidiout.disconnect(device);\r\n\t};\r\n\tdevice = view.value;\r\n\tmidiout.connect(device);\r\n});\r\nif(device.notNil) {\r\n\tdeviceMenu.value_(device);\r\n\tmidiout.connect(device);\r\n};\r\n\r\nstartB.states_([[\"stopped\", Color.white, Color.gray(0.5)], [\"RUNNING\", Color.black, Color(0.7, 1, 0.7)]])\r\n.action_({ |view|\r\n\tif(view.value > 0) {\r\n\t\tif(routine.notNil) { routine.stop };\r\n\t\troutine = Routine {\r\n\t\t\tmidiout.start;\r\n\t\t\t(thisThread.clock.nextBar - thisThread.beats).wait;\r\n\t\t\tloop {\r\n\t\t\t\t23.do { |i|\r\n\t\t\t\t\tmidiout.midiClock;\r\n\t\t\t\t\td.wait;\r\n\t\t\t\t};\r\n\t\t\t\tmidiout.midiClock;\r\n\t\t\t\t(thisThread.clock.beats.ceil - thisThread.beats).wait;\r\n\t\t\t};\r\n\t\t}.play(clock, quant: [-1, -0.5]);\r\n\t} {\r\n\t\tmidiout.stop;\r\n\t\troutine.stop;\r\n\t\troutine = nil;\r\n\t};\r\n});\r\n\r\ntempoCtl = EZSlider(tempoCtlView, Rect(10, 0, 350, 40), \"\", [40, 240],\r\n\tinitVal: clock.tempo * 60, labelWidth: 0, numberWidth: 45);\r\ntempoCtl.action_({ |view|\r\n\tclock.tempo = view.value / 60;\r\n});\r\ntempoWatcher = SimpleController(clock)\r\n.put(\\tempo, {\r\n\tdefer { tempoCtl.value = clock.tempo * 60 }\r\n});\r\n\r\ntempoCtl.numberView.stringColor_(Color.white).normalColor_(Color.white).background_(vBack);\r\ntempoCtl.sliderView.background_(vBack);\r\n\r\n// silly but I need to be sure MIDIClient init has finished\r\n{ { tempoCtl.value = clock.tempo * 60}.defer(1) }.defer(1);\r\n\r\nbeatView\r\n.background_(background)\r\n.drawFunc = { |view|\r\n\tvar b = view.bounds.moveTo(0, 0).insetBy(10, 10);\r\n\tvar bpb = clock.beatsPerBar;\r\n\tvar beat = clock.beatInBar.round;\r\n\tvar div = b.width / bpb;\r\n\tvar dimen = Rect(div * 0.2, b.height * 0.2 + b.top, div * 0.6, b.height * 0.6);\r\n\r\n\tbpb.do { |i|\r\n\t\tPen.color_(\r\n\t\t\tif(i <= beat) { beatColor } { pendingColor }\r\n\t\t)\r\n\t\t.fillOval(\r\n\t\t\tdimen.moveBy(i * div, 0)\r\n\t\t)\r\n\t};\r\n};\r\n\r\nbeatRoutine = Routine {\r\n\tloop {\r\n\t\t{ beatView.refresh }.defer(s.latency);\r\n\t\t1.0.wait;\r\n\t}\r\n}.play(clock, quant: 1);\r\n\r\nwindow.onClose = {\r\n\tbeatRoutine.stop;\r\n\troutine.stop;\r\n\tclock.stop;\r\n\ttempoWatcher.remove;\r\n\tif(thisProcess.platform.ideName != \"scqt\") { 0.exit };\r\n};",
   "is_private" : null,
   "id" : "1-5fy",
   "name" : "Command-line SC utility for MIDI clock out",
   "author" : "jamshark70",
   "ancestor_list" : [],
   "description" : "When 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.\r\n\r\nOne solution is to run the MIDI clock in a separate sclang instance, and sync to the main sclang instance using LinkClock.\r\n\r\nCommand-line:\r\n\r\nsclang path/to/thisUtility.scd deviceMatch tempo\r\n\r\nBoth parameters, after the code file path, are optional. You can change them in a GUI later.\r\n\r\n'deviceMatch' is a partial string match for the MIDI output device. 'tempo' is a float, for BPM.\r\n\r\nWhen 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.\r\n\r\nThis has been battle-tested in a pub live jam, driving a Digitakt, for an hour without glitches.\r\n\r\nNote that the MIDI '.connect' stuff is for Linux. You might need to tweak this for Windows or Mac."
}
