簡體   English   中英

應用程序被 iOS 電話打斷后,音頻將無法播放

[英]Audio won't play after app interrupted by phone call iOS

我在我的 SpriteKit 游戲中遇到一個問題,在應用程序被電話中斷后,使用 playSoundFileNamed(_ soundFile:, waitForCompletion:) 的音頻將無法播放。 (我還在我的應用程序中使用了 SKAudioNodes,它不受影響,但我真的真的很想也能使用 SKAction playSoundFileNamed。)

這是來自一個精簡的 SpriteKit 游戲模板的 gameScene.swift 文件,它重現了這個問題。 您只需要在項目中添加一個音頻文件並將其命名為“note”

我已將應駐留在 appDelegate 中的代碼附加到切換開/關按鈕以模擬電話呼叫中斷。 該代碼 1) 停止 AudioEngine 然后停用 AVAudioSession -(通常在 applicationWillResignActive 中)......和 ​​2)激活 AVAudioSession 然后啟動 AudioEngine -(通常在 applicationDidBecomeActive 中)

錯誤:

AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]: 停用正在運行 I/O 的音頻會話。 在停用音頻會話之前,應停止或暫停所有 I/O。

嘗試停用音頻會話時會發生這種情況,但僅在聲音播放至少一次后才會發生。 重現:

1) 運行應用程序 2) 關閉和打開引擎幾次。 不會發生錯誤。 3) 點擊 playSoundFileNamed 按鈕 1 次或多次以播放聲音。 4) 等待聲音停止 5) 再等一段時間以確保

6) 點擊切換音頻引擎按鈕停止音頻引擎並停用會話 - 發生錯誤。

7) 打開和關閉引擎幾次以查看在調試區域中打印的會話已激活、會話已停用、會話已激活 - 即沒有報告錯誤。 8) 現在會話處於活動狀態且引擎正在運行,playSoundFileNamed 按鈕將不再播放聲音。

我究竟做錯了什么?

import SpriteKit
import AVFoundation

class GameScene: SKScene {
var toggleAudioButton: SKLabelNode?
var playSoundFileButton: SKLabelNode?
var engineIsRunning = true

override func didMove(to view: SKView) {
toggleAudioButton = SKLabelNode(text: "toggle Audio Engine")
toggleAudioButton?.position = CGPoint(x:20, y:100)
toggleAudioButton?.name = "toggleAudioEngine"
toggleAudioButton?.fontSize = 80
addChild(toggleAudioButton!)

playSoundFileButton = SKLabelNode(text: "playSoundFileNamed")
playSoundFileButton?.position = CGPoint(x: (toggleAudioButton?.frame.midX)!, y:    (toggleAudioButton?.frame.midY)!-240)
playSoundFileButton?.name = "playSoundFileNamed"
playSoundFileButton?.fontSize = 80
addChild(playSoundFileButton!)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
  let  location = touch.location(in: self)
  let  nodes = self.nodes(at: location)

  for spriteNode in nodes {
    if spriteNode.name == "toggleAudioEngine" {
      if engineIsRunning { // 1 stop engine, 2 deactivate session

        scene?.audioEngine.stop() // 1
        toggleAudioButton!.text = "engine is paused"
        engineIsRunning = !engineIsRunning
        do{
          // this is the line that fails when hit anytime after the playSoundFileButton has played a sound
          try AVAudioSession.sharedInstance().setActive(false) // 2
          print("session deactivated")
        }
        catch{
          print("DEACTIVATE SESSION FAILED")
        }
      }
      else { // 1 activate session/ 2 start engine
        do{
          try AVAudioSession.sharedInstance().setActive(true) // 1
          print("session activated")
        }
        catch{
          print("couldn't setActive = true")
        }
        do {
          try scene?.audioEngine.start() // 2
          toggleAudioButton!.text = "engine is running"
          engineIsRunning = !engineIsRunning
        }
        catch {
          //
        }
      }
    }

    if spriteNode.name == "playSoundFileNamed" {
      self.run(SKAction.playSoundFileNamed("note", waitForCompletion: false))
    }
   }
  }
 }
}

