Skip to content

Audio

filmstrip folds every level in an edit into one gain curve per clip before a backend sees it, so all four backends land on the same gain.

A clip or a track takes an AudioLevel, and the composition takes an AudioSpec.

val composition =
compositionOf {
clip(MediaSource.of("/path/to/interview.mp4")) {
audio(AudioLevel.Volume(1.5f))
}
clip(MediaSource.of("/path/to/broll.mp4")) {
audio(AudioLevel.Mute)
}
audio(AudioSpec.Volume(0.8f))
}
  • Inherit is the default and leaves the enclosing scope alone.
  • Volume scales by a constant. 1f is unchanged and anything above it boosts.
  • Mute contributes silence without changing timing or the output’s track count.

Levels multiply down the scopes and aren’t capped. A mute at any scope silences everything below it. The composition’s AudioSpec also has Keep, Mute, Remove and AudioOnly, covered in Edits.

A negative or non-finite gain is refused with InvalidComposition at plan time.

fadeIn and fadeOut sit on a clip or a track.

val composition =
compositionOf {
clip(MediaSource.of("/path/to/clip.mp4")) {
trim(start = 5.seconds, endExclusive = 25.seconds)
audio(AudioLevel.Volume(0.5f))
fadeIn(1.seconds)
fadeOut(2.seconds)
}
}

A fade multiplies whatever audio set, so this clip rises from silence to half, not to full. Fading a mute leaves it muted. A clip’s fades are measured against its trim, so retrimming moves them too. A track’s fade in starts where the track does, and its fade out ends where the track does. Fades on different scopes multiply.

A fade that’s negative or longer than its scope is refused with InvalidComposition, and so is a pair that runs longer than the scope, since the two ramps would cross.

These fades only touch audio. To fade an overlay’s picture, see Overlays.

AudioLevel.Envelope is a gain curve you pin at points. The gain ramps linearly between neighbouring points and holds flat before the first and after the last. Nothing is smoothed, so a curve is written as more points.

val composition =
compositionOf {
clip(MediaSource.of("/path/to/clip.mp4")) {
audio(
AudioLevel.Envelope(
listOf(
EnvelopePoint(at = 4.seconds, gain = 1f),
EnvelopePoint(at = 5.seconds, gain = 0.2f),
EnvelopePoint(at = 3.seconds, gain = 0.2f, from = EnvelopeAnchor.End),
EnvelopePoint(at = Duration.ZERO, gain = 1f, from = EnvelopeAnchor.End),
),
),
)
}
}

A point anchored at EnvelopeAnchor.End is measured back from the end of its scope, and placed once the plan knows how long the scope runs. That lets you write a fade out before you know the clip’s length. A clip’s envelope reads against its trimmed length, and a track’s against its whole run.

A point outside its scope, a negative gain, or points that fall out of time order once the end-anchored ones are placed are refused with InvalidComposition.

A track added with TrackContent.Audio plays alongside the primary track and is mixed in. The default content for track carries video too, so name it, or the plan refuses it.

val composition =
compositionOf {
clip(MediaSource.of("/path/to/clip.mp4"))
track(TrackContent.Audio) {
clip(MediaSource.of("/path/to/voice-over.m4a"))
startAt(3.seconds)
}
track(TrackContent.Audio) {
clip(MediaSource.of("/path/to/music.m4a"))
audio(AudioLevel.Volume(0.3f))
looping()
fadeOut(2.seconds)
}
}
  • startAt holds a track off until that far into the composition. A negative start is refused.
  • looping repeats the track from the top until the longest non-looping track ends, and cuts the last pass where it runs past.

The composition runs as long as its longest non-looping track, offset included, so a bed longer than the picture sets the length. The picture holds the primary track’s last frame until the end.

A looping track’s scope is its whole run. Its fadeIn plays once at the start, and its fadeOut and any end-anchored point land where the composition ends. A clip’s own fade or envelope is read against the clip, so it replays on every pass.

A composition where every track loops has nothing to bound it, and is refused with InvalidComposition. So is a track laying more than 2000 passes.