-
Notifications
You must be signed in to change notification settings - Fork 17
2. Lofelt SDK for iOS
Lofelt has developed the an iOS framework to simplify the integration of haptics into applications. The applications you create with this framework will work on iPhones that are compatible with Core Haptics.
The iOS Framework provides a mechanism for playing a pre-authored haptic clip. The goal is to be used when you want a haptic effect to match a sound file used in your app. For example, when you want to play the haptic effect of a door closing simultaneously with the sound of the door closing.
After extracting the downloaded Lofelt SDK .zip file, open the sdk
folder,
then the ios-framework
folder. Inside this folder are two more folders:
Xcode11
and Xcode12AndHigher
. If you are using Xcode 12 or above, import
the LofeltHaptics.xcframework
to your project. If you are using an older
version of Xcode, choose the framework based on what you want to do:
I am using Xcode 11 and only want to debug my app on iPhones and/or I want to release my app on Apple's AppStore | I am using Xcode 11 and only want to debug using the Xcode simulator |
---|---|
Take the LofeltHaptics.framework from the devices folder. |
Take the LofeltHaptics.framework from the simulator folder. |
Note: Apple suggests using XCFrameworks for the latest development and will reject an iOS app containing a
.framework
with support for the Xcode simulator. So, only the XCFramework anddevices
versions can be included in apps intended for App Store submission and distribution.
To use Lofelt SDK haptics in your app, you must first create a LofeltHaptics
object. You will then be able to issue haptic commands to that object. Note that
all of the following code examples are written in Swift.
import LofeltHaptics
let haptics = try? LofeltHaptics.init()
You should have only one LofeltHaptics
object in your application. At the time
of this writing, this means the LofeltHaptics
object you create is capable of
playing back one haptic clip at a time or converting one audio stream to
haptics at a time.
A haptic clip contains high-definition, device-agnostic haptic data that the Studio framework for iOS uses to create an optimal haptic effect on playback devices. The playback of these haptic clips requires only three simple steps:
- Load the haptic clip into the
LofeltHaptics
object with theload()
method. - Start playback of the haptic clip with the
play()
method. - Stop playback of the haptic clip with the
stop()
method (which is only needed when you wish to stop the haptic clip before it has finished playing).
func deviceMeetsMinimumRequirements() -> Bool
The deviceMeetsMinimumRequirements()
method allows you to easily check if the
iOS device meets the minimum requirements to playback haptic clips. At the
moment, the iOS minimum requirements are:
- iOS 13+
- iPhone 8 or newer
func load(data: String) throws
The load()
method allows you to load the contents of a haptic clip into memory
before triggering playback with play()
. This preloading procedure is crucial
for achieving tight synchronization between a haptic clip and its associated
audio sample.
func play() throws
The play()
method initiates haptic playback of the haptic clip currently
loaded into the LofeltHaptics
instance.
Playback of the haptic effect always starts from the beginning of the haptic
clip and automatically stops when playback reaches the end of the haptic clip.
However, you can stop playback of the haptic effect prematurely with the
stop()
method.
func stop() throws
If the LofeltHaptics
object is currently playing a haptic clip, the stop()
method terminates playback. If a haptic clip is not playing, the stop()
method
will have no effect.
Using the stop()
method leaves the currently loaded haptic clip in memory. A
subsequent play()
method will play the same haptic clip again starting from
the beginning.
func seek(time: float) throws
Once a haptic clip has been loaded with load()
, the seek()
method moves
playback to the given time position within the haptic. The playback state
(playing or stopped) will not be changed unless seeking beyond the end of the
haptic clip which will stop playback. Seeking to a position before the start of
the clip (a negative time position) will simply move playback to the start of
the clip leaving the playback state unchanged.
func loop(enabled: bool) throws
This method allows the playback of a haptic clip to loop. This means playback will repeat from the beginning once it reaches the end.
When a new clip is loaded, looping is disabled.
If seek()
is called while playing, playback will jump to the sought position
and then repeat from the start.
func setAmplitudeMultiplication(factor: float) throws
func setFrequencyShift(shift: float) throws
These methods allow you to modulate the amplitude and frequency of a clip.
The amplitude of a clip is modulated by multiplying each breakpoint's amplitude by the given multiplication factor. The multiplication factor must be 0 or greater. The frequency of a clip is modulated by adding the frequency shift to each breakpoint's frequency. The frequency shift needs to be between -1.0 and 1.0.
If the resulting amplitude or frequency is smaller than 0.0 or larger than 1.0, it will be clipped to that range. Clipping can cause artifacts, so you should take care to avoid excessive clipping. For example, if the breakpoints in a clip already have a high amplitude, a high amplitude multiplication factor should be avoided.
The modulation is applied right away, even to a currently playing clip.
func getClipDuration() -> float
Once a haptic clip has been loaded with load()
, it is possible to get its
duration in seconds. If no clip is loaded and getClipDuration
is called, the
returned value will be 0.0.
Haptic output from an app stops when it is put into the background. However,
haptics that were interrupted when the app was put into background do not
automatically resume and need to be started again by calling play()
.
let haptics = try? LofeltHaptics.init()
// Fetch a Haptic Clip from the asset catalog.
let hapticClip = NSDataAsset(name: "haptic_clip.haptic")
// Load its data into an NSString.
let hapticData = NSString(data: hapticClip!.data , encoding: String.Encoding.utf8.rawValue)
// Load it into the LofeltHaptics object as a String.
try? haptics.load(hapticData! as String)
// Play audio and haptics (audio must be played first).
audioPlayer?.play()
haptics?.play()
Where:
-
hapticData
is of typeString
and is the contents of a haptic clip created with Studio. -
audioPlayer
is some kind of audio player, for example,AVAudioPlayer
.
It is important to play the audio before the haptic effect for optimal audio/haptic synchronicity.
ℹ️ There are more advanced ways to achieve audio/haptics sync that the iOS framework currently doesn't implement. Currently, the framework uses the "trigger-and-hope-for-the-best" approach.
let haptics = try? LofeltHaptics.init()
// Fetch a Haptic Clip from the asset catalog.
let hapticClip = NSDataAsset(name: "haptic_clip.haptic")
// Load its data into an NSString.
let hapticData = NSString(data: hapticClip!.data , encoding: String.Encoding.utf8.rawValue)
// Load it into the LofeltHaptics object as a String.
try? haptics.load(hapticData! as String)
haptics?.play()
try! haptics.seek(1.0)
Where:
-
hapticData
is of typeString
and is the contents of a haptic clip created with Studio.
In the sdk/examples/ios
folder within the Lofelt SDK download, there is an
example project for playing haptic clips on iPhone called
LofeltHapticsExamplePreAuthored
. To run the example, open the project in
Xcode, target your iPhone, and build. The example provides two buttons that,
when pressed and released, will trigger audio files and accompanying haptic
clips.
Make sure you play the audio before the haptics for optimal synchronicity.
let haptics = try? LofeltHaptics.init()
haptics?.load(hapticData)
audioPlayer?.play()
haptics?.play()
You should wrap your calls into LofeltHaptics
in try
statements. See:
https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html
do {
try haptics.load(dataString! as String)
} catch let error as NSError {
print("Received an error: (error.localizedDescription)")
}
You can also use the try?
or try!
variations.
In the event you encounter an error message while using the iOS framework, the tips below can help resolve the issue. If the error persists even after trying these remedies, please open issue in this repository with the details of the error you receive.
Function Called | Error Message | Recovery Steps |
---|---|---|
init() | LofeltHaptics initAndReturnError: Error initializing Core Haptics engine: <error details> |
Try running your app again. If it fails consistently, please report it as a bug. |
init() |
LofeltHaptics: Unable to set realtime policy for the streaming thread. Message is logged only, not returned in the NSError . |
iOS was not able to raise the priority of the haptic streaming thread, which affects the timing of haptic playback. Please report it as a bug. |
load() |
The haptic clip you are attempting to play is of a newer version than what is supported by this framework. Message is logged only, not returned in the NSError . This is a warning, not an error. |
Get the latest version of the iOS framework from GitHub releases and include this in your app. |
load() | Error loading haptic data: Error validating V0/V1: <error details> |
1. Make sure you are following the .haptic file format. 2. Read the file correctly into a String with UTF-8 encoding. |
load() | Error loading haptic data: Error deserializing V0/V1: <error details> |
1. Make sure you are following the .haptic file format. 2. Read the file correctly into a String with UTF-8 encoding. |
load() | Error loading haptic data: Unsupported version |
Get the latest version of the iOS framework from GitHub releases and include this in your app. |
play() | Error playing haptic clip: Unable to play, no clip loaded. |
Call play() only after a haptic clip was loaded without any errors. |
stop() | Error stopping haptic clip: Unable to stop, no clip loaded. |
Call stop() only after play() . |
setAmplitudeMultiplication() | Error setting amplitude multiplication to <factor>: Unable to apply amplitude multiplication factor <factor>, needs to be 0 or greater |
Use a multiplication factor between 0.0 and ∞. |
setAmplitudeMultiplication() | Error setting amplitude multiplication to <factor>: Unable to set amplitude multiplication, no clip loaded. |
Call setAmplitudeMultiplication() only after a haptic clip was loaded without any errors. |
setFrequencyShift() | Error setting frequency shift to <shift>: Unable to apply frequency shift <shift>, needs to be between -1 and 1 |
Use a shift between -1.0 and 1.0. |
setFrequencyShift() | Error setting frequency shift to <shift>: Unable to set frequency shift, no clip loaded. |
Call setFrequencyShift() only after a haptic clip was loaded without any errors. |
loop() | Unable to loop, no clip loaded. |
Call loop() only after a haptic clip was loaded without any errors. |
Playback of pre-authored haptic clips These errors are logged during haptic playback only, and not returned from a function |
LofeltHaptics: Could not play amplitude event: <error details> |
iOS was unable to play back a haptic event. Please report it as a bug. |
Playback of pre-authored haptic clips These errors are logged during haptic playback only, and not returned from a function |
LofeltHaptics: Could not play frequency event: <error details> |
iOS was unable to play back a haptic event. Please report it as a bug. |