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-cardAudio inside a scene starts with that scene. Audio outside the scenes spans the video.
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/.
| Sound | Cue name | Install |
|---|---|---|
| Ambient pad | bed.drift | odori add audio bed-drift |
| Repeating pulse | bed.pulse | odori add audio bed-pulse |
| Shimmering chord | bed.lift | odori add audio bed-lift |
| Opening chord | sting.open | odori add audio sting-open |
| Closing chord | sting.close | odori add audio sting-close |
| Pop | ui.pop | odori add audio pop |
| Confirmation | ui.success | odori add audio success |
| Error | ui.error | odori add audio error |
| Notification | ui.notify | odori add audio notify |
| Keyboard typing | ui.typing | odori add audio typing |
| Single keystroke | ui.key | odori 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.mainThe 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:
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:
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 calmodori 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/openerThe 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".
| Prop | Meaning |
|---|---|
src | File URL, brand cue name, or configured asset reference |
from | Offset from the enclosing scene, or from the video at the top level |
duration | Placement length; defaults to the enclosing scene or video |
gain | Linear gain, or a function of the cue's local frame; 1 is unchanged |
fadeIn / fadeOut | Fade lengths |
trimStart | Time to skip at the start of the source |
loop | Repeat the source to fill its placement |
group | Name the audio group, such as voice, music, or sfx |
duckUnder | Lower 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.