fix(android): play impulse-only presets on hardware without primitive support#93
Open
piaskowyk wants to merge 2 commits into
Open
fix(android): play impulse-only presets on hardware without primitive support#93piaskowyk wants to merge 2 commits into
piaskowyk wants to merge 2 commits into
Conversation
… support Impulse-only presets (Castanets, Stomp, Gavel, etc.) play via VibrationEffect.Composition primitives, gated on areAllPrimitivesSupported() and API >= 30. On weaker/older actuators that report no primitive support, createCompositionEffect() returned null and execution fell through to the continuous-envelope path. That path destroys transients: a single impulse collapses to a ~2 ms waveform (the peak's pre-ramp is discarded since the step loop starts at t=0) and, on amplitude-control devices, its energy is averaged across ~76 ms buckets down to ~7-27/255 — below a weak motor's start threshold. The result is silent taps on cheap/old Android, even though the same preset fires on fast LRA hardware and on web (navigator.vibrate is binary on/off at full power). Add a crisp-waveform fallback (createWaveformEffect / buildImpulseWaveform) that renders each impulse as a 20 ms (near-)full-power pulse with a 64/255 amplitude floor, preserving inter-impulse gaps — mirroring the web Vibration API behavior. It runs only for impulse-only presets, immediately after the primitive path returns null, so devices with primitive support and mixed discrete+continuous presets are unaffected. Covered by JVM unit tests for buildImpulseWaveform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Impulse-only presets — the percussive ones built from just discrete signals (Castanets, Stomp, Gavel, FingerDrum, HoofBeat, WarDrum… ~45 presets where the continuous amplitude is empty) — were silent on weaker/older Android hardware, while firing fine on capable phones and on web.
Why
These presets play via
VibrationEffect.Compositionprimitives (addPrimitive(PRIMITIVE_TICK/CLICK…)), gated onareAllPrimitivesSupported()and API ≥ 30. Weaker/older actuators report no primitive support, soImpulseCompositionHapticBuilder.createCompositionEffect()returnsnulland execution falls through to the continuous-envelope path (PeakLineBuilder+ControlLineBuilder.getStepsPoints@ 13 Hz). That path destroys transients:currentTime=0and discards the peak's negative-time pre-ramp, and the peak is only ~14 ms wide (2 ms at full amplitude) to begin with.So the motor never spins up (silent), or on timing-only devices the rhythm smears into one long buzz. A 2 ms pulse does fire on a fast LRA (Pixel-class), which is why it works on better phones — and web works because
navigator.vibrateis binary on/off at full power (no amplitude dilution).Fix
Add a crisp-waveform fallback scoped to impulse-only presets, tried right after the primitive path returns
null(inHapticBuilder.createVibrationEffect):ImpulseCompositionHapticBuilder.createWaveformEffect/buildImpulseWaveformrender each impulse as a 20 ms, (near-)full-power pulse viacreateWaveform, amplitude floored at 64/255, preserving inter-impulse gaps — mirroring the web Vibration API behavior that's confirmed working on the same hardware.Tests
New JVM unit tests for the pure
buildImpulseWaveformbuilder (Pulsar/src/test/.../ImpulseCompositionHapticBuilderTest.kt) — 4 tests, all passing; library compiles.Reviewer notes
VibrationEffect.createOneShot(10, …)(10 ms) inRealtimePrimitiveComposer/RealtimeEnvelopeWithDiscretePrimitivesComposer, which likely has the same too-short-for-weak-hardware issue. Left out to keep this change scoped to preset playback.🤖 Generated with Claude Code