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