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 
VolumeShaper( @onNull Configuration configuration, @NonNull PlayerBase playerBase)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 
getId()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      */
apply(@onNull Operation operation)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      */
replace( @onNull Configuration configuration, @NonNull Operation operation, boolean join)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      */
getVolume()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
close()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
finalize()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      */
applyPlayer( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)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      */
getStatePlayer(int id)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          */
getMaximumCurvePoints()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
toString()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
hashCode()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
equals(Object o)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
describeContents()471         public int describeContents() {
472             return 0;
473         }
474 
475         @Override
writeToParcel(Parcel dest, int flags)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          */
Configuration(int id)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          */
Configuration(@ype int type, int id, @OptionFlag int optionFlags, double durationMs, @InterpolatorType int interpolatorType, @NonNull float[] times, @NonNull float[] volumes)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          */
getType()589         public @Type int getType() {
590             return mType;
591         }
592 
593         /**
594          * @hide
595          * Returns the {@code VolumeShaper} id.
596          */
getId()597         public int getId() {
598             return mId;
599         }
600 
601         /**
602          * Returns the interpolator type.
603          */
getInterpolatorType()604         public @InterpolatorType int getInterpolatorType() {
605             return mInterpolatorType;
606         }
607 
608         /**
609          * @hide
610          * Returns the option flags
611          */
getOptionFlags()612         public @OptionFlag int getOptionFlags() {
613             return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
614         }
615 
getAllOptionFlags()616         /* package */ @OptionFlag int getAllOptionFlags() {
617             return mOptionFlags;
618         }
619 
620         /**
621          * Returns the duration of the volume shape in milliseconds.
622          */
getDuration()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          */
getTimes()631         public float[] getTimes() {
632             return mTimes;
633         }
634 
635         /**
636          * Returns the volumes (y) coordinate array of the volume curve points.
637          */
getVolumes()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          */
checkCurveForErrors( @ullable float[] times, @Nullable float[] volumes, boolean log)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 
checkCurveForErrorsAndThrowException( @ullable float[] times, @Nullable float[] volumes, boolean log, boolean ise)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 
checkValidVolumeAndThrowException(float volume, boolean log)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 
clampVolume(float[] volumes, boolean log)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              */
Builder()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              */
Builder(@onNull Configuration configuration)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              */
setId(int id)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              */
setInterpolatorType(@nterpolatorType int interpolatorType)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
setOptionFlags(@ptionFlag int optionFlags)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              */
setDuration(long durationMillis)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 
setCurve(@onNull float[] times, @NonNull float[] volumes)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              */
reflectTimes()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              */
invertVolumes()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              */
scaleToEndVolume(float volume)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              */
scaleToStartVolume(float volume)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              */
build()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
toString()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
hashCode()1141         public int hashCode() {
1142             return Objects.hash(mFlags, mReplaceId, mXOffset);
1143         }
1144 
1145         @Override
equals(Object o)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
describeContents()1157         public int describeContents() {
1158             return 0;
1159         }
1160 
1161         @Override
writeToParcel(Parcel dest, int flags)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 
Operation(@lag int flags, int replaceId, float xOffset)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              */
Builder()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              */
Builder(@onNull VolumeShaper.Operation operation)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              */
replace(int id, boolean join)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              */
defer()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              */
terminate()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              */
reverse()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              */
createIfNeeded()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              */
setXOffset(float xOffset)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              */
setFlags(@lag int flags)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              */
build()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
toString()1353         public String toString() {
1354             return "VolumeShaper.State{"
1355                     + "mVolume = " + mVolume
1356                     + ", mXOffset = " + mXOffset
1357                     + "}";
1358         }
1359 
1360         @Override
hashCode()1361         public int hashCode() {
1362             return Objects.hash(mVolume, mXOffset);
1363         }
1364 
1365         @Override
equals(Object o)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
describeContents()1375         public int describeContents() {
1376             return 0;
1377         }
1378 
1379         @Override
writeToParcel(Parcel dest, int flags)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 
State(float volume, float xOffset)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          */
getVolume()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          */
getXOffset()1418         public float getXOffset() {
1419             return mXOffset;
1420         }
1421     } // State
1422 }
1423