Open andykent opened 9 months ago
Thanks for trying the project and for this report.
The stack trace is interesting because it appears based on the HAL messages that device 0xab isn't recognized as a valid audio object so AudioObjectBaseClass
returns 0 (in lieu of throwing) which ends up in the default case in AudioObject.make
. The part that seems odd is that the call to AudioObjectClass
succeeds as evidenced by the aagg
in the log (which itself is only due to a fortuitous bug in the logging using the object class instead of the base class).
This report also made me realize that make
should probably return an optional and that it might also be wise to propagate errors more effectively from AudioObjectClass
and AudioObjectBaseClass
.
For this report specifically where did the audio object id 0xab come from? Was it from AudioDevice.devices()
or somewhere else?
I've created the throwing-make branch to attempt to better trap the errors you're seeing, although it doesn't address the root cause at this point.
In answer to your question. Yes, I'm pretty sure 0xab
came from AudioDevice.devices()
.
I just switched to the throwing-make
branch and it stops the crash so that is good progress at least.
I have included some further logging, the sample code I am using to log it with, and a screenshot of my audio devices. Hopefully it might be helpful?
func logDeviceData() {
let defaultDevice = try! AudioDevice.defaultOutputDevice()
for device in try! AudioDevice.devices() {
let transport = (try? device.transportType().debugDescription) ?? "error"
if device.objectID == defaultDevice.objectID {
logger
.warning(
"DEFAULT DEVICE: \(device.debugDescription, privacy: .public) - \(transport, privacy: .public)"
)
} else {
logger
.log(
"DEVICE: \(device.debugDescription, privacy: .public) - \(transport, privacy: .public)"
)
}
if let layout = try? device.preferredChannelLayout(inScope: .output) {
logger.log("preferred layout -> \(layout.debugDescription, privacy: .public)")
}
}
}
Sidequest Note: I think the ('srnd', 'outp', main)) failed: 'who?'
warnings are because I am trying to get preferredChannelLayout
on devices that are input not output. What's the best way to check if a device has output here?
Sidequest Note: I think the ('srnd', 'outp', main)) failed: 'who?' warnings are because I am trying to get preferredChannelLayout on devices that are input not output. What's the best way to check if a device has output here?
I believe hasSelector(.preferredChannelLayout, inScope: .output)
is the best way to check for the preferred channel layout.
This is just a guess, but are you using AVAudioEngine
? If I remember correctly it creates private aggregate devices on the fly, at least in certain situations.
Yes, we are using AVAudioEngine. Interesting, that sounds quite likely then. Now I'm just not sure what to do with them or how to ignore them.
Thanks for the tip about using hasSelector
, that resolved those pieces. Given it seems invalid to call these selectors if they aren't available does it make sense for the library to check hasSelector
in all these functions and either throw or return optionals?
It would allow code like:
if device.hasSelector(.preferredChannelLayout, inScope: .output),
let layout = try? device.preferredChannelLayout(inScope: .output)
to be simplified to:
if let layout = try? device.preferredChannelLayout(inScope: .output)
and avoid having to know/check the selector at every call-site.
Found another very edgy-edge-case in AudioObject.make()
.
I am able to trigger the precondition
. By:
try? AudioDevice.defaultOutputDevice()
after a AVAudioEngineConfigurationChange
notification.To be fair, this also causes weirdness in Audio Midi Setup so I think it's Apple's issue at heart but it does illustrate that the precondition can at least trigger in some situations so now that make
is throwing maybe raising an error here is more appropriate?
Here's the step to select an invalid channel combo in Audio Midi Setup.
Quick update. I managed to trigger the bcls
crash again today, just whilst connecting / disconnecting AirPods with AVAudioEngine running.
To clarify I am using AudioSystemObject.instance.whenSelectorChanges(.defaultOutputDevice, perform:)
and then running this code in the perform. So the code gets run when I switch devices. I think it must be some sort of race condition with AVAudioEngine reconfiguring as sometimes it's fine but other times not.
E.g. trying to select 8 channel output for a hardware device that only supports 6 channels.
The fact that this is allowed at all makes me think this could be a bug in Audio MIDI Setup or even lower down. But I agree that the precondition shouldn't fail. I suppose a question is whether a system will always have a default output device and I'd assumed it always would. From your testing it seems that is an incorrect assumption.
I suppose make
could return an optional for invalid object IDs but I throwing for the case of kAudioObjectUnknown
also seems reasonable.
To clarify I am using
AudioSystemObject.instance.whenSelectorChanges(.defaultOutputDevice, perform:)
and then running this code in the perform. So the code gets run when I switch devices. I think it must be some sort of race condition with AVAudioEngine reconfiguring as sometimes it's fine but other times not.
Thanks for the detail!
It is starting to sound like AVAE's private aggregate device might be changed out from underneath during make
. It would be a great data point to know which device is causing the crash. Do you happen to log all the devices at app launch for a baseline?
It would allow code like:
if device.hasSelector(.preferredChannelLayout, inScope: .output), let layout = try? device.preferredChannelLayout(inScope: .output)
to be simplified to:
if let layout = try? device.preferredChannelLayout(inScope: .output)
and avoid having to know/check the selector at every call-site.
If one is using try?
to discard errors is there a need to check if the property is supported? I do see your point for the general case, though.
Here's a list of the devices from earlier on before the crash. It's possible it's the AudioAggregateDevice
listed there.
If one is using try? to discard errors is there a need to check if the property is supported? I do see your point for the general case, though.
Yes, agreed, the functional result is probably the same but it would avoid the scary HAL warnings in the console. :)
The CADefaultDeviceAggregate-20036-15
is an aggregate device created by AVAudioEngine
although the ID doesn't match the one from the previous crash. But those devices are also created and destroyed somewhat frequently (I think that particular device is the 15th one created in pid 20036) so I'm still leaning toward the race condition/device being destroyed midway through make
. Although I am puzzled on what the best resolution would be.
I just tagged release 0.2.0 which should fix the crashes but not the underlying problem.
Are you doing anything special in the audio engine configuration change notification? From the stack trace it looks like the crash is happening on the main thread from a closure executed from within the AVAE configuration change handling. Could there be multiple configuration change notifications in a row which might cause the creed aggregate device to become stale?
< insert rant about AUGraph behaving predictably here >
Our setup is a little exotic here so it's very possible we are triggering this race.
I monitor changes in output devices so we can reconfigure AE to match the output channel count. We do this because we do some custom rendering that supports different output formats, e.g. stereo, binaural, 5.1, 7.1, etc.
We monitor both AVAudioEngineConfigurationChange
and defaultOutputDevice
and rebuild/restart AE when either of these indicate a change in output formats.
The logging above is the first thing I do when defaultOutputDevice
changes but it is possible AVAudioEngineConfigurationChange
is firing at a similar time however all this is scheduled on the main thread currently so they shouldn't be happening concurrently. Assuming of course AE.stop/start don't trigger concurrent changes internally.
So far I haven't been able to reproduce the issue so it's proving a hard one to track down.
I just committed changes that allow the dispatch queue on which property change notifications are invoked to be specified. A nil
queue equates to direct block invocation. Perhaps trying that or using the queue for your AVAE instance would provide more information on the order changes occur, assuming they are deterministic.
First off, This is a truly amazing project and Herculean effort, so thanks, it's been a great resource and I hope it can continue to grow.
In my experiments I have noticed a fair number of crashes when calling
AudioDevice.devices()
as it seems an object in the list can't be cast correctly.This seems to trigger when listing devices on or around the time that there is change to the default route, e.g. when connecting headphones. I am calling
AudioDevice.devices()
on the main thread in response to a change in the default output device.