Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fc6547ac authored by Ahmad Khalil's avatar Ahmad Khalil
Browse files

Introducing BasicEnvelopeBuilder to VibrationEffect

We’re introducing a new parcelable BasicPwleSegment and VibrationEffect.startBasicEnvelope() method which returns a BasicEnvelopeBuilder allowing the creation of normalized PWLE effects with varying intensity and sharpness.

Bug: 347035826
Fix: 347035624
Fix: 347030924
Flag: android.os.vibrator.normalized_pwle_effects
Test: atest FrameworksVibratorCoreTests

Change-Id: I979e9dea21afd5d680e30848c1e6cc3a0d2050e5
parent 08efab93
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -34757,6 +34757,13 @@ package android.os {
    field public static final int EFFECT_TICK = 2; // 0x2
  }
  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.BasicEnvelopeBuilder {
    ctor public VibrationEffect.BasicEnvelopeBuilder();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0, to=1) float, long);
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.BasicEnvelopeBuilder setInitialSharpness(@FloatRange(from=0, to=1) float);
  }
  public static final class VibrationEffect.Composition {
    method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int);
    method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float);
@@ -34777,7 +34784,7 @@ package android.os {
  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder {
    ctor public VibrationEffect.WaveformEnvelopeBuilder();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int);
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, long);
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder setInitialFrequencyHz(@FloatRange(from=0) float);
  }
+11 −0
Original line number Diff line number Diff line
@@ -2781,6 +2781,17 @@ package android.os.storage {

package android.os.vibrator {

  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class BasicPwleSegment extends android.os.vibrator.VibrationEffectSegment {
    method public int describeContents();
    method public long getDuration();
    method public float getEndIntensity();
    method public float getEndSharpness();
    method public float getStartIntensity();
    method public float getStartSharpness();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.BasicPwleSegment> CREATOR;
  }

  public final class PrebakedSegment extends android.os.vibrator.VibrationEffectSegment {
    method public int describeContents();
    method public long getDuration();
+175 −8
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.os;

import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS;

import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
@@ -34,6 +35,7 @@ import android.hardware.vibrator.IVibrator;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.hardware.vibrator.V1_3.Effect;
import android.net.Uri;
import android.os.vibrator.BasicPwleSegment;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
@@ -1843,6 +1845,8 @@ public abstract class VibrationEffect implements Parcelable {
     *     .build();
     * }</pre>
     *
     * <p>The builder automatically starts all effects at 0 amplitude.
     *
     * <p>It is crucial to ensure that the frequency range used in your effect is compatible with
     * the device's capabilities. The framework will not play any frequencies that fall partially
     * or completely outside the device's supported range. It will also not attempt to correct or
@@ -1908,7 +1912,7 @@ public abstract class VibrationEffect implements Parcelable {
                        firstSegment.getEndAmplitude(),
                        initialFrequencyHz, // Update start frequency
                        firstSegment.getEndFrequencyHz(),
                        (int) firstSegment.getDuration()));
                        firstSegment.getDuration()));
            }

            return this;
@@ -1931,24 +1935,25 @@ public abstract class VibrationEffect implements Parcelable {
         * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
         *
         * @param amplitude      The amplitude value between 0 and 1, inclusive. 0 represents the
         *                    vibrator being off, and 1 represents the maximum achievable amplitude
         *                       vibrator being off, and 1 represents the maximum achievable
         *                       amplitude
         *                       at this frequency.
         * @param frequencyHz    The frequency in Hz, must be greater than zero.
         * @param timeMillis  The transition time in milliseconds.
         * @param durationMillis The transition time in milliseconds.
         */
        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
        @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
        @NonNull
        public WaveformEnvelopeBuilder addControlPoint(
                @FloatRange(from = 0, to = 1) float amplitude,
                @FloatRange(from = 0) float frequencyHz, int timeMillis) {
                @FloatRange(from = 0) float frequencyHz, @DurationMillisLong long durationMillis) {

            if (Float.isNaN(mLastFrequencyHz)) {
                mLastFrequencyHz = frequencyHz;
            }

            mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz,
                    timeMillis));
                    durationMillis));

            mLastAmplitude = amplitude;
            mLastFrequencyHz = frequencyHz;
