VolumeShaper.java revision d4f1e86190fbe6b280635902a3cd734d65eded52
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                .setDurationMs(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                .setDurationMs(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                .setDurationMs(1000.)
352                .build();
353            SCURVE_RAMP = new VolumeShaper.Configuration.Builder()
354                .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
355                .setCurve(times, scurve)
356                .setDurationMs(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
435                dest.writeFloat(0.f); // last slope
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
460                    final float lastSlope = p.readFloat();  // ignored
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 double getDurationMs() {
573            return mDurationMs;
574        }
575
576        /**
577         * Returns the times (x) coordinate array of the volume curve points.
578         */
579        public float[] getTimes() {
580            return mTimes;
581        }
582
583        /**
584         * Returns the volumes (y) coordinate array of the volume curve points.
585         */
586        public float[] getVolumes() {
587            return mVolumes;
588        }
589
590        /**
591         * Checks the validity of times and volumes point representation.
592         *
593         * {@code times[]} and {@code volumes[]} are two arrays representing points
594         * for the volume curve.
595         *
596         * @param times the x coordinates for the points,
597         *        must be between 0.f and 1.f and be monotonic.
598         * @param volumes the y coordinates for the points,
599         *        must be between 0.f and 1.f for linear and
600         *        must be no greater than 0.f for log (dBFS).
601         * @param log set to true if the scale is logarithmic.
602         * @return null if no error, or the reason in a {@code String} for an error.
603         */
604        private static @Nullable String checkCurveForErrors(
605                @Nullable float[] times, @Nullable float[] volumes, boolean log) {
606            if (times == null) {
607                return "times array must be non-null";
608            } else if (volumes == null) {
609                return "volumes array must be non-null";
610            } else if (times.length != volumes.length) {
611                return "array length must match";
612            } else if (times.length < 2) {
613                return "array length must be at least 2";
614            } else if (times.length > MAXIMUM_CURVE_POINTS) {
615                return "array length must be no larger than " + MAXIMUM_CURVE_POINTS;
616            } else if (times[0] != 0.f) {
617                return "times must start at 0.f";
618            } else if (times[times.length - 1] != 1.f) {
619                return "times must end at 1.f";
620            }
621
622            // validate points along the curve
623            for (int i = 1; i < times.length; ++i) {
624                if (!(times[i] > times[i - 1]) /* handle nan */) {
625                    return "times not monotonic increasing, check index " + i;
626                }
627            }
628            if (log) {
629                for (int i = 0; i < volumes.length; ++i) {
630                    if (!(volumes[i] <= 0.f) /* handle nan */) {
631                        return "volumes for log scale cannot be positive, "
632                                + "check index " + i;
633                    }
634                }
635            } else {
636                for (int i = 0; i < volumes.length; ++i) {
637                    if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) {
638                        return "volumes for linear scale must be between 0.f and 1.f, "
639                                + "check index " + i;
640                    }
641                }
642            }
643            return null; // no errors
644        }
645
646        private static void checkCurveForErrorsAndThrowException(
647                @Nullable float[] times, @Nullable float[] volumes, boolean log) {
648            final String error = checkCurveForErrors(times, volumes, log);
649            if (error != null) {
650                throw new IllegalArgumentException(error);
651            }
652        }
653
654        private static void checkValidVolumeAndThrowException(float volume, boolean log) {
655            if (log) {
656                if (!(volume <= 0.f) /* handle nan */) {
657                    throw new IllegalArgumentException("dbfs volume must be 0.f or less");
658                }
659            } else {
660                if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) {
661                    throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f");
662                }
663            }
664        }
665
666        private static void clampVolume(float[] volumes, boolean log) {
667            if (log) {
668                for (int i = 0; i < volumes.length; ++i) {
669                    if (!(volumes[i] <= 0.f) /* handle nan */) {
670                        volumes[i] = 0.f;
671                    }
672                }
673            } else {
674                for (int i = 0; i < volumes.length; ++i) {
675                    if (!(volumes[i] >= 0.f) /* handle nan */) {
676                        volumes[i] = 0.f;
677                    } else if (!(volumes[i] <= 1.f)) {
678                        volumes[i] = 1.f;
679                    }
680                }
681            }
682        }
683
684        /**
685         * Builder class for a {@link VolumeShaper.Configuration} object.
686         * <p> Here is an example where {@code Builder} is used to define the
687         * {@link VolumeShaper.Configuration}.
688         *
689         * <pre class="prettyprint">
690         * VolumeShaper.Configuration LINEAR_RAMP =
691         *         new VolumeShaper.Configuration.Builder()
692         *             .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
693         *             .setCurve(new float[] { 0.f, 1.f }, // times
694         *                       new float[] { 0.f, 1.f }) // volumes
695         *             .setDurationMs(1000.)
696         *             .build();
697         * </pre>
698         * <p>
699         */
700        public static final class Builder {
701            private int mType = TYPE_SCALE;
702            private int mId = -1; // invalid
703            private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC;
704            private int mOptionFlags = OPTION_FLAG_CLOCK_TIME;
705            private double mDurationMs = 1000.;
706            private float[] mTimes = null;
707            private float[] mVolumes = null;
708
709            /**
710             * Constructs a new {@code Builder} with the defaults.
711             */
712            public Builder() {
713            }
714
715            /**
716             * Constructs a new {@code Builder} with settings
717             * copied from a given {@code VolumeShaper.Configuration}.
718             * @param configuration prototypical configuration
719             *        which will be reused in the new {@code Builder}.
720             */
721            public Builder(@NonNull Configuration configuration) {
722                mType = configuration.getType();
723                mId = configuration.getId();
724                mOptionFlags = configuration.getAllOptionFlags();
725                mInterpolatorType = configuration.getInterpolatorType();
726                mDurationMs = configuration.getDurationMs();
727                mTimes = configuration.getTimes().clone();
728                mVolumes = configuration.getVolumes().clone();
729            }
730
731            /**
732             * @hide
733             * Set the {@code id} for system defined shapers.
734             * @param id the {@code id} to set. If non-negative, then it is used.
735             *        If -1, then the system is expected to assign one.
736             * @return the same {@code Builder} instance.
737             * @throws IllegalArgumentException if {@code id} < -1.
738             */
739            public @NonNull Builder setId(int id) {
740                if (id < -1) {
741                    throw new IllegalArgumentException("invalid id: " + id);
742                }
743                mId = id;
744                return this;
745            }
746
747            /**
748             * Sets the interpolator type.
749             *
750             * If omitted the interplator type is {@link #INTERPOLATOR_TYPE_CUBIC}.
751             *
752             * @param interpolatorType method of interpolation used for the volume curve.
753             *        One of {@link #INTERPOLATOR_TYPE_STEP},
754             *        {@link #INTERPOLATOR_TYPE_LINEAR},
755             *        {@link #INTERPOLATOR_TYPE_CUBIC},
756             *        {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}.
757             * @return the same {@code Builder} instance.
758             * @throws IllegalArgumentException if {@code interpolatorType} is not valid.
759             */
760            public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) {
761                switch (interpolatorType) {
762                    case INTERPOLATOR_TYPE_STEP:
763                    case INTERPOLATOR_TYPE_LINEAR:
764                    case INTERPOLATOR_TYPE_CUBIC:
765                    case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
766                        mInterpolatorType = interpolatorType;
767                        break;
768                    default:
769                        throw new IllegalArgumentException("invalid interpolatorType: "
770                                + interpolatorType);
771                }
772                return this;
773            }
774
775            /**
776             * @hide
777             * Sets the optional flags
778             *
779             * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has
780             * changed the volume curve needs to be set again as the acceptable
781             * volume domain has changed.
782             *
783             * @param optionFlags new value to replace the old {@code optionFlags}.
784             * @return the same {@code Builder} instance.
785             * @throws IllegalArgumentException if flag is not recognized.
786             */
787            public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
788                if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
789                    throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
790                }
791                mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags;
792                return this;
793            }
794
795            /**
796             * Sets the volume shaper duration in milliseconds.
797             *
798             * If omitted, the default duration is 1 second.
799             *
800             * @param durationMs
801             * @return the same {@code Builder} instance.
802             * @throws IllegalArgumentException if {@code durationMs}
803             *         is not strictly positive.
804             */
805            public @NonNull Builder setDurationMs(double durationMs) {
806                if (durationMs <= 0.) {
807                    throw new IllegalArgumentException(
808                            "duration: " + durationMs + " not positive");
809                }
810                mDurationMs = durationMs;
811                return this;
812            }
813
814            /**
815             * Sets the volume curve.
816             *
817             * The volume curve is represented by a set of control points given by
818             * two float arrays of equal length,
819             * one representing the time (x) coordinates
820             * and one corresponding to the volume (y) coordinates.
821             * The length must be at least 2
822             * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}.
823             * <p>
824             * The volume curve is normalized as follows:
825             * time (x) coordinates should be monotonically increasing, from 0.f to 1.f;
826             * volume (y) coordinates must be within 0.f to 1.f.
827             * <p>
828             * The time scale is set by {@link #setDurationMs}.
829             * <p>
830             * @param times an array of float values representing
831             *        the time line of the volume curve.
832             * @param volumes an array of float values representing
833             *        the amplitude of the volume curve.
834             * @return the same {@code Builder} instance.
835             * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid.
836             */
837
838            /* Note: volume (y) coordinates must be non-positive for log scaling,
839             * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set.
840             */
841
842            public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) {
843                checkCurveForErrorsAndThrowException(
844                        times, volumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0);
845                mTimes = times.clone();
846                mVolumes = volumes.clone();
847                return this;
848            }
849
850            /**
851             * Reflects the volume curve so that
852             * the shaper changes volume from the end
853             * to the start.
854             *
855             * @return the same {@code Builder} instance.
856             * @throws IllegalArgumentException if curve has not been set.
857             */
858            public @NonNull Builder reflectTimes() {
859                checkCurveForErrorsAndThrowException(
860                        mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0);
861                int i;
862                for (i = 0; i < mTimes.length / 2; ++i) {
863                    float temp = mTimes[i];
864                    mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i];
865                    mTimes[mTimes.length - 1 - i] = 1.f - temp;
866                    temp = mVolumes[i];
867                    mVolumes[i] = mVolumes[mVolumes.length - 1 - i];
868                    mVolumes[mVolumes.length - 1 - i] = temp;
869                }
870                if ((mTimes.length & 1) != 0) {
871                    mTimes[i] = 1.f - mTimes[i];
872                }
873                return this;
874            }
875
876            /**
877             * Inverts the volume curve so that the max volume
878             * becomes the min volume and vice versa.
879             *
880             * @return the same {@code Builder} instance.
881             * @throws IllegalArgumentException if curve has not been set.
882             */
883            public @NonNull Builder invertVolumes() {
884                checkCurveForErrorsAndThrowException(
885                        mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0);
886                float min = mVolumes[0];
887                float max = mVolumes[0];
888                for (int i = 1; i < mVolumes.length; ++i) {
889                    if (mVolumes[i] < min) {
890                        min = mVolumes[i];
891                    } else if (mVolumes[i] > max) {
892                        max = mVolumes[i];
893                    }
894                }
895
896                final float maxmin = max + min;
897                for (int i = 0; i < mVolumes.length; ++i) {
898                    mVolumes[i] = maxmin - mVolumes[i];
899                }
900                return this;
901            }
902
903            /**
904             * Scale the curve end volume to a target value.
905             *
906             * Keeps the start volume the same.
907             * This works best if the volume curve is monotonic.
908             *
909             * @param volume the target end volume to use.
910             * @return the same {@code Builder} instance.
911             * @throws IllegalArgumentException if {@code volume}
912             *         is not valid or if curve has not been set.
913             */
914            public @NonNull Builder scaleToEndVolume(float volume) {
915                final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
916                checkCurveForErrorsAndThrowException(mTimes, mVolumes, log);
917                checkValidVolumeAndThrowException(volume, log);
918                final float startVolume = mVolumes[0];
919                final float endVolume = mVolumes[mVolumes.length - 1];
920                if (endVolume == startVolume) {
921                    // match with linear ramp
922                    final float offset = volume - startVolume;
923                    for (int i = 0; i < mVolumes.length; ++i) {
924                        mVolumes[i] = mVolumes[i] + offset * mTimes[i];
925                    }
926                } else {
927                    // scale
928                    final float scale = (volume - startVolume) / (endVolume - startVolume);
929                    for (int i = 0; i < mVolumes.length; ++i) {
930                        mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume;
931                    }
932                }
933                clampVolume(mVolumes, log);
934                return this;
935            }
936
937            /**
938             * Scale the curve start volume to a target value.
939             *
940             * Keeps the end volume the same.
941             * This works best if the volume curve is monotonic.
942             *
943             * @param volume the target start volume to use.
944             * @return the same {@code Builder} instance.
945             * @throws IllegalArgumentException if {@code volume}
946             *         is not valid or if curve has not been set.
947             */
948            public @NonNull Builder scaleToStartVolume(float volume) {
949                final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
950                checkCurveForErrorsAndThrowException(mTimes, mVolumes, log);
951                checkValidVolumeAndThrowException(volume, log);
952                final float startVolume = mVolumes[0];
953                final float endVolume = mVolumes[mVolumes.length - 1];
954                if (endVolume == startVolume) {
955                    // match with linear ramp
956                    final float offset = volume - startVolume;
957                    for (int i = 0; i < mVolumes.length; ++i) {
958                        mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]);
959                    }
960                } else {
961                    final float scale = (volume - endVolume) / (startVolume - endVolume);
962                    for (int i = 0; i < mVolumes.length; ++i) {
963                        mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume;
964                    }
965                }
966                clampVolume(mVolumes, log);
967                return this;
968            }
969
970            /**
971             * Builds a new {@link VolumeShaper} object.
972             *
973             * @return a new {@link VolumeShaper} object.
974             * @throws IllegalArgumentException if curve is not properly set.
975             */
976            public @NonNull Configuration build() {
977                checkCurveForErrorsAndThrowException(
978                        mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0);
979                return new Configuration(mType, mId, mOptionFlags, mDurationMs,
980                        mInterpolatorType, mTimes, mVolumes);
981            }
982        } // Configuration.Builder
983    } // Configuration
984
985    /**
986     * The {@code VolumeShaper.Operation} class is used to specify operations
987     * to the {@code VolumeShaper} that affect the volume change.
988     */
989    public static final class Operation implements Parcelable {
990        /**
991         * Forward playback from current volume time position.
992         * At the end of the {@code VolumeShaper} curve,
993         * the last volume value persists.
994         */
995        public static final Operation PLAY =
996                new VolumeShaper.Operation.Builder()
997                    .build();
998
999        /**
1000         * Reverse playback from current volume time position.
1001         * When the position reaches the start of the {@code VolumeShaper} curve,
1002         * the first volume value persists.
1003         */
1004        public static final Operation REVERSE =
1005                new VolumeShaper.Operation.Builder()
1006                    .reverse()
1007                    .build();
1008
1009        // No user serviceable parts below.
1010
1011        // These flags must match the native VolumeShaper::Operation::Flag
1012        /** @hide */
1013        @IntDef({
1014            FLAG_NONE,
1015            FLAG_REVERSE,
1016            FLAG_TERMINATE,
1017            FLAG_JOIN,
1018            FLAG_DEFER,
1019            })
1020        @Retention(RetentionPolicy.SOURCE)
1021        public @interface Flag {}
1022
1023        /**
1024         * No special {@code VolumeShaper} operation.
1025         */
1026        private static final int FLAG_NONE = 0;
1027
1028        /**
1029         * Reverse the {@code VolumeShaper} progress.
1030         *
1031         * Reverses the {@code VolumeShaper} curve from its current
1032         * position. If the {@code VolumeShaper} curve has not started,
1033         * it automatically is considered finished.
1034         */
1035        private static final int FLAG_REVERSE = 1 << 0;
1036
1037        /**
1038         * Terminate the existing {@code VolumeShaper}.
1039         * This flag is generally used by itself;
1040         * it takes precedence over all other flags.
1041         */
1042        private static final int FLAG_TERMINATE = 1 << 1;
1043
1044        /**
1045         * Attempt to join as best as possible to the previous {@code VolumeShaper}.
1046         * This requires the previous {@code VolumeShaper} to be active and
1047         * {@link #setReplaceId} to be set.
1048         */
1049        private static final int FLAG_JOIN = 1 << 2;
1050
1051        /**
1052         * Defer playback until next operation is sent. This is used
1053         * when starting a VolumeShaper effect.
1054         */
1055        private static final int FLAG_DEFER = 1 << 3;
1056
1057        /**
1058         * Use the id specified in the configuration, creating
1059         * VolumeShaper as needed; the configuration should be
1060         * TYPE_SCALE.
1061         */
1062        private static final int FLAG_CREATE_IF_NEEDED = 1 << 4;
1063
1064        private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE;
1065
1066        private final int mFlags;
1067        private final int mReplaceId;
1068
1069        @Override
1070        public String toString() {
1071            return "VolumeShaper.Operation{"
1072                    + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase()
1073                    + ", mReplaceId = " + mReplaceId
1074                    + "}";
1075        }
1076
1077        @Override
1078        public int hashCode() {
1079            return Objects.hash(mFlags, mReplaceId);
1080        }
1081
1082        @Override
1083        public boolean equals(Object o) {
1084            if (!(o instanceof Operation)) return false;
1085            if (o == this) return true;
1086            final Operation other = (Operation) o;
1087            // if xOffset (native field only) is brought into Java
1088            // we need to do proper NaN comparison as that is allowed.
1089            return mFlags == other.mFlags
1090                    && mReplaceId == other.mReplaceId;
1091        }
1092
1093        @Override
1094        public int describeContents() {
1095            return 0;
1096        }
1097
1098        @Override
1099        public void writeToParcel(Parcel dest, int flags) {
1100            // this needs to match the native VolumeShaper.Operation parceling
1101            dest.writeInt(mFlags);
1102            dest.writeInt(mReplaceId);
1103            dest.writeFloat(Float.NaN); // xOffset (ignored at Java level)
1104        }
1105
1106        public static final Parcelable.Creator<VolumeShaper.Operation> CREATOR
1107                = new Parcelable.Creator<VolumeShaper.Operation>() {
1108            @Override
1109            public VolumeShaper.Operation createFromParcel(Parcel p) {
1110                // this needs to match the native VolumeShaper.Operation parceling
1111                final int flags = p.readInt();
1112                final int replaceId = p.readInt();
1113                final float xOffset = p.readFloat(); // ignored at Java level
1114
1115                return new VolumeShaper.Operation(
1116                        flags
1117                        , replaceId);
1118            }
1119
1120            @Override
1121            public VolumeShaper.Operation[] newArray(int size) {
1122                return new VolumeShaper.Operation[size];
1123            }
1124        };
1125
1126        private Operation(@Flag int flags, int replaceId) {
1127            mFlags = flags;
1128            mReplaceId = replaceId;
1129        }
1130
1131        /**
1132         * @hide
1133         * {@code Builder} class for {@link VolumeShaper.Operation} object.
1134         *
1135         * Not for public use.
1136         */
1137        public static final class Builder {
1138            int mFlags;
1139            int mReplaceId;
1140
1141            /**
1142             * Constructs a new {@code Builder} with the defaults.
1143             */
1144            public Builder() {
1145                mFlags = 0;
1146                mReplaceId = -1;
1147            }
1148
1149            /**
1150             * Constructs a new Builder from a given {@code VolumeShaper.Operation}
1151             * @param operation the {@code VolumeShaper.operation} whose data will be
1152             *        reused in the new Builder.
1153             */
1154            public Builder(@NonNull VolumeShaper.Operation operation) {
1155                mReplaceId = operation.mReplaceId;
1156                mFlags = operation.mFlags;
1157            }
1158
1159            /**
1160             * Replaces the previous {@code VolumeShaper} specified by id.
1161             * It has no other effect if the {@code VolumeShaper} is
1162             * already expired.
1163             * @param id the id of the previous {@code VolumeShaper}.
1164             * @param join if true, match the volume of the previous
1165             * shaper to the start volume of the new {@code VolumeShaper}.
1166             * @return the same {@code Builder} instance.
1167             */
1168            public @NonNull Builder replace(int id, boolean join) {
1169                mReplaceId = id;
1170                if (join) {
1171                    mFlags |= FLAG_JOIN;
1172                } else {
1173                    mFlags &= ~FLAG_JOIN;
1174                }
1175                return this;
1176            }
1177
1178            /**
1179             * Defers all operations.
1180             * @return the same {@code Builder} instance.
1181             */
1182            public @NonNull Builder defer() {
1183                mFlags |= FLAG_DEFER;
1184                return this;
1185            }
1186
1187            /**
1188             * Terminates the VolumeShaper.
1189             * Do not call directly, use {@link VolumeShaper#release()}.
1190             * @return the same {@code Builder} instance.
1191             */
1192            public @NonNull Builder terminate() {
1193                mFlags |= FLAG_TERMINATE;
1194                return this;
1195            }
1196
1197            /**
1198             * Reverses direction.
1199             * @return the same {@code Builder} instance.
1200             */
1201            public @NonNull Builder reverse() {
1202                mFlags ^= FLAG_REVERSE;
1203                return this;
1204            }
1205
1206            /**
1207             * Use the id specified in the configuration, creating
1208             * VolumeShaper as needed; the configuration should be
1209             * TYPE_SCALE.
1210             * @return the same {@code Builder} instance.
1211             */
1212            public @NonNull Builder createIfNeeded() {
1213                mFlags |= FLAG_CREATE_IF_NEEDED;
1214                return this;
1215            }
1216
1217            /**
1218             * Sets the operation flag.  Do not call this directly but one of the
1219             * other builder methods.
1220             *
1221             * @param flags new value for {@code flags}, consisting of ORed flags.
1222             * @return the same {@code Builder} instance.
1223             * @throws IllegalArgumentException if {@code flags} contains invalid set bits.
1224             */
1225            private @NonNull Builder setFlags(@Flag int flags) {
1226                if ((flags & ~FLAG_PUBLIC_ALL) != 0) {
1227                    throw new IllegalArgumentException("flag has unknown bits set: " + flags);
1228                }
1229                mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags;
1230                return this;
1231            }
1232
1233            /**
1234             * Builds a new {@link VolumeShaper.Operation} object.
1235             *
1236             * @return a new {@code VolumeShaper.Operation} object
1237             */
1238            public @NonNull Operation build() {
1239                return new Operation(mFlags, mReplaceId);
1240            }
1241        } // Operation.Builder
1242    } // Operation
1243
1244    /**
1245     * @hide
1246     * {@code VolumeShaper.State} represents the current progress
1247     * of the {@code VolumeShaper}.
1248     *
1249     *  Not for public use.
1250     */
1251    public static final class State implements Parcelable {
1252        private float mVolume;
1253        private float mXOffset;
1254
1255        @Override
1256        public String toString() {
1257            return "VolumeShaper.State{"
1258                    + "mVolume = " + mVolume
1259                    + ", mXOffset = " + mXOffset
1260                    + "}";
1261        }
1262
1263        @Override
1264        public int hashCode() {
1265            return Objects.hash(mVolume, mXOffset);
1266        }
1267
1268        @Override
1269        public boolean equals(Object o) {
1270            if (!(o instanceof State)) return false;
1271            if (o == this) return true;
1272            final State other = (State) o;
1273            return mVolume == other.mVolume
1274                    && mXOffset == other.mXOffset;
1275        }
1276
1277        @Override
1278        public int describeContents() {
1279            return 0;
1280        }
1281
1282        @Override
1283        public void writeToParcel(Parcel dest, int flags) {
1284            dest.writeFloat(mVolume);
1285            dest.writeFloat(mXOffset);
1286        }
1287
1288        public static final Parcelable.Creator<VolumeShaper.State> CREATOR
1289                = new Parcelable.Creator<VolumeShaper.State>() {
1290            @Override
1291            public VolumeShaper.State createFromParcel(Parcel p) {
1292                return new VolumeShaper.State(
1293                        p.readFloat()     // volume
1294                        , p.readFloat()); // xOffset
1295            }
1296
1297            @Override
1298            public VolumeShaper.State[] newArray(int size) {
1299                return new VolumeShaper.State[size];
1300            }
1301        };
1302
1303        /* package */ State(float volume, float xOffset) {
1304            mVolume = volume;
1305            mXOffset = xOffset;
1306        }
1307
1308        /**
1309         * Gets the volume of the {@link VolumeShaper.State}.
1310         */
1311        public float getVolume() {
1312            return mVolume;
1313        }
1314
1315        /**
1316         * Gets the elapsed ms of the {@link VolumeShaper.State}
1317         */
1318        public double getXOffset() {
1319            return mXOffset;
1320        }
1321    } // State
1322}
1323