AirPods を試してみた

<img width=500 src="https://qiita-image-store.s3.amazonaws.com/0/3603/9dad005c-84bc-6ef7-3840-1db60d94bbcd.png">
*http://www.apple.com/jp/airpods/*

待ちに待った AirPods が届いたので、早速使ってみました。

# 普通に使う

## 接続

ケースの蓋そのものが電源投入のようなアクションを兼ねており、パカっと開いたらすぐに iPhone が AirPods の存在を認識しました。実際の接続はユーザーの判断に委ねられていますが、そこには Bluetooth デバイスにありがちな煩わしいアクションが一切なく簡単に接続できるのはとても良い体験だと思いました。

|AirPods の蓋を開くと iPhone が認識する|
|:--:|
|<img width="375" alt="FullSizeRender 4.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/3603/4350e2c3-3230-5588-3836-307dd5cc1e58.jpeg">|

ちなみに iCloud にサインインしているデバイスには AirPods が自動的に設定されるので、初期設定としての接続作業はただこの一度のみです。これ以降は同じ iCloud / Apple ID に紐づいた Mac の Bluetooth メニューからも選べるようになります。

|Mac から AirPods に接続すると Mac のイヤフォンとして使える|
|:--:|
|<img width="453" src="https://qiita-image-store.s3.amazonaws.com/0/3603/7651c609-d40b-9d30-d65d-841d289af139.png">|

