Execution breakpoint when trying to play a music library file with AVAudioEngine

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!

Answered by DTS Engineer in 850591022

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:

  1. Annotate tapBlock enclosure as @Sendable
  2. Isolate the call of `self?.processAudioBuffer(buffer)
  3. 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

  1. Annotate tapBlock enclosure as @Sendable
  2. Extract data from AVAudioBuffer within the closure
  3. 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

I've made a small breakthrough, the code crashes because of the installTap call, but the bad news is that I'm unable to figure out why. Any ideas?

Accepted Answer

I accidentally selected this reply as an accepted answer, is it possible to undo this action?

Please post a full crash log and I'll see what I can determine.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Please find attached my crash log. I believe this is the right one.

Please find attached my crash log. I believe this is the right one.

So, here is where the crash log shows you're crashing:

Thread 3 Crashed:
...
6   libswift_Concurrency.dylib        	       0x19a4e043c _swift_task_checkIsolatedSwift + 48 
7   libswift_Concurrency.dylib        	       0x19a53fc50 swift_task_isCurrentExecutorWithFlagsImpl(swift::SerialExecutorRef, swift::swift_task_is_current_executor_flag) + 356 
8   VisualMan.debug.dylib             	       0x10285f8a4 closure #1 in AudioEngineManager.setupAudioEngine()
9   VisualMan.debug.dylib             	       0x10285f9c0 thunk for @escaping @callee_guaranteed (@guaranteed AVAudioPCMBuffer, @guaranteed AVAudioTime) -> ()
10  AVFAudio                          	       0x1ab981bf8 AVAudioNodeTap::CheckEmitBuffer() + 1304  [inlined]
...
14  AVFAudio                          	       0x1ab980b0c invocation function for block in CADeprecated::RealtimeMessenger::RealtimeMessenger(applesauce::dispatch::v1::queue) + 108 
15  libclang_rt.asan_ios_dynamic.dylib	       0x102dc7398 __wrap_dispatch_source_set_event_handler_block_invoke + 196 
16  libdispatch.dylib                 	       0x1966b77cc _dispatch_client_callout + 16 
...
37  libsystem_pthread.dylib           	       0x21da8e3b8 _pthread_wqthread + 292 
38  libsystem_pthread.dylib           	       0x21da8d8c0 start_wqthread + 8

It doesn't provide exact line numbers; however, the fact that you're crashing on a block called from AVAudioNodeTap means that this is where the crash has to be coming from:

analyzer.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
  self?.processAudioBuffer(buffer)
}

So, the basic issue here is that the audio system is capturing the block in one execution context (probably the main thread) and calling it in another, something that Swift Concurrency does not allow without additional notations on the block. There is a bug on that issue (r.132434288), however, I'd encourage you to file your own bug on this and post the bug back here.

In terms of solutions, I'm not sure what the best way to sort this out is. I've pinged a few people and I'll let them reply.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

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.

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:

  1. Annotate tapBlock enclosure as @Sendable
  2. Isolate the call of `self?.processAudioBuffer(buffer)
  3. 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

  1. Annotate tapBlock enclosure as @Sendable
  2. Extract data from AVAudioBuffer within the closure
  3. 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

Both approaches work! Thank you so much! I decided to go with the second option as it seems more robust than marking the import with @preconcurrency and just silencing concurrency warnings.

Execution breakpoint when trying to play a music library file with AVAudioEngine
 
 
Q