Skip to content

Conversation

@xThePumaZ
Copy link

This PR closes issue #226 , where the service matching with IOAudioEngine wouldn’t return any devices anymore under macOS 26

To fix this issue and improve the reliability of the process, I switched to CoreAudio.
In the process, I also refactored the get_goxlr_devices function.

As I am not really familiar with Rust, GitHub Copilot wrote most of the code.
If there is a more elegant way of doing this, please suggest it.

xThePumaZ added 4 commits December 12, 2025 21:35
- [ERROR] thread 'main' panicked at 'failed overriding protocol method -[NSApplicationDelegate menu_item:]: method not found':
@FrostyCoolSlug
Copy link
Member

Please do not bump the version number, that's handled during release.

I'll take a look at the rest of this when I get some time.

@xThePumaZ
Copy link
Author

All right, sorry, I didn't know that.
I'll revert the version number to 1.2.3.

@FrostyCoolSlug
Copy link
Member

FrostyCoolSlug commented Dec 21, 2025

Reviewing the code, I'm also not happy with the GoXLR being found by name, simply because it's not very future proof, nor is it particularly defensive against problems. The reason I was previously using the USB VID/PID is because it GUARANTEES the device that's being looked at is a device we're expecting, there's no ambiguity or question, nor is it something that can break in the future.

A simple example of this, is that your code won't detect a GoXLR Mini in is_physical_goxlr_usb, as you're using parts[2] == "GoXLR" as an exact match.

I'm spent some time reading, and from what I can tell, the change in MacOS 26 was a decoupling of the USB parameters from the IOAudioEngine object, so surely the solution here would be to use IORegistryEntryGetParentEntry and walk up from the IOAudioEngine to the attached IOUSBDevice, and from there you can extract the PID and VID as before.

@FrostyCoolSlug
Copy link
Member

FrostyCoolSlug commented Dec 21, 2025

Can you replace get_goxlr_devices from the original code with the following:

pub fn get_goxlr_devices() -> Result<Vec<CoreAudioDevice>> {
    let mut devices: Vec<CoreAudioDevice> = Vec::new();

    let mut iterator = mem::MaybeUninit::<io_iterator_t>::uninit();
    let matcher = unsafe { IOServiceMatching(c"IOAudioEngine".as_ptr() as *const c_char) };
    let status = unsafe {
        IOServiceGetMatchingServices(kIOMasterPortDefault, matcher, iterator.as_mut_ptr())
    };

    if status != KERN_SUCCESS as i32 {
        bail!("Failed to Get Matching Service: {}", status);
    }

    let vid = CFString::new("idVendor");
    let pid = CFString::new("idProduct");
    let uid = CFString::new("IOAudioEngineUID");
    let dsc = CFString::new("IOAudioEngineDescription");

    loop {
        let service = unsafe { IOIteratorNext(iterator.assume_init()) };
        if service == 0 {
            break;
        }

        // Pull the properties for this engine
        let mut dictionary = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
        unsafe {
            IORegistryEntryCreateCFProperties(
                service,
                dictionary.as_mut_ptr(),
                kCFAllocatorDefault,
                0,
            );
        }

        let properties: CFDictionary<CFString, CFType> = unsafe {
            CFMutableDictionary::wrap_under_get_rule(dictionary.assume_init()).to_immutable()
        };

        // Extract the UID for our device
        let engine_uid = match properties.get(&uid).and_then(|v| v.downcast::<CFString>()) {
            Some(u) => u.to_string(),
            None => continue,
        };

        // We need to walk up the IORegistry to find the USB Device
        let mut current = service;
        let usb_device = loop {
            let mut parent: io_registry_entry_t = 0;
            let result = unsafe {
                IORegistryEntryGetParentEntry(current, kIOServicePlane.as_ptr() as _, &mut parent)
            };

            // If this isn't successful (possibly no further parent), break out
            if result != KERN_SUCCESS {
                break None;
            }

            let class_name = unsafe {
                // Ask the parent for its class name
                let mut name = [0i8; 128];
                IOObjectGetClass(parent, name.as_mut_ptr());
                CStr::from_ptr(name.as_ptr()).to_string_lossy().into_owned()
            };

            // Have we reached the top level USB Device (IOUSBDevice hasn't been used since 10.11,
            // but we'll check anyway for SnG)
            if class_name == "IOUSBDevice" || class_name == "IOUSBHostDevice" {
                break Some(parent);
            }

            // Not a USB device, set this as the current, and run again to find the next parent.
            current = parent;
        };

        // Make sure a USB Device was found
        let usb = match usb_device {
            Some(u) => u,
            None => continue,
        };

        // Pull the properties for the USB device
        let mut dictionary = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
        unsafe {
            IORegistryEntryCreateCFProperties(usb, dictionary.as_mut_ptr(), kCFAllocatorDefault, 0);
        }

        let properties: CFDictionary<CFString, CFType> = unsafe {
            CFMutableDictionary::wrap_under_get_rule(dictionary.assume_init()).to_immutable()
        };

        // Extract VID / PID from USB device, if these don't exist, something has gone
        // horribly wrong, because we have a USB device without required properties.
        let vid = properties.get(&vid).downcast::<CFNumber>().unwrap();
        let pid = properties.get(&pid).downcast::<CFNumber>().unwrap();

        // Check whether the Vendor is TC-Helicon
        if vid != VID_GOXLR as i32 {
            continue;
        }

        // Check whether we're a GoXLR
        if pid != PID_GOXLR_FULL as i32 && pid != PID_GOXLR_MINI as i32 {
            continue;
        }

        // Finally grab the description
        let description = properties.get(&dsc).downcast::<CFString>().unwrap();

        // Push to our audio list
        devices.push(CoreAudioDevice {
            display_name: description.to_string(),
            uid: engine_uid,
        });
    }

    Ok(devices)
}

