ヘッドフォンの抜き差しと電話の着信割り込みを検知する


#1. はじめに

久しぶりの記事投稿です。
最近、縁あって動画再生アプリの開発に従事しているため、それに関連する記事を投稿させていただきます。

・本稿範囲：AVAudioSessionを利用しての、ヘッドフォンの抜き差し検知と電話の着信割り込み検知
・対象読者：音楽再生アプリ、または、動画再生アプリを開発している方
・動作検証環境：iPhone6+ iOS 8.1.2
・開発環境：XCode 6.1.1
・前提知識：Objective-Cの知識がある程度あること
・前提条件：今回使用する技術はiOS6以降で有効なものですので、それ以前の環境には適用できません。

#2. 何故、それが必要か？
アプリケーションで音楽や動画を再生している際、ヘッドフォンを抜いたり、電話が掛かってくると音声は停止します。
が、UIの方は自動的には更新されません。
例えば、iPhoneのミュージックアプリでは、再生中と停止中でUIが以下のように変化します。

再生中
![スクリーンショット 2014-12-23 17.46.19.png](https://qiita-image-store.s3.amazonaws.com/0/43853/5556a2c8-a150-cdf2-6de5-b81e4cc8e181.png "スクリーンショット 2014-12-23 17.46.19.png")

停止中
![スクリーンショット 2014-12-23 17.46.31.png](https://qiita-image-store.s3.amazonaws.com/0/43853/8b3f69e2-0239-e7cc-bc54-df85c51b14bd.png "スクリーンショット 2014-12-23 17.46.31.png")

このミュージックアプリのように音声が停止されたのに合わせてUIを変更してやらないと、音声は停止しているのにUIは再生状態のまま、、、といったおかしなことになってしまいます。
このような不整合を防ぐため、ヘッドフォンを抜いた、または、電話の着信があったことを検知したらUIの方も更新してやる必要があります。

#3. AVAudioSession
iOSには、AudioSessionという概念があります。
これはアプリケーションとiOSの音声周りの動作を設定してくれるものです。
例えば、他のアプリの音声をどう扱うか（消音するか、自分のアプリの音声と合成して鳴らすか）、アラーム音等の音声割り込みにどのように対処するか、ヘッドフォンが抜かれた際に、どう対処するか etc..

そして、このAudioSessionを扱うクラスがAVAudioSessionです。
シングルトンなクラスになっており、以下のようにして取得できます。

```
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
```

AVAudioSessionは、ヘッドフォンを抜いた、さした、電話の着信があった、といった事を通知してくれますので、これらの通知を監視することでアプリケーションとしてどのように動作するかを記述する事ができます。

#4. ヘッドフォンの抜き差し
ヘッドフォンが抜かれた、さされた際にはAVAudioSessionRouteChangeNotificationという通知が届きます。
これは、音声の信号経路に変更が生じた際に届く通知です。
NSNotificationのUserInfoには、AVAudioSessionRouteChangeReason（信号経路に変更が生じた理由）とAVAudioSessionRouteDescription（信号経路変更以前の音声信号経路に関する情報）の２つの情報が含まれており、これらを調べる事でどのような変更が生じたのかを知ることができます。
それぞれ、キーはAVAudioSessionRouteChangeReasonKey、AVAudioSessionRouteChangePreviousRouteKeyとなっています。

## 4.1. AVAudioSessionRouteChangeReason
通知のUserInfoに含まれる情報のひとつで、信号経路の変更が生じた理由を知ることができます。
列挙型で以下のように定義されています。

```
enum {
   AVAudioSessionRouteChangeReasonUnknown  = 0,
   AVAudioSessionRouteChangeReasonNewDeviceAvailable  = 1,
   AVAudioSessionRouteChangeReasonOldDeviceUnavailable  = 2,
   AVAudioSessionRouteChangeReasonCategoryChange  = 3,
   AVAudioSessionRouteChangeReasonOverride  = 4,
   AVAudioSessionRouteChangeReasonWakeFromSleep  = 6,
   AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory  = 7,
   AVAudioSessionRouteChangeReasonRouteConfigurationChange  = 8,
};
typedef NSUInteger  AVAudioSessionRouteChangeReason;
```

ヘッドフォンを抜いた場合には、AVAudioSessionRouteChangeReasonOldDeviceUnavailableが返され、逆にさした場合にはAVAudioSessionRouteChangeReasonNewDeviceAvailableが返されます。

## 4.2. AVAudioSessionRouteDescription
通知のUserInfoに含まれるもうひとつの情報で、信号経路が変更する以前の経路情報を知ることができます。
AVAudioSessionRouteDescriptionのoutputsプロパティにAVAudioSessionPortDescriptionの配列が
格納されているのでこれを取り出す事で、変更以前の信号経路の情報を取得できます。

## 4.3. 実践
上述の説明ではうまく説明できた自信がないので、実際にコードを書いていきます。(Swiftでなくて恐縮です！）
ちなみに、`#import <AVFoundation/AVFoundation.h>`をお忘れなく。

``` AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // AVAudioSessionをアクティベートする
    AVAudioSession* audioSession = [AVAudioSession sharedInstance];
    [audioSession setActive:YES error:nil];
    return YES;
}
```

コメントの通りですが、AVAudioSessioをアクティベートしています。
それ以外には特筆すべき事項はありません。


``` ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    // 信号経路変更のオブザーバーを登録
    NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(audioSessionRouteChangeObserver:) name:@"AVAudioSessionRouteChangeNotification" object:nil];
}
```

AVAudioSessionRouteChangeNotificationを監視するよう、オブザーバーを登録します。
ここではViewControllerのviewDidLoadでやっていますが、必要に応じてオブザーバーを登録する箇所は変えてください。
そして、本稿では書いていませんがオブザーバーの削除もお忘れなく！

``` ViewController.m
- (void)audioSessionRouteChangeObserver:(NSNotification*)notification {
    // NSNotificationのUserInfoを取得
    NSDictionary* userInfo = notification.userInfo;
    // 信号経路変更の理由を取得
    AVAudioSessionRouteChangeReason audioSessionRouteChangeReason = [userInfo[@"AVAudioSessionRouteChangeReasonKey"] longValue];
    // 信号経路変更以前の情報を取得
    AVAudioSessionRouteDescription* audioSessionRouteDescription = userInfo[@"AVAudioSessionRouteChangePreviousRouteKey"];
    // 信号経路変更以前の出力に関する情報を取得
    AVAudioSessionPortDescription* audioSessionPortDescription = audioSessionRouteDescription.outputs[0];

    // 信号経路変更の理由により分岐
    switch (audioSessionRouteChangeReason) {
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            NSLog(@"さされた！");
            NSLog(@"さされる前のoutputのtypeは、%@だ！", audioSessionPortDescription.portType);
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
            NSLog(@"抜かれた！");
            NSLog(@"抜かれる前のoutputのtypeは、%@だ！", audioSessionPortDescription.portType);
            // 抜かれたものがヘッドフォンであれば、UIを停止状態に変更する
            if([audioSessionPortDescription.portType isEqualToString:@"Headphones"]) {
                NSLog(@"ヘッドフォンが抜かれたので、ここでUIを音声停止状態のものに変更する。");
            }
            break;
        default:
            NSLog(@"それ以外！");
            break;
    }
}
```

通知（NSNotification）のUserInfoから信号経路の変更理由（AVAudioSessionRouteChangeReason）、および、変更以前の情報（AVAudioSessionRouteDescription）を取得しています。

まず、AVAudioSessionRouteChangeReasonを参照し、何が経路変更の理由かを特定します。
ヘッドフォンがさされた場合は、AVAudioSessionRouteChangeReasonNewDeviceAvailableが格納され、抜かれた場合は、AVAudioSessionRouteChangeReasonOldDeviceUnavailableが格納されます。

次に出力用のデバイスが有効でなくなった場合、変更以前の出力デバイスがヘッドフォンであれば、ヘッドフォンが抜かれたと判定しています。

以下は実際のログです。

```
# 抜いた
2014-12-23 19:24:36.914 TestAudioSession[747:189851] 抜かれた！
2014-12-23 19:24:36.923 TestAudioSession[747:189851] 抜かれる前のoutputのtypeは、Headphonesだ！
2014-12-23 19:24:36.924 TestAudioSession[747:189851] ヘッドフォンが抜かれたので、ここでUIを音声停止状態のものに変更する。

# さした
2014-12-23 19:24:44.295 TestAudioSession[747:189851] さされた！
2014-12-23 19:24:44.295 TestAudioSession[747:189851] さされる前のoutputのtypeは、Speakerだ！
```

#5. 電話の着信割り込みを検知する
電話の着信があった場合にはAVAudioSessionInterruptionNotificationという通知が届きます。
これは、電話の着信等の音声の割り込みが生じた際に届く通知です。
NSNotificationのUserInfoには、
NSNotificationのUserInfoに含まれる、AVAudioSessionInterruptionTypeKeyにはAVAudioSessionInterruptionTypeが含まれており
これらを調べる事で割り込みの開始なのか終了なのかを判定することができます。

AVAudioSessionInterruptionTypeKeyは列挙型で、以下のように定義されています。

```
enum {
   AVAudioSessionInterruptionTypeBegan  = 1,
   AVAudioSessionInterruptionTypeEnded  = 0,
};
typedef NSUInteger  AVAudioSessionInterruptionType;
```

## 5.1. 実践
それでは、実際にコードを書いていきましょう。

``` ViewController.m
    // 割り込みのオブザーバーを登録
    [notificationCenter addObserver:self selector:@selector(audioSessionInterruptionObserver:) name:@"AVAudioSessionInterruptionNotification" object:nil];
```

viewDidLoadに、割り込み通知のオブザーバーを登録します。


```
- (void)audioSessionInterruptionObserver:(NSNotification*)notification {
    // NSNotificationのUserInfoを取得
    NSDictionary* userInfo = notification.userInfo;
    // 割り込みタイプを取得
    AVAudioSessionInterruptionType audioSessionInterruptionType = [userInfo[@"AVAudioSessionInterruptionTypeKey"] longValue];
    switch (audioSessionInterruptionType) {
        case AVAudioSessionInterruptionTypeBegan:
            NSLog(@"割り込みの開始！");
            NSLog(@"割り込みが入ったので音声が停止された、UIを停止状態のものに変更する。");
            break;
        case AVAudioSessionInterruptionTypeEnded:
            NSLog(@"割り込みの終了！");
            break;
            
        default:
            break;
    }
}
```

通知（NSNotification）のUserInfoからAVAudioSessionInterruptionTypeを参照することで割り込みが入ったか、終わったかを判定できます。
電話の着信が入るとAudioSessionは有無を言わさず終了されますので、UIを停止状態のものに変更しましょう。
（ここではログを吐いているだけですが。）

実際に電話の着信があった場合には以下のようにログが出力されます。

```
# 電話の着信があった
2014-12-23 20:02:13.960 TestAudioSession[757:196172] 割り込みの開始！
2014-12-23 20:02:13.964 TestAudioSession[757:196172] 割り込みが入ったので音声が停止された、UIを停止状態のものに変更する。

# 電話を切った、電話をとらずに拒否した
2014-12-23 20:02:16.589 TestAudioSession[757:196172] 割り込みの終了！
```


#6. おわりに
記事のテーマが音声再生、動画再生アプリ開発の中でもかなり狭い領域となってしまいましたが、どれほどの方に需要がありますかどうか。。。
関係するアプリを開発している方の一助となれば幸いです。
今度は動画編集アプリも作ってみたいなぁ。

##6.1. 参考文献
本稿を執筆するにあたり参考にさせていただいた資料です。
音声再生、動画再生アプリに関心のある方は是非ご一読することをおすすめします。

[Audio Sessionプログラミングガイド](https://developer.apple.com/jp/devcenter/ios/library/documentation/AudioSessionProgrammingGuide.pdf)

[AVAudioSession Reference](https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVAudioSession_ClassReference/index.html)