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