| /* |
| * 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.os.Parcel; |
| import android.os.Parcelable; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.AutoCloseable; |
| 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<PlayerBase> mWeakPlayerBase; |
| |
| /* package */ VolumeShaper( |
| @NonNull Configuration configuration, @NonNull PlayerBase playerBase) { |
| mWeakPlayerBase = new WeakReference<PlayerBase>(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}. |
| * <p> |
| * 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}. |
| * <p> |
| * 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. |
| *<p> |
| * 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. |
| *<p> |
| * 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. |
| *<p> |
| * 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 |
| private final int mType; |
| |
| // valid when mType is TYPE_ID |
| private final int mId; |
| |
| // valid when mType is TYPE_SCALE |
| private final int mOptionFlags; |
| private final double mDurationMs; |
| private final int mInterpolatorType; |
| private final float[] mTimes; |
| 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) { |
| // this needs to match the native VolumeShaper.Configuration parceling |
| dest.writeInt(mType); |
| dest.writeInt(mId); |
| if (mType != TYPE_ID) { |
| dest.writeInt(mOptionFlags); |
| dest.writeDouble(mDurationMs); |
| // this needs to match the native Interpolator parceling |
| dest.writeInt(mInterpolatorType); |
| dest.writeFloat(0.f); // first slope (specifying for native side) |
| dest.writeFloat(0.f); // last slope (specifying for native side) |
| // mTimes and mVolumes should have the same length. |
| dest.writeInt(mTimes.length); |
| for (int i = 0; i < mTimes.length; ++i) { |
| dest.writeFloat(mTimes[i]); |
| dest.writeFloat(mVolumes[i]); |
| } |
| } |
| } |
| |
| public static final Parcelable.Creator<VolumeShaper.Configuration> CREATOR |
| = new Parcelable.Creator<VolumeShaper.Configuration>() { |
| @Override |
| public VolumeShaper.Configuration createFromParcel(Parcel p) { |
| // this needs to match the native VolumeShaper.Configuration parceling |
| final int type = p.readInt(); |
| final int id = p.readInt(); |
| if (type == TYPE_ID) { |
| return new VolumeShaper.Configuration(id); |
| } else { |
| final int optionFlags = p.readInt(); |
| final double durationMs = p.readDouble(); |
| // this needs to match the native Interpolator parceling |
| final int interpolatorType = p.readInt(); |
| final float firstSlope = p.readFloat(); // ignored on the Java side |
| final float lastSlope = p.readFloat(); // ignored on the Java side |
| final int length = p.readInt(); |
| final float[] times = new float[length]; |
| final float[] volumes = new float[length]; |
| for (int i = 0; i < length; ++i) { |
| times[i] = p.readFloat(); |
| volumes[i] = p.readFloat(); |
| } |
| |
| return new VolumeShaper.Configuration( |
| type, |
| id, |
| optionFlags, |
| durationMs, |
| interpolatorType, |
| times, |
| volumes); |
| } |
| } |
| |
| @Override |
| public VolumeShaper.Configuration[] newArray(int size) { |
| return new VolumeShaper.Configuration[size]; |
| } |
| }; |
| |
| /** |
| * @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. |
| */ |
| 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. |
| * <p> Here is an example where {@code Builder} is used to define the |
| * {@link VolumeShaper.Configuration}. |
| * |
| * <pre class="prettyprint"> |
| * 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(); |
| * </pre> |
| * <p> |
| */ |
| 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()}. |
| * <p> |
| * 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. |
| * <p> |
| * The time scale is set by {@link #setDuration}. |
| * <p> |
| * @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; |
| |
| private final int mFlags; |
| private final int mReplaceId; |
| 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) { |
| // this needs to match the native VolumeShaper.Operation parceling |
| dest.writeInt(mFlags); |
| dest.writeInt(mReplaceId); |
| dest.writeFloat(mXOffset); |
| } |
| |
| public static final Parcelable.Creator<VolumeShaper.Operation> CREATOR |
| = new Parcelable.Creator<VolumeShaper.Operation>() { |
| @Override |
| public VolumeShaper.Operation createFromParcel(Parcel p) { |
| // this needs to match the native VolumeShaper.Operation parceling |
| final int flags = p.readInt(); |
| final int replaceId = p.readInt(); |
| final float xOffset = p.readFloat(); |
| |
| return new VolumeShaper.Operation( |
| flags |
| , replaceId |
| , xOffset); |
| } |
| |
| @Override |
| public VolumeShaper.Operation[] newArray(int size) { |
| return new VolumeShaper.Operation[size]; |
| } |
| }; |
| |
| 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 { |
| private float mVolume; |
| 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) { |
| dest.writeFloat(mVolume); |
| dest.writeFloat(mXOffset); |
| } |
| |
| public static final Parcelable.Creator<VolumeShaper.State> CREATOR |
| = new Parcelable.Creator<VolumeShaper.State>() { |
| @Override |
| public VolumeShaper.State createFromParcel(Parcel p) { |
| return new VolumeShaper.State( |
| p.readFloat() // volume |
| , p.readFloat()); // xOffset |
| } |
| |
| @Override |
| public VolumeShaper.State[] newArray(int size) { |
| return new VolumeShaper.State[size]; |
| } |
| }; |
| |
| /* 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 |
| } |