Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Audio and sound effects

The audio family from @motionactor/media — Audio for long-form clips, AudioManager and AudioTrack for one-shot sound effects.

The three actors

  • Audio — one persistent clip: narration, ambience, a music bed. An ordinary media actor with a required audio source (audio.file(...), audio.url(...), or pcm(...)) — set at spawn or with setSource(...), then play({ to }) / loop({ window? }) / fastForward / rewind author its sourceTime curve; tween volume directly. loop() alone loops the selected source window, and fade: { in, out } ramps gain at the start and end of each playback, so a music bed is ctx.spawn(Audio, { content: { source }, volume: 0.6, fade: { in: 1, out: 2 } }).loop().
  • AudioTrack — a list of scheduled one-shot cues, each with an authored-seconds start.
  • AudioManager — the scene’s sound-effect entry point. Owns an AudioTrack child and routes cues into it; enabled.set(false) mutes everything it routes.

Sound effect packs

A pack is a typed map of cue id to cue definition, seconds-native. defineSoundEffects preserves the literal map type, so cue ids flow through play and bind calls as a typed union rather than as strings.

const keyboardSounds = defineSoundEffects({
  key:       { source: audio.file("keyboard-sfx/mxbrown-travel/press/GENERIC_R0.mp3"), duration: 0.1667, volume: 0.32 },
  backspace: { source: audio.file("keyboard-sfx/mxbrown-travel/press/BACKSPACE.mp3"),  duration: 0.1667, volume: 0.32 },
  return:    { source: audio.file("keyboard-sfx/mxbrown-travel/press/ENTER.mp3"),      duration: 0.1667, volume: 0.32 },
  space:     { source: audio.file("keyboard-sfx/mxbrown-travel/press/SPACE.mp3"),      duration: 0.1667, volume: 0.32 },
});

const allSounds = mergeSoundEffects(keyboardSounds, remotionSoundEffects);

remotionSoundEffects is a ready-made pack of Remotion’s stock @remotion/sfx clips.

source is an audio ref: audio.file(...) for a project file, audio.url(...) for a hosted clip such as the @remotion/sfx exports. The host’s resolver decides where a file is served (Remotion: staticFile(...)). Never pre-resolve a path in actor state — a resolved URL is a host fact, and baking it in makes the actor unusable in any other host.

Installing and playing

const audio = ctx.spawn(AudioManager);
const sounds = audio.installSoundEffects({ map: keyboardSounds });

yield* sounds.play("return");               // typed cue id
yield* sounds.play("key", { volume: 0.5 }); // per-call overrides

play schedules the cue on the manager’s track at the current timeline cursor. It is a compile-time scheduling write, which is what keeps resolution deterministic — the cue’s position is authored, not decided at playback.

Event-driven sounds

This is the pattern that matters. Actors stay sound-agnostic: they emit typed semantic events and never reference cues or media paths. Which events make noise is declared at the composition.

audio.bindSounds(keyboard, sounds, {
  keyPressed: ({ keyId }) => getKeyboardSoundCue(keyId),
});

Each binding is a static cue id, a function of the event payload returning a cue id, a { cue, ...overrides } request, or null to stay silent. Bindings are typed over the source’s event map and the pack’s cue ids, so neither side can drift.

bindSounds is thin sugar over subscribe(audio, source, handlers) plus pack.play(...). Drop to the unsugared form when a reaction needs more than a cue lookup.

Do not use it for

  • Hardwiring media paths into reusable interaction actors. Emit a semantic event and bind the sound at the composition.
  • Long-form clips through the cue system — that is what Audio is for.

Was this page helpful?