// title: Command-line SC utility for MIDI clock out // author: jamshark70 // 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. // // 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. // code: 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 }; };