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

Commit 2620ef14 authored by Ahmad Khalil's avatar Ahmad Khalil
Browse files

Introducing WaveformEnvelopeBuilder to VibrationEffect

We’re introducing a new parcelable PwleSegment and VibrationEffect.startWaveformEnvelope() method which returns a WaveformEnvelopeBuilder allowing the creation of PWLE effects with varying amplitudes and frequencies.

Bug: 347035826
Flag: android.os.vibrator.normalized_pwle_effects
Test: atest FrameworksVibratorCoreTests
Change-Id: Ia9105c25897152b3262ef735b7dd40009a72bda8
parent 9afbd46d
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -34365,6 +34365,7 @@ package android.os {
    method public static android.os.VibrationEffect createWaveform(long[], int[], int);
    method public int describeContents();
    method @NonNull public static android.os.VibrationEffect.Composition startComposition();
    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope();
    field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR;
    field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff
    field public static final int EFFECT_CLICK = 0; // 0x0
@@ -34388,6 +34389,11 @@ package android.os {
    field public static final int PRIMITIVE_TICK = 7; // 0x7
  }
  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class 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 build();
  }
  public abstract class Vibrator {
    method public final int areAllEffectsSupported(@NonNull int...);
    method public final boolean areAllPrimitivesSupported(@NonNull int...);
+11 −0
Original line number Diff line number Diff line
@@ -2771,6 +2771,17 @@ package android.os.vibrator {
    field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR;
  }

  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class PwleSegment extends android.os.vibrator.VibrationEffectSegment {
    method public int describeContents();
    method public long getDuration();
    method public float getEndAmplitude();
    method public float getEndFrequencyHz();
    method public float getStartAmplitude();
    method public float getStartFrequencyHz();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PwleSegment> CREATOR;
  }

  public final class RampSegment extends android.os.vibrator.VibrationEffectSegment {
    method public int describeContents();
    method public long getDuration();
+8 −1
Original line number Diff line number Diff line
@@ -194,7 +194,6 @@ public abstract class CombinedVibration implements Parcelable {
        int[] getAvailableVibratorIds();

        /** Adapts a {@link VibrationEffect} to a given vibrator. */
        @NonNull
        VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect);
    }

@@ -442,6 +441,10 @@ public abstract class CombinedVibration implements Parcelable {
            boolean hasSameEffects = true;
            for (int vibratorId : adapter.getAvailableVibratorIds()) {
                VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect);
                if (newEffect == null) {
                    // The vibration effect contains unsupported segments and cannot be played.
                    return null;
                }
                combination.addVibrator(vibratorId, newEffect);
                hasSameEffects &= mEffect.equals(newEffect);
            }
@@ -649,6 +652,10 @@ public abstract class CombinedVibration implements Parcelable {
                int vibratorId = mEffects.keyAt(i);
                VibrationEffect effect = mEffects.valueAt(i);
                VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect);
                if (newEffect == null) {
                    // The vibration effect contains unsupported segments and cannot be played.
                    return null;
                }
                combination.addVibrator(vibratorId, newEffect);
                hasSameEffects &= effect.equals(newEffect);
            }
