MIDI Device Add/Remove Callback

Does anyone have any ideas about setting up a callback for physically adding/removing a given MIDI device (as in, “when a MIDI device is added, do this, and when it’s removed, do that”)? My initial thought was to set up a task that would check the MIDI device list every so often, and if it changed, respond accordingly, like so:

scLang
Task({
		var delta;
		loop {
			delta = 0.1;
			~listA = MIDIClient.list;
			delta.yield;
			~listB = MIDIClient.list;
			(~listA != ~listB).if(
				128.do{
					arg i;
					if(
						MIDIClient.destinations.asString.contains("Midi Fighter Twister"),
						{
							AppClock.sched(0.5,{~comp[0].children[i].visible_(false)});
						},
						{
							AppClock.sched(0.5,{~comp[0].children[i].visible_(true)})
						}
					)
				}
			{};
			delta.yield;
			)
	}},
	AppClock
	).start;

This kind of works! The GUI objects appear when the device is unplugged and disappear when it’s plugged. When I tried to get any more complicated than “make these GUI objects appear/disappear,” though, stuff got messy. For one thing, I wanted the values of the GUI objects (knobs) to translate over to the MIDI device (MFT), so I added a line to send those values over when the device was added, like so:

more scLang
Task({
		var delta;
		loop {
			delta = 0.1;
			~listA = MIDIClient.list;
			delta.yield;
			~listB = MIDIClient.list;
			(~listA != ~listB).if(
				128.do{
					arg i;
					if(
						MIDIClient.destinations.asString.contains("Midi Fighter Twister"),
						{
							AppClock.sched(0.5,{~comp[0].children[i].visible_(false)});
							if(
								i<64,
								{
									~mft.control(0,i,~comp[0].children[i].value)
								},
								{
									~mft.control(4,i,~comp[0].children[i].value)
								}
							)
						},
						{
							AppClock.sched(0.5,{~comp[0].children[i].visible_(true)})
						}
					)
				}
			{};
			delta.yield;
			)
	}},
	AppClock
	).start;

But I found this was updating the values on the MFT constantly, such that if I changed them (i.e., by physically turning one of the encoders), they would immediately jump back to whatever value the GUI knob had left for them! I don’t really get why this was happening, since it’s only supposed to send those values if the MIDI device list changes, but for some reason, it was happening. This makes me wonder if there’s a better way to do this than a loop. I’m not aware of a MIDI device add/remove callback that already exists in scLang, but if there is one, of if anyone can think of a better way to build one, please let me know!

Unfortunately, there isn’t.

There are a couple of tricky things here.

One is that MIDIClient.list does not return the list.

~listA = MIDIClient.list;

~listA
-> MIDIClient

So (~listA != ~listB) should always be false, because this will always be MIDIClient != MIDIClient. If you’re finding that it’s sometimes true, then, maybe you have an extension that overrides .list.

The help for MIDIClient.list says “Refresh the list of available sources and destinations” – which, reading the method definition, is correct. After calling the method, MIDIClient.sources and MIDIClient.destinations will have new contents, but the return value from .list is useless.

Next problem – let’s try to detect whether a new device was added, by comparing the destination arrays:

MIDIClient.init;

~old = MIDIClient.destinations;  // save it
-> [ MIDIEndPoint("Midi Through", "Midi Through Port-0"), MIDIEndPoint("SuperCollider", "in0"), MIDIEndPoint("SuperCollider", "in1"), MIDIEndPoint("SuperCollider", "in2") ]

// First, test the case of *not* changing the devices
// ~old and ~new should be equivalent

MIDIClient.list;

~new = MIDIClient.destinations;  // save it
-> [ MIDIEndPoint("Midi Through", "Midi Through Port-0"), MIDIEndPoint("SuperCollider", "in0"), MIDIEndPoint("SuperCollider", "in1"), MIDIEndPoint("SuperCollider", "in2") ]

~old != ~new  // true :-X

So, by directly comparing the arrays, you can’t tell if the device list changed because it reports that the list changed even when it didn’t. (This is because MIDIEndPoint treats == the same as === – probably should be considered a bug.)

I would work around this by defining a function to test for a change:

