使用AVAudioPlayer

时间:2019-10-21 14:39:10

标签: ios sprite-kit avfoundation avaudioplayer mach

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

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

它似乎并没有真正造成任何伤害(没有声音故障或帧频出现打or或其他任何现象),但无法确切了解该消息的含义以及它为什么发生的原因使我们感到紧张。

详细信息:

游戏是所有标准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%。

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

发出声音。执行只是立即返回(即实际上根本不调用闭包,因此没有play())确实消除了消息。

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

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

但是我们只在抽象的层次上不了解它,并且不确定在AVFoundation中如何涉及运行循环。

很多Google搜索都没有帮助。正如我指出的那样,这似乎根本没有引起任何明显的问题。很高兴知道它为什么会发生,以及如何解决它或确定它永远不会成为问题。

1 个答案:

答案 0 :(得分:0)

我们仍在努力,但是已经进行了足够的实验,以至于清楚我们应该如何做。大纲:

使用场景的audioEngine属性。

对于每种音效,制作一个AVAudioFile,以从包中读取音频的URL。将其读入AVAudioPCMBuffer。将缓冲区插入按声音效果索引的字典中。

制作一堆AVAudioPlayerNodes。将它们附加到AudioEngine。连接(playerNode,至: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
  }
}