odori 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allen Zhou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
1
  # odori
2
2
 
3
- Name reserved for an in-development code-first video framework.
3
+ An independent React video framework. Odori owns the deterministic frame
4
+ runtime, timeline compiler, browser player, and render manifest, plus the
5
+ conventions a production video project otherwise has to invent.
6
+
7
+ ```bash
8
+ npm install odori
9
+ ```
10
+
11
+ ```tsx
12
+ import {Scene, Video, defineVideoMetadata} from "odori";
13
+
14
+ export const metadata = defineVideoMetadata({
15
+ id: "launch",
16
+ title: "Product launch",
17
+ duration: "8s",
18
+ });
19
+
20
+ export default function LaunchVideo() {
21
+ return (
22
+ <Video>
23
+ <Scene id="opening" duration="5s">
24
+ <TitleReveal title="Video projects deserve a framework." />
25
+ </Scene>
26
+ <Scene id="end" duration="3s">
27
+ <EndCard title="Built with odori" />
28
+ </Scene>
29
+ </Video>
30
+ );
31
+ }
32
+ ```
33
+
34
+ The CLI and Studio live in `@odori/cli`, and the component registry in
35
+ `@odori/registry`.
@@ -0,0 +1,253 @@
1
+ type Duration = number | `${number}s` | `${number}ms` | `${number}f` | string;
2
+ /**
3
+ * Convert an authoring duration into whole frames.
4
+ *
5
+ * Numbers are seconds, matching the JSX authoring model where `duration={4}`
6
+ * reads as four seconds. Suffixed strings are explicit.
7
+ */
8
+ declare const framesFromDuration: (duration: Duration, fps: number) => number;
9
+ declare const secondsFromFrames: (frames: number, fps: number) => number;
10
+ declare const formatTimecode: (frames: number, fps: number) => string;
11
+
12
+ /**
13
+ * Deterministic synthesis. A cue is code that produces samples, so a sound
14
+ * diffs, reviews, and upgrades like every other piece of source, and preview
15
+ * and export can run the identical function.
16
+ *
17
+ * The rules are the frame runtime's rules: no wall clock, no unseeded
18
+ * randomness, one sample rate. Same score in, same bytes out, on any machine.
19
+ */
20
+ /** Every cue renders at this rate, so a score never depends on the host. */
21
+ declare const SAMPLE_RATE = 48000;
22
+ type Signal = {
23
+ /** One array per channel. Mono is one; the mix upmixes when it needs to. */
24
+ channels: Float32Array[];
25
+ sampleRate: number;
26
+ };
27
+ type Envelope = {
28
+ /** Seconds from the start of the cue to full level. */
29
+ attack?: number;
30
+ /** Seconds from full level to the sustain level. */
31
+ decay?: number;
32
+ /** Level held after the decay, 0 to 1. */
33
+ sustain?: number;
34
+ /** Seconds to fall from the sustain level to silence. */
35
+ release?: number;
36
+ };
37
+ /**
38
+ * A seeded generator, so noise is reproducible. Mulberry32: small, fast, and
39
+ * good enough for audio noise, where the requirement is "no audible pattern"
40
+ * rather than cryptographic quality.
41
+ */
42
+ declare const seeded: (seed: number) => (() => number);
43
+ declare const silence: (samples: number, channels?: number) => Signal;
44
+ /** A sine partial. The building block for tones, ticks, and sub weight. */
45
+ declare const sine: (samples: number, frequency: number, phase?: number) => Signal;
46
+ /** A triangle partial: softer than a square, brighter than a sine. */
47
+ declare const triangle: (samples: number, frequency: number) => Signal;
48
+ /** White noise from a seeded generator, so a click is the same click twice. */
49
+ declare const noise: (samples: number, seed?: number) => Signal;
50
+ /**
51
+ * A frequency sweep, which is what a riser is. Linear in frequency rather than
52
+ * in pitch, because the shape is easier to reason about when writing one.
53
+ */
54
+ declare const sweep: (samples: number, from: number, to: number) => Signal;
55
+ /** Apply an ADSR shape. Anything omitted is skipped rather than defaulted. */
56
+ declare const shape: (signal: Signal, envelope: Envelope) => Signal;
57
+ declare const gain: (signal: Signal, amount: number) => Signal;
58
+ /** Sum signals, padding to the longest. Levels are the caller's business. */
59
+ declare const mix: (...signals: Signal[]) => Signal;
60
+ /**
61
+ * One pole filters. Enough to shape a click or take the top off a pad, and
62
+ * cheap enough to stay readable next to the sound it is making.
63
+ */
64
+ declare const lowPass: (signal: Signal, frequency: number) => Signal;
65
+ declare const highPass: (signal: Signal, frequency: number) => Signal;
66
+ /**
67
+ * Join a signal's tail to its own head, so repeating it is inaudible.
68
+ *
69
+ * `declick` is wrong for a loop: ramping both edges to zero puts a hole at
70
+ * every seam, once per bar for the length of the video. Instead the tail is
71
+ * crossfaded over the head, which is the same trick a sampler uses — the last
72
+ * few milliseconds fade out while the copy of them under the first few
73
+ * milliseconds fades in, so the two ends already agree when they meet.
74
+ *
75
+ * The crossfade eats `milliseconds` from the end, which is why a looping score
76
+ * should be written a hair long rather than exactly one bar.
77
+ */
78
+ declare const seamless: (signal: Signal, milliseconds?: number) => Signal;
79
+ /** Peak normalize, so a cue lands predictably before the mix's loudness pass. */
80
+ declare const normalize: (signal: Signal, peak?: number) => Signal;
81
+ /** Repeat a signal until it fills `samples`, for beds under a long scene. */
82
+ declare const loop: (signal: Signal, samples: number) => Signal;
83
+ /** Seconds to samples at the fixed rate, so scores read in musical time. */
84
+ declare const seconds: (value: number) => number;
85
+ /**
86
+ * Scientific pitch to frequency: `a4` is 440 Hz, `c#3` and `db3` are the same
87
+ * key. A score names notes because "a2" survives transposition and reading,
88
+ * and 110 does neither.
89
+ */
90
+ declare const note: (name: string) => number;
91
+ /** Samples in one step of a grid, so a score can size its own voices. */
92
+ declare const step: (bpm: number, division?: number) => number;
93
+ type Step = {
94
+ /** Grid position, in steps from the start. Fractions are allowed. */
95
+ at: number;
96
+ /** The voice to place there, built at whatever length it needs. */
97
+ play: (samples: number) => Signal;
98
+ /** Length in steps. Defaults to one step. */
99
+ length?: number;
100
+ /** Linear gain for this hit. */
101
+ gain?: number;
102
+ };
103
+ /**
104
+ * Place voices on a tempo grid.
105
+ *
106
+ * A step's `play` receives the samples its length works out to, so one voice
107
+ * definition serves a sixteenth and a whole bar. Swing delays every odd step
108
+ * by a fraction of a step, which is the difference between a drum machine and
109
+ * a groove; it is off by default because a bed under narration should not
110
+ * shuffle unless it was asked to.
111
+ */
112
+ declare const sequence: (steps: Step[], options: {
113
+ bpm: number;
114
+ division?: number;
115
+ swing?: number;
116
+ samples?: number;
117
+ }) => Signal;
118
+ /** Intervals from the root, in semitones. Enough colour for product scoring. */
119
+ declare const QUALITIES: {
120
+ readonly major: readonly [0, 4, 7];
121
+ readonly minor: readonly [0, 3, 7];
122
+ readonly sus2: readonly [0, 2, 7];
123
+ readonly sus4: readonly [0, 5, 7];
124
+ readonly major7: readonly [0, 4, 7, 11];
125
+ readonly minor7: readonly [0, 3, 7, 10];
126
+ readonly add9: readonly [0, 4, 7, 14];
127
+ };
128
+ type ChordQuality = keyof typeof QUALITIES;
129
+ /**
130
+ * The frequencies of a chord, root first. Voicing is left to the caller: what
131
+ * makes a pad sound like a pad is which of these you hand to which oscillator,
132
+ * and at what level.
133
+ */
134
+ declare const chord: (root: string | number, quality?: ChordQuality) => number[];
135
+ /**
136
+ * Sustained tones under one envelope: the sound of a bed that is holding
137
+ * rather than playing. Each voice is detuned by a few cents against the last
138
+ * so the stack beats slowly instead of standing still, which is what separates
139
+ * a pad from a test tone.
140
+ */
141
+ declare const pad: (frequencies: number[], samples: number, options?: {
142
+ envelope?: Envelope;
143
+ detuneCents?: number;
144
+ level?: number;
145
+ }) => Signal;
146
+
147
+ type CueContext = {
148
+ /** How many samples the cue must fill. */
149
+ samples: number;
150
+ sampleRate: number;
151
+ };
152
+ type CueDefinition = {
153
+ readonly kind: "odori-cue";
154
+ /** The name a brand registers and a video asks for. */
155
+ name: string;
156
+ /** Length at 30fps, which is how a contract states it. */
157
+ durationInFrames: number;
158
+ /**
159
+ * Whether this score is one repeatable phrase rather than a single event.
160
+ *
161
+ * A looping cue renders its phrase once, at `durationInFrames`, and the
162
+ * placement repeats it for as long as the window runs: one render, cached by
163
+ * content, however long the scene. It also changes how the edges are
164
+ * treated — see `defineCue` — and makes `<Audio>` loop by default, so a bed
165
+ * fills its scene without the author restating what the cue already knows.
166
+ */
167
+ loops: boolean;
168
+ /**
169
+ * Anything the score reads. Held separately from the function so the
170
+ * manifest can hash what a cue depends on: two cues that differ only in a
171
+ * parameter are two different sounds and must not share a cached render.
172
+ */
173
+ params: Record<string, number | string | boolean>;
174
+ render(context: CueContext): Signal;
175
+ };
176
+ type CueInput = Omit<CueDefinition, "kind" | "loops" | "params"> & {
177
+ loops?: boolean;
178
+ params?: Record<string, number | string | boolean>;
179
+ };
180
+ /**
181
+ * A sound as source. The function runs unchanged in the browser for preview and
182
+ * in Node for export, so the cue a reviewer approves is the cue that ships.
183
+ */
184
+ declare const defineCue: (input: CueInput) => CueDefinition;
185
+ declare const isCueDefinition: (value: unknown) => value is CueDefinition;
186
+ /**
187
+ * Identity of a rendered cue. The name alone is not enough, because a project
188
+ * can redefine what a name means; the params and the duration are what change
189
+ * the samples, so they are what the cache and the manifest key on.
190
+ */
191
+ declare const cueSignature: (cue: CueDefinition) => string;
192
+ /** The samples a cue occupies at the given frame rate. */
193
+ declare const cueSamples: (cue: CueDefinition, fps: number) => number;
194
+ /**
195
+ * A generated cue resolves to a URL that names its own content, so the render
196
+ * worker can serve it, the mix can find it on disk, and an unchanged cue is
197
+ * never rendered twice.
198
+ */
199
+ declare const cueUrl: (cue: CueDefinition) => string;
200
+
201
+ type BrandColors = {
202
+ background: string;
203
+ surface: string;
204
+ foreground: string;
205
+ muted: string;
206
+ accent: string;
207
+ border: string;
208
+ };
209
+ type BrandTypography = {
210
+ sans: string;
211
+ mono: string;
212
+ display?: string;
213
+ };
214
+ type BrandFont = {
215
+ family: string;
216
+ url: string;
217
+ weight?: string;
218
+ integrity?: string;
219
+ };
220
+ type Brand = {
221
+ readonly kind: "odori-brand";
222
+ name: string;
223
+ colors: BrandColors;
224
+ typography: BrandTypography;
225
+ fonts: BrandFont[];
226
+ logos: Record<string, string>;
227
+ motion: {
228
+ standard: [number, number, number, number];
229
+ staggerFrames: number;
230
+ };
231
+ audio: {
232
+ /**
233
+ * Symbolic cue names, so a video says what a sound means and the brand
234
+ * decides which file that is. Values are asset references or URLs.
235
+ */
236
+ cues: Record<string, string | CueDefinition>;
237
+ targetLufs: number;
238
+ };
239
+ };
240
+ type BrandInput = {
241
+ name?: string;
242
+ colors?: Partial<BrandColors>;
243
+ typography?: Partial<BrandTypography>;
244
+ fonts?: BrandFont[];
245
+ logos?: Record<string, string>;
246
+ motion?: Partial<Brand["motion"]>;
247
+ audio?: Partial<Brand["audio"]>;
248
+ };
249
+ declare const defaultBrand: Brand;
250
+ declare const defineBrand: (input: BrandInput) => Brand;
251
+ declare const brandCssVariables: (brand: Brand) => Record<string, string>;
252
+
253
+ export { note as A, type Brand as B, type ChordQuality as C, type Duration as D, type Envelope as E, pad as F, seamless as G, seconds as H, secondsFromFrames as I, seeded as J, sequence as K, shape as L, silence as M, sine as N, step as O, sweep as P, triangle as Q, type Signal as S, type BrandColors as a, type BrandFont as b, type BrandInput as c, type BrandTypography as d, type CueContext as e, type CueDefinition as f, SAMPLE_RATE as g, type Step as h, brandCssVariables as i, chord as j, cueSamples as k, cueSignature as l, cueUrl as m, defaultBrand as n, defineBrand as o, defineCue as p, formatTimecode as q, framesFromDuration as r, gain as s, highPass as t, isCueDefinition as u, loop as v, lowPass as w, mix as x, noise as y, normalize as z };