+142 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.net.Uri;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
@@ -1691,6 +1692,147 @@ public abstract class VibrationEffect implements Parcelable {
        }
    }

    /**
     * Start building a waveform vibration.
     *
     * <p>The waveform envelope builder offers more flexibility for creating waveform effects,
     * allowing control over vibration amplitude and frequency via smooth transitions between
     * values.
     *
     * <p>Note: To check whether waveform envelope effects are supported, use
     * {@link Vibrator#areEnvelopeEffectsSupported()}.
     *
     * @see VibrationEffect.WaveformEnvelopeBuilder
     */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    @NonNull
    public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope() {
        return new WaveformEnvelopeBuilder();
    }

    /**
     * A builder for waveform effects described by its envelope.
     *
     * <p>Waveform effect envelopes are defined by one or more control points describing a target
     * vibration amplitude and frequency, and a duration to reach those targets. The vibrator
     * will perform smooth transitions between control points.
     *
     * <p>For example, the following code ramps a vibrator from off to full amplitude at 120Hz over
     * 100ms, holds that state for 200ms, and then ramps back down over 100ms:
     *
     * <pre>{@code
     * VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
     *     .addControlPoint(1.0f, 120f, 100)
     *     .addControlPoint(1.0f, 120f, 200)
     *     .addControlPoint(0.0f, 120f, 100)
     *     .build();
     * }</pre>
     *
     * <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
     * modify these frequencies.
     *
     * <p>Therefore, it is strongly recommended that you design your haptic effects with the
     * device's frequency profile in mind. You can obtain the supported frequency range and other
     * relevant frequency-related information by getting the
     * {@link android.os.vibrator.VibratorFrequencyProfile} using the
     * {@link Vibrator#getFrequencyProfile()} method.
     *
     * <p>In addition to these limitations, when designing vibration patterns, it is important to
     * consider the physical limitations of the vibration actuator. These limitations include
     * factors such as the maximum number of control points allowed in an envelope effect, the
     * minimum and maximum durations permitted for each control point, and the maximum overall
     * duration of the effect. 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>You can use the following APIs to obtain these limits:
     * <ul>
     * <li>Maximum envelope control points: {@link Vibrator#getMaxEnvelopeEffectSize()}</li>
     * <li>Minimum control point duration:
     * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}</li>
     * <li>Maximum control point duration:
     * {@link Vibrator#getMaxEnvelopeEffectControlPointDurationMillis()}</li>
     * <li>Maximum total effect duration: {@link Vibrator#getMaxEnvelopeEffectDurationMillis()}</li>
     * </ul>
     *
     * @see VibrationEffect#startWaveformEnvelope()
     */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public static final class WaveformEnvelopeBuilder {

        private ArrayList<PwleSegment> mSegments = new ArrayList<>();
        private float mLastAmplitude = 0f;
        private float mLastFrequencyHz = 0f;

        private WaveformEnvelopeBuilder() {}

        /**
         * Adds a new control point to the end of this waveform envelope.
         *
         * <p>Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1
         * (maximum achievable strength). This value scales linearly with output strength, not
         * perceived intensity. It's determined by the actuator response curve.
         *
         * <p>Frequency must be greater than zero and within the supported range. To determine
         * the supported range, use {@link Vibrator#getFrequencyProfile()}. This method returns a
         * {@link android.os.vibrator.VibratorFrequencyProfile} object, which contains the
         * minimum and maximum frequencies, among other frequency-related information. Creating
         * effects using frequencies outside this range will result in the vibration not playing.
         *
         * <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 Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}.
         *
         * @param amplitude   The amplitude value between 0 and 1, inclusive. 0 represents the
         *                    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.
         */
        @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) {

            if (mSegments.isEmpty()) {
                mLastFrequencyHz = frequencyHz;
            }

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

            mLastAmplitude = amplitude;
            mLastFrequencyHz = frequencyHz;

            return this;
        }

        /**
         * Build the waveform as a single {@link VibrationEffect}.
         *
         * <p>The {@link WaveformEnvelopeBuilder} 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.
         */
        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
        @NonNull
        public VibrationEffect build() {
            if (mSegments.isEmpty()) {
                throw new IllegalStateException(
                        "WaveformEnvelopeBuilder must have at least one control point to build.");
            }
            VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
            effect.validate();
            return effect;
        }
    }

    /**
     * A builder for waveform haptic effects.
     *
+234 −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
 * amplitude and frequency to new values over a specified duration.
 *
 * <p>The amplitudes are expressed by float values in the range [0, 1], representing the relative
 * output acceleration for the vibrator. The frequencies are expressed in hertz by positive finite
 * float values.
 * @hide
 */
@TestApi
@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
public final class PwleSegment extends VibrationEffectSegment {
    private final float mStartAmplitude;
    private final float mStartFrequencyHz;
    private final float mEndAmplitude;
    private final float mEndFrequencyHz;
    private final int mDuration;

