@hyperframes/core 0.8.159 → 0.8.161

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.
Files changed (63) hide show
  1. package/dist/adapters/gsap.d.ts +1 -1
  2. package/dist/adapters/index.d.ts +3 -3
  3. package/dist/audioAnalysis.d.ts +121 -0
  4. package/dist/audioAnalysis.js +466 -0
  5. package/dist/audioAnalysisLadder.d.ts +52 -0
  6. package/dist/audioAnalysisLadder.js +78 -0
  7. package/dist/audioCarve.js +1 -36
  8. package/dist/audioResample.d.ts +2 -0
  9. package/dist/audioResample.js +37 -0
  10. package/dist/audioRhythm.d.ts +48 -0
  11. package/dist/audioRhythm.js +407 -0
  12. package/dist/audioSections.d.ts +21 -0
  13. package/dist/audioSections.js +117 -0
  14. package/dist/audioSnap.d.ts +18 -0
  15. package/dist/audioSnap.js +41 -0
  16. package/dist/beats/beatDetection.d.ts +6 -8
  17. package/dist/beats/beatDetection.js +25 -217
  18. package/dist/colorGrading.d.ts +3 -3
  19. package/dist/compiler/htmlBundler.d.ts +2 -2
  20. package/dist/compiler/index.d.ts +19 -19
  21. package/dist/compiler/inlineSubCompositions.d.ts +4 -4
  22. package/dist/compiler/svgIdNamespacing.d.ts +1 -1
  23. package/dist/fft.d.ts +2 -0
  24. package/dist/fft.js +36 -0
  25. package/dist/figma/assetSnippet.d.ts +1 -1
  26. package/dist/figma/client.d.ts +1 -1
  27. package/dist/figma/emitTimelineScript.d.ts +1 -1
  28. package/dist/figma/index.d.ts +22 -22
  29. package/dist/figma/manifest.d.ts +1 -1
  30. package/dist/figma/motionContextToDocs.d.ts +1 -1
  31. package/dist/figma/motionEase.d.ts +1 -1
  32. package/dist/figma/motionToGsap.d.ts +1 -1
  33. package/dist/figma/nodeDocument.d.ts +1 -1
  34. package/dist/figma/nodeToHtml.d.ts +3 -3
  35. package/dist/figma/parseFigmaRef.d.ts +1 -1
  36. package/dist/figma/resolveBindings.d.ts +2 -2
  37. package/dist/figma/tokensToVariables.d.ts +2 -2
  38. package/dist/generated/runtime-inline.js +1 -1
  39. package/dist/generators/hyperframes.d.ts +1 -1
  40. package/dist/hyperframe.runtime.iife.js +55 -55
  41. package/dist/index.d.ts +37 -37
  42. package/dist/inline-scripts/hyperframe.d.ts +1 -1
  43. package/dist/projects.d.ts +1 -1
  44. package/dist/runtime/bridge.d.ts +2 -2
  45. package/dist/runtime/clipTree.d.ts +1 -1
  46. package/dist/runtime/compositionLength.d.ts +2 -2
  47. package/dist/runtime/media.d.ts +1 -0
  48. package/dist/runtime/media.js +35 -0
  49. package/dist/runtime/mediaVolumeEnvelope.d.ts +1 -1
  50. package/dist/runtime/playbackRate.d.ts +1 -1
  51. package/dist/runtime/startResolver.d.ts +3 -3
  52. package/dist/runtime/state.d.ts +3 -3
  53. package/dist/runtime/timeline.d.ts +2 -2
  54. package/dist/runtime/timelineDuration.d.ts +1 -1
  55. package/dist/runtime/types.d.ts +2 -2
  56. package/dist/runtime/validateVariables.d.ts +1 -1
  57. package/dist/templates/base.d.ts +1 -1
  58. package/dist/vfx/refs/displacementMap.d.ts +1 -1
  59. package/dist/vfx/refs/fractalNoise.d.ts +1 -1
  60. package/dist/vfx/refs/lumaMatte.d.ts +1 -1
  61. package/dist/vfx/refs/noise.d.ts +1 -1
  62. package/dist/vfx/refs/waveWarp.d.ts +1 -1
  63. package/package.json +9 -6
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Sections from signal processing: band energies and chroma averaged into half-second blocks, a self-similarity matrix, and
3
+ * Foote's checkerboard novelty, whose peaks are where the music changes. Boundaries snap to the nearest beat.
4
+ */
5
+ import { bandFramesOf, medianInterval, rhythm } from "./audioRhythm.js";
6
+ import { snapper } from "./audioSnap.js";
7
+ const BLOCK_SECONDS = 0.5;
8
+ /** Checkerboard half-width: eight seconds of music either side of a candidate boundary. */
9
+ const KERNEL_BLOCKS = 16;
10
+ const SAME_SECTION = 0.9;
11
+ const round = (t) => Math.round(t * 1000) / 1000;
12
+ function meanOf(frames) {
13
+ const sum = new Array(frames[0]?.length ?? 0).fill(0);
14
+ for (const frame of frames)
15
+ for (let b = 0; b < sum.length; b++)
16
+ sum[b] += frame[b];
17
+ return sum.map((v) => v / (frames.length || 1));
18
+ }
19
+ /** Each band moved to zero mean and unit spread over time, in place. */
20
+ function standardizeBands(blocks) {
21
+ for (let b = 0; b < (blocks[0]?.length ?? 0); b++) {
22
+ const column = blocks.map((block) => block[b]);
23
+ const mean = column.reduce((a, v) => a + v, 0) / column.length;
24
+ const spread = Math.sqrt(column.reduce((a, v) => a + (v - mean) ** 2, 0) / column.length);
25
+ for (const block of blocks)
26
+ block[b] = (block[b] - mean) / (spread || 1);
27
+ }
28
+ }
29
+ /** Block means of the band energies and of the log chroma, each part unit length within a block. */
30
+ function blockParts({ fps, bands, chroma, chromaFps }) {
31
+ const per = Math.max(1, Math.round(BLOCK_SECONDS * fps));
32
+ const chromaPer = Math.max(1, Math.round(BLOCK_SECONDS * chromaFps));
33
+ const blocks = [];
34
+ for (let start = 0, b = 0; start + per <= bands.length; start += per, b++) {
35
+ const pitch = meanOf(chroma.slice(b * chromaPer, (b + 1) * chromaPer)).map(Math.log1p);
36
+ blocks.push([...meanOf(bands.slice(start, start + per)), ...unit(pitch)]);
37
+ }
38
+ return blocks;
39
+ }
40
+ function blockFeatures(frames) {
41
+ const blocks = blockParts(frames);
42
+ standardizeBands(blocks);
43
+ const split = frames.bands[0]?.length ?? 0;
44
+ return blocks.map((block) => [...unit(block.slice(0, split)), ...unit(block.slice(split))].map((v) => v * Math.SQRT1_2));
45
+ }
46
+ function unit(vector) {
47
+ const length = Math.hypot(...vector) || 1;
48
+ return vector.map((v) => v / length);
49
+ }
50
+ const dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
51
+ /** The checkerboard kernel, offsets -KERNEL_BLOCKS…KERNEL_BLOCKS-1 each way: + within a side, − across, tapered. */
52
+ const CHECKERBOARD = (() => {
53
+ const offsets = Array.from({ length: 2 * KERNEL_BLOCKS }, (_, k) => k - KERNEL_BLOCKS);
54
+ const taper = (k) => Math.exp(-0.5 * (((k < 0 ? -k - 1 : k) + 0.5) / (KERNEL_BLOCKS / 2)) ** 2);
55
+ return offsets.map((a) => offsets.map((b) => (a < 0 === b < 0 ? 1 : -1) * taper(a) * taper(b)));
56
+ })();
57
+ /** Foote novelty at block `i`: similarity within each side of it minus similarity across it. */
58
+ function noveltyAt(blocks, i) {
59
+ let sum = 0;
60
+ CHECKERBOARD.forEach((row, a) => {
61
+ const x = blocks[i + a - KERNEL_BLOCKS];
62
+ if (!x)
63
+ return;
64
+ row.forEach((weight, b) => {
65
+ const y = blocks[i + b - KERNEL_BLOCKS];
66
+ if (y)
67
+ sum += weight * dot(x, y);
68
+ });
69
+ });
70
+ return sum;
71
+ }
72
+ const novelty = (blocks) => blocks.map((_, i) => noveltyAt(blocks, i));
73
+ /** Blocks where novelty peaks over its neighbourhood and clears the mean by a standard deviation. */
74
+ function boundaryBlocks(curve, minBlocks) {
75
+ const mean = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
76
+ const spread = Math.sqrt(curve.reduce((a, v) => a + (v - mean) ** 2, 0) / (curve.length || 1));
77
+ const peaks = [];
78
+ for (let i = minBlocks; i <= curve.length - minBlocks; i++) {
79
+ const neighbourhood = curve.slice(Math.max(0, i - minBlocks), i + minBlocks + 1);
80
+ if (curve[i] < Math.max(...neighbourhood) || curve[i] <= mean + spread)
81
+ continue;
82
+ if (!peaks.length || i - peaks[peaks.length - 1] >= minBlocks)
83
+ peaks.push(i);
84
+ }
85
+ return peaks;
86
+ }
87
+ /** A letter per span: the letter of the first earlier span whose mean features are this alike, else a new one. */
88
+ function labels(blocks, edges) {
89
+ const means = [];
90
+ const letters = [];
91
+ for (let s = 0; s + 1 < edges.length; s++) {
92
+ const part = blocks.slice(edges[s], edges[s + 1]);
93
+ const mean = unit(part[0].map((_, b) => part.reduce((a, block) => a + block[b], 0)));
94
+ const match = means.findIndex((other) => dot(other, mean) >= SAME_SECTION);
95
+ letters.push(match >= 0 ? letters[match] : String.fromCharCode(65 + (new Set(letters).size % 26)));
96
+ means.push(mean);
97
+ }
98
+ return letters;
99
+ }
100
+ /** Half the median beat interval: a boundary moves to a beat, never past one. */
101
+ const beatTolerance = (beats) => (medianInterval(beats) ?? 0) / 2;
102
+ export function sections(signal, options = {}) {
103
+ const duration = round((signal.channels[0]?.length ?? 0) / signal.sampleRate);
104
+ const blocks = blockFeatures(bandFramesOf(signal));
105
+ const minBlocks = Math.max(1, Math.round((options.minSeconds ?? 4) / BLOCK_SECONDS));
106
+ if (blocks.length < 2 * minBlocks)
107
+ return { spans: [{ start: 0, end: duration, label: "A" }] };
108
+ const edges = [0, ...boundaryBlocks(novelty(blocks), minBlocks), blocks.length];
109
+ const letters = labels(blocks, edges);
110
+ const beats = rhythm(signal).beats.times;
111
+ const onBeat = snapper(beats, beatTolerance(beats), "signal beats");
112
+ const times = edges.map((e, i) => i === 0 ? 0 : i === edges.length - 1 ? duration : round(onBeat.snap(e * BLOCK_SECONDS)));
113
+ return {
114
+ spans: letters.map((label, s) => ({ start: times[s], end: times[s + 1], label })),
115
+ snap: onBeat.report(),
116
+ };
117
+ }
@@ -0,0 +1,18 @@
1
+ /** Moving reported times onto measured ones, one at a time, and saying how far they moved. */
2
+ export interface SnapReport {
3
+ /** What the times were moved onto. */
4
+ reference: string;
5
+ toleranceMs: number;
6
+ /** Times moved onto a measured one. */
7
+ snapped: number;
8
+ /** Times with nothing measured within the tolerance, kept as reported. */
9
+ unsnapped: number[];
10
+ /** Median distance between a reported time and the measured one it moved to, in ms: low means they agreed. */
11
+ medianShiftMs: number | null;
12
+ }
13
+ export interface Snapper {
14
+ snap: (t: number) => number;
15
+ report: () => SnapReport;
16
+ }
17
+ /** Snaps each time to the nearest of `measured` (ascending) when one is within `toleranceSeconds`, else keeps it. */
18
+ export declare function snapper(measured: readonly number[], toleranceSeconds: number, reference: string): Snapper;
@@ -0,0 +1,41 @@
1
+ /** Moving reported times onto measured ones, one at a time, and saying how far they moved. */
2
+ /** Index of the value in ascending `sorted` nearest `t`. */
3
+ function nearestIndex(sorted, t) {
4
+ let low = 0;
5
+ let high = sorted.length - 1;
6
+ while (low < high) {
7
+ const mid = (low + high) >> 1;
8
+ if (sorted[mid] < t)
9
+ low = mid + 1;
10
+ else
11
+ high = mid;
12
+ }
13
+ return low > 0 && t - sorted[low - 1] <= sorted[low] - t ? low - 1 : low;
14
+ }
15
+ /** Snaps each time to the nearest of `measured` (ascending) when one is within `toleranceSeconds`, else keeps it. */
16
+ export function snapper(measured, toleranceSeconds, reference) {
17
+ const shifts = [];
18
+ const unsnapped = [];
19
+ return {
20
+ snap(t) {
21
+ const nearest = measured.length ? measured[nearestIndex(measured, t)] : null;
22
+ if (nearest === null || Math.abs(nearest - t) > toleranceSeconds) {
23
+ unsnapped.push(t);
24
+ return t;
25
+ }
26
+ shifts.push(Math.abs(nearest - t));
27
+ return nearest;
28
+ },
29
+ report() {
30
+ const sorted = [...shifts].sort((a, b) => a - b);
31
+ const median = sorted.length ? sorted[sorted.length >> 1] : null;
32
+ return {
33
+ reference,
34
+ toleranceMs: Math.round(toleranceSeconds * 1000),
35
+ snapped: shifts.length,
36
+ unsnapped: [...unsnapped],
37
+ medianShiftMs: median === null ? null : Math.round(median * 1000),
38
+ };
39
+ },
40
+ };
41
+ }
@@ -1,13 +1,12 @@
1
- type BpmDetect = (buffer: AudioBuffer) => number;
2
- export declare function loadBpmDetective(importFn?: () => Promise<unknown>): Promise<BpmDetect | null>;
1
+ import { type AudioSignal } from "../audioAnalysis.js";
3
2
  export interface MusicBeatAnalysis {
4
3
  beatTimes: number[];
5
4
  /** Per-beat loudness 0–1 (local RMS / peak), aligned by index with beatTimes. */
6
5
  beatStrengths: number[];
7
6
  bpm: number | null;
8
7
  bpmConfidence: "high" | "low" | "uncertain";
9
- /** Decoded mono samples — retained so strength can be measured at user-added
10
- * beats. Audio-file coordinates. May be null if decode data was dropped. */
8
+ /** Per-sample level across channels — retained so strength can be measured at
9
+ * user-added beats. Audio-file coordinates. May be null if decode data was dropped. */
11
10
  channelData: Float32Array | null;
12
11
  sampleRate: number;
13
12
  /** Reference peak RMS used to normalize beat strengths. */
@@ -17,11 +16,10 @@ export interface MusicBeatAnalysis {
17
16
  export declare function computeRmsAt(channelData: Float32Array, sampleRate: number, time: number): number;
18
17
  /** Normalized beat strength (0–1) at an audio-file time, using a track peak. */
19
18
  export declare function strengthAtTime(analysis: Pick<MusicBeatAnalysis, "channelData" | "sampleRate" | "peak">, time: number): number;
20
- export declare function detectBeats(audioBuffer: AudioBuffer): Promise<number[]>;
21
19
  export interface MusicAnalysisOptions {
22
20
  pause?: () => Promise<void>;
23
21
  }
24
- export declare function analyzeMusicFromBuffer(audioBuffer: AudioBuffer, { pause }?: MusicAnalysisOptions): Promise<MusicBeatAnalysis>;
25
- export declare function detectBeatsFromUrl(url: string): Promise<number[]>;
22
+ /** Beats, tempo and per-beat strength of decoded audio; no beats and a null bpm when it has no steady beat. */
23
+ export declare function analyzeMusicFromSignal(signal: AudioSignal, { pause }?: MusicAnalysisOptions): Promise<MusicBeatAnalysis>;
24
+ export declare function analyzeMusicFromBuffer(audioBuffer: AudioBuffer, options?: MusicAnalysisOptions): Promise<MusicBeatAnalysis>;
26
25
  export declare function analyzeMusicFromUrl(url: string, options?: MusicAnalysisOptions): Promise<MusicBeatAnalysis>;
27
- export {};
@@ -1,28 +1,5 @@
1
- let bpmDetectivePromise = null;
2
- const defaultBpmDetectiveImport = () =>
3
- // @ts-ignore -- no type declarations for bpm-detective
4
- import("bpm-detective");
5
- export function loadBpmDetective(importFn = defaultBpmDetectiveImport) {
6
- const useCache = importFn === defaultBpmDetectiveImport;
7
- if (useCache && bpmDetectivePromise) {
8
- return bpmDetectivePromise;
9
- }
10
- const promise = importFn()
11
- .then((m) => (m.default ?? m) || null)
12
- .catch(() => {
13
- // Reset the cached promise so a transient import failure can be retried
14
- // on the next call instead of being cached as null for the session.
15
- if (useCache)
16
- bpmDetectivePromise = null;
17
- return null;
18
- });
19
- if (useCache) {
20
- bpmDetectivePromise = promise;
21
- }
22
- return promise;
23
- }
24
- const WINDOW_SIZE = 1024;
25
- const HOP_SIZE = 512;
1
+ import { energyMix } from "../audioAnalysis.js";
2
+ import { bandFramesOf, rhythm, tempoOf } from "../audioRhythm.js";
26
3
  const STRENGTH_WINDOW_S = 0.05; // ±50ms RMS window
27
4
  /** Local RMS amplitude at a given audio-file time. */
28
5
  export function computeRmsAt(channelData, sampleRate, time) {
@@ -43,208 +20,39 @@ export function strengthAtTime(analysis, time) {
43
20
  return 0.5;
44
21
  return Math.min(1, computeRmsAt(analysis.channelData, analysis.sampleRate, time) / analysis.peak);
45
22
  }
46
- // fallow-ignore-next-line complexity
47
- export async function detectBeats(audioBuffer) {
48
- const channelData = audioBuffer.getChannelData(0);
49
- const sampleRate = audioBuffer.sampleRate;
50
- const energies = [];
51
- for (let i = 0; i < channelData.length - WINDOW_SIZE; i += HOP_SIZE) {
52
- let sum = 0;
53
- for (let j = 0; j < WINDOW_SIZE; j++) {
54
- const sample = channelData[i + j];
55
- sum += sample * sample;
56
- }
57
- energies.push(sum / WINDOW_SIZE);
58
- }
59
- const beats = [];
60
- const localWindowSize = 20;
61
- for (let i = localWindowSize; i < energies.length - localWindowSize; i++) {
62
- let localMean = 0;
63
- for (let j = i - localWindowSize; j < i + localWindowSize; j++) {
64
- localMean += energies[j];
65
- }
66
- localMean /= localWindowSize * 2;
67
- const threshold = localMean * 1.5;
68
- const current = energies[i];
69
- if (current > threshold &&
70
- current > (energies[i - 1] ?? 0) &&
71
- current > (energies[i + 1] ?? 0)) {
72
- const timeInSeconds = (i * HOP_SIZE) / sampleRate;
73
- if (beats.length === 0 || timeInSeconds - beats[beats.length - 1] > 0.1) {
74
- beats.push(Math.round(timeInSeconds * 1000) / 1000);
75
- }
76
- }
77
- }
78
- return beats;
79
- }
80
- function computeBpmFromBeats(beatTimes) {
81
- if (beatTimes.length < 4)
82
- return null;
83
- const iois = [];
84
- for (let i = 1; i < beatTimes.length; i++) {
85
- iois.push(beatTimes[i] - beatTimes[i - 1]);
86
- }
87
- const sorted = [...iois].sort((a, b) => a - b);
88
- const mid = Math.floor(sorted.length / 2);
89
- const medianIoi = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
90
- if (medianIoi <= 0)
91
- return null;
92
- return Math.round((60 / medianIoi) * 10) / 10;
93
- }
94
- // Fold into 60–120 for octave-safe BPM comparison
95
- function canonicalizeBpm(bpm) {
96
- let b = bpm;
97
- while (b > 120)
98
- b /= 2;
99
- while (b < 60)
100
- b *= 2;
101
- return b;
102
- }
103
- // Pick the *2/÷2 octave of `bpm` whose beat interval is closest to the onset
104
- // pulse, so a half-time detective reading (e.g. 174 for an 87bpm song) doesn't
105
- // produce a double-density grid.
106
- function octaveAlignBpm(bpm, reference) {
107
- const candidates = [bpm / 2, bpm, bpm * 2];
108
- let best = bpm;
109
- let bestDist = Number.POSITIVE_INFINITY;
110
- for (const c of candidates) {
111
- if (c <= 0)
112
- continue;
113
- const dist = Math.abs(c - reference);
114
- if (dist < bestDist) {
115
- bestDist = dist;
116
- best = c;
117
- }
118
- }
119
- return best;
120
- }
121
- // fallow-ignore-next-line complexity
122
- function regularizeBeats(rawBeats, bpm, duration) {
123
- if (rawBeats.length === 0 || bpm <= 0 || duration <= 0)
124
- return rawBeats;
125
- const beatInterval = 60 / bpm;
126
- // Guard against a pathological (octave-misread) tempo producing a millisecond
127
- // interval → tens of thousands of grid beats that freeze the timeline. 480 BPM
128
- // (0.125s) is well above any real music tempo; bail to the raw onsets instead.
129
- if (beatInterval < 0.125)
130
- return rawBeats;
131
- const threshold = beatInterval * 0.25;
132
- // Find phase offset that maximally aligns with raw onsets
133
- let bestOffset = 0;
134
- let bestScore = -1;
135
- for (const anchor of rawBeats.slice(0, 10)) {
136
- const offset = ((anchor % beatInterval) + beatInterval) % beatInterval;
137
- let score = 0;
138
- for (const rb of rawBeats) {
139
- const phase = ((rb % beatInterval) + beatInterval) % beatInterval;
140
- const dist = Math.min(Math.abs(phase - offset), beatInterval - Math.abs(phase - offset));
141
- if (dist < threshold)
142
- score++;
143
- }
144
- if (score > bestScore) {
145
- bestScore = score;
146
- bestOffset = offset;
147
- }
148
- }
149
- const beats = [];
150
- for (let t = bestOffset; t <= duration + 0.001; t += beatInterval) {
151
- beats.push(Math.round(t * 1000) / 1000);
152
- }
153
- return beats;
154
- }
155
- // Drop beats that fall in silent/near-silent regions (e.g. intro/outro) and
156
- // score each surviving beat's loudness (local RMS / peak) for brightness.
157
- function gateBeatsBySilence(beats, channelData, sampleRate) {
158
- if (beats.length === 0)
159
- return { times: beats, strengths: [], peak: 1e-6 };
160
- const energies = beats.map((t) => computeRmsAt(channelData, sampleRate, t));
23
+ /** At or above this the tempo is reported as "high" confidence. */
24
+ const HIGH_CONFIDENCE = 0.35;
25
+ /** Each beat's local RMS against the loudest beat's, and that loudest RMS. */
26
+ function beatStrengths(beats, levels, sampleRate) {
27
+ const energies = beats.map((t) => computeRmsAt(levels, sampleRate, t));
161
28
  const peak = Math.max(...energies, 1e-6);
162
- const threshold = peak * 0.12;
163
- const times = [];
164
- const strengths = [];
165
- for (let i = 0; i < beats.length; i++) {
166
- if (energies[i] >= threshold) {
167
- times.push(beats[i]);
168
- strengths.push(Math.min(1, energies[i] / peak));
169
- }
170
- }
171
- return { times, strengths, peak };
29
+ return { strengths: energies.map((e) => Math.min(1, e / peak)), peak };
172
30
  }
173
- // fallow-ignore-next-line complexity
174
- export async function analyzeMusicFromBuffer(audioBuffer, { pause } = {}) {
175
- const channelData = audioBuffer.getChannelData(0);
176
- const sampleRate = audioBuffer.sampleRate;
177
- const duration = audioBuffer.duration;
31
+ /** Beats, tempo and per-beat strength of decoded audio; no beats and a null bpm when it has no steady beat. */
32
+ export async function analyzeMusicFromSignal(signal, { pause } = {}) {
178
33
  await pause?.();
179
- const rawBeats = await detectBeats(audioBuffer);
180
- const onsetBpm = computeBpmFromBeats(rawBeats);
181
- let detectiveBpm = null;
182
- const detect = await loadBpmDetective();
34
+ bandFramesOf(signal);
183
35
  await pause?.();
184
- try {
185
- if (detect)
186
- detectiveBpm = detect(audioBuffer);
187
- }
188
- catch {
189
- // Not enough peaks or browser context unavailable
190
- }
191
- let bpm = onsetBpm;
192
- let confidence = "uncertain";
193
- let regularizeBpm = null;
194
- if (onsetBpm !== null && detectiveBpm !== null) {
195
- const pctDiff = Math.abs(canonicalizeBpm(onsetBpm) - canonicalizeBpm(detectiveBpm)) /
196
- canonicalizeBpm(detectiveBpm);
197
- if (pctDiff < 0.05) {
198
- // Detective folds tempo into 90–180; re-pick the octave nearest the onset
199
- // pulse so a half-time track isn't gridded at double density.
200
- bpm = octaveAlignBpm(detectiveBpm, onsetBpm);
201
- confidence = "high";
202
- regularizeBpm = bpm;
203
- }
204
- else if (pctDiff < 0.1) {
205
- bpm = Math.round((onsetBpm + detectiveBpm) / 2);
206
- confidence = "low";
207
- regularizeBpm = bpm;
208
- }
209
- else {
210
- bpm = onsetBpm;
211
- confidence = "uncertain";
212
- }
213
- }
214
- else if (onsetBpm !== null) {
215
- bpm = onsetBpm;
216
- confidence = "low";
217
- regularizeBpm = onsetBpm;
218
- }
219
- else if (detectiveBpm !== null) {
220
- bpm = detectiveBpm;
221
- confidence = "low";
222
- regularizeBpm = detectiveBpm;
223
- }
36
+ const { beats } = rhythm(signal);
224
37
  await pause?.();
225
- const gridBeats = regularizeBpm !== null ? regularizeBeats(rawBeats, regularizeBpm, duration) : rawBeats;
226
- const gated = gateBeatsBySilence(gridBeats, channelData, sampleRate);
38
+ const channelData = energyMix(signal);
39
+ const confidence = beats.confidence ?? 0;
40
+ const times = beats.times;
41
+ const { strengths, peak } = beatStrengths(times, channelData, signal.sampleRate);
42
+ const bpm = tempoOf(times).value;
227
43
  return {
228
- beatTimes: gated.times,
229
- beatStrengths: gated.strengths,
44
+ beatTimes: times,
45
+ beatStrengths: strengths,
230
46
  bpm,
231
- bpmConfidence: confidence,
47
+ bpmConfidence: bpm === null ? "uncertain" : confidence >= HIGH_CONFIDENCE ? "high" : "low",
232
48
  channelData,
233
- sampleRate,
234
- peak: gated.peak,
49
+ sampleRate: signal.sampleRate,
50
+ peak,
235
51
  };
236
52
  }
237
- export async function detectBeatsFromUrl(url) {
238
- const audioContext = new AudioContext();
239
- try {
240
- const response = await fetch(url);
241
- const arrayBuffer = await response.arrayBuffer();
242
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
243
- return detectBeats(audioBuffer);
244
- }
245
- finally {
246
- await audioContext.close();
247
- }
53
+ export function analyzeMusicFromBuffer(audioBuffer, options = {}) {
54
+ const channels = Array.from({ length: audioBuffer.numberOfChannels }, (_, c) => audioBuffer.getChannelData(c));
55
+ return analyzeMusicFromSignal({ sampleRate: audioBuffer.sampleRate, channels }, options);
248
56
  }
249
57
  export async function analyzeMusicFromUrl(url, options = {}) {
250
58
  const audioContext = new AudioContext();
@@ -1,7 +1,7 @@
1
- import type { HfColorCurvePoint, HfHueCurvePoint } from "./colorGradingCurves";
1
+ import type { HfColorCurvePoint, HfHueCurvePoint } from "./colorGradingCurves.js";
2
2
  import { COLOR_GRADING_ADJUST_KEYS, COLOR_GRADING_CURVE_KEYS, COLOR_GRADING_DETAIL_KEYS, COLOR_GRADING_EFFECT_KEYS, COLOR_GRADING_HUE_CURVE_KEYS, COLOR_GRADING_WHEEL_KEYS } from "@hyperframes/parsers/color-grading-contract";
3
- export type { HfColorCurvePoint, HfHueCurvePoint } from "./colorGradingCurves";
4
- export { compileHfColorCurve, compileHfHueCurve, HF_COLOR_CURVE_MAX_POINTS, } from "./colorGradingCurves";
3
+ export type { HfColorCurvePoint, HfHueCurvePoint } from "./colorGradingCurves.js";
4
+ export { compileHfColorCurve, compileHfHueCurve, HF_COLOR_CURVE_MAX_POINTS, } from "./colorGradingCurves.js";
5
5
  export declare const HF_COLOR_GRADING_ATTR = "data-color-grading";
6
6
  /** Set on a graded source while its pixels render on the grading canvas. */
7
7
  export declare const COLOR_GRADING_SOURCE_HIDDEN_ATTR = "data-hf-color-grading-source-hidden";
@@ -1,5 +1,5 @@
1
- export { FLATTENED_INNER_ROOT_STRIP_ATTRS } from "../runtime/flattenedRoot";
2
- import { type MediaDurationProber } from "./htmlCompiler";
1
+ export { FLATTENED_INNER_ROOT_STRIP_ATTRS } from "../runtime/flattenedRoot.js";
2
+ import { type MediaDurationProber } from "./htmlCompiler.js";
3
3
  export type BundledHostCompositionIdentity = {
4
4
  authoredCompositionId: string | null;
5
5
  runtimeCompositionId: string | null;
@@ -1,19 +1,19 @@
1
- export { resolveTimings, type WordTiming, type ElementAnchor, type AuthoredTiming, type ResolvedTiming, type ResolveTimingsInput, type ResolveTimingsResult, } from "./timingResolver";
2
- export { compileTimingAttrs, injectDurations, extractResolvedMedia, clampDurations, shouldClampMediaDuration, shouldClampResolvedMediaDuration, type UnresolvedElement, type ResolvedDuration, type ResolvedMediaElement, type CompilationResult, } from "./timingCompiler";
3
- export { compileHtml, type MediaDurationProber } from "./htmlCompiler";
4
- export { assignBundledRuntimeCompositionIds, type BundledHostCompositionIdentity, bundleToSingleHtml, type BundleOptions, type BundleScriptProvenance, prepareFlattenedInnerRoot, FLATTENED_INNER_ROOT_STRIP_ATTRS, emitRootCompositionVariableStyles, parsesAsScript, } from "./htmlBundler";
5
- export { readDeclaredDefaults, parseHostVariableValues } from "../runtime/getVariables";
6
- export { extractCompiledHtmlParityContract, type CompiledHtmlParityContract, type HtmlParityComposition, type HtmlParityResource, type HtmlParityTimedElement, } from "./htmlParityContract";
7
- export { RUNTIME_BOOTSTRAP_ATTR, injectScriptsAtHeadStart, injectTagsAtHeadStart, injectScriptsIntoHtml, insertBeforeCloseTag, insertRuntimeTag, isRuntimeFileUrl, parseHTMLContent, stripEmbeddedRuntimeScripts, } from "./htmlDocument";
8
- export { addScenePartsManifest } from "./scenePartsManifest";
9
- export { AFTER_FONTS_SCRIPT_TYPE, compositionStyle, cssStyleMergeKey, deferScriptsUntilFonts, headStyleRuns, inlineScriptRuns, styleElementsFor, type CompositionStyle, type InlineScriptRun, } from "./scriptRuns";
10
- export { isJavaScriptType } from "./compositionAssembly";
11
- export { validateHyperframeHtmlContract, type HyperframeStaticFailureReason, type HyperframeStaticGuardResult, } from "./staticGuard";
12
- export { buildVariablesByCompScript, scopeCssToComposition, wrapScopedCompositionScript, } from "./compositionScoping";
13
- export { ensureExternalLinkTag, inlineSubCompositions, type InlineSubCompositionsOptions, type InlineSubCompositionsResult, } from "./inlineSubCompositions";
14
- export { extractStandaloneEntryFromIndex, isTemplateEntry } from "./standaloneEntry";
15
- export { checkSubCompositionUsability, type ParsableDocumentLike, type SubCompositionValidity, type SubCompositionValidityReason, } from "./subCompositionValidity";
16
- export { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl, isPathInside } from "./assetPaths";
17
- export { AUDIO_GROUP_RENDER_ID_ATTR, MEDIA_RENDER_ID_ATTR, assignMediaRenderIds, } from "./mediaRenderIds";
18
- export { ensureExternalScriptTag } from "./externalScripts";
19
- export { emitMountedModuleScripts } from "./importMaps";
1
+ export { resolveTimings, type WordTiming, type ElementAnchor, type AuthoredTiming, type ResolvedTiming, type ResolveTimingsInput, type ResolveTimingsResult, } from "./timingResolver.js";
2
+ export { compileTimingAttrs, injectDurations, extractResolvedMedia, clampDurations, shouldClampMediaDuration, shouldClampResolvedMediaDuration, type UnresolvedElement, type ResolvedDuration, type ResolvedMediaElement, type CompilationResult, } from "./timingCompiler.js";
3
+ export { compileHtml, type MediaDurationProber } from "./htmlCompiler.js";
4
+ export { assignBundledRuntimeCompositionIds, type BundledHostCompositionIdentity, bundleToSingleHtml, type BundleOptions, type BundleScriptProvenance, prepareFlattenedInnerRoot, FLATTENED_INNER_ROOT_STRIP_ATTRS, emitRootCompositionVariableStyles, parsesAsScript, } from "./htmlBundler.js";
5
+ export { readDeclaredDefaults, parseHostVariableValues } from "../runtime/getVariables.js";
6
+ export { extractCompiledHtmlParityContract, type CompiledHtmlParityContract, type HtmlParityComposition, type HtmlParityResource, type HtmlParityTimedElement, } from "./htmlParityContract.js";
7
+ export { RUNTIME_BOOTSTRAP_ATTR, injectScriptsAtHeadStart, injectTagsAtHeadStart, injectScriptsIntoHtml, insertBeforeCloseTag, insertRuntimeTag, isRuntimeFileUrl, parseHTMLContent, stripEmbeddedRuntimeScripts, } from "./htmlDocument.js";
8
+ export { addScenePartsManifest } from "./scenePartsManifest.js";
9
+ export { AFTER_FONTS_SCRIPT_TYPE, compositionStyle, cssStyleMergeKey, deferScriptsUntilFonts, headStyleRuns, inlineScriptRuns, styleElementsFor, type CompositionStyle, type InlineScriptRun, } from "./scriptRuns.js";
10
+ export { isJavaScriptType } from "./compositionAssembly.js";
11
+ export { validateHyperframeHtmlContract, type HyperframeStaticFailureReason, type HyperframeStaticGuardResult, } from "./staticGuard.js";
12
+ export { buildVariablesByCompScript, scopeCssToComposition, wrapScopedCompositionScript, } from "./compositionScoping.js";
13
+ export { ensureExternalLinkTag, inlineSubCompositions, type InlineSubCompositionsOptions, type InlineSubCompositionsResult, } from "./inlineSubCompositions.js";
14
+ export { extractStandaloneEntryFromIndex, isTemplateEntry } from "./standaloneEntry.js";
15
+ export { checkSubCompositionUsability, type ParsableDocumentLike, type SubCompositionValidity, type SubCompositionValidityReason, } from "./subCompositionValidity.js";
16
+ export { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl, isPathInside } from "./assetPaths.js";
17
+ export { AUDIO_GROUP_RENDER_ID_ATTR, MEDIA_RENDER_ID_ATTR, assignMediaRenderIds, } from "./mediaRenderIds.js";
18
+ export { ensureExternalScriptTag } from "./externalScripts.js";
19
+ export { emitMountedModuleScripts } from "./importMaps.js";
@@ -1,6 +1,6 @@
1
- import { type ExternalScriptAttributes } from "./externalScripts";
2
- import { type ImportMap } from "./importMaps";
3
- import { type CompositionStyle } from "./scriptRuns";
1
+ import { type ExternalScriptAttributes } from "./externalScripts.js";
2
+ import { type ImportMap } from "./importMaps.js";
3
+ import { type CompositionStyle } from "./scriptRuns.js";
4
4
  /**
5
5
  * Shared sub-composition inlining logic.
6
6
  *
@@ -10,7 +10,7 @@ import { type CompositionStyle } from "./scriptRuns";
10
10
  * divergence that previously caused bugs (e.g. producer not setting
11
11
  * `data-composition-file`).
12
12
  */
13
- import { type AssetExists } from "./rewriteSubCompPaths";
13
+ import { type AssetExists } from "./rewriteSubCompPaths.js";
14
14
  /** Marks each scene whose nodes a script outside it names by selector, id or class: a swap would strand it. */
15
15
  export declare function refuseSwapsReachedByRootScripts(document: Document, rootScripts: string[]): void;
16
16
  export interface InlineSubCompositionsOptions {
@@ -5,7 +5,7 @@
5
5
  * keeps the authored id (otherwise the first element does). Renamed elements retain
6
6
  * data-hf-authored-id for script lookup compatibility.
7
7
  */
8
- import { type SvgReferenceAlias } from "./svgSelectorAliases";
8
+ import { type SvgReferenceAlias } from "./svgSelectorAliases.js";
9
9
  /** Reused from `compositionScoping.ts`'s `AUTHORED_ROOT_ID_ATTR` in spirit —
10
10
  * same purpose (let a renamed id still resolve by its authored name), now
11
11
  * generalized from "the composition root" to any renamed descendant. */
package/dist/fft.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** In-place iterative radix-2 FFT. */
2
+ export declare function fft(re: Float64Array, im: Float64Array): void;
package/dist/fft.js ADDED
@@ -0,0 +1,36 @@
1
+ /** In-place iterative radix-2 FFT. */
2
+ export function fft(re, im) {
3
+ const n = re.length;
4
+ for (let i = 1, j = 0; i < n; i++) {
5
+ let bit = n >> 1;
6
+ for (; j & bit; bit >>= 1)
7
+ j ^= bit;
8
+ j ^= bit;
9
+ if (i < j) {
10
+ [re[i], re[j]] = [re[j], re[i]];
11
+ [im[i], im[j]] = [im[j], im[i]];
12
+ }
13
+ }
14
+ for (let len = 2; len <= n; len <<= 1) {
15
+ const ang = (-2 * Math.PI) / len;
16
+ const wr = Math.cos(ang);
17
+ const wi = Math.sin(ang);
18
+ for (let i = 0; i < n; i += len) {
19
+ let cr = 1;
20
+ let ci = 0;
21
+ for (let k = 0; k < len / 2; k++) {
22
+ const ur = re[i + k];
23
+ const ui = im[i + k];
24
+ const vr = re[i + k + len / 2] * cr - im[i + k + len / 2] * ci;
25
+ const vi = re[i + k + len / 2] * ci + im[i + k + len / 2] * cr;
26
+ re[i + k] = ur + vr;
27
+ im[i + k] = ui + vi;
28
+ re[i + k + len / 2] = ur - vr;
29
+ im[i + k + len / 2] = ui - vi;
30
+ const ncr = cr * wr - ci * wi;
31
+ ci = cr * wi + ci * wr;
32
+ cr = ncr;
33
+ }
34
+ }
35
+ }
36
+ }
@@ -1,2 +1,2 @@
1
- import type { AssetSnippet, FigmaManifestRecord } from "./types";
1
+ import type { AssetSnippet, FigmaManifestRecord } from "./types.js";
2
2
  export declare function buildAssetSnippet(record: FigmaManifestRecord): AssetSnippet;
@@ -1,4 +1,4 @@
1
- import type { FigmaAssetFormat, FigmaRef } from "./types";
1
+ import type { FigmaAssetFormat, FigmaRef } from "./types.js";
2
2
  /** Typed capability/transport failures per design spec §4.4. */
3
3
  export type FigmaClientErrorCode = "NO_TOKEN" | "BAD_TOKEN" | "FORBIDDEN" | "REQUIRES_ENTERPRISE" | "RATE_LIMITED" | "RENDER_FAILED" | "NODE_NOT_FOUND" | "HTTP_ERROR";
4
4
  export declare class FigmaClientError extends Error {
@@ -1,2 +1,2 @@
1
- import type { TimelineSpec } from "./types";
1
+ import type { TimelineSpec } from "./types.js";
2
2
  export declare function emitTimelineScript(spec: TimelineSpec): string;