ハイライト
Core Motion は iOS 17 と watchOS 10 で 3 つの機能を追加しました。AirPods のヘッドトラッキングデータの macOS 対応、Apple Watch Ultra の水中深度と水温の監視、そして HealthKit ワークアウト中の高周波センサーデータストリーム(800Hz 加速度計 / 200Hz デバイスモーション)です。
主な内容
モーションデータは Apple エコシステムの見えない基盤です。歩数計測、衝突検出、空間オーディオといった体験は、加速度計、ジャイロスコープ、気圧計などのセンサーの上に成り立っています。Core Motion は統一された入口として、開発者もこれらのデータを利用できるようにします。
AirPods のヘッドトラッキングが macOS に拡張
(02:22)空間オーディオの動的ヘッドトラッキングは、iPhone や Apple Watch と同じモーションアルゴリズムに依存しています。CMHeadphoneMotionManager は iOS 14 からこれらのデータを公開しており、App はユーザーの頭部姿勢、加速度、回転を追跡できます。
今年、この機能は macOS 14 に拡張されました。
import CoreMotion
let headphoneManager = CMHeadphoneMotionManager()
headphoneManager.delegate = self
// デバイスが対応しているか確認
if headphoneManager.isDeviceMotionAvailable {
headphoneManager.startDeviceMotionUpdates(to: .main) { motion, error in
guard let motion = motion else { return }
// 頭部姿勢(ピッチ、ヨー、ロール)
let attitude = motion.attitude
// ユーザー加速度
let acceleration = motion.userAcceleration
// 回転速度
let rotation = motion.rotationRate
// データソース: 左耳または右耳
let location = motion.sensorLocation // .left or .right
}
}
キーポイント:
- AirPods Pro など、空間オーディオの動的ヘッドトラッキングに対応するヘッドフォンをサポート
sensorLocationでデータが左耳由来か右耳由来かを区別する- 自動耳検出が有効な場合、片耳を外すともう片方へ自動的に切り替えて送信を継続する
- Info.plist に
Motion Usage Descriptionの権限説明を設定する必要がある
接続状態の監視:
extension ViewController: CMHeadphoneMotionManagerDelegate {
func headphoneMotionManagerDidConnect(_ manager: CMHeadphoneMotionManager) {
// ヘッドフォンが接続され、データ受信を開始できる
}
func headphoneMotionManagerDidDisconnect(_ manager: CMHeadphoneMotionManager) {
// ヘッドフォンが切断されたため、関連機能を停止する
}
}
キーポイント:
- delegate メソッドは接続状態が変化したときに呼ばれる
- 自動耳検出が有効な場合、ヘッドフォンの取り外し/装着でも disconnect/connect が発生する
- オーバーイヤーヘッドフォンの装着検出にも対応する
Apple Watch Ultra の水中データ監視
(07:22)CMWaterSubmersionManager は Apple Watch Ultra の気圧計を利用し、シュノーケリングや水泳などの水上アクティビティ中に水深と水温を追跡します。
import CoreMotion
let submersionManager = CMWaterSubmersionManager()
submersionManager.delegate = self
Delegate の実装:
extension WorkoutManager: CMWaterSubmersionManagerDelegate {
// 水没状態の変化(入水/出水)
func waterSubmersionManager(
_ manager: CMWaterSubmersionManager,
didUpdate event: CMWaterSubmersionEvent
) {
switch event.state {
case .notSubmerged:
// 水面上
case .submergedShallow:
// 水深 1 メートル以内
case .submergedDeep:
// 水深 1 メートル超
case .approachingMaxDepth:
// 最大深度 6 メートルに接近
case .pastMaxDepth:
// 6 メートル超
case .sensorDepthError:
// センサーが測定範囲外
}
}
// 深度、圧力、水温の測定データ
func waterSubmersionManager(
_ manager: CMWaterSubmersionManager,
didUpdate measurement: CMWaterSubmersionMeasurement
) {
let depth = measurement.depth?.value // 水深(メートル)
let pressure = measurement.pressure?.value // 水圧
let surfacePressure = measurement.surfacePressure?.value // 水面気圧
}
// 水温更新
func waterSubmersionManager(
_ manager: CMWaterSubmersionManager,
didUpdate temperature: CMWaterTemperature
) {
let waterTemp = temperature.temperature.value // 水温
let uncertainty = temperature.uncertainty // 不確かさ
}
}
キーポイント:
- Apple Watch Ultra のみ対応し、watchOS 9+ が必要
- “Shallow Depth and Pressure” capability の追加が必要
- 水温データは水没状態でのみ利用でき、入水直後は不確かさが高く、時間とともに収束する
- 最大監視深度は 6 メートルで、超えると
pastMaxDepth状態に入る - 深度区分:
notSubmerged→submergedShallow(1m 未満)→submergedDeep(1m 超)
高周波センサーデータストリーム CMBatchedSensorManager
(11:35)Apple Watch Series 8 と Ultra は、新しい高周波センサーデータストリームに対応しています。既存の CMMotionManager(最大 100Hz、サンプル単位でリアルタイム送信)とは異なり、CMBatchedSensorManager は 1 秒ごとにデータをまとめて転送し、加速度計は 800Hz、デバイスモーションは 200Hz まで周波数が上がります。
2 つのデータ取得方式の比較:
| 特性 | CMMotionManager | CMBatchedSensorManager |
|---|---|---|
| 加速度計の周波数 | 最大 100Hz | 800Hz |
| デバイスモーションの周波数 | 最大 100Hz | 200Hz |
| 転送方式 | サンプル単位のリアルタイム送信 | 1 秒ごとのバッチ転送 |
| 遅延 | 低い(サブ秒) | 高め(秒単位) |
| 利用シーン | UI フィードバック、リアルタイム制御 | 動作分析、事後計算 |
| 前提条件 | なし | アクティブな HealthKit Workout が必要 |
(13:18)講演者は野球のスイングを例に、高周波データの価値を示しました。スイング全体は約 0.3 秒で、100Hz サンプリングでは 30 個のデータ点しか得られませんが、800Hz なら 240 個を取得でき、インパクト瞬間の微細な振動差を判別できます。
詳細
高周波データストリームの使用手順
(16:17)CMBatchedSensorManager を使うには、アクティブな HealthKit Workout Session が必要です。
import CoreMotion
import HealthKit
let batchedManager = CMBatchedSensorManager()
// 1. デバイス対応を確認
guard batchedManager.isAccelerometerSupported else { return }
// 2. HealthKit Workout を開始
let workoutConfiguration = HKWorkoutConfiguration()
workoutConfiguration.activityType = .baseball
// ... workout session を構成して開始
// 3. Swift async でバッチデータを受信
Task {
do {
for try await batch in batchedManager.accelerometerUpdates() {
processAccelerometerBatch(batch)
}
} catch {
// 認可エラーまたはプラットフォーム非対応を処理
}
}
キーポイント:
isAccelerometerSupportedとisDeviceMotionSupportedでデバイス対応を確認する- Apple Watch Series 8 と Ultra のみ対応
- データ取得にはアクティブな HealthKit Workout が必須
- Swift async/await インターフェイスでバッチ単位にデータを処理する
- ループ内で workout が終了したか確認し、無限ループを避ける
スイング分析アルゴリズムの実装
(17:08)講演者は、高周波データを使って「インパクトまでの時間」(time to contact)を計算する方法を示しました。
ステップ 1: インパクト瞬間を検出
func detectImpact(from batch: [CMAccelerometerData]) -> TimeInterval? {
// z 軸は Digital Crown 方向に垂直で、インパクト時に明確な振動が発生する
let filteredZ = batch.map { highPassFilter($0.acceleration.z) }
// フィルタ後の信号のピークを見つける
guard let maxIndex = filteredZ.enumerated().max(by: { $0.element < $1.element })?.offset else {
return nil
}
// 元データから対応するタイムスタンプを取得
return batch[maxIndex].timestamp
}
キーポイント:
- z 軸方向は Apple Watch の Digital Crown に垂直
- ハイパスフィルタで高周波の衝撃信号を分離する
- ピークがインパクト瞬間に対応する
ステップ 2: スイング開始を検出
func detectSwingStart(impactTime: TimeInterval, motionBuffer: [CMDeviceMotion]) -> TimeInterval? {
// impactTime から前方へたどり、回転速度の重力方向成分を探す
// しきい値超過からしきい値未満へ変わる転換点
for sample in motionBuffer.reversed() {
// impactTime より前のサンプルだけを調べる
guard sample.timestamp < impactTime else { continue }
// スイング継続時間は妥当な範囲(例: 500ms)を超えないはず
guard impactTime - sample.timestamp < 0.5 else { break }
// 回転速度の重力方向成分を計算
let rotationAlongGravity =
sample.rotationRate.x * sample.gravity.x +
sample.rotationRate.y * sample.gravity.y +
sample.rotationRate.z * sample.gravity.z
// しきい値超過からしきい値未満への変化点を探す
if abs(rotationAlongGravity) < swingThreshold {
return sample.timestamp
}
}
return nil
}
キーポイント:
- impactTime から前方へデバイスモーションデータをたどる
- 回転速度の重力方向成分は、手首が体の周りを回る動きを反映する
- スイング中はこの成分が明確に非ゼロになり、静止時はゼロに近い
- 妥当なスイング継続時間の上限を設定してノイズを除去する
ステップ 3: 接触時間を計算
func computeTimeToContact(impactTime: TimeInterval, swingStartTime: TimeInterval?) -> TimeInterval? {
guard let startTime = swingStartTime else { return nil }
// スイング中の累積回転角度が妥当な範囲にあるか検証
let accumulatedRotation = computeAccumulatedRotation(from: startTime, to: impactTime)
guard accumulatedRotation > minRotationThreshold && accumulatedRotation < maxRotationThreshold else {
return nil
}
return impactTime - startTime
}
キーポイント:
- 接触時間 = インパクト瞬間のタイムスタンプ - スイング開始のタイムスタンプ
- 累積回転角度によって、検出したスイングが有効か検証する
- 手を振る、姿勢を調整するなど、スイングではない動作の誤検出を除去する
ヒット時と空振り時の信号差
(20:45)講演者は、ボールに当たった場合と空振りした場合の加速度計信号を比較しました。どちらの場合もスイング動作自体は似ていますが、インパクト瞬間の振動パターンは明確に異なります。ヒット時は鋭い衝撃ピークが生じ、空振り時は振動がよりなだらかです。800Hz データストリームならこの差を捉えられますが、100Hz では判別が困難です。
重要ポイント
1. ヘッドトラッキングで駆動する空間オーディオゲーム
- 何をするか: AirPods の頭部姿勢データでゲーム視点やメニュー選択を制御する
- 取り組む価値: ユーザーは手持ちデバイスなしで頭を向けるだけで操作でき、フィットネスや VR 系体験に向く
- 始め方:
CMHeadphoneMotionManagerでattitudeデータを取得し、ゲームカメラのオイラー角にマッピングする
2. ウォータースポーツ記録 App
- 何をするか: シュノーケリングやフリーダイビングの愛好者向けに、深度曲線、水温変化、水中時間を記録する
- 取り組む価値: Apple Watch Ultra の
CMWaterSubmersionManagerはネイティブの水深・水温 API を提供し、外部センサーが不要 - 始め方: “Shallow Depth and Pressure” capability を構成し、delegate で深度と温度更新を受け取り、HealthKit Workout に重ねる
3. 運動フォーム分析ツール
- 何をするか: 800Hz 加速度計と 200Hz デバイスモーションデータで、ゴルフスイングやテニスサーブなどの動作を分析する
- 取り組む価値: 高周波データなら、100Hz では判別できない微細な動作差、例えばインパクト瞬間のヘッドスピードや手首の返り角を捉えられる
- 始め方: HealthKit Workout を開始し、
CMBatchedSensorManagerの async インターフェイスでデータをバッチ取得し、特定スポーツ向けの検出アルゴリズムを設計する
4. フィットネス動作のカウントと品質評価
- 何をするか: 腕立て伏せやスクワットなどの完了回数を自動認識し、動作品質を評価する
- 取り組む価値: ヘッドトラッキングデータで身体姿勢を判断でき、高周波加速度計で動作のリズムと振幅を検出できる
- 始め方: AirPods が頭部姿勢の基準を提供し、
CMBatchedSensorManagerが手首の動作詳細を提供するため、両者を組み合わせて全身運動モデルを構築する
関連セッション
- Build a multi-device workout app — Apple Watch と iPhone を連携させてワークアウト App を構築する流れ
- Building custom workouts with WorkoutKit — WorkoutKit のカスタムトレーニング構造と CMBatchedSensorManager との組み合わせ
- What’s new in HealthKit — HealthKit の新機能、運動データの保存と共有
- Meet Core ML Tools — 高周波センサーデータを機械学習モデルのトレーニングに使う
コメント
GitHub Issues · utterances