Error
Two falling notes for an error.
Customize
import {Scene, Video} from "odori";
import {Error} from "../components/error/error";
<Video>
<Scene id="error" duration="0.57s">
<Error />
</Scene>
</Video>Installation
pnpm odori add audio errornpx odori add audio errorbunx odori add audio errorEdit the source in videos/components/error/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/error/error.tsx
import {defineCue, gain, lowPass, mix, normalize, note, sequence, shape, sine, triangle, type CueDefinition} from "odori";
export type ErrorOptions = {
/** The note the sound starts from before it falls. */
root?: string;
/** Peak level before the mix normalizes the whole track. */
peak?: number;
};
/**
* Two notes falling a fourth, with a little edge under them: the inverse of
* `success`, which is how an interface says the thing did not work.
*
* Deliberately not harsh. A buzzer punishes the viewer for watching; a falling
* interval reports the failure and gets out of the way.
*/
export const error = ({root = "c5", peak = 0.55}: ErrorOptions = {}): CueDefinition => {
const samples = Math.round(48000 * 0.55);
const base = note(root);
return defineCue({
name: "ui.error",
durationInFrames: 17,
params: {root, peak},
render: () =>
normalize(
sequence(
[base, base * 2 ** (-5 / 12)].map((frequency, index) => ({
at: index,
play: (length: number) =>
mix(
shape(sine(length, frequency), {attack: 0.004, decay: 0.26, sustain: 0.05, release: 0.14}),
// A triangle under the second note gives it the edge that
// reads as "wrong" without reaching for distortion.
gain(lowPass(shape(triangle(length, frequency / 2), {attack: 0.006, decay: 0.3, sustain: 0}), 1800), 0.35),
),
gain: index === 0 ? 0.75 : 0.9,
})),
{bpm: 132, division: 16, samples},
),
peak,
),
});
};
videos/components/error/error.preview.tsx
import {CueWave, defineComponentPreview} from "odori/preview";
import {error, type ErrorOptions} from "./error";
const Wave = ({root, peak}: ErrorOptions) => <CueWave cue={error({root, peak})} />;
export default defineComponentPreview({
title: "Error",
category: "Sound/Interface",
description: "Two falling notes for an error.",
component: Wave,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
root: {type: "select", defaultValue: "c5", options: ["a4", "c5", "d5", "e5"]},
peak: {type: "number", defaultValue: 0.55, min: 0.1, max: 1, step: 0.05},
},
examples: [{name: "Default", props: {}}, {name: "Lower", props: {root: "a4"}}],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Error} from "../components/error/error";
<Video>
<Scene id="error" duration="0.57s">
<Error />
</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
- ui.error
- Recommended duration
- 17 frames · 0.57s
- Minimum duration
- 17 frames · 0.57s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- the waveform renders without a playhead
- Requires
- mono font
- Content limits
- none