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

Commit 14278aec authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce vibrator frequency mapping." into sc-dev

parents 7030bd35 e574d3bb
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