-
-
Notifications
You must be signed in to change notification settings - Fork 51
Fixed issues with macOS 26 to close #226 #235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- [ERROR] thread 'main' panicked at 'failed overriding protocol method -[NSApplicationDelegate menu_item:]: method not found':
|
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. |
|
All right, sorry, I didn't know that. |
|
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 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. |
|
Can you replace 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. |
|
I’ve tried it, but no devices are being returned. |
|
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. |
|
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)
} |
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.