All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.media.VolumeShaper Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright 2017 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.media;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.BadParcelableException;
import android.os.Parcel;
import android.os.Parcelable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Objects;

/**
 * The {@code VolumeShaper} class is used to automatically control audio volume during media
 * playback, allowing simple implementation of transition effects and ducking.
 * It is created from implementations of {@code VolumeAutomation},
 * such as {@code MediaPlayer} and {@code AudioTrack} (referred to as "players" below),
 * by {@link MediaPlayer#createVolumeShaper} or {@link AudioTrack#createVolumeShaper}.
 *
 * A {@code VolumeShaper} is intended for short volume changes.
 * If the audio output sink changes during
 * a {@code VolumeShaper} transition, the precise curve position may be lost, and the
 * {@code VolumeShaper} may advance to the end of the curve for the new audio output sink.
 *
 * The {@code VolumeShaper} appears as an additional scaling on the audio output,
 * and adjusts independently of track or stream volume controls.
 */
public final class VolumeShaper implements AutoCloseable {
    /* member variables */
    private int mId;
    private final WeakReference mWeakPlayerBase;

    /* package */ VolumeShaper(
            @NonNull Configuration configuration, @NonNull PlayerBase playerBase) {
        mWeakPlayerBase = new WeakReference(playerBase);
        mId = applyPlayer(configuration, new Operation.Builder().defer().build());
    }

    /* package */ int getId() {
        return mId;
    }

    /**
     * Applies the {@link VolumeShaper.Operation} to the {@code VolumeShaper}.
     *
     * Applying {@link VolumeShaper.Operation#PLAY} after {@code PLAY}
     * or {@link VolumeShaper.Operation#REVERSE} after
     * {@code REVERSE} has no effect.
     *
     * Applying {@link VolumeShaper.Operation#PLAY} when the player
     * hasn't started will synchronously start the {@code VolumeShaper} when
     * playback begins.
     *
     * @param operation the {@code operation} to apply.
     * @throws IllegalStateException if the player is uninitialized or if there
     *         is a critical failure. In that case, the {@code VolumeShaper} should be
     *         recreated.
     */
    public void apply(@NonNull Operation operation) {
        /* void */ applyPlayer(new VolumeShaper.Configuration(mId), operation);
    }

    /**
     * Replaces the current {@code VolumeShaper}
     * {@code configuration} with a new {@code configuration}.
     *
     * This allows the user to change the volume shape
     * while the existing {@code VolumeShaper} is in effect.
     *
     * The effect of {@code replace()} is similar to an atomic close of
     * the existing {@code VolumeShaper} and creation of a new {@code VolumeShaper}.
     *
     * If the {@code operation} is {@link VolumeShaper.Operation#PLAY} then the
     * new curve starts immediately.
     *
     * If the {@code operation} is
     * {@link VolumeShaper.Operation#REVERSE}, then the new curve will
     * be delayed until {@code PLAY} is applied.
     *
     * @param configuration the new {@code configuration} to use.
     * @param operation the {@code operation} to apply to the {@code VolumeShaper}
     * @param join if true, match the start volume of the
     *             new {@code configuration} to the current volume of the existing
     *             {@code VolumeShaper}, to avoid discontinuity.
     * @throws IllegalStateException if the player is uninitialized or if there
     *         is a critical failure. In that case, the {@code VolumeShaper} should be
     *         recreated.
     */
    public void replace(
            @NonNull Configuration configuration, @NonNull Operation operation, boolean join) {
        mId = applyPlayer(
                configuration,
                new Operation.Builder(operation).replace(mId, join).build());
    }

    /**
     * Returns the current volume scale attributable to the {@code VolumeShaper}.
     *
     * This is the last volume from the {@code VolumeShaper} used for the player,
     * or the initial volume if the {@code VolumeShaper} hasn't been started with
     * {@link VolumeShaper.Operation#PLAY}.
     *
     * @return the volume, linearly represented as a value between 0.f and 1.f.
     * @throws IllegalStateException if the player is uninitialized or if there
     *         is a critical failure.  In that case, the {@code VolumeShaper} should be
     *         recreated.
     */
    public float getVolume() {
        return getStatePlayer(mId).getVolume();
    }

    /**
     * Releases the {@code VolumeShaper} object; any volume scale due to the
     * {@code VolumeShaper} is removed after closing.
     *
     * If the volume does not reach 1.f when the {@code VolumeShaper} is closed
     * (or finalized), there may be an abrupt change of volume.
     *
     * {@code close()} may be safely called after a prior {@code close()}.
     * This class implements the Java {@code AutoClosable} interface and
     * may be used with try-with-resources.
     */
    @Override
    public void close() {
        try {
            /* void */ applyPlayer(
                    new VolumeShaper.Configuration(mId),
                    new Operation.Builder().terminate().build());
        } catch (IllegalStateException ise) {
            ; // ok
        }
        if (mWeakPlayerBase != null) {
            mWeakPlayerBase.clear();
        }
    }

    @Override
    protected void finalize() {
        close(); // ensure we remove the native VolumeShaper
    }

