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