@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 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
- - Getting started: [`docs/getting-started.md`](./docs/getting-started.md)
53
- - Testing guide: [`docs/guides/testing.md`](./docs/guides/testing.md)
54
- - Recipes: [`docs/guides/recipes.md`](./docs/guides/recipes.md)
55
- - Accessibility checklist: [`docs/guides/accessibility.md`](./docs/guides/accessibility.md)
56
- - Streaming adapters: [`docs/guides/streaming-adapters.md`](./docs/guides/streaming-adapters.md)
57
- - Components reference: [`docs/reference/components.md`](./docs/reference/components.md)
58
- - Hooks reference: [`docs/reference/hooks.md`](./docs/reference/hooks.md)
59
- - Subpath exports (waveform, EQ, spatial, transcript, remote, …): [`docs/reference/subpaths.md`](./docs/reference/subpaths.md)
60
- - Generated API docs: [`docs/api/index.html`](./docs/api/index.html)
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: [`../../.github/release-template.md`](../../.github/release-template.md)
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 build
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useGingerEqualizer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerEqualizer.test.d.ts","sourceRoot":"","sources":["../../src/equalizer/useGingerEqualizer.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store.test.d.ts.map
@@ -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"),v=require("../GingerSplitContexts-C7puo0M7.cjs");function h(n){return n.replace(/<[^>]+>/g,"").trim()}function T(n){const r=n.trim().replace(",",".").split(":");if(r.length===3){const s=Number(r[0]),t=Number(r[1]),c=Number(r[2]);return[s,t,c].every(Number.isFinite)?s*3600+t*60+c:Number.NaN}if(r.length===2){const s=Number(r[0]),t=Number(r[1]);return[s,t].every(Number.isFinite)?s*60+t:Number.NaN}return Number.NaN}function S(n){const i=n.indexOf("-->");if(i<0)return null;const r=n.slice(0,i).trim(),t=n.slice(i+3).trim().match(/^(\S+)/);return t?{start:r,end:t[1]}:null}function N(n){const i=[],r=n.replace(/\r\n/g,`
2
- `).split(/\n\s*\n/);for(const s of r){const t=s.split(`
3
- `).map(a=>a.trim()).filter(a=>a.length>0);if(t.length===0)continue;let c=0;/^\d+$/.test(t[0])&&(c=1);const o=t[c];if(!o)continue;const e=S(o);if(!e)continue;const m=T(e.start),u=T(e.end);if(!Number.isFinite(m)||!Number.isFinite(u))continue;const l=t.slice(c+1),f=h(l.join(`
4
- `));f&&i.push({startTime:m,endTime:u,text:f})}return i.sort((s,t)=>s.startTime-t.startTime)}function b(n){const i=[];let r=n.replace(/\r\n/g,`
5
- `);if(r.startsWith("WEBVTT")){const t=r.search(/\n\s*\n/);r=t>=0?r.slice(t).trim():""}const s=r.split(/\n\s*\n/);for(const t of s){const o=t.split(`
6
- `).map(p=>p.trimEnd());if(o.length===0||o[0].startsWith("NOTE")||o[0].startsWith("STYLE")||o[0].startsWith("REGION"))continue;let e=0,m,u=o[e];if(u.includes("-->")||(m=o[e],e+=1,u=o[e]),!(u!=null&&u.includes("-->")))continue;const l=S(u);if(!l)continue;const f=T(l.start),a=T(l.end);if(!Number.isFinite(f)||!Number.isFinite(a))continue;const F=o.slice(e+1).filter(p=>p.trim().length>0),g=h(F.join(`
7
- `));g&&i.push({startTime:f,endTime:a,text:g,...m?{id:m}:{}})}return i.sort((t,c)=>t.startTime-c.startTime)}function y(n){return n.trimStart().startsWith("WEBVTT")?b(n):N(n)}function x(n,i){return i==="vtt"?b(n):i==="srt"?N(n):y(n)}function W(n){const{transcript:i,format:r="auto"}=n,{currentTime:s}=v.useGingerMedia(),t=d.useMemo(()=>Array.isArray(i)?[...i].filter(e=>Number.isFinite(e.startTime)&&Number.isFinite(e.endTime)&&e.startTime>=0&&e.endTime>=e.startTime).sort((e,m)=>e.startTime-m.startTime):x(i,r),[i,r]),c=d.useMemo(()=>{for(let e=t.length-1;e>=0;e-=1)if(s>=t[e].startTime)return e;return-1},[s,t]),o=d.useMemo(()=>t.filter(e=>s>=e.startTime&&s<e.endTime),[s,t]);return{cues:t,activeIndex:c,activeCue:c>=0?t[c]??null:null,activeCues:o}}exports.parseSrt=N;exports.parseTimestampToSeconds=T;exports.parseTranscriptAuto=y;exports.parseVtt=b;exports.useGingerTranscriptSync=W;
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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"}
@@ -1,99 +1,104 @@
1
- import { useMemo as p } from "react";
2
- import { u as S } from "../GingerSplitContexts-BzBExb95.js";
3
- function d(n) {
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 i = n.trim().replace(",", ".").split(":");
8
- if (i.length === 3) {
9
- const s = Number(i[0]), t = Number(i[1]), o = Number(i[2]);
10
- return [s, t, o].every(Number.isFinite) ? s * 3600 + t * 60 + o : Number.NaN;
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 (i.length === 2) {
13
- const s = Number(i[0]), t = Number(i[1]);
14
- return [s, t].every(Number.isFinite) ? s * 60 + t : Number.NaN;
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 i = n.slice(0, r).trim(), t = n.slice(r + 3).trim().match(/^(\S+)/);
22
- return t ? { start: i, end: t[1] } : null;
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 = [], i = n.replace(/\r\n/g, `
25
+ const r = [], e = n.replace(/\r\n/g, `
26
26
  `).split(/\n\s*\n/);
