みんな大好き`AVSpeechSynthsizer`について

[iOS Second Stage](http://qiita.com/advent-calendar/2015/ios-2)の21日目を担当させていただく@ushisantoasobuと申します。よろしくお願いします。

トレンディなネタでもなければ、ほとんどの人が興味ないであろう`AVSpeechSynthsizer`についての記事です。タイトルは冗談です。

`AVSpeechSynthsizer`は`iOS7`から利用できるようになった**テキストを音声で読み上げるAPI**です。
`AVSpeechSynthsizer`を利用したアプリを当時個人でリリースしたのですが、`iOS9`で少し変更点があったので（`iOS8`のときは変更点なし）、今年そのアプリをフルスクラッチで書き直して別アプリとしてリリースしました。その備忘録としてこの記事では、

- `AVSpeechSynthsizer`の基本的な話
- `AVSpeechSynthsizer`の`iOS9`での変更点について
- `AVSpeechSynthsizer`を利用したサービスをつくってみて

といった内容で書いていきたいと思います

# リリースしたアプリ

[Twitilized](https://itunes.apple.com/jp/app/twitilized/id1042204784?mt=8)

`Twitter`を耳で楽しむ、音声読み上げアプリです。

- ホーム
- トレンド
- リスト

のツイートのリストを取得して、それらを`AVSpeechSynthsizer`が順次読み上げていき、耳で聞いて楽しむというアプリになっています。
[こちら](https://vimeo.com/149550665)に実際に動いているところの動画もあげています（`Nexus 9`で撮ったのですが、画質悪いです）。普段自分は非公開のiOSデベロッパの`リスト`から情報を追っているのですが、この動画ではそれを実際に読み上げたものになります。
もし気になった方は良ければ使ってみてください。

# `AVSpeechSynthsizer`の基本的な使い方

以下がテキストを音声で読み上げるまでの基本的なコードです

```swift
import AVFoundation

class SomeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let synthesizer = AVSpeechSynthesizer()

        // 読み上げる文字列を指定する
        let utterance = AVSpeechUtterance(string: "ushisantoasobu")

        // 読み上げの速度を指定する
        utterance.rate = AVSpeechUtteranceMinimumSpeechRate
        // 声の高さを指定する
        utterance.pitchMultiplier = 1
        // 声のボリュームを指定する
        utterance.volume = 1.0

        let voice = AVSpeechSynthesisVoice()
        utterance.voice = voice
        
        // 読み上げる    
        synthesizer.speakUtterance(utterance)
    }
}
```

もちろん、読み上げを一時停止したりすることもできます

```swift
// 読み上げを途中で終了する（終了したところからまた再生したい場合は下のpauseを使う）
synthesizer.stopSpeakingAtBoundary(AVSpeechBoundary.Immediate)

// 読み上げを一時停止する
synthesizer.pauseSpeakingAtBoundary(AVSpeechBoundary.Immediate)

// 一時停止していた読み上げを再生する
synthesizer.continueSpeaking()
```

デリゲート（`AVSpeechSynthesizerDelegate`）メソッドには以下のようなものがあります

```swift
// 読み上げが開始したとき
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didStartSpeechUtterance utterance: AVSpeechUtterance)

// 読み上げが終了したとき
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance)

// 読み上げが一時停止したとき
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didPauseSpeechUtterance utterance: AVSpeechUtterance)

// 読み上げが一時停止から再生されたとき
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didContinueSpeechUtterance utterance: AVSpeechUtterance)

// 読み上げが途中で終了したとき
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didCancelSpeechUtterance utterance: AVSpeechUtterance)

// 読み上げ中で字句ごとに呼ばれる
func speechSynthesizer(synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance)
```


## `iOS9`で新しくなったポイント

残念なことにワクワクするような変更点はありません。以下が新たに追加された部分になります

```swift:AVSpeechSynthesis
@available(iOS 9.0, *)
public enum AVSpeechSynthesisVoiceQuality : Int {
    case Default
    case Enhanced
}

// Use the Alex identifier with voiceWithIdentifier:. If the voice is present on the system,
// an AVSpeechSynthesisVoice will be returned. Alex is en-US only.
@available(iOS 9.0, *)
public let AVSpeechSynthesisVoiceIdentifierAlex: String

/*!
 @method        voiceWithIdentifier:
 @abstract      Retrieve a voice by its identifier.
 @param			identifier
 A unique identifier for a voice.
 @discussion
 Passing in an invalid identifier will return nil.
 Returns nil if the identifier is valid, but the voice is not available on device (i.e. not yet downloaded by the user).
 */
@available(iOS 9.0, *)
public /*not inherited*/ init?(identifier: String)

@available(iOS 9.0, *)
public var identifier: String { get }
@available(iOS 9.0, *)
public var name: String { get }
@available(iOS 9.0, *)
public var quality: AVSpeechSynthesisVoiceQuality { get }
```

下の３つのプロパティは`get-only`です。なので注目すべきは`init?(identifier: String)`になると思います。`identifier`をなにやら指定できるとのこと。ふむふむ。ただしここの`identifier`に指定できるのが、現状だと`AVSpeechSynthesisVoiceIdentifierAlex`のみのようです。コメントに、

> Alex is en-US only

とありますが、読み上げの言語設定が`en-US`の状態で日本語を読み上げようとしても、正しく読み上げられません（無音）。

ちなみに`Alex`の声を利用する場合はダウンロードする必要があるらしく、その方法が[こちら](https://support.apple.com/ja-jp/HT203077)に書かれていて、

> 「設定」>「一般」>「アクセシビリティ」>「スピーチ」>「声」

を言語設定が日本語の状態でみてみると、以下のような感じになっています。

<img width="414" alt="image" src="https://qiita-image-store.s3.amazonaws.com/0/50827/a6a0e437-dd8c-b0e3-7c9a-4ef184871060.png">

ここらへんの`Siri`の声だったり、`Mac OS X`にある`Otoya`が`AVSpeechSynthsizer`でいずれダウンロードすることで利用できるようになるのかなと思っています。

![image](https://qiita-image-store.s3.amazonaws.com/0/50827/56a1d776-e510-f125-44fe-2c802e2cb699.png)

# 気をつけるべきこと

こちらは`AVSpeechSynthsizer`に限った話ではないのですが、音を再生するようなアプリケーションをつくるときにはいくつか考慮しておくべき項目がありますので、以下に挙げていこうと思います

## バックグラウンド再生

バックグラウンド再生に対応するかどうかはもちろん考慮する必要があります。これについては他の記事で多く見つかると思うのでここでは割愛させていただきます

## イヤホンジャックを抜いたとき

音を再生するアプリにおいて、イヤホンジャックを抜いたときに再生されていた音が停止する、というのは大抵の場合対応マストの項目だと思います。電車でイヤホンジャックが抜けて、聞いていたものが車内に響き渡って大恥をかかせる、なんてことはあってはいけないですからね。。。「ミュージック」や「Podcast」ももちろんそうなっています。
実装方法は以下のようになります

```swift
NSNotificationCenter.defaultCenter().addObserver(self, selector: "onAudioSessionRouteChange:", name: AVAudioSessionRouteChangeNotification, object: nil)
```

```swift

@objc func onAudioSessionRouteChange(notification :NSNotification) {
    // ここで再生をとめる処理を書く
}
```

## リモコン等からの操作に対応する

Apple純正のイヤホンやロックスクリーンからの操作に対応しておくとベターかと思います。
実装方法は以下のようになります

```swift
UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
```

```swift
override func remoteControlReceivedWithEvent(event: UIEvent?) {
    //　以下に各イベントに対する処理を書く
    switch event!.subtype {
    case UIEventSubtype.RemoteControlPlay:
        //
    case UIEventSubtype.RemoteControlPause:
        //
    case UIEventSubtype.RemoteControlTogglePlayPause:
        //
    case UIEventSubtype.RemoteControlPreviousTrack:
        //
    case UIEventSubtype.RemoteControlNextTrack:
        //
    default:
        //
    }
}
```

# 実際にサービスでつくってみて思ったこと

`AVSpeechSynthsizer`を使ったiOSアプリを実際につくってみて思ったことを以下に書いていきたいと思います

## 読みの精度はまだまだ（日本語）

少なくとも日本語の読み上げの精度はまだまだで、ここらへんは難しいとは思うのですが重要なところです。以下自分のタイムラインでよく出てくるテキストで読み上げに不満があるものです

- `（日）` => 「ひ」（イベント情報なんかでよく出てくる）
- `Github` => 「ギサブ」

ちなみに`Twitilized`では、'Twitter'クライアントアプリということで一部独自に変換しているものがあります（工夫しているつもり）。

- 各種リンクは「リンク」という文字列に変換
- 各種ハッシュタグは「#」を「ハッシュタグ」という文字列に変換
- リツイートをあらわす「RT」は「リツイート」という文字列に変換

## `rate`は実質`Default`のみ

```swift
utterance.rate = AVSpeechUtteranceDefaultSpeechRate
```

で指定できる読み上げの速度のパラメータである`rate`については以下の３つがあります。

```swift
@available(iOS 7.0, *)
public let AVSpeechUtteranceMinimumSpeechRate: Float
@available(iOS 7.0, *)
public let AVSpeechUtteranceMaximumSpeechRate: Float
@available(iOS 7.0, *)
public let AVSpeechUtteranceDefaultSpeechRate: Float
```

が、`Minimum`は遅すぎるし、`Maximum`は速すぎて・・・個人的には`Degault`以外は使い物にならないように感じていますが・・・

# 参考記事

http://koze.hatenablog.jp/entry/2015/06/10/130000