Upgrade to Pro — share decks privately, control downloads, hide ads and more …

空間オーディオで過去の 自分(ゴースト)と競うランニング 〜HealthKitのルートを足音...

Avatar for Nao-RandD Nao-RandD
September 12, 2026

空間オーディオで過去の 自分(ゴースト)と競うランニング 〜HealthKitのルートを足音に変える実装〜

iOSDC Japan 2026 の発表スライド
https://fortee.jp/iosdc-japan-2026/proposal/5324591d-3a1e-4aff-ba39-353224d3ea28

---
趣味でランニングをしています。

同じコースを何度も走っていると、ふと過去の自分と競いたいと思い立ちました。
先月の自分は、この坂をもっと速く登っていたはず……。けれど走っている最中は、その過去の自分が今どこにいるのか見ることはできません。iPhone や Apple Watch の画面の数字を常に覗くのは、臨場感が不足し何より危険です。そこで、過去の自分を「数字」ではなく「足音」として、前後の空間に置くという発想に至りました。

本トークでは、過去のランを HealthKit から読み出してゴースト(一緒に走る過去の自分の記録)として再構築し、そこに空間オーディオを当てるまでの実装を紹介します。

・HKWorkoutRouteQuery によるルートの読み出しと「累積距離 → 経過時間」テーブルへの前計算
・現在の自分との秒差の算出と、前後の位置への変換
・AVAudioEngine と AVAudioEnvironmentNode を使ったゴーストの足音の空間配置

リードすれば足音は背後から、離されれば前方から聞こえてきます。
過去の自分を、画面ではなく耳で感じる。そんなランニング体験を一緒に作ってみましょう。
---

Avatar for Nao-RandD

Nao-RandD

September 12, 2026

More Decks by Nao-RandD

Other Decks in Technology

