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.os.Parcel;
22import android.os.Parcelable;
23
24import java.lang.annotation.Retention;
25import java.lang.annotation.RetentionPolicy;
26import java.lang.AutoCloseable;
27import java.lang.ref.WeakReference;
28import java.util.Arrays;
29import 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 */
46public final class VolumeShaper implements AutoCloseable {
47    /* member variables */
48    private int mId;
49    private final WeakReference<PlayerBase> mWeakPlayerBase;
50
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
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     */
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     */
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     */
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
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
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     */
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     */
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         */
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
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
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
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
470        public int describeContents() {
471            return 0;
472        }
473
474        @Override
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         */
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         */
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         */
588        public @Type int getType() {
589            return mType;
590        }
591
592        /**
593         * @hide
594         * Returns the {@code VolumeShaper} id.
595         */
596        public int getId() {
597            return mId;
598        }
599
600        /**
601         * Returns the interpolator type.
602         */
603        public @InterpolatorType int getInterpolatorType() {
604            return mInterpolatorType;
605        }
606
607        /**
608         * @hide
609         * Returns the option flags
610         */
611        public @OptionFlag int getOptionFlags() {
612            return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
613        }
614
615        /* package */ @OptionFlag int getAllOptionFlags() {
616            return mOptionFlags;
617        }
618
619        /**
620         * Returns the duration of the volume shape in milliseconds.
621         */
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         */
630        public float[] getTimes() {
631            return mTimes;
632        }
633
634        /**
635         * Returns the volumes (y) coordinate array of the volume curve points.
636         */
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         */
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
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
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
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             */
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             */
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             */
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             */
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             */
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             */
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
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             */
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             */
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             */
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             */
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             */
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
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
1139        public int hashCode() {
1140            return Objects.hash(mFlags, mReplaceId, mXOffset);
1141        }
1142
1143        @Override
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
1155        public int describeContents() {
1156            return 0;
1157        }
1158
1159        @Override
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
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             */
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             */
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             */
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             */
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             */
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             */
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             */
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             */
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             */
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             */
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
1351        public String toString() {
1352            return "VolumeShaper.State{"
1353                    + "mVolume = " + mVolume
1354                    + ", mXOffset = " + mXOffset
1355                    + "}";
1356        }
1357
1358        @Override
1359        public int hashCode() {
1360            return Objects.hash(mVolume, mXOffset);
1361        }
1362
1363        @Override
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
1373        public int describeContents() {
1374            return 0;
1375        }
1376
1377        @Override
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
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         */
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         */
1416        public float getXOffset() {
1417            return mXOffset;
1418        }
1419    } // State
1420}
1421