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