@@ -1979,6 +1984,168 @@ public abstract class VibrationEffect implements Parcelable {
        }
    }

    /**
     * A builder for waveform effects defined by their envelope, designed to provide a consistent
     * haptic perception across devices with varying capabilities.
     *
     * <p>This builder simplifies the creation of waveform effects by automatically adapting them
     * to different devices based on their capabilities. Effects are defined by control points
     * specifying target vibration intensity and sharpness, along with durations to reach those
     * targets. The vibrator will smoothly transition between these control points.
     *
     * <p><b>Intensity:</b> Defines the overall strength of the vibration, ranging from
     * 0 (off) to 1 (maximum achievable strength). Higher values result in stronger
     * vibrations. Supported intensity values guarantee sensitivity levels (SL) above
     * 10 dB SL to ensure human perception.
     *
     * <p><b>Sharpness:</b> Defines the crispness of the vibration, ranging from 0 to 1.
     * Lower values produce smoother vibrations, while higher values create a sharper,
     * more snappy sensation. Sharpness is mapped to its equivalent frequency within
     * the device's supported frequency range.
     *
     * <p>While this builder handles most of the adaptation logic, it does come with some
     * limitations:
     * <ul>
     *     <li>It may not use the full range of frequencies</li>
     *     <li>It's restricted to a frequency range that can generate output of at least 10 db
     *     SL</li>
     *     <li>Effects must end with a zero intensity control point. Failure to end at a zero
     *     intensity control point will result in an {@link IllegalStateException}.</li>
     * </ul>
     *
     * <p>The builder automatically starts all effects at 0 intensity.
     *
     * <p>To avoid these limitations and to have more control over the effects output, use
     * {@link WaveformEnvelopeBuilder}, where direct amplitude and frequency values can be used.
     *
     * <p>For optimal cross-device consistency, it's recommended to limit the number of control
     * points to a maximum of 16. However this is not mandatory, and if a pattern exceeds the
     * maximum number of allowed control points, the framework will automatically break down the
     * effect to ensure it plays correctly.
     *
     * <p>For example, the following code creates a vibration effect that ramps up the intensity
     * from a low-pitched to a high-pitched strong vibration over 500ms and then ramps it down to
     * 0 (off) over 100ms:
     *
     * <pre>{@code
     * VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
     *     .setInitialSharpness(0.0f)
     *     .addControlPoint(1.0f, 1.0f, 500)
     *     .addControlPoint(0.0f, 1.0f, 100)
     *     .build();
     * }</pre>
     */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public static final class BasicEnvelopeBuilder {

        private ArrayList<BasicPwleSegment> mSegments = new ArrayList<>();
        private float mLastIntensity = 0f;
        private float mLastSharpness = Float.NaN;

        public BasicEnvelopeBuilder() {}

        /**
         * Sets the initial sharpness for the basic envelope effect.
         *
         * <p>The effect will start vibrating at this sharpness when it transitions to the
         * intensity and sharpness defined by the first control point.
         *
         * <p> The sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
         * values translate to smoother vibrations, while higher values create a sharper more snappy
         * sensation. This value is mapped to the supported frequency range of the device.
         *
         * @param initialSharpness The starting sharpness of the vibration in the range of [0, 1].
         */
        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
        @SuppressWarnings("MissingGetterMatchingBuilder")// No getter to initial sharpness once set.
        @NonNull
        public BasicEnvelopeBuilder setInitialSharpness(
                @FloatRange(from = 0, to = 1) float initialSharpness) {

            if (mSegments.isEmpty()) {
                mLastSharpness = initialSharpness;
            } else {
                BasicPwleSegment firstSegment = mSegments.getFirst();
                mSegments.set(0, new BasicPwleSegment(
                        firstSegment.getStartIntensity(),
                        firstSegment.getEndIntensity(),
                        initialSharpness, // Update start sharpness
                        firstSegment.getEndSharpness(),
                        firstSegment.getDuration()));
            }

            return this;
        }

        /**
         * Adds a new control point to the end of this waveform envelope.
         *
         * <p>Intensity defines the overall strength of the vibration, ranging from 0 (off) to 1
         * (maximum achievable strength). Higher values translate to stronger vibrations.
         *
         * <p>Sharpness defines the crispness of the vibration, ranging from 0 to 1. Lower
         * values translate to smoother vibrations, while higher values create a sharper more snappy
         * sensation. This value is mapped to the supported frequency range of the device.
         *
         * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
         * from the previous control point to this new one. It must be greater than zero. To
         * transition as quickly as possible, use
         * {@link VibratorEnvelopeEffectInfo#getMinControlPointDurationMillis()}.
         *
         * @param intensity      The target vibration intensity, ranging from 0 (off) to 1 (maximum
         *                       strength).
         * @param sharpness      The target sharpness, ranging from 0 (smoothest) to 1 (sharpest).
         * @param durationMillis The transition time in milliseconds.
         */
        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
        @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
        @NonNull
        public BasicEnvelopeBuilder addControlPoint(
                @FloatRange(from = 0, to = 1) float intensity,
                @FloatRange(from = 0, to = 1) float sharpness,
                @DurationMillisLong long durationMillis) {

            if (Float.isNaN(mLastSharpness)) {
                mLastSharpness = sharpness;
            }

            mSegments.add(new BasicPwleSegment(mLastIntensity, intensity, mLastSharpness, sharpness,
                    durationMillis));

            mLastIntensity = intensity;
            mLastSharpness = sharpness;

            return this;
        }

        /**
         * Build the waveform as a single {@link VibrationEffect}.
         *
         * <p>The {@link BasicEnvelopeBuilder} object is still valid after this call, so you can
         * continue adding more primitives to it and generating more {@link VibrationEffect}s by
         * calling this method again.
         *
         * @return The {@link VibrationEffect} resulting from the list of control points.
         * @throws IllegalStateException if the last control point does not end at zero intensity.
         */
        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
        @NonNull
        public VibrationEffect build() {
            if (mSegments.isEmpty()) {
                throw new IllegalStateException(
                        "BasicEnvelopeBuilder must have at least one control point to build.");
            }
            if (mSegments.getLast().getEndIntensity() != 0) {
                throw new IllegalStateException(
                        "Basic envelope effects must end at a zero intensity control point.");
            }
            VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
            effect.validate();
            return effect;
        }

    }

    /**
     * A builder for waveform haptic effects.
     *
+222 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.os.vibrator;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;

import com.android.internal.util.Preconditions;

import java.util.Locale;
import java.util.Objects;

/**
 * A {@link VibrationEffectSegment} that represents a smooth transition from the starting
 * intensity and sharpness to new values over a specified duration.
 *
 * <p>The intensity and sharpness are expressed by float values in the range [0, 1], where
 * intensity represents the user-perceived strength of the vibration, while sharpness represents
 * the crispness of the vibration.
 *
 * @hide
 */
