---
title: Audio and sound effects
description: Audio plays one clip along the timeline. AudioManager schedules short cues, by name or in answer to another actor's events.
---

`@motionactor/media` has two ways to make sound. `Audio` is one clip that plays along the
timeline: a voice-over, a music bed, room tone. `AudioManager` schedules short cues, such as
a click per keystroke, from a named pack.

## A clip

`Audio` plays its source through `sourceTime`, the seconds into the clip at each frame, like
[`Video`](/docs/rendering/media-assets-and-fonts#video-and-audio). A looped bed is one line:

```ts twoslash
import { createScene } from "@motionactor/core";
import { Audio } from "@motionactor/media";
import { audio } from "@motionactor/schema";

createScene(function* (ctx) {
  ctx.spawn(Audio, {
    content: { source: audio.file("music/bed.mp3") },
    volume: 0.6,
    fade: { in: 1, out: 2 },
  }).loop();

  yield* ctx.wait(12);
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

| Member | |
| --- | --- |
| `content.source` | The clip, an `audio` reference. `setSource(ref)` swaps it |
| `timing.start`, `timing.end` | The window of the clip to use, in seconds. `end: -1` is the clip's end |
| `volume`, `muted` | Gain, and a switch. Both are signals, so `volume.tween(0, 2)` fades out |
| `fade.in`, `fade.out` | Seconds of ramp at the start and end of each play |
| `play({ from?, to?, rate? })` | Plays through the window. Takes that long |
| `loop({ window?, rate? })` | Loops the window from here on. Takes no time |
| `hold(seconds)`, `seekTo(time)` | Stays put, or jumps |
| `fastForward`, `rewind`, `rampTo` | Scrubs at a chosen speed, or eases between rates |

## Cues

A pack is a map from cue name to clip. `defineSoundEffects` keeps the names as a type, so a
misspelt cue does not compile.

```ts twoslash title="sounds.ts"
import { defineSoundEffects } from "@motionactor/media";
import { audio } from "@motionactor/schema";

export const keys = defineSoundEffects({
  key: {
    source: audio.file("sfx/key.mp3"),
    duration: 0.15,
    volume: 0.3,
  },
  enter: { source: audio.file("sfx/enter.mp3"), volume: 0.5 },
});
```

A cue may set `duration` in seconds (the whole clip when omitted), `volume`, `playbackRate`
and `loop`.

Spawn an `AudioManager`, install the pack and play cues by name. `play` schedules the cue at
the cursor and takes no time, so the script carries on while it sounds:

```ts twoslash
// @filename: sounds.ts
import { defineSoundEffects } from "@motionactor/media";
import { audio } from "@motionactor/schema";
export const keys = defineSoundEffects({
  key: { source: audio.file("sfx/key.mp3"), duration: 0.15, volume: 0.3 },
  enter: { source: audio.file("sfx/enter.mp3"), volume: 0.5 },
});
// @filename: scene.ts
// ---cut---
import { createScene } from "@motionactor/core";
import { AudioManager } from "@motionactor/media";
import { keys } from "./sounds";

createScene(function* (ctx) {
  const sound = ctx.spawn(AudioManager);
  const pack = sound.installSoundEffects({ map: keys });

  yield* pack.play("key");
  yield* ctx.wait(0.4);
  yield* pack.play("enter", { volume: 1 });
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

The second argument overrides the pack's values for that one cue.

`mergeSoundEffects(a, b)` joins packs. `@motionactor/remotion` exports
`remotionSoundEffects`, a pack of Remotion's stock effects: `whoosh`, `whip`, `pageTurn`,
`uiSwitch`, `mouseClick`, `shutterModern`, `shutterOld`, `ding`, `bruh`, `vineBoom` and
`windowsXpError`.

## Sounds from events

A keyboard actor that plays its own clicks is stuck with them. Have it
[emit an event](/docs/reactivity/events-and-subscribe) instead, and let the scene choose the sound:

```ts twoslash
// @filename: sounds.ts
import { defineSoundEffects } from "@motionactor/media";
import { audio } from "@motionactor/schema";
export const keys = defineSoundEffects({
  key: { source: audio.file("sfx/key.mp3"), duration: 0.15, volume: 0.3 },
  enter: { source: audio.file("sfx/enter.mp3"), volume: 0.5 },
});
// @filename: scene.ts
// ---cut---
import { Actor, createScene, signal } from "@motionactor/core";
import { AudioManager } from "@motionactor/media";
import { keys } from "./sounds";

class Keyboard extends Actor<{ pressed: { key: string } }> {
  text = signal("");

  *type(word: string) {
    for (const key of word) {
      this.text.set(this.text() + key);
      yield* this.emit.pressed({ key });
      yield* this.runtime.wait(0.1);
    }
    yield* this.emit.pressed({ key: "Enter" });
  }
}

createScene(function* (ctx) {
  const sound = ctx.spawn(AudioManager);
  const pack = sound.installSoundEffects({ map: keys });
  const keyboard = ctx.spawn(Keyboard);

  sound.bindSounds(keyboard, pack, {
    pressed: ({ key }) => (key === "Enter" ? "enter" : "key"),
  });

  yield* keyboard.type("hi"); // key at 0s, key at 0.1s, enter at 0.2s
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

A binding is a cue name, or a function of the event's payload that returns a cue name,
`{ cue, ...overrides }`, or `null` for silence. Both the event names and the cue names are
checked against their types. `bindSounds` is a [`subscribe`](/docs/reactivity/events-and-subscribe)
underneath, so use `subscribe` directly when an event should do more than play a cue.

## Muting

`sound.enabled.set(false)` stops the manager scheduling cues from that point of the script.
The check happens when `play` runs or when the event fires, so cues already scheduled still
sound and a key pressed while it is off stays silent after it is turned back on.

## Where it plays

Previews play `Audio` clips and scheduled cues alongside the picture, through the host's media
player. Exported video files have no sound yet. See
[Capture and export](/docs/rendering/capture-and-export).
