Opening chord
A swell and chord for an opening title.
Customize
import {Scene, Video} from "odori";
import {StingOpen} from "../components/sting-open/sting-open";
<Video>
<Scene id="sting-open" duration="2s">
<StingOpen />
</Scene>
</Video>Installation
pnpm odori add audio sting-opennpx odori add audio sting-openbunx odori add audio sting-openEdit the source in videos/components/sting-open/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/sting-open/sting-open.tsx
import {
chord,
defineCue,
gain,
highPass,
mix,
noise,
normalize,
note,
sequence,
shape,
sine,
type CueDefinition,
} from "odori";
export type StingOpenOptions = {
/** Root of the arrival chord, as a note name. */
root?: string;
/** Peak level before the mix normalizes the whole track. */
peak?: number;
};
/**
* The opening bookend: a breath of air, then a chord arriving under it. Two
* seconds, so a title has something to land on without the sound outstaying
* the first shot.
*
* The notes enter as an arpeggio a few frames apart rather than together,
* which is the difference between a chord and a stab.
*/
export const stingOpen = ({root = "a3", peak = 0.85}: StingOpenOptions = {}): CueDefinition => {
const samples = 48000 * 2;
const voices = chord(root, "add9");
return defineCue({
name: "sting.open",
durationInFrames: 60,
params: {root, peak},
render: () =>
normalize(
mix(
// Air: filtered noise opening the moment.
gain(shape(highPass(noise(samples, 21), 3000), {attack: 0.18, decay: 0.5, sustain: 0.05}), 0.25),
// The chord, one voice every sixteenth.
sequence(
voices.map((frequency, index) => ({
at: index,
play: (length: number) =>
shape(sine(length, frequency), {attack: 0.008, decay: 0.9, sustain: 0.15, release: 0.4}),
gain: 0.7 - index * 0.08,
})),
{bpm: 120, division: 16, samples},
),
// A sub under the root gives the arrival a floor.
gain(shape(sine(samples, note(root) / 2), {attack: 0.02, decay: 1.1, sustain: 0}), 0.4),
),
peak,
),
});
};
videos/components/sting-open/sting-open.preview.tsx
import {CueWave, defineComponentPreview} from "odori/preview";
import {stingOpen, type StingOpenOptions} from "./sting-open";
const Wave = ({root, peak}: StingOpenOptions) => <CueWave cue={stingOpen({root, peak})} />;
export default defineComponentPreview({
title: "Opening chord",
category: "Sound/Stings",
description: "A swell and chord for an opening title.",
component: Wave,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
root: {type: "select", defaultValue: "a3", options: ["f3", "g3", "a3", "c4"]},
peak: {type: "number", defaultValue: 0.85, min: 0.1, max: 1, step: 0.05},
},
examples: [{name: "Default", props: {}}, {name: "Higher", props: {root: "c4"}}],
});
Add it to a scene
import {Scene, Video} from "odori";
import {StingOpen} from "../components/sting-open/sting-open";
<Video>
<Scene id="sting-open" duration="2s">
<StingOpen />
</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
- sting.open
- Recommended duration
- 60 frames · 2s
- Minimum duration
- 60 frames · 2s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none