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

Commit e574d3bb authored by Lais Andrade's avatar Lais Andrade
Browse files

Introduce vibrator frequency mapping.

Introduce frequency data to VibratorInfo and a transformation for
VibrationEffect that maps relative frequency and amplitude values to the
device absolute values.

The transformation also clips amplitude values to the device bandwidth
map provided.

Pending implementation of chaining composed waveforms and sending PWLE
primitives to the HAL.

Bug: 167947076
Test: DeviceVibrationEffectAdapterTest
Change-Id: Ie7505a1c0f50bb7d907b0752c02987f258316874
parent 601a944e
Loading
Loading
Loading
Loading
+263 −10
Original line number Diff line number Diff line
@@ -16,9 +16,13 @@

package android.os;

import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.vibrator.IVibrator;
import android.util.Log;
import android.util.MathUtils;
import android.util.Range;
import android.util.SparseBooleanArray;

import java.util.ArrayList;
@@ -42,27 +46,27 @@ public final class VibratorInfo implements Parcelable {
    private final SparseBooleanArray mSupportedEffects;
    @Nullable
    private final SparseBooleanArray mSupportedPrimitives;
    private final float mResonantFrequency;
    private final float mQFactor;
    private final FrequencyMapping mFrequencyMapping;

    VibratorInfo(Parcel in) {
        mId = in.readInt();
        mCapabilities = in.readLong();
        mSupportedEffects = in.readSparseBooleanArray();
        mSupportedPrimitives = in.readSparseBooleanArray();
        mResonantFrequency = in.readFloat();
        mQFactor = in.readFloat();
        mFrequencyMapping = in.readParcelable(VibratorInfo.class.getClassLoader());
    }

    /** @hide */
    public VibratorInfo(int id, long capabilities, int[] supportedEffects,
            int[] supportedPrimitives, float resonantFrequency, float qFactor) {
            int[] supportedPrimitives, float qFactor, @NonNull FrequencyMapping frequencyMapping) {
        mId = id;
        mCapabilities = capabilities;
        mSupportedEffects = toSparseBooleanArray(supportedEffects);
        mSupportedPrimitives = toSparseBooleanArray(supportedPrimitives);
        mResonantFrequency = resonantFrequency;
        mQFactor = qFactor;
        mFrequencyMapping = frequencyMapping;
    }

    @Override
@@ -71,8 +75,8 @@ public final class VibratorInfo implements Parcelable {
        dest.writeLong(mCapabilities);
        dest.writeSparseBooleanArray(mSupportedEffects);
        dest.writeSparseBooleanArray(mSupportedPrimitives);
        dest.writeFloat(mResonantFrequency);
        dest.writeFloat(mQFactor);
        dest.writeParcelable(mFrequencyMapping, flags);
    }

    @Override
@@ -92,14 +96,14 @@ public final class VibratorInfo implements Parcelable {
        return mId == that.mId && mCapabilities == that.mCapabilities
                && Objects.equals(mSupportedEffects, that.mSupportedEffects)
                && Objects.equals(mSupportedPrimitives, that.mSupportedPrimitives)
                && Objects.equals(mResonantFrequency, that.mResonantFrequency)
                && Objects.equals(mQFactor, that.mQFactor);
                && Objects.equals(mQFactor, that.mQFactor)
                && Objects.equals(mFrequencyMapping, that.mFrequencyMapping);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives,
                mResonantFrequency, mQFactor);
                mQFactor, mFrequencyMapping);
    }

    @Override
@@ -110,8 +114,8 @@ public final class VibratorInfo implements Parcelable {
                + ", mCapabilities flags=" + Long.toBinaryString(mCapabilities)
                + ", mSupportedEffects=" + Arrays.toString(getSupportedEffectsNames())
                + ", mSupportedPrimitives=" + Arrays.toString(getSupportedPrimitivesNames())
                + ", mResonantFrequency=" + mResonantFrequency
                + ", mQFactor=" + mQFactor
                + ", mFrequencyMapping=" + mFrequencyMapping
                + '}';
    }

