在使用AVAudioPlayer时,出现了“__CFRunLoopModeFindSourceForMachPort returned NULL”消息。

5
我们正在开发一个SpriteKit游戏。为了更好地控制音效,我们从使用SKAudioNodes切换到使用一些AVAudioPlayers。虽然在游戏玩法、帧率和音效方面一切似乎都运行良好,但在物理设备上测试时,有时会在控制台输出中看到偶尔的错误(?)消息:
... [general] __CFRunLoopModeFindSourceForMachPort returned NULL for mode 'kCFRunLoopDefaultMode' livePort: #####
当它发生时,似乎并没有真正造成任何损害(没有声音故障或帧速率问题等),但不理解这个消息的确切含义以及为什么会发生,让我们感到紧张。
细节如下:
游戏全部采用标准的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,因此我们不会尝试在已经播放的内容上调用play。
这个消息并不经常出现。在可能有数千个声音效果的5-10分钟的游戏过程中,我们可能会看到这个消息5-10次。
这个消息似乎最常出现在一堆声音效果被快速连续播放时,但感觉它并不完全与此相关。
不使用dispatch队列(即让sounds.execute直接调用soundActions())无法解决问题(尽管这会导致游戏明显卡顿)。将dispatch队列更改为其他优先级,如.utility,也不会影响问题。
使sounds.execute立即返回(即根本不调用闭包,因此没有play())确实消除了消息。
我们在此链接中找到了产生消息的源代码。

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

但我们只在抽象层面上理解它,不确定如何运行循环与AVFoundation相关。大量搜索没有找到任何有用的信息。正如我所说,这似乎并没有造成明显的问题。知道为什么会发生这种情况,要么知道如何修复它,要么确信它永远不会成为问题,这将是很好的。

为什么要切换呢?AvAudioPlayer已经内置在SKAudioNode中,您可以从AVAudioNode中访问所需内容。无论如何,看起来有人正在尝试从音频端口获取数据,但在抓取时该端口不可用。也许是因为您正在后台线程上执行此操作。 - Knight0fDragon
我们对所有AV...类之间的关系理解不是很全面。SKAudioNode方法对于简单的声音效果可以正常工作,但当我们想要根据游戏中发生的事情添加一些立体声效果时,SpriteKit就无法配合了。要么我们禁用位置信息,立体声就无法工作,要么我们启用位置信息,然后就会出现淡入淡出和其他不需要的效果。如果有一种方法可以从SKAudioNode的avAudioNode中获取底层的AVAudioPlayer,我们可以尝试一下。 - bg2b
我尝试添加了一个SKAudioNode,抓取avAudioNode!as!AVAudioPlayerNode,设置pan,并使用SKAction.play()运行节点。它确实播放声音,但我无法使其移动。 (与我们之前尝试过的SKAction.stereoPan操作相同。)也许我会尝试自己制作一些AVAudioPlayerNodes(而不是制作AVAudioPlayers),并将它们连接到场景的audioEngine,以便我更了解发生了什么。 - bg2b
这是我做这个已经有一段时间了,但在场景移动到视图之后才能创建SKAudioNodes。你这样做了吗?有一种方法可以确定音频节点是否存在。 - Knight0fDragon
就此问题而言,仅仅关闭场景的音频引擎并不能解决问题。我会尝试与引擎一起工作,但是我需要学习很多东西并重新制定一些计划,所以这需要一些时间。感谢您的评论;它们帮助我更全面地了解了音频相关的AV...如何协同工作,因此至少我有一些想法可以继续进行。 - bg2b
显示剩余3条评论
1个回答

3
我们仍在进行改进,但已经尝试了足够多的实验,清楚我们应该如何处理。概述:
使用场景的audioEngine属性。
对于每个音效,从捆绑包中创建一个AVAudioFile,读取音频的URL。将其读入AVAudioPCMBuffer。将缓冲器放入以声音效果为索引的字典中。
制作一堆AVAudioPlayerNodes。将它们附加到audioEngine上。连接(playerNode, to:audioEngine.mainMixerNode)。现在我们正在动态地创建这些节点,搜索我们当前的播放器节点列表,找到一个没有播放的,并且如果没有可用的,则新建一个。那可能比需要更多的开销,因为我们必须有回调来观察播放器节点完成播放并将其设置回停止状态时。我们将尝试切换到只使用固定数量的活动音效,并按顺序轮流播放播放器。
要播放音效,请获取效果的缓冲器,找到一个非忙碌的playerNode,然后执行playerNode.scheduleBuffer(buffer,...)。如果目前未播放,则执行playerNode.play()。
完全转换和整理之后,我可能会更新一些更详细的代码。我们仍然有几个长时间运行的AVAudioPlayers,我们还没有切换到使用通过混合器的AVAudioPlayerNode。但是,通过以上方案传输绝大部分音效已经消除了错误消息,而且需要的内存更少,因为不再像以前那样在内存中重复声音效果。存在一点延迟,但我们甚至还没有尝试将一些内容放到后台线程上,而且也许不必搜索和不断启动/停止播放器可能会消除它,而不必担心这个问题。
自从切换到这种方法以来,我们再没有运行循环的问题了。
编辑:一些示例代码...
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
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接