Сообщения «__CFRunLoopModeFindSourceForMachPort вернули NULL» при использовании AVAudioPlayer - PullRequest
0 голосов
/ 21 октября 2019

Мы работаем над игрой SpriteKit. Чтобы получить больше контроля над звуковыми эффектами, мы перешли от использования SKAudioNodes к использованию некоторых AVAudioPlayers. Хотя кажется, что все работает хорошо с точки зрения игрового процесса, частоты кадров и звуков, мы видим случайные сообщения об ошибках (?) В выводе консоли при тестировании на физических устройствах:

... [general] __CFRunLoopModeFindSourceForMachPort возвратил NULL для режима '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 раз.

Похоже, что сообщение возникает чаще всего, когда в звуковом эффекте играютбыстрая последовательность, но не похоже, что это на 100% коррелирует с этим.

Не использование очереди отправки (т. е. наличие звука.execute только для прямого вызова soundActions ()) не решает проблему (хотячто приводит к значительному отставанию игры). Изменение очереди отправки на некоторые другие приоритеты, такие как .utility, также не влияет на проблему.

Звуковое выполнение .execute просто немедленно возвращается (т. Е. Вообще не вызывает закрытие, поэтому нетplay ()) удаляет сообщения.

Мы нашли исходный код, который создает сообщение по этой ссылке:

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

, но мы не понимаемкроме как на абстрактном уровне, и не уверены, как циклы выполнения участвуют в AVFoundation.

Многие поиски в Google ничего не дали. И, как я указал, это, кажется, не вызывает заметных проблем вообще. Хотелось бы знать, почему это происходит, и как это исправить, или быть уверенным, что это никогда не станет проблемой.

1 Ответ

0 голосов
/ 22 октября 2019

Мы все еще работаем над этим, но достаточно экспериментировали, чтобы было ясно, как мы должны действовать. Структура:

Используйте свойство сцены audioEngine.

Для каждого звукового эффекта создайте AVAudioFile для чтения URL-адреса аудио из комплекта. Прочитайте это в AVAudioPCMBuffer. Вставьте буферы в словарь, индексированный по звуковому эффекту.

Создайте несколько AVAudioPlayerNodes. Присоедините () их к аудио-двигателю. Подключиться (playerNode, to: audioEngine.mainMixerNode). На данный момент мы создаем их динамически, просматривая наш текущий список узлов игроков, чтобы найти тот, который не играет, и создаем новый, если его нет. Это, вероятно, потребовало больше накладных расходов, чем необходимо, поскольку мы должны иметь обратные вызовы, чтобы наблюдать, когда узел игрока завершает все, что он играет, и возвращать его в состояние останова. Мы попытаемся переключиться на фиксированное максимальное количество активных звуковых эффектов и вращаться между игроками по порядку.

Чтобы воспроизвести звуковой эффект, возьмите буфер для эффекта, найдите незанятый playerNode исделать playerNode.scheduleBuffer (буфер, ...). И 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
  }
}
...