@hyperframes/studio 0.4.28 → 0.4.29
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/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<title>HyperFrames Studio</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-aCeL3Cf-.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-dpgHnQGg.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.29",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"codemirror": "^6.0.1",
|
|
34
34
|
"motion": "^12.38.0",
|
|
35
|
-
"@hyperframes/
|
|
36
|
-
"@hyperframes/
|
|
35
|
+
"@hyperframes/core": "0.4.29",
|
|
36
|
+
"@hyperframes/player": "0.4.29"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"vite": "^6.4.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"zustand": "^5.0.0",
|
|
50
|
-
"@hyperframes/producer": "0.4.
|
|
50
|
+
"@hyperframes/producer": "0.4.29"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -405,13 +405,28 @@ export function StudioApp() {
|
|
|
405
405
|
|
|
406
406
|
// Audio clips — waveform visualization
|
|
407
407
|
if (el.tag === "audio") {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
408
|
+
const previewBase = `/api/projects/${pid}/preview/`;
|
|
409
|
+
const previewIdx = el.src?.startsWith("http") ? el.src.indexOf(previewBase) : -1;
|
|
410
|
+
const srcRelative = el.src
|
|
411
|
+
? previewIdx !== -1
|
|
412
|
+
? decodeURIComponent(el.src.slice(previewIdx + previewBase.length))
|
|
413
|
+
: el.src.startsWith("http")
|
|
414
|
+
? null
|
|
415
|
+
: el.src
|
|
416
|
+
: null;
|
|
417
|
+
const audioUrl = srcRelative
|
|
418
|
+
? `/api/projects/${pid}/preview/${srcRelative}`
|
|
419
|
+
: (el.src ?? "");
|
|
420
|
+
const waveformUrl = srcRelative
|
|
421
|
+
? `/api/projects/${pid}/waveform/${srcRelative}`
|
|
422
|
+
: undefined;
|
|
413
423
|
return (
|
|
414
|
-
<AudioWaveform
|
|
424
|
+
<AudioWaveform
|
|
425
|
+
audioUrl={audioUrl}
|
|
426
|
+
waveformUrl={waveformUrl}
|
|
427
|
+
label={el.id || el.tag}
|
|
428
|
+
labelColor={style.label}
|
|
429
|
+
/>
|
|
415
430
|
);
|
|
416
431
|
}
|
|
417
432
|
|
|
@@ -2,6 +2,7 @@ import { memo, useRef, useState, useCallback, useEffect } from "react";
|
|
|
2
2
|
|
|
3
3
|
interface AudioWaveformProps {
|
|
4
4
|
audioUrl: string;
|
|
5
|
+
waveformUrl?: string;
|
|
5
6
|
label: string;
|
|
6
7
|
labelColor: string;
|
|
7
8
|
}
|
|
@@ -49,6 +50,7 @@ function fakePeaks(url: string, count: number): number[] {
|
|
|
49
50
|
|
|
50
51
|
// Module-level cache so decoded audio persists across re-renders and re-mounts
|
|
51
52
|
const peaksCache = new Map<string, number[]>();
|
|
53
|
+
const decodeInFlight = new Map<string, Promise<number[]>>();
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Audio waveform rendered from real PCM data via Web Audio API.
|
|
@@ -57,43 +59,56 @@ const peaksCache = new Map<string, number[]>();
|
|
|
57
59
|
*/
|
|
58
60
|
export const AudioWaveform = memo(function AudioWaveform({
|
|
59
61
|
audioUrl,
|
|
62
|
+
waveformUrl,
|
|
60
63
|
label,
|
|
61
64
|
labelColor,
|
|
62
65
|
}: AudioWaveformProps) {
|
|
63
66
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
64
67
|
const barsRef = useRef<HTMLDivElement | null>(null);
|
|
65
68
|
const roRef = useRef<ResizeObserver | null>(null);
|
|
66
|
-
const
|
|
69
|
+
const cacheKey = waveformUrl ?? audioUrl;
|
|
70
|
+
const [peaks, setPeaks] = useState<number[] | null>(peaksCache.get(cacheKey) ?? null);
|
|
67
71
|
|
|
68
|
-
// Fetch + decode audio once
|
|
69
72
|
useEffect(() => {
|
|
70
|
-
if (peaks || !
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
if (peaks || !cacheKey) return;
|
|
74
|
+
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
|
|
77
|
+
let promise = decodeInFlight.get(cacheKey);
|
|
78
|
+
if (!promise) {
|
|
79
|
+
promise = (
|
|
80
|
+
waveformUrl
|
|
81
|
+
? fetch(waveformUrl)
|
|
82
|
+
.then((r) => r.json())
|
|
83
|
+
.then((d: { peaks?: number[] }) => {
|
|
84
|
+
if (!Array.isArray(d.peaks)) throw new Error("bad response");
|
|
85
|
+
return d.peaks;
|
|
86
|
+
})
|
|
87
|
+
: fetch(audioUrl)
|
|
88
|
+
.then((r) => r.arrayBuffer())
|
|
89
|
+
.then((buf) => {
|
|
90
|
+
const ctx = new AudioContext();
|
|
91
|
+
return ctx.decodeAudioData(buf).finally(() => ctx.close());
|
|
92
|
+
})
|
|
93
|
+
.then((decoded) => extractPeaks(decoded.getChannelData(0), 4000))
|
|
94
|
+
)
|
|
95
|
+
.catch(() => fakePeaks(cacheKey, 4000))
|
|
96
|
+
.then((p) => {
|
|
97
|
+
peaksCache.set(cacheKey, p);
|
|
98
|
+
return p;
|
|
99
|
+
})
|
|
100
|
+
.finally(() => decodeInFlight.delete(cacheKey));
|
|
101
|
+
|
|
102
|
+
decodeInFlight.set(cacheKey, promise);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
promise.then((p) => {
|
|
106
|
+
if (!cancelled) setPeaks(p);
|
|
107
|
+
});
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [audioUrl, waveformUrl, cacheKey, peaks]);
|
|
97
112
|
|
|
98
113
|
// Draw bars into the container using innerHTML (fast, zoom-resilient)
|
|
99
114
|
const draw = useCallback(() => {
|