1MIDIEndPoint {
2	var <>device, <>name, <>uid;
3	*new{ arg device, name, uid;
4		^super.newCopyArgs(device, name, uid)
5	}
6	printOn { arg stream;
7		stream << this.class.name << "(" <<<*
8			[device, name]  <<")"
9	}
10}
11
12MIDIClient {
13	classvar <myinports, <myoutports; // for linux it is useful to keep track of how many we open ourselves
14	classvar <sources, <destinations;
15	classvar <initialized=false;
16	*init { arg inports, outports, verbose=true; // by default initialize all available ports
17								// you still must connect to them using MIDIIn.connect
18
19		this.prInitClient;
20		this.list;
21		if(inports.isNil,{inports = sources.size});
22		if(outports.isNil,{outports = destinations.size});
23		// this.disposeClient;
24
25		this.prInit(inports,outports);
26		initialized = true;
27		// might ask for 1 and get 2 if your device has it
28		// or you might ask for a 1 and get 0 of nothing is plugged in
29		// so we warn you
30		if(sources.size < inports or: {destinations.size < outports},{
31			"WARNING:".postln;
32			("MIDIClient-init requested " ++ inports ++ " inport(s) and " ++ outports
33				++ " outport(s),").postln;
34			("but found only " ++ sources.size ++ " inport(s) and " ++ destinations.size
35				++ " outport(s).").postln;
36			"Some expected MIDI devices may not be available.".postln;
37		});
38		myinports = inports;
39		myoutports = outports;
40
41		this.list;
42
43		ShutDown.add { this.disposeClient };
44
45		if ( verbose,{
46			Post << "MIDI Sources:" << Char.nl;
47			sources.do({ |x| Post << Char.tab << x << Char.nl });
48			Post << "MIDI Destinations:" << Char.nl;
49			destinations.do({ |x| Post << Char.tab << x << Char.nl });
50		});
51	}
52	*list {
53		var list;
54		list = this.prList;
55		if(list.notNil, {
56			sources = list.at(0).collect({ arg id,i;
57				MIDIEndPoint(list.at(1).at(i), list.at(2).at(i), id)
58			});
59			destinations = list.at(3).collect({arg id, i;
60				MIDIEndPoint(list.at(5).at(i), list.at(4).at(i), id)
61			});
62		});
63	}
64	*prInit { arg inports, outports;
65		_InitMIDI
66		^this.primitiveFailed
67	}
68	*prInitClient {
69		_InitMIDIClient
70		^this.primitiveFailed
71	}
72	*prList {
73		_ListMIDIEndpoints
74		^this.primitiveFailed
75	}
76	*disposeClient {
77		this.prDisposeClient;
78		initialized = false;
79	}
80	*prDisposeClient {
81		_DisposeMIDIClient
82		^this.primitiveFailed
83	}
84	*restart {
85		_RestartMIDI
86		^this.primitiveFailed
87	}
88
89	// overridden in Linux:
90	*externalSources{
91		^sources;
92	}
93
94	*externalDestinations{
95		^destinations;
96	}
97
98	*getClientID {
99		if (thisProcess.platform.name != \linux) {
100			warn("% is only implemented in linux,"
101				"and should never be called directly in user code."
102				.format(thisMethod)
103			);
104		};
105		^nil
106	}
107}
108
109
110MIDIEvent {
111	var <>status, <>port, <>chan, <>b, <>c, <>thread;
112
113	*new { arg status, port, chan, b, c, thread;
114		^super.newCopyArgs(status, port, chan, b, c, thread)
115	}
116	set { arg inStatus, inPort, inChan, inB, inC, inThread;
117		status = inStatus;
118		port = inPort;
119		chan = inChan;
120		b = inB;
121		c = inC;
122		inThread !? { thread = inThread };
123	}
124	match { arg inPort, inChan, inB, inC;
125		^port.matchItem(inPort) and: {
126			chan.matchItem(inChan) and: {
127			b.matchItem(inB) and: {
128			c.matchItem(inC)
129		}}}
130	}
131	// convenience accessors
132	note { ^b }
133	veloc { ^c }
134	ctlnum { ^b }
135	ctlval { ^c }
136}
137
138MIDIIn {
139	var port;
140	classvar <>action,
141	<> noteOn, <> noteOff, <> polytouch,
142	<> control, <> program,
143	<> touch, <> bend,
144	<> sysex, sysexPacket, <> sysrt, <> smpte, <> invalid;
145
146	classvar
147	<> noteOnList, <> noteOffList, <> polyList,
148	<> controlList, <> programList,
149	<> touchList, <> bendList;
150
151	classvar
152	<> noteOnZeroAsNoteOff = true;
153
154	// safer than global setters
155	*addFuncTo { |what, func|
156		this.perform(what.asSetter, this.perform(what).addFunc(func))
157	}
158
159	*removeFuncFrom { |what, func|
160		this.perform(what.asSetter, this.perform(what).removeFunc(func))
161	}
162
163	*replaceFuncTo { |what, func, newFunc|
164		this.perform(what.asSetter, this.perform(what).replaceFunc(func, newFunc))
165	}
166
167	*waitNoteOn { arg port, chan, note, veloc;
168		var event;
169		event = MIDIEvent(\noteOn, port, chan, note, veloc, thisThread);
170		noteOnList = noteOnList.add(event); // add to waiting list
171		nil.yield; // pause the thread.
172		^event
173	}
174	*waitNoteOff { arg port, chan, note, veloc;
175		var event;
176		event = MIDIEvent(\noteOff, port, chan, note, veloc, thisThread);
177		noteOffList = noteOffList.add(event); // add to waiting list
178		nil.yield; // pause the thread.
179		^event
180	}
181	*waitPoly { arg port, chan, note, veloc;
182		var event;
183		event = MIDIEvent(\poly, port, chan, note, veloc, thisThread);
184		polyList = polyList.add(event); // add to waiting list
185		nil.yield; // pause the thread.
186		^event
187	}
188	*waitTouch { arg port, chan, val;
189		var event;
190		event = MIDIEvent(\touch, port, chan, val, nil, thisThread);
191		touchList = touchList.add(event); // add to waiting list
192		nil.yield; // pause the thread.
193		^event
194	}
195	*waitControl { arg port, chan, num, val;
196		var event;
197		event = MIDIEvent(\control, port, chan, num, val, thisThread);
198		controlList = controlList.add(event); // add to waiting list
199		nil.yield; // pause the thread.
200		^event
201	}
202	*waitBend { arg port, chan, val;
203		var event;
204		event = MIDIEvent(\bend, port, chan, val, nil, thisThread);
205		bendList = bendList.add(event); // add to waiting list
206		nil.yield; // pause the thread.
207		^event
208	}
209	*waitProgram { arg port, chan, num;
210		var event;
211		event = MIDIEvent(\program, port, chan, num, nil, thisThread);
212		programList = programList.add(event); // add to waiting list
213		nil.yield; // pause the thread.
214		^event
215	}
216
217	*doAction { arg src, status, a, b, c;
218		action.value(src, status, a, b, c);
219	}
220	*doNoteOnAction { arg src, chan, num, veloc;
221		if ( noteOnZeroAsNoteOff and: ( veloc == 0 ) ){
222			noteOff.value(src, chan, num, veloc);
223			this.prDispatchEvent(noteOffList, \noteOff, src, chan, num, veloc);
224		}{
225			noteOn.value(src, chan, num, veloc);
226			this.prDispatchEvent(noteOnList, \noteOn, src, chan, num, veloc);
227		};
228	}
229	*doNoteOffAction { arg src, chan, num, veloc;
230		noteOff.value(src, chan, num, veloc);
231		this.prDispatchEvent(noteOffList, \noteOff, src, chan, num, veloc);
232	}
233	*doPolyTouchAction { arg src, chan, num, val;
234		polytouch.value(src, chan, num, val);
235		this.prDispatchEvent(polyList, \poly, src, chan, num, val);
236	}
237	*doControlAction { arg src, chan, num, val;
238		control.value(src, chan, num, val);
239		this.prDispatchEvent(controlList, \control, src, chan, num, val);
240	}
241	*doProgramAction { arg src, chan, val;
242		program.value(src, chan, val);
243		this.prDispatchEvent(programList, \program, src, chan, val);
244	}
245	*doTouchAction { arg src, chan, val;
246		touch.value(src, chan, val);
247		this.prDispatchEvent(touchList, \touch, src, chan, val);
248	}
249	*doBendAction { arg src, chan, val;
250		bend.value(src, chan, val);
251		this.prDispatchEvent(bendList, \bend, src, chan, val);
252	}
253
254	*doSysexAction { arg src,  packet;
255		sysexPacket = sysexPacket ++ packet;
256		if (packet.last == -9, {
257			sysex.value(src, sysexPacket);
258			sysexPacket = nil
259		});
260	}
261	*doInvalidSysexAction { arg src, packet;
262		invalid.value(src, packet);
263	}
264
265	*doSysrtAction { arg src, index, val;
266		sysrt.value(src, index, val);
267	}
268
269	*doSMPTEaction { arg src, frameRate, timecode;
270		smpte.value(src, frameRate, timecode);
271	}
272
273	*findPort { arg deviceName,portName;
274		^MIDIClient.sources.detect({ |endPoint| endPoint.device == deviceName and: {endPoint.name == portName}});
275	}
276
277	*disconnectAll {
278		if(MIDIClient.initialized,{
279			MIDIClient.externalSources.do({ |src,i|
280				MIDIIn.disconnect(i,src);
281			});
282		});
283	}
284
285	*connectAll { |verbose=true|
286		if(MIDIClient.initialized.not,
287			{ MIDIClient.init(verbose: verbose) },
288			{ MIDIIn.disconnectAll; MIDIClient.list; }
289		);
290		MIDIClient.externalSources.do({ |src,i|
291			MIDIIn.connect(i,src);
292		});
293	}
294
295	*connect { arg inport=0, device=0;
296		var uid,source;
297		if(MIDIClient.initialized.not,{ MIDIClient.init });
298		if(device.isNumber, {
299			if(device >= 0, {
300				if ( device > MIDIClient.sources.size,{ // on linux the uid's are very large numbers
301					source = MIDIClient.sources.detect{ |it| it.uid == device };
302					if(source.isNil,{
303						("MIDI device with uid"+device+ "not found").warn;
304					},{
305						uid = source.uid;
306					})
307				},{
308					source = MIDIClient.sources.at(device);
309					if(source.isNil,{
310						"MIDIClient failed to init".warn;
311					},{
312						uid = MIDIClient.sources.at(device).uid;
313					});
314				});
315			},{ // elsewhere they tend to be negative
316				uid = device;
317			});
318		},{
319			if(device.isKindOf(MIDIEndPoint), {uid = device.uid}); // else error
320		});
321		this.connectByUID(inport,uid);
322	}
323	*disconnect { arg inport=0, device=0;
324		var uid, source;
325		if(device.isKindOf(MIDIEndPoint), {uid = device.uid});
326		if(device.isNumber, {
327			if(device.isPositive, {
328				if ( device > MIDIClient.sources.size,
329					{
330						source = MIDIClient.sources.select{ |it| it.uid == device }.first;
331						if(source.isNil,{
332							("MIDI device with uid"+device+ "not found").warn;
333						},{
334							uid = source.uid;
335						})
336					},
337					{
338						source = MIDIClient.sources.at(device);
339						if(source.isNil,{
340							"MIDIClient failed to init".warn;
341						},{
342							uid = MIDIClient.sources.at(device).uid;
343						});
344					});
345			},{
346				uid = device;
347			});
348		});
349		this.disconnectByUID(inport,uid);
350	}
351	*connectByUID {arg inport, uid;
352		_ConnectMIDIIn
353		^this.primitiveFailed;
354	}
355	*disconnectByUID {arg inport, uid;
356		_DisconnectMIDIIn
357		^this.primitiveFailed;
358	}
359
360	*prDispatchEvent { arg eventList, status, port, chan, b, c;
361		var selectedEvents;
362		eventList ?? {^this};
363		eventList.takeThese {| event |
364			if (event.match(port, chan, b, c))
365			{
366				selectedEvents = selectedEvents.add(event);
367				true
368			}
369			{ false };
370		};
371		selectedEvents.do{ |event|
372				event.set(status, port, chan, b, c);
373				event.thread.next;
374		}
375	}
376}
377
378MIDIOut {
379	var <>port, <>uid, <>latency = 0.2;
380
381	*new { arg port, uid;
382		if(thisProcess.platform.name != \linux) {
383			^super.newCopyArgs(port, uid ?? { MIDIClient.destinations[port].uid });
384		} {
385			^super.newCopyArgs(port, uid ?? 0 );
386		}
387	}
388	*newByName { arg deviceName, portName, dieIfNotFound = true;
389		var endPoint, index;
390		endPoint = MIDIClient.destinations.detect { |ep,epi|
391			index = epi;
392			ep.device == deviceName and: { ep.name == portName }
393		};
394		if(endPoint.isNil) {
395			if(dieIfNotFound) {
396				Error("Failed to find MIDIOut port " + deviceName + portName).throw;
397			} {
398				("Failed to find MIDIOut port " + deviceName + portName).warn;
399			};
400		};
401		if(thisProcess.platform.name != \linux) {
402			^this.new(index, endPoint.uid)
403		} {
404			if(index < MIDIClient.myoutports) {
405				// 'index' here is for "SuperCollider:out0", ":out1" etc.
406				// Practically speaking, this establishes associations:
407				// out0 --> destinations[0]
408				// out1 --> destinations[1] and so on.
409				// It looks weird but, in fact, it does ensure a 1-to-1 connection.
410				// Explained further in MIDIOut help.
411				^this.new(index, endPoint.uid)
412			} {
413				// If you didn't initialize enough MIDI output ports,
414				// it will connect the new device to 0.
415				// Connections with a UID are always 1-to-1.
416				^this.new(0, endPoint.uid)
417			}
418		}
419	}
420	*findPort { arg deviceName, portName;
421		^MIDIClient.destinations.detect { |endPoint|
422			endPoint.device == deviceName and: { endPoint.name == portName }
423		};
424	}
425
426	write { arg len, hiStatus, loStatus, a=0, b=0;
427		this.send(port, uid, len, hiStatus, loStatus, a, b, latency);
428	}
429
430	noteOn { arg chan, note=60, veloc=64;
431		this.write(3, 16r90, chan.asInteger, note.asInteger, veloc.asInteger);
432	}
433	noteOff { arg chan, note=60, veloc=64;
434		this.write(3, 16r80, chan.asInteger, note.asInteger, veloc.asInteger);
435	}
436	polyTouch { arg chan, note=60, val=64;
437		this.write(3, 16rA0, chan.asInteger, note.asInteger, val.asInteger);
438	}
439	control { arg chan, ctlNum=7, val=64;
440		this.write(3, 16rB0, chan.asInteger, ctlNum.asInteger, val.asInteger);
441	}
442	program { arg chan, num=1;
443		this.write(2, 16rC0, chan.asInteger, num.asInteger);
444	}
445	touch { arg chan, val=64;
446		this.write(2, 16rD0, chan.asInteger, val.asInteger);
447	}
448	bend { arg chan, val=8192;
449		val = val.asInteger;
450		this.write(3, 16rE0, chan, val bitAnd: 127, val >> 7);
451	}
452	allNotesOff { arg chan;
453		this.control(chan, 123, 0);
454	}
455	smpte	{ arg frames=0, seconds=0, minutes=0, hours=0, frameRate = 3;
456		var packet;
457		packet = [frames, seconds, minutes, hours]
458			.asInteger
459			.collect({ arg v, i; [(i * 2 << 4) | (v & 16rF), (i * 2 + 1 << 4) | (v >> 4) ] });
460		packet = packet.flat;
461		packet.put(7, packet.at(7) | ( frameRate << 1 ) );
462		packet.do({ arg v; this.write(2, 16rF0, 16r01, v); });
463	}
464	songPtr { arg songPtr;
465		songPtr = songPtr.asInteger;
466		this.write(4, 16rF0, 16r02, songPtr & 16r7f, songPtr >> 7 & 16r7f);
467	}
468	songSelect { arg song;
469		this.write(3, 16rF0, 16r03, song.asInteger);
470	}
471	midiClock {
472		this.write(1, 16rF0, 16r08);
473	}
474	start {
475		this.write(1, 16rF0, 16r0A);
476	}
477	continue {
478		this.write(1, 16rF0, 16r0B);
479	}
480	stop {
481		this.write(1, 16rF0, 16r0C);
482	}
483	reset {
484		this.write(1, 16rF0, 16r0F);
485	}
486
487	sysex { arg packet;
488		^this.prSysex( uid, packet );
489	}
490
491	send { arg outport, uid, len, hiStatus, loStatus, a=0, b=0, late;
492		_SendMIDIOut
493		^this.primitiveFailed;
494	}
495
496	prSysex { arg uid, packet;
497		_SendSysex
498		^this.primitiveFailed;
499	}
500}
501