ObjecTips

Swift & Objective-C で iOS とか macOS とか

複数の AVSpeechSynthesizer の同時再生 iOS 13

最近 iOS 13 でバグも含め挙動が変わった点で色々と苦労している。そんな中良い改善点もあった。タイトルの複数の AVSpeechSynthesizer の同時再生がその1つ。

これまでは AVSpeechSynthesizer の同時再生はできなかった。できないと一口に言っても妙な挙動をするのでこの機会にまとめて整理しておく。

検証のためのコード

以下の様なソースコードでポチポチボタンをタップして色々なパターンで動作を確認してみた。

import UIKit
import AVFoundation

class ViewController: UIViewController {
    
    let synthesizerA = AVSpeechSynthesizer()
    let synthesizerB = AVSpeechSynthesizer()
    let synthesizerC = AVSpeechSynthesizer()

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


    @IBAction func speak1() {
        let utterance = AVSpeechUtterance(string: "1, 1, 1")
        utterance.rate = 0.2
        synthesizerA.speak(utterance)
    }

    @IBAction func speak2() {
        let utterance = AVSpeechUtterance(string: "2, 2, 2")
        utterance.rate = 0.2
        synthesizerA.speak(utterance)
    }

    @IBAction func speak3() {
        let utterance = AVSpeechUtterance(string: "3, 3, 3")
        utterance.rate = 0.2
        synthesizerB.speak(utterance)
    }

    @IBAction func speak4() {
        let utterance = AVSpeechUtterance(string: "4, 4, 4")
        utterance.rate = 0.2
        synthesizerB.speak(utterance)
    }

    @IBAction func speak5() {
        let utterance = AVSpeechUtterance(string: "5, 5, 5")
        utterance.rate = 0.2
        synthesizerC.speak(utterance)
    }

    @IBAction func speak6() {
        let utterance = AVSpeechUtterance(string: "6, 6, 6")
        utterance.rate = 0.2
        synthesizerC.speak(utterance)
    }

    @IBAction func speakAll() {
        speak1()
        speak2()
        speak3()
        speak4()
        speak5()
        speak6()
    }
}

検証結果

記号の意味は以下とする
| は再生開始点
- は生成中
> は再生終了点

iOS 7

未確認

iOS 8~12

Bの開始時にAが再生中であればBは開始されない。

A  |----->
B      |   

Aの再生中にBを2度開始すると、2回目のBは順番待ちにスタックされ再生中のAの終わりにBの2つめが再生される。また2つ目のBのスタックはCによって打ち消されない。

A  |---------->
B      |
B        |     ----->

上記のケースでAの再生中にCを2度開始してもスタックされたBの2つ目が再生される。

A  |---------->
B      |
B        |     ----->
C          |
C            |

Aの再生中にBを2度開始してさらにAを開始するとAの終わりにはAの2つ目が再生される。
再生中のAへのスタックは他のスタックを打ち消す。

A  |---------->
B      |
B        |
A          |   ----->   
iOS 13

同時再生可能!
AとBは完全にパラレルでインスタンス毎に動作する。
音声再生中の次の音声再生開始はインスタンス毎にスタックされ他には影響しない。

A |------->
B |----------->
A       |  -------->
B       |      --------->

別の視点での利点

以下の様に実装した再生の一時停止と再生再開のコードを使って検証

    @IBAction func pauseOrResumeA() {
        if synthesizerA.isSpeaking {
            if synthesizerA.isPaused {
                synthesizerA.continueSpeaking()
            }
            else {
                synthesizerA.pauseSpeaking(at: .immediate)
            }
        }
    }

先ほどの図式に / を一時停止と再生再開として追加する。

iOS 12

これまでは同時再生ができなかったので、Aを再生後、一時停止をしてBを2回再生開始した場合でも、Aの再生再開と再生完了までBは永久に再生が行われないという問題(仕様)があった。

Aが一時停止中(isSpeaking && isPaused)だとBは永久に再生されない

A  |---------/
B      |
B        |

Aが再生再開(continueSpeaking)して再生終了(!isSpeaking)するとようやくBの再生が始まる。一時停止と再生再開の挙動を除くと最初のケースで説明した状況と同じ。

A  |---------/     /--->
B      |
B        |              ----->

実際の実装ケースでは画面別に AVSpeechSynthesizer を使っている場合、一時停止のまま別の画面に移動してしまうと他の画面で一切 AVSpeechSynthesizer 動作しなくなるという問題がある。このため画面から移動する際には強制的に AVSpeechSynthesizer を停止する必要がある。
さらにまた元の画面に戻った際に続きから再生を再開したいのであれば一時停止した位置を保持しておき、そこから新たに再生を開始するという工夫も必要。

iOS 13

同時再生が可能なので、Aが一時停止中だろうがなかろうがBは関係なく再生できる。

A  |---------/     /--->
B      |------------>

実装への影響としては複数の AVSpeechSynthesizer のインスタンスの同時再生が可能になったので、画面毎(もしくはView単位、Class単位)で使っている AVSpeechSynthesizer が再生中か、一時停止状態か、停止状態かを気にする必要がなくなった。
それぞれ別個に一時停止とその地点からの再生再開が可能。
他のインスタンスとの協調動作を気にかける必要が無く実装の切り出しや細分化もしやすくなる。