Juce Midi device list and Tracktion midi device list desynced

I am trying to use the AudioDeviceManager from juce to get callbacks to handle midi events in a custom way.
However calling juce::DeviceManager::setMidiInputDeviceEnabled also tries to open the midi device much like calling te::DeviceManager::rescanMidiDeviceList(false)

When these two are used in tandem the juce and tracktion engine midi input device lists are out of sync so some apis that rely on one or the other don’t work, especially if we are checking for something in the underlying engine to see if the device is enable or so.

To add more context here, this was on Android and I believe that on Android cannot open a midi device more than once, if it’s already opened the open device call in juce_android_midi returns nullptr, instead of returning the already opened device pointer. Unsure what happens on Linux etc,

Do you need to open a device more than once? I.e. from somewhere other than Tracktion Engine?
Does using the te::DeviceManager’s juce::AudioDeviceManager member and then
addMidiInputDeviceCallback give you the functionality you need?

As an aside, I believe on Windows this also used to be an issue so there’s some internal JUCE functionality to only open the system MIDI device once but allow multiple JUCE handles to it to open and forward the events as expected. Maybe this could be added to the Android MIDI code as well?

Hey Dave!
No I do not need to open the device more than once, however if you look at the definition of that add callback handler, it checks of Juce has the midi device enabled. This does not work because the device was opened through tracktion and Juce has no idea that it was open.
The array enabledMidiDevices is always empty on Juce. And secondary to that android forwards callbacks only to the physical midi inputs device class and the chain ends there.
There is no routing of the messages back to AudioDeviceManager and hence we get no callbacks
The converse is also true where if I open it with Juce then I don’t get callbacks into Tracktion because the callbacks is set to AudioDeviceManager and physical midi inputs device gets no callbacks

Ok, I see the problem now.

It seems to me that there are two possible solutions:

  1. We re-write all the PhysicalMidiInput code in Tracktion Engine to use the AudioDeviceManager MIDI listener API
  2. JUCE add’s support for Android to open a MIDI device multiple times

Personally, 1 is very risky for us and we’re unlikely to get bandwidth to do it for a while. It feels a bit wrong to try and synchronise the juce::AudioDeviceManager list of open devices with ours, I can see that causing problems for other people.

It also sounds like 2 is probably more what JUCE users would expect and would be more in line with other platforms.

However, is there a third option from your perspective which would be to use addMidiInputDeviceCallback with no device ID? Do you then get all the callbacks for all devices regardless if they’ve been enabled or not?

I’ve tried option 3 as well, and I don’t get callbacks there either, the problem is that Juce android midi only sends midi messages to a single callback listener. Handle incoming midi messages is only sent from MidiMessageConcatenator.

Option 4 is to use a ControlSurface

Ah yeah, I just dug a bit deeper and it looks like it only forwards the callbacks when the device is enable from the AudioDeviceManager and it succeeds (i.e. hasn’t been opened elsewhere).

Just to clarify the requirements, you don’t actually need to go via the juce::AudioDeviceManager, you just need to get a callback when a te::MidiInputDevice receives an event?

Correct! Thats all we really need

void MidiInputDevice::handleIncomingMidiMessage (juce::MidiInput*, const juce::MidiMessage& m)
{
    const juce::ScopedValueSetter<bool> svs (eventReceivedFromDevice, true, false);
    handleIncomingMidiMessage (m);
    keyboardState.processNextMidiEvent (m);
}

In this function, if we can call the juce::AudioDeviceManager::handleIncomingMidiMessage or (we can do that, but only if we subscribe with an empty identifier, not the cleanest approach) provide a new API to subscribe to this, that would be the easiest.

void MidiInputDevice::sendMessageToInstances (const juce::MidiMessage& message)
{
    bool messageUnused = true;

    {
        const juce::ScopedLock sl (instanceLock);

        for (auto i : instances)
            if (i->handleIncomingMidiMessage (message))
                messageUnused = false;
    }

    if (messageUnused && message.isNoteOn())
        if (auto&& warnOfWasted = engine.getDeviceManager().warnOfWastedMidiMessagesFunction)
            warnOfWasted (this);
}

This second block would be an option as well, we get the processed message with timestamps appended from systems like PhysicalMidiInputDevice and VirtualMidiInputDevice

bool handleIncomingMidiMessage (const juce::MidiMessage& message)
    {
        if (recording)
            recorded.addEvent (juce::MidiMessage (message, context.globalStreamTimeToEditTimeUnlooped (message.getTimeStamp()).inSeconds()));

        juce::ScopedLock sl (consumerLock);

        for (auto c : consumers)
            c->handleIncomingMidiMessage (message);

        return recording || consumers.size() > 0;
    }

MidiInputDeviceInstanceBase::handleIncomingMidiMessage is probably my favorite, as I can get instances and I can do subscribe/unsubscribe as needed.

Hey @dave96,
Can I use consumers? As tracktion seems to have InputDeviceInstance::Consumers, and that seems to be pretty generic from what I can see, was that meant to be the replacement for AudioDeviceManager callbacks?
It seems to be doing exactly that, have not yet tried it but wondering if that is what its meant to do.
Let me know.
Cheers

So @dave96 , I used Consumers yesterday and that seems like what we can use which requires no engine changes whatsoever.
I managed to get everything working and this is overall very good news.

1 Like