«Command-line SC utility for MIDI clock out» by jamshark70

on 27 Dec'21 05:52 in midisyncclock

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.

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 };
};
raw 3420 chars (focus & ctrl+a+c to copy)
reception
comments