🕌

(iOS)AVAudioPlayerNodeで音声が再生されない場合の対処法

2021/06/14に公開
1

はじめに

以下の環境で動作検証しました。

  • Xcode 12.5 (12E262)
  • iOS 14.5.1

結論

AVAudioPlayerNodeの出力先が設定されていない状態でscheduleFileを呼び出すとアプリがクラッシュします。以降はPlayボタンを押すと音声が再生されるアプリを例に説明します。

間違った実装

import AVFoundation
import SwiftUI

struct ContentView: View {
    let engine = AVAudioEngine()
    let playerNode = AVAudioPlayerNode()

    var body: some View {
        Button("Play") {
            self.playerNode.play()
        }
    }
    init() {
        guard let url = Bundle.main.url(forResource: "Music", withExtension: "wav") else {
            fatalError("failed to build file path")
        }
        guard let music = try? AVAudioFile(forReading: url) else {
            fatalError("failed to read file")
        }

        self.playerNode.scheduleFile(music, at: nil, completionHandler: nil)

        let outputFormat = engine.outputNode.inputFormat(forBus: 0)

        self.engine.attach(self.playerNode)
        self.engine.connect(self.playerNode, to: self.engine.mainMixerNode, format: outputFormat)

        do {
            try self.engine.start()
        } catch {
            fatalError("failed to start audio engine")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

上記の実装ではAVAudioEngineのconnect()メソッドを呼び出す前にscheduleFile()を呼び出しています。実行するとデバッグコンソールに「required condition is false ...」というメッセージが表示されてアプリがクラッシュします。

アプリのクラッシュと同時にLLDBのプロンプトが起動するので、btと入力してバックトレースを表示してみてください。scheduleFile()がクラッシュの原因であると確認できます。

正しい実装

import AVFoundation
import SwiftUI

struct ContentView: View {
    let engine = AVAudioEngine()
    let playerNode = AVAudioPlayerNode()

    var body: some View {
        Button("Play") {
            self.playerNode.play()
        }
    }
    init() {
        guard let url = Bundle.main.url(forResource: "Music", withExtension: "wav") else {
            fatalError("failed to build file path")
        }
        guard let music = try? AVAudioFile(forReading: url) else {
            fatalError("failed to read file")
        }

        let outputFormat = engine.outputNode.inputFormat(forBus: 0)

        self.engine.attach(self.playerNode)
        self.engine.connect(self.playerNode, to: self.engine.mainMixerNode, format: outputFormat)

        self.playerNode.scheduleFile(music, at: nil, completionHandler: nil)

        do {
            try self.engine.start()
        } catch {
            fatalError("failed to start audio engine")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

上記が正しい実装です。この実装であればアプリはクラッシュしません。たかが1行ですが、呼び出すタイミングには注意してください。

バグなのか仕様なのか

断定口調で記事を書きましたが、この挙動はバグなのか仕様なのかわかりません。Apple Developerのドキュメントには呼び出しタイミングについて一歳記述がありません。なにかご存知の方はコメントいただけると助かります。

参考資料

Discussion

ふくららふくらら

正しいコードですが、ipad os17.5 swift playgroundで試したところ、一度目は再生されますが、二度目からボタンを押しても再生されず、クラッシュします。何が原因かわかりません