Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.media;
     17 
     18 import android.annotation.IntDef;
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.annotation.TestApi;
     22 import android.os.Parcel;
     23 import android.os.Parcelable;
     24 
     25 import java.lang.annotation.Retention;
     26 import java.lang.annotation.RetentionPolicy;
     27 import java.lang.AutoCloseable;
     28 import java.lang.ref.WeakReference;
     29 import java.util.Arrays;
     30 import java.util.Objects;
     31 
     32 /**
     33  * The {@code VolumeShaper} class is used to automatically control audio volume during media
     34  * playback, allowing simple implementation of transition effects and ducking.
     35  * It is created from implementations of {@code VolumeAutomation},
     36  * such as {@code MediaPlayer} and {@code AudioTrack} (referred to as "players" below),
     37  * by {@link MediaPlayer#createVolumeShaper} or {@link AudioTrack#createVolumeShaper}.
     38  *
     39  * A {@code VolumeShaper} is intended for short volume changes.
     40  * If the audio output sink changes during
     41  * a {@code VolumeShaper} transition, the precise curve position may be lost, and the
     42  * {@code VolumeShaper} may advance to the end of the curve for the new audio output sink.
     43  *
     44  * The {@code VolumeShaper} appears as an additional scaling on the audio output,
     45  * and adjusts independently of track or stream volume controls.
     46  */
     47 public final class VolumeShaper implements AutoCloseable {
     48     /* member variables */
     49     private int mId;
     50     private final WeakReference<PlayerBase> mWeakPlayerBase;
     51 
     52     /* package */ VolumeShaper(
     53             @NonNull Configuration configuration, @NonNull PlayerBase playerBase) {
     54         mWeakPlayerBase = new WeakReference<PlayerBase>(playerBase);
     55         mId = applyPlayer(configuration, new Operation.Builder().defer().build());
     56     }
     57 
     58     /* package */ int getId() {
     59         return mId;
     60     }
     61 
     62     /**
     63      * Applies the {@link VolumeShaper.Operation} to the {@code VolumeShaper}.
     64      *
     65      * Applying {@link VolumeShaper.Operation#PLAY} after {@code PLAY}
     66      * or {@link VolumeShaper.Operation#REVERSE} after
     67      * {@code REVERSE} has no effect.
     68      *
     69      * Applying {@link VolumeShaper.Operation#PLAY} when the player
     70      * hasn't started will synchronously start the {@code VolumeShaper} when
     71      * playback begins.
     72      *
     73      * @param operation the {@code operation} to apply.
     74      * @throws IllegalStateException if the player is uninitialized or if there
     75      *         is a critical failure. In that case, the {@code VolumeShaper} should be
     76      *         recreated.
     77      */
     78     public void apply(@NonNull Operation operation) {
     79         /* void */ applyPlayer(new VolumeShaper.Configuration(mId), operation);
     80     }
     81 
     82     /**
     83      * Replaces the current {@code VolumeShaper}
     84      * {@code configuration} with a new {@code configuration}.
     85      *
     86      * This allows the user to change the volume shape
     87      * while the existing {@code VolumeShaper} is in effect.
     88      *
     89      * The effect of {@code replace()} is similar to an atomic close of
     90      * the existing {@code VolumeShaper} and creation of a new {@code VolumeShaper}.
     91      *
     92      * If the {@code operation} is {@link VolumeShaper.Operation#PLAY} then the
     93      * new curve starts immediately.
     94      *
     95      * If the {@code operation} is
     96      * {@link VolumeShaper.Operation#REVERSE}, then the new curve will
     97      * be delayed until {@code PLAY} is applied.
     98      *
     99      * @param configuration the new {@code configuration} to use.
    100      * @param operation the {@code operation} to apply to the {@code VolumeShaper}
    101      * @param join if true, match the start volume of the
    102      *             new {@code configuration} to the current volume of the existing
    103      *             {@code VolumeShaper}, to avoid discontinuity.
    104      * @throws IllegalStateException if the player is uninitialized or if there
    105      *         is a critical failure. In that case, the {@code VolumeShaper} should be
    106      *         recreated.
    107      */
    108     public void replace(
    109             @NonNull Configuration configuration, @NonNull Operation operation, boolean join) {
    110         mId = applyPlayer(
    111                 configuration,
    112                 new Operation.Builder(operation).replace(mId, join).build());
    113     }
    114 
    115     /**
    116      * Returns the current volume scale attributable to the {@code VolumeShaper}.
    117      *
    118      * This is the last volume from the {@code VolumeShaper} used for the player,
    119      * or the initial volume if the {@code VolumeShaper} hasn't been started with
    120      * {@link VolumeShaper.Operation#PLAY}.
    121      *
    122      * @return the volume, linearly represented as a value between 0.f and 1.f.
    123      * @throws IllegalStateException if the player is uninitialized or if there
    124      *         is a critical failure.  In that case, the {@code VolumeShaper} should be
    125      *         recreated.
    126      */
    127     public float getVolume() {
    128         return getStatePlayer(mId).getVolume();
    129     }
    130 
    131     /**
    132      * Releases the {@code VolumeShaper} object; any volume scale due to the
    133      * {@code VolumeShaper} is removed after closing.
    134      *
    135      * If the volume does not reach 1.f when the {@code VolumeShaper} is closed
    136      * (or finalized), there may be an abrupt change of volume.
    137      *
    138      * {@code close()} may be safely called after a prior {@code close()}.
    139      * This class implements the Java {@code AutoClosable} interface and
    140      * may be used with try-with-resources.
    141      */
    142     @Override
    143     public void close() {
    144         try {
    145             /* void */ applyPlayer(
    146                     new VolumeShaper.Configuration(mId),
    147                     new Operation.Builder().terminate().build());
    148         } catch (IllegalStateException ise) {
    149             ; // ok
    150         }
    151         if (mWeakPlayerBase != null) {
    152             mWeakPlayerBase.clear();
    153         }
    154     }
    155 
    156     @Override
    157     protected void finalize() {
    158         close(); // ensure we remove the native VolumeShaper
    159     }
    160 
    161     /**
    162      * Internal call to apply the {@code configuration} and {@code operation} to the player.
    163      * Returns a valid shaper id or throws the appropriate exception.
    164      * @param configuration
    165      * @param operation
    166      * @return id a non-negative shaper id.
    167      * @throws IllegalStateException if the player has been deallocated or is uninitialized.
    168      */
    169     private int applyPlayer(
    170             @NonNull VolumeShaper.Configuration configuration,
    171             @NonNull VolumeShaper.Operation operation) {
    172         final int id;
    173         if (mWeakPlayerBase != null) {
    174             PlayerBase player = mWeakPlayerBase.get();
    175             if (player == null) {
    176                 throw new IllegalStateException("player deallocated");
    177             }
    178             id = player.playerApplyVolumeShaper(configuration, operation);
    179         } else {
    180             throw new IllegalStateException("uninitialized shaper");
    181         }
    182         if (id < 0) {
    183             // TODO - get INVALID_OPERATION from platform.
    184             final int VOLUME_SHAPER_INVALID_OPERATION = -38; // must match with platform
    185             // Due to RPC handling, we translate integer codes to exceptions right before
    186             // delivering to the user.
    187             if (id == VOLUME_SHAPER_INVALID_OPERATION) {
    188                 throw new IllegalStateException("player or VolumeShaper deallocated");
    189             } else {
    190                 throw new IllegalArgumentException("invalid configuration or operation: " + id);
    191             }
    192         }
    193         return id;
    194     }
    195 
    196     /**
    197      * Internal call to retrieve the current {@code VolumeShaper} state.
    198      * @param id
    199      * @return the current {@code VolumeShaper.State}
    200      * @throws IllegalStateException if the player has been deallocated or is uninitialized.
    201      */
    202     private @NonNull VolumeShaper.State getStatePlayer(int id) {
    203         final VolumeShaper.State state;
    204         if (mWeakPlayerBase != null) {
    205             PlayerBase player = mWeakPlayerBase.get();
    206             if (player == null) {
    207                 throw new IllegalStateException("player deallocated");
    208             }
    209             state = player.playerGetVolumeShaperState(id);
    210         } else {
    211             throw new IllegalStateException("uninitialized shaper");
    212         }
    213         if (state == null) {
    214             throw new IllegalStateException("shaper cannot be found");
    215         }
    216         return state;
    217     }
    218 
    219     /**
    220      * The {@code VolumeShaper.Configuration} class contains curve
    221      * and duration information.
    222      * It is constructed by the {@link VolumeShaper.Configuration.Builder}.
    223      * <p>
    224      * A {@code VolumeShaper.Configuration} is used by
    225      * {@link VolumeAutomation#createVolumeShaper(Configuration)
    226      * VolumeAutomation.createVolumeShaper(Configuration)} to create
    227      * a {@code VolumeShaper} and
    228      * by {@link VolumeShaper#replace(Configuration, Operation, boolean)
    229      * VolumeShaper.replace(Configuration, Operation, boolean)}
    230      * to replace an existing {@code configuration}.
    231      * <p>
    232      * The {@link AudioTrack} and {@link MediaPlayer} classes implement
    233      * the {@link VolumeAutomation} interface.
    234      */
    235     public static final class Configuration implements Parcelable {
    236         private static final int MAXIMUM_CURVE_POINTS = 16;
    237 
    238         /**
    239          * Returns the maximum number of curve points allowed for
    240          * {@link VolumeShaper.Builder#setCurve(float[], float[])}.
    241          */
    242         public static int getMaximumCurvePoints() {
    243             return MAXIMUM_CURVE_POINTS;
    244         }
    245 
    246         // These values must match the native VolumeShaper::Configuration::Type
    247         /** @hide */
    248         @IntDef({
    249             TYPE_ID,
    250             TYPE_SCALE,
    251             })
    252         @Retention(RetentionPolicy.SOURCE)
    253         public @interface Type {}
    254 
    255         /**
    256          * Specifies a {@link VolumeShaper} handle created by {@link #VolumeShaper(int)}
    257          * from an id returned by {@code setVolumeShaper()}.
    258          * The type, curve, etc. may not be queried from
    259          * a {@code VolumeShaper} object of this type;
    260          * the handle is used to identify and change the operation of
    261          * an existing {@code VolumeShaper} sent to the player.
    262          */
    263         /* package */ static final int TYPE_ID = 0;
    264 
    265         /**
    266          * Specifies a {@link VolumeShaper} to be used
    267          * as an additional scale to the current volume.
    268          * This is created by the {@link VolumeShaper.Builder}.
    269          */
    270         /* package */ static final int TYPE_SCALE = 1;
    271 
    272         // These values must match the native InterpolatorType enumeration.
    273         /** @hide */
    274         @IntDef({
    275             INTERPOLATOR_TYPE_STEP,
    276             INTERPOLATOR_TYPE_LINEAR,
    277             INTERPOLATOR_TYPE_CUBIC,
    278             INTERPOLATOR_TYPE_CUBIC_MONOTONIC,
    279             })
    280         @Retention(RetentionPolicy.SOURCE)
    281         public @interface InterpolatorType {}
    282 
    283         /**
    284          * Stepwise volume curve.
    285          */
    286         public static final int INTERPOLATOR_TYPE_STEP = 0;
    287 
    288         /**
    289          * Linear interpolated volume curve.
    290          */
    291         public static final int INTERPOLATOR_TYPE_LINEAR = 1;
    292 
    293         /**
    294          * Cubic interpolated volume curve.
    295          * This is default if unspecified.
    296          */
    297         public static final int INTERPOLATOR_TYPE_CUBIC = 2;
    298 
    299         /**
    300          * Cubic interpolated volume curve
    301          * that preserves local monotonicity.
    302          * So long as the control points are locally monotonic,
    303          * the curve interpolation between those points are monotonic.
    304          * This is useful for cubic spline interpolated
    305          * volume ramps and ducks.
    306          */
    307         public static final int INTERPOLATOR_TYPE_CUBIC_MONOTONIC = 3;
    308 
    309         // These values must match the native VolumeShaper::Configuration::InterpolatorType
    310         /** @hide */
    311         @IntDef({
    312             OPTION_FLAG_VOLUME_IN_DBFS,
    313             OPTION_FLAG_CLOCK_TIME,
    314             })
    315         @Retention(RetentionPolicy.SOURCE)
    316         public @interface OptionFlag {}
    317 
    318         /**
    319          * @hide
    320          * Use a dB full scale volume range for the volume curve.
    321          *<p>
    322          * The volume scale is typically from 0.f to 1.f on a linear scale;
    323          * this option changes to -inf to 0.f on a db full scale,
    324          * where 0.f is equivalent to a scale of 1.f.
    325          */
    326         public static final int OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0);
    327 
    328         /**
    329          * @hide
    330          * Use clock time instead of media time.
    331          *<p>
    332          * The default implementation of {@code VolumeShaper} is to apply
    333          * volume changes by the media time of the player.
    334          * Hence, the {@code VolumeShaper} will speed or slow down to
    335          * match player changes of playback rate, pause, or resume.
    336          *<p>
    337          * The {@code OPTION_FLAG_CLOCK_TIME} option allows the {@code VolumeShaper}
    338          * progress to be determined by clock time instead of media time.
    339          */
    340         public static final int OPTION_FLAG_CLOCK_TIME = (1 << 1);
    341 
    342         private static final int OPTION_FLAG_PUBLIC_ALL =
    343                 OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME;
    344 
    345         /**
    346          * A one second linear ramp from silence to full volume.
    347          * Use {@link VolumeShaper.Builder#reflectTimes()}
    348          * or {@link VolumeShaper.Builder#invertVolumes()} to generate
    349          * the matching linear duck.
    350          */
    351         public static final Configuration LINEAR_RAMP = new VolumeShaper.Configuration.Builder()
    352                 .setInterpolatorType(INTERPOLATOR_TYPE_LINEAR)
    353                 .setCurve(new float[] {0.f, 1.f} /* times */,
    354                         new float[] {0.f, 1.f} /* volumes */)
    355                 .setDuration(1000)
    356                 .build();
    357 
    358         /**
    359          * A one second cubic ramp from silence to full volume.
    360          * Use {@link VolumeShaper.Builder#reflectTimes()}
    361          * or {@link VolumeShaper.Builder#invertVolumes()} to generate
    362          * the matching cubic duck.
    363          */
    364         public static final Configuration CUBIC_RAMP = new VolumeShaper.Configuration.Builder()
    365                 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
    366                 .setCurve(new float[] {0.f, 1.f} /* times */,
    367                         new float[] {0.f, 1.f}  /* volumes */)
    368                 .setDuration(1000)
    369                 .build();
    370 
    371         /**
    372          * A one second sine curve
    373          * from silence to full volume for energy preserving cross fades.
    374          * Use {@link VolumeShaper.Builder#reflectTimes()} to generate
    375          * the matching cosine duck.
    376          */
    377         public static final Configuration SINE_RAMP;
    378 
    379         /**
    380          * A one second sine-squared s-curve ramp
    381          * from silence to full volume.
    382          * Use {@link VolumeShaper.Builder#reflectTimes()}
    383          * or {@link VolumeShaper.Builder#invertVolumes()} to generate
    384          * the matching sine-squared s-curve duck.
    385          */
    386         public static final Configuration SCURVE_RAMP;
    387 
    388         static {
    389             final int POINTS = MAXIMUM_CURVE_POINTS;
    390             final float times[] = new float[POINTS];
    391             final float sines[] = new float[POINTS];
    392             final float scurve[] = new float[POINTS];
    393             for (int i = 0; i < POINTS; ++i) {
    394                 times[i] = (float)i / (POINTS - 1);
    395                 final float sine = (float)Math.sin(times[i] * Math.PI / 2.);
    396                 sines[i] = sine;
    397                 scurve[i] = sine * sine;
    398             }
    399             SINE_RAMP = new VolumeShaper.Configuration.Builder()
    400                 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
    401                 .setCurve(times, sines)
    402                 .setDuration(1000)
    403                 .build();
    404             SCURVE_RAMP = new VolumeShaper.Configuration.Builder()
    405                 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
    406                 .setCurve(times, scurve)
    407                 .setDuration(1000)
    408                 .build();
    409         }
    410 
    411         /*
    412          * member variables - these are all final
    413          */
    414 
    415         // type of VolumeShaper
    416         private final int mType;
    417 
    418         // valid when mType is TYPE_ID
    419         private final int mId;
    420 
    421         // valid when mType is TYPE_SCALE
    422         private final int mOptionFlags;
    423         private final double mDurationMs;
    424         private final int mInterpolatorType;
    425         private final float[] mTimes;
    426         private final float[] mVolumes;
    427 
    428         @Override
    429         public String toString() {
    430             return "VolumeShaper.Configuration{"
    431                     + "mType = " + mType
    432                     + ", mId = " + mId
    433                     + (mType == TYPE_ID
    434                         ? "}"
    435                         : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase()
    436                         + ", mDurationMs = " + mDurationMs
    437                         + ", mInterpolatorType = " + mInterpolatorType
    438                         + ", mTimes[] = " + Arrays.toString(mTimes)
    439                         + ", mVolumes[] = " + Arrays.toString(mVolumes)
    440                         + "}");
    441         }
    442 
    443         @Override
    444         public int hashCode() {
    445             return mType == TYPE_ID
    446                     ? Objects.hash(mType, mId)
    447                     : Objects.hash(mType, mId,
    448                             mOptionFlags, mDurationMs, mInterpolatorType,
    449                             Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes));
    450         }
    451 
    452         @Override
    453         public boolean equals(Object o) {
    454             if (!(o instanceof Configuration)) return false;
    455             if (o == this) return true;
    456             final Configuration other = (Configuration) o;
    457             // Note that exact floating point equality may not be guaranteed
    458             // for a theoretically idempotent operation; for example,
    459             // there are many cases where a + b - b != a.
    460             return mType == other.mType
    461                     && mId == other.mId
    462                     && (mType == TYPE_ID
    463                         ||  (mOptionFlags == other.mOptionFlags
    464                             && mDurationMs == other.mDurationMs
    465                             && mInterpolatorType == other.mInterpolatorType
    466                             && Arrays.equals(mTimes, other.mTimes)
    467                             && Arrays.equals(mVolumes, other.mVolumes)));
    468         }
    469 
    470         @Override
    471         public int describeContents() {
    472             return 0;
    473         }
    474 
    475         @Override
    476         public void writeToParcel(Parcel dest, int flags) {
    477             // this needs to match the native VolumeShaper.Configuration parceling
    478             dest.writeInt(mType);
    479             dest.writeInt(mId);
    480             if (mType != TYPE_ID) {
    481                 dest.writeInt(mOptionFlags);
    482                 dest.writeDouble(mDurationMs);
    483                 // this needs to match the native Interpolator parceling
    484                 dest.writeInt(mInterpolatorType);
    485                 dest.writeFloat(0.f); // first slope (specifying for native side)
    486                 dest.writeFloat(0.f); // last slope (specifying for native side)
    487                 // mTimes and mVolumes should have the same length.
    488                 dest.writeInt(mTimes.length);
    489                 for (int i = 0; i < mTimes.length; ++i) {
    490                     dest.writeFloat(mTimes[i]);
    491                     dest.writeFloat(mVolumes[i]);
    492                 }
    493             }
    494         }
    495 
    496         public static final Parcelable.Creator<VolumeShaper.Configuration> CREATOR
    497                 = new Parcelable.Creator<VolumeShaper.Configuration>() {
    498             @Override
    499             public VolumeShaper.Configuration createFromParcel(Parcel p) {
    500                 // this needs to match the native VolumeShaper.Configuration parceling
    501                 final int type = p.readInt();
    502                 final int id = p.readInt();
    503                 if (type == TYPE_ID) {
    504                     return new VolumeShaper.Configuration(id);
    505                 } else {
    506                     final int optionFlags = p.readInt();
    507                     final double durationMs = p.readDouble();
    508                     // this needs to match the native Interpolator parceling
    509                     final int interpolatorType = p.readInt();
    510                     final float firstSlope = p.readFloat(); // ignored on the Java side
    511                     final float lastSlope = p.readFloat();  // ignored on the Java side
    512                     final int length = p.readInt();
    513                     final float[] times = new float[length];
    514                     final float[] volumes = new float[length];
    515                     for (int i = 0; i < length; ++i) {
    516                         times[i] = p.readFloat();
    517                         volumes[i] = p.readFloat();
    518                     }
    519 
    520                     return new VolumeShaper.Configuration(
    521                         type,
    522                         id,
    523                         optionFlags,
    524                         durationMs,
    525                         interpolatorType,
    526                         times,
    527                         volumes);
    528                 }
    529             }
    530 
    531             @Override
    532             public VolumeShaper.Configuration[] newArray(int size) {
    533                 return new VolumeShaper.Configuration[size];
    534             }
    535         };
    536 
    537         /**
    538          * @hide
    539          * Constructs a {@code VolumeShaper} from an id.
    540          *
    541          * This is an opaque handle for controlling a {@code VolumeShaper} that has
    542          * already been sent to a player.  The {@code id} is returned from the
    543          * initial {@code setVolumeShaper()} call on success.
    544          *
    545          * These configurations are for native use only,
    546          * they are never returned directly to the user.
    547          *
    548          * @param id
    549          * @throws IllegalArgumentException if id is negative.
    550          */
    551         public Configuration(int id) {
    552             if (id < 0) {
    553                 throw new IllegalArgumentException("negative id " + id);
    554             }
    555             mType = TYPE_ID;
    556             mId = id;
    557             mInterpolatorType = 0;
    558             mOptionFlags = 0;
    559             mDurationMs = 0;
    560             mTimes = null;
    561             mVolumes = null;
    562         }
    563 
    564         /**
    565          * Direct constructor for VolumeShaper.
    566          * Use the Builder instead.
    567          */
    568         private Configuration(@Type int type,
    569                 int id,
    570                 @OptionFlag int optionFlags,
    571                 double durationMs,
    572                 @InterpolatorType int interpolatorType,
    573                 @NonNull float[] times,
    574                 @NonNull float[] volumes) {
    575             mType = type;
    576             mId = id;
    577             mOptionFlags = optionFlags;
    578             mDurationMs = durationMs;
    579             mInterpolatorType = interpolatorType;
    580             // Builder should have cloned these arrays already.
    581             mTimes = times;
    582             mVolumes = volumes;
    583         }
    584 
    585         /**
    586          * @hide
    587          * Returns the {@code VolumeShaper} type.
    588          */
    589         public @Type int getType() {
    590             return mType;
    591         }
    592 
    593         /**
    594          * @hide
    595          * Returns the {@code VolumeShaper} id.
    596          */
    597         public int getId() {
    598             return mId;
    599         }
    600 
    601         /**
    602          * Returns the interpolator type.
    603          */
    604         public @InterpolatorType int getInterpolatorType() {
    605             return mInterpolatorType;
    606         }
    607 
    608         /**
    609          * @hide
    610          * Returns the option flags
    611          */
    612         public @OptionFlag int getOptionFlags() {
    613             return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
    614         }
    615 
    616         /* package */ @OptionFlag int getAllOptionFlags() {
    617             return mOptionFlags;
    618         }
    619 
    620         /**
    621          * Returns the duration of the volume shape in milliseconds.
    622          */
    623         public long getDuration() {
    624             // casting is safe here as the duration was set as a long in the Builder
    625             return (long) mDurationMs;
    626         }
    627 
    628         /**
    629          * Returns the times (x) coordinate array of the volume curve points.
    630          */
    631         public float[] getTimes() {
    632             return mTimes;
    633         }
    634 
    635         /**
    636          * Returns the volumes (y) coordinate array of the volume curve points.
    637          */
    638         public float[] getVolumes() {
    639             return mVolumes;
    640         }
    641 
    642         /**
    643          * Checks the validity of times and volumes point representation.
    644          *
    645          * {@code times[]} and {@code volumes[]} are two arrays representing points
    646          * for the volume curve.
    647          *
    648          * Note that {@code times[]} and {@code volumes[]} are explicitly checked against
    649          * null here to provide the proper error string - those are legitimate
    650          * arguments to this method.
    651          *
    652          * @param times the x coordinates for the points,
    653          *        must be between 0.f and 1.f and be monotonic.
    654          * @param volumes the y coordinates for the points,
    655          *        must be between 0.f and 1.f for linear and
    656          *        must be no greater than 0.f for log (dBFS).
    657          * @param log set to true if the scale is logarithmic.
    658          * @return null if no error, or the reason in a {@code String} for an error.
    659          */
    660         private static @Nullable String checkCurveForErrors(
    661                 @Nullable float[] times, @Nullable float[] volumes, boolean log) {
    662             if (times == null) {
    663                 return "times array must be non-null";
    664             } else if (volumes == null) {
    665                 return "volumes array must be non-null";
    666             } else if (times.length != volumes.length) {
    667                 return "array length must match";
    668             } else if (times.length < 2) {
    669                 return "array length must be at least 2";
    670             } else if (times.length > MAXIMUM_CURVE_POINTS) {
    671                 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS;
    672             } else if (times[0] != 0.f) {
    673                 return "times must start at 0.f";
    674             } else if (times[times.length - 1] != 1.f) {
    675                 return "times must end at 1.f";
    676             }
    677 
    678             // validate points along the curve
    679             for (int i = 1; i < times.length; ++i) {
    680                 if (!(times[i] > times[i - 1]) /* handle nan */) {
    681                     return "times not monotonic increasing, check index " + i;
    682                 }
    683             }
    684             if (log) {
    685                 for (int i = 0; i < volumes.length; ++i) {
    686                     if (!(volumes[i] <= 0.f) /* handle nan */) {
    687                         return "volumes for log scale cannot be positive, "
    688                                 + "check index " + i;
    689                     }
    690                 }
    691             } else {
    692                 for (int i = 0; i < volumes.length; ++i) {
    693                     if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) {
    694                         return "volumes for linear scale must be between 0.f and 1.f, "
    695                                 + "check index " + i;
    696                     }
    697                 }
    698             }
    699             return null; // no errors
    700         }
    701 
    702         private static void checkCurveForErrorsAndThrowException(
    703                 @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) {
    704             final String error = checkCurveForErrors(times, volumes, log);
    705             if (error != null) {
    706                 if (ise) {
    707                     throw new IllegalStateException(error);
    708                 } else {
    709                     throw new IllegalArgumentException(error);
    710                 }
    711             }
    712         }
    713 
    714         private static void checkValidVolumeAndThrowException(float volume, boolean log) {
    715             if (log) {
    716                 if (!(volume <= 0.f) /* handle nan */) {
    717                     throw new IllegalArgumentException("dbfs volume must be 0.f or less");
    718                 }
    719             } else {
    720                 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) {
    721                     throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f");
    722                 }
    723             }
    724         }
    725 
    726         private static void clampVolume(float[] volumes, boolean log) {
    727             if (log) {
    728                 for (int i = 0; i < volumes.length; ++i) {
    729                     if (!(volumes[i] <= 0.f) /* handle nan */) {
    730                         volumes[i] = 0.f;
    731                     }
    732                 }
    733             } else {
    734                 for (int i = 0; i < volumes.length; ++i) {
    735                     if (!(volumes[i] >= 0.f) /* handle nan */) {
    736                         volumes[i] = 0.f;
    737                     } else if (!(volumes[i] <= 1.f)) {
    738                         volumes[i] = 1.f;
    739                     }
    740                 }
    741             }
    742         }
    743 
    744         /**
    745          * Builder class for a {@link VolumeShaper.Configuration} object.
    746          * <p> Here is an example where {@code Builder} is used to define the
    747          * {@link VolumeShaper.Configuration}.
    748          *
    749          * <pre class="prettyprint">
    750          * VolumeShaper.Configuration LINEAR_RAMP =
    751          *         new VolumeShaper.Configuration.Builder()
    752          *             .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
    753          *             .setCurve(new float[] { 0.f, 1.f }, // times
    754          *                       new float[] { 0.f, 1.f }) // volumes
    755          *             .setDuration(1000)
    756          *             .build();
    757          * </pre>
    758          * <p>
    759          */
    760         public static final class Builder {
    761             private int mType = TYPE_SCALE;
    762             private int mId = -1; // invalid
    763             private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC;
    764             private int mOptionFlags = OPTION_FLAG_CLOCK_TIME;
    765             private double mDurationMs = 1000.;
    766             private float[] mTimes = null;
    767             private float[] mVolumes = null;
    768 
    769             /**
    770              * Constructs a new {@code Builder} with the defaults.
    771              */
    772             public Builder() {
    773             }
    774 
    775             /**
    776              * Constructs a new {@code Builder} with settings
    777              * copied from a given {@code VolumeShaper.Configuration}.
    778              * @param configuration prototypical configuration
    779              *        which will be reused in the new {@code Builder}.
    780              */
    781             public Builder(@NonNull Configuration configuration) {
    782                 mType = configuration.getType();
    783                 mId = configuration.getId();
    784                 mOptionFlags = configuration.getAllOptionFlags();
    785                 mInterpolatorType = configuration.getInterpolatorType();
    786                 mDurationMs = configuration.getDuration();
    787                 mTimes = configuration.getTimes().clone();
    788                 mVolumes = configuration.getVolumes().clone();
    789             }
    790 
    791             /**
    792              * @hide
    793              * Set the {@code id} for system defined shapers.
    794              * @param id the {@code id} to set. If non-negative, then it is used.
    795              *        If -1, then the system is expected to assign one.
    796              * @return the same {@code Builder} instance.
    797              * @throws IllegalArgumentException if {@code id} < -1.
    798              */
    799             public @NonNull Builder setId(int id) {
    800                 if (id < -1) {
    801                     throw new IllegalArgumentException("invalid id: " + id);
    802                 }
    803                 mId = id;
    804                 return this;
    805             }
    806 
    807             /**
    808              * Sets the interpolator type.
    809              *
    810              * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}.
    811              *
    812              * @param interpolatorType method of interpolation used for the volume curve.
    813              *        One of {@link #INTERPOLATOR_TYPE_STEP},
    814              *        {@link #INTERPOLATOR_TYPE_LINEAR},
    815              *        {@link #INTERPOLATOR_TYPE_CUBIC},
    816              *        {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}.
    817              * @return the same {@code Builder} instance.
    818              * @throws IllegalArgumentException if {@code interpolatorType} is not valid.
    819              */
    820             public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) {
    821                 switch (interpolatorType) {
    822                     case INTERPOLATOR_TYPE_STEP:
    823                     case INTERPOLATOR_TYPE_LINEAR:
    824                     case INTERPOLATOR_TYPE_CUBIC:
    825                     case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
    826                         mInterpolatorType = interpolatorType;
    827                         break;
    828                     default:
    829                         throw new IllegalArgumentException("invalid interpolatorType: "
    830                                 + interpolatorType);
    831                 }
    832                 return this;
    833             }
    834 
    835             /**
    836              * @hide
    837              * Sets the optional flags
    838              *
    839              * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has
    840              * changed the volume curve needs to be set again as the acceptable
    841              * volume domain has changed.
    842              *
    843              * @param optionFlags new value to replace the old {@code optionFlags}.
    844              * @return the same {@code Builder} instance.
    845              * @throws IllegalArgumentException if flag is not recognized.
    846              */
    847             @TestApi
    848             public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
    849                 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
    850                     throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
    851                 }
    852                 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags;
    853                 return this;
    854             }
    855 
    856             /**
    857              * Sets the {@code VolumeShaper} duration in milliseconds.
    858              *
    859              * If omitted, the default duration is 1 second.
    860              *
    861              * @param durationMillis
    862              * @return the same {@code Builder} instance.
    863              * @throws IllegalArgumentException if {@code durationMillis}
    864              *         is not strictly positive.
    865              */
    866             public @NonNull Builder setDuration(long durationMillis) {
    867                 if (durationMillis <= 0) {
    868                     throw new IllegalArgumentException(
    869                             "duration: " + durationMillis + " not positive");
    870                 }
    871                 mDurationMs = (double) durationMillis;
    872                 return this;
    873             }
    874 
    875             /**
    876              * Sets the volume curve.
    877              *
    878              * The volume curve is represented by a set of control points given by
    879              * two float arrays of equal length,
    880              * one representing the time (x) coordinates
    881              * and one corresponding to the volume (y) coordinates.
    882              * The length must be at least 2
    883              * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}.
    884              * <p>
    885              * The volume curve is normalized as follows:
    886              * time (x) coordinates should be monotonically increasing, from 0.f to 1.f;
    887              * volume (y) coordinates must be within 0.f to 1.f.
    888              * <p>
    889              * The time scale is set by {@link #setDuration}.
    890              * <p>
    891              * @param times an array of float values representing
    892              *        the time line of the volume curve.
    893              * @param volumes an array of float values representing
    894              *        the amplitude of the volume curve.
    895              * @return the same {@code Builder} instance.
    896              * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid.
    897              */
    898 
    899             /* Note: volume (y) coordinates must be non-positive for log scaling,
    900              * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set.
    901              */
    902 
    903             public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) {
    904                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
    905                 checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */);
    906                 mTimes = times.clone();
    907                 mVolumes = volumes.clone();
    908                 return this;
    909             }
    910 
    911             /**
    912              * Reflects the volume curve so that
    913              * the shaper changes volume from the end
    914              * to the start.
    915              *
    916              * @return the same {@code Builder} instance.
    917              * @throws IllegalStateException if curve has not been set.
    918              */
    919             public @NonNull Builder reflectTimes() {
    920                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
    921                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
    922                 int i;
    923                 for (i = 0; i < mTimes.length / 2; ++i) {
    924                     float temp = mTimes[i];
    925                     mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i];
    926                     mTimes[mTimes.length - 1 - i] = 1.f - temp;
    927                     temp = mVolumes[i];
    928                     mVolumes[i] = mVolumes[mVolumes.length - 1 - i];
    929                     mVolumes[mVolumes.length - 1 - i] = temp;
    930                 }
    931                 if ((mTimes.length & 1) != 0) {
    932                     mTimes[i] = 1.f - mTimes[i];
    933                 }
    934                 return this;
    935             }
    936 
    937             /**
    938              * Inverts the volume curve so that the max volume
    939              * becomes the min volume and vice versa.
    940              *
    941              * @return the same {@code Builder} instance.
    942              * @throws IllegalStateException if curve has not been set.
    943              */
    944             public @NonNull Builder invertVolumes() {
    945                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
    946                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
    947                 float min = mVolumes[0];
    948                 float max = mVolumes[0];
    949                 for (int i = 1; i < mVolumes.length; ++i) {
    950                     if (mVolumes[i] < min) {
    951                         min = mVolumes[i];
    952                     } else if (mVolumes[i] > max) {
    953                         max = mVolumes[i];
    954                     }
    955                 }
    956 
    957                 final float maxmin = max + min;
    958                 for (int i = 0; i < mVolumes.length; ++i) {
    959                     mVolumes[i] = maxmin - mVolumes[i];
    960                 }
    961                 return this;
    962             }
    963 
    964             /**
    965              * Scale the curve end volume to a target value.
    966              *
    967              * Keeps the start volume the same.
    968              * This works best if the volume curve is monotonic.
    969              *
    970              * @param volume the target end volume to use.
    971              * @return the same {@code Builder} instance.
    972              * @throws IllegalArgumentException if {@code volume} is not valid.
    973              * @throws IllegalStateException if curve has not been set.
    974              */
    975             public @NonNull Builder scaleToEndVolume(float volume) {
    976                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
    977                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
    978                 checkValidVolumeAndThrowException(volume, log);
    979                 final float startVolume = mVolumes[0];
    980                 final float endVolume = mVolumes[mVolumes.length - 1];
    981                 if (endVolume == startVolume) {
    982                     // match with linear ramp
    983                     final float offset = volume - startVolume;
    984                     for (int i = 0; i < mVolumes.length; ++i) {
    985                         mVolumes[i] = mVolumes[i] + offset * mTimes[i];
    986                     }
    987                 } else {
    988                     // scale
    989                     final float scale = (volume - startVolume) / (endVolume - startVolume);
    990                     for (int i = 0; i < mVolumes.length; ++i) {
    991                         mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume;
    992                     }
    993                 }
    994                 clampVolume(mVolumes, log);
    995                 return this;
    996             }
    997 
    998             /**
    999              * Scale the curve start volume to a target value.
   1000              *
   1001              * Keeps the end volume the same.
   1002              * This works best if the volume curve is monotonic.
   1003              *
   1004              * @param volume the target start volume to use.
   1005              * @return the same {@code Builder} instance.
   1006              * @throws IllegalArgumentException if {@code volume} is not valid.
   1007              * @throws IllegalStateException if curve has not been set.
   1008              */
   1009             public @NonNull Builder scaleToStartVolume(float volume) {
   1010                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
   1011                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
   1012                 checkValidVolumeAndThrowException(volume, log);
   1013                 final float startVolume = mVolumes[0];
   1014                 final float endVolume = mVolumes[mVolumes.length - 1];
   1015                 if (endVolume == startVolume) {
   1016                     // match with linear ramp
   1017                     final float offset = volume - startVolume;
   1018                     for (int i = 0; i < mVolumes.length; ++i) {
   1019                         mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]);
   1020                     }
   1021                 } else {
   1022                     final float scale = (volume - endVolume) / (startVolume - endVolume);
   1023                     for (int i = 0; i < mVolumes.length; ++i) {
   1024                         mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume;
   1025                     }
   1026                 }
   1027                 clampVolume(mVolumes, log);
   1028                 return this;
   1029             }
   1030 
   1031             /**
   1032              * Builds a new {@link VolumeShaper} object.
   1033              *
   1034              * @return a new {@link VolumeShaper} object.
   1035              * @throws IllegalStateException if curve is not properly set.
   1036              */
   1037             public @NonNull Configuration build() {
   1038                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
   1039                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
   1040                 return new Configuration(mType, mId, mOptionFlags, mDurationMs,
   1041                         mInterpolatorType, mTimes, mVolumes);
   1042             }
   1043         } // Configuration.Builder
   1044     } // Configuration
   1045 
   1046     /**
   1047      * The {@code VolumeShaper.Operation} class is used to specify operations
   1048      * to the {@code VolumeShaper} that affect the volume change.
   1049      */
   1050     public static final class Operation implements Parcelable {
   1051         /**
   1052          * Forward playback from current volume time position.
   1053          * At the end of the {@code VolumeShaper} curve,
   1054          * the last volume value persists.
   1055          */
   1056         public static final Operation PLAY =
   1057                 new VolumeShaper.Operation.Builder()
   1058                     .build();
   1059 
   1060         /**
   1061          * Reverse playback from current volume time position.
   1062          * When the position reaches the start of the {@code VolumeShaper} curve,
   1063          * the first volume value persists.
   1064          */
   1065         public static final Operation REVERSE =
   1066                 new VolumeShaper.Operation.Builder()
   1067                     .reverse()
   1068                     .build();
   1069 
   1070         // No user serviceable parts below.
   1071 
   1072         // These flags must match the native VolumeShaper::Operation::Flag
   1073         /** @hide */
   1074         @IntDef({
   1075             FLAG_NONE,
   1076             FLAG_REVERSE,
   1077             FLAG_TERMINATE,
   1078             FLAG_JOIN,
   1079             FLAG_DEFER,
   1080             })
   1081         @Retention(RetentionPolicy.SOURCE)
   1082         public @interface Flag {}
   1083 
   1084         /**
   1085          * No special {@code VolumeShaper} operation.
   1086          */
   1087         private static final int FLAG_NONE = 0;
   1088 
   1089         /**
   1090          * Reverse the {@code VolumeShaper} progress.
   1091          *
   1092          * Reverses the {@code VolumeShaper} curve from its current
   1093          * position. If the {@code VolumeShaper} curve has not started,
   1094          * it automatically is considered finished.
   1095          */
   1096         private static final int FLAG_REVERSE = 1 << 0;
   1097 
   1098         /**
   1099          * Terminate the existing {@code VolumeShaper}.
   1100          * This flag is generally used by itself;
   1101          * it takes precedence over all other flags.
   1102          */
   1103         private static final int FLAG_TERMINATE = 1 << 1;
   1104 
   1105         /**
   1106          * Attempt to join as best as possible to the previous {@code VolumeShaper}.
   1107          * This requires the previous {@code VolumeShaper} to be active and
   1108          * {@link #setReplaceId} to be set.
   1109          */
   1110         private static final int FLAG_JOIN = 1 << 2;
   1111 
   1112         /**
   1113          * Defer playback until next operation is sent. This is used
   1114          * when starting a {@code VolumeShaper} effect.
   1115          */
   1116         private static final int FLAG_DEFER = 1 << 3;
   1117 
   1118         /**
   1119          * Use the id specified in the configuration, creating
   1120          * {@code VolumeShaper} as needed; the configuration should be
   1121          * TYPE_SCALE.
   1122          */
   1123         private static final int FLAG_CREATE_IF_NEEDED = 1 << 4;
   1124 
   1125         private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE;
   1126 
   1127         private final int mFlags;
   1128         private final int mReplaceId;
   1129         private final float mXOffset;
   1130 
   1131         @Override
   1132         public String toString() {
   1133             return "VolumeShaper.Operation{"
   1134                     + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase()
   1135                     + ", mReplaceId = " + mReplaceId
   1136                     + ", mXOffset = " + mXOffset
   1137                     + "}";
   1138         }
   1139 
   1140         @Override
   1141         public int hashCode() {
   1142             return Objects.hash(mFlags, mReplaceId, mXOffset);
   1143         }
   1144 
   1145         @Override
   1146         public boolean equals(Object o) {
   1147             if (!(o instanceof Operation)) return false;
   1148             if (o == this) return true;
   1149             final Operation other = (Operation) o;
   1150 
   1151             return mFlags == other.mFlags
   1152                     && mReplaceId == other.mReplaceId
   1153                     && Float.compare(mXOffset, other.mXOffset) == 0;
   1154         }
   1155 
   1156         @Override
   1157         public int describeContents() {
   1158             return 0;
   1159         }
   1160 
   1161         @Override
   1162         public void writeToParcel(Parcel dest, int flags) {
   1163             // this needs to match the native VolumeShaper.Operation parceling
   1164             dest.writeInt(mFlags);
   1165             dest.writeInt(mReplaceId);
   1166             dest.writeFloat(mXOffset);
   1167         }
   1168 
   1169         public static final Parcelable.Creator<VolumeShaper.Operation> CREATOR
   1170                 = new Parcelable.Creator<VolumeShaper.Operation>() {
   1171             @Override
   1172             public VolumeShaper.Operation createFromParcel(Parcel p) {
   1173                 // this needs to match the native VolumeShaper.Operation parceling
   1174                 final int flags = p.readInt();
   1175                 final int replaceId = p.readInt();
   1176                 final float xOffset = p.readFloat();
   1177 
   1178                 return new VolumeShaper.Operation(
   1179                         flags
   1180                         , replaceId
   1181                         , xOffset);
   1182             }
   1183 
   1184             @Override
   1185             public VolumeShaper.Operation[] newArray(int size) {
   1186                 return new VolumeShaper.Operation[size];
   1187             }
   1188         };
   1189 
   1190         private Operation(@Flag int flags, int replaceId, float xOffset) {
   1191             mFlags = flags;
   1192             mReplaceId = replaceId;
   1193             mXOffset = xOffset;
   1194         }
   1195 
   1196         /**
   1197          * @hide
   1198          * {@code Builder} class for {@link VolumeShaper.Operation} object.
   1199          *
   1200          * Not for public use.
   1201          */
   1202         public static final class Builder {
   1203             int mFlags;
   1204             int mReplaceId;
   1205             float mXOffset;
   1206 
   1207             /**
   1208              * Constructs a new {@code Builder} with the defaults.
   1209              */
   1210             public Builder() {
   1211                 mFlags = 0;
   1212                 mReplaceId = -1;
   1213                 mXOffset = Float.NaN;
   1214             }
   1215 
   1216             /**
   1217              * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation}
   1218              * @param operation the {@code VolumeShaper.operation} whose data will be
   1219              *        reused in the new {@code Builder}.
   1220              */
   1221             public Builder(@NonNull VolumeShaper.Operation operation) {
   1222                 mReplaceId = operation.mReplaceId;
   1223                 mFlags = operation.mFlags;
   1224                 mXOffset = operation.mXOffset;
   1225             }
   1226 
   1227             /**
   1228              * Replaces the previous {@code VolumeShaper} specified by {@code id}.
   1229              *
   1230              * The {@code VolumeShaper} specified by the {@code id} is removed
   1231              * if it exists. The configuration should be TYPE_SCALE.
   1232              *
   1233              * @param id the {@code id} of the previous {@code VolumeShaper}.
   1234              * @param join if true, match the volume of the previous
   1235              * shaper to the start volume of the new {@code VolumeShaper}.
   1236              * @return the same {@code Builder} instance.
   1237              */
   1238             public @NonNull Builder replace(int id, boolean join) {
   1239                 mReplaceId = id;
   1240                 if (join) {
   1241                     mFlags |= FLAG_JOIN;
   1242                 } else {
   1243                     mFlags &= ~FLAG_JOIN;
   1244                 }
   1245                 return this;
   1246             }
   1247 
   1248             /**
   1249              * Defers all operations.
   1250              * @return the same {@code Builder} instance.
   1251              */
   1252             public @NonNull Builder defer() {
   1253                 mFlags |= FLAG_DEFER;
   1254                 return this;
   1255             }
   1256 
   1257             /**
   1258              * Terminates the {@code VolumeShaper}.
   1259              *
   1260              * Do not call directly, use {@link VolumeShaper#close()}.
   1261              * @return the same {@code Builder} instance.
   1262              */
   1263             public @NonNull Builder terminate() {
   1264                 mFlags |= FLAG_TERMINATE;
   1265                 return this;
   1266             }
   1267 
   1268             /**
   1269              * Reverses direction.
   1270              * @return the same {@code Builder} instance.
   1271              */
   1272             public @NonNull Builder reverse() {
   1273                 mFlags ^= FLAG_REVERSE;
   1274                 return this;
   1275             }
   1276 
   1277             /**
   1278              * Use the id specified in the configuration, creating
   1279              * {@code VolumeShaper} only as needed; the configuration should be
   1280              * TYPE_SCALE.
   1281              *
   1282              * If the {@code VolumeShaper} with the same id already exists
   1283              * then the operation has no effect.
   1284              *
   1285              * @return the same {@code Builder} instance.
   1286              */
   1287             public @NonNull Builder createIfNeeded() {
   1288                 mFlags |= FLAG_CREATE_IF_NEEDED;
   1289                 return this;
   1290             }
   1291 
   1292             /**
   1293              * Sets the {@code xOffset} to use for the {@code VolumeShaper}.
   1294              *
   1295              * The {@code xOffset} is the position on the volume curve,
   1296              * and setting takes effect when the {@code VolumeShaper} is used next.
   1297              *
   1298              * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore.
   1299              * @return the same {@code Builder} instance.
   1300              * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f,
   1301              *         or a Float.NaN.
   1302              */
   1303             public @NonNull Builder setXOffset(float xOffset) {
   1304                 if (xOffset < -0.f) {
   1305                     throw new IllegalArgumentException("Negative xOffset not allowed");
   1306                 } else if (xOffset > 1.f) {
   1307                     throw new IllegalArgumentException("xOffset > 1.f not allowed");
   1308                 }
   1309                 // Float.NaN passes through
   1310                 mXOffset = xOffset;
   1311                 return this;
   1312             }
   1313 
   1314             /**
   1315              * Sets the operation flag.  Do not call this directly but one of the
   1316              * other builder methods.
   1317              *
   1318              * @param flags new value for {@code flags}, consisting of ORed flags.
   1319              * @return the same {@code Builder} instance.
   1320              * @throws IllegalArgumentException if {@code flags} contains invalid set bits.
   1321              */
   1322             private @NonNull Builder setFlags(@Flag int flags) {
   1323                 if ((flags & ~FLAG_PUBLIC_ALL) != 0) {
   1324                     throw new IllegalArgumentException("flag has unknown bits set: " + flags);
   1325                 }
   1326                 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags;
   1327                 return this;
   1328             }
   1329 
   1330             /**
   1331              * Builds a new {@link VolumeShaper.Operation} object.
   1332              *
   1333              * @return a new {@code VolumeShaper.Operation} object
   1334              */
   1335             public @NonNull Operation build() {
   1336                 return new Operation(mFlags, mReplaceId, mXOffset);
   1337             }
   1338         } // Operation.Builder
   1339     } // Operation
   1340 
   1341     /**
   1342      * @hide
   1343      * {@code VolumeShaper.State} represents the current progress
   1344      * of the {@code VolumeShaper}.
   1345      *
   1346      *  Not for public use.
   1347      */
   1348     public static final class State implements Parcelable {
   1349         private float mVolume;
   1350         private float mXOffset;
   1351 
   1352         @Override
   1353         public String toString() {
   1354             return "VolumeShaper.State{"
   1355                     + "mVolume = " + mVolume
   1356                     + ", mXOffset = " + mXOffset
   1357                     + "}";
   1358         }
   1359 
   1360         @Override
   1361         public int hashCode() {
   1362             return Objects.hash(mVolume, mXOffset);
   1363         }
   1364 
   1365         @Override
   1366         public boolean equals(Object o) {
   1367             if (!(o instanceof State)) return false;
   1368             if (o == this) return true;
   1369             final State other = (State) o;
   1370             return mVolume == other.mVolume
   1371                     && mXOffset == other.mXOffset;
   1372         }
   1373 
   1374         @Override
   1375         public int describeContents() {
   1376             return 0;
   1377         }
   1378 
   1379         @Override
   1380         public void writeToParcel(Parcel dest, int flags) {
   1381             dest.writeFloat(mVolume);
   1382             dest.writeFloat(mXOffset);
   1383         }
   1384 
   1385         public static final Parcelable.Creator<VolumeShaper.State> CREATOR
   1386                 = new Parcelable.Creator<VolumeShaper.State>() {
   1387             @Override
   1388             public VolumeShaper.State createFromParcel(Parcel p) {
   1389                 return new VolumeShaper.State(
   1390                         p.readFloat()     // volume
   1391                         , p.readFloat()); // xOffset
   1392             }
   1393 
   1394             @Override
   1395             public VolumeShaper.State[] newArray(int size) {
   1396                 return new VolumeShaper.State[size];
   1397             }
   1398         };
   1399 
   1400         /* package */ State(float volume, float xOffset) {
   1401             mVolume = volume;
   1402             mXOffset = xOffset;
   1403         }
   1404 
   1405         /**
   1406          * Gets the volume of the {@link VolumeShaper.State}.
   1407          * @return linear volume between 0.f and 1.f.
   1408          */
   1409         public float getVolume() {
   1410             return mVolume;
   1411         }
   1412 
   1413         /**
   1414          * Gets the {@code xOffset} position on the normalized curve
   1415          * of the {@link VolumeShaper.State}.
   1416          * @return the curve x position between 0.f and 1.f.
   1417          */
   1418         public float getXOffset() {
   1419             return mXOffset;
   1420         }
   1421     } // State
   1422 }
   1423