Audio

Use <Audio> to place sound in a scene or across a video. Preview sounds in the audio library, then install them in your project.

Add a sound

In an existing Odori project, install the sounds and components used below. The command registers both audio cue names in your brand.

pnpm odori add audio bed-pulse success
pnpm odori add component title-reveal end-card

Audio inside a scene starts with that scene. Audio outside the scenes spans the video.

videos/launch/video.tsx
import {Audio, Scene, Video, defineVideoMetadata} from "odori";
import {TitleReveal} from "../components/title-reveal/title-reveal";
import {EndCard} from "../components/end-card/end-card";
import {productLayout} from "../layout";

export const metadata = defineVideoMetadata({layout: productLayout});

export default function LaunchVideo() {
  return (
    <Video>
      <Audio src="bed.pulse" gain={0.7} fadeOut="1s" duckUnder="foreground" />
      <Scene id="opening" duration="4s">
        <TitleReveal title="Introducing our new editor" />
      </Scene>
      <Scene id="end" duration="3s">
        <Audio group="foreground" src="ui.success" from="0.2s" duration="15f" />
        <EndCard title="Available today" />
      </Scene>
    </Video>
  );
}

duckUnder lowers the music while the confirmation sound plays.

Choose audio

The library includes synthesized sounds, music files, and keyboard recordings. odori add copies synthesized sounds as editable source. For files, it verifies the download and saves it under public/audio/.

SoundCue nameInstall
Ambient padbed.driftodori add audio bed-drift
Repeating pulsebed.pulseodori add audio bed-pulse
Shimmering chordbed.liftodori add audio bed-lift
Opening chordsting.openodori add audio sting-open
Closing chordsting.closeodori add audio sting-close
Popui.popodori add audio pop
Confirmationui.successodori add audio success
Errorui.errorodori add audio error
Notificationui.notifyodori add audio notify
Keyboard typingui.typingodori add audio typing
Single keystrokeui.keyodori add audio ui-key

Music tracks include bed-horizons, bed-tomorrow, bed-innovation, bed-launch, and bed-velocity. Each registers a matching cue, such as bed.horizons. Listen to them on the audio page.

Import your own track

Prepare a local file and register it as bed.main:

odori audio import ./track.wav --name bed.main

The command copies the original file into public/audio/ without changing its level, and registers the name in your brand. To normalize explicitly, add --loudness -20. To generate music or narration, configure ElevenLabs.

You can also use a file directly. Store it under public/ and reference its URL:

<Audio group="foreground" src="/audio/track.m4a" />

Studio lists files from audioDir, which defaults to public/audio. To name a file in your configuration, add an asset reference:

odori.config.ts
import {defineConfig} from "@odori/cli";

export default defineConfig({
  audioDir: "public/audio",
  assets: [{reference: "score", url: "/audio/track.m4a"}],
});

You can then use <Audio group="foreground" src="score" />. An unknown reference throws an error.

Loop, trim, and fade

Synthesized music cues declare whether they loop. For a music file, set loop to repeat it through the scene:

<Audio src="bed.main" loop gain={0.7} duckUnder="foreground" />

Use trimStart to skip the beginning of a track, and fades to adjust its entrance and exit:

<Audio
  src="bed.main"
  trimStart="6s"
  fadeIn="0.5s"
  fadeOut="1s"
  gain={0.65}
  duckUnder="foreground"
/>

odori test reports synthesized cues whose placement exceeds their phrase length without looping.

Share audio through a brand

Map cue names to files in your brand to change audio across videos that use it:

videos/layout.tsx
import {defineBrand} from "odori";

export const productBrand = defineBrand({
  name: "product",
  audio: {
    cues: {"bed.main": "/audio/track.m4a"},
    targetLufs: -14,
  },
});

Reference that cue with <Audio group="foreground" src="bed.main" />. Odori resolves src as a path, a brand cue name, or an asset reference, in that order.

Compare audio variants

Define audio variants to try different tracks without copying the video. Each variant replaces cue names already used by the scenes:

import {defineVideoMetadata} from "odori";

export const metadata = defineVideoMetadata({
  id: "launch",
  audio: {
    variants: {
      calm: {"bed.main": "/audio/calm.m4a"},
      upbeat: {"bed.main": "/audio/upbeat.m4a"},
    },
  },
});

Variants accept file paths or synthesized cues. Select one in Studio's Audio inspector, or export it by name:

odori export launch --audio-variant calm

odori test checks the default audio. Export validates the selected variant's files and fails if one is missing.

Generate narration and captions

After setting up ElevenLabs, generate narration with word timings:

odori audio generate --type speech "Introducing our new editor." --output public/audio/opener

The command writes the audio and opener.narration.json, then registers the voice.narration cue. Use --voice to select a voice and --name to set a different cue name.

Install the captions component with odori add component captions. Use the saved word timings to generate caption cues at your video's frame rate:

import {Audio, captionCues, useVideo} from "odori";
import {Captions} from "../components/captions/captions";
import narration from "../../public/audio/opener.narration.json";

export function Narrated() {
  const {fps} = useVideo();
  return (
    <>
      <Audio group="foreground" src="voice.narration" />
      <Captions cues={captionCues(narration, fps)} />
    </>
  );
}

Regenerating the narration updates its timing file. Use narrationEndSeconds to calculate the duration needed for the spoken line. Preview and export read the saved files without calling the provider.

Audio props

Time values accept seconds as numbers, or strings such as "0.5s" and "15f".

PropMeaning
srcFile URL, brand cue name, or configured asset reference
fromOffset from the enclosing scene, or from the video at the top level
durationPlacement length; defaults to the enclosing scene or video
gainLinear gain, or a function of the cue's local frame; 1 is unchanged
fadeIn / fadeOutFade lengths
trimStartTime to skip at the start of the source
loopRepeat the source to fill its placement
groupName the audio group, such as voice, music, or sfx
duckUnderLower the gain while cues in the named group or groups overlap

Preview audio

In Studio, press play to hear the mix. Hover the timeline to preview a frame silently; click to move the playhead. Click an audio cue to solo it, then click again to hear the full mix. Press m to mute.

If the browser blocks playback, click Enable sound. The workspace setting for audio on open applies to the next video you open.

Use the Assets view to listen to individual files. Run odori inspect launch to list the video's audio placements.

Export audio

The encoder trims, fades, and mixes audio, then normalizes it to the brand's targetLufs. Studio's Audio panel shows measured loudness beside that target.

Ducking reduces a cue's gain to 0.35 while a cue in a targeted group overlaps it, with six-frame ramps. Preview and export use the same gain envelope. Browser playback follows the frame clock; the exported mix uses the exact frame positions.

:::note Export requires project-local audio. Remote URLs can play in Studio, but the encoder needs files on disk. Unresolved sources are skipped with a warning. :::

Duck under narration

Target the groups that should lower the music. Other sounds do not affect it.

<Audio src="launch-music" group="music" loop duckUnder="voice" />
<Audio src="intro" group="voice" />
<Audio src="typing" group="sfx" />

Use duckUnder={["voice", "alerts"]} to target multiple groups. Ducking follows their timeline windows, including silent portions of a clip. The same envelope drives preview and export.

On this page