@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.
- package/dist/adapters/gsap.d.ts +1 -1
- package/dist/adapters/index.d.ts +3 -3
- package/dist/audioAnalysis.d.ts +121 -0
- package/dist/audioAnalysis.js +466 -0
- package/dist/audioAnalysisLadder.d.ts +52 -0
- package/dist/audioAnalysisLadder.js +78 -0
- package/dist/audioCarve.js +1 -36
- package/dist/audioResample.d.ts +2 -0
- package/dist/audioResample.js +37 -0
- package/dist/audioRhythm.d.ts +48 -0
- package/dist/audioRhythm.js +407 -0
- package/dist/audioSections.d.ts +21 -0
- package/dist/audioSections.js +117 -0
- package/dist/audioSnap.d.ts +18 -0
- package/dist/audioSnap.js +41 -0
- package/dist/beats/beatDetection.d.ts +6 -8
- package/dist/beats/beatDetection.js +25 -217
- package/dist/colorGrading.d.ts +3 -3
- package/dist/compiler/htmlBundler.d.ts +2 -2
- package/dist/compiler/index.d.ts +19 -19
- package/dist/compiler/inlineSubCompositions.d.ts +4 -4
- package/dist/compiler/svgIdNamespacing.d.ts +1 -1
- package/dist/fft.d.ts +2 -0
- package/dist/fft.js +36 -0
- package/dist/figma/assetSnippet.d.ts +1 -1
- package/dist/figma/client.d.ts +1 -1
- package/dist/figma/emitTimelineScript.d.ts +1 -1
- package/dist/figma/index.d.ts +22 -22
- package/dist/figma/manifest.d.ts +1 -1
- package/dist/figma/motionContextToDocs.d.ts +1 -1
- package/dist/figma/motionEase.d.ts +1 -1
- package/dist/figma/motionToGsap.d.ts +1 -1
- package/dist/figma/nodeDocument.d.ts +1 -1
- package/dist/figma/nodeToHtml.d.ts +3 -3
- package/dist/figma/parseFigmaRef.d.ts +1 -1
- package/dist/figma/resolveBindings.d.ts +2 -2
- package/dist/figma/tokensToVariables.d.ts +2 -2
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/generators/hyperframes.d.ts +1 -1
- package/dist/hyperframe.runtime.iife.js +55 -55
- package/dist/index.d.ts +37 -37
- package/dist/inline-scripts/hyperframe.d.ts +1 -1
- package/dist/projects.d.ts +1 -1
- package/dist/runtime/bridge.d.ts +2 -2
- package/dist/runtime/clipTree.d.ts +1 -1
- package/dist/runtime/compositionLength.d.ts +2 -2
- package/dist/runtime/media.d.ts +1 -0
- package/dist/runtime/media.js +35 -0
- package/dist/runtime/mediaVolumeEnvelope.d.ts +1 -1
- package/dist/runtime/playbackRate.d.ts +1 -1
- package/dist/runtime/startResolver.d.ts +3 -3
- package/dist/runtime/state.d.ts +3 -3
- package/dist/runtime/timeline.d.ts +2 -2
- package/dist/runtime/timelineDuration.d.ts +1 -1
- package/dist/runtime/types.d.ts +2 -2
- package/dist/runtime/validateVariables.d.ts +1 -1
- package/dist/templates/base.d.ts +1 -1
- package/dist/vfx/refs/displacementMap.d.ts +1 -1
- package/dist/vfx/refs/fractalNoise.d.ts +1 -1
- package/dist/vfx/refs/lumaMatte.d.ts +1 -1
- package/dist/vfx/refs/noise.d.ts +1 -1
- package/dist/vfx/refs/waveWarp.d.ts +1 -1
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
25
|
-
export declare function
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
export async function
|
|
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
|
-
|
|
180
|
-
const onsetBpm = computeBpmFromBeats(rawBeats);
|
|
181
|
-
let detectiveBpm = null;
|
|
182
|
-
const detect = await loadBpmDetective();
|
|
34
|
+
bandFramesOf(signal);
|
|
183
35
|
await pause?.();
|
|
184
|
-
|
|
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
|
|
226
|
-
const
|
|
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:
|
|
229
|
-
beatStrengths:
|
|
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
|
|
49
|
+
sampleRate: signal.sampleRate,
|
|
50
|
+
peak,
|
|
235
51
|
};
|
|
236
52
|
}
|
|
237
|
-
export
|
|
238
|
-
const
|
|
239
|
-
|
|
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();
|
package/dist/colorGrading.d.ts
CHANGED
|
@@ -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;
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -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
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;
|
package/dist/figma/client.d.ts
CHANGED
|
@@ -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;
|