Is a Timer Callback still the best way to update plugged in MIDI devices? (callback received while device is still shutting down)

I’ve been using some code I derived from the MidiDemo, which uses a TimerCallback every 500ms to check the list of available devices and update them, close no longer available ports, open new ones etc.

Is this still the best way to do it? Or is there some sort of hook now where you can be notified (by the OS) when the connected devices change?

While the following discussion relates to Mac OS, I am also interested in cross-platform.

The reason I ask is, I have noticed a problem with one particular device I have been testing on Mac OS, which is the TouchOSC Bridge virtual port. When I select that as an output port in my app, for example, and then Quit it (remove it), it will occasionally generate coreMIDI errors in the debugger.

My best guess is that it is still in the process of being freed when the TimerCallback arrives, and therefore returns errors because it is still an “available device” yet it has no MidiObjectRef properties.

If this device can do it, it seems others could be in the process of being freed when the TimerCallback arrives.

Here is where it fails in juce_mac_CoreMidi.cpp (with my debugging modifications):

    #undef CHECK_ERROR
    #define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__)

    static MidiDeviceInfo getMidiObjectInfo (MIDIObjectRef entity)
    {
        MidiDeviceInfo info;

        {
            ScopedCFString str;

            if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.cfString)))
                info.name = String::fromCFString (str.cfString);
// srk - debugging
            else
                DBG ("getMidiObjectInfo failed");
        }

        SInt32 objectID = 0;

        if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID)))
            info.identifier = String (objectID);
// srk - debugging
        else
            DBG ("getMidiObjectInfo failed");

        return info;
    }

…and it prints out these errors in the debugger:

CoreMIDI error: 65 - ffffffce
CoreMIDI error: 56 - ffffffce

It actually shows up momentarily in the new list of available devices, with the name “[error]” from this code:

    static Array<MidiDeviceInfo> findDevices (bool forInput)
    {
        // It seems that OSX can be a bit picky about the thread that's first used to
        // search for devices. It's safest to use the message thread for calling this.
        JUCE_ASSERT_MESSAGE_THREAD

        if (getGlobalMidiClient() == 0)
        {
            jassertfalse;
            return {};
        }

        enableSimulatorMidiSession();

        Array<MidiDeviceInfo> devices;
        auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations());

        for (ItemCount i = 0; i < numDevices; ++i)
        {
            MidiDeviceInfo deviceInfo;

            if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i))
                deviceInfo = getConnectedEndpointInfo (dest);

 // RIGHT HERE ===>
           if (deviceInfo == MidiDeviceInfo())
                deviceInfo.name = deviceInfo.identifier = "<error>";

            devices.add (deviceInfo);
        }

        return devices;
    }

My debugging printouts show the contents of the getAvailableDevices() call when this happens (14 devices). TouchOSC Bridge was number 14, it has been removed, but when the timerCallback arrives it is apparently still being freed:

updateOutputDeviceList: Output Device List changed numItems 14!
  0: IAC Driver Bus 1
  1: Motif XS6
  2: YAMAHA MOTIF XS6 Port2
  3: YAMAHA MOTIF XS6 Port3
  4: YAMAHA MOTIF XS6 Port4
  5: Steinberg CMC-PD Port1
  6: Steinberg CMC-PD Port2
  7: Steinberg CMC-PD Port3
  8: YAMAHA MOTIF XF7 Port1
  9: YAMAHA MOTIF XF7 Port2
  10: YAMAHA MOTIF XF7 Port3
  11: YAMAHA MOTIF XF7 Port4
  12: KRONOS SOUND
  13: <error>

And then the next timerCallback fixes it, because it is fully gone and now there are only 13 outputs available.

updateOutputDeviceList: Output Device List changed numItems 13!
  0: IAC Driver Bus 1
  1: Motif XS6
  2: YAMAHA MOTIF XS6 Port2
  3: YAMAHA MOTIF XS6 Port3
  4: YAMAHA MOTIF XS6 Port4
  5: Steinberg CMC-PD Port1
  6: Steinberg CMC-PD Port2
  7: Steinberg CMC-PD Port3
  8: YAMAHA MOTIF XF7 Port1
  9: YAMAHA MOTIF XF7 Port2
  10: YAMAHA MOTIF XF7 Port3
  11: YAMAHA MOTIF XF7 Port4
  12: KRONOS SOUND

Is there a better way of doing this, or does the fact that the JUCE code handles it gracefully enough and fills in the MidiDeviceInfo with [error] momentarily rather than asserting or crashing mean that it’s OK to ignore this kind of thing (and the coreMIDI errors it generates)?

1 Like

I do not have an answer for your general question (I do not know if this is still the suggested way). I just want to add something that may already be obvious to you: no matter how often a resource is polled, a resource can (and eventually will) become unavailable while in use. It is a good idea to attempt to trigger these errors during testing, and ensure that every function that assumes a device is present will gracefully handle disconnection/errors. Unfortunately it is time consuming and difficult to simulate every combination, as a resource can become disconnected at any point and it becomes tedious to mock/simulate/automate a resource disconnection in a way that adequately accounts for what happens in the wild. The cost of not doing this is potentially confusing crash reports from users, or a reputation of bugs in the software. IMO there is not a silver bullet for this – I have witnessed overzealous testing/mocking efforts in codebases that still fail to capture the reality of how resources fail all the while adding burdensome complexity to the codebase via dependency injection etc. I suggest starting with casual investigation, turning down the rate of polling to once every 5 seconds, and disconnecting/ re-connecting devices during different activities – if you are unable to find an ungraceful bug/crash, just leave it be until a problem is found.

1 Like