27
- for (const s of i) {
28
- const t = s.split(`
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 o = 0;
32
- /^\d+$/.test(t[0]) && (o = 1);
33
- const c = t[o];
31
+ let s = 0;
32
+ /^\d+$/.test(t[0]) && (s = 1);
33
+ const c = t[s];
34
34
  if (!c) continue;
35
- const e = h(c);
36
- if (!e) continue;
37
- const m = T(e.start), u = T(e.end);
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(o + 1), f = d(l.join(`
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((s, t) => s.startTime - t.startTime);
43
+ return r.sort((i, t) => i.startTime - t.startTime);
44
44
  }
45
- function F(n) {
45
+ function x(n) {
46
46
  const r = [];
47
- let i = n.replace(/\r\n/g, `
47
+ let e = n.replace(/\r\n/g, `
48
48
  `);
49
- if (i.startsWith("WEBVTT")) {
50
- const t = i.search(/\n\s*\n/);
51
- i = t >= 0 ? i.slice(t).trim() : "";
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 s = i.split(/\n\s*\n/);
54
- for (const t of s) {
53
+ const i = e.split(/\n\s*\n/);
54
+ for (const t of i) {
55
55
  const c = t.split(`
56
- `).map((N) => N.trimEnd());
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 e = 0, m, u = c[e];
60
- if (u.includes("-->") || (m = c[e], e += 1, u = c[e]), !(u != null && u.includes("-->"))) continue;
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 x = c.slice(e + 1).filter((N) => N.trim().length > 0), b = d(x.join(`
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, o) => t.startTime - o.startTime);
69
+ return r.sort((t, s) => t.startTime - s.startTime);
70
70
  }
71
- function W(n) {
72
- return n.trimStart().startsWith("WEBVTT") ? F(n) : g(n);
71
+ function S(n) {
72
+ return n.trimStart().startsWith("WEBVTT") ? x(n) : g(n);
73
73
  }
74
- function v(n, r) {
75
- return r === "vtt" ? F(n) : r === "srt" ? g(n) : W(n);
74
+ function W(n, r) {
75
+ return r === "vtt" ? x(n) : r === "srt" ? g(n) : S(n);
76
76
  }
77
- function L(n) {
78
- const { transcript: r, format: i = "auto" } = n, { currentTime: s } = S(), t = p(() => Array.isArray(r) ? [...r].filter(
79
- (e) => Number.isFinite(e.startTime) && Number.isFinite(e.endTime) && e.startTime >= 0 && e.endTime >= e.startTime
80
- ).sort((e, m) => e.startTime - m.startTime) : v(r, i), [r, i]), o = p(() => {
81
- for (let e = t.length - 1; e >= 0; e -= 1)
82
- if (s >= t[e].startTime) return e;
83
- return -1;
84
- }, [s, t]), c = p(() => t.filter((e) => s >= e.startTime && s < e.endTime), [s, t]);
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: o,
88
- activeCue: o >= 0 ? t[o] ?? null : null,
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
- W as parseTranscriptAuto,
96
- F as parseVtt,
97
- L as useGingerTranscriptSync
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;AAQF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,yBAAyB,CAoC3B"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useGingerTranscriptSync.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerTranscriptSync.test.d.ts","sourceRoot":"","sources":["../../src/transcript/useGingerTranscriptSync.test.tsx"],"names":[],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("react");function p(){return typeof window>"u"?null:window.AudioContext??window.webkitAudioContext??null}function F(n,t=64){const[a,o]=y.useState({peaks:[],isLoading:!1,error:null});return y.useEffect(()=>{if(!n){o({peaks:[],isLoading:!1,error:null});return}if(typeof window>"u"){o({peaks:[],isLoading:!1,error:null});return}let r=!1;return o(e=>({...e,isLoading:!0,error:null})),(async()=>{try{const e=await fetch(n);if(!e.ok)throw new Error(`Fetch failed: ${e.status} ${e.statusText}`);const l=await e.arrayBuffer(),s=p();if(!s)throw new Error("Web Audio API is not available");const h=new s;try{const u=(await h.decodeAudioData(l)).getChannelData(0),i=Math.max(1,Math.floor(u.length/t)),w=[];for(let f=0;f<t;f+=1){let g=0;const M=f*i,d=Math.min(u.length,M+i);for(let m=M;m<d;m+=1)g=Math.max(g,Math.abs(u[m]??0));w.push(g)}r||o({peaks:w,isLoading:!1,error:null})}finally{await h.close().catch(()=>{})}}catch(e){r||o({peaks:[],isLoading:!1,error:e instanceof Error?e.message:"Failed to decode peaks"})}})(),()=>{r=!0}},[t,n]),a}function L(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 o=0;for(let r=0;r<a-1;r+=1){if(r<o){const l=n[r],s=t[r];n[r]=n[o],t[r]=t[o],n[o]=l,t[o]=s}let e=a>>1;for(;e<=o;)o-=e,e>>=1;o+=e}for(let r=2;r<=a;r<<=1){const e=-2*Math.PI/r,l=Math.cos(e),s=Math.sin(e);for(let h=0;h<a;h+=r){let c=1,u=0;const i=r>>1;for(let w=0;w<i;w+=1){const f=h+w,g=f+i,M=c*n[g]-u*t[g],d=c*t[g]+u*n[g];n[g]=n[f]-M,t[g]=t[f]-d,n[f]=n[f]+M,t[f]=t[f]+d;const m=c*l-u*s,C=c*s+u*l;c=m,u=C}}}}function k(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),o=new Float64Array(t);for(let e=0;e<t;e+=1)a[e]=n[e];L(a,o);const r=new Float64Array(t>>1);for(let e=0;e<t>>1;e+=1)r[e]=Math.hypot(a[e],o[e]);return r}function b(n){const t=new Float64Array(n);if(n===1)return t[0]=1,t;const a=n-1;for(let o=0;o<n;o+=1)t[o]=.5*(1-Math.cos(2*Math.PI*o/a));return t}function S(n){const t=2**Math.round(Math.log2(n));return Math.min(8192,Math.max(32,t))}function E(n){const{numberOfChannels:t,length:a}=n;if(t===1)return n.getChannelData(0);const o=new Float32Array(a),r=1/t;for(let e=0;e<t;e+=1){const l=n.getChannelData(e);for(let s=0;s<a;s+=1)o[s]+=l[s]*r}return o}function B(n,t){if(t==="mix")return E(n);const a=Math.max(0,Math.min(n.numberOfChannels-1,t));return n.getChannelData(a)}function P(n,t,a){const o=[],r=n.length;if(r===0){for(let l=0;l<t;l+=1)o.push(Array.from({length:a},()=>0));return o}const e=r/t;for(let l=0;l<t;l+=1){const s=[],h=Math.floor(l*e),c=Math.floor((l+1)*e),i=Math.max(1,c-h)/a;for(let w=0;w<a;w+=1){const f=Math.floor(h+w*i),g=Math.min(c,Math.floor(h+(w+1)*i));let M=0;for(let d=f;d<g;d+=1)M=Math.max(M,Math.abs(n[d]??0));s.push(M)}o.push(s)}return o}function z(n,t,a,o){const r=[];let e=1e-12;const l=n.length,s=S(a),h=s>>1,c=Math.min(o,h),u=b(s);if(l<s){for(let i=0;i<t;i+=1)r.push(Array.from({length:c},()=>0));return{rows:r,maxMag:1}}for(let i=0;i<t;i+=1){const w=t<=1?0:Math.min(Math.floor(i*(l-s)/(t-1)),l-s),f=new Float64Array(s);for(let d=0;d<s;d+=1)f[d]=(n[w+d]??0)*(u[d]??0);const g=k(f),M=[];for(let d=0;d<c;d+=1){const m=g[d]??0;M.push(m),e=Math.max(e,m)}r.push(M)}return{rows:r,maxMag:e}}function A(n,t={}){const a=Math.max(1,t.timeSlices??128),o=Math.max(1,t.samplesPerSlice??8),r=!!t.spectrogram,e=t.fftSize??1024,l=Math.max(1,t.frequencyBins??256),s=t.channel??0,h=B(n,s),c=P(h,a,o),u={duration:n.duration,sampleRate:n.sampleRate,amplitudeGrid:c};if(r){const{rows:i,maxMag:w}=z(h,a,e,l),f=w>0?1/w:1;u.spectrogram=i.map(g=>g.map(M=>M*f))}return u}async function x(n,t={}){const a=await fetch(n);if(!a.ok)throw new Error(`Fetch failed: ${a.status} ${a.statusText}`);const o=await a.arrayBuffer(),r=p();if(!r)throw new Error("Web Audio API is not available");const e=new r;try{const l=await e.decodeAudioData(o.slice(0));return A(l,t)}finally{await e.close()}}function v(n,t={}){const{timeSlices:a,samplesPerSlice:o,spectrogram:r,fftSize:e,frequencyBins:l,channel:s}=t,[h,c]=y.useState({data:null,isLoading:!1,error:null});return y.useEffect(()=>{if(!n){c({data:null,isLoading:!1,error:null});return}if(typeof window>"u"){c({data:null,isLoading:!1,error:null});return}let u=!1;return c(i=>({...i,isLoading:!0,error:null})),(async()=>{try{const i=await x(n,{timeSlices:a,samplesPerSlice:o,spectrogram:r,fftSize:e,frequencyBins:l,channel:s});u||c({data:i,isLoading:!1,error:null})}catch(i){u||c({data:null,isLoading:!1,error:i instanceof Error?i.message:"Failed to analyze audio file"})}})(),()=>{u=!0}},[n,a,o,r,e,l,s]),h}exports.analyzeAudioBuffer=A;exports.analyzeAudioFile=x;exports.useAudioFileAnalysis=v;exports.useAudioPeaks=F;
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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"}
@@ -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;AAC1D,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"}
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"}
@@ -1,242 +1,242 @@
1
- import { useState as p, useEffect as y } from "react";
2
- function x() {
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 v(n, t = 64) {
6
- const [a, o] = p({
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 y(() => {
11
+ return L(() => {
12
12
  if (!n) {
13
- o({ peaks: [], isLoading: !1, error: null });
13
+ s({ peaks: [], isLoading: !1, error: null });
14
14
  return;
15
15
  }
16
16
  if (typeof window > "u") {
17
- o({ peaks: [], isLoading: !1, error: null });
17
+ s({ peaks: [], isLoading: !1, error: null });
18
18
  return;
19
19
  }
20
- let r = !1;
21
- return o((e) => ({ ...e, isLoading: !0, error: null })), (async () => {
20
+ let l = !1;
21
+ return s((c) => ({ ...c, isLoading: !0, error: null })), (async () => {
22
22
  try {
23
- const e = await fetch(n);
24
- if (!e.ok)
25
- throw new Error(`Fetch failed: ${e.status} ${e.statusText}`);
26
- const l = await e.arrayBuffer(), s = x();
27
- if (!s)
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 h = new s();
29
+ const i = new h();
30
30
  try {
31
- const f = (await h.decodeAudioData(l)).getChannelData(0), i = Math.max(1, Math.floor(f.length / t)), w = [];
32
- for (let u = 0; u < t; u += 1) {
33
- let g = 0;
34
- const m = u * i, d = Math.min(f.length, m + i);
35
- for (let M = m; M < d; M += 1)
36
- g = Math.max(g, Math.abs(f[M] ?? 0));
37
- w.push(g);
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
- r || o({ peaks: w, isLoading: !1, error: null });
39
+ l || s({ peaks: f, isLoading: !1, error: null });
40
40
  } finally {
41
- await h.close().catch(() => {
41
+ await i.close().catch(() => {
42
42
  });
43
43
  }
44
- } catch (e) {
45
- r || o({
44
+ } catch (c) {
45
+ l || s({
46
46
  peaks: [],
47
47
  isLoading: !1,
48
- error: e instanceof Error ? e.message : "Failed to decode peaks"
48
+ error: c instanceof Error ? c.message : "Failed to decode peaks"
49
49
  });
50
50
  }
51
51
  })(), () => {
52
- r = !0;
52
+ l = !0;
53
53
  };
54
- }, [t, n]), a;
54
+ }, [t, n, r, o]), e;
55
55
  }
56
- function C(n, t) {
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 o = 0;
61
- for (let r = 0; r < a - 1; r += 1) {
62
- if (r < o) {
63
- const l = n[r], s = t[r];
64
- n[r] = n[o], t[r] = t[o], n[o] = l, t[o] = s;
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 <= o; )
68
- o -= e, e >>= 1;
69
- o += e;
67
+ for (; e <= r; )
68
+ r -= e, e >>= 1;
69
+ r += e;
70
70
  }
71
- for (let r = 2; r <= a; r <<= 1) {
72
- const e = -2 * Math.PI / r, l = Math.cos(e), s = Math.sin(e);
73
- for (let h = 0; h < a; h += r) {
74
- let c = 1, f = 0;
75
- const i = r >> 1;
76
- for (let w = 0; w < i; w += 1) {
77
- const u = h + w, g = u + i, m = c * n[g] - f * t[g], d = c * t[g] + f * n[g];
78
- n[g] = n[u] - m, t[g] = t[u] - d, n[u] = n[u] + m, t[u] = t[u] + d;
79
- const M = c * l - f * s, A = c * s + f * l;
80
- c = M, f = A;
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 L(n) {
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), o = 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
- C(a, o);
92
- const r = new Float64Array(t >> 1);
91
+ b(a, r);
92
+ const o = new Float64Array(t >> 1);
93
93
  for (let e = 0; e < t >> 1; e += 1)
94
- r[e] = Math.hypot(a[e], o[e]);
95
- return r;
94
+ o[e] = Math.hypot(a[e], r[e]);
95
+ return o;
96
96
  }
97
- function F(n) {
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 o = 0; o < n; o += 1)
103
- t[o] = 0.5 * (1 - Math.cos(2 * Math.PI * o / a));
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 k(n) {
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 b(n) {
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 o = new Float32Array(a), r = 1 / t;
114
+ const r = new Float32Array(a), o = 1 / t;
115
115
  for (let e = 0; e < t; e += 1) {
116
- const l = n.getChannelData(e);
117
- for (let s = 0; s < a; s += 1)
118
- o[s] += l[s] * r;
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 o;
120
+ return r;
121
121
  }
122
- function E(n, t) {
123
- if (t === "mix") return b(n);
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 B(n, t, a) {
128
- const o = [], r = n.length;
129
- if (r === 0) {
130
- for (let l = 0; l < t; l += 1)
131
- o.push(Array.from({ length: a }, () => 0));
132
- return o;
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 = r / t;
135
- for (let l = 0; l < t; l += 1) {
136
- const s = [], h = Math.floor(l * e), c = Math.floor((l + 1) * e), i = Math.max(1, c - h) / a;
137
- for (let w = 0; w < a; w += 1) {
138
- const u = Math.floor(h + w * i), g = Math.min(c, Math.floor(h + (w + 1) * i));
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 d = u; d < g; d += 1)
141
- m = Math.max(m, Math.abs(n[d] ?? 0));
142
- s.push(m);
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
- o.push(s);
144
+ r.push(l);
145
145
  }
146
- return o;
146
+ return r;
147
147
  }
148
- function S(n, t, a, o) {
149
- const r = [];
148
+ function D(n, t, a, r) {
149
+ const o = [];
150
150
  let e = 1e-12;
151
- const l = n.length, s = k(a), h = s >> 1, c = Math.min(o, h), f = F(s);
152
- if (l < s) {
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
- r.push(Array.from({ length: c }, () => 0));
155
- return { rows: r, maxMag: 1 };
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 w = t <= 1 ? 0 : Math.min(Math.floor(i * (l - s) / (t - 1)), l - s), u = new Float64Array(s);
159
- for (let d = 0; d < s; d += 1)
160
- u[d] = (n[w + d] ?? 0) * (f[d] ?? 0);
161
- const g = L(u), m = [];
162
- for (let d = 0; d < c; d += 1) {
163
- const M = g[d] ?? 0;
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
- r.push(m);
166
+ o.push(m);
167
167
  }
168
- return { rows: r, maxMag: e };
168
+ return { rows: o, maxMag: e };
169
169
  }
170
- function P(n, t = {}) {
171
- const a = Math.max(1, t.timeSlices ?? 128), o = Math.max(1, t.samplesPerSlice ?? 8), r = !!t.spectrogram, e = t.fftSize ?? 1024, l = Math.max(1, t.frequencyBins ?? 256), s = t.channel ?? 0, h = E(n, s), c = B(h, a, o), f = {
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: c
174
+ amplitudeGrid: u
175
175
  };
176
- if (r) {
177
- const { rows: i, maxMag: w } = S(h, a, e, l), u = w > 0 ? 1 / w : 1;
178
- f.spectrogram = i.map((g) => g.map((m) => m * u));
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 f;
180
+ return h;
181
181
  }
182
- async function z(n, t = {}) {
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 o = await a.arrayBuffer(), r = x();
187
- if (!r)
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 r();
189
+ const e = new o();
190
190
  try {
191
- const l = await e.decodeAudioData(o.slice(0));
192
- return P(l, t);
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 D(n, t = {}) {
198
- const { timeSlices: a, samplesPerSlice: o, spectrogram: r, fftSize: e, frequencyBins: l, channel: s } = t, [h, c] = p({
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 y(() => {
203
+ return L(() => {
204
204
  if (!n) {
205
- c({ data: null, isLoading: !1, error: null });
205
+ u({ data: null, isLoading: !1, error: null });
206
206
  return;
207
207
  }
208
208
  if (typeof window > "u") {
209
- c({ data: null, isLoading: !1, error: null });
209
+ u({ data: null, isLoading: !1, error: null });
210
210
  return;
211
211
  }
212
- let f = !1;
213
- return c((i) => ({ ...i, isLoading: !0, error: null })), (async () => {
212
+ let h = !1;
213
+ return u((i) => ({ ...i, isLoading: !0, error: null })), (async () => {
214
214
  try {
215
- const i = await z(n, {
215
+ const i = await $(n, {
216
216
  timeSlices: a,
217
- samplesPerSlice: o,
218
- spectrogram: r,
217
+ samplesPerSlice: r,
218
+ spectrogram: o,
219
219
  fftSize: e,
220
- frequencyBins: l,
221
- channel: s
220
+ frequencyBins: s,
221
+ channel: l
222
222
  });
223
- f || c({ data: i, isLoading: !1, error: null });
223
+ h || u({ data: i, isLoading: !1, error: null });
224
224
  } catch (i) {
225
- f || c({
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
- f = !0;
232
+ h = !0;
233
233
  };
234
- }, [n, a, o, r, e, l, s]), h;
234
+ }, [n, a, r, o, e, s, l]), c;
235
235
  }
236
236
  export {
237
- P as analyzeAudioBuffer,
238
- z as analyzeAudioFile,
239
- D as useAudioFileAnalysis,
240
- v as useAudioPeaks
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":"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"}
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 declare function useAudioPeaks(fileUrl: string | null | undefined, buckets?: number): UseAudioPeaksState;
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,GACX,kBAAkB,CAkEpB"}
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.38",
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
- "prepublishOnly": "npm run build"
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",