@TestApi
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
public final class BasicPwleSegment extends VibrationEffectSegment {
    private final float mStartIntensity;
    private final float mEndIntensity;
    private final float mStartSharpness;
    private final float mEndSharpness;
    private final long mDuration;

    BasicPwleSegment(@NonNull Parcel in) {
        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readLong());
    }

    /** @hide */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public BasicPwleSegment(float startIntensity, float endIntensity, float startSharpness,
            float endSharpness, long duration) {
        mStartIntensity = startIntensity;
        mEndIntensity = endIntensity;
        mStartSharpness = startSharpness;
        mEndSharpness = endSharpness;
        mDuration = duration;
    }

    public float getStartIntensity() {
        return mStartIntensity;
    }

    public float getEndIntensity() {
        return mEndIntensity;
    }

    public float getStartSharpness() {
        return mStartSharpness;
    }

    public float getEndSharpness() {
        return mEndSharpness;
    }

    @Override
    public long getDuration() {
        return mDuration;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BasicPwleSegment)) {
            return false;
        }
        BasicPwleSegment other = (BasicPwleSegment) o;
        return Float.compare(mStartIntensity, other.mStartIntensity) == 0
                && Float.compare(mEndIntensity, other.mEndIntensity) == 0
                && Float.compare(mStartSharpness, other.mStartSharpness) == 0
                && Float.compare(mEndSharpness, other.mEndSharpness) == 0
                && mDuration == other.mDuration;
    }

    /** @hide */
    @Override
    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
        return vibratorInfo.areEnvelopeEffectsSupported();
    }

    /** @hide */
    @Override
    public boolean isHapticFeedbackCandidate() {
        return true;
    }

    /** @hide */
    @Override
    public void validate() {
        Preconditions.checkArgumentInRange(mStartSharpness, 0f, 1f, "startSharpness");
        Preconditions.checkArgumentInRange(mEndSharpness, 0f, 1f, "endSharpness");
        Preconditions.checkArgumentInRange(mStartIntensity, 0f, 1f, "startIntensity");
        Preconditions.checkArgumentInRange(mEndIntensity, 0f, 1f, "endIntensity");
        Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero.");
    }

    /** @hide */
    @NonNull
    @Override
    public BasicPwleSegment resolve(int defaultAmplitude) {
        return this;
    }

    /** @hide */
    @NonNull
    @Override
    public BasicPwleSegment scale(float scaleFactor) {
        float newStartIntensity = VibrationEffect.scale(mStartIntensity, scaleFactor);
        float newEndIntensity = VibrationEffect.scale(mEndIntensity, scaleFactor);
        if (Float.compare(mStartIntensity, newStartIntensity) == 0
                && Float.compare(mEndIntensity, newEndIntensity) == 0) {
            return this;
        }
        return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness,
                mEndSharpness,
                mDuration);
    }

    /** @hide */
    @NonNull
    @Override
    public BasicPwleSegment scaleLinearly(float scaleFactor) {
        float newStartIntensity = VibrationEffect.scaleLinearly(mStartIntensity, scaleFactor);
        float newEndIntensity = VibrationEffect.scaleLinearly(mEndIntensity, scaleFactor);
        if (Float.compare(mStartIntensity, newStartIntensity) == 0
                && Float.compare(mEndIntensity, newEndIntensity) == 0) {
            return this;
        }
        return new BasicPwleSegment(newStartIntensity, newEndIntensity, mStartSharpness,
                mEndSharpness,
                mDuration);
    }

    /** @hide */
    @NonNull
    @Override
    public BasicPwleSegment applyEffectStrength(int effectStrength) {
        return this;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mStartIntensity, mEndIntensity, mStartSharpness, mEndSharpness,
                mDuration);
    }

    @Override
    public String toString() {
        return "BasicPwle{startIntensity=" + mStartIntensity
                + ", endIntensity=" + mEndIntensity
                + ", startSharpness=" + mStartSharpness
                + ", endSharpness=" + mEndSharpness
                + ", duration=" + mDuration
                + "}";
    }

    /** @hide */
    @Override
    public String toDebugString() {
        return String.format(Locale.US, "Pwle=%dms(intensity=%.2f @ %.2f to %.2f @ %.2f)",
                mDuration,
                mStartIntensity,
                mStartSharpness,
                mEndIntensity,
                mEndSharpness);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(PARCEL_TOKEN_PWLE);
        dest.writeFloat(mStartIntensity);
        dest.writeFloat(mEndIntensity);
        dest.writeFloat(mStartSharpness);
        dest.writeFloat(mEndSharpness);
        dest.writeLong(mDuration);
    }

    @NonNull
    public static final Creator<BasicPwleSegment> CREATOR =
            new Creator<BasicPwleSegment>() {
                @Override
                public BasicPwleSegment createFromParcel(Parcel in) {
                    // Skip the type token
                    in.readInt();
                    return new BasicPwleSegment(in);
                }

                @Override
                public BasicPwleSegment[] newArray(int size) {
                    return new BasicPwleSegment[size];
                }
            };
}
+8 −0
Original line number Diff line number Diff line
@@ -63,4 +63,12 @@ public final class PwlePoint {
    public int hashCode() {
        return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis);
    }

    @Override
    public String toString() {
        return "PwlePoint{amplitude=" + mAmplitude
                + ", frequency=" + mFrequencyHz
                + ", time=" + mTimeMillis
                + "}";
    }
}
Loading