libremidi icon indicating copy to clipboard operation
libremidi copied to clipboard

API function to get macOS LocationID and Windows ContainerID of an USB device

Open JoergAtGithub opened this issue 1 year ago • 4 comments

For compound USB devices, that have not only a MIDI interface, but interfaces used with other USB device classes (e.g. a display) as well, it is needed to group them. The operating systems macOS and Windows provide identifiers for this purpose:

  • macOS: LocationID
  • Windows: ContainerID It would be nice, if the libremidi API would provide getters, to get these IDs for an USB device.

JoergAtGithub avatar Apr 04 '24 20:04 JoergAtGithub

The operating systems macOS and Windows provide identifiers for this purpose:

Do you have some sample code of how to do this ?

jcelerier avatar Apr 05 '24 01:04 jcelerier

For macOS you need to get the USBLocationID property from MIDIDeviceRef, similar to the code example here: https://stackoverflow.com/questions/43071011/is-there-a-way-to-tell-if-a-midi-device-is-connected-via-usb-on-ios/50637087#50637087

For Windows you need to get the DEVPKEY_Device_ContainerId property returned by CM_Get_DevNode_PropertyW: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/devpkey-device-containerid#:~:text=The%20DEVPKEY_Device_ContainerId%20device%20property%20is%20used%20by%20the,that%20represents%20an%20instance%20of%20a%20physical%20device

JoergAtGithub avatar Apr 05 '24 16:04 JoergAtGithub

Thanks. Now I have to find how I get these device nodes in MS Windows, as right now the code does not use these APIs AFAIK. For macOS it looks pretty doable.

jcelerier avatar Apr 08 '24 17:04 jcelerier

I'm revisiting this and am wondering: port_information already stores the MIDIObjectRef's UUID so you can get it back in the following way (typing this without access to macOS so likely there are some mistakes but you get the gist):

int32_t usb_id_from_port(const libremidi::port_information& info) {
  // Get the MIDI object from the uid
  auto uid = std::bit_cast<std::int32_t>((uint32_t)info.port);
  MIDIObjectRef object{};
  MIDIObjectType type{};
  auto ret = MIDIObjectFindByUniqueID(uid, &object, &type);
  assert(type == kMIDIObjectType_Source || type == kMIDIObjectType_Destination);

  // Get the MIDI entity from the object
  MIDIEntityRef entity{};
  MIDIEndpointGetEntity(object, &entity); 
  if(!entity) 
    throw; // It means it's not a hardware port

  // Get the MIDI device from the entity
  MIDIDeviceRef device{};
  MIDIEntityGetDevice(entity, &device);
  if(!device)
    throw;

  SInt32 res{};
  MIDIObjectGetIntegerProperty(device, CFSTR("USBDeviceID"), &res);
  return res;
}

For Windows I investigated but I see absolutely no way to get the device ID from the WinMM API. :/

jcelerier avatar Aug 25 '24 15:08 jcelerier

For Windows I investigated but I see absolutely no way to get the device ID from the WinMM API. :/

What about this approach:

#include <windows.h>
#include <setupapi.h>
#include <devpkey.h>
#include <iostream>
#include <string>
#include <initguid.h>

#pragma comment(lib, "setupapi.lib")

void CheckMidiDevices() {
    // Get the device information set for all MIDI devices
    HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "SWD\\MMDEVAPI\\{0.0.1.00000000}.{00000000-0000-0000-0000-000000000000}", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
    if (deviceInfoSet == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to get device information set." << std::endl;
        return;
    }

    SP_DEVINFO_DATA deviceInfoData;
    deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);

    // Enumerate through all devices in the set
    for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) {
        // Get the device instance ID
        char deviceInstanceId[MAX_DEVICE_ID_LEN];
        if (CM_Get_Device_ID(deviceInfoData.DevInst, deviceInstanceId, MAX_DEVICE_ID_LEN, 0) != CR_SUCCESS) {
            continue;
        }

        // Get the device description
        char deviceDescription[256];
        if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_DEVICEDESC, NULL, (PBYTE)deviceDescription, sizeof(deviceDescription), NULL)) {
            std::cout << "Device: " << deviceDescription << std::endl;
        }

        // Get the ContainerID
        DEVPROPTYPE propType;
        GUID containerId;
        if (SetupDiGetDeviceProperty(deviceInfoSet, &deviceInfoData, &DEVPKEY_Device_ContainerId, &propType, (PBYTE)&containerId, sizeof(containerId), NULL, 0)) {
            LPOLESTR guidString;
            StringFromCLSID(containerId, &guidString);
            std::wcout << L"ContainerID: " << guidString << std::endl;
            CoTaskMemFree(guidString);
        }

        // Get the device's parent to determine the connection type
        DEVINST parentDevInst;
        if (CM_Get_Parent(&parentDevInst, deviceInfoData.DevInst, 0) == CR_SUCCESS) {
            char parentDeviceInstanceId[MAX_DEVICE_ID_LEN];
            if (CM_Get_Device_ID(parentDevInst, parentDeviceInstanceId, MAX_DEVICE_ID_LEN, 0) == CR_SUCCESS) {
                // Check if the parent device is a USB or Bluetooth device
                if (strstr(parentDeviceInstanceId, "USB") != NULL) {
                    std::cout << "Connection Type: USB" << std::endl;
                } else if (strstr(parentDeviceInstanceId, "BTH") != NULL) {
                    std::cout << "Connection Type: Bluetooth" << std::endl;
                } else {
                    std::cout << "Connection Type: Other" << std::endl;
                }
            }
        }
    }

    // Clean up
    SetupDiDestroyDeviceInfoList(deviceInfoSet);
}

int main() {
    CheckMidiDevices();
    return 0;
}

JoergAtGithub avatar Nov 16 '24 19:11 JoergAtGithub

hm but do we know for sure that the index of

for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) 

is the same index than the one used by WinMM to enumerate MIDI devices ?

jcelerier avatar Nov 17 '24 00:11 jcelerier

especially in "borderline" cases where we plug multiple identical MIDI devices which have historically been iffy in winmm

jcelerier avatar Nov 17 '24 00:11 jcelerier

I don't know unfortunately. But in general the purpose of the ContainerId is to handle these "iffy" devices, multi functional(e.g. MIDI and screens) but without proper serial number or with multiple physical interfaces. The most common use case for the ContainerId is a multifunctional printer/scanner connected via USB and WLAN at the same time. The ContainerId groups everything and ensures, that it appears as one device.

JoergAtGithub avatar Nov 17 '24 00:11 JoergAtGithub