AVSpeechSynthesizerをバックグラウンド再生ありで使う

(2019/08/09更新。バックグラウンド移行時に音が止まっているならAudioSessionを無効化する方法に書き換えました。)

以前[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応 – nackpan Blogという記事を書きました。

そこでのkkさんのコメントをきっかけにAVSpeechSynthesizerの使い方について調べました。

AVSpeechSynthesizerを用いてテキストを読み上げるアプリ。フォアグラウンドで再生している場合は問題ありません。
バックグラウンド再生を絡めた場合にトラブル。
AVSpeechSynthesizerでspeak中にpauseしたあとアプリを中断、再びアプリを開くと勝手に音が鳴るなど、アプリ再開時の挙動が妙でした。

ログを見るとアプリ再開時にinterruptionの通知が届いていたので、interruptionNotificationについて調査。

InterruptionNotificationについて

Interruptionの通知は、電話がかかってきた時などでAudioSessionに割り込みが発生した時に届くものと考えていました。
今回、アプリの再開時に、interruptionの通知が届きました。

Appleのドキュメントを見直しました。

interruptionNotification – AVAudioSession | Apple Developer Documentation

Noteとして以下の記述があります。

Starting in iOS 10, the system will deactivate the audio session of most apps in response to the app process being suspended. When the app starts running again, it will receive an interruption notification that its audio session has been deactivated by the system. This notification is necessarily delayed in time because it can only be delivered once the app is running again. If your app’s audio session was suspended for this reason, the userInfo dictionary will contain the AVAudioSessionInterruptionWasSuspendedKey key with a value of true.

If your audio session is configured to be non-mixable (the default behavior for the playback, playAndRecord, soloAmbient, and multiRoute categories), it’s recommended that you deactivate your audio session if you’re not actively using audio when you go into the background. Doing so will avoid having your audio session deactivated by the system (and receiving this somewhat confusing notification).

interruptionNotification – AVAudioSession | Apple Developer Documentation

 * iOS 10以降では、システムは、アプリプロセスが中断されると、ほとんどのアプリの AudioSessionを無効にする。
*  アプリの再開時に、AudioSessionがシステムによって無効にされたことを示す割り込み(interruption)通知を受け取る。
* この通知でAVAudioSessionInterruptionWasSuspendedKeyの値はtrue
* AudioSessionがミックス不可(playback、playAndRecord、soloAmbient、multiRouteのデフォルトの動作)設定なら、バックグラウンド移行時に、アクティブに使用していないAudioSessionを無効にすることをお勧め
 * そうすることで、この通知を受け取らなくて済む

とあるように、システムによってAudioSessionが無効にされたことを示すinterruptionがアプリの再開時に届く、とのことでした。

InterruptionNotificationの実験

バックグラウンド再生ありのAVSpeechSynthesizerを使ったアプリで、interruptionの通知について実験しました。
当初、実験した際(07/29)には、iOS 12.4で
* AVSpeechSynthesizerでspeak中に、pause。電源ボタンを押してアプリを中断。すると音声が勝手に鳴ってしまう。電源ボタンを押した直後にinterruptionの通知が届きました。(willResignActiveの前に発生)
という症状が発生しました。
しかし、その後あらためて実験すると、まったくこの症状は出ず、interruptionNotification – AVAudioSession のドキュメントにあるように再開後にinterruptionの通知が届きました。

対処法

アプリ再開時に音が勝手になるなど挙動が変になる問題は、システムによるAudioSessionの無効化が関係しているのだろう。AVSpeechSynthesizerでは再開時に上手いことやってくれない。Noteにあるように、明示的にAudioSessionを無効化すれば、通知を受け取らずに済むし挙動も適切なものになると考えました。

interruptionNotificationのNoteでは、アプリのAudioSessionが無効にされたことを示す通知がアプリ再開後に届くとあり、それを避けるには、バックグラウンド移行時にAudioSessionを無効化するとよい、とありました。


当初の実験(07/29)では、iOS 12.4での電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、バックグラウンド移行時にAudioSessionを無効化する処理を書いても、音声が短いながらも勝手に鳴ってしまう症状が出ました。そのため、AVSpeechSynthesizerでの音声再生が終了したら、その段階でAudioSessionを無効化にするということで対処することにしていました。これにはAVSpeechSynthesizerDelegateを使いAVSpeechSynthesizerのpause, stop, finishの完了を知り、そこでAudioSessionを無効化にしていました。

しかし、その後の実験では「電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、音声が短いながらも勝手に鳴ってしまう」症状は再現しませんでした。


そこで、interruptionNotificationのNoteが勧める方法に従いました。
バックグラウンド移行時に音が止まっている場合、AudioSessionを無効化。
speakのさいにAudioSessionを有効化しました。

バックグラウンド移行を検知する方法はこちらの記事が参考になります。
NotificationCenterを用いたライフサイクルイベントの検知

実装

今回はViewController.swiftにすべて記述することとしました。
メッセージラベル、Speakボタン、Pauseボタン、Stopボタンを貼り付けます。

アプリ画面

メッセージラベルとViewControllerをOutletで結びます。
@IBOutlet weak var messageLabel: UILabel!
SpeakボタンとViewControllerをActionで結びます。
@IBAction func speakBtnTapped(_ sender: Any) 
PauseボタンとViewControllerをActionで結びます。
@IBAction func pauseBtnTapped(_ sender: Any)
StopボタンとViewControllerをActionで結びます。
@IBAction func stopBtnTapped(_ sender: Any)

バックグラウンド再生用。
Capabilities > Background Modes をON > Audio, AirPlay, and Picture in Pictureにチェック

ViewController.swift

import UIKit
import AVFoundation
import AVKit
import MediaPlayer

class ViewController: UIViewController, AVSpeechSynthesizerDelegate {

    @IBOutlet weak var messageLabel: UILabel!
    var syntherizer = AVSpeechSynthesizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // AudioSessionカテゴリをbackground再生ができるものに設定
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback)
        } catch {
            print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }

        addAudioSessionObservers()
        addRemoteCommandEvent()
        addLifeCycleObserver()
    }
    
    // 電話による割り込みと、オーディオルートの変化を監視します
    func addAudioSessionObservers() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        center.addObserver(self, selector: #selector(audioSessionRouteChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
    }
    
    /// Interruption : 電話などによる割り込み
    @objc func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
            let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                return
        }
        
        if type == .began {
            // interruptionが開始した時(電話がかかってきたなど)
            if let wasSuspendedKeyValue = userInfo[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber {
                let wasSuspendedKey = wasSuspendedKeyValue.boolValue
                if wasSuspendedKey {
                    // suspeended key : true
                } else {
                    // suspended key : false
                }
            } else {
                // suspended key : nil
            }
        }
        else if type == .ended {
            // interruptionが終了した時の処理
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                } else {
                    // Interruption Ended - playback should NOT resume
                }
            }
        }
    }
    
    /// Audio Session Route Change : ルートが変化した(ヘッドフォンが抜き差しされた)
    @objc func audioSessionRouteChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
            let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
                return
        }
        
        DispatchQueue.main.async {
            self.messageLabel.text = self.routeChangeReasonDescription(reason: reason)
        }

        switch reason {
        case .newDeviceAvailable:
            let session = AVAudioSession.sharedInstance()
            for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                // ヘッドフォンがつながった
                
                break
            }
        case .oldDeviceUnavailable:
            if let previousRoute =
                userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
                for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                    // ヘッドフォンが外れた
                    
                    // 音声をpauseしています
                    pause()
                    break
                }
            }
        default: ()
        }
        
    }
    
    public func speak() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(true)
        } catch {
            
        }
        let voice = AVSpeechSynthesisVoice(language: "ja-JP")
        let utterance = AVSpeechUtterance(string: "おはようございます。今日もいい天気ですね。音声合成で読み上げています。文章をうまくよめていますか?")
        utterance.voice = voice
        utterance.preUtteranceDelay = 5
        
        if syntherizer.isPaused {
            syntherizer.continueSpeaking()
        } else {
            syntherizer.speak(utterance)
        }
    }
    
    public func pause() {
        syntherizer.pauseSpeaking(at: .immediate)
    }
    
    /// Speakボタンが押された
    @IBAction func speakBtnTapped(_ sender: Any) {
        // 音声出力を行う
        speak()
    }
    
    /// pauseボタンが押された
    @IBAction func pauseBtnTapped(_ sender: Any) {
        pause()
    }
    
    @IBAction func stopBtnTapped(_ sender: Any) {
        syntherizer.stopSpeaking(at: .immediate)
    }
    
    /// 監視する必要がなくなった段階で、Observerを取り外します
    private func removeAudioSessionObservers() {
        let center = NotificationCenter.default
        
        // AVAudio Session
        center.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        center.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
        
    }
    
    /// route変更の理由表示用
    private func routeChangeReasonDescription(reason: AVAudioSession.RouteChangeReason) -> String {
        switch reason {
        case .unknown:
            return "unknown"
        case .newDeviceAvailable:
            return "newDeviceAvailable"
        case .oldDeviceUnavailable:
            return "oldDeviceUnailable"
        case .categoryChange:
            return "categoryChange"
        case .override:
            return "override"
        case .wakeFromSleep:
            return "wakeFromSleep"
        case .noSuitableRouteForCategory:
            return "noSuitableRouteForCategory"
        case .routeConfigurationChange:
            return "routeConfigurationChange"
        default:
            return "default"
        }
    }
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
        commandCenter.playCommand.addTarget(self, action: #selector(type(of: self).remotePlay(_:)))
        commandCenter.pauseCommand.addTarget(self, action: #selector(type(of: self).remotePause(_:)))
        commandCenter.nextTrackCommand.addTarget(self, action: #selector(type(of: self).remoteNextTrack(_:)))
        commandCenter.previousTrackCommand.addTarget(self, action: #selector(type(of: self).remotePrevTrack(_:)))
    }
    
    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        // 略

    }
    
    @objc func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        speak()

    }
    
    @objc func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        pause()
        
    }
    
    @objc func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    @objc func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }
    
    // MARK: - Life Cycle
    func addLifeCycleObserver() {
        let center = NotificationCenter.default
        // 今回はbackground移行を検知
        center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func didEnterBackground(_ notification: Notification) {
        // 音が止まっていたらAudioSessionを無効化
        if !syntherizer.isSpeaking || syntherizer.isPaused {
            deactivateAudioSession()
        }

        let app = UIApplication.shared
        app.beginBackgroundTask(expirationHandler: {
            // このblockはバックグラウンド処理に入って、所定時間後(180秒程度後)に実行される
            // (バックグラウンド再生が継続している間は実行されない)
            // ここでAudioSessionを無効化しておく
            // (この処理がないとバックグラウンド再生中にpauseしたあと、3分すぎにアプリを再開すると
            // いきなり音が鳴る症状が出てしまう)
            self.deactivateAudioSession()
            
        })
    }
    
    func deactivateAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            
        }
    }
}