Transcript

  1. 自己紹介 • 名前 :菅 直之(Naoyuki Kan) • 所属 :株式会社ヤプリ •

    X :Nao-RandD(ナオランド) • 備考 : 趣味でランニングをしています🏃
  2. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする ゴーストの元データは

    HealthKit から作成する HealthKit とは何か? • Appleデバイス間で健康・フィットネスデータを共有するためのデータ基盤 • 過去のワークアウト記録の読み出しや書き込みを行える … HealthKit:https://developer.apple.com/documentation/healthkit
  3. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする ゴーストの元データは

    HealthKit から作成する HealthKit とは何か? • Appleデバイス間で健康・フィットネスデータを共有するためのデータ基盤 • 過去のワークアウト記録の読み出しや書き込みを行える 1回のランニング = 1回のワークアウト • ルート:HKWorkoutRoute 緯度・経度 + 時刻 の配列 → いつ、どこにいたか • 他に歩数 / 心拍 / 開始時間 など … HealthKit:https://developer.apple.com/documentation/healthkit
  4. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする ゴーストの元データは

    HealthKit から作成する HealthKit とは何か? • Appleデバイス間で健康・フィットネスデータを共有するためのデータ基盤 • 過去のワークアウト記録の読み出しや書き込みを行える 1回のランニング = 1回のワークアウト • ルート:HKWorkoutRoute 緯度・経度 + 時刻 の配列 → いつ、どこにいたか • 他に歩数 / 心拍 / 開始時間 など … HealthKit:https://developer.apple.com/documentation/healthkit
  5. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする ゴーストの元データは

    HealthKit から作成する 必要なデータは以下の三つ 1. 経過時間 2. 緯度経度 3. 走行距離 struct GhostRoutePoint: Sendable { let elapsedSeconds: TimeInterval let coordinate: RouteCoordinate let distanceMeters: Double } 時間から走行距離や 緯度経度を把握できる状態
  6. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得する 過去のワークアウトからルート情報が得られる // ワークアウトに紐づくルート本体(HKWorkoutRoute)を探索 let routeDescriptor = HKSampleQueryDescriptor( predicates: [.workoutRoute( HKQuery.predicateForObjects(from: workout) )], sortDescriptors: [SortDescriptor(\.startDate, order: .forward)] ) let routes = try await routeDescriptor.result(for: healthStore) var locations: [CLLocation] = [] for route in routes { let descriptor = HKWorkoutRouteQueryDescriptor(route) // 1点ごとにtimestampがついた緯度経度を取得 } for try await location in descriptor.results(for: healthStore) { locations.append(location) } 緯度経度 経過時間 35.6812, 139.7671 0.0 s 35.6813, 139.7676 1.8 s … …
  7. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得 取得できるデータは緯度経度と経過時間で「走行距離」がない 点を距離に変換 緯度経度 経過時間 35.6812, 139.7671 0.0 s 35.6813, 139.7676 1.8 s … … 隣り合う点と点の距離を 足し合わせることで求まる
  8. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得 点を距離に変換 ・・・ var cumulativeMeters = 0.0 // 先頭からの走行距離 var points: [GhostRoutePoint] = [] CLLocation.distance(from:)で 点と点の距離求めて足し合わせる for (index, location) in locations.enumerated() { if index > 0 { // 2点間の距離を足し合わせる cumulativeMeters += location.distance(from: locations[index - 1]) } // 経過時間の算出 let elapsedSeconds = location.timestamp.timeIntervalSince(first.timestamp) points.append( GhostRoutePoint( elapsedSeconds: max(0, elapsedSeconds), // 開始は0秒 coordinate: location.coordinate, // 緯度経度 distanceMeters: cumulativeMeters // 走行距離 ) } ・・・ )
  9. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得 取得できるデータ ゴーストに必要なデータ 緯度経度 経過時間 緯度経度 経過時間 走行距離 35.6812, 139.7671 0.0 s 35.6812, 139.7671 0.0 s 0m 35.6813, 139.7676 1.8 s 35.6813, 139.7676 1.8 s 5.1 m … … … … …
  10. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得 ゴーストに必要なデータ 緯度経度 経過時間 走行距離 struct Ghost { // 経過時間, 緯度経度, 走行距離のデータ群 35.6812, 139.7671 0.0 s 0m let points: [GhostRoutePoint] 35.6813, 139.7676 1.8 s 5.1 m ・・・ // 時刻, 距離, 緯度経度を変換するメソッド … … … }
  11. ① 過去の記録から ゴーストを作る ② ゴーストと自分との ③ ゴーストの位置を 「前後の差」がわかる 「足音」にする HKWorkoutRoute

    からデータを取得 ゴーストに必要なデータ 緯度経度 経過時間 走行距離 struct Ghost { // 経過時間, 緯度経度, 走行距離のデータ群 35.6812, 139.7671 0.0 s 0m let points: [GhostRoutePoint] 35.6813, 139.7676 1.8 s 5.1 m ・・・ // 時刻, 距離, 緯度経度を変換するメソッド … … … } ✅ 過去の記録からゴーストを作る
  12. 􀐳􀐳 ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    次は現在の走行距離を取得して ゴーストと自分の「前後の差」を測れるようにする
  13. ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 前後差に必要なのは、経過時間と走行距離

    同じ時間に自分とゴーストがそれぞれ何m地点だったかを測れる状態 現在の自分を (経過時間, 走行距離) の1点として表現 struct LiveProgress { let distance: Double // スタートからの走行距離 let elapsedSeconds: Double // タイマーによる経過時刻 }
  14. 􂘒 ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    前後差に必要なのは、経過時間と走行距離 同じ時間に自分とゴーストがそれぞれ何m地点だったかを測れる状態 現在の自分を (経過時間, 走行距離) の1点として表現 struct LiveProgress { 走行距離の取得は Core Motion から取得する let distance: Double // スタートからの走行距離 let elapsedSeconds: Double // タイマーによる経過時刻 } Core Motion:https://developer.apple.com/documentation/coremotion
  15. 􂘒 ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    Core Motion とは? 端末の動きに関するセンサー情報とOSが処理した活動データを扱う • CMPedometer から OS が処理した歩行・走行データを取得できる • 歩数、ペース、ケイデンスなど • 今回利用するのはスタートからの累積の推定距離 CMPedometer:https://developer.apple.com/documentation/coremotion/cmpedometer
  16. 􀋒 􂘒 ① 過去の記録から ゴーストを作る ③ ゴーストの位置を ② ゴーストと自分との 「前後の差」がわかる

    「足音」にする Core Motion と Core Location の役割分担 走行距離は 現在の位置測位に依存せず更新できる Core Motion を使う Core Motion Core Location 得られるもの スタートからの走行距離 緯度・経度・進行方向 更新の特徴 OSが処理した距離を継続的に取得 利用環境や設定で精度・更新間隔が変化 消費電力 比較的軽い 高頻度・高精度では大きくなりやすい 判断軸
  17. 􂘒􀋒 ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    Core Motion と Core Location の役割分担 Core Motion 走行距離 Core Location 緯度経度 ・ 進行方向 ゴーストとの「前後の差」 ゴーストの「左右位置」(後述) HKWorkoutRouteとして保存 次回は過去のゴーストとして読み出す
  18. ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする Core

    Motion から現在の走行距離を取得する CMPedometer で開始地点からの走行距離を取得できる func startLiveProgressUpdates( pedometer: CMPedometer, startedAt: Date, elapsedProvider: @escaping () -> TimeInterval, onUpdate: @escaping (LiveProgress) -> Void) { guard CMPedometer.isDistanceAvailable() else { return } pedometer.startUpdates(from: startedAt) { data, error in guard error == nil, let distance = data?.distance?.doubleValue else { return } // 更新される度にゴーストの走行距離と比較する onUpdate( LiveProgress( elapsedSeconds: elapsedProvider(), // 自前のタイマーから経過時間を取得 } } ) ) distance: max(0, distance) CMPedometer:https://developer.apple.com/documentation/coremotion/cmpedometer
  19. ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする Core

    Motion から現在の走行距離を取得する CMPedometer で開始地点からの走行距離を取得できる 経過時間ごとの現在の走行距離が取得できた 次はゴーストのデータを取得する
  20. ① 過去の記録から ② ゴーストと自分との 「前後の差」がわかる ゴーストを作る ③ ゴーストの位置を 「足音」にする ゴーストのデータは

    経過時間 から 走行距離 が取得できる 緯度経度 経過時間 走行距離 35.6812, 139.7671 0.0 s 0m 35.6813, 139.7676 1.8 s 5.1 m … … … 35.6825, 139.7721 150 s 482 m 時間から距離 150秒 → 482m 同じ時間でどこまで進んだか // 経過時間から走行距離を取得 let ghostDistance = ghost.distance( atTime: live.elapsedSeconds )
  21. ① 過去の記録から ② ゴーストと自分との 「前後の差」がわかる ゴーストを作る ③ ゴーストの位置を 「足音」にする ゴーストのデータは

    経過時間 から 走行距離 が取得できる 緯度経度 経過時間 走行距離 35.6812, 139.7671 0.0 s 0m 35.6813, 139.7676 1.8 s 5.1 m … … … 35.6825, 139.7721 150 s 482 m 時間から距離 150秒 → 482m 同じ時間でどこまで進んだか // 経過時間から走行距離を取得 let ghostDistance = ghost.distance( atTime: live.elapsedSeconds ) 同じ経過時間でゴーストの走行距離が取得できた
  22. ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする ゴーストとの走行距離を比較できる

    現在の経過時間で自分とゴーストの走行距離を引けば「前後の差」 // 同じ経過時間で走行距離を比較。正ならコース上でゴーストが前にいる let courseFowardGap = ghostDistance - live.distance
  23. ① 過去の記録から ゴーストを作る ② ゴーストと自分との 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする ゴーストとの走行距離を比較できる

    現在の経過時間で自分とゴーストの走行距離を引けば「前後の差」 // 同じ経過時間で走行距離を比較。正ならコース上でゴーストが前にいる let courseFowardGap = ghostDistance - live.distance ✅ ゴーストと自分との「前後の差」がわかる
  24. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 走行距離だと前後の距離はわかっても左右がわからない

    前後だけでは空間オーディオの体験として不十分 前後だけ 前後左右 ゴースト 自分 体を貫く 自分 ゴースト 身体の横を通りすぎる
  25. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする ゴーストが進行方向に対して左右のどこにいるかを取得する

    以下の4ステップ 1. 現在の緯度経度 2. 現在の進行方向 3. 同じ経過時間にいるゴーストの緯度経度 4. 緯度経度の差を、進行方向から見た左右の位置(m)へ変換
  26. 􀋒 ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    ゴーストが進行方向に対して左右のどこにいるかを取得する 以下の4ステップ 1. 現在の緯度経度 Core Location から把握する 2. 現在の進行方向 3. 同じ経過時間にいるゴーストの緯度経度 4. 緯度経度の差を、進行方向から見た左右の位置(m)へ変換
  27. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 現在の位置情報は

    Core Location から取得する CLLocationUpdate から位置情報を継続的に取得 • 現在の緯度経度を取得 let updates = CLLocationUpdate.liveUpdates(.fitness) for try await update in updates { guard let location = update.location else { continue } // 現在位置の緯度・経度 currentCoordinate = location.coordinate // HealthKit のルートへ保存する処理など ・・・ // 現在の移動方向(負であればエラー) } guard location.course >= 0 else { continue } courseDegrees = location.course liveUpdates(̲:):https://developer.apple.com/documentation/corelocation/cllocationupdate/liveupdates(̲:)
  28. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 現在の位置情報は

    Core Location から取得する CLLocation.course*から進行方向を取得 • 北を0 とした進行方向を取得 let updates = CLLocationUpdate.liveUpdates(.fitness) for try await update in updates { guard let location = update.location else { continue } // 現在位置の緯度・経度 currentCoordinate = location.coordinate // HealthKit のルートへ保存する処理など ・・・ // 現在の移動方向(負であればエラー) *端末が向いている方向ではなく、実際に移動している方向 } guard location.course >= 0 else { continue } courseDegrees = location.course CLocation.course:https://developer.apple.com/documentation/corelocation/cllocation/course
  29. 􀐳􀐳 ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    現在の位置情報は Core Location から取得する CLLocation.course*から進行方向を取得 • 北を0 とした進行方向を取得 北(0°) 西(270°) 東(90°) 現在位置 45 (北東方向) 南(180°) 前回位置 *端末が向いている方向ではなく、実際に移動している方向 CLocation.course:https://developer.apple.com/documentation/corelocation/cllocation/course
  30. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 現在の位置情報は

    Core Location から取得する CLLocation.course から進行方向を取得 • 北を0 とした進行方向を取得 現在の進行方向と緯度経度が取得できた
  31. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする ゴーストが進行方向に対して左右のどこにいるかを取得する

    以下の4ステップ 1. 現在の緯度経度 2. 現在の進行方向 3. 同じ経過時間にいるゴーストの緯度経度 4. 緯度経度の差を、進行方向から見た左右の位置(m)へ変換
  32. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 同じ経過時間にいるゴーストの緯度経度

    ゴーストのデータは 経過時間 から 緯度経度 が取得できる 緯度経度 経過時間 走行距離 35.6812, 139.7671 0.0 s 0m 35.6813, 139.7676 1.8 s 5.1 m … … … 35.6825, 139.7721 150 s 482 m 時間から緯度経度 150秒 → (35.6825, 139.7721) 同じ時間に地図上のどこにいたか // 経過時間から緯度経度を取得 let ghostCoordinate = ghost.coordinate( atTime: live.elapsedSeconds )
  33. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする ゴーストが進行方向に対して左右のどこにいるかを取得する

    以下の4ステップ 1. 現在の緯度経度 2. 現在の進行方向 3. 同じ経過時間にいるゴーストの緯度経度 4. 緯度経度の差を、進行方向から見た左右の位置(m)へ変換
  34. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 緯度経度の差を、進行方向から見た左右の位置(m)へ変換

    地図の北・東の位置を、走者の前・右へ変換する 地図座標 走者座標 北 前 進行方向 ゴースト ゴースト 東 自分 右 走者の前・右へ変換 自分
  35. ↑ ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    緯度経度の差を、進行方向から見た左右の位置(m)へ変換 まず地図上の東・北の距離(m)へ変える let radians = Double.pi / 180 let earthRadius = 6_371_000.0 // 地球の半径 // 度からラジアンへ変換 let latitude = origin.latitude * radians let latitudeDelta = (target.latitude - origin.latitude) * radians let longitudeDelta = (target.longitude - origin.longitude) * radians // 地図上の東・北の距離(m) let eastMeters = longitudeDelta * cos(latitude) * earthRadius let northMeters = latitudeDelta * earthRadius
  36. ↑ ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    緯度経度の差を、進行方向から見た左右の位置(m)へ変換 まず地図上の東・北の距離(m)へ変える let radians = Double.pi / 180 let earthRadius = 6_371_000.0 // 地球の半径 // 度からラジアンへ変換 let latitude = origin.latitude * radians let latitudeDelta = (target.latitude - origin.latitude) * radians let longitudeDelta = (target.longitude - origin.longitude) * radians // 地図上の東・北の距離(m) let eastMeters = longitudeDelta * cos(latitude) * earthRadius let northMeters = latitudeDelta * earthRadius 単位円
  37. ↑ ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    緯度経度の差を、進行方向から見た左右の位置(m)へ変換 まず地図上の東・北の距離(m)へ変える 単位円 let radians = Double.pi / 180 let earthRadius = 6_371_000.0 // 地球の半径 // 度からラジアンへ変換 let latitude = origin.latitude * radians let latitudeDelta = (target.latitude - origin.latitude) * radians let longitudeDelta = (target.longitude - origin.longitude) * radians // 地図上の東・北の距離(m) let eastMeters = longitudeDelta * cos(latitude) * earthRadius let northMeters = latitudeDelta * earthRadius 地球の半径
  38. ↑ ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    緯度経度の差を、進行方向から見た左右の位置(m)へ変換 まず地図上の東・北の距離(m)へ変える let radians = Double.pi / 180 let earthRadius = 6_371_000.0 // 地球の半径 // 度からラジアンへ変換 let latitude = origin.latitude * radians let latitudeDelta = (target.latitude - origin.latitude) * radians let longitudeDelta = (target.longitude - origin.longitude) * radians // 地図上の東・北の距離(m) let eastMeters = longitudeDelta * cos(latitude) * earthRadius let northMeters = latitudeDelta * earthRadius ゴーストが地図上で東・北のどの距離にいるかわかった
  39. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 緯度経度の差を、進行方向から見た左右の位置(m)へ変換

    北を進行方向に合わせると、ゴーストが右側か左側か分かる ✅ 地図座標 北 走者座標 前 進行方向 ゴースト ゴースト 東 自分 右 走者の前・右へ変換 自分
  40. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 緯度経度の差を、進行方向から見た左右の位置(m)へ変換

    北を進行方向に合わせると、ゴーストが右側か左側か分かる // CLLocation,courseは度なのでラジアンへ変換 let course = courseDegrees * .pi / 180 // 進行方向から見た左右の距離(単位 m) let rightMeters = eastMeters * cos(course) - northMeters * sin(course)
  41. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 緯度経度の差を、進行方向から見た左右の位置(m)へ変換

    北を進行方向に合わせると、ゴーストが右側か左側か分かる // CLLocation,courseは度なのでラジアンへ変換 let course = courseDegrees * .pi / 180 // 進行方向から見た左右の距離(単位 m) let rightMeters = eastMeters * cos(course) - northMeters * sin(course) 進行方向に対するゴーストの左右の距離がわかった
  42. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 進行方向に対するゴーストの前後左右の位置

    Core Motion で「前後」、 Core Location で「左右」 // Core Motionから求めた前後(単位 m) let ghostDistance = ghost.distance(atTime: elapsedSeconds) let forwardDistance = ghostDistance - liveDistance // Core Locationから前述の計算から求めた左右(単位 m) let rightDistance = rightDistance( from: liveLocation.coordinate, to: ghostCoordinate, courseDegrees: liveLocation.course ) 進行方向に対するゴーストの前後左右の位置がわかった
  43. 􀐳􀐳􀪷􁀨􀙱 ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    ゴーストの位置を、聞こえる位置へ変える ここまでに求めた「右・前」の位置を、足音の3D座標として配置する
  44. 􁀨 􀪷 􀐳􀙱 ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③

    ゴーストの位置を 「足音」にする 足音を3D空間で感じられるオーディオにする 単に足音を再生するだけでは、前後左右から聞こえるように感じない AVAudioEngine の AVAudioEnvironmentNode を利用する AVAudioPlayerNode AVAudioEnvironmentNode 足音を再生する 方向と距離を音へ反映する 足音 イヤホン AVFAudio:https://developer.apple.com/documentation/avfaudio AVAudioEnvironmentNode:https://developer.apple.com/documentation/avfaudio/avaudioenvironmentnode
  45. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 空間オーディオで決めるのは「どんな音をどこから聞くか」

    • 聞き手 :現在の自分。座標の原点に固定 // 聞き手である自分は、常に原点 • 足音 :1点から鳴る音として扱う • 聞こえ方:前後左右の方向感を作る environment.listenerPosition = AVAudio3DPoint(x: 0, y: 0, z: 0) // 足音を1つの位置から鳴る音として扱う player.sourceMode = .pointSource // イヤホンで方向を感じられるようにする player.renderingAlgorithm = .HRTFHQ HRTF:左右の耳へ届く音の違いを再現し、音が来る方向を感じさせる仕組み。 HRTFHQ は仕組みを高品質に引き上げたもの。 pointSource:https://developer.apple.com/documentation/avfaudio/avaudio3dmixingsourcemode/pointsource HRTFHQ:https://developer.apple.com/documentation/avfaudio/avaudio3dmixingrenderingalgorithm/hrtfhq
  46. 􀪷􀙱􀐳􀐳 ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする

    走者に対するゴーストの位置を空間オーディオに配置 前後左右(m)の位置を AVAudioPlayerNode の座標に指定 // Core Motionから求めた前後(単位 m) let forwardDistance = ghostDistance - liveDistance // Core Locationから前述の計算から求めた左右(単位 m) let rightDistance = rightDistance( from: liveLocation.coordinate, to: ghostCoordinate, courseDegrees: liveLocation.course ) // Playerの音源位置を設定 player.position = AVAudio3DPoint( x: Float(rightDistance), // 右が +X ) y: 0, // 高さは固定(地面) z: Float(-forwardDistance) // 前が -Z
  47. ① 過去の記録から ② ゴーストと自分との ゴーストを作る 「前後の差」がわかる ③ ゴーストの位置を 「足音」にする 1回の更新ごとに距離差が空間オーディオの「足音」になる

    Core Motion // Core Motionから求めた前後(単位 m) let forwardDistance = ghostDistance - liveDistance 前後:走行距離の差 // Core Locationから前述の計算から求めた左右(単位 m) let rightDistance = rightDistance( from: liveLocation.coordinate, to: ghostCoordinate, courseDegrees: liveLocation.course ) CoreLocation 左右:進行方向に対してどちら側か // Playerの音源位置を設定 player.position = AVAudio3DPoint( x: Float(rightDistance), // 右が +X ) y: 0, // 高さは固定(地面) z: Float(-forwardDistance) // 前が -Z AVFAudio 足音:空間オーディオを座標に配置
  48. まとめ • HealthKit にあるワークアウトを空間上の「足音」にした • 前後は走行距離(Core Motion)、左右は緯度経度(Core Location)で把握 足音は AVFAudio

    の 空間オーディオで表現した • 「実装は AI、実走は私」 • コードは AI と書けても、聞き心地は自分で走るしかない
  49. まとめ • HealthKit にあるワークアウトを空間上の「足音」にした • 前後は走行距離(Core Motion)、左右は緯度経度(Core Location)で把握 足音は AVFAudio

    の 空間オーディオで表現した • 「実装は AI、実走は私」 実装して、走ったその距離 • コードは AI と書けても、聞き心地は自分で走るしかない 115.3 km
  50. まとめ • HealthKit にあるワークアウトを空間上の「足音」にした • 前後は走行距離(Core Motion)、左右は緯度経度(Core Location)で把握 体験向上のために 様々な工夫を入れたりしてます

    足音は AVFAudio の 空間オーディオで表現した Ask the Speaker、または • 「実装は AI、実走は私」 会場で声をかけてください • コードは AI と書けても、聞き心地は自分で走るしかない TestFlight もお渡しできます