    PwleSegment(@android.annotation.NonNull Parcel in) {
        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt());
    }

    /** @hide */
    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz,
            float endFrequencyHz, int duration) {
        mStartAmplitude = startAmplitude;
        mEndAmplitude = endAmplitude;
        mStartFrequencyHz = startFrequencyHz;
        mEndFrequencyHz = endFrequencyHz;
        mDuration = duration;
    }

    public float getStartAmplitude() {
        return mStartAmplitude;
    }

    public float getEndAmplitude() {
        return mEndAmplitude;
    }

    public float getStartFrequencyHz() {
        return mStartFrequencyHz;
    }

    public float getEndFrequencyHz() {
        return mEndFrequencyHz;
    }

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

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof PwleSegment)) {
            return false;
        }
        PwleSegment other = (PwleSegment) o;
        return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0
                && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0
                && Float.compare(mStartFrequencyHz, other.mStartFrequencyHz) == 0
                && Float.compare(mEndFrequencyHz, other.mEndFrequencyHz) == 0
                && mDuration == other.mDuration;
    }

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

        // Check that the frequency is within the supported range
        float minFrequency = vibratorInfo.getFrequencyProfile().getMinFrequencyHz();
        float maxFrequency = vibratorInfo.getFrequencyProfile().getMaxFrequencyHz();

        areFeaturesSupported &=
                mStartFrequencyHz >= minFrequency && mStartFrequencyHz <= maxFrequency
                        && mEndFrequencyHz >= minFrequency && mEndFrequencyHz <= maxFrequency;

        return areFeaturesSupported;
    }

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

    /** @hide */
    @Override
    public void validate() {
        Preconditions.checkArgumentPositive(mStartFrequencyHz,
                "Start frequency must be greater than zero.");
        Preconditions.checkArgumentPositive(mEndFrequencyHz,
                "End frequency must be greater than zero.");
        Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero.");

        Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude");
        Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude");
    }

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

    /** @hide */
    @NonNull
    @Override
    public PwleSegment scale(float scaleFactor) {
        float newStartAmplitude = VibrationEffect.scale(mStartAmplitude, scaleFactor);
        float newEndAmplitude = VibrationEffect.scale(mEndAmplitude, scaleFactor);
        if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
                && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
            return this;
        }
        return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
                mEndFrequencyHz,
                mDuration);
    }

    /** @hide */
    @NonNull
    @Override
    public PwleSegment scaleLinearly(float scaleFactor) {
        float newStartAmplitude = VibrationEffect.scaleLinearly(mStartAmplitude, scaleFactor);
        float newEndAmplitude = VibrationEffect.scaleLinearly(mEndAmplitude, scaleFactor);
        if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
                && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
            return this;
        }
        return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
                mEndFrequencyHz,
                mDuration);
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequencyHz, mEndFrequencyHz,
                mDuration);
    }

    @Override
    public String toString() {
        return "Pwle{startAmplitude=" + mStartAmplitude
                + ", endAmplitude=" + mEndAmplitude
                + ", startFrequencyHz=" + mStartFrequencyHz
                + ", endFrequencyHz=" + mEndFrequencyHz
                + ", duration=" + mDuration
                + "}";
    }

    /** @hide */
    @Override
    public String toDebugString() {
        return String.format(Locale.US, "Pwle=%dms(amplitude=%.2f @ %.2fHz to %.2f @ %.2fHz)",
                mDuration,
                mStartAmplitude,
                mStartFrequencyHz,
                mEndAmplitude,
                mEndFrequencyHz);
    }

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

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(PARCEL_TOKEN_PWLE);
        dest.writeFloat(mStartAmplitude);
        dest.writeFloat(mEndAmplitude);
        dest.writeFloat(mStartFrequencyHz);
        dest.writeFloat(mEndFrequencyHz);
        dest.writeInt(mDuration);
    }

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

                @Override
                public PwleSegment[] newArray(int size) {
                    return new PwleSegment[size];
                }
            };
}
Loading