投稿者:

nackpan

nackpan

iOSアプリを作っています。 リピーティングに便利な「語学学習支援プレイヤー」つくりました。

「AVSpeechSynthesizerをバックグラウンド再生ありで使う」への5件のフィードバック

  1. nackpan様

    kkです。

    新しい記事、調査、ありがとうございます。

    interruptionNotificationに関するnote、大変勉強になります。
    また、appleへのアクセスの重要さを真に実感させて頂き、感謝
    いたします。今後、継続して勉強いたします。

    私の方での実機確認結果ですが、
    新しい記事で頂いた内容(記事のコード)をそのまま、コピーし
    下記の1行のみ追加して、以下の手順で実験を行ったところ、
    utterance.preUtteranceDelay = 5

    手順:
    ①Speakボタンを2〜3回(1回以上)押す。
    ②1回目の発声が終わって、待ち時間に入った時点でpauseを押す。
    ③ホームボタンを押す。
    ④アイコンを押して、アプリを立ち上げる。
    ⑤発声が、再び開始される。

    ④のアプリ立ち上げの時に、『電話の割り込み』が働いているようです。
    utterance.preUtteranceDelayを”=0”にすると、上記現象は起きません。

    素人なりに色々と検討してみましたが、現象変わらずでしたので、
    コメント投稿をさせて頂きます。

    何か間違っているとこ、不十分なところがあれば、ご指摘頂きたく、お手数をおかけして大変申し訳ありませんが、どうかよろしくお願い致します。

  2. kkさん、こんにちは。
    コメントありがとうございます。

    まず、ごめんなさい。
    元記事(07/29)に記した方法でなくとも対応できることがわかりました。

    元記事(07/29)で、AudioSessionの無効化を行わずにバックグラウンドに移行したさいのinterruptionについて検証していました。
    そこで、
    “`
    IOS 12.4
    * AVSpeechSynthesizerでspeak中に、pause。電源ボタンを押してアプリを中断。すると音声が勝手に鳴ってしまう。電源ボタンを押した直後にinterruptionの通知が届きました。(willResignActiveの前に発生)
    “`
    と記しました。
    今回あらためて検証するとまったく再現しませんでした。
    (デバイスを再起動するとトラブルが解消するというケースがありましたが、今回もそのたぐいでしょうか…)
    AppleのinterruptionNotificationのドキュメントにあるように、再開後にinterruptionの通知が届きました。

    元記事(07/29)では、「電源ボタンを押した直後、バックグラウンドへ移行する前にinterruptionの通知が届き、音声が鳴ってしまう」という症状がでていたので、バックグラウンド移行時にAudioSessionを無効化するという対処法ではなく、音声が止まった段階(pause, stop, finish)でAudioSessionを明示的に無効化するという方法を使っていました。

    今回、その症状は発生しないとなったので、AppleのinterruptionNotificationのドキュメントが勧めている「バックグラウンド移行時に、音が止まっているならAudioSessionを明示的に無効化する」という方法に書き換えました。

    それに合わせて、記事も更新しました。

    元記事(07/29)の「音声が止まった段階(pause, stop, finish)でAudioSessionを明示的に無効化する」という方法では、preUtteranceDelayを使用した際にうまくいかない問題について。

    検証したところ、preUtteranceDelayでの待ち時間にpauseした場合は、delegateメソッドspeechSynthesizer(_:didPause:)
    が呼び出されませんでした。(音声再生中にpauseした場合は、呼び出されます)
    そのため、待ち時間にpauseした場合は、speechSynthesizer(_:didPause:)内で行なっているAudioSessionの無効化が行われません。
    これにより、バックグランド移行からのアプリ再開で音声が鳴ってしまう症状が出ています。

    対処法としては、「バックグラウンド移行時に、音が止まっているならAudioSessionを明示的に無効化する」処理を追加することになります。
    これで、カバーできるので、「音声が止まった段階(pause, stop, finish)でAudioSessionを明示的に無効化する」処理を用いる必要はなくなります。

    AVSpeechSynthesizerは癖が強いと申しますかなんというか…
    AVAudioPlayerでは再開時に思わぬ音が鳴るなどの症状は出なかったので、参りますね…

  3. nackpan様
    kkと申します。
    以前、2019/08/09更新の記事で、大変お世話になり、感謝しております。ご説明頂いた、下記の件については、私の方でも、同じように再現しなくなってしまい、以降は、ご教示頂いた内容で、不具合なく動作できております。ありがとうございました。
    ===2019/08/09の記事:ここから===
    “AVSpeechSynthesizerでspeak中に、pause。電源ボタンを押してアプリを中断。すると音声が勝手に鳴ってしまう。電源ボタンを押した直後にinterruptionの通知が届きました。(willResignActiveの前に発生)”と記しました。今回あらためて検証するとまったく再現しませんでした。
    ===2019/08/09の記事:ここまで===
    今回、質問させて頂きたいのは、動画に関するバックグラウンド再生についてでございます。
    動画でも、バックグラウンド再生できないものかと考え、2019/08/09に頂いたコードに動画再生を実装(以下の6箇所追加、■2箇所削除)してみましたが、「ホームボタン押し」や「電源ボタン押し」で再生が停止してしまします。
    iPhoneにインストールされている、mp4プレーヤーでは、ホームボタン押し、または電源ボタン押しで、一旦停止し、電源OFFから電源ONで、リモートモード(と呼ぶのが正しいのかわかりませんが)で再生ボタンを押すと、以降、電源ボタンでOFFしても、音声のみの再生が可能になっていました。
    動画に対し、更に、追加する必要のあるコード、または、チュートリアル等をアドバイス頂ければ、検討したく、お手数をお掛けして申し訳ありませんが、お時間が許す範囲で、ご教示頂ければ幸いです。どうかよろしくお願い致します。

    以下は、実装実験をしているコードです。
    import UIKit
    import AVFoundation
    import AVKit
    import MediaPlayer

    class ViewController: UIViewController, AVSpeechSynthesizerDelegate {

    //added no.1/6 from here
    var player = AVPlayer(url: URL(fileURLWithPath:Bundle.main.path(forResource: “yui life”, ofType: “mp4”)!))
    //added no.1/6 until here

    @IBOutlet weak var messageLabel: UILabel!
    var syntherizer = AVSpeechSynthesizer()

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    // AudioSessionカテゴリをbackground再生ができるものに設定
    let audioSession = AVAudioSession.sharedInstance()

    do {
    //try audioSession.setCategory(AVAudioSession.Category.playback)
    try audioSession.setCategory(.playback, mode: .default, options: .mixWithOthers)
    } catch {
    print(“Setting category to AVAudioSessionCategoryPlayback failed.”)
    }

    addAudioSessionObservers()
    addRemoteCommandEvent()
    addLifeCycleObserver()
    }

    // 電話による割り込みと、オーディオルートの変化を監視します
    func addAudioSessionObservers() {
    let center = NotificationCenter.default
    center.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
    center.addObserver(self, selector: #selector(audioSessionRouteChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
    }

    /// Interruption : 電話などによる割り込み
    @objc func handleInterruption(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
    let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
    let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
    return
    }

    if type == .began {
    // interruptionが開始した時(電話がかかってきたなど)
    if let wasSuspendedKeyValue = userInfo[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber {
    let wasSuspendedKey = wasSuspendedKeyValue.boolValue
    if wasSuspendedKey {
    // suspeended key : true
    } else {
    // suspended key : false
    }
    } else {
    // suspended key : nil
    }
    }
    else if type == .ended {
    // interruptionが終了した時の処理
    if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
    let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
    if options.contains(.shouldResume) {
    // Interruption Ended – playback should resume
    } else {
    // Interruption Ended – playback should NOT resume
    }
    }
    }
    }

    /// Audio Session Route Change : ルートが変化した(ヘッドフォンが抜き差しされた)
    @objc func audioSessionRouteChanged(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
    let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
    let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
    return
    }

    DispatchQueue.main.async {
    self.messageLabel.text = self.routeChangeReasonDescription(reason: reason)
    }

    switch reason {
    case .newDeviceAvailable:
    let session = AVAudioSession.sharedInstance()
    for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
    // ヘッドフォンがつながった

    break
    }
    case .oldDeviceUnavailable:
    if let previousRoute =
    userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
    for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
    // ヘッドフォンが外れた

    // 音声をpauseしています
    pause()
    break
    }
    }
    default: ()
    }

    }

    public func speak() {
    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setActive(true)
    } catch {

    }
    let voice = AVSpeechSynthesisVoice(language: “ja-JP”)
    let utterance = AVSpeechUtterance(string: “おはようございます。今日もいい天気ですね。音声合成で読み上げています。文章をうまくよめていますか?”)
    utterance.voice = voice
    utterance.preUtteranceDelay = 0

    if syntherizer.isPaused {
    syntherizer.continueSpeaking()
    } else {
    syntherizer.speak(utterance)
    }
    }

    public func pause() {
    syntherizer.pauseSpeaking(at: .immediate)
    }

    //added no.2/6 from here
    func playMovieFromProjectBundle() {//func playMovie
    if let bundlePath = Bundle.main.path(forResource: “yui life”, ofType: “mp4”) {
    let videoPlayer = AVPlayer(url: URL(fileURLWithPath: bundlePath))
    // 動画プレイヤーの用意
    let playerController = AVPlayerViewController()
    playerController.player = videoPlayer
    self.present(playerController, animated: true, completion: {
    videoPlayer.play()
    })
    } else {
    print(“no such file”)
    }
    }//func playMovie
    //added no.2/6 until here

    /// Speakボタンが押された
    @IBAction func speakBtnTapped(_ sender: Any) {//func speakBtn
    //①音声出力を行う
    //■deleted no.1/2 from here
    //speak()
    //■delete no.1/2 until here

    //added no.3/6 from here
    //②AVPlayerを使ったmp4再生コマンド ここから
    // Bundle Resourcesから***.mp4を読み込んで再生
    //let path = Bundle.main.path(forResource: “yui life”, ofType: “mp4”)!
    //let player = AVPlayer(url: URL(fileURLWithPath: path))
    player.actionAtItemEnd = .none // default: pause
    player.play()
    // AVPlayer用のLayerを生成
    let playerLayer = AVPlayerLayer(player: player)
    //add01
    playerLayer.frame = self.view.bounds
    self.view.layer.addSublayer(playerLayer)
    playerLayer.zPosition = -2// ボタン等よりも後ろに表示
    view.layer.insertSublayer(playerLayer, at: 0)// 動画をレイヤーとして追加
    //AVPlayerを使ったmp4再生コマンド ここまで
    //added no.3/6 until here

    }//func speakBtn

    /// pauseボタンが押された
    @IBAction func pauseBtnTapped(_ sender: Any) {
    pause()
    }

    @IBAction func stopBtnTapped(_ sender: Any) {
    syntherizer.stopSpeaking(at: .immediate)
    }

    /// 監視する必要がなくなった段階で、Observerを取り外します
    private func removeAudioSessionObservers() {
    let center = NotificationCenter.default

    // AVAudio Session
    center.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
    center.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)

    }

    /// route変更の理由表示用
    private func routeChangeReasonDescription(reason: AVAudioSession.RouteChangeReason) -> String {
    switch reason {
    case .unknown:
    return “unknown”
    case .newDeviceAvailable:
    return “newDeviceAvailable”
    case .oldDeviceUnavailable:
    return “oldDeviceUnailable”
    case .categoryChange:
    return “categoryChange”
    case .override:
    return “override”
    case .wakeFromSleep:
    return “wakeFromSleep”
    case .noSuitableRouteForCategory:
    return “noSuitableRouteForCategory”
    case .routeConfigurationChange:
    return “routeConfigurationChange”
    default:
    return “default”
    }
    }

    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
    commandCenter.playCommand.addTarget(self, action: #selector(type(of: self).remotePlay(_:)))
    commandCenter.pauseCommand.addTarget(self, action: #selector(type(of: self).remotePause(_:)))
    commandCenter.nextTrackCommand.addTarget(self, action: #selector(type(of: self).remoteNextTrack(_:)))
    commandCenter.previousTrackCommand.addTarget(self, action: #selector(type(of: self).remotePrevTrack(_:)))
    }

    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // 略

    }

    @objc func remotePlay(_ event: MPRemoteCommandEvent) {
    // プレイボタンが押された時の処理
    //■deleted no.2/2 from here
    //speak()
    //■deleted no.2/2 from here

    //added no.4/6 from here
    player.play()
    //added no.4/6 until here

    }

    @objc func remotePause(_ event: MPRemoteCommandEvent) {
    // ポーズボタンが押された時の処理
    pause()

    //added no.5/6 from here
    player.pause()
    //added no.5/6 until here

    }

    @objc func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    // 「次へ」ボタンが押された時の処理
    // (略)
    }

    @objc func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    // 「前へ」ボタンが押された時の処理
    // (略)

    }

    // MARK: – Life Cycle
    func addLifeCycleObserver() {
    let center = NotificationCenter.default
    // 今回はbackground移行を検知
    center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }

    @objc func didEnterBackground(_ notification: Notification) {
    // 音が止まっていたらAudioSessionを無効化
    if !syntherizer.isSpeaking || syntherizer.isPaused {
    deactivateAudioSession()
    }

    //add no.6/6 from here
    //mp4再生コマンド ここから
    // Bundle Resourcesから***.mp4を読み込んで再生
    player.actionAtItemEnd = .none // default: pause
    player.play()
    // AVPlayer用のLayerを生成
    var playerLayer = AVPlayerLayer(player: player)
    //add01
    playerLayer.frame = self.view.bounds
    self.view.layer.addSublayer(playerLayer)
    playerLayer.zPosition = -2// ボタン等よりも後ろに表示
    view.layer.insertSublayer(playerLayer, at: 0)// 動画をレイヤーとして追加
    //mp4再生コマンド ここまで
    //added no.6/6 until here

    let app = UIApplication.shared
    app.beginBackgroundTask(expirationHandler: {
    // このblockはバックグラウンド処理に入って、所定時間後(180秒程度後)に実行される
    // (バックグラウンド再生が継続している間は実行されない)
    // ここでAudioSessionを無効化しておく
    // (この処理がないとバックグラウンド再生中にpauseしたあと、3分すぎにアプリを再開すると
    // いきなり音が鳴る症状が出てしまう)
    self.deactivateAudioSession()

    })
    }

    func deactivateAudioSession() {
    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setActive(false)
    } catch {

    }
    }
    }

  4. nackpan様
    kkです。
    新しい記事をありがとうございます。
    内容を勉強させて頂きます。
    まずは、お礼まで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください