JSN
JSN

Reputation: 11

User initial press of an arrow key in TextField causes AVAudioEngine "skipping cycle due to overload"

Working on a SwiftUI app (targeting macOS 14) which automatically plays a song via AVFoundation's AVAudioEngine, and provides the user with several text editing fields (using TextField).

When the user first presses one of the text fields, and presses either of the four arrow keys, there's a very noticeable blip in the song playback. At that instant, Xcode's console log displays the error " 76291 HALC_ProxyIOContext.cpp:1327 HALC_ProxyIOContext::IOWorkLoop: skipping cycle due to overload".

If the user presses any of the arrow keys again in the same or other TextFields, then the problem does not reoccur. It only occurs when the app first loads, and the user first presses one of the arrow keys when first editing text in a TextField.

Here's a simple example demonstrating the problem (note: you'll need to enter the url for a song on your device; and in the target's 'App Sandbox' settings, 'File access' may need to be set to 'read' if your song is in the 'Music Folder'):

import SwiftUI
import AVFoundation

struct ContentView: View {

  @StateObject var song = Song(urlString: "[enterPathToAudioFile]")

  @State var editableText = "Change me!"

  var body: some View {
    HStack {
      TextField("", text: $editableText) {
      }
    }
    .onAppear() {
      song.load()
      song.play()
    }
  }
}

class Song: ObservableObject {

  var url:            URL
  var audioFile       = AVAudioFile()
  var audioEngine     = AVAudioEngine()
  var playerNode      = AVAudioPlayerNode()
  var audioSampleRate = Double()

  let timeDelay: TimeInterval = 0.01 // seconds

  init(urlString: String) {

    self.url = URL(string: urlString)!
    audioEngine.attach(playerNode)
    audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: nil)
    let outputNode = audioEngine.outputNode
    audioEngine.connect(playerNode, to: outputNode, format: nil)
    audioEngine.prepare()
    do {
      try audioEngine.start()
    } catch {
      print("Error starting audio engine: \(error)")
    }

    try? audioEngine.start()
  }

  func preloadSong() {
    DispatchQueue.global(qos: .userInitiated).async {
      do {
        self.audioFile = try AVAudioFile(forReading: self.url)

        let audioFormat = self.audioFile.fileFormat
        self.audioSampleRate = Double(audioFormat.sampleRate)

        DispatchQueue.main.async {
            // Schedule the audio file for playback using the player node
          let audioTime = AVAudioTime(sampleTime: AVAudioFramePosition(self.timeDelay * self.audioSampleRate),
                                      atRate: self.audioSampleRate)

          self.playerNode.scheduleFile(self.audioFile, at: audioTime, completionHandler: nil)
        }
      } catch {
        print("Error loading audio file: \(error)")
      }
    }
  }

  func load() {
    if playerNode.isPlaying { playerNode.stop() }
    preloadSong()
  }

  func play() {
    playerNode.play()
  }

}

I've searched, but have not found a way to pre-perform whatever the TextField tries to do at the instant the user first presses an arrow key. My understanding is that AVAudioEngine and its components handle their own internal threading for audio processing. I also understand a solution might involve creating a dedicated thread for audio operations, however this may be a bit tricky and seems like overkill for this.

UPDATE: Thanks to comment by @sbooth, I realized this issue only occurs when running the code within Xcode. It does not occur in the release build.

Upvotes: 0

Views: 826

Answers (0)

Related Questions