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.compat.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.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         @UnsupportedAppUsage
417         private final int mType;
418 
419         // valid when mType is TYPE_ID
420         @UnsupportedAppUsage
421         private final int mId;
422 
423         // valid when mType is TYPE_SCALE
424         @UnsupportedAppUsage
425         private final int mOptionFlags;
426         @UnsupportedAppUsage
427         private final double mDurationMs;
428         @UnsupportedAppUsage
429         private final int mInterpolatorType;
430         @UnsupportedAppUsage
431         private final float[] mTimes;
432         @UnsupportedAppUsage
433         private final float[] mVolumes;
434 
435         @Override
toString()436         public String toString() {
437             return "VolumeShaper.Configuration{"
438                     + "mType = " + mType
439                     + ", mId = " + mId
440                     + (mType == TYPE_ID
441                         ? "}"
442                         : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase()
443                         + ", mDurationMs = " + mDurationMs
444                         + ", mInterpolatorType = " + mInterpolatorType
445                         + ", mTimes[] = " + Arrays.toString(mTimes)
446                         + ", mVolumes[] = " + Arrays.toString(mVolumes)
447                         + "}");
448         }
449 
450         @Override
hashCode()451         public int hashCode() {
452             return mType == TYPE_ID
453                     ? Objects.hash(mType, mId)
454                     : Objects.hash(mType, mId,
455                             mOptionFlags, mDurationMs, mInterpolatorType,
456                             Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes));
457         }
458 
459         @Override
equals(Object o)460         public boolean equals(Object o) {
461             if (!(o instanceof Configuration)) return false;
462             if (o == this) return true;
463             final Configuration other = (Configuration) o;
464             // Note that exact floating point equality may not be guaranteed
465             // for a theoretically idempotent operation; for example,
466             // there are many cases where a + b - b != a.
467             return mType == other.mType
468                     && mId == other.mId
469                     && (mType == TYPE_ID
470                         ||  (mOptionFlags == other.mOptionFlags
471                             && mDurationMs == other.mDurationMs
472                             && mInterpolatorType == other.mInterpolatorType
473                             && Arrays.equals(mTimes, other.mTimes)
474                             && Arrays.equals(mVolumes, other.mVolumes)));
475         }
476 
477         @Override
describeContents()478         public int describeContents() {
479             return 0;
480         }
481 
482         @Override
writeToParcel(Parcel dest, int flags)483         public void writeToParcel(Parcel dest, int flags) {
484             // this needs to match the native VolumeShaper.Configuration parceling
485             dest.writeInt(mType);
486             dest.writeInt(mId);
487             if (mType != TYPE_ID) {
488                 dest.writeInt(mOptionFlags);
489                 dest.writeDouble(mDurationMs);
490                 // this needs to match the native Interpolator parceling
491                 dest.writeInt(mInterpolatorType);
492                 dest.writeFloat(0.f); // first slope (specifying for native side)
493                 dest.writeFloat(0.f); // last slope (specifying for native side)
494                 // mTimes and mVolumes should have the same length.
495                 dest.writeInt(mTimes.length);
496                 for (int i = 0; i < mTimes.length; ++i) {
497                     dest.writeFloat(mTimes[i]);
498                     dest.writeFloat(mVolumes[i]);
499                 }
500             }
501         }
502 
503         public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Configuration> CREATOR
504                 = new Parcelable.Creator<VolumeShaper.Configuration>() {
505             @Override
506             public VolumeShaper.Configuration createFromParcel(Parcel p) {
507                 // this needs to match the native VolumeShaper.Configuration parceling
508                 final int type = p.readInt();
509                 final int id = p.readInt();
510                 if (type == TYPE_ID) {
511                     return new VolumeShaper.Configuration(id);
512                 } else {
513                     final int optionFlags = p.readInt();
514                     final double durationMs = p.readDouble();
515                     // this needs to match the native Interpolator parceling
516                     final int interpolatorType = p.readInt();
517                     final float firstSlope = p.readFloat(); // ignored on the Java side
518                     final float lastSlope = p.readFloat();  // ignored on the Java side
519                     final int length = p.readInt();
520                     final float[] times = new float[length];
521                     final float[] volumes = new float[length];
522                     for (int i = 0; i < length; ++i) {
523                         times[i] = p.readFloat();
524                         volumes[i] = p.readFloat();
525                     }
526 
527                     return new VolumeShaper.Configuration(
528                         type,
529                         id,
530                         optionFlags,
531                         durationMs,
532                         interpolatorType,
533                         times,
534                         volumes);
535                 }
536             }
537 
538             @Override
539             public VolumeShaper.Configuration[] newArray(int size) {
540                 return new VolumeShaper.Configuration[size];
541             }
542         };
543 
544         /**
545          * @hide
546          * Constructs a {@code VolumeShaper} from an id.
547          *
548          * This is an opaque handle for controlling a {@code VolumeShaper} that has
549          * already been sent to a player.  The {@code id} is returned from the
550          * initial {@code setVolumeShaper()} call on success.
551          *
552          * These configurations are for native use only,
553          * they are never returned directly to the user.
554          *
555          * @param id
556          * @throws IllegalArgumentException if id is negative.
557          */
Configuration(int id)558         public Configuration(int id) {
559             if (id < 0) {
560                 throw new IllegalArgumentException("negative id " + id);
561             }
562             mType = TYPE_ID;
563             mId = id;
564             mInterpolatorType = 0;
565             mOptionFlags = 0;
566             mDurationMs = 0;
567             mTimes = null;
568             mVolumes = null;
569         }
570 
571         /**
572          * Direct constructor for VolumeShaper.
573          * Use the Builder instead.
574          */
575         @UnsupportedAppUsage
Configuration(@ype int type, int id, @OptionFlag int optionFlags, double durationMs, @InterpolatorType int interpolatorType, @NonNull float[] times, @NonNull float[] volumes)576         private Configuration(@Type int type,
577                 int id,
578                 @OptionFlag int optionFlags,
579                 double durationMs,
580                 @InterpolatorType int interpolatorType,
581                 @NonNull float[] times,
582                 @NonNull float[] volumes) {
583             mType = type;
584             mId = id;
585             mOptionFlags = optionFlags;
586             mDurationMs = durationMs;
587             mInterpolatorType = interpolatorType;
588             // Builder should have cloned these arrays already.
589             mTimes = times;
590             mVolumes = volumes;
591         }
592 
593         /**
594          * @hide
595          * Returns the {@code VolumeShaper} type.
596          */
getType()597         public @Type int getType() {
598             return mType;
599         }
600 
601         /**
602          * @hide
603          * Returns the {@code VolumeShaper} id.
604          */
getId()605         public int getId() {
606             return mId;
607         }
608 
609         /**
610          * Returns the interpolator type.
611          */
getInterpolatorType()612         public @InterpolatorType int getInterpolatorType() {
613             return mInterpolatorType;
614         }
615 
616         /**
617          * @hide
618          * Returns the option flags
619          */
getOptionFlags()620         public @OptionFlag int getOptionFlags() {
621             return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
622         }
623 
getAllOptionFlags()624         /* package */ @OptionFlag int getAllOptionFlags() {
625             return mOptionFlags;
626         }
627 
628         /**
629          * Returns the duration of the volume shape in milliseconds.
630          */
getDuration()631         public long getDuration() {
632             // casting is safe here as the duration was set as a long in the Builder
633             return (long) mDurationMs;
634         }
635 
636         /**
637          * Returns the times (x) coordinate array of the volume curve points.
638          */
getTimes()639         public float[] getTimes() {
640             return mTimes;
641         }
642 
643         /**
644          * Returns the volumes (y) coordinate array of the volume curve points.
645          */
getVolumes()646         public float[] getVolumes() {
647             return mVolumes;
648         }
649 
650         /**
651          * Checks the validity of times and volumes point representation.
652          *
653          * {@code times[]} and {@code volumes[]} are two arrays representing points
654          * for the volume curve.
655          *
656          * Note that {@code times[]} and {@code volumes[]} are explicitly checked against
657          * null here to provide the proper error string - those are legitimate
658          * arguments to this method.
659          *
660          * @param times the x coordinates for the points,
661          *        must be between 0.f and 1.f and be monotonic.
662          * @param volumes the y coordinates for the points,
663          *        must be between 0.f and 1.f for linear and
664          *        must be no greater than 0.f for log (dBFS).
665          * @param log set to true if the scale is logarithmic.
666          * @return null if no error, or the reason in a {@code String} for an error.
667          */
checkCurveForErrors( @ullable float[] times, @Nullable float[] volumes, boolean log)668         private static @Nullable String checkCurveForErrors(
669                 @Nullable float[] times, @Nullable float[] volumes, boolean log) {
670             if (times == null) {
671                 return "times array must be non-null";
672             } else if (volumes == null) {
673                 return "volumes array must be non-null";
674             } else if (times.length != volumes.length) {
675                 return "array length must match";
676             } else if (times.length < 2) {
677                 return "array length must be at least 2";
678             } else if (times.length > MAXIMUM_CURVE_POINTS) {
679                 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS;
680             } else if (times[0] != 0.f) {
681                 return "times must start at 0.f";
682             } else if (times[times.length - 1] != 1.f) {
683                 return "times must end at 1.f";
684             }
685 
686             // validate points along the curve
687             for (int i = 1; i < times.length; ++i) {
688                 if (!(times[i] > times[i - 1]) /* handle nan */) {
689                     return "times not monotonic increasing, check index " + i;
690                 }
691             }
692             if (log) {
693                 for (int i = 0; i < volumes.length; ++i) {
694                     if (!(volumes[i] <= 0.f) /* handle nan */) {
695                         return "volumes for log scale cannot be positive, "
696                                 + "check index " + i;
697                     }
698                 }
699             } else {
700                 for (int i = 0; i < volumes.length; ++i) {
701                     if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) {
702                         return "volumes for linear scale must be between 0.f and 1.f, "
703                                 + "check index " + i;
704                     }
705                 }
706             }
707             return null; // no errors
708         }
709 
checkCurveForErrorsAndThrowException( @ullable float[] times, @Nullable float[] volumes, boolean log, boolean ise)710         private static void checkCurveForErrorsAndThrowException(
711                 @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) {
712             final String error = checkCurveForErrors(times, volumes, log);
713             if (error != null) {
714                 if (ise) {
715                     throw new IllegalStateException(error);
716                 } else {
717                     throw new IllegalArgumentException(error);
718                 }
719             }
720         }
721 
checkValidVolumeAndThrowException(float volume, boolean log)722         private static void checkValidVolumeAndThrowException(float volume, boolean log) {
723             if (log) {
724                 if (!(volume <= 0.f) /* handle nan */) {
725                     throw new IllegalArgumentException("dbfs volume must be 0.f or less");
726                 }
727             } else {
728                 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) {
729                     throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f");
730                 }
731             }
732         }
733 
clampVolume(float[] volumes, boolean log)734         private static void clampVolume(float[] volumes, boolean log) {
735             if (log) {
736                 for (int i = 0; i < volumes.length; ++i) {
737                     if (!(volumes[i] <= 0.f) /* handle nan */) {
738                         volumes[i] = 0.f;
739                     }
740                 }
741             } else {
742                 for (int i = 0; i < volumes.length; ++i) {
743                     if (!(volumes[i] >= 0.f) /* handle nan */) {
744                         volumes[i] = 0.f;
745                     } else if (!(volumes[i] <= 1.f)) {
746                         volumes[i] = 1.f;
747                     }
748                 }
749             }
750         }
751 
752         /**
753          * Builder class for a {@link VolumeShaper.Configuration} object.
754          * <p> Here is an example where {@code Builder} is used to define the
755          * {@link VolumeShaper.Configuration}.
756          *
757          * <pre class="prettyprint">
758          * VolumeShaper.Configuration LINEAR_RAMP =
759          *         new VolumeShaper.Configuration.Builder()
760          *             .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
761          *             .setCurve(new float[] { 0.f, 1.f }, // times
762          *                       new float[] { 0.f, 1.f }) // volumes
763          *             .setDuration(1000)
764          *             .build();
765          * </pre>
766          * <p>
767          */
768         public static final class Builder {
769             private int mType = TYPE_SCALE;
770             private int mId = -1; // invalid
771             private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC;
772             private int mOptionFlags = OPTION_FLAG_CLOCK_TIME;
773             private double mDurationMs = 1000.;
774             private float[] mTimes = null;
775             private float[] mVolumes = null;
776 
777             /**
778              * Constructs a new {@code Builder} with the defaults.
779              */
Builder()780             public Builder() {
781             }
782 
783             /**
784              * Constructs a new {@code Builder} with settings
785              * copied from a given {@code VolumeShaper.Configuration}.
786              * @param configuration prototypical configuration
787              *        which will be reused in the new {@code Builder}.
788              */
Builder(@onNull Configuration configuration)789             public Builder(@NonNull Configuration configuration) {
790                 mType = configuration.getType();
791                 mId = configuration.getId();
792                 mOptionFlags = configuration.getAllOptionFlags();
793                 mInterpolatorType = configuration.getInterpolatorType();
794                 mDurationMs = configuration.getDuration();
795                 mTimes = configuration.getTimes().clone();
796                 mVolumes = configuration.getVolumes().clone();
797             }
798 
799             /**
800              * @hide
801              * Set the {@code id} for system defined shapers.
802              * @param id the {@code id} to set. If non-negative, then it is used.
803              *        If -1, then the system is expected to assign one.
804              * @return the same {@code Builder} instance.
805              * @throws IllegalArgumentException if {@code id} < -1.
806              */
setId(int id)807             public @NonNull Builder setId(int id) {
808                 if (id < -1) {
809                     throw new IllegalArgumentException("invalid id: " + id);
810                 }
811                 mId = id;
812                 return this;
813             }
814 
815             /**
816              * Sets the interpolator type.
817              *
818              * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}.
819              *
820              * @param interpolatorType method of interpolation used for the volume curve.
821              *        One of {@link #INTERPOLATOR_TYPE_STEP},
822              *        {@link #INTERPOLATOR_TYPE_LINEAR},
823              *        {@link #INTERPOLATOR_TYPE_CUBIC},
824              *        {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}.
825              * @return the same {@code Builder} instance.
826              * @throws IllegalArgumentException if {@code interpolatorType} is not valid.
827              */
setInterpolatorType(@nterpolatorType int interpolatorType)828             public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) {
829                 switch (interpolatorType) {
830                     case INTERPOLATOR_TYPE_STEP:
831                     case INTERPOLATOR_TYPE_LINEAR:
832                     case INTERPOLATOR_TYPE_CUBIC:
833                     case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
834                         mInterpolatorType = interpolatorType;
835                         break;
836                     default:
837                         throw new IllegalArgumentException("invalid interpolatorType: "
838                                 + interpolatorType);
839                 }
840                 return this;
841             }
842 
843             /**
844              * @hide
845              * Sets the optional flags
846              *
847              * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has
848              * changed the volume curve needs to be set again as the acceptable
849              * volume domain has changed.
850              *
851              * @param optionFlags new value to replace the old {@code optionFlags}.
852              * @return the same {@code Builder} instance.
853              * @throws IllegalArgumentException if flag is not recognized.
854              */
855             @TestApi
setOptionFlags(@ptionFlag int optionFlags)856             public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
857                 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
858                     throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
859                 }
860                 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags;
861                 return this;
862             }
863 
864             /**
865              * Sets the {@code VolumeShaper} duration in milliseconds.
866              *
867              * If omitted, the default duration is 1 second.
868              *
869              * @param durationMillis
870              * @return the same {@code Builder} instance.
871              * @throws IllegalArgumentException if {@code durationMillis}
872              *         is not strictly positive.
873              */
setDuration(long durationMillis)874             public @NonNull Builder setDuration(long durationMillis) {
875                 if (durationMillis <= 0) {
876                     throw new IllegalArgumentException(
877                             "duration: " + durationMillis + " not positive");
878                 }
879                 mDurationMs = (double) durationMillis;
880                 return this;
881             }
882 
883             /**
884              * Sets the volume curve.
885              *
886              * The volume curve is represented by a set of control points given by
887              * two float arrays of equal length,
888              * one representing the time (x) coordinates
889              * and one corresponding to the volume (y) coordinates.
890              * The length must be at least 2
891              * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}.
892              * <p>
893              * The volume curve is normalized as follows:
894              * time (x) coordinates should be monotonically increasing, from 0.f to 1.f;
895              * volume (y) coordinates must be within 0.f to 1.f.
896              * <p>
897              * The time scale is set by {@link #setDuration}.
898              * <p>
899              * @param times an array of float values representing
900              *        the time line of the volume curve.
901              * @param volumes an array of float values representing
902              *        the amplitude of the volume curve.
903              * @return the same {@code Builder} instance.
904              * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid.
905              */
906 
907             /* Note: volume (y) coordinates must be non-positive for log scaling,
908              * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set.
909              */
910 
setCurve(@onNull float[] times, @NonNull float[] volumes)911             public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) {
912                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
913                 checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */);
914                 mTimes = times.clone();
915                 mVolumes = volumes.clone();
916                 return this;
917             }
918 
919             /**
920              * Reflects the volume curve so that
921              * the shaper changes volume from the end
922              * to the start.
923              *
924              * @return the same {@code Builder} instance.
925              * @throws IllegalStateException if curve has not been set.
926              */
reflectTimes()927             public @NonNull Builder reflectTimes() {
928                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
929                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
930                 int i;
931                 for (i = 0; i < mTimes.length / 2; ++i) {
932                     float temp = mTimes[i];
933                     mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i];
934                     mTimes[mTimes.length - 1 - i] = 1.f - temp;
935                     temp = mVolumes[i];
936                     mVolumes[i] = mVolumes[mVolumes.length - 1 - i];
937                     mVolumes[mVolumes.length - 1 - i] = temp;
938                 }
939                 if ((mTimes.length & 1) != 0) {
940                     mTimes[i] = 1.f - mTimes[i];
941                 }
942                 return this;
943             }
944 
945             /**
946              * Inverts the volume curve so that the max volume
947              * becomes the min volume and vice versa.
948              *
949              * @return the same {@code Builder} instance.
950              * @throws IllegalStateException if curve has not been set.
951              */
invertVolumes()952             public @NonNull Builder invertVolumes() {
953                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
954                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
955                 float min = mVolumes[0];
956                 float max = mVolumes[0];
957                 for (int i = 1; i < mVolumes.length; ++i) {
958                     if (mVolumes[i] < min) {
959                         min = mVolumes[i];
960                     } else if (mVolumes[i] > max) {
961                         max = mVolumes[i];
962                     }
963                 }
964 
965                 final float maxmin = max + min;
966                 for (int i = 0; i < mVolumes.length; ++i) {
967                     mVolumes[i] = maxmin - mVolumes[i];
968                 }
969                 return this;
970             }
971 
972             /**
973              * Scale the curve end volume to a target value.
974              *
975              * Keeps the start volume the same.
976              * This works best if the volume curve is monotonic.
977              *
978              * @param volume the target end volume to use.
979              * @return the same {@code Builder} instance.
980              * @throws IllegalArgumentException if {@code volume} is not valid.
981              * @throws IllegalStateException if curve has not been set.
982              */
scaleToEndVolume(float volume)983             public @NonNull Builder scaleToEndVolume(float volume) {
984                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
985                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
986                 checkValidVolumeAndThrowException(volume, log);
987                 final float startVolume = mVolumes[0];
988                 final float endVolume = mVolumes[mVolumes.length - 1];
989                 if (endVolume == startVolume) {
990                     // match with linear ramp
991                     final float offset = volume - startVolume;
992                     for (int i = 0; i < mVolumes.length; ++i) {
993                         mVolumes[i] = mVolumes[i] + offset * mTimes[i];
994                     }
995                 } else {
996                     // scale
997                     final float scale = (volume - startVolume) / (endVolume - startVolume);
998                     for (int i = 0; i < mVolumes.length; ++i) {
999                         mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume;
1000                     }
1001                 }
1002                 clampVolume(mVolumes, log);
1003                 return this;
1004             }
1005 
1006             /**
1007              * Scale the curve start volume to a target value.
1008              *
1009              * Keeps the end volume the same.
1010              * This works best if the volume curve is monotonic.
1011              *
1012              * @param volume the target start volume to use.
1013              * @return the same {@code Builder} instance.
1014              * @throws IllegalArgumentException if {@code volume} is not valid.
1015              * @throws IllegalStateException if curve has not been set.
1016              */
scaleToStartVolume(float volume)1017             public @NonNull Builder scaleToStartVolume(float volume) {
1018                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
1019                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
1020                 checkValidVolumeAndThrowException(volume, log);
1021                 final float startVolume = mVolumes[0];
1022                 final float endVolume = mVolumes[mVolumes.length - 1];
1023                 if (endVolume == startVolume) {
1024                     // match with linear ramp
1025                     final float offset = volume - startVolume;
1026                     for (int i = 0; i < mVolumes.length; ++i) {
1027                         mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]);
1028                     }
1029                 } else {
1030                     final float scale = (volume - endVolume) / (startVolume - endVolume);
1031                     for (int i = 0; i < mVolumes.length; ++i) {
1032                         mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume;
1033                     }
1034                 }
1035                 clampVolume(mVolumes, log);
1036                 return this;
1037             }
1038 
1039             /**
1040              * Builds a new {@link VolumeShaper} object.
1041              *
1042              * @return a new {@link VolumeShaper} object.
1043              * @throws IllegalStateException if curve is not properly set.
1044              */
build()1045             public @NonNull Configuration build() {
1046                 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
1047                 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
1048                 return new Configuration(mType, mId, mOptionFlags, mDurationMs,
1049                         mInterpolatorType, mTimes, mVolumes);
1050             }
1051         } // Configuration.Builder
1052     } // Configuration
1053 
1054     /**
1055      * The {@code VolumeShaper.Operation} class is used to specify operations
1056      * to the {@code VolumeShaper} that affect the volume change.
1057      */
1058     public static final class Operation implements Parcelable {
1059         /**
1060          * Forward playback from current volume time position.
1061          * At the end of the {@code VolumeShaper} curve,
1062          * the last volume value persists.
1063          */
1064         public static final Operation PLAY =
1065                 new VolumeShaper.Operation.Builder()
1066                     .build();
1067 
1068         /**
1069          * Reverse playback from current volume time position.
1070          * When the position reaches the start of the {@code VolumeShaper} curve,
1071          * the first volume value persists.
1072          */
1073         public static final Operation REVERSE =
1074                 new VolumeShaper.Operation.Builder()
1075                     .reverse()
1076                     .build();
1077 
1078         // No user serviceable parts below.
1079 
1080         // These flags must match the native VolumeShaper::Operation::Flag
1081         /** @hide */
1082         @IntDef({
1083             FLAG_NONE,
1084             FLAG_REVERSE,
1085             FLAG_TERMINATE,
1086             FLAG_JOIN,
1087             FLAG_DEFER,
1088             })
1089         @Retention(RetentionPolicy.SOURCE)
1090         public @interface Flag {}
1091 
1092         /**
1093          * No special {@code VolumeShaper} operation.
1094          */
1095         private static final int FLAG_NONE = 0;
1096 
1097         /**
1098          * Reverse the {@code VolumeShaper} progress.
1099          *
1100          * Reverses the {@code VolumeShaper} curve from its current
1101          * position. If the {@code VolumeShaper} curve has not started,
1102          * it automatically is considered finished.
1103          */
1104         private static final int FLAG_REVERSE = 1 << 0;
1105 
1106         /**
1107          * Terminate the existing {@code VolumeShaper}.
1108          * This flag is generally used by itself;
1109          * it takes precedence over all other flags.
1110          */
1111         private static final int FLAG_TERMINATE = 1 << 1;
1112 
1113         /**
1114          * Attempt to join as best as possible to the previous {@code VolumeShaper}.
1115          * This requires the previous {@code VolumeShaper} to be active and
1116          * {@link #setReplaceId} to be set.
1117          */
1118         private static final int FLAG_JOIN = 1 << 2;
1119 
1120         /**
1121          * Defer playback until next operation is sent. This is used
1122          * when starting a {@code VolumeShaper} effect.
1123          */
1124         private static final int FLAG_DEFER = 1 << 3;
1125 
1126         /**
1127          * Use the id specified in the configuration, creating
1128          * {@code VolumeShaper} as needed; the configuration should be
1129          * TYPE_SCALE.
1130          */
1131         private static final int FLAG_CREATE_IF_NEEDED = 1 << 4;
1132 
1133         private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE;
1134 
1135         @UnsupportedAppUsage
1136         private final int mFlags;
1137         @UnsupportedAppUsage
1138         private final int mReplaceId;
1139         @UnsupportedAppUsage
1140         private final float mXOffset;
1141 
1142         @Override
toString()1143         public String toString() {
1144             return "VolumeShaper.Operation{"
1145                     + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase()
1146                     + ", mReplaceId = " + mReplaceId
1147                     + ", mXOffset = " + mXOffset
1148                     + "}";
1149         }
1150 
1151         @Override
hashCode()1152         public int hashCode() {
1153             return Objects.hash(mFlags, mReplaceId, mXOffset);
1154         }
1155 
1156         @Override
equals(Object o)1157         public boolean equals(Object o) {
1158             if (!(o instanceof Operation)) return false;
1159             if (o == this) return true;
1160             final Operation other = (Operation) o;
1161 
1162             return mFlags == other.mFlags
1163                     && mReplaceId == other.mReplaceId
1164                     && Float.compare(mXOffset, other.mXOffset) == 0;
1165         }
1166 
1167         @Override
describeContents()1168         public int describeContents() {
1169             return 0;
1170         }
1171 
1172         @Override
writeToParcel(Parcel dest, int flags)1173         public void writeToParcel(Parcel dest, int flags) {
1174             // this needs to match the native VolumeShaper.Operation parceling
1175             dest.writeInt(mFlags);
1176             dest.writeInt(mReplaceId);
1177             dest.writeFloat(mXOffset);
1178         }
1179 
1180         public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Operation> CREATOR
1181                 = new Parcelable.Creator<VolumeShaper.Operation>() {
1182             @Override
1183             public VolumeShaper.Operation createFromParcel(Parcel p) {
1184                 // this needs to match the native VolumeShaper.Operation parceling
1185                 final int flags = p.readInt();
1186                 final int replaceId = p.readInt();
1187                 final float xOffset = p.readFloat();
1188 
1189                 return new VolumeShaper.Operation(
1190                         flags
1191                         , replaceId
1192                         , xOffset);
1193             }
1194 
1195             @Override
1196             public VolumeShaper.Operation[] newArray(int size) {
1197                 return new VolumeShaper.Operation[size];
1198             }
1199         };
1200 
1201         @UnsupportedAppUsage
Operation(@lag int flags, int replaceId, float xOffset)1202         private Operation(@Flag int flags, int replaceId, float xOffset) {
1203             mFlags = flags;
1204             mReplaceId = replaceId;
1205             mXOffset = xOffset;
1206         }
1207 
1208         /**
1209          * @hide
1210          * {@code Builder} class for {@link VolumeShaper.Operation} object.
1211          *
1212          * Not for public use.
1213          */
1214         public static final class Builder {
1215             int mFlags;
1216             int mReplaceId;
1217             float mXOffset;
1218 
1219             /**
1220              * Constructs a new {@code Builder} with the defaults.
1221              */
Builder()1222             public Builder() {
1223                 mFlags = 0;
1224                 mReplaceId = -1;
1225                 mXOffset = Float.NaN;
1226             }
1227 
1228             /**
1229              * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation}
1230              * @param operation the {@code VolumeShaper.operation} whose data will be
1231              *        reused in the new {@code Builder}.
1232              */
Builder(@onNull VolumeShaper.Operation operation)1233             public Builder(@NonNull VolumeShaper.Operation operation) {
1234                 mReplaceId = operation.mReplaceId;
1235                 mFlags = operation.mFlags;
1236                 mXOffset = operation.mXOffset;
1237             }
1238 
1239             /**
1240              * Replaces the previous {@code VolumeShaper} specified by {@code id}.
1241              *
1242              * The {@code VolumeShaper} specified by the {@code id} is removed
1243              * if it exists. The configuration should be TYPE_SCALE.
1244              *
1245              * @param id the {@code id} of the previous {@code VolumeShaper}.
1246              * @param join if true, match the volume of the previous
1247              * shaper to the start volume of the new {@code VolumeShaper}.
1248              * @return the same {@code Builder} instance.
1249              */
replace(int id, boolean join)1250             public @NonNull Builder replace(int id, boolean join) {
1251                 mReplaceId = id;
1252                 if (join) {
1253                     mFlags |= FLAG_JOIN;
1254                 } else {
1255                     mFlags &= ~FLAG_JOIN;
1256                 }
1257                 return this;
1258             }
1259 
1260             /**
1261              * Defers all operations.
1262              * @return the same {@code Builder} instance.
1263              */
defer()1264             public @NonNull Builder defer() {
1265                 mFlags |= FLAG_DEFER;
1266                 return this;
1267             }
1268 
1269             /**
1270              * Terminates the {@code VolumeShaper}.
1271              *
1272              * Do not call directly, use {@link VolumeShaper#close()}.
1273              * @return the same {@code Builder} instance.
1274              */
terminate()1275             public @NonNull Builder terminate() {
1276                 mFlags |= FLAG_TERMINATE;
1277                 return this;
1278             }
1279 
1280             /**
1281              * Reverses direction.
1282              * @return the same {@code Builder} instance.
1283              */
reverse()1284             public @NonNull Builder reverse() {
1285                 mFlags ^= FLAG_REVERSE;
1286                 return this;
1287             }
1288 
1289             /**
1290              * Use the id specified in the configuration, creating
1291              * {@code VolumeShaper} only as needed; the configuration should be
1292              * TYPE_SCALE.
1293              *
1294              * If the {@code VolumeShaper} with the same id already exists
1295              * then the operation has no effect.
1296              *
1297              * @return the same {@code Builder} instance.
1298              */
createIfNeeded()1299             public @NonNull Builder createIfNeeded() {
1300                 mFlags |= FLAG_CREATE_IF_NEEDED;
1301                 return this;
1302             }
1303 
1304             /**
1305              * Sets the {@code xOffset} to use for the {@code VolumeShaper}.
1306              *
1307              * The {@code xOffset} is the position on the volume curve,
1308              * and setting takes effect when the {@code VolumeShaper} is used next.
1309              *
1310              * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore.
1311              * @return the same {@code Builder} instance.
1312              * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f,
1313              *         or a Float.NaN.
1314              */
setXOffset(float xOffset)1315             public @NonNull Builder setXOffset(float xOffset) {
1316                 if (xOffset < -0.f) {
1317                     throw new IllegalArgumentException("Negative xOffset not allowed");
1318                 } else if (xOffset > 1.f) {
1319                     throw new IllegalArgumentException("xOffset > 1.f not allowed");
1320                 }
1321                 // Float.NaN passes through
1322                 mXOffset = xOffset;
1323                 return this;
1324             }
1325 
1326             /**
1327              * Sets the operation flag.  Do not call this directly but one of the
1328              * other builder methods.
1329              *
1330              * @param flags new value for {@code flags}, consisting of ORed flags.
1331              * @return the same {@code Builder} instance.
1332              * @throws IllegalArgumentException if {@code flags} contains invalid set bits.
1333              */
setFlags(@lag int flags)1334             private @NonNull Builder setFlags(@Flag int flags) {
1335                 if ((flags & ~FLAG_PUBLIC_ALL) != 0) {
1336                     throw new IllegalArgumentException("flag has unknown bits set: " + flags);
1337                 }
1338                 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags;
1339                 return this;
1340             }
1341 
1342             /**
1343              * Builds a new {@link VolumeShaper.Operation} object.
1344              *
1345              * @return a new {@code VolumeShaper.Operation} object
1346              */
build()1347             public @NonNull Operation build() {
1348                 return new Operation(mFlags, mReplaceId, mXOffset);
1349             }
1350         } // Operation.Builder
1351     } // Operation
1352 
1353     /**
1354      * @hide
1355      * {@code VolumeShaper.State} represents the current progress
1356      * of the {@code VolumeShaper}.
1357      *
1358      *  Not for public use.
1359      */
1360     public static final class State implements Parcelable {
1361         @UnsupportedAppUsage
1362         private float mVolume;
1363         @UnsupportedAppUsage
1364         private float mXOffset;
1365 
1366         @Override
toString()1367         public String toString() {
1368             return "VolumeShaper.State{"
1369                     + "mVolume = " + mVolume
1370                     + ", mXOffset = " + mXOffset
1371                     + "}";
1372         }
1373 
1374         @Override
hashCode()1375         public int hashCode() {
1376             return Objects.hash(mVolume, mXOffset);
1377         }
1378 
1379         @Override
equals(Object o)1380         public boolean equals(Object o) {
1381             if (!(o instanceof State)) return false;
1382             if (o == this) return true;
1383             final State other = (State) o;
1384             return mVolume == other.mVolume
1385                     && mXOffset == other.mXOffset;
1386         }
1387 
1388         @Override
describeContents()1389         public int describeContents() {
1390             return 0;
1391         }
1392 
1393         @Override
writeToParcel(Parcel dest, int flags)1394         public void writeToParcel(Parcel dest, int flags) {
1395             dest.writeFloat(mVolume);
1396             dest.writeFloat(mXOffset);
1397         }
1398 
1399         public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.State> CREATOR
1400                 = new Parcelable.Creator<VolumeShaper.State>() {
1401             @Override
1402             public VolumeShaper.State createFromParcel(Parcel p) {
1403                 return new VolumeShaper.State(
1404                         p.readFloat()     // volume
1405                         , p.readFloat()); // xOffset
1406             }
1407 
1408             @Override
1409             public VolumeShaper.State[] newArray(int size) {
1410                 return new VolumeShaper.State[size];
1411             }
1412         };
1413 
1414         @UnsupportedAppUsage
State(float volume, float xOffset)1415         /* package */ State(float volume, float xOffset) {
1416             mVolume = volume;
1417             mXOffset = xOffset;
1418         }
1419 
1420         /**
1421          * Gets the volume of the {@link VolumeShaper.State}.
1422          * @return linear volume between 0.f and 1.f.
1423          */
getVolume()1424         public float getVolume() {
1425             return mVolume;
1426         }
1427 
1428         /**
1429          * Gets the {@code xOffset} position on the normalized curve
1430          * of the {@link VolumeShaper.State}.
1431          * @return the curve x position between 0.f and 1.f.
1432          */
getXOffset()1433         public float getXOffset() {
1434             return mXOffset;
1435         }
1436     } // State
1437 }
1438