@lucaismyname/ginger 0.0.38 → 0.0.40
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/CHANGELOG.md +18 -0
- package/README.md +14 -15
- package/dist/equalizer/useGingerEqualizer.test.d.ts +2 -0
- package/dist/equalizer/useGingerEqualizer.test.d.ts.map +1 -0
- package/dist/store.test.d.ts +2 -0
- package/dist/store.test.d.ts.map +1 -0
- package/dist/transcript/index.cjs +7 -7
- package/dist/transcript/index.cjs.map +1 -1
- package/dist/transcript/index.js +57 -52
- package/dist/transcript/index.js.map +1 -1
- package/dist/transcript/useGingerTranscriptSync.d.ts.map +1 -1
- package/dist/transcript/useGingerTranscriptSync.test.d.ts +2 -0
- package/dist/transcript/useGingerTranscriptSync.test.d.ts.map +1 -0
- package/dist/waveform/index.cjs +1 -1
- package/dist/waveform/index.cjs.map +1 -1
- package/dist/waveform/index.d.ts +1 -1
- package/dist/waveform/index.d.ts.map +1 -1
- package/dist/waveform/index.js +129 -129
- package/dist/waveform/index.js.map +1 -1
- package/dist/waveform/useAudioPeaks.d.ts +14 -1
- package/dist/waveform/useAudioPeaks.d.ts.map +1 -1
- package/package.json +5 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@lucaismyname/ginger` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Clarified documentation links in `README.md` so npm and GitHub readers land on valid docs.
|
|
13
|
+
|
|
14
|
+
## [0.0.40] - 2026-04-12
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Latest package release.
|
package/README.md
CHANGED
|
@@ -49,15 +49,17 @@ Mount **`<Ginger.Player />`** once inside the same provider tree so the hidden a
|
|
|
49
49
|
|
|
50
50
|
## Documentation
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
52
|
+
For docs beyond this README, use the repository links below:
|
|
53
|
+
|
|
54
|
+
- Getting started: [`docs/getting-started.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/getting-started.md)
|
|
55
|
+
- Testing guide: [`docs/guides/testing.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/guides/testing.md)
|
|
56
|
+
- Recipes: [`docs/guides/recipes.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/guides/recipes.md)
|
|
57
|
+
- Accessibility checklist: [`docs/guides/accessibility.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/guides/accessibility.md)
|
|
58
|
+
- Streaming adapters: [`docs/guides/streaming-adapters.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/guides/streaming-adapters.md)
|
|
59
|
+
- Components reference: [`docs/reference/components.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/components.md)
|
|
60
|
+
- Hooks reference: [`docs/reference/hooks.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/hooks.md)
|
|
61
|
+
- Subpath exports (waveform, EQ, spatial, transcript, remote, …): [`docs/reference/subpaths.md`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/reference/subpaths.md)
|
|
62
|
+
- Generated API docs: [`docs/api/index.html`](https://github.com/lucaismyname/ginger/blob/main/packages/ginger/docs/api/index.html)
|
|
61
63
|
|
|
62
64
|
## Subpath Exports
|
|
63
65
|
|
|
@@ -206,15 +208,12 @@ It currently provides capability metadata only and does not alter playback behav
|
|
|
206
208
|
## Release Process
|
|
207
209
|
|
|
208
210
|
- Changelog: [`CHANGELOG.md`](./CHANGELOG.md)
|
|
209
|
-
- Release note template: [
|
|
211
|
+
- Release note template: [`.github/release-template.md`](https://github.com/lucaismyname/ginger/blob/main/.github/release-template.md)
|
|
210
212
|
|
|
211
213
|
Before publishing:
|
|
212
214
|
|
|
213
215
|
```bash
|
|
214
|
-
npm run
|
|
215
|
-
npm run test
|
|
216
|
-
npm run typecheck
|
|
217
|
-
npm run lint
|
|
216
|
+
npm run verify:release
|
|
218
217
|
npm run docs:api
|
|
219
218
|
```
|
|
220
219
|
|
|
@@ -960,7 +959,7 @@ function FileViz({ url }: { url: string }) {
|
|
|
960
959
|
}
|
|
961
960
|
```
|
|
962
961
|
|
|
963
|
-
**`useAudioPeaks`** (same subpath) remains a lightweight helper: a single row of decoded amplitude peaks. Prefer **`useAudioFileAnalysis`** when you need a 2D amplitude grid or spectrogram.
|
|
962
|
+
**`useAudioPeaks`** (same subpath) remains a lightweight helper: a single row of decoded amplitude peaks. For large files, pass `maxBuckets` and `maxSamplesPerBucket` as the third argument to cap compute cost. Prefer **`useAudioFileAnalysis`** when you need a 2D amplitude grid or spectrogram.
|
|
964
963
|
|
|
965
964
|
## Recipes
|
|
966
965
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGingerEqualizer.test.d.ts","sourceRoot":"","sources":["../../src/equalizer/useGingerEqualizer.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../src/store.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react"),
|
|
2
|
-
`).split(/\n\s*\n/);for(const
|
|
3
|
-
`).map(
|
|
4
|
-
`));f&&
|
|
5
|
-
`);if(
|
|
6
|
-
`).map(p=>p.trimEnd());if(
|
|
7
|
-
`));
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react"),F=require("../GingerSplitContexts-C7puo0M7.cjs");function g(n){return n.replace(/<[^>]+>/g,"").trim()}function T(n){const e=n.trim().replace(",",".").split(":");if(e.length===3){const i=Number(e[0]),t=Number(e[1]),s=Number(e[2]);return[i,t,s].every(Number.isFinite)?i*3600+t*60+s:Number.NaN}if(e.length===2){const i=Number(e[0]),t=Number(e[1]);return[i,t].every(Number.isFinite)?i*60+t:Number.NaN}return Number.NaN}function S(n){const r=n.indexOf("-->");if(r<0)return null;const e=n.slice(0,r).trim(),t=n.slice(r+3).trim().match(/^(\S+)/);return t?{start:e,end:t[1]}:null}function b(n){const r=[],e=n.replace(/\r\n/g,`
|
|
2
|
+
`).split(/\n\s*\n/);for(const i of e){const t=i.split(`
|
|
3
|
+
`).map(m=>m.trim()).filter(m=>m.length>0);if(t.length===0)continue;let s=0;/^\d+$/.test(t[0])&&(s=1);const c=t[s];if(!c)continue;const o=S(c);if(!o)continue;const a=T(o.start),u=T(o.end);if(!Number.isFinite(a)||!Number.isFinite(u))continue;const l=t.slice(s+1),f=g(l.join(`
|
|
4
|
+
`));f&&r.push({startTime:a,endTime:u,text:f})}return r.sort((i,t)=>i.startTime-t.startTime)}function N(n){const r=[];let e=n.replace(/\r\n/g,`
|
|
5
|
+
`);if(e.startsWith("WEBVTT")){const t=e.search(/\n\s*\n/);e=t>=0?e.slice(t).trim():""}const i=e.split(/\n\s*\n/);for(const t of i){const c=t.split(`
|
|
6
|
+
`).map(p=>p.trimEnd());if(c.length===0||c[0].startsWith("NOTE")||c[0].startsWith("STYLE")||c[0].startsWith("REGION"))continue;let o=0,a,u=c[o];if(u.includes("-->")||(a=c[o],o+=1,u=c[o]),!(u!=null&&u.includes("-->")))continue;const l=S(u);if(!l)continue;const f=T(l.start),m=T(l.end);if(!Number.isFinite(f)||!Number.isFinite(m))continue;const x=c.slice(o+1).filter(p=>p.trim().length>0),h=g(x.join(`
|
|
7
|
+
`));h&&r.push({startTime:f,endTime:m,text:h,...a?{id:a}:{}})}return r.sort((t,s)=>t.startTime-s.startTime)}function y(n){return n.trimStart().startsWith("WEBVTT")?N(n):b(n)}function v(n,r){return r==="vtt"?N(n):r==="srt"?b(n):y(n)}function L(n,r){let e=0,i=n.length-1,t=-1;for(;e<=i;){const s=e+Math.floor((i-e)/2),c=n[s];if(!c)break;c.startTime<=r?(t=s,e=s+1):i=s-1}return t}function M(n){const{transcript:r,format:e="auto"}=n,{currentTime:i}=F.useGingerMedia(),t=d.useMemo(()=>Array.isArray(r)?[...r].filter(o=>Number.isFinite(o.startTime)&&Number.isFinite(o.endTime)&&o.startTime>=0&&o.endTime>=o.startTime).sort((o,a)=>o.startTime-a.startTime):v(r,e),[r,e]),s=d.useMemo(()=>L(t,i),[i,t]),c=d.useMemo(()=>s<0?[]:t.slice(0,s+1).filter(o=>i>=o.startTime&&i<o.endTime),[i,t,s]);return{cues:t,activeIndex:s,activeCue:s>=0?t[s]??null:null,activeCues:c}}exports.parseSrt=b;exports.parseTimestampToSeconds=T;exports.parseTranscriptAuto=y;exports.parseVtt=N;exports.useGingerTranscriptSync=M;
|
|
8
8
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/transcript/parseTranscript.ts","../../src/transcript/useGingerTranscriptSync.ts"],"sourcesContent":["/**\n * A single timed transcript cue (SRT / WebVTT).\n */\nexport type TranscriptCue = {\n /** Start time in seconds. */\n startTime: number;\n /** End time in seconds. */\n endTime: number;\n text: string;\n /** Present for WebVTT cues that declare an identifier line. */\n id?: string;\n};\n\nfunction stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\").trim();\n}\n\n/** Parse `HH:MM:SS.mmm` / `HH:MM:SS,mmm` / `MM:SS.mmm` (WebVTT) to seconds. */\nexport function parseTimestampToSeconds(raw: string): number {\n const t = raw.trim().replace(\",\", \".\");\n const segs = t.split(\":\");\n if (segs.length === 3) {\n const h = Number(segs[0]);\n const m = Number(segs[1]);\n const sec = Number(segs[2]);\n if (![h, m, sec].every(Number.isFinite)) return Number.NaN;\n return h * 3600 + m * 60 + sec;\n }\n if (segs.length === 2) {\n const m = Number(segs[0]);\n const sec = Number(segs[1]);\n if (![m, sec].every(Number.isFinite)) return Number.NaN;\n return m * 60 + sec;\n }\n return Number.NaN;\n}\n\nfunction parseTimingLine(line: string): { start: string; end: string } | null {\n const arrow = line.indexOf(\"-->\");\n if (arrow < 0) return null;\n const start = line.slice(0, arrow).trim();\n const rest = line.slice(arrow + 3).trim();\n const endMatch = rest.match(/^(\\S+)/);\n if (!endMatch) return null;\n return { start, end: endMatch[1]! };\n}\n\n/**\n * Parse SubRip (`.srt`) content into ordered cues.\n */\nexport function parseSrt(srt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n const blocks = srt.replace(/\\r\\n/g, \"\\n\").split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const lines = block\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n if (lines.length === 0) continue;\n\n let i = 0;\n if (/^\\d+$/.test(lines[0]!)) {\n i = 1;\n }\n const timingLine = lines[i];\n if (!timingLine) continue;\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n const textLines = lines.slice(i + 1);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Parse WebVTT (`.vtt`) content into ordered cues. Ignores `NOTE` blocks and region/style headers.\n */\nexport function parseVtt(vtt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n let body = vtt.replace(/\\r\\n/g, \"\\n\");\n if (body.startsWith(\"WEBVTT\")) {\n const firstBlank = body.search(/\\n\\s*\\n/);\n body = firstBlank >= 0 ? body.slice(firstBlank).trim() : \"\";\n }\n\n const blocks = body.split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const rawLines = block.split(\"\\n\");\n const lines = rawLines.map((l) => l.trimEnd());\n if (lines.length === 0) continue;\n if (\n lines[0]!.startsWith(\"NOTE\") ||\n lines[0]!.startsWith(\"STYLE\") ||\n lines[0]!.startsWith(\"REGION\")\n ) {\n continue;\n }\n\n let i = 0;\n let id: string | undefined;\n let timingLine = lines[i]!;\n if (!timingLine.includes(\"-->\")) {\n id = lines[i]!;\n i += 1;\n timingLine = lines[i]!;\n }\n if (!timingLine?.includes(\"-->\")) continue;\n\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n\n const textLines = lines.slice(i + 1).filter((l) => l.trim().length > 0);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text, ...(id ? { id } : {}) });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Auto-detect format: WebVTT if the string starts with `WEBVTT`, otherwise SRT.\n */\nexport function parseTranscriptAuto(input: string): TranscriptCue[] {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"WEBVTT\")) {\n return parseVtt(input);\n }\n return parseSrt(input);\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\nimport { type TranscriptCue, parseSrt, parseTranscriptAuto, parseVtt } from \"./parseTranscript\";\n\nexport type UseGingerTranscriptSyncOptions = {\n transcript: string | TranscriptCue[];\n /** Default: `\"auto\"` (WEBVTT header → VTT, else SRT). Ignored when `transcript` is a cue array. */\n format?: \"vtt\" | \"srt\" | \"auto\";\n};\n\nexport type GingerTranscriptSyncState = {\n cues: TranscriptCue[];\n /** Last cue index where `startTime <= currentTime` (same scan as lyrics sync). */\n activeIndex: number;\n activeCue: TranscriptCue | null;\n /** All cues active at `currentTime` (`startTime <= t < endTime`), including overlaps. */\n activeCues: TranscriptCue[];\n};\n\nfunction parseString(transcript: string, format: \"vtt\" | \"srt\" | \"auto\"): TranscriptCue[] {\n if (format === \"vtt\") return parseVtt(transcript);\n if (format === \"srt\") return parseSrt(transcript);\n return parseTranscriptAuto(transcript);\n}\n\n/**\n * Syncs SRT / WebVTT transcript cues to the current Ginger playback time.\n *\n * ```ts\n * import { useGingerTranscriptSync } from \"@lucaismyname/ginger/transcript\";\n * ```\n */\nexport function useGingerTranscriptSync(\n options: UseGingerTranscriptSyncOptions,\n): GingerTranscriptSyncState {\n const { transcript, format = \"auto\" } = options;\n const { currentTime } = useGingerMedia();\n\n const cues = useMemo(() => {\n if (Array.isArray(transcript)) {\n return [...transcript]\n .filter(\n (c) =>\n Number.isFinite(c.startTime) &&\n Number.isFinite(c.endTime) &&\n c.startTime >= 0 &&\n c.endTime >= c.startTime,\n )\n .sort((a, b) => a.startTime - b.startTime);\n }\n return parseString(transcript, format);\n }, [transcript, format]);\n\n const activeIndex = useMemo(() => {\n for (let i = cues.length - 1; i >= 0; i -= 1) {\n if (currentTime >= cues[i]!.startTime) return i;\n }\n return -1;\n }, [currentTime, cues]);\n\n const activeCues = useMemo(() => {\n return cues.filter((c) => currentTime >= c.startTime && currentTime < c.endTime);\n }, [currentTime, cues]);\n\n return {\n cues,\n activeIndex,\n activeCue: activeIndex >= 0 ? (cues[activeIndex] ?? null) : null,\n activeCues,\n };\n}\n"],"names":["stripHtmlTags","text","parseTimestampToSeconds","raw","segs","h","m","sec","parseTimingLine","line","arrow","start","endMatch","parseSrt","srt","cues","blocks","block","lines","l","i","timingLine","times","startTime","endTime","textLines","a","b","parseVtt","vtt","body","firstBlank","id","parseTranscriptAuto","input","parseString","transcript","format","useGingerTranscriptSync","options","currentTime","useGingerMedia","useMemo","c","activeIndex","activeCues"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/transcript/parseTranscript.ts","../../src/transcript/useGingerTranscriptSync.ts"],"sourcesContent":["/**\n * A single timed transcript cue (SRT / WebVTT).\n */\nexport type TranscriptCue = {\n /** Start time in seconds. */\n startTime: number;\n /** End time in seconds. */\n endTime: number;\n text: string;\n /** Present for WebVTT cues that declare an identifier line. */\n id?: string;\n};\n\nfunction stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\").trim();\n}\n\n/** Parse `HH:MM:SS.mmm` / `HH:MM:SS,mmm` / `MM:SS.mmm` (WebVTT) to seconds. */\nexport function parseTimestampToSeconds(raw: string): number {\n const t = raw.trim().replace(\",\", \".\");\n const segs = t.split(\":\");\n if (segs.length === 3) {\n const h = Number(segs[0]);\n const m = Number(segs[1]);\n const sec = Number(segs[2]);\n if (![h, m, sec].every(Number.isFinite)) return Number.NaN;\n return h * 3600 + m * 60 + sec;\n }\n if (segs.length === 2) {\n const m = Number(segs[0]);\n const sec = Number(segs[1]);\n if (![m, sec].every(Number.isFinite)) return Number.NaN;\n return m * 60 + sec;\n }\n return Number.NaN;\n}\n\nfunction parseTimingLine(line: string): { start: string; end: string } | null {\n const arrow = line.indexOf(\"-->\");\n if (arrow < 0) return null;\n const start = line.slice(0, arrow).trim();\n const rest = line.slice(arrow + 3).trim();\n const endMatch = rest.match(/^(\\S+)/);\n if (!endMatch) return null;\n return { start, end: endMatch[1]! };\n}\n\n/**\n * Parse SubRip (`.srt`) content into ordered cues.\n */\nexport function parseSrt(srt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n const blocks = srt.replace(/\\r\\n/g, \"\\n\").split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const lines = block\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n if (lines.length === 0) continue;\n\n let i = 0;\n if (/^\\d+$/.test(lines[0]!)) {\n i = 1;\n }\n const timingLine = lines[i];\n if (!timingLine) continue;\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n const textLines = lines.slice(i + 1);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Parse WebVTT (`.vtt`) content into ordered cues. Ignores `NOTE` blocks and region/style headers.\n */\nexport function parseVtt(vtt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n let body = vtt.replace(/\\r\\n/g, \"\\n\");\n if (body.startsWith(\"WEBVTT\")) {\n const firstBlank = body.search(/\\n\\s*\\n/);\n body = firstBlank >= 0 ? body.slice(firstBlank).trim() : \"\";\n }\n\n const blocks = body.split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const rawLines = block.split(\"\\n\");\n const lines = rawLines.map((l) => l.trimEnd());\n if (lines.length === 0) continue;\n if (\n lines[0]!.startsWith(\"NOTE\") ||\n lines[0]!.startsWith(\"STYLE\") ||\n lines[0]!.startsWith(\"REGION\")\n ) {\n continue;\n }\n\n let i = 0;\n let id: string | undefined;\n let timingLine = lines[i]!;\n if (!timingLine.includes(\"-->\")) {\n id = lines[i]!;\n i += 1;\n timingLine = lines[i]!;\n }\n if (!timingLine?.includes(\"-->\")) continue;\n\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n\n const textLines = lines.slice(i + 1).filter((l) => l.trim().length > 0);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text, ...(id ? { id } : {}) });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Auto-detect format: WebVTT if the string starts with `WEBVTT`, otherwise SRT.\n */\nexport function parseTranscriptAuto(input: string): TranscriptCue[] {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"WEBVTT\")) {\n return parseVtt(input);\n }\n return parseSrt(input);\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\nimport { type TranscriptCue, parseSrt, parseTranscriptAuto, parseVtt } from \"./parseTranscript\";\n\nexport type UseGingerTranscriptSyncOptions = {\n transcript: string | TranscriptCue[];\n /** Default: `\"auto\"` (WEBVTT header → VTT, else SRT). Ignored when `transcript` is a cue array. */\n format?: \"vtt\" | \"srt\" | \"auto\";\n};\n\nexport type GingerTranscriptSyncState = {\n cues: TranscriptCue[];\n /** Last cue index where `startTime <= currentTime` (same scan as lyrics sync). */\n activeIndex: number;\n activeCue: TranscriptCue | null;\n /** All cues active at `currentTime` (`startTime <= t < endTime`), including overlaps. */\n activeCues: TranscriptCue[];\n};\n\nfunction parseString(transcript: string, format: \"vtt\" | \"srt\" | \"auto\"): TranscriptCue[] {\n if (format === \"vtt\") return parseVtt(transcript);\n if (format === \"srt\") return parseSrt(transcript);\n return parseTranscriptAuto(transcript);\n}\n\nfunction findLastCueIndexAtOrBeforeTime(cues: TranscriptCue[], currentTime: number): number {\n let low = 0;\n let high = cues.length - 1;\n let best = -1;\n\n while (low <= high) {\n const mid = low + Math.floor((high - low) / 2);\n const cue = cues[mid];\n if (!cue) break;\n if (cue.startTime <= currentTime) {\n best = mid;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return best;\n}\n\n/**\n * Syncs SRT / WebVTT transcript cues to the current Ginger playback time.\n *\n * ```ts\n * import { useGingerTranscriptSync } from \"@lucaismyname/ginger/transcript\";\n * ```\n */\nexport function useGingerTranscriptSync(\n options: UseGingerTranscriptSyncOptions,\n): GingerTranscriptSyncState {\n const { transcript, format = \"auto\" } = options;\n const { currentTime } = useGingerMedia();\n\n const cues = useMemo(() => {\n if (Array.isArray(transcript)) {\n return [...transcript]\n .filter(\n (c) =>\n Number.isFinite(c.startTime) &&\n Number.isFinite(c.endTime) &&\n c.startTime >= 0 &&\n c.endTime >= c.startTime,\n )\n .sort((a, b) => a.startTime - b.startTime);\n }\n return parseString(transcript, format);\n }, [transcript, format]);\n\n const activeIndex = useMemo(() => {\n return findLastCueIndexAtOrBeforeTime(cues, currentTime);\n }, [currentTime, cues]);\n\n const activeCues = useMemo(() => {\n if (activeIndex < 0) return [];\n return cues\n .slice(0, activeIndex + 1)\n .filter((c) => currentTime >= c.startTime && currentTime < c.endTime);\n }, [currentTime, cues, activeIndex]);\n\n return {\n cues,\n activeIndex,\n activeCue: activeIndex >= 0 ? (cues[activeIndex] ?? null) : null,\n activeCues,\n };\n}\n"],"names":["stripHtmlTags","text","parseTimestampToSeconds","raw","segs","h","m","sec","parseTimingLine","line","arrow","start","endMatch","parseSrt","srt","cues","blocks","block","lines","l","i","timingLine","times","startTime","endTime","textLines","a","b","parseVtt","vtt","body","firstBlank","id","parseTranscriptAuto","input","parseString","transcript","format","findLastCueIndexAtOrBeforeTime","currentTime","low","high","best","mid","cue","useGingerTranscriptSync","options","useGingerMedia","useMemo","c","activeIndex","activeCues"],"mappings":"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"}
|
package/dist/transcript/index.js
CHANGED
|
@@ -1,99 +1,104 @@
|
|
|
1
|
-
import { useMemo as
|
|
2
|
-
import { u as
|
|
3
|
-
function
|
|
1
|
+
import { useMemo as N } from "react";
|
|
2
|
+
import { u as L } from "../GingerSplitContexts-BzBExb95.js";
|
|
3
|
+
function p(n) {
|
|
4
4
|
return n.replace(/<[^>]+>/g, "").trim();
|
|
5
5
|
}
|
|
6
6
|
function T(n) {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
const
|
|
10
|
-
return [
|
|
7
|
+
const e = n.trim().replace(",", ".").split(":");
|
|
8
|
+
if (e.length === 3) {
|
|
9
|
+
const i = Number(e[0]), t = Number(e[1]), s = Number(e[2]);
|
|
10
|
+
return [i, t, s].every(Number.isFinite) ? i * 3600 + t * 60 + s : Number.NaN;
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
13
|
-
const
|
|
14
|
-
return [
|
|
12
|
+
if (e.length === 2) {
|
|
13
|
+
const i = Number(e[0]), t = Number(e[1]);
|
|
14
|
+
return [i, t].every(Number.isFinite) ? i * 60 + t : Number.NaN;
|
|
15
15
|
}
|
|
16
16
|
return Number.NaN;
|
|
17
17
|
}
|
|
18
18
|
function h(n) {
|
|
19
19
|
const r = n.indexOf("-->");
|
|
20
20
|
if (r < 0) return null;
|
|
21
|
-
const
|
|
22
|
-
return t ? { start:
|
|
21
|
+
const e = n.slice(0, r).trim(), t = n.slice(r + 3).trim().match(/^(\S+)/);
|
|
22
|
+
return t ? { start: e, end: t[1] } : null;
|
|
23
23
|
}
|
|
24
24
|
function g(n) {
|
|
25
|
-
const r = [],
|
|
25
|
+
const r = [], e = n.replace(/\r\n/g, `
|
|
26
26
|
`).split(/\n\s*\n/);
|
|
27
|
-
for (const
|
|
28
|
-
const t =
|
|
27
|
+
for (const i of e) {
|
|
28
|
+
const t = i.split(`
|
|
29
29
|
`).map((a) => a.trim()).filter((a) => a.length > 0);
|
|
30
30
|
if (t.length === 0) continue;
|
|
31
|
-
let
|
|
32
|
-
/^\d+$/.test(t[0]) && (
|
|
33
|
-
const c = t[
|
|
31
|
+
let s = 0;
|
|
32
|
+
/^\d+$/.test(t[0]) && (s = 1);
|
|
33
|
+
const c = t[s];
|
|
34
34
|
if (!c) continue;
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
37
|
-
const m = T(
|
|
35
|
+
const o = h(c);
|
|
36
|
+
if (!o) continue;
|
|
37
|
+
const m = T(o.start), u = T(o.end);
|
|
38
38
|
if (!Number.isFinite(m) || !Number.isFinite(u)) continue;
|
|
39
|
-
const l = t.slice(
|
|
39
|
+
const l = t.slice(s + 1), f = p(l.join(`
|
|
40
40
|
`));
|
|
41
41
|
f && r.push({ startTime: m, endTime: u, text: f });
|
|
42
42
|
}
|
|
43
|
-
return r.sort((
|
|
43
|
+
return r.sort((i, t) => i.startTime - t.startTime);
|
|
44
44
|
}
|
|
45
|
-
function
|
|
45
|
+
function x(n) {
|
|
46
46
|
const r = [];
|
|
47
|
-
let
|
|
47
|
+
let e = n.replace(/\r\n/g, `
|
|
48
48
|
`);
|
|
49
|
-
if (
|
|
50
|
-
const t =
|
|
51
|
-
|
|
49
|
+
if (e.startsWith("WEBVTT")) {
|
|
50
|
+
const t = e.search(/\n\s*\n/);
|
|
51
|
+
e = t >= 0 ? e.slice(t).trim() : "";
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
for (const t of
|
|
53
|
+
const i = e.split(/\n\s*\n/);
|
|
54
|
+
for (const t of i) {
|
|
55
55
|
const c = t.split(`
|
|
56
|
-
`).map((
|
|
56
|
+
`).map((d) => d.trimEnd());
|
|
57
57
|
if (c.length === 0 || c[0].startsWith("NOTE") || c[0].startsWith("STYLE") || c[0].startsWith("REGION"))
|
|
58
58
|
continue;
|
|
59
|
-
let
|
|
60
|
-
if (u.includes("-->") || (m = c[
|
|
59
|
+
let o = 0, m, u = c[o];
|
|
60
|
+
if (u.includes("-->") || (m = c[o], o += 1, u = c[o]), !(u != null && u.includes("-->"))) continue;
|
|
61
61
|
const l = h(u);
|
|
62
62
|
if (!l) continue;
|
|
63
63
|
const f = T(l.start), a = T(l.end);
|
|
64
64
|
if (!Number.isFinite(f) || !Number.isFinite(a)) continue;
|
|
65
|
-
const
|
|
65
|
+
const F = c.slice(o + 1).filter((d) => d.trim().length > 0), b = p(F.join(`
|
|
66
66
|
`));
|
|
67
67
|
b && r.push({ startTime: f, endTime: a, text: b, ...m ? { id: m } : {} });
|
|
68
68
|
}
|
|
69
|
-
return r.sort((t,
|
|
69
|
+
return r.sort((t, s) => t.startTime - s.startTime);
|
|
70
70
|
}
|
|
71
|
-
function
|
|
72
|
-
return n.trimStart().startsWith("WEBVTT") ?
|
|
71
|
+
function S(n) {
|
|
72
|
+
return n.trimStart().startsWith("WEBVTT") ? x(n) : g(n);
|
|
73
73
|
}
|
|
74
|
-
function
|
|
75
|
-
return r === "vtt" ?
|
|
74
|
+
function W(n, r) {
|
|
75
|
+
return r === "vtt" ? x(n) : r === "srt" ? g(n) : S(n);
|
|
76
76
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
function k(n, r) {
|
|
78
|
+
let e = 0, i = n.length - 1, t = -1;
|
|
79
|
+
for (; e <= i; ) {
|
|
80
|
+
const s = e + Math.floor((i - e) / 2), c = n[s];
|
|
81
|
+
if (!c) break;
|
|
82
|
+
c.startTime <= r ? (t = s, e = s + 1) : i = s - 1;
|
|
83
|
+
}
|
|
84
|
+
return t;
|
|
85
|
+
}
|
|
86
|
+
function E(n) {
|
|
87
|
+
const { transcript: r, format: e = "auto" } = n, { currentTime: i } = L(), t = N(() => Array.isArray(r) ? [...r].filter(
|
|
88
|
+
(o) => Number.isFinite(o.startTime) && Number.isFinite(o.endTime) && o.startTime >= 0 && o.endTime >= o.startTime
|
|
89
|
+
).sort((o, m) => o.startTime - m.startTime) : W(r, e), [r, e]), s = N(() => k(t, i), [i, t]), c = N(() => s < 0 ? [] : t.slice(0, s + 1).filter((o) => i >= o.startTime && i < o.endTime), [i, t, s]);
|
|
85
90
|
return {
|
|
86
91
|
cues: t,
|
|
87
|
-
activeIndex:
|
|
88
|
-
activeCue:
|
|
92
|
+
activeIndex: s,
|
|
93
|
+
activeCue: s >= 0 ? t[s] ?? null : null,
|
|
89
94
|
activeCues: c
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
97
|
export {
|
|
93
98
|
g as parseSrt,
|
|
94
99
|
T as parseTimestampToSeconds,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
S as parseTranscriptAuto,
|
|
101
|
+
x as parseVtt,
|
|
102
|
+
E as useGingerTranscriptSync
|
|
98
103
|
};
|
|
99
104
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/transcript/parseTranscript.ts","../../src/transcript/useGingerTranscriptSync.ts"],"sourcesContent":["/**\n * A single timed transcript cue (SRT / WebVTT).\n */\nexport type TranscriptCue = {\n /** Start time in seconds. */\n startTime: number;\n /** End time in seconds. */\n endTime: number;\n text: string;\n /** Present for WebVTT cues that declare an identifier line. */\n id?: string;\n};\n\nfunction stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\").trim();\n}\n\n/** Parse `HH:MM:SS.mmm` / `HH:MM:SS,mmm` / `MM:SS.mmm` (WebVTT) to seconds. */\nexport function parseTimestampToSeconds(raw: string): number {\n const t = raw.trim().replace(\",\", \".\");\n const segs = t.split(\":\");\n if (segs.length === 3) {\n const h = Number(segs[0]);\n const m = Number(segs[1]);\n const sec = Number(segs[2]);\n if (![h, m, sec].every(Number.isFinite)) return Number.NaN;\n return h * 3600 + m * 60 + sec;\n }\n if (segs.length === 2) {\n const m = Number(segs[0]);\n const sec = Number(segs[1]);\n if (![m, sec].every(Number.isFinite)) return Number.NaN;\n return m * 60 + sec;\n }\n return Number.NaN;\n}\n\nfunction parseTimingLine(line: string): { start: string; end: string } | null {\n const arrow = line.indexOf(\"-->\");\n if (arrow < 0) return null;\n const start = line.slice(0, arrow).trim();\n const rest = line.slice(arrow + 3).trim();\n const endMatch = rest.match(/^(\\S+)/);\n if (!endMatch) return null;\n return { start, end: endMatch[1]! };\n}\n\n/**\n * Parse SubRip (`.srt`) content into ordered cues.\n */\nexport function parseSrt(srt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n const blocks = srt.replace(/\\r\\n/g, \"\\n\").split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const lines = block\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n if (lines.length === 0) continue;\n\n let i = 0;\n if (/^\\d+$/.test(lines[0]!)) {\n i = 1;\n }\n const timingLine = lines[i];\n if (!timingLine) continue;\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n const textLines = lines.slice(i + 1);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Parse WebVTT (`.vtt`) content into ordered cues. Ignores `NOTE` blocks and region/style headers.\n */\nexport function parseVtt(vtt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n let body = vtt.replace(/\\r\\n/g, \"\\n\");\n if (body.startsWith(\"WEBVTT\")) {\n const firstBlank = body.search(/\\n\\s*\\n/);\n body = firstBlank >= 0 ? body.slice(firstBlank).trim() : \"\";\n }\n\n const blocks = body.split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const rawLines = block.split(\"\\n\");\n const lines = rawLines.map((l) => l.trimEnd());\n if (lines.length === 0) continue;\n if (\n lines[0]!.startsWith(\"NOTE\") ||\n lines[0]!.startsWith(\"STYLE\") ||\n lines[0]!.startsWith(\"REGION\")\n ) {\n continue;\n }\n\n let i = 0;\n let id: string | undefined;\n let timingLine = lines[i]!;\n if (!timingLine.includes(\"-->\")) {\n id = lines[i]!;\n i += 1;\n timingLine = lines[i]!;\n }\n if (!timingLine?.includes(\"-->\")) continue;\n\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n\n const textLines = lines.slice(i + 1).filter((l) => l.trim().length > 0);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text, ...(id ? { id } : {}) });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Auto-detect format: WebVTT if the string starts with `WEBVTT`, otherwise SRT.\n */\nexport function parseTranscriptAuto(input: string): TranscriptCue[] {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"WEBVTT\")) {\n return parseVtt(input);\n }\n return parseSrt(input);\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\nimport { type TranscriptCue, parseSrt, parseTranscriptAuto, parseVtt } from \"./parseTranscript\";\n\nexport type UseGingerTranscriptSyncOptions = {\n transcript: string | TranscriptCue[];\n /** Default: `\"auto\"` (WEBVTT header → VTT, else SRT). Ignored when `transcript` is a cue array. */\n format?: \"vtt\" | \"srt\" | \"auto\";\n};\n\nexport type GingerTranscriptSyncState = {\n cues: TranscriptCue[];\n /** Last cue index where `startTime <= currentTime` (same scan as lyrics sync). */\n activeIndex: number;\n activeCue: TranscriptCue | null;\n /** All cues active at `currentTime` (`startTime <= t < endTime`), including overlaps. */\n activeCues: TranscriptCue[];\n};\n\nfunction parseString(transcript: string, format: \"vtt\" | \"srt\" | \"auto\"): TranscriptCue[] {\n if (format === \"vtt\") return parseVtt(transcript);\n if (format === \"srt\") return parseSrt(transcript);\n return parseTranscriptAuto(transcript);\n}\n\n/**\n * Syncs SRT / WebVTT transcript cues to the current Ginger playback time.\n *\n * ```ts\n * import { useGingerTranscriptSync } from \"@lucaismyname/ginger/transcript\";\n * ```\n */\nexport function useGingerTranscriptSync(\n options: UseGingerTranscriptSyncOptions,\n): GingerTranscriptSyncState {\n const { transcript, format = \"auto\" } = options;\n const { currentTime } = useGingerMedia();\n\n const cues = useMemo(() => {\n if (Array.isArray(transcript)) {\n return [...transcript]\n .filter(\n (c) =>\n Number.isFinite(c.startTime) &&\n Number.isFinite(c.endTime) &&\n c.startTime >= 0 &&\n c.endTime >= c.startTime,\n )\n .sort((a, b) => a.startTime - b.startTime);\n }\n return parseString(transcript, format);\n }, [transcript, format]);\n\n const activeIndex = useMemo(() => {\n for (let i = cues.length - 1; i >= 0; i -= 1) {\n if (currentTime >= cues[i]!.startTime) return i;\n }\n return -1;\n }, [currentTime, cues]);\n\n const activeCues = useMemo(() => {\n return cues.filter((c) => currentTime >= c.startTime && currentTime < c.endTime);\n }, [currentTime, cues]);\n\n return {\n cues,\n activeIndex,\n activeCue: activeIndex >= 0 ? (cues[activeIndex] ?? null) : null,\n activeCues,\n };\n}\n"],"names":["stripHtmlTags","text","parseTimestampToSeconds","raw","segs","h","m","sec","parseTimingLine","line","arrow","start","endMatch","parseSrt","srt","cues","blocks","block","lines","l","i","timingLine","times","startTime","endTime","textLines","a","b","parseVtt","vtt","body","firstBlank","id","parseTranscriptAuto","input","parseString","transcript","format","useGingerTranscriptSync","options","currentTime","useGingerMedia","useMemo","c","activeIndex","activeCues"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/transcript/parseTranscript.ts","../../src/transcript/useGingerTranscriptSync.ts"],"sourcesContent":["/**\n * A single timed transcript cue (SRT / WebVTT).\n */\nexport type TranscriptCue = {\n /** Start time in seconds. */\n startTime: number;\n /** End time in seconds. */\n endTime: number;\n text: string;\n /** Present for WebVTT cues that declare an identifier line. */\n id?: string;\n};\n\nfunction stripHtmlTags(text: string): string {\n return text.replace(/<[^>]+>/g, \"\").trim();\n}\n\n/** Parse `HH:MM:SS.mmm` / `HH:MM:SS,mmm` / `MM:SS.mmm` (WebVTT) to seconds. */\nexport function parseTimestampToSeconds(raw: string): number {\n const t = raw.trim().replace(\",\", \".\");\n const segs = t.split(\":\");\n if (segs.length === 3) {\n const h = Number(segs[0]);\n const m = Number(segs[1]);\n const sec = Number(segs[2]);\n if (![h, m, sec].every(Number.isFinite)) return Number.NaN;\n return h * 3600 + m * 60 + sec;\n }\n if (segs.length === 2) {\n const m = Number(segs[0]);\n const sec = Number(segs[1]);\n if (![m, sec].every(Number.isFinite)) return Number.NaN;\n return m * 60 + sec;\n }\n return Number.NaN;\n}\n\nfunction parseTimingLine(line: string): { start: string; end: string } | null {\n const arrow = line.indexOf(\"-->\");\n if (arrow < 0) return null;\n const start = line.slice(0, arrow).trim();\n const rest = line.slice(arrow + 3).trim();\n const endMatch = rest.match(/^(\\S+)/);\n if (!endMatch) return null;\n return { start, end: endMatch[1]! };\n}\n\n/**\n * Parse SubRip (`.srt`) content into ordered cues.\n */\nexport function parseSrt(srt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n const blocks = srt.replace(/\\r\\n/g, \"\\n\").split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const lines = block\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n if (lines.length === 0) continue;\n\n let i = 0;\n if (/^\\d+$/.test(lines[0]!)) {\n i = 1;\n }\n const timingLine = lines[i];\n if (!timingLine) continue;\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n const textLines = lines.slice(i + 1);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Parse WebVTT (`.vtt`) content into ordered cues. Ignores `NOTE` blocks and region/style headers.\n */\nexport function parseVtt(vtt: string): TranscriptCue[] {\n const cues: TranscriptCue[] = [];\n let body = vtt.replace(/\\r\\n/g, \"\\n\");\n if (body.startsWith(\"WEBVTT\")) {\n const firstBlank = body.search(/\\n\\s*\\n/);\n body = firstBlank >= 0 ? body.slice(firstBlank).trim() : \"\";\n }\n\n const blocks = body.split(/\\n\\s*\\n/);\n\n for (const block of blocks) {\n const rawLines = block.split(\"\\n\");\n const lines = rawLines.map((l) => l.trimEnd());\n if (lines.length === 0) continue;\n if (\n lines[0]!.startsWith(\"NOTE\") ||\n lines[0]!.startsWith(\"STYLE\") ||\n lines[0]!.startsWith(\"REGION\")\n ) {\n continue;\n }\n\n let i = 0;\n let id: string | undefined;\n let timingLine = lines[i]!;\n if (!timingLine.includes(\"-->\")) {\n id = lines[i]!;\n i += 1;\n timingLine = lines[i]!;\n }\n if (!timingLine?.includes(\"-->\")) continue;\n\n const times = parseTimingLine(timingLine);\n if (!times) continue;\n const startTime = parseTimestampToSeconds(times.start);\n const endTime = parseTimestampToSeconds(times.end);\n if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;\n\n const textLines = lines.slice(i + 1).filter((l) => l.trim().length > 0);\n const text = stripHtmlTags(textLines.join(\"\\n\"));\n if (!text) continue;\n cues.push({ startTime, endTime, text, ...(id ? { id } : {}) });\n }\n\n return cues.sort((a, b) => a.startTime - b.startTime);\n}\n\n/**\n * Auto-detect format: WebVTT if the string starts with `WEBVTT`, otherwise SRT.\n */\nexport function parseTranscriptAuto(input: string): TranscriptCue[] {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"WEBVTT\")) {\n return parseVtt(input);\n }\n return parseSrt(input);\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\nimport { type TranscriptCue, parseSrt, parseTranscriptAuto, parseVtt } from \"./parseTranscript\";\n\nexport type UseGingerTranscriptSyncOptions = {\n transcript: string | TranscriptCue[];\n /** Default: `\"auto\"` (WEBVTT header → VTT, else SRT). Ignored when `transcript` is a cue array. */\n format?: \"vtt\" | \"srt\" | \"auto\";\n};\n\nexport type GingerTranscriptSyncState = {\n cues: TranscriptCue[];\n /** Last cue index where `startTime <= currentTime` (same scan as lyrics sync). */\n activeIndex: number;\n activeCue: TranscriptCue | null;\n /** All cues active at `currentTime` (`startTime <= t < endTime`), including overlaps. */\n activeCues: TranscriptCue[];\n};\n\nfunction parseString(transcript: string, format: \"vtt\" | \"srt\" | \"auto\"): TranscriptCue[] {\n if (format === \"vtt\") return parseVtt(transcript);\n if (format === \"srt\") return parseSrt(transcript);\n return parseTranscriptAuto(transcript);\n}\n\nfunction findLastCueIndexAtOrBeforeTime(cues: TranscriptCue[], currentTime: number): number {\n let low = 0;\n let high = cues.length - 1;\n let best = -1;\n\n while (low <= high) {\n const mid = low + Math.floor((high - low) / 2);\n const cue = cues[mid];\n if (!cue) break;\n if (cue.startTime <= currentTime) {\n best = mid;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return best;\n}\n\n/**\n * Syncs SRT / WebVTT transcript cues to the current Ginger playback time.\n *\n * ```ts\n * import { useGingerTranscriptSync } from \"@lucaismyname/ginger/transcript\";\n * ```\n */\nexport function useGingerTranscriptSync(\n options: UseGingerTranscriptSyncOptions,\n): GingerTranscriptSyncState {\n const { transcript, format = \"auto\" } = options;\n const { currentTime } = useGingerMedia();\n\n const cues = useMemo(() => {\n if (Array.isArray(transcript)) {\n return [...transcript]\n .filter(\n (c) =>\n Number.isFinite(c.startTime) &&\n Number.isFinite(c.endTime) &&\n c.startTime >= 0 &&\n c.endTime >= c.startTime,\n )\n .sort((a, b) => a.startTime - b.startTime);\n }\n return parseString(transcript, format);\n }, [transcript, format]);\n\n const activeIndex = useMemo(() => {\n return findLastCueIndexAtOrBeforeTime(cues, currentTime);\n }, [currentTime, cues]);\n\n const activeCues = useMemo(() => {\n if (activeIndex < 0) return [];\n return cues\n .slice(0, activeIndex + 1)\n .filter((c) => currentTime >= c.startTime && currentTime < c.endTime);\n }, [currentTime, cues, activeIndex]);\n\n return {\n cues,\n activeIndex,\n activeCue: activeIndex >= 0 ? (cues[activeIndex] ?? null) : null,\n activeCues,\n };\n}\n"],"names":["stripHtmlTags","text","parseTimestampToSeconds","raw","segs","h","m","sec","parseTimingLine","line","arrow","start","endMatch","parseSrt","srt","cues","blocks","block","lines","l","i","timingLine","times","startTime","endTime","textLines","a","b","parseVtt","vtt","body","firstBlank","id","parseTranscriptAuto","input","parseString","transcript","format","findLastCueIndexAtOrBeforeTime","currentTime","low","high","best","mid","cue","useGingerTranscriptSync","options","useGingerMedia","useMemo","c","activeIndex","activeCues"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGingerTranscriptSync.d.ts","sourceRoot":"","sources":["../../src/transcript/useGingerTranscriptSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAA2C,MAAM,mBAAmB,CAAC;AAEhG,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CAAC;IACrC,mGAAmG;IACnG,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,yFAAyF;IACzF,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B,CAAC;
|
|
1
|
+
{"version":3,"file":"useGingerTranscriptSync.d.ts","sourceRoot":"","sources":["../../src/transcript/useGingerTranscriptSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAA2C,MAAM,mBAAmB,CAAC;AAEhG,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CAAC;IACrC,mGAAmG;IACnG,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,yFAAyF;IACzF,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B,CAAC;AA4BF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,yBAAyB,CAoC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGingerTranscriptSync.test.d.ts","sourceRoot":"","sources":["../../src/transcript/useGingerTranscriptSync.test.tsx"],"names":[],"mappings":""}
|
package/dist/waveform/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("react");function F(){return typeof window>"u"?null:window.AudioContext??window.webkitAudioContext??null}function S(n,t=64,a={}){const{maxBuckets:r=512,maxSamplesPerBucket:o=2e4}=a,[e,s]=p.useState({peaks:[],isLoading:!1,error:null});return p.useEffect(()=>{if(!n){s({peaks:[],isLoading:!1,error:null});return}if(typeof window>"u"){s({peaks:[],isLoading:!1,error:null});return}let l=!1;return s(i=>({...i,isLoading:!0,error:null})),(async()=>{try{const i=await fetch(n);if(!i.ok)throw new Error(`Fetch failed: ${i.status} ${i.statusText}`);const u=await i.arrayBuffer(),d=F();if(!d)throw new Error("Web Audio API is not available");const c=new d;try{const h=(await c.decodeAudioData(u)).getChannelData(0),w=Math.min(Math.max(1,t),Math.max(1,r)),m=Math.max(1,Math.floor(h.length/w)),f=[];for(let M=0;M<w;M+=1){let y=0;const x=M*m,C=Math.min(h.length,x+m),k=C-x,B=k>o?Math.ceil(k/o):1;for(let A=x;A<C;A+=B)y=Math.max(y,Math.abs(h[A]??0));f.push(y)}l||s({peaks:f,isLoading:!1,error:null})}finally{await c.close().catch(()=>{})}}catch(i){l||s({peaks:[],isLoading:!1,error:i instanceof Error?i.message:"Failed to decode peaks"})}})(),()=>{l=!0}},[t,n,r,o]),e}function E(n,t){const a=n.length;if(a!==t.length||a<2||a&a-1)throw new Error("fftInPlace: length must be equal powers of 2 >= 2");let r=0;for(let o=0;o<a-1;o+=1){if(o<r){const s=n[o],l=t[o];n[o]=n[r],t[o]=t[r],n[r]=s,t[r]=l}let e=a>>1;for(;e<=r;)r-=e,e>>=1;r+=e}for(let o=2;o<=a;o<<=1){const e=-2*Math.PI/o,s=Math.cos(e),l=Math.sin(e);for(let i=0;i<a;i+=o){let u=1,d=0;const c=o>>1;for(let g=0;g<c;g+=1){const h=i+g,w=h+c,m=u*n[w]-d*t[w],f=u*t[w]+d*n[w];n[w]=n[h]-m,t[w]=t[h]-f,n[h]=n[h]+m,t[h]=t[h]+f;const M=u*s-d*l,y=u*l+d*s;u=M,d=y}}}}function P(n){const t=n.length;if(t<2||t&t-1)throw new Error("realFftMagnitudes: length must be a power of 2 >= 2");const a=new Float64Array(t),r=new Float64Array(t);for(let e=0;e<t;e+=1)a[e]=n[e];E(a,r);const o=new Float64Array(t>>1);for(let e=0;e<t>>1;e+=1)o[e]=Math.hypot(a[e],r[e]);return o}function z(n){const t=new Float64Array(n);if(n===1)return t[0]=1,t;const a=n-1;for(let r=0;r<n;r+=1)t[r]=.5*(1-Math.cos(2*Math.PI*r/a));return t}function v(n){const t=2**Math.round(Math.log2(n));return Math.min(8192,Math.max(32,t))}function I(n){const{numberOfChannels:t,length:a}=n;if(t===1)return n.getChannelData(0);const r=new Float32Array(a),o=1/t;for(let e=0;e<t;e+=1){const s=n.getChannelData(e);for(let l=0;l<a;l+=1)r[l]+=s[l]*o}return r}function D(n,t){if(t==="mix")return I(n);const a=Math.max(0,Math.min(n.numberOfChannels-1,t));return n.getChannelData(a)}function q(n,t,a){const r=[],o=n.length;if(o===0){for(let s=0;s<t;s+=1)r.push(Array.from({length:a},()=>0));return r}const e=o/t;for(let s=0;s<t;s+=1){const l=[],i=Math.floor(s*e),u=Math.floor((s+1)*e),c=Math.max(1,u-i)/a;for(let g=0;g<a;g+=1){const h=Math.floor(i+g*c),w=Math.min(u,Math.floor(i+(g+1)*c));let m=0;for(let f=h;f<w;f+=1)m=Math.max(m,Math.abs(n[f]??0));l.push(m)}r.push(l)}return r}function R(n,t,a,r){const o=[];let e=1e-12;const s=n.length,l=v(a),i=l>>1,u=Math.min(r,i),d=z(l);if(s<l){for(let c=0;c<t;c+=1)o.push(Array.from({length:u},()=>0));return{rows:o,maxMag:1}}for(let c=0;c<t;c+=1){const g=t<=1?0:Math.min(Math.floor(c*(s-l)/(t-1)),s-l),h=new Float64Array(l);for(let f=0;f<l;f+=1)h[f]=(n[g+f]??0)*(d[f]??0);const w=P(h),m=[];for(let f=0;f<u;f+=1){const M=w[f]??0;m.push(M),e=Math.max(e,M)}o.push(m)}return{rows:o,maxMag:e}}function L(n,t={}){const a=Math.max(1,t.timeSlices??128),r=Math.max(1,t.samplesPerSlice??8),o=!!t.spectrogram,e=t.fftSize??1024,s=Math.max(1,t.frequencyBins??256),l=t.channel??0,i=D(n,l),u=q(i,a,r),d={duration:n.duration,sampleRate:n.sampleRate,amplitudeGrid:u};if(o){const{rows:c,maxMag:g}=R(i,a,e,s),h=g>0?1/g:1;d.spectrogram=c.map(w=>w.map(m=>m*h))}return d}async function b(n,t={}){const a=await fetch(n);if(!a.ok)throw new Error(`Fetch failed: ${a.status} ${a.statusText}`);const r=await a.arrayBuffer(),o=F();if(!o)throw new Error("Web Audio API is not available");const e=new o;try{const s=await e.decodeAudioData(r.slice(0));return L(s,t)}finally{await e.close()}}function $(n,t={}){const{timeSlices:a,samplesPerSlice:r,spectrogram:o,fftSize:e,frequencyBins:s,channel:l}=t,[i,u]=p.useState({data:null,isLoading:!1,error:null});return p.useEffect(()=>{if(!n){u({data:null,isLoading:!1,error:null});return}if(typeof window>"u"){u({data:null,isLoading:!1,error:null});return}let d=!1;return u(c=>({...c,isLoading:!0,error:null})),(async()=>{try{const c=await b(n,{timeSlices:a,samplesPerSlice:r,spectrogram:o,fftSize:e,frequencyBins:s,channel:l});d||u({data:c,isLoading:!1,error:null})}catch(c){d||u({data:null,isLoading:!1,error:c instanceof Error?c.message:"Failed to analyze audio file"})}})(),()=>{d=!0}},[n,a,r,o,e,s,l]),i}exports.analyzeAudioBuffer=L;exports.analyzeAudioFile=b;exports.useAudioFileAnalysis=$;exports.useAudioPeaks=S;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/waveform/getAudioContextConstructor.ts","../../src/waveform/useAudioPeaks.ts","../../src/internal/fft.ts","../../src/waveform/analyzeAudioFile.ts","../../src/waveform/useAudioFileAnalysis.ts"],"sourcesContent":["/**\n * Resolves the runtime `AudioContext` constructor, including legacy `webkitAudioContext` (Safari).\n * Returns `null` when `window` is unavailable or Web Audio is not implemented.\n */\nexport function getAudioContextConstructor():\n | (new (\n contextOptions?: AudioContextOptions,\n ) => AudioContext)\n | null {\n if (typeof window === \"undefined\") return null;\n const C =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n return C ?? null;\n}\n","import { useEffect, useState } from \"react\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type UseAudioPeaksState = {\n peaks: number[];\n isLoading: boolean;\n error: string | null;\n};\n\nexport function useAudioPeaks(\n fileUrl: string | null | undefined,\n buckets = 64,\n): UseAudioPeaksState {\n const [state, setState] = useState<UseAudioPeaksState>({\n peaks: [],\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n void (async () => {\n try {\n const response = await fetch(fileUrl);\n if (!response.ok)\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n const buffer = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const audioBuffer = await audioContext.decodeAudioData(buffer);\n const channel = audioBuffer.getChannelData(0);\n const step = Math.max(1, Math.floor(channel.length / buckets));\n const peaks: number[] = [];\n for (let i = 0; i < buckets; i += 1) {\n let max = 0;\n const start = i * step;\n const end = Math.min(channel.length, start + step);\n for (let j = start; j < end; j += 1) {\n max = Math.max(max, Math.abs(channel[j] ?? 0));\n }\n peaks.push(max);\n }\n if (!cancelled) {\n setState({ peaks, isLoading: false, error: null });\n }\n } finally {\n await audioContext.close().catch(() => {});\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n peaks: [],\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to decode peaks\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [buckets, fileUrl]);\n\n return state;\n}\n","/** In-place radix-2 Cooley–Tukey FFT; `length` must be a power of 2 and >= 2. */\nexport function fftInPlace(re: Float64Array, im: Float64Array): void {\n const n = re.length;\n if (n !== im.length || n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"fftInPlace: length must be equal powers of 2 >= 2\");\n }\n\n let j = 0;\n for (let i = 0; i < n - 1; i += 1) {\n if (i < j) {\n const tr = re[i]!;\n const ti = im[i]!;\n re[i] = re[j]!;\n im[i] = im[j]!;\n re[j] = tr;\n im[j] = ti;\n }\n let k = n >> 1;\n while (k <= j) {\n j -= k;\n k >>= 1;\n }\n j += k;\n }\n\n for (let len = 2; len <= n; len <<= 1) {\n const ang = (-2 * Math.PI) / len;\n const wlenR = Math.cos(ang);\n const wlenI = Math.sin(ang);\n for (let i = 0; i < n; i += len) {\n let wr = 1;\n let wi = 0;\n const half = len >> 1;\n for (let k = 0; k < half; k += 1) {\n const u = i + k;\n const v = u + half;\n const tr = wr * re[v]! - wi * im[v]!;\n const ti = wr * im[v]! + wi * re[v]!;\n re[v] = re[u]! - tr;\n im[v] = im[u]! - ti;\n re[u] = re[u]! + tr;\n im[u] = im[u]! + ti;\n const nwr = wr * wlenR - wi * wlenI;\n const nwi = wr * wlenI + wi * wlenR;\n wr = nwr;\n wi = nwi;\n }\n }\n }\n}\n\n/** Magnitude spectrum for real input: length `n` (power of 2). Returns `n/2` magnitudes for bins 0..n/2-1. */\nexport function realFftMagnitudes(samples: Float64Array): Float64Array {\n const n = samples.length;\n if (n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"realFftMagnitudes: length must be a power of 2 >= 2\");\n }\n const re = new Float64Array(n);\n const im = new Float64Array(n);\n for (let i = 0; i < n; i += 1) re[i] = samples[i]!;\n fftInPlace(re, im);\n const out = new Float64Array(n >> 1);\n for (let k = 0; k < n >> 1; k += 1) {\n out[k] = Math.hypot(re[k]!, im[k]!);\n }\n return out;\n}\n\nexport function hanningWindow(length: number): Float64Array {\n const w = new Float64Array(length);\n if (length === 1) {\n w[0] = 1;\n return w;\n }\n const denom = length - 1;\n for (let i = 0; i < length; i += 1) {\n w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / denom));\n }\n return w;\n}\n\nexport function clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(8192, Math.max(32, p));\n}\n","import { clampFftSize, hanningWindow, realFftMagnitudes } from \"../internal/fft\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type AnalyzeAudioFileOptions = {\n /** Number of time rows in the amplitude grid (and spectrogram if enabled). Default 128. */\n timeSlices?: number;\n /** Sub-buckets per time slice for the amplitude grid. Default 8. */\n samplesPerSlice?: number;\n /** When true, include `spectrogram` using windowed FFT per time slice. Default false. */\n spectrogram?: boolean;\n /** FFT length for spectrogram (power of 2). Default 1024. */\n fftSize?: number;\n /** Number of frequency bins to keep per row (first bins; capped by fftSize/2). Default 256. */\n frequencyBins?: number;\n /** Channel index, or `\"mix\"` for equal mix of all channels. Default 0. */\n channel?: number | \"mix\";\n};\n\nexport type AudioFileAnalysis = {\n duration: number;\n sampleRate: number;\n /** Peak amplitudes in [0, 1]: `timeSlices` rows × `samplesPerSlice` columns. */\n amplitudeGrid: number[][];\n /** Optional magnitude spectrogram rows, each length `frequencyBins`, normalized to [0, 1] globally. */\n spectrogram?: number[][];\n};\n\nfunction mixChannels(buffer: AudioBuffer): Float32Array {\n const { numberOfChannels, length } = buffer;\n if (numberOfChannels === 1) {\n return buffer.getChannelData(0);\n }\n const out = new Float32Array(length);\n const scale = 1 / numberOfChannels;\n for (let c = 0; c < numberOfChannels; c += 1) {\n const ch = buffer.getChannelData(c);\n for (let i = 0; i < length; i += 1) {\n out[i] += ch[i]! * scale;\n }\n }\n return out;\n}\n\nfunction getChannel(buffer: AudioBuffer, channel: number | \"mix\"): Float32Array {\n if (channel === \"mix\") return mixChannels(buffer);\n const idx = Math.max(0, Math.min(buffer.numberOfChannels - 1, channel));\n return buffer.getChannelData(idx);\n}\n\nfunction buildAmplitudeGrid(\n channel: Float32Array,\n timeSlices: number,\n samplesPerSlice: number,\n): number[][] {\n const grid: number[][] = [];\n const len = channel.length;\n if (len === 0) {\n for (let t = 0; t < timeSlices; t += 1) {\n grid.push(Array.from({ length: samplesPerSlice }, () => 0));\n }\n return grid;\n }\n\n const segmentLen = len / timeSlices;\n\n for (let t = 0; t < timeSlices; t += 1) {\n const row: number[] = [];\n const segStart = Math.floor(t * segmentLen);\n const segEnd = Math.floor((t + 1) * segmentLen);\n const segLen = Math.max(1, segEnd - segStart);\n const subLen = segLen / samplesPerSlice;\n\n for (let s = 0; s < samplesPerSlice; s += 1) {\n const a = Math.floor(segStart + s * subLen);\n const b = Math.min(segEnd, Math.floor(segStart + (s + 1) * subLen));\n let peak = 0;\n for (let i = a; i < b; i += 1) {\n peak = Math.max(peak, Math.abs(channel[i] ?? 0));\n }\n row.push(peak);\n }\n grid.push(row);\n }\n return grid;\n}\n\nfunction buildSpectrogram(\n channel: Float32Array,\n timeSlices: number,\n fftSize: number,\n frequencyBins: number,\n): { rows: number[][]; maxMag: number } {\n const rows: number[][] = [];\n let maxMag = 1e-12;\n const len = channel.length;\n const n = clampFftSize(fftSize);\n const half = n >> 1;\n const bins = Math.min(frequencyBins, half);\n const window = hanningWindow(n);\n\n if (len < n) {\n for (let t = 0; t < timeSlices; t += 1) {\n rows.push(Array.from({ length: bins }, () => 0));\n }\n return { rows, maxMag: 1 };\n }\n\n for (let t = 0; t < timeSlices; t += 1) {\n const start =\n timeSlices <= 1 ? 0 : Math.min(Math.floor((t * (len - n)) / (timeSlices - 1)), len - n);\n const frame = new Float64Array(n);\n for (let i = 0; i < n; i += 1) {\n frame[i] = (channel[start + i] ?? 0) * (window[i] ?? 0);\n }\n const mags = realFftMagnitudes(frame);\n const row: number[] = [];\n for (let k = 0; k < bins; k += 1) {\n const v = mags[k] ?? 0;\n row.push(v);\n maxMag = Math.max(maxMag, v);\n }\n rows.push(row);\n }\n\n return { rows, maxMag };\n}\n\n/**\n * Decodes an `AudioBuffer` into visualization-friendly grids (no network).\n */\nexport function analyzeAudioBuffer(\n buffer: AudioBuffer,\n options: AnalyzeAudioFileOptions = {},\n): AudioFileAnalysis {\n const timeSlices = Math.max(1, options.timeSlices ?? 128);\n const samplesPerSlice = Math.max(1, options.samplesPerSlice ?? 8);\n const wantSpec = Boolean(options.spectrogram);\n const fftSize = options.fftSize ?? 1024;\n const frequencyBins = Math.max(1, options.frequencyBins ?? 256);\n const channel = options.channel ?? 0;\n\n const data = getChannel(buffer, channel);\n const amplitudeGrid = buildAmplitudeGrid(data, timeSlices, samplesPerSlice);\n\n const result: AudioFileAnalysis = {\n duration: buffer.duration,\n sampleRate: buffer.sampleRate,\n amplitudeGrid,\n };\n\n if (wantSpec) {\n const { rows, maxMag } = buildSpectrogram(data, timeSlices, fftSize, frequencyBins);\n const norm = maxMag > 0 ? 1 / maxMag : 1;\n result.spectrogram = rows.map((row) => row.map((v) => v * norm));\n }\n\n return result;\n}\n\n/**\n * Fetches a URL, decodes audio to an `AudioBuffer`, runs {@link analyzeAudioBuffer}, then closes the temporary `AudioContext`.\n */\nexport async function analyzeAudioFile(\n fileUrl: string,\n options: AnalyzeAudioFileOptions = {},\n): Promise<AudioFileAnalysis> {\n const response = await fetch(fileUrl);\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n }\n const raw = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const buffer = await audioContext.decodeAudioData(raw.slice(0));\n return analyzeAudioBuffer(buffer, options);\n } finally {\n await audioContext.close();\n }\n}\n","import { useEffect, useState } from \"react\";\nimport {\n type AnalyzeAudioFileOptions,\n type AudioFileAnalysis,\n analyzeAudioFile,\n} from \"./analyzeAudioFile\";\n\nexport type UseAudioFileAnalysisState = {\n data: AudioFileAnalysis | null;\n isLoading: boolean;\n error: string | null;\n};\n\n/** When extending {@link AnalyzeAudioFileOptions}, thread new fields through the destructuring below and the effect dependency array. */\nexport function useAudioFileAnalysis(\n fileUrl: string | null | undefined,\n options: AnalyzeAudioFileOptions = {},\n): UseAudioFileAnalysisState {\n const { timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel } = options;\n\n const [state, setState] = useState<UseAudioFileAnalysisState>({\n data: null,\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n\n void (async () => {\n try {\n const data = await analyzeAudioFile(fileUrl, {\n timeSlices,\n samplesPerSlice,\n spectrogram,\n fftSize,\n frequencyBins,\n channel,\n });\n if (!cancelled) {\n setState({ data, isLoading: false, error: null });\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n data: null,\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to analyze audio file\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [fileUrl, timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel]);\n\n return state;\n}\n"],"names":["getAudioContextConstructor","useAudioPeaks","fileUrl","buckets","state","setState","useState","useEffect","cancelled","prev","response","buffer","Context","audioContext","channel","step","peaks","i","max","start","end","j","error","fftInPlace","re","im","n","tr","ti","k","len","ang","wlenR","wlenI","wr","wi","half","u","v","nwr","nwi","realFftMagnitudes","samples","out","hanningWindow","length","w","denom","clampFftSize","p","mixChannels","numberOfChannels","scale","c","ch","getChannel","idx","buildAmplitudeGrid","timeSlices","samplesPerSlice","grid","t","segmentLen","row","segStart","segEnd","subLen","s","a","b","peak","buildSpectrogram","fftSize","frequencyBins","rows","maxMag","bins","window","frame","mags","analyzeAudioBuffer","options","wantSpec","data","amplitudeGrid","result","norm","analyzeAudioFile","raw","useAudioFileAnalysis","spectrogram"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/waveform/getAudioContextConstructor.ts","../../src/waveform/useAudioPeaks.ts","../../src/internal/fft.ts","../../src/waveform/analyzeAudioFile.ts","../../src/waveform/useAudioFileAnalysis.ts"],"sourcesContent":["/**\n * Resolves the runtime `AudioContext` constructor, including legacy `webkitAudioContext` (Safari).\n * Returns `null` when `window` is unavailable or Web Audio is not implemented.\n */\nexport function getAudioContextConstructor():\n | (new (\n contextOptions?: AudioContextOptions,\n ) => AudioContext)\n | null {\n if (typeof window === \"undefined\") return null;\n const C =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n return C ?? null;\n}\n","import { useEffect, useState } from \"react\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type UseAudioPeaksState = {\n peaks: number[];\n isLoading: boolean;\n error: string | null;\n};\n\nexport type UseAudioPeaksOptions = {\n /**\n * Hard cap for computed buckets to avoid very expensive loops.\n * Default: 512.\n */\n maxBuckets?: number;\n /**\n * Maximum number of samples scanned per bucket.\n * Large files are sampled with a stride once this limit is exceeded.\n * Default: 20_000.\n */\n maxSamplesPerBucket?: number;\n};\n\nexport function useAudioPeaks(\n fileUrl: string | null | undefined,\n buckets = 64,\n options: UseAudioPeaksOptions = {},\n): UseAudioPeaksState {\n const { maxBuckets = 512, maxSamplesPerBucket = 20_000 } = options;\n const [state, setState] = useState<UseAudioPeaksState>({\n peaks: [],\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n void (async () => {\n try {\n const response = await fetch(fileUrl);\n if (!response.ok)\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n const buffer = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const audioBuffer = await audioContext.decodeAudioData(buffer);\n const channel = audioBuffer.getChannelData(0);\n const safeBuckets = Math.min(Math.max(1, buckets), Math.max(1, maxBuckets));\n const step = Math.max(1, Math.floor(channel.length / safeBuckets));\n const peaks: number[] = [];\n for (let i = 0; i < safeBuckets; i += 1) {\n let max = 0;\n const start = i * step;\n const end = Math.min(channel.length, start + step);\n const sampleCount = end - start;\n const stride =\n sampleCount > maxSamplesPerBucket ? Math.ceil(sampleCount / maxSamplesPerBucket) : 1;\n for (let j = start; j < end; j += stride) {\n max = Math.max(max, Math.abs(channel[j] ?? 0));\n }\n peaks.push(max);\n }\n if (!cancelled) {\n setState({ peaks, isLoading: false, error: null });\n }\n } finally {\n await audioContext.close().catch(() => {});\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n peaks: [],\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to decode peaks\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [buckets, fileUrl, maxBuckets, maxSamplesPerBucket]);\n\n return state;\n}\n","/** In-place radix-2 Cooley–Tukey FFT; `length` must be a power of 2 and >= 2. */\nexport function fftInPlace(re: Float64Array, im: Float64Array): void {\n const n = re.length;\n if (n !== im.length || n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"fftInPlace: length must be equal powers of 2 >= 2\");\n }\n\n let j = 0;\n for (let i = 0; i < n - 1; i += 1) {\n if (i < j) {\n const tr = re[i]!;\n const ti = im[i]!;\n re[i] = re[j]!;\n im[i] = im[j]!;\n re[j] = tr;\n im[j] = ti;\n }\n let k = n >> 1;\n while (k <= j) {\n j -= k;\n k >>= 1;\n }\n j += k;\n }\n\n for (let len = 2; len <= n; len <<= 1) {\n const ang = (-2 * Math.PI) / len;\n const wlenR = Math.cos(ang);\n const wlenI = Math.sin(ang);\n for (let i = 0; i < n; i += len) {\n let wr = 1;\n let wi = 0;\n const half = len >> 1;\n for (let k = 0; k < half; k += 1) {\n const u = i + k;\n const v = u + half;\n const tr = wr * re[v]! - wi * im[v]!;\n const ti = wr * im[v]! + wi * re[v]!;\n re[v] = re[u]! - tr;\n im[v] = im[u]! - ti;\n re[u] = re[u]! + tr;\n im[u] = im[u]! + ti;\n const nwr = wr * wlenR - wi * wlenI;\n const nwi = wr * wlenI + wi * wlenR;\n wr = nwr;\n wi = nwi;\n }\n }\n }\n}\n\n/** Magnitude spectrum for real input: length `n` (power of 2). Returns `n/2` magnitudes for bins 0..n/2-1. */\nexport function realFftMagnitudes(samples: Float64Array): Float64Array {\n const n = samples.length;\n if (n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"realFftMagnitudes: length must be a power of 2 >= 2\");\n }\n const re = new Float64Array(n);\n const im = new Float64Array(n);\n for (let i = 0; i < n; i += 1) re[i] = samples[i]!;\n fftInPlace(re, im);\n const out = new Float64Array(n >> 1);\n for (let k = 0; k < n >> 1; k += 1) {\n out[k] = Math.hypot(re[k]!, im[k]!);\n }\n return out;\n}\n\nexport function hanningWindow(length: number): Float64Array {\n const w = new Float64Array(length);\n if (length === 1) {\n w[0] = 1;\n return w;\n }\n const denom = length - 1;\n for (let i = 0; i < length; i += 1) {\n w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / denom));\n }\n return w;\n}\n\nexport function clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(8192, Math.max(32, p));\n}\n","import { clampFftSize, hanningWindow, realFftMagnitudes } from \"../internal/fft\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type AnalyzeAudioFileOptions = {\n /** Number of time rows in the amplitude grid (and spectrogram if enabled). Default 128. */\n timeSlices?: number;\n /** Sub-buckets per time slice for the amplitude grid. Default 8. */\n samplesPerSlice?: number;\n /** When true, include `spectrogram` using windowed FFT per time slice. Default false. */\n spectrogram?: boolean;\n /** FFT length for spectrogram (power of 2). Default 1024. */\n fftSize?: number;\n /** Number of frequency bins to keep per row (first bins; capped by fftSize/2). Default 256. */\n frequencyBins?: number;\n /** Channel index, or `\"mix\"` for equal mix of all channels. Default 0. */\n channel?: number | \"mix\";\n};\n\nexport type AudioFileAnalysis = {\n duration: number;\n sampleRate: number;\n /** Peak amplitudes in [0, 1]: `timeSlices` rows × `samplesPerSlice` columns. */\n amplitudeGrid: number[][];\n /** Optional magnitude spectrogram rows, each length `frequencyBins`, normalized to [0, 1] globally. */\n spectrogram?: number[][];\n};\n\nfunction mixChannels(buffer: AudioBuffer): Float32Array {\n const { numberOfChannels, length } = buffer;\n if (numberOfChannels === 1) {\n return buffer.getChannelData(0);\n }\n const out = new Float32Array(length);\n const scale = 1 / numberOfChannels;\n for (let c = 0; c < numberOfChannels; c += 1) {\n const ch = buffer.getChannelData(c);\n for (let i = 0; i < length; i += 1) {\n out[i] += ch[i]! * scale;\n }\n }\n return out;\n}\n\nfunction getChannel(buffer: AudioBuffer, channel: number | \"mix\"): Float32Array {\n if (channel === \"mix\") return mixChannels(buffer);\n const idx = Math.max(0, Math.min(buffer.numberOfChannels - 1, channel));\n return buffer.getChannelData(idx);\n}\n\nfunction buildAmplitudeGrid(\n channel: Float32Array,\n timeSlices: number,\n samplesPerSlice: number,\n): number[][] {\n const grid: number[][] = [];\n const len = channel.length;\n if (len === 0) {\n for (let t = 0; t < timeSlices; t += 1) {\n grid.push(Array.from({ length: samplesPerSlice }, () => 0));\n }\n return grid;\n }\n\n const segmentLen = len / timeSlices;\n\n for (let t = 0; t < timeSlices; t += 1) {\n const row: number[] = [];\n const segStart = Math.floor(t * segmentLen);\n const segEnd = Math.floor((t + 1) * segmentLen);\n const segLen = Math.max(1, segEnd - segStart);\n const subLen = segLen / samplesPerSlice;\n\n for (let s = 0; s < samplesPerSlice; s += 1) {\n const a = Math.floor(segStart + s * subLen);\n const b = Math.min(segEnd, Math.floor(segStart + (s + 1) * subLen));\n let peak = 0;\n for (let i = a; i < b; i += 1) {\n peak = Math.max(peak, Math.abs(channel[i] ?? 0));\n }\n row.push(peak);\n }\n grid.push(row);\n }\n return grid;\n}\n\nfunction buildSpectrogram(\n channel: Float32Array,\n timeSlices: number,\n fftSize: number,\n frequencyBins: number,\n): { rows: number[][]; maxMag: number } {\n const rows: number[][] = [];\n let maxMag = 1e-12;\n const len = channel.length;\n const n = clampFftSize(fftSize);\n const half = n >> 1;\n const bins = Math.min(frequencyBins, half);\n const window = hanningWindow(n);\n\n if (len < n) {\n for (let t = 0; t < timeSlices; t += 1) {\n rows.push(Array.from({ length: bins }, () => 0));\n }\n return { rows, maxMag: 1 };\n }\n\n for (let t = 0; t < timeSlices; t += 1) {\n const start =\n timeSlices <= 1 ? 0 : Math.min(Math.floor((t * (len - n)) / (timeSlices - 1)), len - n);\n const frame = new Float64Array(n);\n for (let i = 0; i < n; i += 1) {\n frame[i] = (channel[start + i] ?? 0) * (window[i] ?? 0);\n }\n const mags = realFftMagnitudes(frame);\n const row: number[] = [];\n for (let k = 0; k < bins; k += 1) {\n const v = mags[k] ?? 0;\n row.push(v);\n maxMag = Math.max(maxMag, v);\n }\n rows.push(row);\n }\n\n return { rows, maxMag };\n}\n\n/**\n * Decodes an `AudioBuffer` into visualization-friendly grids (no network).\n */\nexport function analyzeAudioBuffer(\n buffer: AudioBuffer,\n options: AnalyzeAudioFileOptions = {},\n): AudioFileAnalysis {\n const timeSlices = Math.max(1, options.timeSlices ?? 128);\n const samplesPerSlice = Math.max(1, options.samplesPerSlice ?? 8);\n const wantSpec = Boolean(options.spectrogram);\n const fftSize = options.fftSize ?? 1024;\n const frequencyBins = Math.max(1, options.frequencyBins ?? 256);\n const channel = options.channel ?? 0;\n\n const data = getChannel(buffer, channel);\n const amplitudeGrid = buildAmplitudeGrid(data, timeSlices, samplesPerSlice);\n\n const result: AudioFileAnalysis = {\n duration: buffer.duration,\n sampleRate: buffer.sampleRate,\n amplitudeGrid,\n };\n\n if (wantSpec) {\n const { rows, maxMag } = buildSpectrogram(data, timeSlices, fftSize, frequencyBins);\n const norm = maxMag > 0 ? 1 / maxMag : 1;\n result.spectrogram = rows.map((row) => row.map((v) => v * norm));\n }\n\n return result;\n}\n\n/**\n * Fetches a URL, decodes audio to an `AudioBuffer`, runs {@link analyzeAudioBuffer}, then closes the temporary `AudioContext`.\n */\nexport async function analyzeAudioFile(\n fileUrl: string,\n options: AnalyzeAudioFileOptions = {},\n): Promise<AudioFileAnalysis> {\n const response = await fetch(fileUrl);\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n }\n const raw = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const buffer = await audioContext.decodeAudioData(raw.slice(0));\n return analyzeAudioBuffer(buffer, options);\n } finally {\n await audioContext.close();\n }\n}\n","import { useEffect, useState } from \"react\";\nimport {\n type AnalyzeAudioFileOptions,\n type AudioFileAnalysis,\n analyzeAudioFile,\n} from \"./analyzeAudioFile\";\n\nexport type UseAudioFileAnalysisState = {\n data: AudioFileAnalysis | null;\n isLoading: boolean;\n error: string | null;\n};\n\n/** When extending {@link AnalyzeAudioFileOptions}, thread new fields through the destructuring below and the effect dependency array. */\nexport function useAudioFileAnalysis(\n fileUrl: string | null | undefined,\n options: AnalyzeAudioFileOptions = {},\n): UseAudioFileAnalysisState {\n const { timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel } = options;\n\n const [state, setState] = useState<UseAudioFileAnalysisState>({\n data: null,\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n\n void (async () => {\n try {\n const data = await analyzeAudioFile(fileUrl, {\n timeSlices,\n samplesPerSlice,\n spectrogram,\n fftSize,\n frequencyBins,\n channel,\n });\n if (!cancelled) {\n setState({ data, isLoading: false, error: null });\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n data: null,\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to analyze audio file\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [fileUrl, timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel]);\n\n return state;\n}\n"],"names":["getAudioContextConstructor","useAudioPeaks","fileUrl","buckets","options","maxBuckets","maxSamplesPerBucket","state","setState","useState","useEffect","cancelled","prev","response","buffer","Context","audioContext","channel","safeBuckets","step","peaks","i","max","start","end","sampleCount","stride","j","error","fftInPlace","re","im","n","tr","ti","k","len","ang","wlenR","wlenI","wr","wi","half","u","v","nwr","nwi","realFftMagnitudes","samples","out","hanningWindow","length","w","denom","clampFftSize","p","mixChannels","numberOfChannels","scale","c","ch","getChannel","idx","buildAmplitudeGrid","timeSlices","samplesPerSlice","grid","t","segmentLen","row","segStart","segEnd","subLen","s","a","b","peak","buildSpectrogram","fftSize","frequencyBins","rows","maxMag","bins","window","frame","mags","analyzeAudioBuffer","wantSpec","data","amplitudeGrid","result","norm","analyzeAudioFile","raw","useAudioFileAnalysis","spectrogram"],"mappings":"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"}
|
package/dist/waveform/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useAudioPeaks } from './useAudioPeaks';
|
|
2
|
-
export type { UseAudioPeaksState } from './useAudioPeaks';
|
|
2
|
+
export type { UseAudioPeaksOptions, UseAudioPeaksState } from './useAudioPeaks';
|
|
3
3
|
export { analyzeAudioBuffer, analyzeAudioFile } from './analyzeAudioFile';
|
|
4
4
|
export type { AnalyzeAudioFileOptions, AudioFileAnalysis } from './analyzeAudioFile';
|
|
5
5
|
export { useAudioFileAnalysis } from './useAudioFileAnalysis';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/waveform/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/waveform/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/waveform/index.js
CHANGED
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
import { useState as
|
|
2
|
-
function
|
|
1
|
+
import { useState as k, useEffect as L } from "react";
|
|
2
|
+
function F() {
|
|
3
3
|
return typeof window > "u" ? null : window.AudioContext ?? window.webkitAudioContext ?? null;
|
|
4
4
|
}
|
|
5
|
-
function
|
|
6
|
-
const
|
|
5
|
+
function W(n, t = 64, a = {}) {
|
|
6
|
+
const { maxBuckets: r = 512, maxSamplesPerBucket: o = 2e4 } = a, [e, s] = k({
|
|
7
7
|
peaks: [],
|
|
8
8
|
isLoading: !1,
|
|
9
9
|
error: null
|
|
10
10
|
});
|
|
11
|
-
return
|
|
11
|
+
return L(() => {
|
|
12
12
|
if (!n) {
|
|
13
|
-
|
|
13
|
+
s({ peaks: [], isLoading: !1, error: null });
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
if (typeof window > "u") {
|
|
17
|
-
|
|
17
|
+
s({ peaks: [], isLoading: !1, error: null });
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
let
|
|
21
|
-
return
|
|
20
|
+
let l = !1;
|
|
21
|
+
return s((c) => ({ ...c, isLoading: !0, error: null })), (async () => {
|
|
22
22
|
try {
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
-
throw new Error(`Fetch failed: ${
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
23
|
+
const c = await fetch(n);
|
|
24
|
+
if (!c.ok)
|
|
25
|
+
throw new Error(`Fetch failed: ${c.status} ${c.statusText}`);
|
|
26
|
+
const u = await c.arrayBuffer(), h = F();
|
|
27
|
+
if (!h)
|
|
28
28
|
throw new Error("Web Audio API is not available");
|
|
29
|
-
const
|
|
29
|
+
const i = new h();
|
|
30
30
|
try {
|
|
31
|
-
const
|
|
32
|
-
for (let
|
|
33
|
-
let
|
|
34
|
-
const
|
|
35
|
-
for (let
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const d = (await i.decodeAudioData(u)).getChannelData(0), w = Math.min(Math.max(1, t), Math.max(1, r)), m = Math.max(1, Math.floor(d.length / w)), f = [];
|
|
32
|
+
for (let M = 0; M < w; M += 1) {
|
|
33
|
+
let p = 0;
|
|
34
|
+
const x = M * m, A = Math.min(d.length, x + m), C = A - x, B = C > o ? Math.ceil(C / o) : 1;
|
|
35
|
+
for (let y = x; y < A; y += B)
|
|
36
|
+
p = Math.max(p, Math.abs(d[y] ?? 0));
|
|
37
|
+
f.push(p);
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
l || s({ peaks: f, isLoading: !1, error: null });
|
|
40
40
|
} finally {
|
|
41
|
-
await
|
|
41
|
+
await i.close().catch(() => {
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
} catch (
|
|
45
|
-
|
|
44
|
+
} catch (c) {
|
|
45
|
+
l || s({
|
|
46
46
|
peaks: [],
|
|
47
47
|
isLoading: !1,
|
|
48
|
-
error:
|
|
48
|
+
error: c instanceof Error ? c.message : "Failed to decode peaks"
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
})(), () => {
|
|
52
|
-
|
|
52
|
+
l = !0;
|
|
53
53
|
};
|
|
54
|
-
}, [t, n]),
|
|
54
|
+
}, [t, n, r, o]), e;
|
|
55
55
|
}
|
|
56
|
-
function
|
|
56
|
+
function b(n, t) {
|
|
57
57
|
const a = n.length;
|
|
58
58
|
if (a !== t.length || a < 2 || a & a - 1)
|
|
59
59
|
throw new Error("fftInPlace: length must be equal powers of 2 >= 2");
|
|
60
|
-
let
|
|
61
|
-
for (let
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
n[
|
|
60
|
+
let r = 0;
|
|
61
|
+
for (let o = 0; o < a - 1; o += 1) {
|
|
62
|
+
if (o < r) {
|
|
63
|
+
const s = n[o], l = t[o];
|
|
64
|
+
n[o] = n[r], t[o] = t[r], n[r] = s, t[r] = l;
|
|
65
65
|
}
|
|
66
66
|
let e = a >> 1;
|
|
67
|
-
for (; e <=
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
for (; e <= r; )
|
|
68
|
+
r -= e, e >>= 1;
|
|
69
|
+
r += e;
|
|
70
70
|
}
|
|
71
|
-
for (let
|
|
72
|
-
const e = -2 * Math.PI /
|
|
73
|
-
for (let
|
|
74
|
-
let
|
|
75
|
-
const i =
|
|
76
|
-
for (let
|
|
77
|
-
const
|
|
78
|
-
n[
|
|
79
|
-
const M =
|
|
80
|
-
|
|
71
|
+
for (let o = 2; o <= a; o <<= 1) {
|
|
72
|
+
const e = -2 * Math.PI / o, s = Math.cos(e), l = Math.sin(e);
|
|
73
|
+
for (let c = 0; c < a; c += o) {
|
|
74
|
+
let u = 1, h = 0;
|
|
75
|
+
const i = o >> 1;
|
|
76
|
+
for (let g = 0; g < i; g += 1) {
|
|
77
|
+
const d = c + g, w = d + i, m = u * n[w] - h * t[w], f = u * t[w] + h * n[w];
|
|
78
|
+
n[w] = n[d] - m, t[w] = t[d] - f, n[d] = n[d] + m, t[d] = t[d] + f;
|
|
79
|
+
const M = u * s - h * l, p = u * l + h * s;
|
|
80
|
+
u = M, h = p;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
function
|
|
85
|
+
function E(n) {
|
|
86
86
|
const t = n.length;
|
|
87
87
|
if (t < 2 || t & t - 1)
|
|
88
88
|
throw new Error("realFftMagnitudes: length must be a power of 2 >= 2");
|
|
89
|
-
const a = new Float64Array(t),
|
|
89
|
+
const a = new Float64Array(t), r = new Float64Array(t);
|
|
90
90
|
for (let e = 0; e < t; e += 1) a[e] = n[e];
|
|
91
|
-
|
|
92
|
-
const
|
|
91
|
+
b(a, r);
|
|
92
|
+
const o = new Float64Array(t >> 1);
|
|
93
93
|
for (let e = 0; e < t >> 1; e += 1)
|
|
94
|
-
|
|
95
|
-
return
|
|
94
|
+
o[e] = Math.hypot(a[e], r[e]);
|
|
95
|
+
return o;
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function S(n) {
|
|
98
98
|
const t = new Float64Array(n);
|
|
99
99
|
if (n === 1)
|
|
100
100
|
return t[0] = 1, t;
|
|
101
101
|
const a = n - 1;
|
|
102
|
-
for (let
|
|
103
|
-
t[
|
|
102
|
+
for (let r = 0; r < n; r += 1)
|
|
103
|
+
t[r] = 0.5 * (1 - Math.cos(2 * Math.PI * r / a));
|
|
104
104
|
return t;
|
|
105
105
|
}
|
|
106
|
-
function
|
|
106
|
+
function P(n) {
|
|
107
107
|
const t = 2 ** Math.round(Math.log2(n));
|
|
108
108
|
return Math.min(8192, Math.max(32, t));
|
|
109
109
|
}
|
|
110
|
-
function
|
|
110
|
+
function z(n) {
|
|
111
111
|
const { numberOfChannels: t, length: a } = n;
|
|
112
112
|
if (t === 1)
|
|
113
113
|
return n.getChannelData(0);
|
|
114
|
-
const
|
|
114
|
+
const r = new Float32Array(a), o = 1 / t;
|
|
115
115
|
for (let e = 0; e < t; e += 1) {
|
|
116
|
-
const
|
|
117
|
-
for (let
|
|
118
|
-
|
|
116
|
+
const s = n.getChannelData(e);
|
|
117
|
+
for (let l = 0; l < a; l += 1)
|
|
118
|
+
r[l] += s[l] * o;
|
|
119
119
|
}
|
|
120
|
-
return
|
|
120
|
+
return r;
|
|
121
121
|
}
|
|
122
|
-
function
|
|
123
|
-
if (t === "mix") return
|
|
122
|
+
function I(n, t) {
|
|
123
|
+
if (t === "mix") return z(n);
|
|
124
124
|
const a = Math.max(0, Math.min(n.numberOfChannels - 1, t));
|
|
125
125
|
return n.getChannelData(a);
|
|
126
126
|
}
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
for (let
|
|
131
|
-
|
|
132
|
-
return
|
|
127
|
+
function v(n, t, a) {
|
|
128
|
+
const r = [], o = n.length;
|
|
129
|
+
if (o === 0) {
|
|
130
|
+
for (let s = 0; s < t; s += 1)
|
|
131
|
+
r.push(Array.from({ length: a }, () => 0));
|
|
132
|
+
return r;
|
|
133
133
|
}
|
|
134
|
-
const e =
|
|
135
|
-
for (let
|
|
136
|
-
const
|
|
137
|
-
for (let
|
|
138
|
-
const
|
|
134
|
+
const e = o / t;
|
|
135
|
+
for (let s = 0; s < t; s += 1) {
|
|
136
|
+
const l = [], c = Math.floor(s * e), u = Math.floor((s + 1) * e), i = Math.max(1, u - c) / a;
|
|
137
|
+
for (let g = 0; g < a; g += 1) {
|
|
138
|
+
const d = Math.floor(c + g * i), w = Math.min(u, Math.floor(c + (g + 1) * i));
|
|
139
139
|
let m = 0;
|
|
140
|
-
for (let
|
|
141
|
-
m = Math.max(m, Math.abs(n[
|
|
142
|
-
|
|
140
|
+
for (let f = d; f < w; f += 1)
|
|
141
|
+
m = Math.max(m, Math.abs(n[f] ?? 0));
|
|
142
|
+
l.push(m);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
r.push(l);
|
|
145
145
|
}
|
|
146
|
-
return
|
|
146
|
+
return r;
|
|
147
147
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
148
|
+
function D(n, t, a, r) {
|
|
149
|
+
const o = [];
|
|
150
150
|
let e = 1e-12;
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
151
|
+
const s = n.length, l = P(a), c = l >> 1, u = Math.min(r, c), h = S(l);
|
|
152
|
+
if (s < l) {
|
|
153
153
|
for (let i = 0; i < t; i += 1)
|
|
154
|
-
|
|
155
|
-
return { rows:
|
|
154
|
+
o.push(Array.from({ length: u }, () => 0));
|
|
155
|
+
return { rows: o, maxMag: 1 };
|
|
156
156
|
}
|
|
157
157
|
for (let i = 0; i < t; i += 1) {
|
|
158
|
-
const
|
|
159
|
-
for (let
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
for (let
|
|
163
|
-
const M =
|
|
158
|
+
const g = t <= 1 ? 0 : Math.min(Math.floor(i * (s - l) / (t - 1)), s - l), d = new Float64Array(l);
|
|
159
|
+
for (let f = 0; f < l; f += 1)
|
|
160
|
+
d[f] = (n[g + f] ?? 0) * (h[f] ?? 0);
|
|
161
|
+
const w = E(d), m = [];
|
|
162
|
+
for (let f = 0; f < u; f += 1) {
|
|
163
|
+
const M = w[f] ?? 0;
|
|
164
164
|
m.push(M), e = Math.max(e, M);
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
o.push(m);
|
|
167
167
|
}
|
|
168
|
-
return { rows:
|
|
168
|
+
return { rows: o, maxMag: e };
|
|
169
169
|
}
|
|
170
|
-
function
|
|
171
|
-
const a = Math.max(1, t.timeSlices ?? 128),
|
|
170
|
+
function q(n, t = {}) {
|
|
171
|
+
const a = Math.max(1, t.timeSlices ?? 128), r = Math.max(1, t.samplesPerSlice ?? 8), o = !!t.spectrogram, e = t.fftSize ?? 1024, s = Math.max(1, t.frequencyBins ?? 256), l = t.channel ?? 0, c = I(n, l), u = v(c, a, r), h = {
|
|
172
172
|
duration: n.duration,
|
|
173
173
|
sampleRate: n.sampleRate,
|
|
174
|
-
amplitudeGrid:
|
|
174
|
+
amplitudeGrid: u
|
|
175
175
|
};
|
|
176
|
-
if (
|
|
177
|
-
const { rows: i, maxMag:
|
|
178
|
-
|
|
176
|
+
if (o) {
|
|
177
|
+
const { rows: i, maxMag: g } = D(c, a, e, s), d = g > 0 ? 1 / g : 1;
|
|
178
|
+
h.spectrogram = i.map((w) => w.map((m) => m * d));
|
|
179
179
|
}
|
|
180
|
-
return
|
|
180
|
+
return h;
|
|
181
181
|
}
|
|
182
|
-
async function
|
|
182
|
+
async function $(n, t = {}) {
|
|
183
183
|
const a = await fetch(n);
|
|
184
184
|
if (!a.ok)
|
|
185
185
|
throw new Error(`Fetch failed: ${a.status} ${a.statusText}`);
|
|
186
|
-
const
|
|
187
|
-
if (!
|
|
186
|
+
const r = await a.arrayBuffer(), o = F();
|
|
187
|
+
if (!o)
|
|
188
188
|
throw new Error("Web Audio API is not available");
|
|
189
|
-
const e = new
|
|
189
|
+
const e = new o();
|
|
190
190
|
try {
|
|
191
|
-
const
|
|
192
|
-
return
|
|
191
|
+
const s = await e.decodeAudioData(r.slice(0));
|
|
192
|
+
return q(s, t);
|
|
193
193
|
} finally {
|
|
194
194
|
await e.close();
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
function
|
|
198
|
-
const { timeSlices: a, samplesPerSlice:
|
|
197
|
+
function j(n, t = {}) {
|
|
198
|
+
const { timeSlices: a, samplesPerSlice: r, spectrogram: o, fftSize: e, frequencyBins: s, channel: l } = t, [c, u] = k({
|
|
199
199
|
data: null,
|
|
200
200
|
isLoading: !1,
|
|
201
201
|
error: null
|
|
202
202
|
});
|
|
203
|
-
return
|
|
203
|
+
return L(() => {
|
|
204
204
|
if (!n) {
|
|
205
|
-
|
|
205
|
+
u({ data: null, isLoading: !1, error: null });
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
if (typeof window > "u") {
|
|
209
|
-
|
|
209
|
+
u({ data: null, isLoading: !1, error: null });
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
let
|
|
213
|
-
return
|
|
212
|
+
let h = !1;
|
|
213
|
+
return u((i) => ({ ...i, isLoading: !0, error: null })), (async () => {
|
|
214
214
|
try {
|
|
215
|
-
const i = await
|
|
215
|
+
const i = await $(n, {
|
|
216
216
|
timeSlices: a,
|
|
217
|
-
samplesPerSlice:
|
|
218
|
-
spectrogram:
|
|
217
|
+
samplesPerSlice: r,
|
|
218
|
+
spectrogram: o,
|
|
219
219
|
fftSize: e,
|
|
220
|
-
frequencyBins:
|
|
221
|
-
channel:
|
|
220
|
+
frequencyBins: s,
|
|
221
|
+
channel: l
|
|
222
222
|
});
|
|
223
|
-
|
|
223
|
+
h || u({ data: i, isLoading: !1, error: null });
|
|
224
224
|
} catch (i) {
|
|
225
|
-
|
|
225
|
+
h || u({
|
|
226
226
|
data: null,
|
|
227
227
|
isLoading: !1,
|
|
228
228
|
error: i instanceof Error ? i.message : "Failed to analyze audio file"
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
231
|
})(), () => {
|
|
232
|
-
|
|
232
|
+
h = !0;
|
|
233
233
|
};
|
|
234
|
-
}, [n, a,
|
|
234
|
+
}, [n, a, r, o, e, s, l]), c;
|
|
235
235
|
}
|
|
236
236
|
export {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
q as analyzeAudioBuffer,
|
|
238
|
+
$ as analyzeAudioFile,
|
|
239
|
+
j as useAudioFileAnalysis,
|
|
240
|
+
W as useAudioPeaks
|
|
241
241
|
};
|
|
242
242
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/waveform/getAudioContextConstructor.ts","../../src/waveform/useAudioPeaks.ts","../../src/internal/fft.ts","../../src/waveform/analyzeAudioFile.ts","../../src/waveform/useAudioFileAnalysis.ts"],"sourcesContent":["/**\n * Resolves the runtime `AudioContext` constructor, including legacy `webkitAudioContext` (Safari).\n * Returns `null` when `window` is unavailable or Web Audio is not implemented.\n */\nexport function getAudioContextConstructor():\n | (new (\n contextOptions?: AudioContextOptions,\n ) => AudioContext)\n | null {\n if (typeof window === \"undefined\") return null;\n const C =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n return C ?? null;\n}\n","import { useEffect, useState } from \"react\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type UseAudioPeaksState = {\n peaks: number[];\n isLoading: boolean;\n error: string | null;\n};\n\nexport function useAudioPeaks(\n fileUrl: string | null | undefined,\n buckets = 64,\n): UseAudioPeaksState {\n const [state, setState] = useState<UseAudioPeaksState>({\n peaks: [],\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n void (async () => {\n try {\n const response = await fetch(fileUrl);\n if (!response.ok)\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n const buffer = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const audioBuffer = await audioContext.decodeAudioData(buffer);\n const channel = audioBuffer.getChannelData(0);\n const step = Math.max(1, Math.floor(channel.length / buckets));\n const peaks: number[] = [];\n for (let i = 0; i < buckets; i += 1) {\n let max = 0;\n const start = i * step;\n const end = Math.min(channel.length, start + step);\n for (let j = start; j < end; j += 1) {\n max = Math.max(max, Math.abs(channel[j] ?? 0));\n }\n peaks.push(max);\n }\n if (!cancelled) {\n setState({ peaks, isLoading: false, error: null });\n }\n } finally {\n await audioContext.close().catch(() => {});\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n peaks: [],\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to decode peaks\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [buckets, fileUrl]);\n\n return state;\n}\n","/** In-place radix-2 Cooley–Tukey FFT; `length` must be a power of 2 and >= 2. */\nexport function fftInPlace(re: Float64Array, im: Float64Array): void {\n const n = re.length;\n if (n !== im.length || n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"fftInPlace: length must be equal powers of 2 >= 2\");\n }\n\n let j = 0;\n for (let i = 0; i < n - 1; i += 1) {\n if (i < j) {\n const tr = re[i]!;\n const ti = im[i]!;\n re[i] = re[j]!;\n im[i] = im[j]!;\n re[j] = tr;\n im[j] = ti;\n }\n let k = n >> 1;\n while (k <= j) {\n j -= k;\n k >>= 1;\n }\n j += k;\n }\n\n for (let len = 2; len <= n; len <<= 1) {\n const ang = (-2 * Math.PI) / len;\n const wlenR = Math.cos(ang);\n const wlenI = Math.sin(ang);\n for (let i = 0; i < n; i += len) {\n let wr = 1;\n let wi = 0;\n const half = len >> 1;\n for (let k = 0; k < half; k += 1) {\n const u = i + k;\n const v = u + half;\n const tr = wr * re[v]! - wi * im[v]!;\n const ti = wr * im[v]! + wi * re[v]!;\n re[v] = re[u]! - tr;\n im[v] = im[u]! - ti;\n re[u] = re[u]! + tr;\n im[u] = im[u]! + ti;\n const nwr = wr * wlenR - wi * wlenI;\n const nwi = wr * wlenI + wi * wlenR;\n wr = nwr;\n wi = nwi;\n }\n }\n }\n}\n\n/** Magnitude spectrum for real input: length `n` (power of 2). Returns `n/2` magnitudes for bins 0..n/2-1. */\nexport function realFftMagnitudes(samples: Float64Array): Float64Array {\n const n = samples.length;\n if (n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"realFftMagnitudes: length must be a power of 2 >= 2\");\n }\n const re = new Float64Array(n);\n const im = new Float64Array(n);\n for (let i = 0; i < n; i += 1) re[i] = samples[i]!;\n fftInPlace(re, im);\n const out = new Float64Array(n >> 1);\n for (let k = 0; k < n >> 1; k += 1) {\n out[k] = Math.hypot(re[k]!, im[k]!);\n }\n return out;\n}\n\nexport function hanningWindow(length: number): Float64Array {\n const w = new Float64Array(length);\n if (length === 1) {\n w[0] = 1;\n return w;\n }\n const denom = length - 1;\n for (let i = 0; i < length; i += 1) {\n w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / denom));\n }\n return w;\n}\n\nexport function clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(8192, Math.max(32, p));\n}\n","import { clampFftSize, hanningWindow, realFftMagnitudes } from \"../internal/fft\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type AnalyzeAudioFileOptions = {\n /** Number of time rows in the amplitude grid (and spectrogram if enabled). Default 128. */\n timeSlices?: number;\n /** Sub-buckets per time slice for the amplitude grid. Default 8. */\n samplesPerSlice?: number;\n /** When true, include `spectrogram` using windowed FFT per time slice. Default false. */\n spectrogram?: boolean;\n /** FFT length for spectrogram (power of 2). Default 1024. */\n fftSize?: number;\n /** Number of frequency bins to keep per row (first bins; capped by fftSize/2). Default 256. */\n frequencyBins?: number;\n /** Channel index, or `\"mix\"` for equal mix of all channels. Default 0. */\n channel?: number | \"mix\";\n};\n\nexport type AudioFileAnalysis = {\n duration: number;\n sampleRate: number;\n /** Peak amplitudes in [0, 1]: `timeSlices` rows × `samplesPerSlice` columns. */\n amplitudeGrid: number[][];\n /** Optional magnitude spectrogram rows, each length `frequencyBins`, normalized to [0, 1] globally. */\n spectrogram?: number[][];\n};\n\nfunction mixChannels(buffer: AudioBuffer): Float32Array {\n const { numberOfChannels, length } = buffer;\n if (numberOfChannels === 1) {\n return buffer.getChannelData(0);\n }\n const out = new Float32Array(length);\n const scale = 1 / numberOfChannels;\n for (let c = 0; c < numberOfChannels; c += 1) {\n const ch = buffer.getChannelData(c);\n for (let i = 0; i < length; i += 1) {\n out[i] += ch[i]! * scale;\n }\n }\n return out;\n}\n\nfunction getChannel(buffer: AudioBuffer, channel: number | \"mix\"): Float32Array {\n if (channel === \"mix\") return mixChannels(buffer);\n const idx = Math.max(0, Math.min(buffer.numberOfChannels - 1, channel));\n return buffer.getChannelData(idx);\n}\n\nfunction buildAmplitudeGrid(\n channel: Float32Array,\n timeSlices: number,\n samplesPerSlice: number,\n): number[][] {\n const grid: number[][] = [];\n const len = channel.length;\n if (len === 0) {\n for (let t = 0; t < timeSlices; t += 1) {\n grid.push(Array.from({ length: samplesPerSlice }, () => 0));\n }\n return grid;\n }\n\n const segmentLen = len / timeSlices;\n\n for (let t = 0; t < timeSlices; t += 1) {\n const row: number[] = [];\n const segStart = Math.floor(t * segmentLen);\n const segEnd = Math.floor((t + 1) * segmentLen);\n const segLen = Math.max(1, segEnd - segStart);\n const subLen = segLen / samplesPerSlice;\n\n for (let s = 0; s < samplesPerSlice; s += 1) {\n const a = Math.floor(segStart + s * subLen);\n const b = Math.min(segEnd, Math.floor(segStart + (s + 1) * subLen));\n let peak = 0;\n for (let i = a; i < b; i += 1) {\n peak = Math.max(peak, Math.abs(channel[i] ?? 0));\n }\n row.push(peak);\n }\n grid.push(row);\n }\n return grid;\n}\n\nfunction buildSpectrogram(\n channel: Float32Array,\n timeSlices: number,\n fftSize: number,\n frequencyBins: number,\n): { rows: number[][]; maxMag: number } {\n const rows: number[][] = [];\n let maxMag = 1e-12;\n const len = channel.length;\n const n = clampFftSize(fftSize);\n const half = n >> 1;\n const bins = Math.min(frequencyBins, half);\n const window = hanningWindow(n);\n\n if (len < n) {\n for (let t = 0; t < timeSlices; t += 1) {\n rows.push(Array.from({ length: bins }, () => 0));\n }\n return { rows, maxMag: 1 };\n }\n\n for (let t = 0; t < timeSlices; t += 1) {\n const start =\n timeSlices <= 1 ? 0 : Math.min(Math.floor((t * (len - n)) / (timeSlices - 1)), len - n);\n const frame = new Float64Array(n);\n for (let i = 0; i < n; i += 1) {\n frame[i] = (channel[start + i] ?? 0) * (window[i] ?? 0);\n }\n const mags = realFftMagnitudes(frame);\n const row: number[] = [];\n for (let k = 0; k < bins; k += 1) {\n const v = mags[k] ?? 0;\n row.push(v);\n maxMag = Math.max(maxMag, v);\n }\n rows.push(row);\n }\n\n return { rows, maxMag };\n}\n\n/**\n * Decodes an `AudioBuffer` into visualization-friendly grids (no network).\n */\nexport function analyzeAudioBuffer(\n buffer: AudioBuffer,\n options: AnalyzeAudioFileOptions = {},\n): AudioFileAnalysis {\n const timeSlices = Math.max(1, options.timeSlices ?? 128);\n const samplesPerSlice = Math.max(1, options.samplesPerSlice ?? 8);\n const wantSpec = Boolean(options.spectrogram);\n const fftSize = options.fftSize ?? 1024;\n const frequencyBins = Math.max(1, options.frequencyBins ?? 256);\n const channel = options.channel ?? 0;\n\n const data = getChannel(buffer, channel);\n const amplitudeGrid = buildAmplitudeGrid(data, timeSlices, samplesPerSlice);\n\n const result: AudioFileAnalysis = {\n duration: buffer.duration,\n sampleRate: buffer.sampleRate,\n amplitudeGrid,\n };\n\n if (wantSpec) {\n const { rows, maxMag } = buildSpectrogram(data, timeSlices, fftSize, frequencyBins);\n const norm = maxMag > 0 ? 1 / maxMag : 1;\n result.spectrogram = rows.map((row) => row.map((v) => v * norm));\n }\n\n return result;\n}\n\n/**\n * Fetches a URL, decodes audio to an `AudioBuffer`, runs {@link analyzeAudioBuffer}, then closes the temporary `AudioContext`.\n */\nexport async function analyzeAudioFile(\n fileUrl: string,\n options: AnalyzeAudioFileOptions = {},\n): Promise<AudioFileAnalysis> {\n const response = await fetch(fileUrl);\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n }\n const raw = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const buffer = await audioContext.decodeAudioData(raw.slice(0));\n return analyzeAudioBuffer(buffer, options);\n } finally {\n await audioContext.close();\n }\n}\n","import { useEffect, useState } from \"react\";\nimport {\n type AnalyzeAudioFileOptions,\n type AudioFileAnalysis,\n analyzeAudioFile,\n} from \"./analyzeAudioFile\";\n\nexport type UseAudioFileAnalysisState = {\n data: AudioFileAnalysis | null;\n isLoading: boolean;\n error: string | null;\n};\n\n/** When extending {@link AnalyzeAudioFileOptions}, thread new fields through the destructuring below and the effect dependency array. */\nexport function useAudioFileAnalysis(\n fileUrl: string | null | undefined,\n options: AnalyzeAudioFileOptions = {},\n): UseAudioFileAnalysisState {\n const { timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel } = options;\n\n const [state, setState] = useState<UseAudioFileAnalysisState>({\n data: null,\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n\n void (async () => {\n try {\n const data = await analyzeAudioFile(fileUrl, {\n timeSlices,\n samplesPerSlice,\n spectrogram,\n fftSize,\n frequencyBins,\n channel,\n });\n if (!cancelled) {\n setState({ data, isLoading: false, error: null });\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n data: null,\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to analyze audio file\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [fileUrl, timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel]);\n\n return state;\n}\n"],"names":["getAudioContextConstructor","useAudioPeaks","fileUrl","buckets","state","setState","useState","useEffect","cancelled","prev","response","buffer","Context","audioContext","channel","step","peaks","i","max","start","end","j","error","fftInPlace","re","im","n","tr","ti","k","len","ang","wlenR","wlenI","wr","wi","half","v","nwr","nwi","realFftMagnitudes","samples","out","hanningWindow","length","w","denom","clampFftSize","p","mixChannels","numberOfChannels","scale","c","ch","getChannel","idx","buildAmplitudeGrid","timeSlices","samplesPerSlice","grid","t","segmentLen","row","segStart","segEnd","subLen","s","a","b","peak","buildSpectrogram","fftSize","frequencyBins","rows","maxMag","bins","window","frame","mags","analyzeAudioBuffer","options","wantSpec","data","amplitudeGrid","result","norm","analyzeAudioFile","raw","useAudioFileAnalysis","spectrogram"],"mappings":";AAIO,SAASA,IAIP;AACP,SAAI,OAAO,SAAW,MAAoB,OAExC,OAAO,gBACN,OAAmE,sBAC1D;AACd;ACLO,SAASC,EACdC,GACAC,IAAU,IACU;AACpB,QAAM,CAACC,GAAOC,CAAQ,IAAIC,EAA6B;AAAA,IACrD,OAAO,CAAA;AAAA,IACP,WAAW;AAAA,IACX,OAAO;AAAA,EAAA,CACR;AAED,SAAAC,EAAU,MAAM;AACd,QAAI,CAACL,GAAS;AACZ,MAAAG,EAAS,EAAE,OAAO,CAAA,GAAI,WAAW,IAAO,OAAO,MAAM;AACrD;AAAA,IACF;AACA,QAAI,OAAO,SAAW,KAAa;AACjC,MAAAA,EAAS,EAAE,OAAO,CAAA,GAAI,WAAW,IAAO,OAAO,MAAM;AACrD;AAAA,IACF;AACA,QAAIG,IAAY;AAChB,WAAAH,EAAS,CAACI,OAAU,EAAE,GAAGA,GAAM,WAAW,IAAM,OAAO,KAAA,EAAO,IACxD,YAAY;AAChB,UAAI;AACF,cAAMC,IAAW,MAAM,MAAMR,CAAO;AACpC,YAAI,CAACQ,EAAS;AACZ,gBAAM,IAAI,MAAM,iBAAiBA,EAAS,MAAM,IAAIA,EAAS,UAAU,EAAE;AAC3E,cAAMC,IAAS,MAAMD,EAAS,YAAA,GACxBE,IAAUZ,EAAA;AAChB,YAAI,CAACY;AACH,gBAAM,IAAI,MAAM,gCAAgC;AAElD,cAAMC,IAAe,IAAID,EAAA;AACzB,YAAI;AAEF,gBAAME,KADc,MAAMD,EAAa,gBAAgBF,CAAM,GACjC,eAAe,CAAC,GACtCI,IAAO,KAAK,IAAI,GAAG,KAAK,MAAMD,EAAQ,SAASX,CAAO,CAAC,GACvDa,IAAkB,CAAA;AACxB,mBAASC,IAAI,GAAGA,IAAId,GAASc,KAAK,GAAG;AACnC,gBAAIC,IAAM;AACV,kBAAMC,IAAQF,IAAIF,GACZK,IAAM,KAAK,IAAIN,EAAQ,QAAQK,IAAQJ,CAAI;AACjD,qBAASM,IAAIF,GAAOE,IAAID,GAAKC,KAAK;AAChC,cAAAH,IAAM,KAAK,IAAIA,GAAK,KAAK,IAAIJ,EAAQO,CAAC,KAAK,CAAC,CAAC;AAE/C,YAAAL,EAAM,KAAKE,CAAG;AAAA,UAChB;AACA,UAAKV,KACHH,EAAS,EAAE,OAAAW,GAAO,WAAW,IAAO,OAAO,MAAM;AAAA,QAErD,UAAA;AACE,gBAAMH,EAAa,QAAQ,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC3C;AAAA,MACF,SAASS,GAAO;AACd,QAAKd,KACHH,EAAS;AAAA,UACP,OAAO,CAAA;AAAA,UACP,WAAW;AAAA,UACX,OAAOiB,aAAiB,QAAQA,EAAM,UAAU;AAAA,QAAA,CACjD;AAAA,MAEL;AAAA,IACF,GAAA,GAEO,MAAM;AACX,MAAAd,IAAY;AAAA,IACd;AAAA,EACF,GAAG,CAACL,GAASD,CAAO,CAAC,GAEdE;AACT;AC7EO,SAASmB,EAAWC,GAAkBC,GAAwB;AACnE,QAAMC,IAAIF,EAAG;AACb,MAAIE,MAAMD,EAAG,UAAUC,IAAI,KAAMA,IAAKA,IAAI;AACxC,UAAM,IAAI,MAAM,mDAAmD;AAGrE,MAAIL,IAAI;AACR,WAASJ,IAAI,GAAGA,IAAIS,IAAI,GAAGT,KAAK,GAAG;AACjC,QAAIA,IAAII,GAAG;AACT,YAAMM,IAAKH,EAAGP,CAAC,GACTW,IAAKH,EAAGR,CAAC;AACf,MAAAO,EAAGP,CAAC,IAAIO,EAAGH,CAAC,GACZI,EAAGR,CAAC,IAAIQ,EAAGJ,CAAC,GACZG,EAAGH,CAAC,IAAIM,GACRF,EAAGJ,CAAC,IAAIO;AAAA,IACV;AACA,QAAIC,IAAIH,KAAK;AACb,WAAOG,KAAKR;AACV,MAAAA,KAAKQ,GACLA,MAAM;AAER,IAAAR,KAAKQ;AAAA,EACP;AAEA,WAASC,IAAM,GAAGA,KAAOJ,GAAGI,MAAQ,GAAG;AACrC,UAAMC,IAAO,KAAK,KAAK,KAAMD,GACvBE,IAAQ,KAAK,IAAID,CAAG,GACpBE,IAAQ,KAAK,IAAIF,CAAG;AAC1B,aAASd,IAAI,GAAGA,IAAIS,GAAGT,KAAKa,GAAK;AAC/B,UAAII,IAAK,GACLC,IAAK;AACT,YAAMC,IAAON,KAAO;AACpB,eAASD,IAAI,GAAGA,IAAIO,GAAMP,KAAK,GAAG;AAChC,cAAM,IAAIZ,IAAIY,GACRQ,IAAI,IAAID,GACRT,IAAKO,IAAKV,EAAGa,CAAC,IAAKF,IAAKV,EAAGY,CAAC,GAC5BT,IAAKM,IAAKT,EAAGY,CAAC,IAAKF,IAAKX,EAAGa,CAAC;AAClC,QAAAb,EAAGa,CAAC,IAAIb,EAAG,CAAC,IAAKG,GACjBF,EAAGY,CAAC,IAAIZ,EAAG,CAAC,IAAKG,GACjBJ,EAAG,CAAC,IAAIA,EAAG,CAAC,IAAKG,GACjBF,EAAG,CAAC,IAAIA,EAAG,CAAC,IAAKG;AACjB,cAAMU,IAAMJ,IAAKF,IAAQG,IAAKF,GACxBM,IAAML,IAAKD,IAAQE,IAAKH;AAC9B,QAAAE,IAAKI,GACLH,IAAKI;AAAA,MACP;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAASC,EAAkBC,GAAqC;AACrE,QAAMf,IAAIe,EAAQ;AAClB,MAAIf,IAAI,KAAMA,IAAKA,IAAI;AACrB,UAAM,IAAI,MAAM,qDAAqD;AAEvE,QAAMF,IAAK,IAAI,aAAaE,CAAC,GACvBD,IAAK,IAAI,aAAaC,CAAC;AAC7B,WAAST,IAAI,GAAGA,IAAIS,GAAGT,KAAK,EAAG,CAAAO,EAAGP,CAAC,IAAIwB,EAAQxB,CAAC;AAChD,EAAAM,EAAWC,GAAIC,CAAE;AACjB,QAAMiB,IAAM,IAAI,aAAahB,KAAK,CAAC;AACnC,WAASG,IAAI,GAAGA,IAAIH,KAAK,GAAGG,KAAK;AAC/B,IAAAa,EAAIb,CAAC,IAAI,KAAK,MAAML,EAAGK,CAAC,GAAIJ,EAAGI,CAAC,CAAE;AAEpC,SAAOa;AACT;AAEO,SAASC,EAAcC,GAA8B;AAC1D,QAAMC,IAAI,IAAI,aAAaD,CAAM;AACjC,MAAIA,MAAW;AACb,WAAAC,EAAE,CAAC,IAAI,GACAA;AAET,QAAMC,IAAQF,IAAS;AACvB,WAAS3B,IAAI,GAAGA,IAAI2B,GAAQ3B,KAAK;AAC/B,IAAA4B,EAAE5B,CAAC,IAAI,OAAO,IAAI,KAAK,IAAK,IAAI,KAAK,KAAKA,IAAK6B,CAAK;AAEtD,SAAOD;AACT;AAEO,SAASE,EAAa,GAAmB;AAC9C,QAAMC,IAAI,KAAK,KAAK,MAAM,KAAK,KAAK,CAAC,CAAC;AACtC,SAAO,KAAK,IAAI,MAAM,KAAK,IAAI,IAAIA,CAAC,CAAC;AACvC;ACzDA,SAASC,EAAYtC,GAAmC;AACtD,QAAM,EAAE,kBAAAuC,GAAkB,QAAAN,EAAA,IAAWjC;AACrC,MAAIuC,MAAqB;AACvB,WAAOvC,EAAO,eAAe,CAAC;AAEhC,QAAM+B,IAAM,IAAI,aAAaE,CAAM,GAC7BO,IAAQ,IAAID;AAClB,WAASE,IAAI,GAAGA,IAAIF,GAAkBE,KAAK,GAAG;AAC5C,UAAMC,IAAK1C,EAAO,eAAeyC,CAAC;AAClC,aAASnC,IAAI,GAAGA,IAAI2B,GAAQ3B,KAAK;AAC/B,MAAAyB,EAAIzB,CAAC,KAAKoC,EAAGpC,CAAC,IAAKkC;AAAA,EAEvB;AACA,SAAOT;AACT;AAEA,SAASY,EAAW3C,GAAqBG,GAAuC;AAC9E,MAAIA,MAAY,MAAO,QAAOmC,EAAYtC,CAAM;AAChD,QAAM4C,IAAM,KAAK,IAAI,GAAG,KAAK,IAAI5C,EAAO,mBAAmB,GAAGG,CAAO,CAAC;AACtE,SAAOH,EAAO,eAAe4C,CAAG;AAClC;AAEA,SAASC,EACP1C,GACA2C,GACAC,GACY;AACZ,QAAMC,IAAmB,CAAA,GACnB7B,IAAMhB,EAAQ;AACpB,MAAIgB,MAAQ,GAAG;AACb,aAAS8B,IAAI,GAAGA,IAAIH,GAAYG,KAAK;AACnC,MAAAD,EAAK,KAAK,MAAM,KAAK,EAAE,QAAQD,EAAA,GAAmB,MAAM,CAAC,CAAC;AAE5D,WAAOC;AAAA,EACT;AAEA,QAAME,IAAa/B,IAAM2B;AAEzB,WAASG,IAAI,GAAGA,IAAIH,GAAYG,KAAK,GAAG;AACtC,UAAME,IAAgB,CAAA,GAChBC,IAAW,KAAK,MAAMH,IAAIC,CAAU,GACpCG,IAAS,KAAK,OAAOJ,IAAI,KAAKC,CAAU,GAExCI,IADS,KAAK,IAAI,GAAGD,IAASD,CAAQ,IACpBL;AAExB,aAASQ,IAAI,GAAGA,IAAIR,GAAiBQ,KAAK,GAAG;AAC3C,YAAMC,IAAI,KAAK,MAAMJ,IAAWG,IAAID,CAAM,GACpCG,IAAI,KAAK,IAAIJ,GAAQ,KAAK,MAAMD,KAAYG,IAAI,KAAKD,CAAM,CAAC;AAClE,UAAII,IAAO;AACX,eAASpD,IAAIkD,GAAGlD,IAAImD,GAAGnD,KAAK;AAC1B,QAAAoD,IAAO,KAAK,IAAIA,GAAM,KAAK,IAAIvD,EAAQG,CAAC,KAAK,CAAC,CAAC;AAEjD,MAAA6C,EAAI,KAAKO,CAAI;AAAA,IACf;AACA,IAAAV,EAAK,KAAKG,CAAG;AAAA,EACf;AACA,SAAOH;AACT;AAEA,SAASW,EACPxD,GACA2C,GACAc,GACAC,GACsC;AACtC,QAAMC,IAAmB,CAAA;AACzB,MAAIC,IAAS;AACb,QAAM5C,IAAMhB,EAAQ,QACdY,IAAIqB,EAAawB,CAAO,GACxBnC,IAAOV,KAAK,GACZiD,IAAO,KAAK,IAAIH,GAAepC,CAAI,GACnCwC,IAASjC,EAAcjB,CAAC;AAE9B,MAAII,IAAMJ,GAAG;AACX,aAASkC,IAAI,GAAGA,IAAIH,GAAYG,KAAK;AACnC,MAAAa,EAAK,KAAK,MAAM,KAAK,EAAE,QAAQE,EAAA,GAAQ,MAAM,CAAC,CAAC;AAEjD,WAAO,EAAE,MAAAF,GAAM,QAAQ,EAAA;AAAA,EACzB;AAEA,WAASb,IAAI,GAAGA,IAAIH,GAAYG,KAAK,GAAG;AACtC,UAAMzC,IACJsC,KAAc,IAAI,IAAI,KAAK,IAAI,KAAK,MAAOG,KAAK9B,IAAMJ,MAAO+B,IAAa,EAAE,GAAG3B,IAAMJ,CAAC,GAClFmD,IAAQ,IAAI,aAAanD,CAAC;AAChC,aAAST,IAAI,GAAGA,IAAIS,GAAGT,KAAK;AAC1B,MAAA4D,EAAM5D,CAAC,KAAKH,EAAQK,IAAQF,CAAC,KAAK,MAAM2D,EAAO3D,CAAC,KAAK;AAEvD,UAAM6D,IAAOtC,EAAkBqC,CAAK,GAC9Bf,IAAgB,CAAA;AACtB,aAASjC,IAAI,GAAGA,IAAI8C,GAAM9C,KAAK,GAAG;AAChC,YAAMQ,IAAIyC,EAAKjD,CAAC,KAAK;AACrB,MAAAiC,EAAI,KAAKzB,CAAC,GACVqC,IAAS,KAAK,IAAIA,GAAQrC,CAAC;AAAA,IAC7B;AACA,IAAAoC,EAAK,KAAKX,CAAG;AAAA,EACf;AAEA,SAAO,EAAE,MAAAW,GAAM,QAAAC,EAAA;AACjB;AAKO,SAASK,EACdpE,GACAqE,IAAmC,IAChB;AACnB,QAAMvB,IAAa,KAAK,IAAI,GAAGuB,EAAQ,cAAc,GAAG,GAClDtB,IAAkB,KAAK,IAAI,GAAGsB,EAAQ,mBAAmB,CAAC,GAC1DC,IAAW,EAAQD,EAAQ,aAC3BT,IAAUS,EAAQ,WAAW,MAC7BR,IAAgB,KAAK,IAAI,GAAGQ,EAAQ,iBAAiB,GAAG,GACxDlE,IAAUkE,EAAQ,WAAW,GAE7BE,IAAO5B,EAAW3C,GAAQG,CAAO,GACjCqE,IAAgB3B,EAAmB0B,GAAMzB,GAAYC,CAAe,GAEpE0B,IAA4B;AAAA,IAChC,UAAUzE,EAAO;AAAA,IACjB,YAAYA,EAAO;AAAA,IACnB,eAAAwE;AAAA,EAAA;AAGF,MAAIF,GAAU;AACZ,UAAM,EAAE,MAAAR,GAAM,QAAAC,MAAWJ,EAAiBY,GAAMzB,GAAYc,GAASC,CAAa,GAC5Ea,IAAOX,IAAS,IAAI,IAAIA,IAAS;AACvC,IAAAU,EAAO,cAAcX,EAAK,IAAI,CAACX,MAAQA,EAAI,IAAI,CAACzB,MAAMA,IAAIgD,CAAI,CAAC;AAAA,EACjE;AAEA,SAAOD;AACT;AAKA,eAAsBE,EACpBpF,GACA8E,IAAmC,IACP;AAC5B,QAAMtE,IAAW,MAAM,MAAMR,CAAO;AACpC,MAAI,CAACQ,EAAS;AACZ,UAAM,IAAI,MAAM,iBAAiBA,EAAS,MAAM,IAAIA,EAAS,UAAU,EAAE;AAE3E,QAAM6E,IAAM,MAAM7E,EAAS,YAAA,GACrBE,IAAUZ,EAAA;AAChB,MAAI,CAACY;AACH,UAAM,IAAI,MAAM,gCAAgC;AAElD,QAAMC,IAAe,IAAID,EAAA;AACzB,MAAI;AACF,UAAMD,IAAS,MAAME,EAAa,gBAAgB0E,EAAI,MAAM,CAAC,CAAC;AAC9D,WAAOR,EAAmBpE,GAAQqE,CAAO;AAAA,EAC3C,UAAA;AACE,UAAMnE,EAAa,MAAA;AAAA,EACrB;AACF;ACxKO,SAAS2E,EACdtF,GACA8E,IAAmC,IACR;AAC3B,QAAM,EAAE,YAAAvB,GAAY,iBAAAC,GAAiB,aAAA+B,GAAa,SAAAlB,GAAS,eAAAC,GAAe,SAAA1D,MAAYkE,GAEhF,CAAC5E,GAAOC,CAAQ,IAAIC,EAAoC;AAAA,IAC5D,MAAM;AAAA,IACN,WAAW;AAAA,IACX,OAAO;AAAA,EAAA,CACR;AAED,SAAAC,EAAU,MAAM;AACd,QAAI,CAACL,GAAS;AACZ,MAAAG,EAAS,EAAE,MAAM,MAAM,WAAW,IAAO,OAAO,MAAM;AACtD;AAAA,IACF;AACA,QAAI,OAAO,SAAW,KAAa;AACjC,MAAAA,EAAS,EAAE,MAAM,MAAM,WAAW,IAAO,OAAO,MAAM;AACtD;AAAA,IACF;AAEA,QAAIG,IAAY;AAChB,WAAAH,EAAS,CAACI,OAAU,EAAE,GAAGA,GAAM,WAAW,IAAM,OAAO,KAAA,EAAO,IAExD,YAAY;AAChB,UAAI;AACF,cAAMyE,IAAO,MAAMI,EAAiBpF,GAAS;AAAA,UAC3C,YAAAuD;AAAA,UACA,iBAAAC;AAAA,UACA,aAAA+B;AAAA,UACA,SAAAlB;AAAA,UACA,eAAAC;AAAA,UACA,SAAA1D;AAAA,QAAA,CACD;AACD,QAAKN,KACHH,EAAS,EAAE,MAAA6E,GAAM,WAAW,IAAO,OAAO,MAAM;AAAA,MAEpD,SAAS5D,GAAO;AACd,QAAKd,KACHH,EAAS;AAAA,UACP,MAAM;AAAA,UACN,WAAW;AAAA,UACX,OAAOiB,aAAiB,QAAQA,EAAM,UAAU;AAAA,QAAA,CACjD;AAAA,MAEL;AAAA,IACF,GAAA,GAEO,MAAM;AACX,MAAAd,IAAY;AAAA,IACd;AAAA,EACF,GAAG,CAACN,GAASuD,GAAYC,GAAiB+B,GAAalB,GAASC,GAAe1D,CAAO,CAAC,GAEhFV;AACT;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/waveform/getAudioContextConstructor.ts","../../src/waveform/useAudioPeaks.ts","../../src/internal/fft.ts","../../src/waveform/analyzeAudioFile.ts","../../src/waveform/useAudioFileAnalysis.ts"],"sourcesContent":["/**\n * Resolves the runtime `AudioContext` constructor, including legacy `webkitAudioContext` (Safari).\n * Returns `null` when `window` is unavailable or Web Audio is not implemented.\n */\nexport function getAudioContextConstructor():\n | (new (\n contextOptions?: AudioContextOptions,\n ) => AudioContext)\n | null {\n if (typeof window === \"undefined\") return null;\n const C =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n return C ?? null;\n}\n","import { useEffect, useState } from \"react\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type UseAudioPeaksState = {\n peaks: number[];\n isLoading: boolean;\n error: string | null;\n};\n\nexport type UseAudioPeaksOptions = {\n /**\n * Hard cap for computed buckets to avoid very expensive loops.\n * Default: 512.\n */\n maxBuckets?: number;\n /**\n * Maximum number of samples scanned per bucket.\n * Large files are sampled with a stride once this limit is exceeded.\n * Default: 20_000.\n */\n maxSamplesPerBucket?: number;\n};\n\nexport function useAudioPeaks(\n fileUrl: string | null | undefined,\n buckets = 64,\n options: UseAudioPeaksOptions = {},\n): UseAudioPeaksState {\n const { maxBuckets = 512, maxSamplesPerBucket = 20_000 } = options;\n const [state, setState] = useState<UseAudioPeaksState>({\n peaks: [],\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ peaks: [], isLoading: false, error: null });\n return;\n }\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n void (async () => {\n try {\n const response = await fetch(fileUrl);\n if (!response.ok)\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n const buffer = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const audioBuffer = await audioContext.decodeAudioData(buffer);\n const channel = audioBuffer.getChannelData(0);\n const safeBuckets = Math.min(Math.max(1, buckets), Math.max(1, maxBuckets));\n const step = Math.max(1, Math.floor(channel.length / safeBuckets));\n const peaks: number[] = [];\n for (let i = 0; i < safeBuckets; i += 1) {\n let max = 0;\n const start = i * step;\n const end = Math.min(channel.length, start + step);\n const sampleCount = end - start;\n const stride =\n sampleCount > maxSamplesPerBucket ? Math.ceil(sampleCount / maxSamplesPerBucket) : 1;\n for (let j = start; j < end; j += stride) {\n max = Math.max(max, Math.abs(channel[j] ?? 0));\n }\n peaks.push(max);\n }\n if (!cancelled) {\n setState({ peaks, isLoading: false, error: null });\n }\n } finally {\n await audioContext.close().catch(() => {});\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n peaks: [],\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to decode peaks\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [buckets, fileUrl, maxBuckets, maxSamplesPerBucket]);\n\n return state;\n}\n","/** In-place radix-2 Cooley–Tukey FFT; `length` must be a power of 2 and >= 2. */\nexport function fftInPlace(re: Float64Array, im: Float64Array): void {\n const n = re.length;\n if (n !== im.length || n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"fftInPlace: length must be equal powers of 2 >= 2\");\n }\n\n let j = 0;\n for (let i = 0; i < n - 1; i += 1) {\n if (i < j) {\n const tr = re[i]!;\n const ti = im[i]!;\n re[i] = re[j]!;\n im[i] = im[j]!;\n re[j] = tr;\n im[j] = ti;\n }\n let k = n >> 1;\n while (k <= j) {\n j -= k;\n k >>= 1;\n }\n j += k;\n }\n\n for (let len = 2; len <= n; len <<= 1) {\n const ang = (-2 * Math.PI) / len;\n const wlenR = Math.cos(ang);\n const wlenI = Math.sin(ang);\n for (let i = 0; i < n; i += len) {\n let wr = 1;\n let wi = 0;\n const half = len >> 1;\n for (let k = 0; k < half; k += 1) {\n const u = i + k;\n const v = u + half;\n const tr = wr * re[v]! - wi * im[v]!;\n const ti = wr * im[v]! + wi * re[v]!;\n re[v] = re[u]! - tr;\n im[v] = im[u]! - ti;\n re[u] = re[u]! + tr;\n im[u] = im[u]! + ti;\n const nwr = wr * wlenR - wi * wlenI;\n const nwi = wr * wlenI + wi * wlenR;\n wr = nwr;\n wi = nwi;\n }\n }\n }\n}\n\n/** Magnitude spectrum for real input: length `n` (power of 2). Returns `n/2` magnitudes for bins 0..n/2-1. */\nexport function realFftMagnitudes(samples: Float64Array): Float64Array {\n const n = samples.length;\n if (n < 2 || (n & (n - 1)) !== 0) {\n throw new Error(\"realFftMagnitudes: length must be a power of 2 >= 2\");\n }\n const re = new Float64Array(n);\n const im = new Float64Array(n);\n for (let i = 0; i < n; i += 1) re[i] = samples[i]!;\n fftInPlace(re, im);\n const out = new Float64Array(n >> 1);\n for (let k = 0; k < n >> 1; k += 1) {\n out[k] = Math.hypot(re[k]!, im[k]!);\n }\n return out;\n}\n\nexport function hanningWindow(length: number): Float64Array {\n const w = new Float64Array(length);\n if (length === 1) {\n w[0] = 1;\n return w;\n }\n const denom = length - 1;\n for (let i = 0; i < length; i += 1) {\n w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / denom));\n }\n return w;\n}\n\nexport function clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(8192, Math.max(32, p));\n}\n","import { clampFftSize, hanningWindow, realFftMagnitudes } from \"../internal/fft\";\nimport { getAudioContextConstructor } from \"./getAudioContextConstructor\";\n\nexport type AnalyzeAudioFileOptions = {\n /** Number of time rows in the amplitude grid (and spectrogram if enabled). Default 128. */\n timeSlices?: number;\n /** Sub-buckets per time slice for the amplitude grid. Default 8. */\n samplesPerSlice?: number;\n /** When true, include `spectrogram` using windowed FFT per time slice. Default false. */\n spectrogram?: boolean;\n /** FFT length for spectrogram (power of 2). Default 1024. */\n fftSize?: number;\n /** Number of frequency bins to keep per row (first bins; capped by fftSize/2). Default 256. */\n frequencyBins?: number;\n /** Channel index, or `\"mix\"` for equal mix of all channels. Default 0. */\n channel?: number | \"mix\";\n};\n\nexport type AudioFileAnalysis = {\n duration: number;\n sampleRate: number;\n /** Peak amplitudes in [0, 1]: `timeSlices` rows × `samplesPerSlice` columns. */\n amplitudeGrid: number[][];\n /** Optional magnitude spectrogram rows, each length `frequencyBins`, normalized to [0, 1] globally. */\n spectrogram?: number[][];\n};\n\nfunction mixChannels(buffer: AudioBuffer): Float32Array {\n const { numberOfChannels, length } = buffer;\n if (numberOfChannels === 1) {\n return buffer.getChannelData(0);\n }\n const out = new Float32Array(length);\n const scale = 1 / numberOfChannels;\n for (let c = 0; c < numberOfChannels; c += 1) {\n const ch = buffer.getChannelData(c);\n for (let i = 0; i < length; i += 1) {\n out[i] += ch[i]! * scale;\n }\n }\n return out;\n}\n\nfunction getChannel(buffer: AudioBuffer, channel: number | \"mix\"): Float32Array {\n if (channel === \"mix\") return mixChannels(buffer);\n const idx = Math.max(0, Math.min(buffer.numberOfChannels - 1, channel));\n return buffer.getChannelData(idx);\n}\n\nfunction buildAmplitudeGrid(\n channel: Float32Array,\n timeSlices: number,\n samplesPerSlice: number,\n): number[][] {\n const grid: number[][] = [];\n const len = channel.length;\n if (len === 0) {\n for (let t = 0; t < timeSlices; t += 1) {\n grid.push(Array.from({ length: samplesPerSlice }, () => 0));\n }\n return grid;\n }\n\n const segmentLen = len / timeSlices;\n\n for (let t = 0; t < timeSlices; t += 1) {\n const row: number[] = [];\n const segStart = Math.floor(t * segmentLen);\n const segEnd = Math.floor((t + 1) * segmentLen);\n const segLen = Math.max(1, segEnd - segStart);\n const subLen = segLen / samplesPerSlice;\n\n for (let s = 0; s < samplesPerSlice; s += 1) {\n const a = Math.floor(segStart + s * subLen);\n const b = Math.min(segEnd, Math.floor(segStart + (s + 1) * subLen));\n let peak = 0;\n for (let i = a; i < b; i += 1) {\n peak = Math.max(peak, Math.abs(channel[i] ?? 0));\n }\n row.push(peak);\n }\n grid.push(row);\n }\n return grid;\n}\n\nfunction buildSpectrogram(\n channel: Float32Array,\n timeSlices: number,\n fftSize: number,\n frequencyBins: number,\n): { rows: number[][]; maxMag: number } {\n const rows: number[][] = [];\n let maxMag = 1e-12;\n const len = channel.length;\n const n = clampFftSize(fftSize);\n const half = n >> 1;\n const bins = Math.min(frequencyBins, half);\n const window = hanningWindow(n);\n\n if (len < n) {\n for (let t = 0; t < timeSlices; t += 1) {\n rows.push(Array.from({ length: bins }, () => 0));\n }\n return { rows, maxMag: 1 };\n }\n\n for (let t = 0; t < timeSlices; t += 1) {\n const start =\n timeSlices <= 1 ? 0 : Math.min(Math.floor((t * (len - n)) / (timeSlices - 1)), len - n);\n const frame = new Float64Array(n);\n for (let i = 0; i < n; i += 1) {\n frame[i] = (channel[start + i] ?? 0) * (window[i] ?? 0);\n }\n const mags = realFftMagnitudes(frame);\n const row: number[] = [];\n for (let k = 0; k < bins; k += 1) {\n const v = mags[k] ?? 0;\n row.push(v);\n maxMag = Math.max(maxMag, v);\n }\n rows.push(row);\n }\n\n return { rows, maxMag };\n}\n\n/**\n * Decodes an `AudioBuffer` into visualization-friendly grids (no network).\n */\nexport function analyzeAudioBuffer(\n buffer: AudioBuffer,\n options: AnalyzeAudioFileOptions = {},\n): AudioFileAnalysis {\n const timeSlices = Math.max(1, options.timeSlices ?? 128);\n const samplesPerSlice = Math.max(1, options.samplesPerSlice ?? 8);\n const wantSpec = Boolean(options.spectrogram);\n const fftSize = options.fftSize ?? 1024;\n const frequencyBins = Math.max(1, options.frequencyBins ?? 256);\n const channel = options.channel ?? 0;\n\n const data = getChannel(buffer, channel);\n const amplitudeGrid = buildAmplitudeGrid(data, timeSlices, samplesPerSlice);\n\n const result: AudioFileAnalysis = {\n duration: buffer.duration,\n sampleRate: buffer.sampleRate,\n amplitudeGrid,\n };\n\n if (wantSpec) {\n const { rows, maxMag } = buildSpectrogram(data, timeSlices, fftSize, frequencyBins);\n const norm = maxMag > 0 ? 1 / maxMag : 1;\n result.spectrogram = rows.map((row) => row.map((v) => v * norm));\n }\n\n return result;\n}\n\n/**\n * Fetches a URL, decodes audio to an `AudioBuffer`, runs {@link analyzeAudioBuffer}, then closes the temporary `AudioContext`.\n */\nexport async function analyzeAudioFile(\n fileUrl: string,\n options: AnalyzeAudioFileOptions = {},\n): Promise<AudioFileAnalysis> {\n const response = await fetch(fileUrl);\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);\n }\n const raw = await response.arrayBuffer();\n const Context = getAudioContextConstructor();\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const audioContext = new Context();\n try {\n const buffer = await audioContext.decodeAudioData(raw.slice(0));\n return analyzeAudioBuffer(buffer, options);\n } finally {\n await audioContext.close();\n }\n}\n","import { useEffect, useState } from \"react\";\nimport {\n type AnalyzeAudioFileOptions,\n type AudioFileAnalysis,\n analyzeAudioFile,\n} from \"./analyzeAudioFile\";\n\nexport type UseAudioFileAnalysisState = {\n data: AudioFileAnalysis | null;\n isLoading: boolean;\n error: string | null;\n};\n\n/** When extending {@link AnalyzeAudioFileOptions}, thread new fields through the destructuring below and the effect dependency array. */\nexport function useAudioFileAnalysis(\n fileUrl: string | null | undefined,\n options: AnalyzeAudioFileOptions = {},\n): UseAudioFileAnalysisState {\n const { timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel } = options;\n\n const [state, setState] = useState<UseAudioFileAnalysisState>({\n data: null,\n isLoading: false,\n error: null,\n });\n\n useEffect(() => {\n if (!fileUrl) {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n if (typeof window === \"undefined\") {\n setState({ data: null, isLoading: false, error: null });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true, error: null }));\n\n void (async () => {\n try {\n const data = await analyzeAudioFile(fileUrl, {\n timeSlices,\n samplesPerSlice,\n spectrogram,\n fftSize,\n frequencyBins,\n channel,\n });\n if (!cancelled) {\n setState({ data, isLoading: false, error: null });\n }\n } catch (error) {\n if (!cancelled) {\n setState({\n data: null,\n isLoading: false,\n error: error instanceof Error ? error.message : \"Failed to analyze audio file\",\n });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [fileUrl, timeSlices, samplesPerSlice, spectrogram, fftSize, frequencyBins, channel]);\n\n return state;\n}\n"],"names":["getAudioContextConstructor","useAudioPeaks","fileUrl","buckets","options","maxBuckets","maxSamplesPerBucket","state","setState","useState","useEffect","cancelled","prev","response","buffer","Context","audioContext","channel","safeBuckets","step","peaks","i","max","start","end","sampleCount","stride","j","error","fftInPlace","re","im","n","tr","ti","k","len","ang","wlenR","wlenI","wr","wi","half","u","v","nwr","nwi","realFftMagnitudes","samples","out","hanningWindow","length","w","denom","clampFftSize","p","mixChannels","numberOfChannels","scale","c","ch","getChannel","idx","buildAmplitudeGrid","timeSlices","samplesPerSlice","grid","t","segmentLen","row","segStart","segEnd","subLen","s","a","b","peak","buildSpectrogram","fftSize","frequencyBins","rows","maxMag","bins","window","frame","mags","analyzeAudioBuffer","wantSpec","data","amplitudeGrid","result","norm","analyzeAudioFile","raw","useAudioFileAnalysis","spectrogram"],"mappings":"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"}
|
|
@@ -3,5 +3,18 @@ export type UseAudioPeaksState = {
|
|
|
3
3
|
isLoading: boolean;
|
|
4
4
|
error: string | null;
|
|
5
5
|
};
|
|
6
|
-
export
|
|
6
|
+
export type UseAudioPeaksOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Hard cap for computed buckets to avoid very expensive loops.
|
|
9
|
+
* Default: 512.
|
|
10
|
+
*/
|
|
11
|
+
maxBuckets?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of samples scanned per bucket.
|
|
14
|
+
* Large files are sampled with a stride once this limit is exceeded.
|
|
15
|
+
* Default: 20_000.
|
|
16
|
+
*/
|
|
17
|
+
maxSamplesPerBucket?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function useAudioPeaks(fileUrl: string | null | undefined, buckets?: number, options?: UseAudioPeaksOptions): UseAudioPeaksState;
|
|
7
20
|
//# sourceMappingURL=useAudioPeaks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioPeaks.d.ts","sourceRoot":"","sources":["../../src/waveform/useAudioPeaks.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,OAAO,SAAK,
|
|
1
|
+
{"version":3,"file":"useAudioPeaks.d.ts","sourceRoot":"","sources":["../../src/waveform/useAudioPeaks.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,OAAO,SAAK,EACZ,OAAO,GAAE,oBAAyB,GACjC,kBAAkB,CAuEpB"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucaismyname/ginger",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"description": "A headless & batteries-included React audio-player primitive",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
9
|
"LICENSE",
|
|
10
|
-
"README.md"
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
11
12
|
],
|
|
12
13
|
"main": "./dist/index.cjs",
|
|
13
14
|
"module": "./dist/index.js",
|
|
@@ -70,7 +71,8 @@
|
|
|
70
71
|
"format": "biome format --write src",
|
|
71
72
|
"docs:api": "typedoc --options typedoc.json",
|
|
72
73
|
"check-bundle": "node scripts/check-bundle-size.mjs",
|
|
73
|
-
"
|
|
74
|
+
"verify:release": "npm run build && npm run test && npm run typecheck && npm run lint && npm run check-bundle",
|
|
75
|
+
"prepublishOnly": "npm run verify:release"
|
|
74
76
|
},
|
|
75
77
|
"peerDependencies": {
|
|
76
78
|
"react": ">=18",
|