(
var endPointEquals = { |a, b|
	a.isKindOf(MIDIEndPoint) and: {
		b.isKindOf(MIDIEndPoint) and: {
			a.device == b.device and: {
				a.name == b.name and: {
					a.uid == b.uid
				}
			}
		}
	}
};

var midiListNotEquals = { |a, b|
	a.size != b.size or: {
		a.any { |aItem, i|
			endPointEquals.(aItem, b[i]).not
		}
	}
};

Task({
	var old, new, delta = 0.1;
	MIDIClient.list;
	old = MIDIClient.destinations;
	loop {
		delta.yield;
		MIDIClient.list;
		new = MIDIClient.destinations;
		if(midiListNotEquals.(old, new)) {
			if(new.detect { |midiEndPoint|
				midiEndPoint.device.containsi("fighter twister")
			}) {
				AppClock.sched(0.5, {
					128.do { |i|
						~comp[0].children[i].visible_(false)
					};
				});
				128.do { |i|
					... set controls...
				};
			} {
				AppClock.sched(0.5, {
					128.do { |i|
						~comp[0].children[i].visible_(true)
					};
				});				
			};
		};
		old = new;
	};
}, AppClock).play;
)

Also note – the presence or absence of the Fighter Twister is not going to change in the middle of your 128.do loop. So there’s no point in repeating the test for each iteration. So I restructured it to check for the device only once.

hjh

As an alternative, maybe store the uids in a Set.

(
a = Set["a","b","c"];  // uids at start of system
b = Set["b", "d"]; // uids after a while
a.debug("starting point");
b.debug("new situation");
a.difference(b).debug("these uids were removed");
b.difference(a).debug("these uids were added");
)

This might do what you need: GitHub - scztt/MIDIWatcher.quark: Watch for connections and disconnections of MIDI devices
No help yet, sorry!

~changed = {
  |object, signal, endpoint|
  "%: %".format(signal, endpoint).postln;
};

MIDIWatcher.start; // required at startup / before monitoring will happen

// watch for one destination endpoint added
MIDIWatcher.deviceSignal("DeviceName", "EndpointName").signal(\destinationAdded).connectTo(~changed);

// watch for any source endpoint being added
MIDIWatcher.deviceSignal("DeviceName", "*").signal(\sourceAdded).connectTo(~changed);

// watch for any endpoint or device being removed
MIDIWatcher.deviceSignal("*", "*").signal(\sourceRemoved).connectTo(~changed);

The signals are: \sourceAdded, \sourceRemoved, \destinationAdded, \destinationRemoved
Device and endpoints are either names or “*”, which will match any device/endpoint.

Since your example code is referencing a MIDI Fighter, here’s an active example of this for MIDI Fighter Twister:

good catch! Thanks.

This basically works. What I mean by this is that the gui knobs disappear when the mft is plugged in, the gui knobs reappear when the mft is unplugged, and values are being migrated between the gui knobs and the mft on plug/unplug (and not the rest of the time, so there’s no jumping back to the previous knob value when I plug the mft in and change the knob value) more or less successfully. Here’s my tweaked version of this scLang that I’ve got doing all of the above:

scLang
	Task({
		var old, new, delta = 0.5;
		MIDIClient.list;
		old = MIDIClient.destinations;
		loop {
			delta.yield;
			MIDIClient.list;
			new = MIDIClient.destinations;
			if(~midiListNotEquals.(old, new)) {
				if(
					MIDIClient.destinations.asString.contains("Fighter Twister")
				) {
					AppClock.sched(0.5, {
						128.do { |i|
							~comp[0].children[i].visible_(false)
						};
					});
					128.do { |i|
						if(
							i<64,
							{
								~mft.control(0,i,~comp[0].children[i].value.linlin(0,1,0,127))
							},
							{
								~mft.control(4,i-64,~comp[0].children[i].value.linlin(0,1,0,127))
							}
						)
					};
				} {
					AppClock.sched(0, {
						128.do { |i|
							~comp[0].children[i].visible_(true)
						};
					});
				};
			};
			old = new;
		};
	}, AppClock).play;