And test that instead? There might be some imports missing, and I have no way to test it directly because I don't own a Mac, but based on the MacOS documentation and APIs it should fix the problem by walking the audio node parents until it finds an IOUSBHostDevice object, then will extract the VID/PID from there instead.

@xThePumaZ
Copy link
Author

I’ve tried it, but no devices are being returned.
It always breaks at the point where it checks if service is equal to zero.

@FrostyCoolSlug
Copy link
Member

Try this instead:

/*
    This function iterates over all the IOUSBHostDevices, attempting to find a GoXLR. If found, it
    then iterates over the children of that device, looking for audio engines, which it then
    collects and returns.
*/
pub fn get_goxlr_devices() -> Result<Vec<CoreAudioDevice>> {
    let mut devices: Vec<CoreAudioDevice> = Vec::new();

    let mut iterator = mem::MaybeUninit::<io_iterator_t>::uninit();
    let matcher = unsafe { IOServiceMatching(b"IOUSBHostDevice\0".as_ptr() as *const c_char) };
    let status = unsafe {
        IOServiceGetMatchingServices(kIOMasterPortDefault, matcher, iterator.as_mut_ptr())
    };

    if status != KERN_SUCCESS as i32 {
        bail!("Failed to Get Matching Service: {}", status);
    }

    let vid = CFString::new("idVendor");
    let pid = CFString::new("idProduct");
    let uid = CFString::new("IOAudioEngineUID");
    let dsc = CFString::new("IOAudioEngineDescription");

    loop {
        let service = unsafe { IOIteratorNext(iterator.assume_init()) };
        if service == 0 {
            break;
        }

        // Pull the properties for this USB device
        let mut dictionary = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
        unsafe {
            IORegistryEntryCreateCFProperties(
                service,
                dictionary.as_mut_ptr(),
                kCFAllocatorDefault,
                0,
            );
        }
        let properties: CFDictionary<CFString, CFType> = unsafe {
            CFMutableDictionary::wrap_under_get_rule(dictionary.assume_init()).to_immutable()
        };

        // Extract VID / PID
        let vid_value = properties.get(&vid).downcast::<CFNumber>().unwrap();
        let pid_value = properties.get(&pid).downcast::<CFNumber>().unwrap();

        // Filter for GoXLR
        if vid_value != VID_GOXLR as i32 {
            continue;
        }
        if pid_value != PID_GOXLR_FULL as i32 && pid_value != PID_GOXLR_MINI as i32 {
            continue;
        }

        // Iterate children to find audio engines
        let mut child_iterator: io_iterator_t = 0;
        unsafe {
            IORegistryEntryGetChildIterator(
                service,
                kIOServicePlane.as_ptr() as _,
                &mut child_iterator,
            );
        }

        loop {
            let child = unsafe { IOIteratorNext(child_iterator) };
            if child == 0 {
                break;
            }

            let mut dict = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
            unsafe {
                IORegistryEntryCreateCFProperties(child, dict.as_mut_ptr(), kCFAllocatorDefault, 0);
            }
            let props: CFDictionary<CFString, CFType> = unsafe {
                CFMutableDictionary::wrap_under_get_rule(dict.assume_init()).to_immutable()
            };

            if let Some(engine_uid) = props.get(&uid).and_then(|v| v.downcast::<CFString>()) {
                let description = props.get(&dsc).downcast::<CFString>().unwrap();

                devices.push(CoreAudioDevice {
                    display_name: description.to_string(),
                    uid: engine_uid.to_string(),
                });
            }
        }
    }

    Ok(devices)
}

