Core Hapticsは、iOS 13から追加された振動と音声を統合的に制御するフレームワークです。Core Hapticsを用いることで振動機能のついたメトロノームを作成することができます。
1. はじめに
1.1 本記事で作成するメトロノーム
本記事では以下の機能がついた簡単なメトロノームを作成します。なお、UIの配置にはSwiftUIを用います。
- BPMを表示する機能
- ボタンによりBPMを1ずつ増やす機能
- 設定したBPMのテンポで音を繰り返し鳴らす機能
- 音に合わせてiPhone本体を振動させる機能
1.2 本記事のソースコード
本記事のソースコードは、https://github.com/Ossamoon/sample-haptics に掲載してあります。
1.3 Core Hapticsについて
この記事では、Core Hapticsフレームワークの基本的な使い方は省略します。基本的な使い方を学びたい方は、Qiitaの記事『Core Haptics - カスタムハプティックパターンの作成と再生』および、Appleの公式ドキュメントを参考にしてください。
1.4 動作環境
- Xcode 12.5
- Swift 5.4
2. アプリの作成
2.1 プロジェクトの作成
Xcodeで新規プロジェクトを作成します。SwiftUIが使えるようInterfaceにはSwiftUIを指定し、Life CycleにはSwiftUI Appを指定します。
プロジェクトを作成するとContentView.swiftファイルが自動生成されます。このファイル内のContentView構造体は2.4 Viewの作成にて書き直します。
2.2 音源の取り込み
メトロノームの音として利用するための外部音源をXcodeに取り込みます。今回はフリーの効果音を配布されている効果音ラボさんの音源を利用させていただきます。
ボタン・システム音のページに移動し、**"決定、ボタン押下2"**の音源をダウンロードしてください。
ダウンロードした音源はXcode上にドラッグアンドドロップすることでXcodeに取り込むことができます。場所はどこでもいいですが、ContentView.swiftと同じ階層が分かりやすいでしょう。後で使いやすいように、音源のファイル名をsound.mp3に変更しておきましょう。
2.3 Controllerの作成
振動機能を制御するコントローラーを記述していきます。ContentView.swiftと同じ階層に新規ファイルを作成し、ファイル名をHapticController.swiftとしてください。
HapticController.swiftを開き、以下のコードを記述します。
import Foundation
import CoreHaptics
import AVFoundation
class HapticController {
// メトロノームのパラメーター
var bpm: Double = 120.0
// AudioSession
private var audioSession: AVAudioSession
// 音声データに関わるパラメータ
private let audioResorceNames = "sound"
private var audioURL: URL?
private var audioResorceID: CHHapticAudioResourceID?
// HapticEngine
private var engine: CHHapticEngine!
// 端末がCore Hapticsに対応しているか
private var supportsHaptics: Bool = false
// HapticPatternPlayer
private var player: CHHapticAdvancedPatternPlayer?
// HapticEventのパラメーター
private let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
private let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
private var hapticDuration: TimeInterval = TimeInterval(0.08)
// AudioEventのパラメーター
private let audioVolume = CHHapticEventParameter(parameterID: .audioVolume, value: 1.0)
private var audioDuration: TimeInterval {
TimeInterval(60.0 / bpm)
}
init(){
// AudioSessionの設定
audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback)
try audioSession.setActive(true)
} catch {
print("Failed to set and activate audio session category.")
}
// 端末がCore Hapticsに対応しているかを調べる
let hapticCapability = CHHapticEngine.capabilitiesForHardware()
supportsHaptics = hapticCapability.supportsHaptics
// 外部音源の取り込み
if let path = Bundle.main.path(forResource: audioResorceNames, ofType: "mp3") {
audioURL = URL(fileURLWithPath: path)
} else {
print("Error: Failed to find audioURL")
}
createAndStartHapticEngine() //この関数は下で定義
}
// Engineの作成と開始
private func createAndStartHapticEngine() {
// 端末の対応を確認
guard supportsHaptics else {
print("This device does not support CoreHaptics")
return
}
// AudioSessionを渡してEngineを作成
do {
engine = try CHHapticEngine(audioSession: audioSession)
} catch let error {
fatalError("Engine Creation Error: \(error)")
}
// Engineをスタート
do {
try engine.start()
} catch let error {
print("Engin Start Error: \(error)")
}
}
// メトロノームを再生
func play() {
// 端末の対応を確認
guard supportsHaptics else { return }
do {
// Engineをスタート
try engine.start()
// HapticPatternを作成
let pattern = try createPattern() //この関数は下で定義
// Playerを作成(Advacedの方を利用していることに注意)
player = try engine.makeAdvancedPlayer(with: pattern)
player!.loopEnabled = true
// 再生
try player!.start(atTime: CHHapticTimeImmediate)
} catch let error {
print("Haptic Playback Error: \(error)")
}
}
// メトロノームを停止
func stop(){
// 端末の対応を確認
guard supportsHaptics else { return }
// 停止
engine.stop()
}
// HapticPatternの作成
private func createPattern() throws -> CHHapticPattern {
do {
var eventList: [CHHapticEvent] = []
// AudioResorceIDを取得
audioResorceID = try self.engine.registerAudioResource(audioURL!)
// eventListにHapticEventを加えていく
eventList.append(CHHapticEvent(audioResourceID: audioResorceID!, parameters: [audioVolume], relativeTime: 0, duration: self.audioDuration))
eventList.append(CHHapticEvent(eventType: .hapticTransient, parameters: [sharpness, intensity], relativeTime: 0))
eventList.append(CHHapticEvent(eventType: .hapticContinuous, parameters: [sharpness, intensity], relativeTime: 0, duration: self.hapticDuration))
// HapticPatternを生成し返す
let pattern = try CHHapticPattern(events: eventList, parameters: [])
return pattern
} catch let error {
throw error
}
}
}
いくつかのポイントを詳しく説明します。
- 外部音源を利用するにはEngineにAudioSessionを渡す
Core Hapticsで外部音源を利用するには、
- AudioSessionのインスタンスを取得し、アクティブにする
-
CHHapticEngine(audioSession: audioSession)
のように、Engineのインスタンスを取得する際にオーディオセッションを渡す - 外部音源のパスを取得し、AudioResorceIDを取得する
- AudioResorceIDを渡したHapticEventを生成し、それをHapticPatternに組み込む
といった手順が必要になります。EngineはAudioSessionを渡さなくてもインスタンスが取得できるのですが、その場合は外部音源が再生できなかったり、音量が端末本体の音量と連動しなかったりします。
- メトロノームアプリにTimerは使えない
一定の時間感覚で繰り返し処理を行う場合、一般的にはTimerが使用されます。しかしながら、Timerの時間間隔は厳密でなく、メトロノームアプリに採用してしまうとテンポの揺らぎを感じてしまいます。(少なくとも筆者は違和感を感じました)
そこで、Core Hapticsに内蔵されたループ機能を用います。ループの長さはHapticPatternやHapticEventの長さに準拠するので、それらを調節することによってループの長さを調節できます。
- ループ機能を使うにはmakeAdvancedPlayerメソッドでPlayer作成する
HapticPatternPlayerを作るためのメソッドはmakePlayer(with:)
とmakeAdvancedPlayer(with:)
の2種類が用意されていますが、ループ機能を用いる時はAdvancedの方が必須となります。Advancedは、再生速度を変えたいときなどにも用いられます。
2.4 Viewの作成
ContentView.swiftファイル内のContentView構造体を以下のように書き直します。
import SwiftUI
struct ContentView: View {
// UI表示に必要なパラメーター
@State private var bpm: Int = 60
@State private var isPlaying: Bool = false
// コントローラーのインスタンス
private var hapticController = HapticController()
var body: some View {
VStack {
Text("BPM")
.font(.system(size: 20))
// BPMを表示
Text(String(bpm))
.font(.system(size: 68))
// "+"ボタン: タップするとBPMを1増やす
Button(action: {
self.bpm += 1
}, label: {
Image(systemName: "plus.circle")
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 60.0)
})
.disabled(isPlaying)
// 再生・停止ボタン
if isPlaying == false {
Button(action: {
hapticController.bpm = Double(self.bpm)
hapticController.play()
isPlaying = true
}) {
Text("Start")
.font(.system(size: 60))
}
} else {
Button(action: {
hapticController.stop()
isPlaying = false
}) {
Text("Stop")
.font(.system(size: 60))
}
}
}
}
}
プレビューでUIの見た目を確認すると1.1 本記事で作成するメトロノームで紹介したスクリーンショットのような見た目になっていると思います。
2.5 実機で確認
Core HapticsをSimulatorでテストすることはできません。よって、想像通りのものができているか確認するためには、実機テストが必要不可欠になります。
お手元にiPhone 8以降の端末があれば、ぜひBuildして実機テストを行ってみてください。指定のBPMで音が鳴ると同時に端末が振動していれば完成です。
3. より深く知りたい方へ
筆者が作成したメトロノームアプリがApp Storeに公開されています。ソースコードも筆者のGitHubにて公開しているので、まずはApp Storeでダウンロードして使い心地を試した後、ソースコードをじっくり読んでみることをお勧めします。