Unfortunately, in practice, the experience is a little buggy. For one thing, changes on MIDI channel 1 seem to be propagating on channel 4, which shouldn’t be happening based on the MIDIDef. I’m not sure what’s going on there. In the context of the script I’m working on, that means a timbral parameter of a given synth (mapped to channel 4) changes along with the amp value for that synth (mapped to channel 1). In a way, this is kind of cool; it’s a bit like an LPG. But it wasn’t what I was going for. The other issues, which are much more relevant, are:

  1. I can’t seem to get things set up such that the mft can be hotplugged for the first time after script launch. As long as it’s plugged in when the script is run, it’s fine and can be hotplugged in and out without a problem. But I can’t seem to get the MIDIdef to run from inside the task, which means the mft has to be present at launch.
  2. the soft knobs sometimes appear at random intervals when the mft is plugged in. I have no idea why that’s happening. It would seem to imply that either A) the midiListNotEquals function is sometimes returning true even when there are no changes to the list of midi devices, or B) the mft is sometimes slipping off the list of midi devices while plugged in. Either way, I think it would be distracting in a performance context to have the knobs popping up and disappearing at irregular intervals every 30-60 seconds.

All of the above is leading me to the conclusion that maybe my idea of making the mft hot-pluggable was sort of ill-conceived, and I should just resign myself to needing to have the mft present or absent at boot and rolling with it from there. In reality, the script is perfectly useable that way! I think I was just trying to fit a square peg in a round hole here.

The MIDIdef was never posted here, so I can’t have any input on that.

This shouldn’t be a problem. Again, without seeing the code that you tried, there is no way to answer that.

One potential issue here is that the Task delta is 0.1 while the AppClock.sched for the GUI update is 0.5. Maybe try with the Task delta = 1.0 instead? So that each iteration will always finish all of its work before the next one.

Otherwise, I think it would have to be B. The appearing- or disappearing-knobs behavior is based on the MFT being found in the list or not. If you have two different lists, then midiListNotEquals would be true, but if both of those lists include the MFT, then there should be no change in the GUI.

We get the list from the operating system. If those results are inconsistent, then either the results from the OS are inconsistent, or we are doing something wrong with those results.

Which OS, btw? (Because the C++ code to get the list is different in macOS, Linux and Windows.)

hjh

This is a pretty reasonable goal - allowing your code to be runnable without the hardware plugged in will prevent huge headaches in the future…

If it helps, the way I architected this in my MIDI Fighter class (I would do this a little differently now, this is quite old - but I think it’s still a more or less valid approach):

  1. On device detection, I create a global object corresponding to the device. I never “disconnect” / trash these because it doesn’t make sense - I just re-use if the device is unplugged and re-plugged.
    Twister.quark/Twister.sc at 41c6395adcda44f301182613e9308de80d298602 · scztt/Twister.quark · GitHub
  2. When I create the individual objects representing e.g. MIDI knob connections (TwisterDeviceKnob), I make MIDIFunc’s to respond to the correct MIDI events. These have to be re-connected on CmdPeriod, because this de-registers all MIDI responders.
  3. These responders only fire a signal via .changed(), so they are decoupled from the objects I use to actually track and store control values.
  4. These ________Device objects are connected to the class I use for handling the actual behavior and parameter values of e.g. knobs (TwisterKnob). This means one virtual knob can be re-connected to different devices, and will also work with no connected device at all. (The specific signal connections are here: Twister.quark/Twister.sc at 41c6395adcda44f301182613e9308de80d298602 · scztt/Twister.quark · GitHub)
  5. The actual behavior when a MIDI values changes is handled here: Twister.quark/Twister.sc at 41c6395adcda44f301182613e9308de80d298602 · scztt/Twister.quark · GitHub. The ultimate parameter value is stored in a NumericControlValue, knobCV. I am using relative MIDI, so this is slightly more complex than absolute midi values, where you could just set the cv value directly.
  6. Changes to the cv are connected to an updateValue method, which in turn calls some methods to set the actual display ring values on the device - these become MIDI messages back to the Twister, This is required primarily because I want SC to be able to change / modulate parameter values, and have those reflected back to the device to avoid it becoming out of sync.