ios - 使用 AVAudioPlayer 时出现 "__CFRunLoopModeFindSourceForMachPort returned NULL"消息

标签 ios sprite-kit avfoundation avaudioplayer mach

我们正在开发一款 SpriteKit 游戏。为了更好地控制音效,我们从使用 SKAudioNode 改为使用一些 AVAudioPlayers。虽然在游戏、帧速率和声音方面一切似乎都运行良好,但在物理设备上进行测试时,我们偶尔会在控制台输出中看到错误(?)消息:

... [常规] __CFRunLoopModeFindSourceForMachPort 对于模式“kCFRunLoopDefaultMode”livePort 返回 NULL:#####

当它发生时,它似乎并没有真正造成任何伤害(没有声音故障或帧速率或其他任何问题),但不准确理解该消息的含义以及它发生的原因让我们感到紧张。

详细信息:

游戏都是标准的 SpriteKit,所有事件都由 SKActions 驱动,没有什么异常。

AVFoundation 东西的用途如下。应用程序声音的初始化:

class Sounds {
  let soundQueue: DispatchQueue

  init() {
    do {
      try AVAudioSession.sharedInstance().setActive(true)
    } catch {
      print(error.localizedDescription)
    }
    soundQueue = DispatchQueue.global(qos: .background)
  }

  func execute(_ soundActions: @escaping () -> Void) {
    soundQueue.async(execute: soundActions)
  }
}

创建各种音效播放器:

guard let player = try? AVAudioPlayer(contentsOf: url) else {
  fatalError("Unable to instantiate AVAudioPlayer")
}
player.prepareToPlay()

播放音效:

let pan = stereoBalance(...)
sounds.execute {
  if player.pan != pan {
    player.pan = pan
  }
  player.play()
}

AVAudioPlayers 都是用于没有循环的短音效,并且它们被重用。我们总共创建了大约 25 个玩家,其中包括多个可以快速连续重复的特定效果的玩家。对于特定的效果,我们以固定的顺序轮流播放该效果的玩家。我们已经验证,每当触发播放器时,其 isPlaying 都是 false,因此我们不会尝试对已经在播放的内容调用播放。

这条消息并不常见。在一个 5-10 分钟的游戏过程中,可能有数千种音效,我们可能会看到该消息 5-10 次。

该消息似乎最常在快速连续播放一堆音效时出现,但感觉与此并不 100% 相关。

不使用调度队列(即让sound.execute直接调用soundActions())并不能解决问题(尽管这确实会导致游戏明显滞后)。将调度队列更改为其他一些优先级(例如 .utility)也不会影响该问题。

使sounds.execute立即返回(即,实际上根本不调用闭包,因此没有play())确实消除了消息。

我们确实在此链接中找到了生成消息的源代码:

https://github.com/apple/swift-corelibs-foundation/blob/master/CoreFoundation/RunLoop.subproj/CFRunLoop.c

但我们除了抽象层面之外并不理解它,并且不确定运行循环是如何参与 AVFoundation 的内容的。

大量谷歌搜索没有发现任何有用的信息。正如我所指出的,它似乎根本没有引起明显的问题。不过,如果知道为什么会发生这种情况,以及如何解决它或确定它永远不会成为问题,那就太好了。

最佳答案

我们仍在努力解决这个问题,但已经进行了足够的实验,因此我们应该如何做事情已经很清楚了。概要:

使用场景的audioEngine属性。

对于每个音效,创建一个 AVAudioFile 用于从包中读取音频的 URL。将其读入 AVAudioPCMBuffer。将缓冲区粘贴到按声音效果索引的字典中。

制作一堆 AVAudioPlayerNode。 Attach() 它们到audioEngine。连接(playerNode,到:audioEngine.mainMixerNode)。目前,我们正在动态创建这些节点,搜索当前的玩家节点列表以找到未玩的节点,如果没有可用的则创建一个新节点。这可能会产生比需要的更多的开销,因为我们必须有回调来观察播放器节点何时完成正在播放的内容并将其设置回停止状态。我们将尝试切换到固定的最大数量的事件音效,并按顺序轮流播放。

要播放声音效果,请获取该效果的缓冲区,找到一个不繁忙的playerNode,然后执行playerNode.scheduleBuffer(buffer, ...)。如果当前没有播放,则为playerNode.play()。

一旦我们完全转换和清理了内容,我可能会用一些更详细的代码更新它。我们仍然有几个长期运行的 AVAudioPlayer,我们尚未切换为使用 AVAudioPlayerNode 通过混音器。但无论如何,通过上述方案泵送绝大多数音效已经消除了错误消息,并且它需要的东西要少得多,因为内存中不再像我们以前那样重复音效。有一点延迟,但我们还没有尝试将一些东西放在后台线程上,也许不必搜索并不断启动/停止玩家甚至可以消除它,而不必担心这一点。