讓我在這里為您節省一些時間:playSoundFileNamed 在理論上聽起來很棒,如此美妙以至於您可能會說在您花了 4 年時間開發的應用程序中使用它,直到有一天您意識到它不僅會因中斷而完全中斷,而且甚至會導致您的應用程序崩潰最關鍵的中斷,你的 IAP。 不要這樣做。 我仍然不完全確定 SKAudioNode 還是 AVPlayer 是答案,但這可能取決於您的用例。 只是不要這樣做。

如果您需要科學證據,請創建一個應用程序並創建一個 for 循環,在 touchesBegan 中播放您想要的任何內容,然后查看您的內存使用情況。 該方法是一個泄漏的垃圾。

為我們的最終解決方案編輯:

我們發現使用 prepareToPlay() 在內存中預加載適當數量的 AVAudioPlayer 實例是最好的方法。 SwiftySound 音頻類使用動態生成器,但動態創建 AVAudioPlayers 會導致動畫變慢。 我們發現有最大數量的 AVAudioPlayers 並檢查數組中 isPlaying == false 是最簡單和最好的; 如果一個不可用,你就不會聽到聲音,類似於你在 PSFN 中看到的,如果你讓它在彼此之上播放很多聲音。 總的來說,我們還沒有找到理想的解決方案,但這對我們來說已經很接近了。

為了回應 Mike Pandolfini 不使用 playSoundFileNamed 的建議,我已將代碼轉換為僅使用 SKAudioNodes。 (並將錯誤報告發送給蘋果)。 然后我發現其中一些 SKAudioNodes 在應用程序中斷后也無法播放……我偶然發現了一個修復程序。 當應用退出或從后台返回時,您需要告訴每個 SKAudioNode stop() - 即使它們沒有在播放。

(我現在沒有在我的第一篇文章中使用任何停止音頻引擎並停用會話的代碼)

然后問題就變成了如何在可能自己播放的地方快速播放相同的聲音。 這就是 playSoundFileNamed 的優點所在。

1) SKAudioNode 修復:

預加載您的 SKAudioNodes 即

let sound = SKAudioNode(fileNamed: "super-20")

在 didMoveToView 中添加它們

sound.autoplayLooped = false
addChild(sound)

添加 willResignActive 通知

notificationCenter.addObserver(self, selector:#selector(willResignActive), name:UIApplication.willResignActiveNotification, object: nil)

然后創建停止所有音頻節點播放的選擇器函數:

@objc func willResignActive() {
  for node in self.children {
    if NSStringFromClass(type(of: node)) == “SKAudioNode" {
      node.run(SKAction.stop())
    }
  }
}

在應用程序中斷后,所有 SKAudioNodes 現在都可以可靠地播放。

2) 要復制 playSoundFileNamed 播放短的快速重復聲音或可能需要播放多次並因此可能重疊的較長聲音的能力,請為每個聲音創建/預加載 1 個以上的屬性並像這樣使用它們:

let sound1 = SKAudioNode(fileNamed: "super-20")
let sound2 = SKAudioNode(fileNamed: "super-20")
let sound3 = SKAudioNode(fileNamed: "super-20")
let sound4 = SKAudioNode(fileNamed: "super-20")

var soundArray: [SKAudioNode] = []
var soundCounter: Int = 0

在 didMoveToView 中

soundArray = [sound1, sound2, sound3, sound4]
for sound in soundArray {
  sound.autoplayLooped = false
  addChild(sound)
}

創建播放功能

func playFastSound(from array:[SKAudioNode], with counter:inout Int) {
counter += 1
if counter > array.count-1 {
  counter = 0
}
  array[counter].run(SKAction.play())
}

要播放聲音,請將特定聲音的數組及其對播放函數的計數器傳遞給播放函數。

playFastSound(from: soundArray, with: &soundCounter)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM