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