🎱

AirPodsの首振り・頷きジェスチャーを検知するiOSアプリ

に公開

はじめに

AirPods Pro、AirPods(第3世代)、AirPods Maxには、モーションセンサーが内蔵されています。このセンサーを使うと、ユーザーの頭の動き(頷き・首振り)を検知することができます。

本記事では、AppleのCMHeadphoneMotionManagerを使って、首振りジェスチャーを検知する方法を解説します。

当記事のサンプルコードはこちらです。
https://github.com/hitonomichi/iOS-Samples/blob/main/SampleDeviceMotion/

対応デバイス

以下のAirPodsが対応しています。

  • AirPods Pro(第1世代・第2世代)
  • AirPods(第3世代)
  • AirPods Max

注意: AirPods(第1世代・第2世代)は非対応です。

事前準備

Info.plistの設定

モーションセンサーにアクセスするには、Info.plist に使用目的の説明を追加する必要があります。

<key>NSMotionUsageDescription</key>
<string>AirPodsのジェスチャーを検知するために使用します</string>

ユーザー側の設定

アプリを使用する前に、以下の設定が必要です。

  1. AirPodsをiPhoneに接続する
  2. 設定 > AirPods(自分のAirPodsの名前) > 頭のジェスチャー をONにする
  3. アプリ初回起動時にモーションアクセスを許可する

実装の解説

CMHeadphoneMotionManagerの初期化

まず、CoreMotionフレームワークをインポートし、CMHeadphoneMotionManagerのインスタンスを作成します。

import CoreMotion

class HeadphoneMotionManager {
    private let motionManager = CMHeadphoneMotionManager()
    
    var isAvailable: Bool {
        return motionManager.isDeviceMotionAvailable
    }
}

isDeviceMotionAvailableプロパティで、対応したAirPodsが接続されているかを確認できます。

モーションデータの取得開始

startDeviceMotionUpdatesメソッドを呼び出すと、AirPodsからリアルタイムでモーションデータを受信できます。

func startUpdates() {
    guard motionManager.isDeviceMotionAvailable else {
        print("Headphone motion is not available")
        return
    }
    
    motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
        guard let self = self, let motion = motion else { return }
        self.processMotion(motion)
    }
}

コールバックで受け取るCMDeviceMotionオブジェクトには、AirPodsの姿勢情報が含まれています。

姿勢データ(attitude)の理解

CMDeviceMotionのattitudeプロパティから、頭の傾きを取得できます。

let currentPitch = motion.attitude.pitch  // 上下の傾き(頷き)
let currentYaw = motion.attitude.yaw      // 左右の回転(首振り)
プロパティ 動作 正の値 負の値
pitch X軸 頷き 上を向く 下を向く
yaw Z軸 首振り 左を向く 右を向く

値の単位はラジアンです。

ジェスチャー検知のロジック

ジェスチャーを検知するには、前フレームとの差分を計算し、閾値を超えたかどうかを判定します。

// 閾値の設定(ラジアン/フレーム)
private let pitchThreshold: Double = 0.04  // 約2.3度
private let yawThreshold: Double = 0.04    // 約2.3度

// 前回の値を保持
private var previousPitch: Double?
private var previousYaw: Double?

private func processMotion(_ motion: CMDeviceMotion) {
    let currentPitch = motion.attitude.pitch
    let currentYaw = motion.attitude.yaw
    
    defer {
        previousPitch = currentPitch
        previousYaw = currentYaw
    }
    
    guard let prevPitch = previousPitch, let prevYaw = previousYaw else {
        return
    }
    
    let pitchDelta = currentPitch - prevPitch
    let yawDelta = currentYaw - prevYaw
    
    // 上下の検知(pitchの変化)
    if abs(pitchDelta) > pitchThreshold {
        let gesture: HeadGesture = pitchDelta > 0 ? .up : .down
        notifyGesture(gesture)
        return
    }
    
    // 左右の検知(yawの変化)
    if abs(yawDelta) > yawThreshold {
        let gesture: HeadGesture = yawDelta > 0 ? .left : .right
        notifyGesture(gesture)
        return
    }
}

連続検知の防止

1回の首振りが複数回検知されるのを防ぐため、クールダウン期間を設けます。

private var lastGestureTime: Date = Date.distantPast
private let cooldownInterval: TimeInterval = 0.5

private func notifyGesture(_ gesture: HeadGesture) {
    // クールダウン中は検知しない
    guard Date().timeIntervalSince(lastGestureTime) > cooldownInterval else {
        return
    }
    
    lastGestureTime = Date()
    delegate?.headphoneMotionManager(self, didDetect: gesture)
}

この例では0.5秒のクールダウンを設定しています。

閾値のチューニング

閾値の調整は、ユーザー体験に大きく影響します。

閾値 感度 特徴
0.02 高い 軽い動きでも反応。誤検知が増える可能性あり
0.04 中程度 バランスが良い。推奨値
0.08 低い はっきりした動きのみ検知

実際のデバイスでテストしながら、用途に合った値を見つけてください。
私が検証してみた時は、0.08だとかなり大きくジェスチャーする必要がありました。
実用的には0.04程度が良いかと思います。

実機検証

初回起動時にモーションとアクティビティへのアクセス許可を求められます。

実際にAirPodsを装着し、左右、上下に首を振ると検知します。

まとめ

CMHeadphoneMotionManagerを使うことで、AirPodsのモーションセンサーから頭の動きを取得し、ジェスチャーとして活用できます。

ポイントをまとめると以下の通りです。

  • CMHeadphoneMotionManagerでAirPodsのモーションデータを取得
  • attitude.pitchで上下、attitude.yawで左右の動きを検知
  • 前フレームとの差分で動きの方向と大きさを判定
  • 閾値とクールダウンで誤検知を防止

ハンズフリー操作やアクセシビリティ機能など、さまざまな応用が考えられます。ぜひ試してみてください。

参考リンク

Discussion