@hyperframes/studio 0.4.27 → 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-CAscydDF.js"></script>
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.27",
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/core": "0.4.27",
36
- "@hyperframes/player": "0.4.27"
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.27"
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 audioUrl = el.src
409
- ? el.src.startsWith("http")
410
- ? el.src
411
- : `/api/projects/${pid}/preview/${el.src}`
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 audioUrl={audioUrl} label={el.id || el.tag} labelColor={style.label} />
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 [peaks, setPeaks] = useState<number[] | null>(peaksCache.get(audioUrl) ?? null);
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 || !audioUrl) return;
71
-
72
- const ctrl = new AbortController();
73
- fetch(audioUrl, { signal: ctrl.signal })
74
- .then((r) => r.arrayBuffer())
75
- .then((buf) => {
76
- const ctx = new AudioContext();
77
- return ctx.decodeAudioData(buf).finally(() => ctx.close());
78
- })
79
- .then((decoded) => {
80
- if (ctrl.signal.aborted) return;
81
- const channel = decoded.getChannelData(0);
82
- // Extract enough peaks for wide clips (up to 4000 bars)
83
- const p = extractPeaks(channel, 4000);
84
- peaksCache.set(audioUrl, p);
85
- setPeaks(p);
86
- })
87
- .catch(() => {
88
- if (ctrl.signal.aborted) return;
89
- // Fallback to fake waveform
90
- const p = fakePeaks(audioUrl, 4000);
91
- peaksCache.set(audioUrl, p);
92
- setPeaks(p);
93
- });
94
-
95
- return () => ctrl.abort();
96
- }, [audioUrl, peaks]);
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(() => {