Hi all,
I'm working on an audio visualizer app that plays files from the user's music library utilizing MediaPlayer and AVAudioEngine. I'm working on getting the music library functionality working before the visualizer aspect.
After setting up the engine for file playback, my app inexplicably crashes with an EXC_BREAKPOINT with code = 1. Usually this means I'm unwrapping a nil value, but I think I'm handling the optionals correctly with guard statements. I'm not able to pinpoint where it's crashing. I think it's either in the play function or the setupAudioEngine function. I removed the processAudioBuffer function and my code still crashes the same way, so it's not that. The device that I'm testing this on is running iOS 26 beta 3, although my app is designed for iOS 18 and above.
After commenting out code, it seems that the app crashes at the scheduleFile call in the play function, but I'm not fully sure.
Here is the setupAudioEngine function:
private func setupAudioEngine() {
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Audio session error: \(error)")
}
engine.attach(playerNode)
engine.attach(analyzer)
engine.connect(playerNode, to: analyzer, format: nil)
engine.connect(analyzer, to: engine.mainMixerNode, format: nil)
analyzer.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
self?.processAudioBuffer(buffer)
}
}
Here is the play function:
func play(_ mediaItem: MPMediaItem) {
guard let assetURL = mediaItem.assetURL else {
print("No asset URL for media item")
return
}
stop()
do {
audioFile = try AVAudioFile(forReading: assetURL)
guard let audioFile else {
print("Failed to create audio file")
return
}
duration = Double(audioFile.length) / audioFile.fileFormat.sampleRate
if !engine.isRunning {
try engine.start()
}
playerNode.scheduleFile(audioFile, at: nil)
playerNode.play()
DispatchQueue.main.async { [weak self] in
self?.isPlaying = true
self?.startDisplayLink()
}
} catch {
print("Error playing audio: \(error)")
DispatchQueue.main.async { [weak self] in
self?.isPlaying = false
self?.stopDisplayLink()
}
}
}
Here is a link to my test project if you want to try it out for yourself: https://github.com/aabagdi/VisualMan-example
Thanks!
Ah I see, thanks for the explanation! I've already submitted a bug report via Feedback Assistant a few days ago (FB18933857), but have yet to hear anything back.
This hasn't made it back to you, but there are two workaround which added to your bug which should eventually be sent back to you. Those are:
Option 1:
- Annotate tapBlock enclosure as @Sendable
- Isolate the call of `self?.processAudioBuffer(buffer)
- However, since AVAudioBuffer is not marked as senable, either inport AVFAudio.AVAudioBuffer or AVFoundation with @preconcurrency annotation
@preconcurrency import AVFAudio.AVAudioBuffer // or @preconcurrency import AVFoundation
[…]
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { @Sendable [weak self] buffer, _ in
Task { @MainActor in
self?.processAudioBuffer(buffer)
}
}
Option 2:
To avoid annotating the import with @preconcurrency
- Annotate tapBlock enclosure as @Sendable
- Extract data from AVAudioBuffer within the closure
- Isolate the call of `self?.processAudioData(array)
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { @Sendable [weak self] buffer, _ in
// Extract the data from the buffer
guard let channelData = buffer.floatChannelData?[0] else { return }
let frameCount = Int(buffer.frameLength)
let audioData = Array(UnsafeBufferPointer(start: channelData, count: frameCount))
Task { @MainActor in
self?.processAudioData(audioData)
}
}
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware