Core Haptics - カスタムハプティックパターンの作成と再生

Core Hapticsは、iOS 13で新たに追加された「ハプティック（触覚）パターンを作成し、再生する」ためのフレームワークです。ついにTaptic Engineを開発者が制御するためのAPIが公開されたというわけです。[^taptic]

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/c0521d81-3c29-2339-df2c-2125a9e0067f.jpeg" width="80%" title="Taptic Engine（画像はiFixitより引用）">


[^taptic]: Core Hapticsはオーディオによるフィードバックも扱うので、厳密にはTaptic Engineを制御するだけではありません。

## 従来手法（UIFeedbackGenerator）との違い

Taptic Engineが初めて搭載されたのはiPhone 6sで、ハプティックパターンの再生自体は、iOS 10で追加された`UIFeedbackGenerator`により以前から可能でした。

`UIFeedbackGenerator`は抽象クラスで、`UIImpactFeedbackGenerator`, `UINotificationFeedbackGenerator`, `UISelectionFeedbackGenerator`の3種類の具象クラスがあります。それぞれの実装例を以下に示します。

- `UIImpactFeedbackGenerator`

```swift
let impactFeedbacker = UIImpactFeedbackGenerator(style: .heavy)
impactFeedbacker.prepare()
impactFeedbacker.impactOccurred()
```

- `UINotificationFeedbackGenerator`

```swift
let notificationFeedbacker = UINotificationFeedbackGenerator()
notificationFeedbacker.notificationOccurred(.success)
```

- `UISelectionFeedbackGenerator`

```swift
let selectionFeedbacker = UISelectionFeedbackGenerator()

@IBAction func sliderChanged(_ sender: UISlider) {
    selectionFeedbacker.selectionChanged()
}
```

上記のコード例からもわかる通り、いずれの場合も**あらかじめ決められたタイプのハプティックパターンを再生する**というものでした。

Core Hapticsの場合は、この**ハプティックパターンをカスタマイズできる**点が従来手法と大きく違う点です。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/e3f24fda-e284-8a75-3047-f344f30da2bc.png "Core Hapticsの位置付け")


## Core Hapticsの実装

まずはCore Hapticsの全体感を掴むため、基本的な実装の流れを見てみましょう。

#### 1. インポート

CoreHapticsをインポートします。

```swift
import CoreHaptics
```


#### 2. エンジンをスタートする

エンジン（`CHHapticEngine`）を初期化し、

```swift
let engine = try! CHHapticEngine()
```

`start()`メソッドを呼んでスタートします。

```swift
try! engine.start()
```

#### 3. ハプティックパターンを生成する

ハプティックイベント（`CHHapticEvent`）を生成し（詳細は後述）、

```swift
let audioEvent = CHHapticEvent(eventType: .audioContinuous, parameters: [
    CHHapticEventParameter(parameterID: .audioPitch, value: -0.15),
    CHHapticEventParameter(parameterID: .audioVolume, value: volume),
    CHHapticEventParameter(parameterID: .decayTime, value: decay),
    CHHapticEventParameter(parameterID: .sustained, value: 0)
], relativeTime: 0)

let hapticEvent = CHHapticEvent(eventType: .hapticTransient, parameters: [
    CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness),
    CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity)
], relativeTime: 0)
```

複数のハプティックイベントを組み合わせてパターン（`CHHapticPattern`）を生成します。

```swift
let pattern = 
    try! CHHapticPattern(events: [audioEvent, hapticEvent], parameters: [])
```

#### 4. ハプティックパターンを再生する

`CHHapticEngine`の`makePlayer(with:)`メソッドにパターンを渡して、プレイヤー（`CHHapticPatternPlayer`）オブジェクトを生成します。

```swift
let player = try! engine.makePlayer(with: pattern)
```

`CHHapticPatternPlayer`の`start(atTime)`メソッドを呼んでハプティックを再生します。

```swift
try! player.start(atTime: CHHapticTimeImmediate)
```



## ハプティックイベント（CHHapticEvent）

Core Hapticsとはカスタムハプティックパターンをつくって再生できるフレームワークです。そして、そのパターン（`CHHapticPattern`）は複数のハプティックイベント（`CHHapticEvent`）から構成され[^dynamicparam]、

<img width="1247" alt="hapticpattern.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/1c09555a-99df-599b-df10-1b4d0a250b44.png">

ハプティックイベントは発生タイミング、イベントタイプ、長さ、そして複数のイベントパラメータ（`CHHapticEventParameter`）より規定されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/7e747713-953a-b8f8-3c3b-7007f033a66a.png" width="40%">


つまり、カスタムなハプティックパターンをつくる重要な構成要素である`CHHapticEvent`を規定する**各種プロパティと**`CHHapticEventParameter`**を理解することがCore Hapticsを使いこなす鍵**となります。

[^dynamicparam]: 実際にはハプティックパターンを構成する要素としては他に`CHHapticParameterCurve`や`CHHapticDynamicParameter`といったものがあります。


というわけで、以下で`CHHapticEvent`を規定する各種要素について順番に解説していきます。


### ハプティックイベントタイプ（CHHapticEvent.EventType）

`CHHapticEvent`は次のようなイニシャライザを持ち、第1引数にイベントタイプ（`CHHapticEvent.EventType`）を渡せるようになっています。

```swift
init(eventType type: CHHapticEvent.EventType, 
     parameters eventParams: [CHHapticEventParameter], 
     relativeTime time: TimeInterval)
```

これはその名の通りハプティックイベントのタイプを決めるもので、次の4種類が定義されています。

```swift
static let audioContinuous: CHHapticEvent.EventType
static let audioCustom: CHHapticEvent.EventType
static let hapticTransient: CHHapticEvent.EventType
static let hapticContinuous: CHHapticEvent.EventType
```

`audio〜`はオーディオによるフィードバック、`haptic〜`は触覚によるフィードバックです。

hapticタイプの方は"Transient"と"Continuous"とがありますが、"Transient"とは「一時的な」という意味で、`hapticTransient`は短いインパルス的なハプティックを、`hapticContinuous`は任意の長さを持ち、ループするハプティックを示します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/e064af60-0f3c-9fd7-7205-e52f7f9b97c4.png" width="70%">



### CHHapticEventの発生タイミング（relativeTime）

上述した`CHHapticEvent`のイニシャライザは、第3引数にイベントを開始する時間（`relativeTime`）を指定できるようになっています。

名前に"relative"とある通り、絶対時間ではなく、ハプティックパターン内における相対時間（単位は秒）で指定します。

```swift
let hapticEvent = CHHapticEvent(
    eventType: type, parameters:params, 
    relativeTime: 0.1)  // 0.1秒後に開始
```

### CHHapticEventの長さ（duration）

`CHHapticEvent`はハプティックイベントの長さを指定する`duration`プロパティを持ちます。

```swift
var duration: TimeInterval
```

Continuousなイベントタイプの場合は、本プロパティに`0.0`より大きい値をセットしておかないと再生時にクラッシュします。[^sustained]

[^sustained]: ただし、`sustained`パラメータにfalse(`0.0`)をセットしておくと、Continuousなイベントタイプに対して`duration`が`0.0`でも再生可能です。

最大値は30秒です。

## イベントパラメータ（CHHapticEventParameter）

前述の`CHHapticEvent`のイニシャライザの第2引数に配列で渡すのが、イベントパラメータ（`CHHapticEventParameter`）です。

```swift
let audioEvent = CHHapticEvent(
    eventType: type, 
    parameters: [param1, param2],   // [CHHapticEventParameter]
    relativeTime: 0)
```

イニシャライザは次のように定義されており、

```swift
init(parameterID: CHHapticEvent.ParameterID, value: Float)
```

第1引数にパラメータの種類を示すID（`CHHapticEvent.ParameterID`）、第2引数にパラメータの値（`Float`）を渡します。

```swift
let pitch  = CHHapticEventParameter(parameterID: .audioPitch, value: -0.15)
let volume = CHHapticEventParameter(parameterID: .audioVolume, value: 0.5)
```
`CHHapticEvent.ParameterID`には多くの種類があるので、ここでは主なものを紹介します。

#### haptic用

イベントタイプが`hapticTransient`, `hapticContinuous`なハプティックイベント専用のパラメータIDが次の2つです。

```swift
static let hapticIntensity: CHHapticEvent.ParameterID
static let hapticSharpness: CHHapticEvent.ParameterID
```

- `hapticIntensity`：ハプティックの強さを`0.0`〜`1.0`で指定
- `hapticSharpness`：ハプティックの鋭さを`0.0`〜`1.0`で指定


#### audio用

イベントタイプが`audioContinuous`, `audioCustom`なハプティックイベント専用のパラメータIDが次の4つです。

```swift
static let audioVolume: CHHapticEvent.ParameterID
static let audioPitch: CHHapticEvent.ParameterID
static let audioPan: CHHapticEvent.ParameterID
static let audioBrightness: CHHapticEvent.ParameterID
```

- `audioVolume`：音量を`0.0`〜`1.0`で指定
- `audioPitch`：音のピッチを`-1.0`（低い）〜`1.0`（高い）で指定
- `audioPan`：音の位置（定位）を`-1.0`（左）〜`1.0`（右）で指定。デフォルトは`0.0`（中央）
- `audioBrightness`：音の高周波成分を`0.0`〜`1.0`で指定。デフォルトは`1.0`（高周波成分を減らさない）

#### 共通

audio, haptic共通で"Continuous"なイベントタイプに使えるパラメータのIDとして、次のようなものがあります。それぞれハプティックイベントの波形を細かく調整するためのパラメータです。

```swift
static let attackTime: CHHapticEvent.ParameterID
static let decayTime: CHHapticEvent.ParameterID
static let releaseTime: CHHapticEvent.ParameterID
static let sustained: CHHapticEvent.ParameterID
```


## AHAP

AHAP (Apple Haptic and Audio Pattern)はハプティックパターンを定義するJSONライクなファイルフォーマットです。次のような構造でパターンを定義できます。

![ahap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/1e32528e-91d8-f553-faba-7680950677ed.png)


以下にAHAPファイルの例を示します。[^ahap]

[^ahap]: Appleの"Haptic Sampler"というサンプルコードに付属している`Inflate.ahap`というファイルから抜粋したものです。

```json
{
  "Version": 1.0,
  "Metadata":
    {
      "Project" : "Haptic Sampler",
      "Created" : "5 June 2019",
      "Description" : "An effect that builds in sharpness and intensity."
    },
  "Pattern":
  [
    {
      "Event":
      {
        "Time": 0.0,
        "EventType": "HapticContinuous",
        "EventDuration": 1.7,
        "EventParameters":
        [
          { "ParameterID": "HapticIntensity", "ParameterValue": 1.0 },
          { "ParameterID": "HapticSharpness", "ParameterValue": 0.5 }
        ]
      }
    },
    {
      "ParameterCurve":
      {
        "ParameterID": "HapticIntensityControl",
        "Time": 0.0,
        "ParameterCurveControlPoints":
        [
          { "Time": 0, "ParameterValue": 0.0 },
          { "Time": 1.1, "ParameterValue": 0.5 },
          { "Time": 1.7, "ParameterValue": 0.0 }
        ]
      }
    },
    ...（略）
  ]
}
```

この定義のうち`Event`キーはCore Hapticsの`CHHapticEvent`に相当し、その配下の`Time`キーは`relativeTime`プロパティ、`EventType`キーは`eventType`プロパティ、`EventDuration`キーは`duration`、`EventParameters`キーは`eventParameters`プロパティに相当します。また`EventParameters`キー配下の配列に入る要素は`CHHapticEventParameter`に相当します。こうしてみれば、**AHAPというフォーマットはハプティックパターン（**`CHHapticPattern`**）の実装をそのままJSONに落とし込んだだけ**であり、難しくはありません。

JSONフォーマットなので、AHAPは通常のテキストファイルとして閲覧・編集でき、Xcodeプロジェクトへの追加方法も他のリソースと同様です。

Core Hapticsは、このAHAPファイルからハプティックパターンを読み出して、再生することができます。

## AHAPファイルからの再生

AHAPファイルを読み込んでハプティックパターンを再生する実装方法は非常にシンプルです。

エンジン（`CHHapticEngine`）を初期化し、

```swift
let engine = try! CHHapticEngine()
```

開始します。

```swift
try! engine.start()
```

あとは`CHHapticEngine`の`playPattern(from:)`メソッドを呼び、引数にAHAPファイルの`URL`を渡すだけです。

```swift
try! engine.playPattern(from: URL(fileURLWithPath: path))
```

以上でAHAPファイルからハプティックパターンを再生することができます。


## サンプルコードのダウンロード

本記事は2019年9月に発売した書籍「[iOS 13の新機能をざっくり把握する本](https://shu223.booth.pm/items/1461791)」からの転載です。同書籍は現在は**100円**で販売しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">「iOS 13の新機能をざっくり把握する本」の電子版をなんと！100円にしてみました。<br><br>理由：<br>・iOS ○と題された本は時間経過で非常に売れにくくなる<br>・「100円なら買う」層はどれぐらいいるのかの実験<br>・本が売れたときの通知がくると単純に嬉しい<br>・（続）<a href="https://t.co/aRqpnHp6vH">https://t.co/aRqpnHp6vH</a></p>&mdash; Shuichi Tsutsumi (@shu223) <a href="https://twitter.com/shu223/status/1220134566226096128?ref_src=twsrc%5Etfw">January 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

またCore Hapticsは実際に手元で試してみないと文章だけでは伝わりにくいと思います。上記記事やWEBに転がっている情報で十分実装できるとは思いますが、サクッと試してみたい方はBOOTHにてサンプルも100円で販売しているので投げ銭がてらよろしければご利用ください。

https://shu223.booth.pm/items/1461791

カスタムパターンを生成するサンプルと、AHAPファイルを再生するサンプルが入っています。

<img width="375" alt="IMG_0367.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/eb872394-7948-4885-ccee-2c79dcbbebd3.png">