This does it inversely, it finds the GoXLR first, then iterates the children to find the Audio UID from there.

Again, I don't have a Mac, I can't test this, some reading of the documentation (or at least some more pointed AI questions) may be needed to find the answer to this, rather than having AI rewrite all the code while losing the context.

@FrostyCoolSlug
Copy link
Member

If that doesn't work, try this.. (Seriously, there are a LOT of things that could be tried here, this code recursively checks children to find the AudioEngine)..

pub fn get_goxlr_devices() -> Result<Vec<CoreAudioDevice>> {
    let mut devices: Vec<CoreAudioDevice> = Vec::new();

    let mut iterator = mem::MaybeUninit::<io_iterator_t>::uninit();
    let matcher = unsafe { IOServiceMatching(b"IOUSBHostDevice\0".as_ptr() as *const c_char) };
    let status = unsafe {
        IOServiceGetMatchingServices(kIOMasterPortDefault, matcher, iterator.as_mut_ptr())
    };

    if status != KERN_SUCCESS as i32 {
        bail!("Failed to Get Matching Service: {}", status);
    }

    let vid = CFString::new("idVendor");
    let pid = CFString::new("idProduct");
    let uid = CFString::new("IOAudioEngineUID");
    let dsc = CFString::new("IOAudioEngineDescription");

    // Yo dawg..
    fn traverse_children(
        parent: io_registry_entry_t,
        uid_key: &CFString,
        dsc_key: &CFString,
        devices: &mut Vec<CoreAudioDevice>,
    ) {
        let mut child_iterator: io_iterator_t = 0;
        unsafe {
            IORegistryEntryGetChildIterator(
                parent,
                kIOServicePlane.as_ptr() as _,
                &mut child_iterator,
            );
        }

        loop {
            let child = unsafe { IOIteratorNext(child_iterator) };
            if child == 0 {
                break;
            }

            // Pull child properties
            let mut dict = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
            unsafe {
                IORegistryEntryCreateCFProperties(child, dict.as_mut_ptr(), kCFAllocatorDefault, 0);
            }
            let props: CFDictionary<CFString, CFType> = unsafe {
                CFMutableDictionary::wrap_under_get_rule(dict.assume_init()).to_immutable()
            };

            // If this child has an IOAudioEngineUID, it’s an audio engine
            if let Some(engine_uid) = props.get(uid_key).and_then(|v| v.downcast::<CFString>()) {
                let description = props.get(&dsc).downcast::<CFString>().unwrap();

                devices.push(CoreAudioDevice {
                    display_name: description.to_string(),
                    uid: engine_uid.to_string(),
                });
            }

            // Recurse into this child in case the engine is deeper
            traverse_children(child, uid_key, dsc_key, devices);
        }
    }

    loop {
        let service = unsafe { IOIteratorNext(iterator.assume_init()) };
        if service == 0 {
            break;
        }

        // Pull USB device properties
        let mut dictionary = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
        unsafe {
            IORegistryEntryCreateCFProperties(
                service,
                dictionary.as_mut_ptr(),
                kCFAllocatorDefault,
                0,
            );
        }
        let properties: CFDictionary<CFString, CFType> = unsafe {
            CFMutableDictionary::wrap_under_get_rule(dictionary.assume_init()).to_immutable()
        };

        // Extract VID / PID
        let vid = properties.get(&vid).downcast::<CFNumber>().unwrap();
        let pid = properties.get(&pid).downcast::<CFNumber>().unwrap();

        // Filter for GoXLR devices
        if vid_value != VID_GOXLR as i32 {
            continue;
        }
        if pid_value != PID_GOXLR_FULL as i32 && pid_value != PID_GOXLR_MINI as i32 {
            continue;
        }

        // Recursively find all audio engines under this USB device
        traverse_children(service, &uid, &dsc, &mut devices);
    }

    Ok(devices)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants