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