Drift
Soft ambient pad for narration.
Customize
import {Scene, Video} from "odori";
import {BedDrift} from "../components/bed-drift/bed-drift";
<Video>
<Scene id="bed-drift" duration="16s">
<BedDrift />
</Scene>
</Video>Installation
pnpm odori add audio bed-driftnpx odori add audio bed-driftbunx odori add audio bed-driftEdit the source in videos/components/bed-drift/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/bed-drift/bed-drift.tsx
import {chord, defineCue, gain, lowPass, mix, noise, normalize, note, pad, sequence, sine, type CueDefinition} from "odori";
export type BedDriftOptions = {
/** Root of the home chord, as a note name. */
root?: string;
/** Peak level before the mix normalizes the whole track. */
peak?: number;
};
/**
* Sixteen seconds of slow breathing: the home chord swells and lets go, and a
* warmer chord answers it, overlapping so the seam between them is a change of
* colour rather than an event. Nothing in it marks time — no pulse, no beat —
* which is the bed for a cut that narration is already pacing.
*
* Everything sits below the voice: the pads are low passed under a kilohertz,
* the drone is an octave under the root, and the air is closer to room tone
* than to texture.
*/
export const bedDrift = ({root = "a2", peak = 0.5}: BedDriftOptions = {}): CueDefinition => {
// One breath is four seconds. Two breaths each way is the phrase.
const breath = 4 * 48000;
const samples = breath * 4;
// The answering chord, a major third below: the relative major seventh,
// which agrees with the home chord about every note but one.
const answer = note(root) * 2 ** (-4 / 12);
return defineCue({
name: "bed.drift",
durationInFrames: Math.round((samples / 48000) * 30),
loops: true,
params: {root, peak},
render: () =>
normalize(
mix(
// The two swells, placed so one rises while the other releases.
sequence(
[
{
at: 0,
length: 2.5,
play: (length: number) =>
lowPass(pad(chord(root, "minor7"), length, {envelope: {attack: 3.5, sustain: 1, release: 4}}), 850),
gain: 0.85,
},
{
at: 1.5,
length: 2.5,
play: (length: number) =>
lowPass(pad(chord(answer, "major7"), length, {envelope: {attack: 3.5, sustain: 1, release: 4}}), 850),
gain: 0.7,
},
],
{bpm: 60, division: 1, samples},
),
// A drone an octave down holds through both, which is what makes the
// chord change read as weather rather than as a cut.
gain(sine(samples, note(root) / 2), 0.16),
// Room tone. Just enough that silence between swells is not digital.
gain(lowPass(noise(samples, 7), 350), 0.05),
),
peak,
),
});
};
videos/components/bed-drift/bed-drift.preview.tsx
import {CueWave, defineComponentPreview} from "odori/preview";
import {bedDrift, type BedDriftOptions} from "./bed-drift";
const Wave = ({root, peak}: BedDriftOptions) => <CueWave cue={bedDrift({root, peak})} />;
export default defineComponentPreview({
title: "Drift",
category: "Sound/Beds",
description: "Soft ambient pad for narration.",
component: Wave,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
root: {type: "select", defaultValue: "a2", options: ["f2", "g2", "a2", "c3"]},
peak: {type: "number", defaultValue: 0.5, min: 0.1, max: 1, step: 0.05},
},
examples: [{name: "Default", props: {}}, {name: "Lower", props: {root: "f2"}}],
});
Add it to a scene
import {Scene, Video} from "odori";
import {BedDrift} from "../components/bed-drift/bed-drift";
<Video>
<Scene id="bed-drift" duration="16s">
<BedDrift />
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Sound
- Brand cue
- bed.drift
- Recommended duration
- 480 frames · 16s
- Minimum duration
- 30 frames · 1s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none