    /**
     * Internal call to apply the {@code configuration} and {@code operation} to the player.
     * Returns a valid shaper id or throws the appropriate exception.
     * @param configuration
     * @param operation
     * @return id a non-negative shaper id.
     * @throws IllegalStateException if the player has been deallocated or is uninitialized.
     */
    private int applyPlayer(
            @NonNull VolumeShaper.Configuration configuration,
            @NonNull VolumeShaper.Operation operation) {
        final int id;
        if (mWeakPlayerBase != null) {
            PlayerBase player = mWeakPlayerBase.get();
            if (player == null) {
                throw new IllegalStateException("player deallocated");
            }
            id = player.playerApplyVolumeShaper(configuration, operation);
        } else {
            throw new IllegalStateException("uninitialized shaper");
        }
        if (id < 0) {
            // TODO - get INVALID_OPERATION from platform.
            final int VOLUME_SHAPER_INVALID_OPERATION = -38; // must match with platform
            // Due to RPC handling, we translate integer codes to exceptions right before
            // delivering to the user.
            if (id == VOLUME_SHAPER_INVALID_OPERATION) {
                throw new IllegalStateException("player or VolumeShaper deallocated");
            } else {
                throw new IllegalArgumentException("invalid configuration or operation: " + id);
            }
        }
        return id;
    }

    /**
     * Internal call to retrieve the current {@code VolumeShaper} state.
     * @param id
     * @return the current {@code VolumeShaper.State}
     * @throws IllegalStateException if the player has been deallocated or is uninitialized.
     */
    private @NonNull VolumeShaper.State getStatePlayer(int id) {
        final VolumeShaper.State state;
        if (mWeakPlayerBase != null) {
            PlayerBase player = mWeakPlayerBase.get();
            if (player == null) {
                throw new IllegalStateException("player deallocated");
            }
            state = player.playerGetVolumeShaperState(id);
        } else {
            throw new IllegalStateException("uninitialized shaper");
        }
        if (state == null) {
            throw new IllegalStateException("shaper cannot be found");
        }
        return state;
    }

    /**
     * The {@code VolumeShaper.Configuration} class contains curve
     * and duration information.
     * It is constructed by the {@link VolumeShaper.Configuration.Builder}.
     * 

* A {@code VolumeShaper.Configuration} is used by * {@link VolumeAutomation#createVolumeShaper(Configuration) * VolumeAutomation.createVolumeShaper(Configuration)} to create * a {@code VolumeShaper} and * by {@link VolumeShaper#replace(Configuration, Operation, boolean) * VolumeShaper.replace(Configuration, Operation, boolean)} * to replace an existing {@code configuration}. *

* The {@link AudioTrack} and {@link MediaPlayer} classes implement * the {@link VolumeAutomation} interface. */ public static final class Configuration implements Parcelable { private static final int MAXIMUM_CURVE_POINTS = 16; /** * Returns the maximum number of curve points allowed for * {@link VolumeShaper.Builder#setCurve(float[], float[])}. */ public static int getMaximumCurvePoints() { return MAXIMUM_CURVE_POINTS; } // These values must match the native VolumeShaper::Configuration::Type /** @hide */ @IntDef({ TYPE_ID, TYPE_SCALE, }) @Retention(RetentionPolicy.SOURCE) public @interface Type {} /** * Specifies a {@link VolumeShaper} handle created by {@link #VolumeShaper(int)} * from an id returned by {@code setVolumeShaper()}. * The type, curve, etc. may not be queried from * a {@code VolumeShaper} object of this type; * the handle is used to identify and change the operation of * an existing {@code VolumeShaper} sent to the player. */ /* package */ static final int TYPE_ID = 0; /** * Specifies a {@link VolumeShaper} to be used * as an additional scale to the current volume. * This is created by the {@link VolumeShaper.Builder}. */ /* package */ static final int TYPE_SCALE = 1; // These values must match the native InterpolatorType enumeration. /** @hide */ @IntDef({ INTERPOLATOR_TYPE_STEP, INTERPOLATOR_TYPE_LINEAR, INTERPOLATOR_TYPE_CUBIC, INTERPOLATOR_TYPE_CUBIC_MONOTONIC, }) @Retention(RetentionPolicy.SOURCE) public @interface InterpolatorType {} /** * Stepwise volume curve. */ public static final int INTERPOLATOR_TYPE_STEP = 0; /** * Linear interpolated volume curve. */ public static final int INTERPOLATOR_TYPE_LINEAR = 1; /** * Cubic interpolated volume curve. * This is default if unspecified. */ public static final int INTERPOLATOR_TYPE_CUBIC = 2; /** * Cubic interpolated volume curve * that preserves local monotonicity. * So long as the control points are locally monotonic, * the curve interpolation between those points are monotonic. * This is useful for cubic spline interpolated * volume ramps and ducks. */ public static final int INTERPOLATOR_TYPE_CUBIC_MONOTONIC = 3; // These values must match the native VolumeShaper::Configuration::InterpolatorType /** @hide */ @IntDef({ OPTION_FLAG_VOLUME_IN_DBFS, OPTION_FLAG_CLOCK_TIME, }) @Retention(RetentionPolicy.SOURCE) public @interface OptionFlag {} /** * @hide * Use a dB full scale volume range for the volume curve. *

* The volume scale is typically from 0.f to 1.f on a linear scale; * this option changes to -inf to 0.f on a db full scale, * where 0.f is equivalent to a scale of 1.f. */ public static final int OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0); /** * @hide * Use clock time instead of media time. *

* The default implementation of {@code VolumeShaper} is to apply * volume changes by the media time of the player. * Hence, the {@code VolumeShaper} will speed or slow down to * match player changes of playback rate, pause, or resume. *

* The {@code OPTION_FLAG_CLOCK_TIME} option allows the {@code VolumeShaper} * progress to be determined by clock time instead of media time. */ public static final int OPTION_FLAG_CLOCK_TIME = (1 << 1); private static final int OPTION_FLAG_PUBLIC_ALL = OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME; /** * A one second linear ramp from silence to full volume. * Use {@link VolumeShaper.Builder#reflectTimes()} * or {@link VolumeShaper.Builder#invertVolumes()} to generate * the matching linear duck. */ public static final Configuration LINEAR_RAMP = new VolumeShaper.Configuration.Builder() .setInterpolatorType(INTERPOLATOR_TYPE_LINEAR) .setCurve(new float[] {0.f, 1.f} /* times */, new float[] {0.f, 1.f} /* volumes */) .setDuration(1000) .build(); /** * A one second cubic ramp from silence to full volume. * Use {@link VolumeShaper.Builder#reflectTimes()} * or {@link VolumeShaper.Builder#invertVolumes()} to generate * the matching cubic duck. */ public static final Configuration CUBIC_RAMP = new VolumeShaper.Configuration.Builder() .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) .setCurve(new float[] {0.f, 1.f} /* times */, new float[] {0.f, 1.f} /* volumes */) .setDuration(1000) .build(); /** * A one second sine curve * from silence to full volume for energy preserving cross fades. * Use {@link VolumeShaper.Builder#reflectTimes()} to generate * the matching cosine duck. */ public static final Configuration SINE_RAMP; /** * A one second sine-squared s-curve ramp * from silence to full volume. * Use {@link VolumeShaper.Builder#reflectTimes()} * or {@link VolumeShaper.Builder#invertVolumes()} to generate * the matching sine-squared s-curve duck. */ public static final Configuration SCURVE_RAMP; static { final int POINTS = MAXIMUM_CURVE_POINTS; final float times[] = new float[POINTS]; final float sines[] = new float[POINTS]; final float scurve[] = new float[POINTS]; for (int i = 0; i < POINTS; ++i) { times[i] = (float)i / (POINTS - 1); final float sine = (float)Math.sin(times[i] * Math.PI / 2.); sines[i] = sine; scurve[i] = sine * sine; } SINE_RAMP = new VolumeShaper.Configuration.Builder() .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) .setCurve(times, sines) .setDuration(1000) .build(); SCURVE_RAMP = new VolumeShaper.Configuration.Builder() .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) .setCurve(times, scurve) .setDuration(1000) .build(); } /* * member variables - these are all final */ // type of VolumeShaper @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mType; // valid when mType is TYPE_ID @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mId; // valid when mType is TYPE_SCALE @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mOptionFlags; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final double mDurationMs; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mInterpolatorType; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final float[] mTimes; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final float[] mVolumes; @Override public String toString() { return "VolumeShaper.Configuration{" + "mType = " + mType + ", mId = " + mId + (mType == TYPE_ID ? "}" : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase() + ", mDurationMs = " + mDurationMs + ", mInterpolatorType = " + mInterpolatorType + ", mTimes[] = " + Arrays.toString(mTimes) + ", mVolumes[] = " + Arrays.toString(mVolumes) + "}"); } @Override public int hashCode() { return mType == TYPE_ID ? Objects.hash(mType, mId) : Objects.hash(mType, mId, mOptionFlags, mDurationMs, mInterpolatorType, Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes)); } @Override public boolean equals(Object o) { if (!(o instanceof Configuration)) return false; if (o == this) return true; final Configuration other = (Configuration) o; // Note that exact floating point equality may not be guaranteed // for a theoretically idempotent operation; for example, // there are many cases where a + b - b != a. return mType == other.mType && mId == other.mId && (mType == TYPE_ID || (mOptionFlags == other.mOptionFlags && mDurationMs == other.mDurationMs && mInterpolatorType == other.mInterpolatorType && Arrays.equals(mTimes, other.mTimes) && Arrays.equals(mVolumes, other.mVolumes))); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { VolumeShaperConfiguration parcelable = toParcelable(); parcelable.writeToParcel(dest, flags); } /** @hide */ public VolumeShaperConfiguration toParcelable() { VolumeShaperConfiguration parcelable = new VolumeShaperConfiguration(); parcelable.type = typeToAidl(mType); parcelable.id = mId; if (mType != TYPE_ID) { parcelable.optionFlags = optionFlagsToAidl(mOptionFlags); parcelable.durationMs = mDurationMs; parcelable.interpolatorConfig = toInterpolatorParcelable(); } return parcelable; } private InterpolatorConfig toInterpolatorParcelable() { InterpolatorConfig parcelable = new InterpolatorConfig(); parcelable.type = interpolatorTypeToAidl(mInterpolatorType); parcelable.firstSlope = 0.f; // first slope (specifying for native side) parcelable.lastSlope = 0.f; // last slope (specifying for native side) parcelable.xy = new float[mTimes.length * 2]; for (int i = 0; i < mTimes.length; ++i) { parcelable.xy[i * 2] = mTimes[i]; parcelable.xy[i * 2 + 1] = mVolumes[i]; } return parcelable; } /** @hide */ public static Configuration fromParcelable(VolumeShaperConfiguration parcelable) { // this needs to match the native VolumeShaper.Configuration parceling final int type = typeFromAidl(parcelable.type); final int id = parcelable.id; if (type == TYPE_ID) { return new VolumeShaper.Configuration(id); } else { final int optionFlags = optionFlagsFromAidl(parcelable.optionFlags); final double durationMs = parcelable.durationMs; final int interpolatorType = interpolatorTypeFromAidl( parcelable.interpolatorConfig.type); // parcelable.interpolatorConfig.firstSlope is ignored on the Java side // parcelable.interpolatorConfig.lastSlope is ignored on the Java side final int length = parcelable.interpolatorConfig.xy.length; if (length % 2 != 0) { throw new android.os.BadParcelableException("xy length must be even"); } final float[] times = new float[length / 2]; final float[] volumes = new float[length / 2]; for (int i = 0; i < length / 2; ++i) { times[i] = parcelable.interpolatorConfig.xy[i * 2]; volumes[i] = parcelable.interpolatorConfig.xy[i * 2 + 1]; } return new VolumeShaper.Configuration( type, id, optionFlags, durationMs, interpolatorType, times, volumes); } } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public VolumeShaper.Configuration createFromParcel(Parcel p) { return fromParcelable(VolumeShaperConfiguration.CREATOR.createFromParcel(p)); } @Override public VolumeShaper.Configuration[] newArray(int size) { return new VolumeShaper.Configuration[size]; } }; private static @InterpolatorType int interpolatorTypeFromAidl(@android.media.InterpolatorType int aidl) { switch (aidl) { case android.media.InterpolatorType.STEP: return INTERPOLATOR_TYPE_STEP; case android.media.InterpolatorType.LINEAR: return INTERPOLATOR_TYPE_LINEAR; case android.media.InterpolatorType.CUBIC: return INTERPOLATOR_TYPE_CUBIC; case android.media.InterpolatorType.CUBIC_MONOTONIC: return INTERPOLATOR_TYPE_CUBIC_MONOTONIC; default: throw new BadParcelableException("Unknown interpolator type"); } } private static @android.media.InterpolatorType int interpolatorTypeToAidl(@InterpolatorType int type) { switch (type) { case INTERPOLATOR_TYPE_STEP: return android.media.InterpolatorType.STEP; case INTERPOLATOR_TYPE_LINEAR: return android.media.InterpolatorType.LINEAR; case INTERPOLATOR_TYPE_CUBIC: return android.media.InterpolatorType.CUBIC; case INTERPOLATOR_TYPE_CUBIC_MONOTONIC: return android.media.InterpolatorType.CUBIC_MONOTONIC; default: throw new RuntimeException("Unknown interpolator type"); } } private static @Type int typeFromAidl(@android.media.VolumeShaperConfigurationType int aidl) { switch (aidl) { case VolumeShaperConfigurationType.ID: return TYPE_ID; case VolumeShaperConfigurationType.SCALE: return TYPE_SCALE; default: throw new BadParcelableException("Unknown type"); } } private static @android.media.VolumeShaperConfigurationType int typeToAidl(@Type int type) { switch (type) { case TYPE_ID: return VolumeShaperConfigurationType.ID; case TYPE_SCALE: return VolumeShaperConfigurationType.SCALE; default: throw new RuntimeException("Unknown type"); } } private static int optionFlagsFromAidl(int aidl) { int result = 0; if ((aidl & (1 << VolumeShaperConfigurationOptionFlag.VOLUME_IN_DBFS)) != 0) { result |= OPTION_FLAG_VOLUME_IN_DBFS; } if ((aidl & (1 << VolumeShaperConfigurationOptionFlag.CLOCK_TIME)) != 0) { result |= OPTION_FLAG_CLOCK_TIME; } return result; } private static int optionFlagsToAidl(int flags) { int result = 0; if ((flags & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { result |= (1 << VolumeShaperConfigurationOptionFlag.VOLUME_IN_DBFS); } if ((flags & OPTION_FLAG_CLOCK_TIME) != 0) { result |= (1 << VolumeShaperConfigurationOptionFlag.CLOCK_TIME); } return result; } /** * @hide * Constructs a {@code VolumeShaper} from an id. * * This is an opaque handle for controlling a {@code VolumeShaper} that has * already been sent to a player. The {@code id} is returned from the * initial {@code setVolumeShaper()} call on success. * * These configurations are for native use only, * they are never returned directly to the user. * * @param id * @throws IllegalArgumentException if id is negative. */ public Configuration(int id) { if (id < 0) { throw new IllegalArgumentException("negative id " + id); } mType = TYPE_ID; mId = id; mInterpolatorType = 0; mOptionFlags = 0; mDurationMs = 0; mTimes = null; mVolumes = null; } /** * Direct constructor for VolumeShaper. * Use the Builder instead. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private Configuration(@Type int type, int id, @OptionFlag int optionFlags, double durationMs, @InterpolatorType int interpolatorType, @NonNull float[] times, @NonNull float[] volumes) { mType = type; mId = id; mOptionFlags = optionFlags; mDurationMs = durationMs; mInterpolatorType = interpolatorType; // Builder should have cloned these arrays already. mTimes = times; mVolumes = volumes; } /** * @hide * Returns the {@code VolumeShaper} type. */ public @Type int getType() { return mType; } /** * @hide * Returns the {@code VolumeShaper} id. */ public int getId() { return mId; } /** * Returns the interpolator type. */ public @InterpolatorType int getInterpolatorType() { return mInterpolatorType; } /** * @hide * Returns the option flags */ public @OptionFlag int getOptionFlags() { return mOptionFlags & OPTION_FLAG_PUBLIC_ALL; } /* package */ @OptionFlag int getAllOptionFlags() { return mOptionFlags; } /** * Returns the duration of the volume shape in milliseconds. */ public long getDuration() { // casting is safe here as the duration was set as a long in the Builder return (long) mDurationMs; } /** * Returns the times (x) coordinate array of the volume curve points. */ public float[] getTimes() { return mTimes; } /** * Returns the volumes (y) coordinate array of the volume curve points. */ public float[] getVolumes() { return mVolumes; } /** * Checks the validity of times and volumes point representation. * * {@code times[]} and {@code volumes[]} are two arrays representing points * for the volume curve. * * Note that {@code times[]} and {@code volumes[]} are explicitly checked against * null here to provide the proper error string - those are legitimate * arguments to this method. * * @param times the x coordinates for the points, * must be between 0.f and 1.f and be monotonic. * @param volumes the y coordinates for the points, * must be between 0.f and 1.f for linear and * must be no greater than 0.f for log (dBFS). * @param log set to true if the scale is logarithmic. * @return null if no error, or the reason in a {@code String} for an error. */ private static @Nullable String checkCurveForErrors( @Nullable float[] times, @Nullable float[] volumes, boolean log) { if (times == null) { return "times array must be non-null"; } else if (volumes == null) { return "volumes array must be non-null"; } else if (times.length != volumes.length) { return "array length must match"; } else if (times.length < 2) { return "array length must be at least 2"; } else if (times.length > MAXIMUM_CURVE_POINTS) { return "array length must be no larger than " + MAXIMUM_CURVE_POINTS; } else if (times[0] != 0.f) { return "times must start at 0.f"; } else if (times[times.length - 1] != 1.f) { return "times must end at 1.f"; } // validate points along the curve for (int i = 1; i < times.length; ++i) { if (!(times[i] > times[i - 1]) /* handle nan */) { return "times not monotonic increasing, check index " + i; } } if (log) { for (int i = 0; i < volumes.length; ++i) { if (!(volumes[i] <= 0.f) /* handle nan */) { return "volumes for log scale cannot be positive, " + "check index " + i; } } } else { for (int i = 0; i < volumes.length; ++i) { if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) { return "volumes for linear scale must be between 0.f and 1.f, " + "check index " + i; } } } return null; // no errors } private static void checkCurveForErrorsAndThrowException( @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) { final String error = checkCurveForErrors(times, volumes, log); if (error != null) { if (ise) { throw new IllegalStateException(error); } else { throw new IllegalArgumentException(error); } } } private static void checkValidVolumeAndThrowException(float volume, boolean log) { if (log) { if (!(volume <= 0.f) /* handle nan */) { throw new IllegalArgumentException("dbfs volume must be 0.f or less"); } } else { if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) { throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f"); } } } private static void clampVolume(float[] volumes, boolean log) { if (log) { for (int i = 0; i < volumes.length; ++i) { if (!(volumes[i] <= 0.f) /* handle nan */) { volumes[i] = 0.f; } } } else { for (int i = 0; i < volumes.length; ++i) { if (!(volumes[i] >= 0.f) /* handle nan */) { volumes[i] = 0.f; } else if (!(volumes[i] <= 1.f)) { volumes[i] = 1.f; } } } } /** * Builder class for a {@link VolumeShaper.Configuration} object. *

