Skip to main content

iOS developers

Syncing haptics with audio and animation

Put sound and touch on a shared timeline where possible, prepare playback early, and test the visual alignment on a real device.

Published 4 minute read

Choose the moment that matters

Start by naming the event you want someone to perceive: a ball hits the floor, a latch closes, a countdown reaches zero. Put a marker at that event in the sound and the animation. “Start everything when the button is pressed” is insufficient if the sound’s impact occurs partway through the file.

Keep three times distinct: the app’s decision to begin, the scheduled event time, and the moment the output reaches the person. A resource load can delay the first. A badly chosen offset can misplace the second. Output hardware can affect the third.

Use one engine for authored sound and touch

If the sound belongs to the effect, putting it in the same Core Haptics pattern gives the audio and tactile events a shared timeline. Apple’s collision example uses this approach. It removes the need to coordinate two independent player start calls. Collision-based haptic patterns

This factory registers your audio file and puts a tactile impact at an offset you supply. For a sound whose impact begins 0.12 seconds into the recording, pass 0.12. Use a local audio file supported by Core Haptics.

import Foundation
import CoreHaptics

func makeImpactCue(
    engine: CHHapticEngine,
    audioURL: URL,
    impactOffset: TimeInterval
) throws -> any CHHapticPatternPlayer {
    let audioID = try engine.registerAudioResource(audioURL, options: [:])
    let audio = CHHapticEvent(
        audioResourceID: audioID,
        parameters: [],
        relativeTime: 0
    )
    let impact = CHHapticEvent(
        eventType: .hapticTransient,
        parameters: [
            CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
            CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
        ],
        relativeTime: max(0, impactOffset)
    )
    let pattern = try CHHapticPattern(events: [audio, impact], parameters: [])
    return try engine.makePlayer(with: pattern)
}

Call this while preparing the effect, retain the returned player and its engine, then start the player when appropriate. The function is a resource-preparation example, not a complete playback service. Your service still owns audio-session configuration, engine startup, interruption handling and cleanup. Reuse the prepared resources; do not register a fresh copy of the sound for every collision. Registering custom audio

An AHAP file can also reference bundled audio. Its waveform path is relative to the app bundle’s root; do not assume it resolves beside any AHAP you downloaded. AHAP audio paths

Separate players have separate clocks

A music or video app may already have an audio playback system. In that case, do not pass its current time directly to a Core Haptics player. Apple explicitly says CHHapticEngine.currentTime does not correlate with the clocks used by other media playback frameworks. Engine time

Define an app-level event timeline and track how each subsystem starts, pauses and seeks relative to it. Two adjacent “play now” calls are a useful prototype, not a guarantee of synchronized output. Measure the result with the output routes you support before adding compensation. A fixed offset that helps one route may hurt another.

When Core Haptics should produce only touch, configure playsHapticsOnly before starting the engine. Changing it while the engine runs takes effect only after a stop and restart. Haptics-only playback

Prepare before the critical frame

Load the file, construct the pattern and create the player ahead of the interaction when latency matters. Starting a stopped engine inside playPattern(from:) can delay playback. The synchronous start() also blocks its calling thread while startup completes, so it is not work to hide inside a rendering callback. File playback, engine startup

For animation, drive both effects from the same semantic event. A scripted sequence can share authored offsets. An interactive collision should trigger from the collision itself, rather than an earlier prediction that can become wrong when the person changes direction.

Do not use a UI timer as evidence of precise output timing. First review normal-speed playback on the device, then inspect a recording if you need to locate a visible or audible mismatch. A screen recording can help diagnose scheduling; it cannot record what the person felt.

Treat pause and reset as part of the design

If the person seeks to the middle of a clip, decide whether a sustained effect should resume and whether earlier impacts must be skipped. The advanced pattern player supplies seek, pause and resume operations; the rest of your timeline must follow the same decision. Advanced playback

After a media-server reset, recreate players and register custom audio again. After an interruption, wait until playback is appropriate; do not automatically resume a cue for a completed action. Engine recovery

Check a small matrix

Test What to look for
First play and repeated play A cold-start delay or duplicated cue
Sound on, then off Touch still corresponds to the visible action
Pause and resume All parts resume from the intended position
Seek past an impact The old impact does not fire late
Background and interruption No stale playback on return
Each supported audio route Alignment remains acceptable for that route

Record the device, OS and output route with your findings. The goal is a coherent interaction you have actually checked, not an unmeasured latency promise.

More to read

Design haptics in Haptrix

Free, for Mac, iPad and iPhone: the editor, export and publishing included.

New articles by email

Email updates are not switched on yet.