自从改用这种方法后,我们就没有再收到 runloop 投诉。

编辑:一些示例代码...

import SpriteKit
import AVFoundation

enum SoundEffect: String, CaseIterable {
  case playerExplosion = "player_explosion"
  // lots more

  var url: URL {
    guard let url = Bundle.main.url(forResource: self.rawValue, withExtension: "wav") else {
      fatalError("Sound effect file \(self.rawValue) missing")
    }
    return url
  }

  func audioBuffer() -> AVAudioPCMBuffer {
    guard let file = try? AVAudioFile(forReading: self.url) else {
      fatalError("Unable to instantiate AVAudioFile")
    }
    guard let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) else {
      fatalError("Unable to instantiate AVAudioPCMBuffer")
    }
    do {
      try file.read(into: buffer)
    } catch {
      fatalError("Unable to read audio file into buffer, \(error.localizedDescription)")
    }
    return buffer
  }
}

class Sounds {
  var audioBuffers = [SoundEffect: AVAudioPCMBuffer]()
  // more stuff

  init() {
    for effect in SoundEffect.allCases {
      preload(effect)
    }
  }

  func preload(_ sound: SoundEffect) {
    audioBuffers[sound] = sound.audioBuffer()
  }

  func cachedAudioBuffer(_ sound: SoundEffect) -> AVAudioPCMBuffer {
    guard let buffer = audioBuffers[sound] else {
      fatalError("Audio buffer for \(sound.rawValue) was not preloaded")
    }
    return buffer
  }
}

class Globals {
  // Sounds loaded once and shared amount all scenes in the game
  static let sounds = Sounds()
}

class SceneAudio {
  let stereoEffectsFrame: CGRect
  let audioEngine: AVAudioEngine
  var playerNodes = [AVAudioPlayerNode]()
  var nextPlayerNode = 0
  // more stuff

  init(stereoEffectsFrame: CGRect, audioEngine: AVAudioEngine) {
    self.stereoEffectsFrame = stereoEffectsFrame
    self.audioEngine = audioEngine
    do {
      try audioEngine.start()
      let buffer = Globals.sounds.cachedAudioBuffer(.playerExplosion)
      // We got up to about 10 simultaneous sounds when really pushing the game
      for _ in 0 ..< 10 {
        let playerNode = AVAudioPlayerNode()
        playerNodes.append(playerNode)
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: buffer.format)
        playerNode.play()
      }
    } catch {
      logging("Cannot start audio engine, \(error.localizedDescription)")
    }
  }

  func soundEffect(_ sound: SoundEffect, at position: CGPoint = .zero) {
    guard audioEngine.isRunning else { return }
    let buffer = Globals.sounds.cachedAudioBuffer(sound)
    let playerNode = playerNodes[nextPlayerNode]
    nextPlayerNode = (nextPlayerNode + 1) % playerNodes.count
    playerNode.pan = stereoBalance(position)
    playerNode.scheduleBuffer(buffer)
  }

  func stereoBalance(_ position: CGPoint) -> Float {
    guard stereoEffectsFrame.width != 0 else { return 0 }
    guard position.x <= stereoEffectsFrame.maxX else { return 1 }
    guard position.x >= stereoEffectsFrame.minX else { return -1 }
    return Float((position.x - stereoEffectsFrame.midX) / (0.5 * stereoEffectsFrame.width))
  }
}

class GameScene: SKScene {
  var audio: SceneAudio!
  // lots more stuff

  // somewhere in initialization
  // gameFrame is the area where action takes place and which
  // determines panning for stereo sound effects
  audio = SceneAudio(stereoEffectsFrame: gameFrame, audioEngine: audioEngine)

  func destroyPlayer(_ player: SKSpriteNode) {
    audio.soundEffect(.playerExplosion, at: player.position)
    // more stuff
  }
}

关于ios - 使用 AVAudioPlayer 时出现 "__CFRunLoopModeFindSourceForMachPort returned NULL"消息,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58488660/

相关文章:

ios - 如何让我的视频背景继续使用 swift 播放?

ios - 无法将 UITableViewCell 类型的值转换为 "ActuTblCell"

ios - 如何为 NSCameraUsageDescription 显示两个不同的应用程序使用消息

ios - Spritekit SkNodes 相交的矩形/多边形

swift - didBeginContact 没有被称为 Swift

swift - 在 Xcode 9.2 中播放声音

ios - 使用 xcode AVFoundation 扫描 EAN13 条码时发生奇怪

ios - Swift iOS 搜索栏加载自定义 View

iOS 后退手势关闭键盘

ios6 - 是否可以为 iOS 6 编译 Sprite Kit 应用程序/游戏?