Here is an example where {@code Builder} is used to define the * {@link VolumeShaper.Configuration}. * *

         * VolumeShaper.Configuration LINEAR_RAMP =
         *         new VolumeShaper.Configuration.Builder()
         *             .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
         *             .setCurve(new float[] { 0.f, 1.f }, // times
         *                       new float[] { 0.f, 1.f }) // volumes
         *             .setDuration(1000)
         *             .build();
         * 
*

*/ public static final class Builder { private int mType = TYPE_SCALE; private int mId = -1; // invalid private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC; private int mOptionFlags = OPTION_FLAG_CLOCK_TIME; private double mDurationMs = 1000.; private float[] mTimes = null; private float[] mVolumes = null; /** * Constructs a new {@code Builder} with the defaults. */ public Builder() { } /** * Constructs a new {@code Builder} with settings * copied from a given {@code VolumeShaper.Configuration}. * @param configuration prototypical configuration * which will be reused in the new {@code Builder}. */ public Builder(@NonNull Configuration configuration) { mType = configuration.getType(); mId = configuration.getId(); mOptionFlags = configuration.getAllOptionFlags(); mInterpolatorType = configuration.getInterpolatorType(); mDurationMs = configuration.getDuration(); mTimes = configuration.getTimes().clone(); mVolumes = configuration.getVolumes().clone(); } /** * @hide * Set the {@code id} for system defined shapers. * @param id the {@code id} to set. If non-negative, then it is used. * If -1, then the system is expected to assign one. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code id} < -1. */ public @NonNull Builder setId(int id) { if (id < -1) { throw new IllegalArgumentException("invalid id: " + id); } mId = id; return this; } /** * Sets the interpolator type. * * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}. * * @param interpolatorType method of interpolation used for the volume curve. * One of {@link #INTERPOLATOR_TYPE_STEP}, * {@link #INTERPOLATOR_TYPE_LINEAR}, * {@link #INTERPOLATOR_TYPE_CUBIC}, * {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code interpolatorType} is not valid. */ public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) { switch (interpolatorType) { case INTERPOLATOR_TYPE_STEP: case INTERPOLATOR_TYPE_LINEAR: case INTERPOLATOR_TYPE_CUBIC: case INTERPOLATOR_TYPE_CUBIC_MONOTONIC: mInterpolatorType = interpolatorType; break; default: throw new IllegalArgumentException("invalid interpolatorType: " + interpolatorType); } return this; } /** * @hide * Sets the optional flags * * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has * changed the volume curve needs to be set again as the acceptable * volume domain has changed. * * @param optionFlags new value to replace the old {@code optionFlags}. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if flag is not recognized. */ @TestApi public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) { if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) { throw new IllegalArgumentException("invalid bits in flag: " + optionFlags); } mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags; return this; } /** * Sets the {@code VolumeShaper} duration in milliseconds. * * If omitted, the default duration is 1 second. * * @param durationMillis * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code durationMillis} * is not strictly positive. */ public @NonNull Builder setDuration(long durationMillis) { if (durationMillis <= 0) { throw new IllegalArgumentException( "duration: " + durationMillis + " not positive"); } mDurationMs = (double) durationMillis; return this; } /** * Sets the volume curve. * * The volume curve is represented by a set of control points given by * two float arrays of equal length, * one representing the time (x) coordinates * and one corresponding to the volume (y) coordinates. * The length must be at least 2 * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}. *

* The volume curve is normalized as follows: * time (x) coordinates should be monotonically increasing, from 0.f to 1.f; * volume (y) coordinates must be within 0.f to 1.f. *

* The time scale is set by {@link #setDuration}. *

* @param times an array of float values representing * the time line of the volume curve. * @param volumes an array of float values representing * the amplitude of the volume curve. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid. */ /* Note: volume (y) coordinates must be non-positive for log scaling, * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set. */ public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */); mTimes = times.clone(); mVolumes = volumes.clone(); return this; } /** * Reflects the volume curve so that * the shaper changes volume from the end * to the start. * * @return the same {@code Builder} instance. * @throws IllegalStateException if curve has not been set. */ public @NonNull Builder reflectTimes() { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); int i; for (i = 0; i < mTimes.length / 2; ++i) { float temp = mTimes[i]; mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i]; mTimes[mTimes.length - 1 - i] = 1.f - temp; temp = mVolumes[i]; mVolumes[i] = mVolumes[mVolumes.length - 1 - i]; mVolumes[mVolumes.length - 1 - i] = temp; } if ((mTimes.length & 1) != 0) { mTimes[i] = 1.f - mTimes[i]; } return this; } /** * Inverts the volume curve so that the max volume * becomes the min volume and vice versa. * * @return the same {@code Builder} instance. * @throws IllegalStateException if curve has not been set. */ public @NonNull Builder invertVolumes() { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); float min = mVolumes[0]; float max = mVolumes[0]; for (int i = 1; i < mVolumes.length; ++i) { if (mVolumes[i] < min) { min = mVolumes[i]; } else if (mVolumes[i] > max) { max = mVolumes[i]; } } final float maxmin = max + min; for (int i = 0; i < mVolumes.length; ++i) { mVolumes[i] = maxmin - mVolumes[i]; } return this; } /** * Scale the curve end volume to a target value. * * Keeps the start volume the same. * This works best if the volume curve is monotonic. * * @param volume the target end volume to use. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code volume} is not valid. * @throws IllegalStateException if curve has not been set. */ public @NonNull Builder scaleToEndVolume(float volume) { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); checkValidVolumeAndThrowException(volume, log); final float startVolume = mVolumes[0]; final float endVolume = mVolumes[mVolumes.length - 1]; if (endVolume == startVolume) { // match with linear ramp final float offset = volume - startVolume; for (int i = 0; i < mVolumes.length; ++i) { mVolumes[i] = mVolumes[i] + offset * mTimes[i]; } } else { // scale final float scale = (volume - startVolume) / (endVolume - startVolume); for (int i = 0; i < mVolumes.length; ++i) { mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume; } } clampVolume(mVolumes, log); return this; } /** * Scale the curve start volume to a target value. * * Keeps the end volume the same. * This works best if the volume curve is monotonic. * * @param volume the target start volume to use. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code volume} is not valid. * @throws IllegalStateException if curve has not been set. */ public @NonNull Builder scaleToStartVolume(float volume) { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); checkValidVolumeAndThrowException(volume, log); final float startVolume = mVolumes[0]; final float endVolume = mVolumes[mVolumes.length - 1]; if (endVolume == startVolume) { // match with linear ramp final float offset = volume - startVolume; for (int i = 0; i < mVolumes.length; ++i) { mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]); } } else { final float scale = (volume - endVolume) / (startVolume - endVolume); for (int i = 0; i < mVolumes.length; ++i) { mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume; } } clampVolume(mVolumes, log); return this; } /** * Builds a new {@link VolumeShaper} object. * * @return a new {@link VolumeShaper} object. * @throws IllegalStateException if curve is not properly set. */ public @NonNull Configuration build() { final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); return new Configuration(mType, mId, mOptionFlags, mDurationMs, mInterpolatorType, mTimes, mVolumes); } } // Configuration.Builder } // Configuration /** * The {@code VolumeShaper.Operation} class is used to specify operations * to the {@code VolumeShaper} that affect the volume change. */ public static final class Operation implements Parcelable { /** * Forward playback from current volume time position. * At the end of the {@code VolumeShaper} curve, * the last volume value persists. */ public static final Operation PLAY = new VolumeShaper.Operation.Builder() .build(); /** * Reverse playback from current volume time position. * When the position reaches the start of the {@code VolumeShaper} curve, * the first volume value persists. */ public static final Operation REVERSE = new VolumeShaper.Operation.Builder() .reverse() .build(); // No user serviceable parts below. // These flags must match the native VolumeShaper::Operation::Flag /** @hide */ @IntDef({ FLAG_NONE, FLAG_REVERSE, FLAG_TERMINATE, FLAG_JOIN, FLAG_DEFER, }) @Retention(RetentionPolicy.SOURCE) public @interface Flag {} /** * No special {@code VolumeShaper} operation. */ private static final int FLAG_NONE = 0; /** * Reverse the {@code VolumeShaper} progress. * * Reverses the {@code VolumeShaper} curve from its current * position. If the {@code VolumeShaper} curve has not started, * it automatically is considered finished. */ private static final int FLAG_REVERSE = 1 << 0; /** * Terminate the existing {@code VolumeShaper}. * This flag is generally used by itself; * it takes precedence over all other flags. */ private static final int FLAG_TERMINATE = 1 << 1; /** * Attempt to join as best as possible to the previous {@code VolumeShaper}. * This requires the previous {@code VolumeShaper} to be active and * {@link #setReplaceId} to be set. */ private static final int FLAG_JOIN = 1 << 2; /** * Defer playback until next operation is sent. This is used * when starting a {@code VolumeShaper} effect. */ private static final int FLAG_DEFER = 1 << 3; /** * Use the id specified in the configuration, creating * {@code VolumeShaper} as needed; the configuration should be * TYPE_SCALE. */ private static final int FLAG_CREATE_IF_NEEDED = 1 << 4; private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mFlags; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final int mReplaceId; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final float mXOffset; @Override public String toString() { return "VolumeShaper.Operation{" + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase() + ", mReplaceId = " + mReplaceId + ", mXOffset = " + mXOffset + "}"; } @Override public int hashCode() { return Objects.hash(mFlags, mReplaceId, mXOffset); } @Override public boolean equals(Object o) { if (!(o instanceof Operation)) return false; if (o == this) return true; final Operation other = (Operation) o; return mFlags == other.mFlags && mReplaceId == other.mReplaceId && Float.compare(mXOffset, other.mXOffset) == 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { toParcelable().writeToParcel(dest, flags); } /** @hide */ public VolumeShaperOperation toParcelable() { VolumeShaperOperation result = new VolumeShaperOperation(); result.flags = flagsToAidl(mFlags); result.replaceId = mReplaceId; result.xOffset = mXOffset; return result; } /** @hide */ public static Operation fromParcelable(VolumeShaperOperation parcelable) { return new VolumeShaper.Operation( flagsFromAidl(parcelable.flags), parcelable.replaceId, parcelable.xOffset); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public VolumeShaper.Operation createFromParcel(Parcel p) { return fromParcelable(VolumeShaperOperation.CREATOR.createFromParcel(p)); } @Override public VolumeShaper.Operation[] newArray(int size) { return new VolumeShaper.Operation[size]; } }; private static int flagsFromAidl(int aidl) { int result = 0; if ((aidl & (1 << VolumeShaperOperationFlag.REVERSE)) != 0) { result |= FLAG_REVERSE; } if ((aidl & (1 << VolumeShaperOperationFlag.TERMINATE)) != 0) { result |= FLAG_TERMINATE; } if ((aidl & (1 << VolumeShaperOperationFlag.JOIN)) != 0) { result |= FLAG_JOIN; } if ((aidl & (1 << VolumeShaperOperationFlag.DELAY)) != 0) { result |= FLAG_DEFER; } if ((aidl & (1 << VolumeShaperOperationFlag.CREATE_IF_NECESSARY)) != 0) { result |= FLAG_CREATE_IF_NEEDED; } return result; } private static int flagsToAidl(int flags) { int result = 0; if ((flags & FLAG_REVERSE) != 0) { result |= (1 << VolumeShaperOperationFlag.REVERSE); } if ((flags & FLAG_TERMINATE) != 0) { result |= (1 << VolumeShaperOperationFlag.TERMINATE); } if ((flags & FLAG_JOIN) != 0) { result |= (1 << VolumeShaperOperationFlag.JOIN); } if ((flags & FLAG_DEFER) != 0) { result |= (1 << VolumeShaperOperationFlag.DELAY); } if ((flags & FLAG_CREATE_IF_NEEDED) != 0) { result |= (1 << VolumeShaperOperationFlag.CREATE_IF_NECESSARY); } return result; } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private Operation(@Flag int flags, int replaceId, float xOffset) { mFlags = flags; mReplaceId = replaceId; mXOffset = xOffset; } /** * @hide * {@code Builder} class for {@link VolumeShaper.Operation} object. * * Not for public use. */ public static final class Builder { int mFlags; int mReplaceId; float mXOffset; /** * Constructs a new {@code Builder} with the defaults. */ public Builder() { mFlags = 0; mReplaceId = -1; mXOffset = Float.NaN; } /** * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation} * @param operation the {@code VolumeShaper.operation} whose data will be * reused in the new {@code Builder}. */ public Builder(@NonNull VolumeShaper.Operation operation) { mReplaceId = operation.mReplaceId; mFlags = operation.mFlags; mXOffset = operation.mXOffset; } /** * Replaces the previous {@code VolumeShaper} specified by {@code id}. * * The {@code VolumeShaper} specified by the {@code id} is removed * if it exists. The configuration should be TYPE_SCALE. * * @param id the {@code id} of the previous {@code VolumeShaper}. * @param join if true, match the volume of the previous * shaper to the start volume of the new {@code VolumeShaper}. * @return the same {@code Builder} instance. */ public @NonNull Builder replace(int id, boolean join) { mReplaceId = id; if (join) { mFlags |= FLAG_JOIN; } else { mFlags &= ~FLAG_JOIN; } return this; } /** * Defers all operations. * @return the same {@code Builder} instance. */ public @NonNull Builder defer() { mFlags |= FLAG_DEFER; return this; } /** * Terminates the {@code VolumeShaper}. * * Do not call directly, use {@link VolumeShaper#close()}. * @return the same {@code Builder} instance. */ public @NonNull Builder terminate() { mFlags |= FLAG_TERMINATE; return this; } /** * Reverses direction. * @return the same {@code Builder} instance. */ public @NonNull Builder reverse() { mFlags ^= FLAG_REVERSE; return this; } /** * Use the id specified in the configuration, creating * {@code VolumeShaper} only as needed; the configuration should be * TYPE_SCALE. * * If the {@code VolumeShaper} with the same id already exists * then the operation has no effect. * * @return the same {@code Builder} instance. */ public @NonNull Builder createIfNeeded() { mFlags |= FLAG_CREATE_IF_NEEDED; return this; } /** * Sets the {@code xOffset} to use for the {@code VolumeShaper}. * * The {@code xOffset} is the position on the volume curve, * and setting takes effect when the {@code VolumeShaper} is used next. * * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f, * or a Float.NaN. */ public @NonNull Builder setXOffset(float xOffset) { if (xOffset < -0.f) { throw new IllegalArgumentException("Negative xOffset not allowed"); } else if (xOffset > 1.f) { throw new IllegalArgumentException("xOffset > 1.f not allowed"); } // Float.NaN passes through mXOffset = xOffset; return this; } /** * Sets the operation flag. Do not call this directly but one of the * other builder methods. * * @param flags new value for {@code flags}, consisting of ORed flags. * @return the same {@code Builder} instance. * @throws IllegalArgumentException if {@code flags} contains invalid set bits. */ private @NonNull Builder setFlags(@Flag int flags) { if ((flags & ~FLAG_PUBLIC_ALL) != 0) { throw new IllegalArgumentException("flag has unknown bits set: " + flags); } mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags; return this; } /** * Builds a new {@link VolumeShaper.Operation} object. * * @return a new {@code VolumeShaper.Operation} object */ public @NonNull Operation build() { return new Operation(mFlags, mReplaceId, mXOffset); } } // Operation.Builder } // Operation /** * @hide * {@code VolumeShaper.State} represents the current progress * of the {@code VolumeShaper}. * * Not for public use. */ public static final class State implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private float mVolume; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private float mXOffset; @Override public String toString() { return "VolumeShaper.State{" + "mVolume = " + mVolume + ", mXOffset = " + mXOffset + "}"; } @Override public int hashCode() { return Objects.hash(mVolume, mXOffset); } @Override public boolean equals(Object o) { if (!(o instanceof State)) return false; if (o == this) return true; final State other = (State) o; return mVolume == other.mVolume && mXOffset == other.mXOffset; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { toParcelable().writeToParcel(dest, flags); } /** @hide */ public VolumeShaperState toParcelable() { VolumeShaperState result = new VolumeShaperState(); result.volume = mVolume; result.xOffset = mXOffset; return result; } /** @hide */ public static State fromParcelable(VolumeShaperState p) { return new VolumeShaper.State(p.volume, p.xOffset); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public VolumeShaper.State createFromParcel(Parcel p) { return fromParcelable(VolumeShaperState.CREATOR.createFromParcel(p)); } @Override public VolumeShaper.State[] newArray(int size) { return new VolumeShaper.State[size]; } }; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) /* package */ State(float volume, float xOffset) { mVolume = volume; mXOffset = xOffset; } /** * Gets the volume of the {@link VolumeShaper.State}. * @return linear volume between 0.f and 1.f. */ public float getVolume() { return mVolume; } /** * Gets the {@code xOffset} position on the normalized curve * of the {@link VolumeShaper.State}. * @return the curve x position between 0.f and 1.f. */ public float getXOffset() { return mXOffset; } } // State }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy