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 — setSource(...), and tween playback.volume / playback.rate through setPlayback(...).
  • 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:       { src: "keyboard-sfx/mxbrown-travel/press/GENERIC_R0.mp3", duration: 0.1667, volume: 0.32 },
  backspace: { src: "keyboard-sfx/mxbrown-travel/press/BACKSPACE.mp3",  duration: 0.1667, volume: 0.32 },
  return:    { src: "keyboard-sfx/mxbrown-travel/press/ENTER.mp3",      duration: 0.1667, volume: 0.32 },
  space:     { src: "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.

src is a bare public-relative string, or an @remotion/sfx URL export; the renderer resolves it to 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.

Why sound lives at the composition

A keyboard actor that plays its own sounds can only ever be one keyboard. Emitting keyPressed and letting the composition decide means the same actor is silent in a preview, mechanical in one video and soft in another.

On this page