参考：*[AirPods を使う](https://support.apple.com/ja-jp/HT207010)*

以降は接続している AirPods ケースの蓋を開くとこのようなシートが表示されて電池残量などを確認することができます。

|ケースと AirPods 両方の電池残量を確認できる|
|:--:|
|<img width="375" src="https://qiita-image-store.s3.amazonaws.com/0/3603/1fc24168-5e13-cd69-7961-db5220d5b9d7.png">|

電池残量はウィジェットからも確認することができます。

|iOS のウィジェット|
|:--:|
|<img width="375" alt="FullSizeRender.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/3603/5b7199a1-37d4-7edf-d0df-212ec8f741bc.jpeg">|

AirPods と接続すると、耳への装着時に『ヴォン』といういい感じの音が鳴るので AirPods が有効かどうかが瞬時に理解できます。そして iOS ならステータスバーの Bluetooth アイコンがヘッドホンに変化します。

|iOS のステータスバー|
|:--:|
|<img width="143" alt="IMG_0681.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/3603/7cf6ff59-ef69-a70b-e36e-d68320118f3c.png">|

## 使う - 設定変更

AirPods はダブルタップすることでアクションを実行できます。デフォルトでは Siri を起動するようになっていますが、これは iPhone / Mac ともに Bluetooth の設定画面から変更することができます。私は Siri を使わないので「再生/一時停止」にしました。これでダブルタップでミュージックの一時停止ができるようになりました。

なお、EarPods のコントローラーのように早送りだとかはできないようなので、これは iPhone なら Control Center を活用することになります。
（Siri を使えば可能かもしれませんが、試していません。）

|iOS の Bluetooth 設定画面|
|:--:|
|<img width="375" src="https://qiita-image-store.s3.amazonaws.com/0/3603/cdc563bf-9ec3-08d1-fcb4-73f9ed2b3aa6.png">|

|iOS の Control Center|
|:--:|
|<img width="375" alt="IMG_0673.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/3603/cdf29d37-ec0c-4ca3-9b5b-b13fef47e85a.png">|

## 使う - 自動耳検出

AirPods が耳に装着されたことを検知して、自動的に音声が流れるようになります。耳から外れると音声が停止します。この辺りの動作はとてもスマートに働くのでそのまま活用していきたいところです。例えば、ミュージックを再生中に片方の Pod を耳から外すとそこで再生が一時停止します。再び装着すると自動的に再生が再開します。

これは、AirPods 本体の側面に2つある光センサーによって実現しているものと思われます。

|光センサーと思われる黒い点（2つある）|
|:--:|
|<img width="227" src="https://qiita-image-store.s3.amazonaws.com/0/3603/f04076d4-3310-29b8-7c1d-a27ce44ad7cb.png">|

## 使う - 出力デバイスの変更

iPhone なら Control Center, Mac ならサウンドメニューなどから音声出力デバイスを選択することができます。

|iPhone|Mac|
|:--:|:--:|
|<img width="375" src="https://qiita-image-store.s3.amazonaws.com/0/3603/c57bb059-7786-30d9-66be-23cba81f3ff5.png">|<img width="303" src="https://qiita-image-store.s3.amazonaws.com/0/3603/14921ff4-050e-9475-185f-0477ac90107f.png">|

余談ですが、Mac のサウンドメニューのスライダーが縦向きから横向きに変わった理由がこれではっきりしましたね。

| El Capitan までのサウンドメニュー |
|:--:|
|<img width="32" src="https://qiita-image-store.s3.amazonaws.com/0/3603/0a0af437-2189-3880-4d34-6ab17ef7fd2a.png">|

## 使用感

- 概ね満足、とにかく接続の煩わしさが無いのはとても良い
- 遅延を感じることはない
- 無線なのでデバイスと耳の距離を意識しなくてもよくなった（他の無線イヤフォンにも言えるが）
- 耳に装着したまま iPhone と Mac の接続切り替えが楽に行える
- 耳からは落ちにくい（おそらく個人差はある）
- ケースの蓋の開閉感触・質感がとても良い
- ケースが意外と小さい
- 稼働している電子レンジのそばでは途切れてしまう
- 唯一の不満点、音声再生の直前直後に必ずホワイトノイズが乗ってしまう

### デザイン

無線になったことで面倒な接続手順など逆に無線の煩わしさを増やしてしまうようなことは決してやっていないところはさすがです。線がなくなったのであれば、ユーザーが意識しなければならない事象もそこからは減っていなければなりません。ケースの蓋を開いたら即認識するとか、片方の Pod だけ外したら再生が一時停止するとか、人間の行動が機械に阻害されずに自然のままが保たれる、そのようにデザインされたデバイスだと思います。デバイスの形状が丸みを帯びているのも、耳に装着する人間にとても近い領域のデバイスだからなのでしょう。

|AirPods のお立ち台|
|:--:|
|<img width="500" src="https://qiita-image-store.s3.amazonaws.com/0/3603/afb4602a-3021-8672-ff2d-b7145124ae22.jpeg">|

### 欠点

ホワイトノイズに関しては例えば iOS のキーボードを開いた瞬間（テキストフイールドがファーストレスポンダーになった直後）にホワイトノイズが1、2秒程度聞こえてきます。これはキーボードのクリック音を再生するための前処理が走ることによって AirPods が通電するような感覚で、その通電最中にノイズが聞こえてくるという具合です。音声が再生されていない状態が続くと自動的にスタンバイになるのか、ノイズが途切れる瞬間にプツッと小さな破裂音も聞こえます。音楽再生中なら違和感ないのでしょうが、静寂の中の UI のクリック音の前後では、聴力がよければかなり気になってしまうところでしょう。

# AVAudioSession で出力経路が変更された際の通知を見てみる

一応プログラミングのトピックを守らなければならないので、実際に AVAudioSession ではどう扱われるのか、簡単に検証してみました。

オーディオの出力経路が変更された際に通知される Notification を張って中身をみてみました。

```swift
NotificationCenter.default.addObserver(self,
                                       selector: #selector(routeChanged(_:)),
                                       name: .AVAudioSessionRouteChange,
                                       object: nil)
```
```swift
	func routeChanged(_ notif: Notification) {
		print("CURRENT ROUTE:")
		descritionOfRoute(AVAudioSession.sharedInstance().currentRoute)
		
		if let userInfo = notif.userInfo {
			print("PREVIOUS ROUTE:")
			descritionOfRoute(userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription)
			
			if let reasonNumber = userInfo[AVAudioSessionRouteChangeReasonKey] as? NSNumber,
				let reason = AVAudioSessionRouteChangeReason(rawValue: UInt(reasonNumber.intValue)) {
				var reasonName = ""
				
				switch reason {
				case .newDeviceAvailable:
					reasonName = "newDeviceAvailable"
					break
				case .oldDeviceUnavailable:
					reasonName = "oldDeviceUnavailable"
					break
				case .categoryChange:
					reasonName = "categoryChange"
					break
				case .override:
					reasonName = "override"
					break
				case .wakeFromSleep:
					reasonName = "wakeFromSleep"
					break
				case .noSuitableRouteForCategory:
					reasonName = "noSuitableRouteForCategory"
					break
				default:
					reasonName = "unknown"
					break
				}
				
				print("route change reason: \(reasonName)")
			}
			
			print("--------")
		}
	}
	
	private func descritionOfRoute(_ route: AVAudioSessionRouteDescription?) {
		guard let route = route else {
			return
		}
		
		for input in route.inputs {
			print("input route|port uid: \(input.uid), type: \(input.portType), name: \(input.portName)")
		}
		for output in route.outputs {
			print("output route|port uid: \(output.uid), type: \(output.portType), name: \(output.portName)")
			
			if let channels = output.channels {
				for channel in channels {
					print("channel|uid: \(channel.owningPortUID), name: \(channel.channelName), number: \(channel.channelNumber), Core Audio channel label: \(channel.channelLabel)")
				}
			}
		}
	}
```

実行結果：

接続した状態で実行。`00:00:00:00:00:00` のゼロ部分には実際には値が入っています。

```:結果1
CURRENT ROUTE:
input route|port uid: 00:00:00:00:00:00-tsco, type: BluetoothHFP, name: usagimaruのAirPods
output route|port uid: 00:00:00:00:00:00-tsco, type: BluetoothHFP, name: usagimaruのAirPods
channel|uid: 00:00:00:00:00:00-tsco, name: usagimaruのAirPods, number: 1, Core Audio channel label: 42

PREVIOUS ROUTE:
output route|port uid: 00:00:00:00:00:00-tacl, type: BluetoothA2DPOutput, name: usagimaruのAirPods
channel|uid: 00:00:00:00:00:00-tacl, name: usagimaruのAirPods 左, number: 1, Core Audio channel label: 1
channel|uid: 00:00:00:00:00:00-tacl, name: usagimaruのAirPods 右, number: 2, Core Audio channel label: 2
route change reason: categoryChange
--------
```

そして AirPods を耳から外したりしてみます。

```:結果2
CURRENT ROUTE:
input route|port uid: Built-In Microphone, type: MicrophoneBuiltIn, name: iPhone マイク
output route|port uid: Built-In Receiver, type: Receiver, name: レシーバー
channel|uid: Built-In Receiver, name: レシーバー, number: 1, Core Audio channel label: 4294967295

PREVIOUS ROUTE:
input route|port uid: 00:00:00:00:00:00-tsco, type: BluetoothHFP, name: usagimaruのAirPods
output route|port uid: 00:00:00:00:00:00 tsco, type: BluetoothHFP, name: usagimaruのAirPods
channel|uid: 00:00:00:00:00:00-tsco, name: usagimaruのAirPods, number: 1, Core Audio channel label: 42
route change reason: oldDeviceUnavailable
--------

CURRENT ROUTE:
input route|port uid: Built-In Microphone, type: MicrophoneBuiltIn, name: iPhone マイク
output route|port uid: Built-In Receiver, type: Receiver, name: レシーバー
channel|uid: Built-In Receiver, name: レシーバー, number: 1, Core Audio channel label: 4294967295

PREVIOUS ROUTE:
input route|port uid: Built-In Microphone, type: MicrophoneBuiltIn, name: iPhone マイク
output route|port uid: 00:00:00:00:00:00-tacl, type: BluetoothA2DPOutput, name: usagimaruのAirPods
channel|uid: 00:00:00:00:00:00-tacl, name: usagimaruのAirPods 左, number: 1, Core Audio channel label: 1
channel|uid: 00:00:00:00:00:00-tacl, name: usagimaruのAirPods 右, number: 2, Core Audio channel label: 2
route change reason: oldDeviceUnavailable
--------
```

これを見ると、Bluetooth のデバイス名（〜のAirPods）が `portName` として設定されていることがわかります。一方で AirPods のマイクが認識されていないように見えます。

`portType` を見ると AirPods は `BluetoothA2DPOutput`, A2DP に対応したデバイスであることがわかります。iOS 10.0 から `AVAudioSessionCategoryOptions.allowBluetoothA2DP` が追加されているので、AirPods に合わせて A2DP に対応したのだと思われます。

よくわからないのが、`portType=BluetoothHFP` `usagimaruのAirPods` と認識される場合と、`portType=BluetoothA2DPOutput` `usagimaruのAirPods 左` `usagimaruのAirPods 右` と認識される場合があることです。左右が個別に認識されずに Bluetooth HFP として扱われるのはどのような状態なのか、今ひとつわかりませんでした。

## Bluetooth Peripheral としてはどうか

Core Bluetooth を使ってペリフェラルとしてどのように見えるか検証してみました。

```swift
var manager: CBCentralManager?
// ~
manager = CBCentralManager(delegate: self, queue: nil)
// ~
manager?.scanForPeripherals(withServices: nil, options: nil)
```

適当にペリフェラルをスキャンしてみます。

```:結果
Peripheral: A1A013DD-DCED-4C17-ADB2-C4E8D5B38769, Optional("usagimaruのAirPods"), RSSI: -45
Peripheral: A1A013DD-DCED-4C17-ADB2-C4E8D5B38769, Optional("usagimaruのAirPods"), RSSI: -45
Peripheral: CD130EAC-BBC2-4496-B61F-357AFA28CFFD, nil, RSSI: -34
……
```

（UUIDは仮）

`CD130EAC-BBC2-4496-B61F-357AFA28CFFD` が何なのか気になるところですが、私の部屋の謎デバイスが検知された可能性もあるので何とも言えません。これがケースそのものである可能性も考えたのですが、いろいろな挙動からしてケース自体に通信機能があるのではなくケースに収まった AirPods がデバイスと通信しているのだと思われます。

興味深いのが、Pod を耳から外した直後に少しだけスキャンされるのですが、その時はそこで途絶えてしまいます。

片方の Pod をケースにしまうと今度は大量にスキャンされるようになります。そして両方の AirPods をケースにしまうとスキャンから途絶えます。

ケースの蓋の開閉でも一瞬だけスキャンされるようです。

- Pod はどちらも同じペリフェラルとして認識される
- 左右の Pod はチャンネルで区別される
- 片方の Pod を外すと一瞬スキャンされる
- 片方の Pod をケースにしまうと長時間スキャンされる（上限は不明）
- 両方をケースにしまうとスキャンされなくなる（電源オフ的な状態？）
- ケースの蓋の開閉でも一瞬スキャンされる
- ケース自体が別のペリフェラルであるかは不明

このことから、AirPods の装着とケースへの収納の際にはかなり複雑な条件で通信が行われていることがわかります。

## Core Bluetooth から接続できるのか

結論から言うとできなかったのですが、方法としてはスキャンされた Peripheral オブジェクトに対して `connect()` メソッドを投げてみました。

```swift
manager?.connect(airpodsPeripheral!, options: [
				CBConnectPeripheralOptionNotifyOnConnectionKey : NSNumber(value: true),
				CBConnectPeripheralOptionNotifyOnDisconnectionKey : NSNumber(value: true),
				CBConnectPeripheralOptionNotifyOnNotificationKey : NSNumber(value: true)
				])
```

これでどうなるか見てみたのですが、`CBPeripheralState` が `.connecting` のまま何も変化がありませんでした。デリゲートには成功も失敗も一切の通知が来なかったということです。そして、これを実行していたら「ヴォン」が鳴らなくなったりとちょっとバグい挙動をしたので、これ以上深入りするのはやめておくことにしました。買ったばかりでいきなり壊すのも嫌ですしね。

ペリフェラルが "AirPods" であると判定する方法もわかりませんでした。

|なぜか iPhone と AirPods が2つ認識される状態になってしまった|
|:--:|
|<img width="375" src="https://qiita-image-store.s3.amazonaws.com/0/3603/dd1e30d8-0aa2-800a-5cc4-a4e8d519c84a.png">|

挙動不審になってしまったら、一旦接続解除した上でケースのボタンを長押しすることでリセットできることがあります。

# 特にオチがつかなかった

Pencil の時のように AirPods 専用 API があるとかそういった面白いネタでもあればよかったのですが、あまりパッとせずに終わってしまいました。

接続や Siri に関して OS レベルで AirPods を特別に扱う箇所はもちろんありますが、結局はただの Bluetooth イヤフォンです。AVAudioSession から見えるそれは2チャンネルの Bluetooth デバイスでしかないので、アプリでは何もしなくても AirPods から音は鳴りますし、他のデバイスと同じようにオーディオ出力経路の切り替え検知を適切にプログラミングすれば無難に対応することができます。