@hyperframes/studio 0.5.0-alpha.2 → 0.5.0-alpha.3
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-Z9LjUi1W.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BpcIkyVP.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.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.3",
|
|
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/core": "0.5.0-alpha.
|
|
36
|
-
"@hyperframes/player": "0.5.0-alpha.
|
|
35
|
+
"@hyperframes/core": "0.5.0-alpha.3",
|
|
36
|
+
"@hyperframes/player": "0.5.0-alpha.3"
|
|
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.5.0-alpha.
|
|
50
|
+
"@hyperframes/producer": "0.5.0-alpha.3"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -852,13 +852,28 @@ export function StudioApp() {
|
|
|
852
852
|
|
|
853
853
|
// Audio clips — waveform visualization
|
|
854
854
|
if (el.tag === "audio") {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
855
|
+
const previewBase = `/api/projects/${pid}/preview/`;
|
|
856
|
+
const previewIdx = el.src?.startsWith("http") ? el.src.indexOf(previewBase) : -1;
|
|
857
|
+
const srcRelative = el.src
|
|
858
|
+
? previewIdx !== -1
|
|
859
|
+
? decodeURIComponent(el.src.slice(previewIdx + previewBase.length))
|
|
860
|
+
: el.src.startsWith("http")
|
|
861
|
+
? null
|
|
862
|
+
: el.src
|
|
863
|
+
: null;
|
|
864
|
+
const audioUrl = srcRelative
|
|
865
|
+
? `/api/projects/${pid}/preview/${srcRelative}`
|
|
866
|
+
: (el.src ?? "");
|
|
867
|
+
const waveformUrl = srcRelative
|
|
868
|
+
? `/api/projects/${pid}/waveform/${srcRelative}`
|
|
869
|
+
: undefined;
|
|
860
870
|
return (
|
|
861
|
-
<AudioWaveform
|
|
871
|
+
<AudioWaveform
|
|
872
|
+
audioUrl={audioUrl}
|
|
873
|
+
waveformUrl={waveformUrl}
|
|
874
|
+
label={el.id || el.tag}
|
|
875
|
+
labelColor={style.label}
|
|
876
|
+
/>
|
|
862
877
|
);
|
|
863
878
|
}
|
|
864
879
|
|
|
@@ -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(() => {
|