Controlling amplitude with VolumeShaper

You can use a VolumeShaper in an audio app to perform fade-ins, fade-outs, cross fades, ducking, and other short automated volume transitions. The VolumeShaper class is available in Android 8.0 (API level 26) and later.

You create a VolumeShaper by calling createVolumeShaper() on an instance of AudioTrack or MediaPlayer. The VolumeShaper only acts on the audio produced by the AudioTrack or MediaPlayer that created it.

VolumeShaper.Configuration

The behavior of a VolumeShaper is defined by its VolumeShaper.Configuration. The configuration specifies a *volume curve, interpolator type, and duration.*

Volume curve

The volume curve represents amplitude change over time. It is defined by a pair of float arrays, x[] and y[] that define a series of control points. Each (x, y) pair represents time and volume respectively. The arrays must be of equal length and contain at least 2 and no more than 16 values. (The maximum curve length is defined in getMaximumCurvePoints().)

The time coordinates are given over the interval [0.0, 1.0]. The first time point must be 0.0, the last must be 1.0, and the times must be monotonically increasing.

The volume coordinates are specified in linear scale over the interval [0.0, 1.0].

Interpolator type

The volume curve always passes through the specified control points. Values between the control points are derived by a spline according to the configuration's interpolator type. There are four constants for the available VolumeShaper interpolator types:

  • VolumeShaper.Configuration.INTERPOLATOR_TYPE_STEP
  • VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR
  • VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC
  • VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC

Duration

The specified time coordinates in the interval [0.0, 1.0] are scaled to a duration that you specify in milliseconds. This determines the actual length in time of the volume curve when the shaper is running and applying the curve to the audio output.

Using a VolumeShaper

Creating a configuration

Before building a VolumeShaper, you must create an instance of VolumeShaper.Configuration. Do this using a VolumeShaper.Configuration.Builder():

Kotlin

val config: VolumeShaper.Configuration = VolumeShaper.Configuration.Builder()
        .setDuration(3000)
        .setCurve(floatArrayOf(0f, 1f), floatArrayOf(0f, 1f))
        .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
        .build()

Java

VolumeShaper.Configuration config =
  new VolumeShaper.Configuration.Builder()
      .setDuration(3000)
      .setCurve(new float[] {0.f, 1.f}, new float[] {0.f, 1.f})
      .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
      .build();

With no arguments the VolumeShaper.Configuration.Builder constructor returns a builder that creates a configuration with default settings: INTERPOLATOR_TYPE_CUBIC, a one second duration, and no curve. You must add a curve to the builder before calling build().

The framework provides constants for configurations with pre-built curves, each with one second duration:

  • VolumeShaper.Configuration.LINEAR_RAMP
  • VolumeShaper.Configuration.CUBIC_RAMP
  • VolumeShaper.Configuration.SINE_RAMP
  • VolumeShaper.Configuration.SCURVE_RAMP

Creating a VolumeShaper

To create a VolumeShaper, call createVolumeShaper() on an instance of the appropriate class, passing in a VolumeShaper.Configuration:

Kotlin

volumeShaper = myMediaPlayer.createVolumeShaper(config)
volumeShaper = myAudioTrack.createVolumeShaper(config)

Java

volumeShaper = myMediaPlayer.createVolumeShaper(config);
volumeShaper = myAudioTrack.createVolumeShaper(config);

A single track or media player can have many shapers attached to it, and you can control each shaper separately. The outputs of all the shapers on a track or player are multiplied together. A VolumeShaper cannot be shared between AudioTracks or MediaPlayers, but you can use the same configuration in calls to createVolumeShaper to build identical shapers on multiple AudioTracks or MediaPlayers.

When you create the shaper, its first control point (at t = 0) is applied to the audio stream. If the initial volume is not 1.0 and your app is playing material at create time, your audio might have an abrupt change in volume. Best practice is to start playing audio from silence and use a VolumeShaper to implement a fade-in when playback starts. Create a VolumeShaper that starts at 0 volume and fades up. For example:

setCurve(new float[] {0.f, 1.f}, new float[] {0.f, 1.f})

Start playback and the shaper at the same time. This ensures that playback begins from silence and the volume ramps up to full volume. This is explained in the next section.

Running a VolumeShaper

Though the volume level of the first control point is applied to the audio path as soon as the shaper is created, the shaper does not progress along the curve until you call the apply() method with VolumeShaper.Operation.PLAY. After creating the shaper, the first invocation of apply() must specify the PLAY operation in order to start the shaper. This runs the curve from its first to last control points:

Kotlin

shaper.apply(VolumeShaper.Operation.PLAY)

Java

shaper.apply(VolumeShaper.Operation.PLAY);

While the shaper is running you can issue alternating apply() calls specifying REVERSE and PLAY operations. This changes the direction of readout of the control points each time.

The shaper continuously adjusts the volume and passes through all control points until it expires. This happens when the shaper reaches the last (for PLAY operation) or first (for REVERSE operation) control point in the curve.

When the shaper expires, the volume remains at the last setting, which may be the first or the last control point. You can call VolumeShaper.getVolume() for the current volume level at any time.

After the shaper expires, you can issue another apply() call to run the curve in the opposite direction. For example, if the shaper expired while running PLAY, the next apply() must be REVERSE. Calling PLAY after PLAY has expired, or REVERSE after REVERSE has expired has no effect.

You must alternate PLAY and REVERSE operations. There is no way to play a curve from its first to last control points and then restart it again from the first control point. You can use the replace() method, described in the next section, to replace the curve with a copy of itself. This resets the shaper, requiring the PLAY operation to start it again.

Changing the curve

Use the replace() method to change a VolumeShaper's curve. This method takes a configuration, an operation, and a join parameter. You can call the replace() method at any time, while the shaper is running or after it expires:

Kotlin

val newConfig = VolumeShaper.Configuration.Builder()
        .setDuration(1000)
        .setCurve(floatArrayOf(0f, 0.5f), floatArrayOf(0f, 1f))
        .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
        .build()
val join = true
shaper.replace(newConfig, VolumeShaper.Operation.PLAY, join)

Java

VolumeShaper.Configuration newConfig =
  new VolumeShaper.Configuration.Builder()
    .setDuration(1000)
    .setCurve(new float[] {0.f, 0.5f}, new float[] {0.f, 1.f})
    .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
    .build();
boolean join = true;
shaper.replace(newConfig, VolumeShaper.Operation.PLAY, join);

When you call replace() while the shaper is running, it stops changing the volume and remains at its current value. Then the shaper tries to start the new curve from the first control point. This means that the operation argument controls whether or not the shaper runs after the call. Specify PLAY to immediately start the new curve, specify REVERSE to leave the shaper paused at the volume of the first control point in the new curve. You can start the shaper later with apply(VolumeShaper.Operation.PLAY).

When you call replace() with join = false, the shaper starts its curve at the level specified by its first control point. This can cause a discontinuity in the volume. You can avoid this by calling replace() with join = true. This sets the first control point of the new curve to the current level of the shaper and scales the volume of all the control points between the first and last to maintain the relative shape of the new curve (the last control point is unchanged). The scaling operation permanently changes the control points in the shaper's new curve.

Removing a VolumeShaper

The system closes and garbage collects a VolumeShaper when its AudioTrack or MediaPlayer is released or no longer in use. You can call the close() method on a shaper to destroy it immediately. The system removes the shaper from the audio pipeline within about 20 ms. Be careful when closing a VolumeShaper while audio is playing. If the shaper has a volume less than 1.0 when you call close(), the shaper's volume scale changes to 1.0. This can cause a sudden increase in volume.