@@ -177,7 +181,7 @@ public final class VibratorInfo implements Parcelable {
     *         this vibrator is a composite of multiple physical devices.
     */
    public float getResonantFrequency() {
        return mResonantFrequency;
        return mFrequencyMapping.mResonantFrequencyHz;
    }

    /**
@@ -190,6 +194,52 @@ public final class VibratorInfo implements Parcelable {
        return mQFactor;
    }

    /**
     * Return a range of relative frequency values supported by the vibrator.
     *
     * @return A range of relative frequency values supported. The range will always contain the
     * value 0, representing the device resonant frequency. Devices without frequency control will
     * return the range [0,0]. Devices with frequency control will always return a range containing
     * the safe range [-1, 1].
     * @hide
     */
    public Range<Float> getFrequencyRange() {
        return mFrequencyMapping.mRelativeFrequencyRange;
    }

    /**
     * Return the maximum amplitude the vibrator can play at given relative frequency.
     *
     * @return a value in [0,1] representing the maximum amplitude the device can play at given
     * relative frequency. Devices without frequency control will return 1 for the input zero
     * (resonant frequency), and 0 to any other input. Devices with frequency control will return
     * the supported value, for input in {@code #getFrequencyRange()}, and 0 for any other input.
     * @hide
     */
    @FloatRange(from = 0, to = 1)
    public float getMaxAmplitude(float relativeFrequency) {
        if (mFrequencyMapping.isEmpty()) {
            // The vibrator has not provided values for frequency mapping.
            // Return the expected behavior for devices without frequency control.
            return Float.compare(relativeFrequency, 0) == 0 ? 1 : 0;
        }
        return mFrequencyMapping.getMaxAmplitude(relativeFrequency);
    }

    /**
     * Return absolute frequency value for this vibrator, in hertz, that corresponds to given
     * relative frequency.
     *
     * @retur a value in hertz that corresponds to given relative frequency. Input values outside
     * {@link #getFrequencyRange()} will return {@link Float#NaN}. Devices without frequency control
     * will return {@link Float#NaN} for any input.
     * @hide
     */
    @FloatRange(from = 0)
    public float getAbsoluteFrequency(float relativeFrequency) {
        return mFrequencyMapping.toHertz(relativeFrequency);
    }

    private String[] getCapabilitiesNames() {
        List<String> names = new ArrayList<>();
        if (hasCapability(IVibrator.CAP_ON_CALLBACK)) {
@@ -250,6 +300,209 @@ public final class VibratorInfo implements Parcelable {
        return array;
    }

    /**
     * Describes how frequency should be mapped to absolute values for a specific {@link Vibrator}.
     *
     * <p>This mapping is defined by the following parameters:
     *
     * <ol>
     *     <li>{@code minFrequency}, {@code resonantFrequency} and {@code frequencyResolution}, in
     *         hertz, provided by the vibrator.
     *     <li>{@code maxAmplitudes} a list of values in [0,1] provided by the vibrator, where
     *         {@code maxAmplitudes[i]} represents max supported amplitude at frequency
     *         {@code minFrequency + frequencyResolution * i}.
     *     <li>{@code maxFrequency = minFrequency + frequencyResolution * (maxAmplitudes.length-1)}
     *     <li>{@code suggestedSafeRangeHz} is the suggested frequency range in hertz that should be
     *         mapped to relative values -1 and 1, where 0 maps to {@code resonantFrequency}.
     * </ol>
     *
     * <p>The mapping is defined linearly by the following points:
     *
     * <ol>
     *     <li>{@code toHertz(relativeMinFrequency} = minFrequency
     *     <li>{@code                   toHertz(-1) = resonantFrequency - safeRange / 2}
     *     <li>{@code                    toHertz(0) = resonantFrequency}
     *     <li>{@code                    toHertz(1) = resonantFrequency + safeRange / 2}
     *     <li>{@code toHertz(relativeMaxFrequency) = maxFrequency}
     * </ol>
     *
     * @hide
     */
    public static final class FrequencyMapping implements Parcelable {
        private final float mMinFrequencyHz;
        private final float mResonantFrequencyHz;
        private final float mFrequencyResolutionHz;
        private final float mSuggestedSafeRangeHz;
        private final float[] mMaxAmplitudes;

        // Relative fields calculated from input values:
        private final Range<Float> mRelativeFrequencyRange;

        FrequencyMapping(Parcel in) {
            this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(),
                    in.createFloatArray());
        }

        /** @hide */
        public FrequencyMapping(float minFrequencyHz, float resonantFrequencyHz,
                float frequencyResolutionHz, float suggestedSafeRangeHz, float[] maxAmplitudes) {
            mMinFrequencyHz = minFrequencyHz;
            mResonantFrequencyHz = resonantFrequencyHz;
            mFrequencyResolutionHz = frequencyResolutionHz;
            mSuggestedSafeRangeHz = suggestedSafeRangeHz;
            mMaxAmplitudes = new float[maxAmplitudes == null ? 0 : maxAmplitudes.length];
            if (maxAmplitudes != null) {
                System.arraycopy(maxAmplitudes, 0, mMaxAmplitudes, 0, maxAmplitudes.length);
            }

            float maxFrequencyHz =
                    minFrequencyHz + frequencyResolutionHz * (mMaxAmplitudes.length - 1);
            if (Float.isNaN(resonantFrequencyHz) || Float.isNaN(minFrequencyHz)
                    || Float.isNaN(frequencyResolutionHz) || Float.isNaN(suggestedSafeRangeHz)
                    || resonantFrequencyHz < minFrequencyHz
                    || resonantFrequencyHz > maxFrequencyHz) {
                // Some required fields are undefined or have bad values.
                // Leave this mapping empty.
                mRelativeFrequencyRange = Range.create(0f, 0f);
                return;
            }

            // Calculate actual safe range, limiting the suggested one by the device supported range
            float safeDelta = MathUtils.min(
                    suggestedSafeRangeHz / 2,
                    resonantFrequencyHz - minFrequencyHz,
                    maxFrequencyHz - resonantFrequencyHz);
            mRelativeFrequencyRange = Range.create(
                    (minFrequencyHz - resonantFrequencyHz) / safeDelta,
                    (maxFrequencyHz - resonantFrequencyHz) / safeDelta);
        }

        /**
         * Returns true if this frequency mapping is empty, i.e. the only supported relative
         * frequency is 0 (resonant frequency).
         */
        public boolean isEmpty() {
            return Float.compare(mRelativeFrequencyRange.getLower(),
                    mRelativeFrequencyRange.getUpper()) == 0;
        }

        /**
         * Returns the frequency value in hertz that is mapped to the given relative frequency.
         *
         * @return The mapped frequency, in hertz, or {@link Float#NaN} is value outside the device
         * supported range.
         */
        public float toHertz(float relativeFrequency) {
            if (!mRelativeFrequencyRange.contains(relativeFrequency)) {
                return Float.NaN;
            }
            float relativeMinFrequency = mRelativeFrequencyRange.getLower();
            if (Float.compare(relativeMinFrequency, 0) == 0) {
                // relative supported range is [0,0], so toHertz(0) should be the resonant frequency
                return mResonantFrequencyHz;
            }
            float shift = (mMinFrequencyHz - mResonantFrequencyHz) / relativeMinFrequency;
            return mResonantFrequencyHz + relativeFrequency * shift;
        }

        /**
         * Returns the maximum amplitude the vibrator can reach while playing at given relative
         * frequency.
         *
         * @return A value in [0,1] representing the max amplitude supported at given relative
         * frequency. This will return 0 if frequency is outside supported range, or if max
         * amplitude mapping is empty.
         */
        public float getMaxAmplitude(float relativeFrequency) {
            float frequencyHz = toHertz(relativeFrequency);
            if (Float.isNaN(frequencyHz)) {
                // Unsupported frequency requested, vibrator cannot play at this frequency.
                return 0;
            }
            float position = (frequencyHz - mMinFrequencyHz) / mFrequencyResolutionHz;
            int floorIndex = (int) Math.floor(position);
            int ceilIndex = (int) Math.ceil(position);
            if (floorIndex < 0 || floorIndex >= mMaxAmplitudes.length) {
                if (mMaxAmplitudes.length > 0) {
                    // This should never happen if the setup of relative frequencies was correct.
                    Log.w(TAG, "Max amplitudes has " + mMaxAmplitudes.length
                            + " entries and was expected to cover the frequency " + frequencyHz
                            + " Hz when starting at min frequency of " + mMinFrequencyHz
                            + " Hz with resolution of " + mFrequencyResolutionHz + " Hz.");
                }
                return 0;
            }
            if (floorIndex != ceilIndex && ceilIndex < mMaxAmplitudes.length) {
                // Value in between two mapped frequency values, use the lowest supported one.
                return MathUtils.min(mMaxAmplitudes[floorIndex], mMaxAmplitudes[ceilIndex]);
            }
            return mMaxAmplitudes[floorIndex];
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeFloat(mMinFrequencyHz);
            dest.writeFloat(mResonantFrequencyHz);
            dest.writeFloat(mFrequencyResolutionHz);
            dest.writeFloat(mSuggestedSafeRangeHz);
            dest.writeFloatArray(mMaxAmplitudes);
        }

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

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof FrequencyMapping)) {
                return false;
            }
            FrequencyMapping that = (FrequencyMapping) o;
            return Float.compare(mMinFrequencyHz, that.mMinFrequencyHz) == 0
                    && Float.compare(mResonantFrequencyHz, that.mResonantFrequencyHz) == 0
                    && Float.compare(mFrequencyResolutionHz, that.mFrequencyResolutionHz) == 0
                    && Float.compare(mSuggestedSafeRangeHz, that.mSuggestedSafeRangeHz) == 0
                    && Arrays.equals(mMaxAmplitudes, that.mMaxAmplitudes);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mMinFrequencyHz, mFrequencyResolutionHz, mFrequencyResolutionHz,
                    mSuggestedSafeRangeHz, mMaxAmplitudes);
        }

        @Override
        public String toString() {
            return "FrequencyMapping{"
                    + "mMinFrequency=" + mMinFrequencyHz
                    + ", mResonantFrequency=" + mResonantFrequencyHz
                    + ", mMaxFrequency="
                    + (mMinFrequencyHz + mFrequencyResolutionHz * (mMaxAmplitudes.length - 1))
                    + ", mFrequencyResolution=" + mFrequencyResolutionHz
                    + ", mSuggestedSafeRange=" + mSuggestedSafeRangeHz
                    + ", mMaxAmplitudes count=" + mMaxAmplitudes.length
                    + '}';
        }

        @NonNull
        public static final Creator<FrequencyMapping> CREATOR =
                new Creator<FrequencyMapping>() {
                    @Override
                    public FrequencyMapping createFromParcel(Parcel in) {
                        return new FrequencyMapping(in);
                    }

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

    @NonNull
    public static final Creator<VibratorInfo> CREATOR =
            new Creator<VibratorInfo>() {
+165 −15

File changed.

Preview size limit exceeded, changes collapsed.

+120 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.server.vibrator;

import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.MathUtils;
import android.util.Range;

import java.util.ArrayList;
import java.util.List;

/** Adapts a {@link VibrationEffect} to a specific device, taking into account its capabilities. */
final class DeviceVibrationEffectAdapter implements VibrationEffectModifier<VibratorInfo> {

    /**
     * Adapts a sequence of {@link VibrationEffectSegment} to device's absolute frequency values
     * and respective supported amplitudes.
     *
     * <p>This adapter preserves the segment count.
     */
    interface AmplitudeFrequencyAdapter {
        List<VibrationEffectSegment> apply(List<VibrationEffectSegment> segments,
                VibratorInfo info);
    }

    private final AmplitudeFrequencyAdapter mAmplitudeFrequencyAdapter;

    DeviceVibrationEffectAdapter() {
        this(new ClippingAmplitudeFrequencyAdapter());
    }

    DeviceVibrationEffectAdapter(AmplitudeFrequencyAdapter amplitudeFrequencyAdapter) {
        mAmplitudeFrequencyAdapter = amplitudeFrequencyAdapter;
    }

    @Override
    public VibrationEffect apply(VibrationEffect effect, VibratorInfo info) {
        if (!(effect instanceof VibrationEffect.Composed)) {
            return effect;
        }

        VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
        List<VibrationEffectSegment> mappedSegments = mAmplitudeFrequencyAdapter.apply(
                composed.getSegments(), info);

        // TODO(b/167947076): add ramp to step adapter once PWLE capability is introduced
        // TODO(b/167947076): add filter that removes unsupported primitives
        // TODO(b/167947076): add filter that replaces unsupported prebaked with fallback

        return new VibrationEffect.Composed(mappedSegments, composed.getRepeatIndex());
    }

    /**
     * Adapter that clips frequency values to {@link VibratorInfo#getFrequencyRange()} and
     * amplitude values to respective {@link VibratorInfo#getMaxAmplitude}.
     *
     * <p>Devices with no frequency control will collapse all frequencies to zero and leave
     * amplitudes unchanged.
     */
    private static final class ClippingAmplitudeFrequencyAdapter
            implements AmplitudeFrequencyAdapter {
        @Override
        public List<VibrationEffectSegment> apply(List<VibrationEffectSegment> segments,
                VibratorInfo info) {
            List<VibrationEffectSegment> result = new ArrayList<>();
            int segmentCount = segments.size();
            for (int i = 0; i < segmentCount; i++) {
                VibrationEffectSegment segment = segments.get(i);
                if (segment instanceof StepSegment) {
                    result.add(apply((StepSegment) segment, info));
                } else if (segment instanceof RampSegment) {
                    result.add(apply((RampSegment) segment, info));
                } else {
                    result.add(segment);
                }
            }
            return result;
        }

        private StepSegment apply(StepSegment segment, VibratorInfo info) {
            float clampedFrequency = info.getFrequencyRange().clamp(segment.getFrequency());
            return new StepSegment(
                    MathUtils.min(segment.getAmplitude(), info.getMaxAmplitude(clampedFrequency)),
                    info.getAbsoluteFrequency(clampedFrequency),
                    (int) segment.getDuration());
        }

        private RampSegment apply(RampSegment segment, VibratorInfo info) {
            Range<Float> frequencyRange = info.getFrequencyRange();
            float clampedStartFrequency = frequencyRange.clamp(segment.getStartFrequency());
            float clampedEndFrequency = frequencyRange.clamp(segment.getEndFrequency());
            return new RampSegment(
                    MathUtils.min(segment.getStartAmplitude(),
                            info.getMaxAmplitude(clampedStartFrequency)),
                    MathUtils.min(segment.getEndAmplitude(),
                            info.getMaxAmplitude(clampedEndFrequency)),
                    info.getAbsoluteFrequency(clampedStartFrequency),
                    info.getAbsoluteFrequency(clampedEndFrequency),
                    (int) segment.getDuration());
        }
    }
}
+26 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.server.vibrator;

import android.os.VibrationEffect;

/** Function that applies a generic modifier to a {@link VibrationEffect}. */
interface VibrationEffectModifier<T> {

    /** Applies the modifier to given {@link VibrationEffect}. */
    VibrationEffect apply(VibrationEffect effect, T modifier);
}
+38 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading