@lucaismyname/ginger 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.cjs +1 -1
- package/dist/client.js +37 -36
- package/dist/client.js.map +1 -1
- package/dist/equalizer/index.cjs +1 -1
- package/dist/equalizer/index.cjs.map +1 -1
- package/dist/equalizer/index.js +16 -15
- package/dist/equalizer/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +37 -36
- package/dist/index.js.map +1 -1
- package/dist/liveAudioGraph-0cpHD_Ic.cjs +2 -0
- package/dist/liveAudioGraph-0cpHD_Ic.cjs.map +1 -0
- package/dist/liveAudioGraph-DvPaxBCP.js +105 -0
- package/dist/liveAudioGraph-DvPaxBCP.js.map +1 -0
- package/dist/remote/index.cjs +2 -0
- package/dist/remote/index.cjs.map +1 -0
- package/dist/remote/index.d.ts +5 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +149 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/remoteProtocol.d.ts +28 -0
- package/dist/remote/remoteProtocol.d.ts.map +1 -0
- package/dist/remote/useGingerRemote.d.ts +35 -0
- package/dist/remote/useGingerRemote.d.ts.map +1 -0
- package/dist/spatial/index.cjs +2 -0
- package/dist/spatial/index.cjs.map +1 -0
- package/dist/spatial/index.d.ts +3 -0
- package/dist/spatial/index.d.ts.map +1 -0
- package/dist/spatial/index.js +59 -0
- package/dist/spatial/index.js.map +1 -0
- package/dist/spatial/useGingerSpatialAudio.d.ts +34 -0
- package/dist/spatial/useGingerSpatialAudio.d.ts.map +1 -0
- package/dist/spatial/useGingerSpatialAudio.test.d.ts +2 -0
- package/dist/spatial/useGingerSpatialAudio.test.d.ts.map +1 -0
- package/dist/testing/mockWebAudio.d.ts +14 -0
- package/dist/testing/mockWebAudio.d.ts.map +1 -1
- package/dist/transcript/index.cjs +8 -0
- package/dist/transcript/index.cjs.map +1 -0
- package/dist/transcript/index.d.ts +5 -0
- package/dist/transcript/index.d.ts.map +1 -0
- package/dist/transcript/index.js +99 -0
- package/dist/transcript/index.js.map +1 -0
- package/dist/transcript/parseTranscript.d.ts +27 -0
- package/dist/transcript/parseTranscript.d.ts.map +1 -0
- package/dist/transcript/parseTranscript.test.d.ts +2 -0
- package/dist/transcript/parseTranscript.test.d.ts.map +1 -0
- package/dist/transcript/useGingerTranscriptSync.d.ts +23 -0
- package/dist/transcript/useGingerTranscriptSync.d.ts.map +1 -0
- package/dist/useGinger-BXgia32v.cjs +2 -0
- package/dist/useGinger-BXgia32v.cjs.map +1 -0
- package/dist/useGinger-hpp2pAGY.js +48 -0
- package/dist/useGinger-hpp2pAGY.js.map +1 -0
- package/dist/useGingerChapterProgress-BdaalJvX.cjs +2 -0
- package/dist/{useGingerChapterProgress-BOqUimE7.cjs.map → useGingerChapterProgress-BdaalJvX.cjs.map} +1 -1
- package/dist/{useGingerChapterProgress-DLYdGytK.js → useGingerChapterProgress-CZdv-HiI.js} +23 -22
- package/dist/{useGingerChapterProgress-DLYdGytK.js.map → useGingerChapterProgress-CZdv-HiI.js.map} +1 -1
- package/package.json +17 -2
- package/dist/liveAudioGraph-CmEsdLgZ.js +0 -150
- package/dist/liveAudioGraph-CmEsdLgZ.js.map +0 -1
- package/dist/liveAudioGraph-D1BXMv_u.cjs +0 -2
- package/dist/liveAudioGraph-D1BXMv_u.cjs.map +0 -1
- package/dist/useGingerChapterProgress-BOqUimE7.cjs +0 -2
package/dist/client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-NEcOSSJD.cjs"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-NEcOSSJD.cjs"),s=require("./useGinger-BXgia32v.cjs"),r=require("./useGingerChapterProgress-BdaalJvX.cjs"),i=require("./liveAudioGraph-0cpHD_Ic.cjs"),n=require("./selectors-YXnP8Y8g.cjs"),a=require("./GingerSplitContexts-C7puo0M7.cjs");exports.Chapters=e.Chapters;exports.Ginger=e.Ginger;exports.LyricsSynced=e.LyricsSynced;exports.Pause=e.Pause;exports.Play=e.Play;exports.RepeatGlyph=e.RepeatGlyph;exports.ShuffleIcon=e.ShuffleIcon;exports.SkipBack=e.SkipBack;exports.SkipForward=e.SkipForward;exports.Volume2=e.Volume2;exports.VolumeX=e.VolumeX;exports.Wrapper=e.Wrapper;exports.clampPlaybackRate=e.clampPlaybackRate;exports.clampVolume=e.clampVolume;exports.defaultGingerLocale=e.defaultGingerLocale;exports.parseLrc=e.parseLrc;exports.useGingerChapters=e.useGingerChapters;exports.useGingerLocale=e.useGingerLocale;exports.useGingerLyricsSync=e.useGingerLyricsSync;exports.usePlayPauseBinding=e.usePlayPauseBinding;exports.useSeekBarBinding=e.useSeekBarBinding;exports.useVolumeSlider=e.useVolumeSlider;exports.useGinger=s.useGinger;exports.createGingerStore=r.createGingerStore;exports.useGingerChapterProgress=r.useGingerChapterProgress;exports.useGingerDebugLog=r.useGingerDebugLog;exports.useGingerKeyboardShortcuts=r.useGingerKeyboardShortcuts;exports.useGingerLiveAnalyzer=r.useGingerLiveAnalyzer;exports.useGingerPlaybackHistory=r.useGingerPlaybackHistory;exports.useGingerSleepTimer=r.useGingerSleepTimer;exports.useGingerVolumeFade=r.useGingerVolumeFade;exports.useNextTrackPrefetch=r.useNextTrackPrefetch;exports.useSeekDrag=r.useSeekDrag;exports.attachLiveAnalyser=i.attachLiveAnalyser;exports.detachLiveAnalyser=i.detachLiveAnalyser;exports.setProcessingChain=i.setProcessingChain;exports.derivePlaybackUiState=n.derivePlaybackUiState;exports.gingerStateFromContextValues=a.gingerStateFromContextValues;exports.gingerStateFromContexts=a.gingerStateFromContexts;exports.useGingerMedia=a.useGingerMedia;exports.useGingerPlayback=a.useGingerPlayback;exports.useGingerState=a.useGingerState;
|
|
2
2
|
//# sourceMappingURL=client.cjs.map
|
package/dist/client.js
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
import { C as s, G as r, L as i, P as
|
|
2
|
-
import {
|
|
3
|
-
import { c as
|
|
4
|
-
import { d as
|
|
5
|
-
import {
|
|
1
|
+
import { C as s, G as r, L as i, P as t, a as u, R as n, S as o, b as g, c, V as l, d as p, W as G, e as m, f as y, g as S, p as d, u as f, h, i as P, j as k, k as L, l as b } from "./ginger-L2ZFgzH4.js";
|
|
2
|
+
import { u as C } from "./useGinger-hpp2pAGY.js";
|
|
3
|
+
import { c as v, u as B, a as F, b as A, d as R, e as D, f as T, g as W, h as j, i as w } from "./useGingerChapterProgress-CZdv-HiI.js";
|
|
4
|
+
import { a as H, d as I, s as K } from "./liveAudioGraph-DvPaxBCP.js";
|
|
5
|
+
import { d as N } from "./selectors-BalBCc7X.js";
|
|
6
|
+
import { g as X, a as q, u as E, b as J, c as O } from "./GingerSplitContexts-BzBExb95.js";
|
|
6
7
|
export {
|
|
7
8
|
s as Chapters,
|
|
8
9
|
r as Ginger,
|
|
9
10
|
i as LyricsSynced,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
t as Pause,
|
|
12
|
+
u as Play,
|
|
12
13
|
n as RepeatGlyph,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
o as ShuffleIcon,
|
|
15
|
+
g as SkipBack,
|
|
15
16
|
c as SkipForward,
|
|
16
17
|
l as Volume2,
|
|
17
18
|
p as VolumeX,
|
|
18
19
|
G as Wrapper,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
H as attachLiveAnalyser,
|
|
21
|
+
m as clampPlaybackRate,
|
|
22
|
+
y as clampVolume,
|
|
23
|
+
v as createGingerStore,
|
|
24
|
+
S as defaultGingerLocale,
|
|
25
|
+
N as derivePlaybackUiState,
|
|
26
|
+
I as detachLiveAnalyser,
|
|
27
|
+
X as gingerStateFromContextValues,
|
|
28
|
+
q as gingerStateFromContexts,
|
|
29
|
+
d as parseLrc,
|
|
30
|
+
K as setProcessingChain,
|
|
31
|
+
C as useGinger,
|
|
32
|
+
B as useGingerChapterProgress,
|
|
33
|
+
f as useGingerChapters,
|
|
34
|
+
F as useGingerDebugLog,
|
|
35
|
+
A as useGingerKeyboardShortcuts,
|
|
36
|
+
R as useGingerLiveAnalyzer,
|
|
37
|
+
h as useGingerLocale,
|
|
38
|
+
P as useGingerLyricsSync,
|
|
39
|
+
E as useGingerMedia,
|
|
40
|
+
J as useGingerPlayback,
|
|
41
|
+
D as useGingerPlaybackHistory,
|
|
42
|
+
T as useGingerSleepTimer,
|
|
43
|
+
O as useGingerState,
|
|
44
|
+
W as useGingerVolumeFade,
|
|
45
|
+
j as useNextTrackPrefetch,
|
|
45
46
|
k as usePlayPauseBinding,
|
|
46
47
|
L as useSeekBarBinding,
|
|
47
|
-
|
|
48
|
+
w as useSeekDrag,
|
|
48
49
|
b as useVolumeSlider
|
|
49
50
|
};
|
|
50
51
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
package/dist/equalizer/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react"),a=require("../liveAudioGraph-0cpHD_Ic.cjs"),B=require("../useGinger-BXgia32v.cjs"),C=[{frequency:60},{frequency:250},{frequency:1e3},{frequency:4e3},{frequency:16e3}];function E(y={}){const{enabled:d=!0,bands:m=C}=y,{audioRef:o,state:h}=B.useGinger(),[l,g]=s.useState(m),[p,q]=s.useState(null),c=s.useRef([]);s.useEffect(()=>{const e=o.current;if(!(!e||typeof window>"u")){if(!d){a.setProcessingChain(e,[]),c.current=[];return}try{const t=a.attachLiveAnalyser(e,{fftSize:32,smoothingTimeConstant:0,minDecibels:-100,maxDecibels:0}),{context:n,id:f}=t,i=l.map(r=>{const u=n.createBiquadFilter();return u.type=r.type??"peaking",u.frequency.value=r.frequency,u.gain.value=r.gain??0,u.Q.value=r.q??1,u});c.current=i,a.setProcessingChain(e,i),a.detachLiveAnalyser(e,f),q(null)}catch(t){const n=t instanceof Error?t.message:"Failed to create equalizer";q(n),c.current=[]}return()=>{const t=o.current;t&&a.setProcessingChain(t,[]),c.current=[]}}},[d,l,o,h.currentIndex]);const v=s.useCallback((e,t)=>{const n=c.current[e];n&&(n.gain.value=t),g(f=>f.map((i,r)=>r===e?{...i,gain:t}:i))},[]),S=s.useCallback(e=>{g(e)},[]);return{setBandGain:v,setBands:S,bands:l,error:p}}exports.useGingerEqualizer=E;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/equalizer/useGingerEqualizer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\nexport type EqualizerBand = {\n /** Center frequency in Hz (e.g. 60, 250, 1000, 4000, 16000). */\n frequency: number;\n /** Gain in dB. Positive = boost, negative = cut. Typical range: -12 to +12. Default: 0. */\n gain?: number;\n /** Q factor (bandwidth). Higher = narrower. Default: 1. */\n q?: number;\n /** BiquadFilterNode type. Default: \"peaking\". */\n type?: BiquadFilterType;\n};\n\nexport type UseGingerEqualizerOptions = {\n /** When false, EQ nodes are detached and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Band definitions. Each band maps to one BiquadFilterNode. */\n bands?: EqualizerBand[];\n};\n\nexport type UseGingerEqualizerResult = {\n /** Update a single band's gain in dB without rebuilding the filter chain. */\n setBandGain: (bandIndex: number, gainDb: number) => void;\n /** Replace all bands (rebuilds the filter chain). */\n setBands: (bands: EqualizerBand[]) => void;\n /** Current band definitions (reflects the latest `setBands` call). */\n bands: EqualizerBand[];\n /** Error string if Web Audio is unavailable or filter creation failed. */\n error: string | null;\n};\n\nconst DEFAULT_BANDS: EqualizerBand[] = [\n { frequency: 60 },\n { frequency: 250 },\n { frequency: 1000 },\n { frequency: 4000 },\n { frequency: 16000 },\n];\n\n/**\n * Inserts a parametric EQ into the Web Audio graph for the active Ginger media element.\n *\n * Each band is a `BiquadFilterNode` connected in series between the audio source and the\n * speakers. If `useGingerLiveAnalyzer` is also active, the EQ is inserted before the analyser\n * — both share the same `AudioContext` (the browser allows only one `MediaElementAudioSourceNode`\n * per element).\n *\n * Available as a subpath export:\n * ```ts\n * import { useGingerEqualizer } from \"@lucaismyname/ginger/equalizer\";\n * ```\n */\nexport function useGingerEqualizer(\n options: UseGingerEqualizerOptions = {},\n): UseGingerEqualizerResult {\n const { enabled = true, bands: initialBands = DEFAULT_BANDS } = options;\n const { audioRef, state } = useGinger();\n\n const [bands, setBandsState] = useState<EqualizerBand[]>(initialBands);\n const [error, setError] = useState<string | null>(null);\n\n const filterNodesRef = useRef<BiquadFilterNode[]>([]);\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n filterNodesRef.current = [];\n return;\n }\n\n try {\n // Attach a temporary analyser solely to get (or create) the shared AudioContext.\n // The graph is rebuilt by liveAudioGraph after we call setProcessingChain, so this\n // temporary consumer is detached immediately after we have the context reference.\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const filters: BiquadFilterNode[] = bands.map((band) => {\n const node = context.createBiquadFilter();\n node.type = band.type ?? \"peaking\";\n node.frequency.value = band.frequency;\n node.gain.value = band.gain ?? 0;\n node.Q.value = band.q ?? 1;\n return node;\n });\n\n filterNodesRef.current = filters;\n\n // Install processing chain BEFORE removing the temp analyser so the graph stays valid\n setProcessingChain(el, filters);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create equalizer\";\n setError(msg);\n filterNodesRef.current = [];\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n filterNodesRef.current = [];\n };\n }, [enabled, bands, audioRef, state.currentIndex]);\n\n const setBandGain = useCallback((bandIndex: number, gainDb: number) => {\n const node = filterNodesRef.current[bandIndex];\n if (node) {\n node.gain.value = gainDb;\n }\n setBandsState((prev) => prev.map((b, i) => (i === bandIndex ? { ...b, gain: gainDb } : b)));\n }, []);\n\n const setBands = useCallback((nextBands: EqualizerBand[]) => {\n setBandsState(nextBands);\n }, []);\n\n return { setBandGain, setBands, bands, error };\n}\n"],"names":["DEFAULT_BANDS","useGingerEqualizer","options","enabled","initialBands","audioRef","state","useGinger","bands","setBandsState","useState","error","setError","filterNodesRef","useRef","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","context","tempId","filters","band","node","detachLiveAnalyser","e","msg","element","setBandGain","useCallback","bandIndex","gainDb","prev","b","i","setBands","nextBands"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/equalizer/useGingerEqualizer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\nexport type EqualizerBand = {\n /** Center frequency in Hz (e.g. 60, 250, 1000, 4000, 16000). */\n frequency: number;\n /** Gain in dB. Positive = boost, negative = cut. Typical range: -12 to +12. Default: 0. */\n gain?: number;\n /** Q factor (bandwidth). Higher = narrower. Default: 1. */\n q?: number;\n /** BiquadFilterNode type. Default: \"peaking\". */\n type?: BiquadFilterType;\n};\n\nexport type UseGingerEqualizerOptions = {\n /** When false, EQ nodes are detached and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Band definitions. Each band maps to one BiquadFilterNode. */\n bands?: EqualizerBand[];\n};\n\nexport type UseGingerEqualizerResult = {\n /** Update a single band's gain in dB without rebuilding the filter chain. */\n setBandGain: (bandIndex: number, gainDb: number) => void;\n /** Replace all bands (rebuilds the filter chain). */\n setBands: (bands: EqualizerBand[]) => void;\n /** Current band definitions (reflects the latest `setBands` call). */\n bands: EqualizerBand[];\n /** Error string if Web Audio is unavailable or filter creation failed. */\n error: string | null;\n};\n\nconst DEFAULT_BANDS: EqualizerBand[] = [\n { frequency: 60 },\n { frequency: 250 },\n { frequency: 1000 },\n { frequency: 4000 },\n { frequency: 16000 },\n];\n\n/**\n * Inserts a parametric EQ into the Web Audio graph for the active Ginger media element.\n *\n * Each band is a `BiquadFilterNode` connected in series between the audio source and the\n * speakers. If `useGingerLiveAnalyzer` is also active, the EQ is inserted before the analyser\n * — both share the same `AudioContext` (the browser allows only one `MediaElementAudioSourceNode`\n * per element).\n *\n * Available as a subpath export:\n * ```ts\n * import { useGingerEqualizer } from \"@lucaismyname/ginger/equalizer\";\n * ```\n */\nexport function useGingerEqualizer(\n options: UseGingerEqualizerOptions = {},\n): UseGingerEqualizerResult {\n const { enabled = true, bands: initialBands = DEFAULT_BANDS } = options;\n const { audioRef, state } = useGinger();\n\n const [bands, setBandsState] = useState<EqualizerBand[]>(initialBands);\n const [error, setError] = useState<string | null>(null);\n\n const filterNodesRef = useRef<BiquadFilterNode[]>([]);\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n filterNodesRef.current = [];\n return;\n }\n\n try {\n // Attach a temporary analyser solely to get (or create) the shared AudioContext.\n // The graph is rebuilt by liveAudioGraph after we call setProcessingChain, so this\n // temporary consumer is detached immediately after we have the context reference.\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const filters: BiquadFilterNode[] = bands.map((band) => {\n const node = context.createBiquadFilter();\n node.type = band.type ?? \"peaking\";\n node.frequency.value = band.frequency;\n node.gain.value = band.gain ?? 0;\n node.Q.value = band.q ?? 1;\n return node;\n });\n\n filterNodesRef.current = filters;\n\n // Install processing chain BEFORE removing the temp analyser so the graph stays valid\n setProcessingChain(el, filters);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create equalizer\";\n setError(msg);\n filterNodesRef.current = [];\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n filterNodesRef.current = [];\n };\n }, [enabled, bands, audioRef, state.currentIndex]);\n\n const setBandGain = useCallback((bandIndex: number, gainDb: number) => {\n const node = filterNodesRef.current[bandIndex];\n if (node) {\n node.gain.value = gainDb;\n }\n setBandsState((prev) => prev.map((b, i) => (i === bandIndex ? { ...b, gain: gainDb } : b)));\n }, []);\n\n const setBands = useCallback((nextBands: EqualizerBand[]) => {\n setBandsState(nextBands);\n }, []);\n\n return { setBandGain, setBands, bands, error };\n}\n"],"names":["DEFAULT_BANDS","useGingerEqualizer","options","enabled","initialBands","audioRef","state","useGinger","bands","setBandsState","useState","error","setError","filterNodesRef","useRef","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","context","tempId","filters","band","node","detachLiveAnalyser","e","msg","element","setBandGain","useCallback","bandIndex","gainDb","prev","b","i","setBands","nextBands"],"mappings":"4LAqCMA,EAAiC,CACrC,CAAE,UAAW,EAAA,EACb,CAAE,UAAW,GAAA,EACb,CAAE,UAAW,GAAA,EACb,CAAE,UAAW,GAAA,EACb,CAAE,UAAW,IAAA,CACf,EAeO,SAASC,EACdC,EAAqC,GACX,CAC1B,KAAM,CAAE,QAAAC,EAAU,GAAM,MAAOC,EAAeJ,GAAkBE,EAC1D,CAAE,SAAAG,EAAU,MAAAC,CAAA,EAAUC,YAAA,EAEtB,CAACC,EAAOC,CAAa,EAAIC,EAAAA,SAA0BN,CAAY,EAC/D,CAACO,EAAOC,CAAQ,EAAIF,EAAAA,SAAwB,IAAI,EAEhDG,EAAiBC,EAAAA,OAA2B,EAAE,EAEpDC,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAKX,EAAS,QACpB,GAAI,GAACW,GAAM,OAAO,OAAW,KAI7B,IAAI,CAACb,EAAS,CACZc,EAAAA,mBAAmBD,EAAI,EAAE,EACzBH,EAAe,QAAU,CAAA,EACzB,MACF,CAEA,GAAI,CAIF,MAAMK,EAAWC,EAAAA,mBAAmBH,EAAI,CACtC,QAAS,GACT,sBAAuB,EACvB,YAAa,KACb,YAAa,CAAA,CACd,EACK,CAAE,QAAAI,EAAS,GAAIC,CAAA,EAAWH,EAE1BI,EAA8Bd,EAAM,IAAKe,GAAS,CACtD,MAAMC,EAAOJ,EAAQ,mBAAA,EACrB,OAAAI,EAAK,KAAOD,EAAK,MAAQ,UACzBC,EAAK,UAAU,MAAQD,EAAK,UAC5BC,EAAK,KAAK,MAAQD,EAAK,MAAQ,EAC/BC,EAAK,EAAE,MAAQD,EAAK,GAAK,EAClBC,CACT,CAAC,EAEDX,EAAe,QAAUS,EAGzBL,EAAAA,mBAAmBD,EAAIM,CAAO,EAC9BG,EAAAA,mBAAmBT,EAAIK,CAAM,EAE7BT,EAAS,IAAI,CACf,OAASc,EAAG,CACV,MAAMC,EAAMD,aAAa,MAAQA,EAAE,QAAU,6BAC7Cd,EAASe,CAAG,EACZd,EAAe,QAAU,CAAA,CAC3B,CAEA,MAAO,IAAM,CACX,MAAMe,EAAUvB,EAAS,QACrBuB,GACFX,EAAAA,mBAAmBW,EAAS,EAAE,EAEhCf,EAAe,QAAU,CAAA,CAC3B,EACF,EAAG,CAACV,EAASK,EAAOH,EAAUC,EAAM,YAAY,CAAC,EAEjD,MAAMuB,EAAcC,EAAAA,YAAY,CAACC,EAAmBC,IAAmB,CACrE,MAAMR,EAAOX,EAAe,QAAQkB,CAAS,EACzCP,IACFA,EAAK,KAAK,MAAQQ,GAEpBvB,EAAewB,GAASA,EAAK,IAAI,CAACC,EAAGC,IAAOA,IAAMJ,EAAY,CAAE,GAAGG,EAAG,KAAMF,CAAA,EAAWE,CAAE,CAAC,CAC5F,EAAG,CAAA,CAAE,EAECE,EAAWN,cAAaO,GAA+B,CAC3D5B,EAAc4B,CAAS,CACzB,EAAG,CAAA,CAAE,EAEL,MAAO,CAAE,YAAAR,EAAa,SAAAO,EAAU,MAAA5B,EAAO,MAAAG,CAAA,CACzC"}
|
package/dist/equalizer/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useState as y, useRef as A, useEffect as S, useCallback as
|
|
2
|
-
import {
|
|
1
|
+
import { useState as y, useRef as A, useEffect as S, useCallback as p } from "react";
|
|
2
|
+
import { s as f, a as x, d as z } from "../liveAudioGraph-DvPaxBCP.js";
|
|
3
|
+
import { u as C } from "../useGinger-hpp2pAGY.js";
|
|
3
4
|
const D = [
|
|
4
5
|
{ frequency: 60 },
|
|
5
6
|
{ frequency: 250 },
|
|
@@ -7,45 +8,45 @@ const D = [
|
|
|
7
8
|
{ frequency: 4e3 },
|
|
8
9
|
{ frequency: 16e3 }
|
|
9
10
|
];
|
|
10
|
-
function
|
|
11
|
-
const { enabled: l = !0, bands:
|
|
11
|
+
function R(q = {}) {
|
|
12
|
+
const { enabled: l = !0, bands: g = D } = q, { audioRef: i, state: h } = C(), [o, d] = y(g), [v, m] = y(null), s = A([]);
|
|
12
13
|
S(() => {
|
|
13
|
-
const e =
|
|
14
|
+
const e = i.current;
|
|
14
15
|
if (!(!e || typeof window > "u")) {
|
|
15
16
|
if (!l) {
|
|
16
17
|
f(e, []), s.current = [];
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
try {
|
|
20
|
-
const t =
|
|
21
|
+
const t = x(e, {
|
|
21
22
|
fftSize: 32,
|
|
22
23
|
smoothingTimeConstant: 0,
|
|
23
24
|
minDecibels: -100,
|
|
24
25
|
maxDecibels: 0
|
|
25
|
-
}), { context: n, id:
|
|
26
|
+
}), { context: n, id: u } = t, c = o.map((r) => {
|
|
26
27
|
const a = n.createBiquadFilter();
|
|
27
28
|
return a.type = r.type ?? "peaking", a.frequency.value = r.frequency, a.gain.value = r.gain ?? 0, a.Q.value = r.q ?? 1, a;
|
|
28
29
|
});
|
|
29
|
-
s.current = c, f(e, c),
|
|
30
|
+
s.current = c, f(e, c), z(e, u), m(null);
|
|
30
31
|
} catch (t) {
|
|
31
32
|
const n = t instanceof Error ? t.message : "Failed to create equalizer";
|
|
32
33
|
m(n), s.current = [];
|
|
33
34
|
}
|
|
34
35
|
return () => {
|
|
35
|
-
const t =
|
|
36
|
+
const t = i.current;
|
|
36
37
|
t && f(t, []), s.current = [];
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
|
-
}, [l,
|
|
40
|
-
const B =
|
|
40
|
+
}, [l, o, i, h.currentIndex]);
|
|
41
|
+
const B = p((e, t) => {
|
|
41
42
|
const n = s.current[e];
|
|
42
|
-
n && (n.gain.value = t), d((
|
|
43
|
-
}, []), E =
|
|
43
|
+
n && (n.gain.value = t), d((u) => u.map((c, r) => r === e ? { ...c, gain: t } : c));
|
|
44
|
+
}, []), E = p((e) => {
|
|
44
45
|
d(e);
|
|
45
46
|
}, []);
|
|
46
|
-
return { setBandGain: B, setBands: E, bands:
|
|
47
|
+
return { setBandGain: B, setBands: E, bands: o, error: v };
|
|
47
48
|
}
|
|
48
49
|
export {
|
|
49
|
-
|
|
50
|
+
R as useGingerEqualizer
|
|
50
51
|
};
|
|
51
52
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/equalizer/useGingerEqualizer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\nexport type EqualizerBand = {\n /** Center frequency in Hz (e.g. 60, 250, 1000, 4000, 16000). */\n frequency: number;\n /** Gain in dB. Positive = boost, negative = cut. Typical range: -12 to +12. Default: 0. */\n gain?: number;\n /** Q factor (bandwidth). Higher = narrower. Default: 1. */\n q?: number;\n /** BiquadFilterNode type. Default: \"peaking\". */\n type?: BiquadFilterType;\n};\n\nexport type UseGingerEqualizerOptions = {\n /** When false, EQ nodes are detached and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Band definitions. Each band maps to one BiquadFilterNode. */\n bands?: EqualizerBand[];\n};\n\nexport type UseGingerEqualizerResult = {\n /** Update a single band's gain in dB without rebuilding the filter chain. */\n setBandGain: (bandIndex: number, gainDb: number) => void;\n /** Replace all bands (rebuilds the filter chain). */\n setBands: (bands: EqualizerBand[]) => void;\n /** Current band definitions (reflects the latest `setBands` call). */\n bands: EqualizerBand[];\n /** Error string if Web Audio is unavailable or filter creation failed. */\n error: string | null;\n};\n\nconst DEFAULT_BANDS: EqualizerBand[] = [\n { frequency: 60 },\n { frequency: 250 },\n { frequency: 1000 },\n { frequency: 4000 },\n { frequency: 16000 },\n];\n\n/**\n * Inserts a parametric EQ into the Web Audio graph for the active Ginger media element.\n *\n * Each band is a `BiquadFilterNode` connected in series between the audio source and the\n * speakers. If `useGingerLiveAnalyzer` is also active, the EQ is inserted before the analyser\n * — both share the same `AudioContext` (the browser allows only one `MediaElementAudioSourceNode`\n * per element).\n *\n * Available as a subpath export:\n * ```ts\n * import { useGingerEqualizer } from \"@lucaismyname/ginger/equalizer\";\n * ```\n */\nexport function useGingerEqualizer(\n options: UseGingerEqualizerOptions = {},\n): UseGingerEqualizerResult {\n const { enabled = true, bands: initialBands = DEFAULT_BANDS } = options;\n const { audioRef, state } = useGinger();\n\n const [bands, setBandsState] = useState<EqualizerBand[]>(initialBands);\n const [error, setError] = useState<string | null>(null);\n\n const filterNodesRef = useRef<BiquadFilterNode[]>([]);\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n filterNodesRef.current = [];\n return;\n }\n\n try {\n // Attach a temporary analyser solely to get (or create) the shared AudioContext.\n // The graph is rebuilt by liveAudioGraph after we call setProcessingChain, so this\n // temporary consumer is detached immediately after we have the context reference.\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const filters: BiquadFilterNode[] = bands.map((band) => {\n const node = context.createBiquadFilter();\n node.type = band.type ?? \"peaking\";\n node.frequency.value = band.frequency;\n node.gain.value = band.gain ?? 0;\n node.Q.value = band.q ?? 1;\n return node;\n });\n\n filterNodesRef.current = filters;\n\n // Install processing chain BEFORE removing the temp analyser so the graph stays valid\n setProcessingChain(el, filters);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create equalizer\";\n setError(msg);\n filterNodesRef.current = [];\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n filterNodesRef.current = [];\n };\n }, [enabled, bands, audioRef, state.currentIndex]);\n\n const setBandGain = useCallback((bandIndex: number, gainDb: number) => {\n const node = filterNodesRef.current[bandIndex];\n if (node) {\n node.gain.value = gainDb;\n }\n setBandsState((prev) => prev.map((b, i) => (i === bandIndex ? { ...b, gain: gainDb } : b)));\n }, []);\n\n const setBands = useCallback((nextBands: EqualizerBand[]) => {\n setBandsState(nextBands);\n }, []);\n\n return { setBandGain, setBands, bands, error };\n}\n"],"names":["DEFAULT_BANDS","useGingerEqualizer","options","enabled","initialBands","audioRef","state","useGinger","bands","setBandsState","useState","error","setError","filterNodesRef","useRef","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","context","tempId","filters","band","node","detachLiveAnalyser","e","msg","element","setBandGain","useCallback","bandIndex","gainDb","prev","b","i","setBands","nextBands"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/equalizer/useGingerEqualizer.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n attachLiveAnalyser,\n detachLiveAnalyser,\n setProcessingChain,\n} from \"../analyzer/liveAudioGraph\";\nimport { useGinger } from \"../hooks/useGinger\";\n\nexport type EqualizerBand = {\n /** Center frequency in Hz (e.g. 60, 250, 1000, 4000, 16000). */\n frequency: number;\n /** Gain in dB. Positive = boost, negative = cut. Typical range: -12 to +12. Default: 0. */\n gain?: number;\n /** Q factor (bandwidth). Higher = narrower. Default: 1. */\n q?: number;\n /** BiquadFilterNode type. Default: \"peaking\". */\n type?: BiquadFilterType;\n};\n\nexport type UseGingerEqualizerOptions = {\n /** When false, EQ nodes are detached and the audio path is bypassed. Default: true. */\n enabled?: boolean;\n /** Band definitions. Each band maps to one BiquadFilterNode. */\n bands?: EqualizerBand[];\n};\n\nexport type UseGingerEqualizerResult = {\n /** Update a single band's gain in dB without rebuilding the filter chain. */\n setBandGain: (bandIndex: number, gainDb: number) => void;\n /** Replace all bands (rebuilds the filter chain). */\n setBands: (bands: EqualizerBand[]) => void;\n /** Current band definitions (reflects the latest `setBands` call). */\n bands: EqualizerBand[];\n /** Error string if Web Audio is unavailable or filter creation failed. */\n error: string | null;\n};\n\nconst DEFAULT_BANDS: EqualizerBand[] = [\n { frequency: 60 },\n { frequency: 250 },\n { frequency: 1000 },\n { frequency: 4000 },\n { frequency: 16000 },\n];\n\n/**\n * Inserts a parametric EQ into the Web Audio graph for the active Ginger media element.\n *\n * Each band is a `BiquadFilterNode` connected in series between the audio source and the\n * speakers. If `useGingerLiveAnalyzer` is also active, the EQ is inserted before the analyser\n * — both share the same `AudioContext` (the browser allows only one `MediaElementAudioSourceNode`\n * per element).\n *\n * Available as a subpath export:\n * ```ts\n * import { useGingerEqualizer } from \"@lucaismyname/ginger/equalizer\";\n * ```\n */\nexport function useGingerEqualizer(\n options: UseGingerEqualizerOptions = {},\n): UseGingerEqualizerResult {\n const { enabled = true, bands: initialBands = DEFAULT_BANDS } = options;\n const { audioRef, state } = useGinger();\n\n const [bands, setBandsState] = useState<EqualizerBand[]>(initialBands);\n const [error, setError] = useState<string | null>(null);\n\n const filterNodesRef = useRef<BiquadFilterNode[]>([]);\n\n useEffect(() => {\n const el = audioRef.current;\n if (!el || typeof window === \"undefined\") {\n return;\n }\n\n if (!enabled) {\n setProcessingChain(el, []);\n filterNodesRef.current = [];\n return;\n }\n\n try {\n // Attach a temporary analyser solely to get (or create) the shared AudioContext.\n // The graph is rebuilt by liveAudioGraph after we call setProcessingChain, so this\n // temporary consumer is detached immediately after we have the context reference.\n const attached = attachLiveAnalyser(el, {\n fftSize: 32,\n smoothingTimeConstant: 0,\n minDecibels: -100,\n maxDecibels: 0,\n });\n const { context, id: tempId } = attached;\n\n const filters: BiquadFilterNode[] = bands.map((band) => {\n const node = context.createBiquadFilter();\n node.type = band.type ?? \"peaking\";\n node.frequency.value = band.frequency;\n node.gain.value = band.gain ?? 0;\n node.Q.value = band.q ?? 1;\n return node;\n });\n\n filterNodesRef.current = filters;\n\n // Install processing chain BEFORE removing the temp analyser so the graph stays valid\n setProcessingChain(el, filters);\n detachLiveAnalyser(el, tempId);\n\n setError(null);\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to create equalizer\";\n setError(msg);\n filterNodesRef.current = [];\n }\n\n return () => {\n const element = audioRef.current;\n if (element) {\n setProcessingChain(element, []);\n }\n filterNodesRef.current = [];\n };\n }, [enabled, bands, audioRef, state.currentIndex]);\n\n const setBandGain = useCallback((bandIndex: number, gainDb: number) => {\n const node = filterNodesRef.current[bandIndex];\n if (node) {\n node.gain.value = gainDb;\n }\n setBandsState((prev) => prev.map((b, i) => (i === bandIndex ? { ...b, gain: gainDb } : b)));\n }, []);\n\n const setBands = useCallback((nextBands: EqualizerBand[]) => {\n setBandsState(nextBands);\n }, []);\n\n return { setBandGain, setBands, bands, error };\n}\n"],"names":["DEFAULT_BANDS","useGingerEqualizer","options","enabled","initialBands","audioRef","state","useGinger","bands","setBandsState","useState","error","setError","filterNodesRef","useRef","useEffect","el","setProcessingChain","attached","attachLiveAnalyser","context","tempId","filters","band","node","detachLiveAnalyser","e","msg","element","setBandGain","useCallback","bandIndex","gainDb","prev","b","i","setBands","nextBands"],"mappings":";;;AAqCA,MAAMA,IAAiC;AAAA,EACrC,EAAE,WAAW,GAAA;AAAA,EACb,EAAE,WAAW,IAAA;AAAA,EACb,EAAE,WAAW,IAAA;AAAA,EACb,EAAE,WAAW,IAAA;AAAA,EACb,EAAE,WAAW,KAAA;AACf;AAeO,SAASC,EACdC,IAAqC,IACX;AAC1B,QAAM,EAAE,SAAAC,IAAU,IAAM,OAAOC,IAAeJ,MAAkBE,GAC1D,EAAE,UAAAG,GAAU,OAAAC,EAAA,IAAUC,EAAA,GAEtB,CAACC,GAAOC,CAAa,IAAIC,EAA0BN,CAAY,GAC/D,CAACO,GAAOC,CAAQ,IAAIF,EAAwB,IAAI,GAEhDG,IAAiBC,EAA2B,EAAE;AAEpD,EAAAC,EAAU,MAAM;AACd,UAAMC,IAAKX,EAAS;AACpB,QAAI,GAACW,KAAM,OAAO,SAAW,MAI7B;AAAA,UAAI,CAACb,GAAS;AACZ,QAAAc,EAAmBD,GAAI,EAAE,GACzBH,EAAe,UAAU,CAAA;AACzB;AAAA,MACF;AAEA,UAAI;AAIF,cAAMK,IAAWC,EAAmBH,GAAI;AAAA,UACtC,SAAS;AAAA,UACT,uBAAuB;AAAA,UACvB,aAAa;AAAA,UACb,aAAa;AAAA,QAAA,CACd,GACK,EAAE,SAAAI,GAAS,IAAIC,EAAA,IAAWH,GAE1BI,IAA8Bd,EAAM,IAAI,CAACe,MAAS;AACtD,gBAAMC,IAAOJ,EAAQ,mBAAA;AACrB,iBAAAI,EAAK,OAAOD,EAAK,QAAQ,WACzBC,EAAK,UAAU,QAAQD,EAAK,WAC5BC,EAAK,KAAK,QAAQD,EAAK,QAAQ,GAC/BC,EAAK,EAAE,QAAQD,EAAK,KAAK,GAClBC;AAAA,QACT,CAAC;AAED,QAAAX,EAAe,UAAUS,GAGzBL,EAAmBD,GAAIM,CAAO,GAC9BG,EAAmBT,GAAIK,CAAM,GAE7BT,EAAS,IAAI;AAAA,MACf,SAASc,GAAG;AACV,cAAMC,IAAMD,aAAa,QAAQA,EAAE,UAAU;AAC7C,QAAAd,EAASe,CAAG,GACZd,EAAe,UAAU,CAAA;AAAA,MAC3B;AAEA,aAAO,MAAM;AACX,cAAMe,IAAUvB,EAAS;AACzB,QAAIuB,KACFX,EAAmBW,GAAS,EAAE,GAEhCf,EAAe,UAAU,CAAA;AAAA,MAC3B;AAAA;AAAA,EACF,GAAG,CAACV,GAASK,GAAOH,GAAUC,EAAM,YAAY,CAAC;AAEjD,QAAMuB,IAAcC,EAAY,CAACC,GAAmBC,MAAmB;AACrE,UAAMR,IAAOX,EAAe,QAAQkB,CAAS;AAC7C,IAAIP,MACFA,EAAK,KAAK,QAAQQ,IAEpBvB,EAAc,CAACwB,MAASA,EAAK,IAAI,CAACC,GAAGC,MAAOA,MAAMJ,IAAY,EAAE,GAAGG,GAAG,MAAMF,EAAA,IAAWE,CAAE,CAAC;AAAA,EAC5F,GAAG,CAAA,CAAE,GAECE,IAAWN,EAAY,CAACO,MAA+B;AAC3D,IAAA5B,EAAc4B,CAAS;AAAA,EACzB,GAAG,CAAA,CAAE;AAEL,SAAO,EAAE,aAAAR,GAAa,UAAAO,GAAU,OAAA5B,GAAO,OAAAG,EAAA;AACzC;"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-NEcOSSJD.cjs"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-NEcOSSJD.cjs"),s=require("./useGinger-BXgia32v.cjs"),r=require("./useGingerChapterProgress-BdaalJvX.cjs"),i=require("./liveAudioGraph-0cpHD_Ic.cjs"),n=require("./selectors-YXnP8Y8g.cjs"),a=require("./GingerSplitContexts-C7puo0M7.cjs");exports.Chapters=e.Chapters;exports.Ginger=e.Ginger;exports.LyricsSynced=e.LyricsSynced;exports.Pause=e.Pause;exports.Play=e.Play;exports.RepeatGlyph=e.RepeatGlyph;exports.ShuffleIcon=e.ShuffleIcon;exports.SkipBack=e.SkipBack;exports.SkipForward=e.SkipForward;exports.Volume2=e.Volume2;exports.VolumeX=e.VolumeX;exports.Wrapper=e.Wrapper;exports.clampPlaybackRate=e.clampPlaybackRate;exports.clampVolume=e.clampVolume;exports.defaultGingerLocale=e.defaultGingerLocale;exports.parseLrc=e.parseLrc;exports.useGingerChapters=e.useGingerChapters;exports.useGingerLocale=e.useGingerLocale;exports.useGingerLyricsSync=e.useGingerLyricsSync;exports.usePlayPauseBinding=e.usePlayPauseBinding;exports.useSeekBarBinding=e.useSeekBarBinding;exports.useVolumeSlider=e.useVolumeSlider;exports.useGinger=s.useGinger;exports.createGingerStore=r.createGingerStore;exports.useGingerChapterProgress=r.useGingerChapterProgress;exports.useGingerDebugLog=r.useGingerDebugLog;exports.useGingerKeyboardShortcuts=r.useGingerKeyboardShortcuts;exports.useGingerLiveAnalyzer=r.useGingerLiveAnalyzer;exports.useGingerPlaybackHistory=r.useGingerPlaybackHistory;exports.useGingerSleepTimer=r.useGingerSleepTimer;exports.useGingerVolumeFade=r.useGingerVolumeFade;exports.useNextTrackPrefetch=r.useNextTrackPrefetch;exports.useSeekDrag=r.useSeekDrag;exports.attachLiveAnalyser=i.attachLiveAnalyser;exports.detachLiveAnalyser=i.detachLiveAnalyser;exports.setProcessingChain=i.setProcessingChain;exports.derivePlaybackUiState=n.derivePlaybackUiState;exports.gingerStateFromContextValues=a.gingerStateFromContextValues;exports.gingerStateFromContexts=a.gingerStateFromContexts;exports.useGingerMedia=a.useGingerMedia;exports.useGingerPlayback=a.useGingerPlayback;exports.useGingerState=a.useGingerState;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
import { C as s, G as r, L as i, P as
|
|
2
|
-
import {
|
|
3
|
-
import { c as
|
|
4
|
-
import { d as
|
|
5
|
-
import {
|
|
1
|
+
import { C as s, G as r, L as i, P as t, a as u, R as n, S as o, b as g, c, V as l, d as p, W as G, e as m, f as y, g as S, p as d, u as f, h, i as P, j as k, k as L, l as b } from "./ginger-L2ZFgzH4.js";
|
|
2
|
+
import { u as C } from "./useGinger-hpp2pAGY.js";
|
|
3
|
+
import { c as v, u as B, a as F, b as A, d as R, e as D, f as T, g as W, h as j, i as w } from "./useGingerChapterProgress-CZdv-HiI.js";
|
|
4
|
+
import { a as H, d as I, s as K } from "./liveAudioGraph-DvPaxBCP.js";
|
|
5
|
+
import { d as N } from "./selectors-BalBCc7X.js";
|
|
6
|
+
import { g as X, a as q, u as E, b as J, c as O } from "./GingerSplitContexts-BzBExb95.js";
|
|
6
7
|
export {
|
|
7
8
|
s as Chapters,
|
|
8
9
|
r as Ginger,
|
|
9
10
|
i as LyricsSynced,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
t as Pause,
|
|
12
|
+
u as Play,
|
|
12
13
|
n as RepeatGlyph,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
o as ShuffleIcon,
|
|
15
|
+
g as SkipBack,
|
|
15
16
|
c as SkipForward,
|
|
16
17
|
l as Volume2,
|
|
17
18
|
p as VolumeX,
|
|
18
19
|
G as Wrapper,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
H as attachLiveAnalyser,
|
|
21
|
+
m as clampPlaybackRate,
|
|
22
|
+
y as clampVolume,
|
|
23
|
+
v as createGingerStore,
|
|
24
|
+
S as defaultGingerLocale,
|
|
25
|
+
N as derivePlaybackUiState,
|
|
26
|
+
I as detachLiveAnalyser,
|
|
27
|
+
X as gingerStateFromContextValues,
|
|
28
|
+
q as gingerStateFromContexts,
|
|
29
|
+
d as parseLrc,
|
|
30
|
+
K as setProcessingChain,
|
|
31
|
+
C as useGinger,
|
|
32
|
+
B as useGingerChapterProgress,
|
|
33
|
+
f as useGingerChapters,
|
|
34
|
+
F as useGingerDebugLog,
|
|
35
|
+
A as useGingerKeyboardShortcuts,
|
|
36
|
+
R as useGingerLiveAnalyzer,
|
|
37
|
+
h as useGingerLocale,
|
|
38
|
+
P as useGingerLyricsSync,
|
|
39
|
+
E as useGingerMedia,
|
|
40
|
+
J as useGingerPlayback,
|
|
41
|
+
D as useGingerPlaybackHistory,
|
|
42
|
+
T as useGingerSleepTimer,
|
|
43
|
+
O as useGingerState,
|
|
44
|
+
W as useGingerVolumeFade,
|
|
45
|
+
j as useNextTrackPrefetch,
|
|
45
46
|
k as usePlayPauseBinding,
|
|
46
47
|
L as useSeekBarBinding,
|
|
47
|
-
|
|
48
|
+
w as useSeekDrag,
|
|
48
49
|
b as useVolumeSlider
|
|
49
50
|
};
|
|
50
51
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const r=new WeakMap;function h(t){const e=2**Math.round(Math.log2(t));return Math.min(32768,Math.max(32,e))}function d(t){const{processingChain:e}=t;return e.length>0?e[e.length-1]:t.source}function a(t){const{source:e,processingChain:n,consumers:s,context:c}=t;try{e.disconnect()}catch{}for(const i of n)try{i.disconnect()}catch{}for(const{analyser:i}of s.values())try{i.disconnect(c.destination)}catch{}if(n.length>0){e.connect(n[0]);for(let i=0;i<n.length-1;i++)n[i].connect(n[i+1])}const o=d(t);if(s.size===0)o.connect(c.destination);else{let i=!0;for(const{analyser:u}of s.values())o.connect(u),i&&(u.connect(c.destination),i=!1)}}function f(t){let e=r.get(t);if(!e){const n=window.AudioContext??window.webkitAudioContext;if(!n)throw new Error("Web Audio API is not available");const s=new n,c=s.createMediaElementSource(t);e={context:s,source:c,consumers:new Map,nextId:0,processingChain:[],processingActive:!1},r.set(t,e)}return e}function l(t,e){if(e.consumers.size===0&&!e.processingActive){try{e.source.disconnect()}catch{}e.context.close(),r.delete(t)}}function g(t,e){const n=f(t),{context:s}=n,c=s.createAnalyser();c.fftSize=h(e.fftSize),c.smoothingTimeConstant=e.smoothingTimeConstant,c.minDecibels=e.minDecibels,c.maxDecibels=e.maxDecibels;const o=n.nextId;return n.nextId+=1,n.consumers.set(o,{analyser:c}),a(n),{id:o,context:s,analyser:c}}function y(t,e){const n=r.get(t);if(!n)return;const s=n.consumers.get(e);if(s){try{s.analyser.disconnect()}catch{}if(n.consumers.delete(e),n.consumers.size===0&&!n.processingActive){l(t,n);return}a(n)}}function m(t,e){if(typeof window>"u")return;if(e.length===0){const s=r.get(t);if(!s)return;s.processingChain=[],s.processingActive=!1,s.consumers.size===0?l(t,s):a(s);return}const n=f(t);n.processingChain=e,n.processingActive=!0,a(n)}exports.attachLiveAnalyser=g;exports.detachLiveAnalyser=y;exports.setProcessingChain=m;
|
|
2
|
+
//# sourceMappingURL=liveAudioGraph-0cpHD_Ic.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveAudioGraph-0cpHD_Ic.cjs","sources":["../src/analyzer/liveAudioGraph.ts"],"sourcesContent":["/**\n * One MediaElementAudioSourceNode per HTMLAudioElement; multiple AnalyserNodes may tap the source.\n * An optional processing chain (e.g. EQ filters) can be inserted between the source and the\n * analysers via `setProcessingChain`.\n */\n\nexport type LiveAnalyserOptions = {\n fftSize: number;\n smoothingTimeConstant: number;\n minDecibels: number;\n maxDecibels: number;\n};\n\ntype Consumer = {\n analyser: AnalyserNode;\n};\n\ntype ElementEntry = {\n context: AudioContext;\n source: MediaElementAudioSourceNode;\n consumers: Map<number, Consumer>;\n nextId: number;\n /** Ordered processing nodes (e.g. BiquadFilterNode[]) inserted between source and analysers. */\n processingChain: AudioNode[];\n /** True while a processing chain is installed; prevents context teardown when no consumers exist. */\n processingActive: boolean;\n};\n\nconst entries = new WeakMap<HTMLAudioElement, ElementEntry>();\n\nfunction clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(32768, Math.max(32, p));\n}\n\nfunction getChainTail(entry: ElementEntry): AudioNode {\n const { processingChain } = entry;\n return processingChain.length > 0 ? processingChain[processingChain.length - 1]! : entry.source;\n}\n\n/**\n * Rebuild all graph connections from scratch.\n * Call after any structural change (add/remove consumer, change processing chain).\n */\nfunction rebuildGraph(entry: ElementEntry): void {\n const { source, processingChain, consumers, context } = entry;\n\n // Disconnect source outputs\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n\n // Disconnect processing chain node outputs\n for (const node of processingChain) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n\n // Disconnect all analysers from destination (we will reconnect selectively below)\n for (const { analyser } of consumers.values()) {\n try {\n analyser.disconnect(context.destination);\n } catch {\n // ignore\n }\n }\n\n // Build processing chain: source → node[0] → ... → node[N]\n if (processingChain.length > 0) {\n source.connect(processingChain[0]!);\n for (let i = 0; i < processingChain.length - 1; i++) {\n processingChain[i]!.connect(processingChain[i + 1]!);\n }\n }\n\n const tail = getChainTail(entry);\n\n if (consumers.size === 0) {\n // No analyser consumers: route tail directly to destination so audio is audible\n tail.connect(context.destination);\n } else {\n // Connect tail to all analysers; first one routes to destination (playback sink)\n let isFirst = true;\n for (const { analyser } of consumers.values()) {\n tail.connect(analyser);\n if (isFirst) {\n analyser.connect(context.destination);\n isFirst = false;\n }\n }\n }\n}\n\nfunction getOrCreateEntry(element: HTMLAudioElement): ElementEntry {\n let entry = entries.get(element);\n if (!entry) {\n const Context =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const context = new Context();\n const source = context.createMediaElementSource(element);\n entry = {\n context,\n source,\n consumers: new Map(),\n nextId: 0,\n processingChain: [],\n processingActive: false,\n };\n entries.set(element, entry);\n }\n return entry;\n}\n\nfunction maybeCloseEntry(element: HTMLAudioElement, entry: ElementEntry): void {\n if (entry.consumers.size === 0 && !entry.processingActive) {\n try {\n entry.source.disconnect();\n } catch {\n // ignore\n }\n void entry.context.close();\n entries.delete(element);\n }\n}\n\nexport function attachLiveAnalyser(\n element: HTMLAudioElement,\n options: LiveAnalyserOptions,\n): { id: number; context: AudioContext; analyser: AnalyserNode } {\n const entry = getOrCreateEntry(element);\n const { context } = entry;\n\n const analyser = context.createAnalyser();\n analyser.fftSize = clampFftSize(options.fftSize);\n analyser.smoothingTimeConstant = options.smoothingTimeConstant;\n analyser.minDecibels = options.minDecibels;\n analyser.maxDecibels = options.maxDecibels;\n\n const id = entry.nextId;\n entry.nextId += 1;\n entry.consumers.set(id, { analyser });\n\n rebuildGraph(entry);\n\n return { id, context, analyser };\n}\n\nexport function detachLiveAnalyser(element: HTMLAudioElement, id: number): void {\n const entry = entries.get(element);\n if (!entry) return;\n\n const consumer = entry.consumers.get(id);\n if (!consumer) return;\n\n try {\n consumer.analyser.disconnect();\n } catch {\n // ignore\n }\n entry.consumers.delete(id);\n\n if (entry.consumers.size === 0 && !entry.processingActive) {\n maybeCloseEntry(element, entry);\n return;\n }\n\n rebuildGraph(entry);\n}\n\n/**\n * Insert an ordered processing chain (e.g. BiquadFilterNode[]) between the audio source and the\n * analyser consumers. Pass an empty array to clear the chain.\n *\n * Safe to call while analyser consumers are active; the graph is rebuilt immediately.\n * Note: because `createMediaElementSource` can only be called once per element, the EQ and live\n * analyser share the same AudioContext. Calling both for the same element is supported.\n */\nexport function setProcessingChain(element: HTMLAudioElement, nodes: AudioNode[]): void {\n if (typeof window === \"undefined\") return;\n\n if (nodes.length === 0) {\n const entry = entries.get(element);\n if (!entry) return;\n entry.processingChain = [];\n entry.processingActive = false;\n if (entry.consumers.size === 0) {\n maybeCloseEntry(element, entry);\n } else {\n rebuildGraph(entry);\n }\n return;\n }\n\n const entry = getOrCreateEntry(element);\n entry.processingChain = nodes;\n entry.processingActive = true;\n rebuildGraph(entry);\n}\n"],"names":["entries","clampFftSize","n","p","getChainTail","entry","processingChain","rebuildGraph","source","consumers","context","node","analyser","tail","isFirst","getOrCreateEntry","element","Context","maybeCloseEntry","attachLiveAnalyser","options","id","detachLiveAnalyser","consumer","setProcessingChain","nodes"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const r = /* @__PURE__ */ new WeakMap();
|
|
2
|
+
function d(t) {
|
|
3
|
+
const e = 2 ** Math.round(Math.log2(t));
|
|
4
|
+
return Math.min(32768, Math.max(32, e));
|
|
5
|
+
}
|
|
6
|
+
function h(t) {
|
|
7
|
+
const { processingChain: e } = t;
|
|
8
|
+
return e.length > 0 ? e[e.length - 1] : t.source;
|
|
9
|
+
}
|
|
10
|
+
function a(t) {
|
|
11
|
+
const { source: e, processingChain: n, consumers: s, context: c } = t;
|
|
12
|
+
try {
|
|
13
|
+
e.disconnect();
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
for (const o of n)
|
|
17
|
+
try {
|
|
18
|
+
o.disconnect();
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
for (const { analyser: o } of s.values())
|
|
22
|
+
try {
|
|
23
|
+
o.disconnect(c.destination);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
if (n.length > 0) {
|
|
27
|
+
e.connect(n[0]);
|
|
28
|
+
for (let o = 0; o < n.length - 1; o++)
|
|
29
|
+
n[o].connect(n[o + 1]);
|
|
30
|
+
}
|
|
31
|
+
const i = h(t);
|
|
32
|
+
if (s.size === 0)
|
|
33
|
+
i.connect(c.destination);
|
|
34
|
+
else {
|
|
35
|
+
let o = !0;
|
|
36
|
+
for (const { analyser: u } of s.values())
|
|
37
|
+
i.connect(u), o && (u.connect(c.destination), o = !1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function f(t) {
|
|
41
|
+
let e = r.get(t);
|
|
42
|
+
if (!e) {
|
|
43
|
+
const n = window.AudioContext ?? window.webkitAudioContext;
|
|
44
|
+
if (!n)
|
|
45
|
+
throw new Error("Web Audio API is not available");
|
|
46
|
+
const s = new n(), c = s.createMediaElementSource(t);
|
|
47
|
+
e = {
|
|
48
|
+
context: s,
|
|
49
|
+
source: c,
|
|
50
|
+
consumers: /* @__PURE__ */ new Map(),
|
|
51
|
+
nextId: 0,
|
|
52
|
+
processingChain: [],
|
|
53
|
+
processingActive: !1
|
|
54
|
+
}, r.set(t, e);
|
|
55
|
+
}
|
|
56
|
+
return e;
|
|
57
|
+
}
|
|
58
|
+
function l(t, e) {
|
|
59
|
+
if (e.consumers.size === 0 && !e.processingActive) {
|
|
60
|
+
try {
|
|
61
|
+
e.source.disconnect();
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
e.context.close(), r.delete(t);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function g(t, e) {
|
|
68
|
+
const n = f(t), { context: s } = n, c = s.createAnalyser();
|
|
69
|
+
c.fftSize = d(e.fftSize), c.smoothingTimeConstant = e.smoothingTimeConstant, c.minDecibels = e.minDecibels, c.maxDecibels = e.maxDecibels;
|
|
70
|
+
const i = n.nextId;
|
|
71
|
+
return n.nextId += 1, n.consumers.set(i, { analyser: c }), a(n), { id: i, context: s, analyser: c };
|
|
72
|
+
}
|
|
73
|
+
function m(t, e) {
|
|
74
|
+
const n = r.get(t);
|
|
75
|
+
if (!n) return;
|
|
76
|
+
const s = n.consumers.get(e);
|
|
77
|
+
if (s) {
|
|
78
|
+
try {
|
|
79
|
+
s.analyser.disconnect();
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
if (n.consumers.delete(e), n.consumers.size === 0 && !n.processingActive) {
|
|
83
|
+
l(t, n);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
a(n);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function y(t, e) {
|
|
90
|
+
if (typeof window > "u") return;
|
|
91
|
+
if (e.length === 0) {
|
|
92
|
+
const s = r.get(t);
|
|
93
|
+
if (!s) return;
|
|
94
|
+
s.processingChain = [], s.processingActive = !1, s.consumers.size === 0 ? l(t, s) : a(s);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const n = f(t);
|
|
98
|
+
n.processingChain = e, n.processingActive = !0, a(n);
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
g as a,
|
|
102
|
+
m as d,
|
|
103
|
+
y as s
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=liveAudioGraph-DvPaxBCP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveAudioGraph-DvPaxBCP.js","sources":["../src/analyzer/liveAudioGraph.ts"],"sourcesContent":["/**\n * One MediaElementAudioSourceNode per HTMLAudioElement; multiple AnalyserNodes may tap the source.\n * An optional processing chain (e.g. EQ filters) can be inserted between the source and the\n * analysers via `setProcessingChain`.\n */\n\nexport type LiveAnalyserOptions = {\n fftSize: number;\n smoothingTimeConstant: number;\n minDecibels: number;\n maxDecibels: number;\n};\n\ntype Consumer = {\n analyser: AnalyserNode;\n};\n\ntype ElementEntry = {\n context: AudioContext;\n source: MediaElementAudioSourceNode;\n consumers: Map<number, Consumer>;\n nextId: number;\n /** Ordered processing nodes (e.g. BiquadFilterNode[]) inserted between source and analysers. */\n processingChain: AudioNode[];\n /** True while a processing chain is installed; prevents context teardown when no consumers exist. */\n processingActive: boolean;\n};\n\nconst entries = new WeakMap<HTMLAudioElement, ElementEntry>();\n\nfunction clampFftSize(n: number): number {\n const p = 2 ** Math.round(Math.log2(n));\n return Math.min(32768, Math.max(32, p));\n}\n\nfunction getChainTail(entry: ElementEntry): AudioNode {\n const { processingChain } = entry;\n return processingChain.length > 0 ? processingChain[processingChain.length - 1]! : entry.source;\n}\n\n/**\n * Rebuild all graph connections from scratch.\n * Call after any structural change (add/remove consumer, change processing chain).\n */\nfunction rebuildGraph(entry: ElementEntry): void {\n const { source, processingChain, consumers, context } = entry;\n\n // Disconnect source outputs\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n\n // Disconnect processing chain node outputs\n for (const node of processingChain) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n\n // Disconnect all analysers from destination (we will reconnect selectively below)\n for (const { analyser } of consumers.values()) {\n try {\n analyser.disconnect(context.destination);\n } catch {\n // ignore\n }\n }\n\n // Build processing chain: source → node[0] → ... → node[N]\n if (processingChain.length > 0) {\n source.connect(processingChain[0]!);\n for (let i = 0; i < processingChain.length - 1; i++) {\n processingChain[i]!.connect(processingChain[i + 1]!);\n }\n }\n\n const tail = getChainTail(entry);\n\n if (consumers.size === 0) {\n // No analyser consumers: route tail directly to destination so audio is audible\n tail.connect(context.destination);\n } else {\n // Connect tail to all analysers; first one routes to destination (playback sink)\n let isFirst = true;\n for (const { analyser } of consumers.values()) {\n tail.connect(analyser);\n if (isFirst) {\n analyser.connect(context.destination);\n isFirst = false;\n }\n }\n }\n}\n\nfunction getOrCreateEntry(element: HTMLAudioElement): ElementEntry {\n let entry = entries.get(element);\n if (!entry) {\n const Context =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Context) {\n throw new Error(\"Web Audio API is not available\");\n }\n const context = new Context();\n const source = context.createMediaElementSource(element);\n entry = {\n context,\n source,\n consumers: new Map(),\n nextId: 0,\n processingChain: [],\n processingActive: false,\n };\n entries.set(element, entry);\n }\n return entry;\n}\n\nfunction maybeCloseEntry(element: HTMLAudioElement, entry: ElementEntry): void {\n if (entry.consumers.size === 0 && !entry.processingActive) {\n try {\n entry.source.disconnect();\n } catch {\n // ignore\n }\n void entry.context.close();\n entries.delete(element);\n }\n}\n\nexport function attachLiveAnalyser(\n element: HTMLAudioElement,\n options: LiveAnalyserOptions,\n): { id: number; context: AudioContext; analyser: AnalyserNode } {\n const entry = getOrCreateEntry(element);\n const { context } = entry;\n\n const analyser = context.createAnalyser();\n analyser.fftSize = clampFftSize(options.fftSize);\n analyser.smoothingTimeConstant = options.smoothingTimeConstant;\n analyser.minDecibels = options.minDecibels;\n analyser.maxDecibels = options.maxDecibels;\n\n const id = entry.nextId;\n entry.nextId += 1;\n entry.consumers.set(id, { analyser });\n\n rebuildGraph(entry);\n\n return { id, context, analyser };\n}\n\nexport function detachLiveAnalyser(element: HTMLAudioElement, id: number): void {\n const entry = entries.get(element);\n if (!entry) return;\n\n const consumer = entry.consumers.get(id);\n if (!consumer) return;\n\n try {\n consumer.analyser.disconnect();\n } catch {\n // ignore\n }\n entry.consumers.delete(id);\n\n if (entry.consumers.size === 0 && !entry.processingActive) {\n maybeCloseEntry(element, entry);\n return;\n }\n\n rebuildGraph(entry);\n}\n\n/**\n * Insert an ordered processing chain (e.g. BiquadFilterNode[]) between the audio source and the\n * analyser consumers. Pass an empty array to clear the chain.\n *\n * Safe to call while analyser consumers are active; the graph is rebuilt immediately.\n * Note: because `createMediaElementSource` can only be called once per element, the EQ and live\n * analyser share the same AudioContext. Calling both for the same element is supported.\n */\nexport function setProcessingChain(element: HTMLAudioElement, nodes: AudioNode[]): void {\n if (typeof window === \"undefined\") return;\n\n if (nodes.length === 0) {\n const entry = entries.get(element);\n if (!entry) return;\n entry.processingChain = [];\n entry.processingActive = false;\n if (entry.consumers.size === 0) {\n maybeCloseEntry(element, entry);\n } else {\n rebuildGraph(entry);\n }\n return;\n }\n\n const entry = getOrCreateEntry(element);\n entry.processingChain = nodes;\n entry.processingActive = true;\n rebuildGraph(entry);\n}\n"],"names":["entries","clampFftSize","n","p","getChainTail","entry","processingChain","rebuildGraph","source","consumers","context","node","analyser","i","tail","isFirst","getOrCreateEntry","element","Context","maybeCloseEntry","attachLiveAnalyser","options","id","detachLiveAnalyser","consumer","setProcessingChain","nodes"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react"),U=require("../useGinger-BXgia32v.cjs"),S="ginger-remote";function B(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`ginger-tab-${Math.random().toString(36).slice(2)}`}function F(_={}){const{channelName:w=S,heartbeatMs:b=2e3,electionTimeoutMs:N=300}=_,{state:r,init:k}=U.useGinger(),e=a.useRef("");e.current===""&&(e.current=B());const[l,i]=a.useState("pending"),[P,g]=a.useState(0),[G,M]=a.useState(null),t=a.useRef(l);t.current=l;const L=a.useRef(k);L.current=k;const T=a.useRef(null),o=a.useRef(new Set),E=a.useRef(Date.now()),I=a.useRef(null),s=a.useRef(null),m=a.useRef(null),R=a.useCallback(u=>{var c;(c=T.current)==null||c.postMessage(u)},[]),f=a.useCallback(()=>{I.current&&(clearTimeout(I.current),I.current=null)},[]),y=a.useCallback(()=>{s.current&&(clearInterval(s.current),s.current=null)},[]);a.useEffect(()=>{if(typeof window>"u"||typeof BroadcastChannel>"u"){M("BroadcastChannel is not available in this environment");return}M(null);const u=new BroadcastChannel(w);T.current=u,o.current=new Set([e.current]);const c=p=>{u.postMessage(p)},A=()=>{s.current&&clearInterval(s.current),s.current=setInterval(()=>{const p=e.current;o.current.add(p);const n=o.current.size;g(n),c({type:"HEARTBEAT",tabId:p,connectedCount:n})},b)},h=()=>{f(),y(),i("follower"),t.current="follower"},H=()=>{f(),i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A()},v=()=>{f(),I.current=setTimeout(()=>{t.current==="pending"&&(i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A())},N)},C=p=>{const n=p.data;if(!n||typeof n!="object"||!("type"in n))return;const d=e.current;switch(n.type){case"PING":{o.current.add(n.tabId),t.current==="leader"&&c({type:"PONG",tabId:d,leaderTabId:d});break}case"PONG":{n.leaderTabId&&n.leaderTabId!==d&&(h(),E.current=Date.now());break}case"LEADER_ANNOUNCE":{if(n.tabId===d)break;o.current.add(n.tabId),n.tabId<d?(h(),E.current=Date.now()):n.tabId>d&&(t.current==="pending"||t.current==="leader")&&H();break}case"LEADER_RESIGN":{if(n.tabId===d)break;E.current=Date.now(),t.current==="follower"&&(i("pending"),t.current="pending",c({type:"PING",tabId:d}),v());break}case"HEARTBEAT":{t.current==="follower"&&(E.current=Date.now(),g(n.connectedCount));break}case"STATE_SNAPSHOT":{t.current==="follower"&&n.tabId!==d&&L.current(n.snapshot);break}}};u.addEventListener("message",C),c({type:"PING",tabId:e.current}),I.current=setTimeout(()=>{t.current==="pending"&&(i("leader"),t.current="leader",o.current.add(e.current),c({type:"LEADER_ANNOUNCE",tabId:e.current}),A())},N),m.current=setInterval(()=>{t.current==="follower"&&Date.now()-E.current>b*2&&(i("pending"),t.current="pending",c({type:"PING",tabId:e.current}),v())},b);const D=()=>{t.current==="leader"&&c({type:"LEADER_RESIGN",tabId:e.current})};return window.addEventListener("pagehide",D),()=>{window.removeEventListener("pagehide",D),f(),y(),m.current&&(clearInterval(m.current),m.current=null),u.removeEventListener("message",C),t.current==="leader"&&c({type:"LEADER_RESIGN",tabId:e.current}),u.close(),T.current=null}},[w,f,N,b,y]),a.useEffect(()=>{if(l!=="leader")return;const u={tracks:r.tracks,currentIndex:r.currentIndex,playlistMeta:r.playlistMeta,isPaused:r.isPaused,isShuffled:!1,repeatMode:r.repeatMode,playbackMode:r.playbackMode,volume:r.volume,muted:r.muted,playbackRate:r.playbackRate};R({type:"STATE_SNAPSHOT",tabId:e.current,snapshot:u})},[l,r.tracks,r.currentIndex,r.isPaused,r.repeatMode,r.playbackMode,r.playlistMeta,r.volume,r.muted,r.playbackRate,R]);const O=a.useCallback(()=>{f(),y(),i("leader"),t.current="leader",o.current.add(e.current),R({type:"LEADER_ANNOUNCE",tabId:e.current}),s.current&&clearInterval(s.current),s.current=setInterval(()=>{const u=e.current;o.current.add(u);const c=o.current.size;g(c),R({type:"HEARTBEAT",tabId:u,connectedCount:c})},b)},[f,b,R,y]);return{isLeader:l==="leader",isFollower:l==="follower",isPending:l==="pending",connectedTabs:P,claimLeadership:O,error:G}}exports.DEFAULT_REMOTE_CHANNEL_NAME=S;exports.useGingerRemote=F;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/remote/remoteProtocol.ts","../../src/remote/useGingerRemote.ts"],"sourcesContent":["import type { GingerInitPayload } from \"../types\";\n\nexport const DEFAULT_REMOTE_CHANNEL_NAME = \"ginger-remote\";\n\n/**\n * Cross-tab messages for {@link useGingerRemote}.\n */\nexport type RemoteMessage =\n | { type: \"PING\"; tabId: string }\n | { type: \"PONG\"; tabId: string; leaderTabId: string }\n | { type: \"LEADER_ANNOUNCE\"; tabId: string }\n | { type: \"LEADER_RESIGN\"; tabId: string }\n | { type: \"HEARTBEAT\"; tabId: string; connectedCount: number }\n | { type: \"STATE_SNAPSHOT\"; tabId: string; snapshot: GingerInitPayload };\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport type { GingerInitPayload } from \"../types\";\nimport { DEFAULT_REMOTE_CHANNEL_NAME, type RemoteMessage } from \"./remoteProtocol\";\n\nexport type UseGingerRemoteOptions = {\n /** BroadcastChannel name. Default: `\"ginger-remote\"`. */\n channelName?: string;\n /** Leader heartbeat interval in ms. Default: `2000`. */\n heartbeatMs?: number;\n /** Time to wait for an existing leader before claiming leadership. Default: `300`. */\n electionTimeoutMs?: number;\n};\n\nexport type UseGingerRemoteResult = {\n isLeader: boolean;\n isFollower: boolean;\n /** True until a leader is elected or this tab becomes leader. */\n isPending: boolean;\n connectedTabs: number;\n /** Request leadership (other tabs may win if their `tabId` is lexicographically smaller). */\n claimLeadership: () => void;\n error: string | null;\n};\n\nfunction makeTabId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `ginger-tab-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Multi-tab coordination via `BroadcastChannel`: elects a single leader tab and syncs\n * playback state to followers with `INIT` snapshots.\n *\n * Mount `Ginger.Player` only on the leader tab so one `<audio>` element plays:\n *\n * ```tsx\n * const { isLeader } = useGingerRemote();\n * return <>{isLeader && <Ginger.Player />}</>;\n * ```\n *\n * ```ts\n * import { useGingerRemote } from \"@lucaismyname/ginger/remote\";\n * ```\n */\nexport function useGingerRemote(options: UseGingerRemoteOptions = {}): UseGingerRemoteResult {\n const {\n channelName = DEFAULT_REMOTE_CHANNEL_NAME,\n heartbeatMs = 2000,\n electionTimeoutMs = 300,\n } = options;\n\n const { state, init } = useGinger();\n\n const tabIdRef = useRef<string>(\"\");\n if (tabIdRef.current === \"\") {\n tabIdRef.current = makeTabId();\n }\n\n const [role, setRole] = useState<\"pending\" | \"leader\" | \"follower\">(\"pending\");\n const [connectedTabs, setConnectedTabs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const roleRef = useRef(role);\n roleRef.current = role;\n\n const initRef = useRef(init);\n initRef.current = init;\n\n const channelRef = useRef<BroadcastChannel | null>(null);\n const knownTabsRef = useRef<Set<string>>(new Set());\n const lastHeartbeatAtRef = useRef<number>(Date.now());\n const electionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const leaderWatchRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const post = useCallback((msg: RemoteMessage) => {\n channelRef.current?.postMessage(msg);\n }, []);\n\n const clearElectionTimer = useCallback(() => {\n if (electionTimerRef.current) {\n clearTimeout(electionTimerRef.current);\n electionTimerRef.current = null;\n }\n }, []);\n\n const stopHeartbeat = useCallback(() => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n heartbeatTimerRef.current = null;\n }\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof BroadcastChannel === \"undefined\") {\n setError(\"BroadcastChannel is not available in this environment\");\n return;\n }\n\n setError(null);\n const ch = new BroadcastChannel(channelName);\n channelRef.current = ch;\n knownTabsRef.current = new Set([tabIdRef.current]);\n\n const postMsg = (msg: RemoteMessage) => {\n ch.postMessage(msg);\n };\n\n const startLeaderHeartbeat = () => {\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n postMsg({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n };\n\n const becomeFollower = () => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"follower\");\n roleRef.current = \"follower\";\n };\n\n const becomeLeaderFromRemote = () => {\n clearElectionTimer();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n };\n\n const scheduleElection = () => {\n clearElectionTimer();\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n };\n\n const onMessage = (ev: MessageEvent<RemoteMessage>) => {\n const msg = ev.data;\n if (!msg || typeof msg !== \"object\" || !(\"type\" in msg)) return;\n\n const myId = tabIdRef.current;\n\n switch (msg.type) {\n case \"PING\": {\n knownTabsRef.current.add(msg.tabId);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"PONG\", tabId: myId, leaderTabId: myId });\n }\n break;\n }\n case \"PONG\": {\n if (msg.leaderTabId && msg.leaderTabId !== myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n }\n break;\n }\n case \"LEADER_ANNOUNCE\": {\n if (msg.tabId === myId) break;\n knownTabsRef.current.add(msg.tabId);\n if (msg.tabId < myId) {\n becomeFollower();\n lastHeartbeatAtRef.current = Date.now();\n } else if (\n msg.tabId > myId &&\n (roleRef.current === \"pending\" || roleRef.current === \"leader\")\n ) {\n becomeLeaderFromRemote();\n }\n break;\n }\n case \"LEADER_RESIGN\": {\n if (msg.tabId === myId) break;\n lastHeartbeatAtRef.current = Date.now();\n if (roleRef.current === \"follower\") {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: myId });\n scheduleElection();\n }\n break;\n }\n case \"HEARTBEAT\": {\n if (roleRef.current === \"follower\") {\n lastHeartbeatAtRef.current = Date.now();\n setConnectedTabs(msg.connectedCount);\n }\n break;\n }\n case \"STATE_SNAPSHOT\": {\n if (roleRef.current === \"follower\" && msg.tabId !== myId) {\n initRef.current(msg.snapshot);\n }\n break;\n }\n default:\n break;\n }\n };\n\n ch.addEventListener(\"message\", onMessage);\n\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n\n electionTimerRef.current = setTimeout(() => {\n if (roleRef.current === \"pending\") {\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n postMsg({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n startLeaderHeartbeat();\n }\n }, electionTimeoutMs);\n\n leaderWatchRef.current = setInterval(() => {\n if (roleRef.current !== \"follower\") return;\n if (Date.now() - lastHeartbeatAtRef.current > heartbeatMs * 2) {\n setRole(\"pending\");\n roleRef.current = \"pending\";\n postMsg({ type: \"PING\", tabId: tabIdRef.current });\n scheduleElection();\n }\n }, heartbeatMs);\n\n const onPageHide = () => {\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n };\n window.addEventListener(\"pagehide\", onPageHide);\n\n return () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n clearElectionTimer();\n stopHeartbeat();\n if (leaderWatchRef.current) {\n clearInterval(leaderWatchRef.current);\n leaderWatchRef.current = null;\n }\n ch.removeEventListener(\"message\", onMessage);\n if (roleRef.current === \"leader\") {\n postMsg({ type: \"LEADER_RESIGN\", tabId: tabIdRef.current });\n }\n ch.close();\n channelRef.current = null;\n };\n }, [channelName, clearElectionTimer, electionTimeoutMs, heartbeatMs, stopHeartbeat]);\n\n useEffect(() => {\n if (role !== \"leader\") return;\n const snapshot: GingerInitPayload = {\n tracks: state.tracks,\n currentIndex: state.currentIndex,\n playlistMeta: state.playlistMeta,\n isPaused: state.isPaused,\n /** Avoid `createInitialState` re-shuffling on followers; queue order is already canonical. */\n isShuffled: false,\n repeatMode: state.repeatMode,\n playbackMode: state.playbackMode,\n volume: state.volume,\n muted: state.muted,\n playbackRate: state.playbackRate,\n };\n post({\n type: \"STATE_SNAPSHOT\",\n tabId: tabIdRef.current,\n snapshot,\n });\n }, [\n role,\n state.tracks,\n state.currentIndex,\n state.isPaused,\n state.repeatMode,\n state.playbackMode,\n state.playlistMeta,\n state.volume,\n state.muted,\n state.playbackRate,\n post,\n ]);\n\n const claimLeadership = useCallback(() => {\n clearElectionTimer();\n stopHeartbeat();\n setRole(\"leader\");\n roleRef.current = \"leader\";\n knownTabsRef.current.add(tabIdRef.current);\n post({ type: \"LEADER_ANNOUNCE\", tabId: tabIdRef.current });\n if (heartbeatTimerRef.current) {\n clearInterval(heartbeatTimerRef.current);\n }\n heartbeatTimerRef.current = setInterval(() => {\n const leaderId = tabIdRef.current;\n knownTabsRef.current.add(leaderId);\n const count = knownTabsRef.current.size;\n setConnectedTabs(count);\n post({ type: \"HEARTBEAT\", tabId: leaderId, connectedCount: count });\n }, heartbeatMs);\n }, [clearElectionTimer, heartbeatMs, post, stopHeartbeat]);\n\n return {\n isLeader: role === \"leader\",\n isFollower: role === \"follower\",\n isPending: role === \"pending\",\n connectedTabs,\n claimLeadership,\n error,\n };\n}\n"],"names":["DEFAULT_REMOTE_CHANNEL_NAME","makeTabId","useGingerRemote","options","channelName","heartbeatMs","electionTimeoutMs","state","init","useGinger","tabIdRef","useRef","role","setRole","useState","connectedTabs","setConnectedTabs","error","setError","roleRef","initRef","channelRef","knownTabsRef","lastHeartbeatAtRef","electionTimerRef","heartbeatTimerRef","leaderWatchRef","post","useCallback","msg","_a","clearElectionTimer","stopHeartbeat","useEffect","ch","postMsg","startLeaderHeartbeat","leaderId","count","becomeFollower","becomeLeaderFromRemote","scheduleElection","onMessage","ev","myId","onPageHide","snapshot","claimLeadership"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_REMOTE_CHANNEL_NAME } from './remoteProtocol';
|
|
2
|
+
export type { RemoteMessage } from './remoteProtocol';
|
|
3
|
+
export { useGingerRemote } from './useGingerRemote';
|
|
4
|
+
export type { UseGingerRemoteOptions, UseGingerRemoteResult } from './useGingerRemote';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|