@lucaismyname/ginger 0.0.41 → 0.0.43

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.
Files changed (50) hide show
  1. package/README.md +7 -1
  2. package/dist/client.cjs +1 -1
  3. package/dist/client.js +4 -4
  4. package/dist/crossfade/crossfadeGraph.d.ts +47 -0
  5. package/dist/crossfade/crossfadeGraph.d.ts.map +1 -0
  6. package/dist/crossfade/index.cjs +2 -0
  7. package/dist/crossfade/index.cjs.map +1 -0
  8. package/dist/crossfade/index.d.ts +5 -0
  9. package/dist/crossfade/index.d.ts.map +1 -0
  10. package/dist/crossfade/index.js +124 -0
  11. package/dist/crossfade/index.js.map +1 -0
  12. package/dist/crossfade/useGingerCrossfade.d.ts +66 -0
  13. package/dist/crossfade/useGingerCrossfade.d.ts.map +1 -0
  14. package/dist/equalizer/index.cjs +1 -1
  15. package/dist/equalizer/index.js +1 -1
  16. package/dist/{ginger-CgHqHrrG.js → ginger-B2DgE-2a.js} +5 -4
  17. package/dist/{ginger-CgHqHrrG.js.map → ginger-B2DgE-2a.js.map} +1 -1
  18. package/dist/ginger-Dv3iO_xQ.cjs +2 -0
  19. package/dist/{ginger-DFdZGaMi.cjs.map → ginger-Dv3iO_xQ.cjs.map} +1 -1
  20. package/dist/index.cjs +1 -1
  21. package/dist/index.js +4 -4
  22. package/dist/remote/index.cjs +1 -1
  23. package/dist/remote/index.js +1 -1
  24. package/dist/selectors-BT3WSsKN.js +42 -0
  25. package/dist/selectors-BT3WSsKN.js.map +1 -0
  26. package/dist/selectors-CEGlYoFu.cjs +2 -0
  27. package/dist/selectors-CEGlYoFu.cjs.map +1 -0
  28. package/dist/spatial/index.cjs +1 -1
  29. package/dist/spatial/index.js +1 -1
  30. package/dist/testing/index.cjs +1 -1
  31. package/dist/testing/index.js +1 -1
  32. package/dist/transitions-CmNkf3sd.js +90 -0
  33. package/dist/transitions-CmNkf3sd.js.map +1 -0
  34. package/dist/transitions-Dx08t68T.cjs +2 -0
  35. package/dist/transitions-Dx08t68T.cjs.map +1 -0
  36. package/dist/{useGinger-BXgia32v.cjs → useGinger-4uvPoChz.cjs} +2 -2
  37. package/dist/{useGinger-BXgia32v.cjs.map → useGinger-4uvPoChz.cjs.map} +1 -1
  38. package/dist/{useGinger-hpp2pAGY.js → useGinger-Dz0cPyD1.js} +2 -2
  39. package/dist/{useGinger-hpp2pAGY.js.map → useGinger-Dz0cPyD1.js.map} +1 -1
  40. package/dist/useGingerChapterProgress-D2pdmyjg.cjs +2 -0
  41. package/dist/{useGingerChapterProgress-COLWYX2-.cjs.map → useGingerChapterProgress-D2pdmyjg.cjs.map} +1 -1
  42. package/dist/{useGingerChapterProgress-Jj_zfnds.js → useGingerChapterProgress-wxAmN_uo.js} +25 -24
  43. package/dist/{useGingerChapterProgress-Jj_zfnds.js.map → useGingerChapterProgress-wxAmN_uo.js.map} +1 -1
  44. package/package.json +6 -1
  45. package/dist/ginger-DFdZGaMi.cjs +0 -2
  46. package/dist/selectors-BalBCc7X.js +0 -127
  47. package/dist/selectors-BalBCc7X.js.map +0 -1
  48. package/dist/selectors-YXnP8Y8g.cjs +0 -2
  49. package/dist/selectors-YXnP8Y8g.cjs.map +0 -1
  50. package/dist/useGingerChapterProgress-COLWYX2-.cjs +0 -2
package/README.md CHANGED
@@ -524,8 +524,13 @@ Props:
524
524
  | `onTrackChange` | `(track, index) => void` | `undefined` | Fires when current track changes |
525
525
  | `onPlay` | `() => void` | `undefined` | Fires when state changes to playing |
526
526
  | `onPause` | `() => void` | `undefined` | Fires when state changes to paused |
527
- | `onQueueEnd` | `() => void` | `undefined` | Fires when playback reaches the end with repeat off |
527
+ | `onQueueEnd` | `() => void` | `undefined` | Fires when playback end resolves to a stop transition (e.g. end of playlist in `playlist` mode, or any track end in `single` mode unless repeat is `one`) |
528
528
  | `onError` | `(message) => void` | `undefined` | Fires on media/playback errors |
529
+ | `onVolumeChange` | `(volume: number, muted: boolean) => void` | `undefined` | Fires when volume or muted state changes |
530
+ | `onPlaybackRateChange` | `(rate: number) => void` | `undefined` | Fires when playback speed changes |
531
+ | `onSeek` | `(timeSeconds: number) => void` | `undefined` | Fires whenever `seek()` is invoked |
532
+ | `dir` | `"ltr" \| "rtl" \| "auto"` | locale-derived | Explicit provider layout direction |
533
+ | `prevRestartThresholdSeconds` | `number` | `3` | Previous restarts current track when `currentTime > threshold`; set `0` to always skip |
529
534
 
530
535
  ### `Ginger.Player`
531
536
 
@@ -1210,6 +1215,7 @@ Additional entrypoints:
1210
1215
  - `@lucaismyname/ginger/client`
1211
1216
  - `@lucaismyname/ginger/testing`
1212
1217
  - `@lucaismyname/ginger/waveform`
1218
+ - `@lucaismyname/ginger/equalizer`
1213
1219
  - `@lucaismyname/ginger/spatial`
1214
1220
  - `@lucaismyname/ginger/transcript`
1215
1221
  - `@lucaismyname/ginger/remote`
package/dist/client.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-DFdZGaMi.cjs"),s=require("./useGinger-BXgia32v.cjs"),r=require("./useGingerChapterProgress-COLWYX2-.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;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./ginger-Dv3iO_xQ.cjs"),s=require("./useGinger-4uvPoChz.cjs"),r=require("./useGingerChapterProgress-D2pdmyjg.cjs"),i=require("./liveAudioGraph-0cpHD_Ic.cjs"),n=require("./selectors-CEGlYoFu.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,8 +1,8 @@
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-CgHqHrrG.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-Jj_zfnds.js";
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-B2DgE-2a.js";
2
+ import { u as C } from "./useGinger-Dz0cPyD1.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-wxAmN_uo.js";
4
4
  import { a as H, d as I, s as K } from "./liveAudioGraph-DvPaxBCP.js";
5
- import { d as N } from "./selectors-BalBCc7X.js";
5
+ import { d as N } from "./selectors-BT3WSsKN.js";
6
6
  import { g as X, a as q, u as E, b as J, c as O } from "./GingerSplitContexts-BzBExb95.js";
7
7
  export {
8
8
  s as Chapters,
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Web Audio graph management for crossfade transitions.
3
+ *
4
+ * Creates a shared `AudioContext` that routes both the outgoing and incoming
5
+ * `HTMLAudioElement` through individual `GainNode`s into the same destination.
6
+ * Scheduling the gain ramps on both nodes produces the crossfade effect.
7
+ *
8
+ * **Compatibility note:** because the browser only permits one
9
+ * `MediaElementAudioSourceNode` per `HTMLAudioElement`, this module is
10
+ * incompatible with `liveAudioGraph`-based features (`useGingerEqualizer`,
11
+ * `useGingerLiveAnalyzer`) on the same element. Using both simultaneously will
12
+ * throw a `DOMException` when the second source node is requested.
13
+ */
14
+ export type CrossfadeCurve = "linear" | "equal-power";
15
+ export type CrossfadeGraph = {
16
+ context: AudioContext;
17
+ outGain: GainNode;
18
+ inGain: GainNode;
19
+ outSource: MediaElementAudioSourceNode;
20
+ inSource: MediaElementAudioSourceNode;
21
+ };
22
+ /**
23
+ * Creates a shared `AudioContext` and connects both the outgoing and incoming
24
+ * audio elements to it via individual `GainNode`s.
25
+ *
26
+ * The outgoing gain starts at 1, the incoming gain starts at 0.
27
+ * Call `scheduleCrossfade` immediately after to begin the ramps.
28
+ *
29
+ * @throws `DOMException` if either element already has a `MediaElementAudioSourceNode`
30
+ * in another context (e.g. created by `liveAudioGraph`).
31
+ * @throws `Error` if the Web Audio API is unavailable in this environment.
32
+ */
33
+ export declare function attachCrossfadeGraph(outgoing: HTMLAudioElement, incoming: HTMLAudioElement): CrossfadeGraph;
34
+ /**
35
+ * Schedules gain ramps on both gain nodes so that `outGain` fades from 1 → 0
36
+ * and `inGain` fades from 0 → 1 over `durationSec` seconds starting immediately.
37
+ *
38
+ * For `"equal-power"`, a cosine/sine curve is applied via `setValueCurveAtTime`
39
+ * to maintain consistent perceived loudness throughout the transition.
40
+ */
41
+ export declare function scheduleCrossfade(graph: CrossfadeGraph, durationSec: number, curve: CrossfadeCurve): void;
42
+ /**
43
+ * Disconnects all nodes and closes the `AudioContext`.
44
+ * Safe to call multiple times; errors during disconnect are silently ignored.
45
+ */
46
+ export declare function teardownCrossfadeGraph(graph: CrossfadeGraph): void;
47
+ //# sourceMappingURL=crossfadeGraph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossfadeGraph.d.ts","sourceRoot":"","sources":["../../src/crossfade/crossfadeGraph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,QAAQ,CAAC;IAClB,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,EAAE,2BAA2B,CAAC;IACvC,QAAQ,EAAE,2BAA2B,CAAC;CACvC,CAAC;AAuBF;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,EAAE,gBAAgB,GACzB,cAAc,CA0BhB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,cAAc,GACpB,IAAI,CAeN;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAUlE"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("react"),_=require("../GingerSplitContexts-C7puo0M7.cjs"),W=require("../transitions-Dx08t68T.cjs"),w=256;function z(){const r=new Float32Array(w),i=new Float32Array(w);for(let o=0;o<w;o++){const n=o/(w-1);r[o]=Math.cos(n*(Math.PI/2)),i[o]=Math.sin(n*(Math.PI/2))}return{outCurve:r,inCurve:i}}function j(){if(!(typeof window>"u"))return window.AudioContext??window.webkitAudioContext}function O(r,i){const o=j();if(!o)throw new Error("[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.");const n=new o,c=n.createMediaElementSource(r),u=n.createMediaElementSource(i),t=n.createGain(),s=n.createGain();return t.gain.value=1,s.gain.value=0,c.connect(t),t.connect(n.destination),u.connect(s),s.connect(n.destination),{context:n,outGain:t,inGain:s,outSource:c,inSource:u}}function D(r,i,o){const{context:n,outGain:c,inGain:u}=r,t=n.currentTime,s=t+i;if(o==="equal-power"){const{outCurve:g,inCurve:p}=z();c.gain.setValueCurveAtTime(g,t,i),u.gain.setValueCurveAtTime(p,t,i)}else c.gain.setValueAtTime(1,t),c.gain.linearRampToValueAtTime(0,s),u.gain.setValueAtTime(0,t),u.gain.linearRampToValueAtTime(1,s)}function I(r){const i=[r.outSource,r.inSource,r.outGain,r.inGain];for(const o of i)try{o.disconnect()}catch{}r.context.close()}function H(r={}){const{duration:i=3,curve:o="equal-power",crossOrigin:n,enabled:c=!0}=r,{tracks:u,currentIndex:t,isPaused:s,repeatMode:g,playbackMode:p,dispatch:M}=_.useGingerPlayback(),{currentTime:P,duration:G,audioRef:S,muted:A,volume:C}=_.useGingerMedia(),[N,b]=l.useState(!1),[U,v]=l.useState(0),d=l.useRef(null),E=l.useCallback(()=>{const e=d.current;e&&(e.aborted=!0,clearTimeout(e.timeoutId),cancelAnimationFrame(e.rafId),I(e.graph),e.incomingAudio.pause(),e.incomingAudio.removeAttribute("src"),e.incomingAudio.load(),d.current=null,b(!1),v(0))},[]);return l.useEffect(()=>{const e=d.current;e&&(s||t!==e.startedAtIndex)&&E()},[s,t,E]),l.useEffect(()=>()=>E(),[]),l.useEffect(()=>{const e=d.current;e&&(e.incomingAudio.volume=C,e.incomingAudio.muted=A)},[C,A]),l.useEffect(()=>{if(!c||d.current||s||!(G>0))return;const e=G-P;if(e>i||e<=0)return;const T=W.computeEndedTransition({tracks:u,currentIndex:t,repeatMode:g,playbackMode:p});if(T.kind==="stop")return;const q=T.kind==="replay_same"?t:T.nextIndex,h=u[q];if(!(h!=null&&h.fileUrl))return;const R=S.current;if(!R)return;const a=document.createElement("audio");a.preload="auto",a.volume=C,a.muted=A,n&&(a.crossOrigin=n),a.src=h.fileUrl;let m;try{m=O(R,a)}catch(f){process.env.NODE_ENV!=="production"&&console.warn("[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. This may be because the audio element is already connected to a Web Audio graph (e.g. via useGingerEqualizer or useGingerLiveAnalyzer). These features are incompatible with useGingerCrossfade.",f);return}m.context.resume(),a.load(),a.play().catch(()=>{}),D(m,e,o);const k=performance.now(),y=e*1e3;b(!0),v(0);let x=0;const V=()=>{const f=performance.now()-k,F=Math.min(1,f/y);v(F),F<1&&(x=requestAnimationFrame(V))};x=requestAnimationFrame(V);const L=setTimeout(()=>{const f=d.current;!f||f.aborted||(M({type:"SET_INDEX",payload:{index:q,autoPlay:!0}}),I(m),a.pause(),a.removeAttribute("src"),a.load(),d.current=null,b(!1),v(0))},y);d.current={graph:m,incomingAudio:a,startedAtIndex:t,startTime:k,fadeDurationMs:y,timeoutId:L,rafId:x,aborted:!1}},[c,s,G,P,i,o,n,u,t,g,p,S,C,A,M]),{isCrossfading:N,crossfadeProgress:U}}exports.attachCrossfadeGraph=O;exports.scheduleCrossfade=D;exports.teardownCrossfadeGraph=I;exports.useGingerCrossfade=H;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../../src/crossfade/crossfadeGraph.ts","../../src/crossfade/useGingerCrossfade.ts"],"sourcesContent":["/**\n * Web Audio graph management for crossfade transitions.\n *\n * Creates a shared `AudioContext` that routes both the outgoing and incoming\n * `HTMLAudioElement` through individual `GainNode`s into the same destination.\n * Scheduling the gain ramps on both nodes produces the crossfade effect.\n *\n * **Compatibility note:** because the browser only permits one\n * `MediaElementAudioSourceNode` per `HTMLAudioElement`, this module is\n * incompatible with `liveAudioGraph`-based features (`useGingerEqualizer`,\n * `useGingerLiveAnalyzer`) on the same element. Using both simultaneously will\n * throw a `DOMException` when the second source node is requested.\n */\n\nexport type CrossfadeCurve = \"linear\" | \"equal-power\";\n\nexport type CrossfadeGraph = {\n context: AudioContext;\n outGain: GainNode;\n inGain: GainNode;\n outSource: MediaElementAudioSourceNode;\n inSource: MediaElementAudioSourceNode;\n};\n\nconst EQUAL_POWER_CURVE_LENGTH = 256;\n\nfunction buildEqualPowerCurves(): { outCurve: Float32Array; inCurve: Float32Array } {\n const outCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n const inCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n for (let i = 0; i < EQUAL_POWER_CURVE_LENGTH; i++) {\n const t = i / (EQUAL_POWER_CURVE_LENGTH - 1);\n outCurve[i] = Math.cos(t * (Math.PI / 2));\n inCurve[i] = Math.sin(t * (Math.PI / 2));\n }\n return { outCurve, inCurve };\n}\n\nfunction getAudioContextCtor(): (new (options?: AudioContextOptions) => AudioContext) | undefined {\n if (typeof window === \"undefined\") return undefined;\n return (\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext\n );\n}\n\n/**\n * Creates a shared `AudioContext` and connects both the outgoing and incoming\n * audio elements to it via individual `GainNode`s.\n *\n * The outgoing gain starts at 1, the incoming gain starts at 0.\n * Call `scheduleCrossfade` immediately after to begin the ramps.\n *\n * @throws `DOMException` if either element already has a `MediaElementAudioSourceNode`\n * in another context (e.g. created by `liveAudioGraph`).\n * @throws `Error` if the Web Audio API is unavailable in this environment.\n */\nexport function attachCrossfadeGraph(\n outgoing: HTMLAudioElement,\n incoming: HTMLAudioElement,\n): CrossfadeGraph {\n const Ctor = getAudioContextCtor();\n if (!Ctor) {\n throw new Error(\n \"[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.\",\n );\n }\n\n const context = new Ctor();\n\n const outSource = context.createMediaElementSource(outgoing);\n const inSource = context.createMediaElementSource(incoming);\n\n const outGain = context.createGain();\n const inGain = context.createGain();\n\n outGain.gain.value = 1;\n inGain.gain.value = 0;\n\n outSource.connect(outGain);\n outGain.connect(context.destination);\n\n inSource.connect(inGain);\n inGain.connect(context.destination);\n\n return { context, outGain, inGain, outSource, inSource };\n}\n\n/**\n * Schedules gain ramps on both gain nodes so that `outGain` fades from 1 → 0\n * and `inGain` fades from 0 → 1 over `durationSec` seconds starting immediately.\n *\n * For `\"equal-power\"`, a cosine/sine curve is applied via `setValueCurveAtTime`\n * to maintain consistent perceived loudness throughout the transition.\n */\nexport function scheduleCrossfade(\n graph: CrossfadeGraph,\n durationSec: number,\n curve: CrossfadeCurve,\n): void {\n const { context, outGain, inGain } = graph;\n const startTime = context.currentTime;\n const endTime = startTime + durationSec;\n\n if (curve === \"equal-power\") {\n const { outCurve, inCurve } = buildEqualPowerCurves();\n outGain.gain.setValueCurveAtTime(outCurve, startTime, durationSec);\n inGain.gain.setValueCurveAtTime(inCurve, startTime, durationSec);\n } else {\n outGain.gain.setValueAtTime(1, startTime);\n outGain.gain.linearRampToValueAtTime(0, endTime);\n inGain.gain.setValueAtTime(0, startTime);\n inGain.gain.linearRampToValueAtTime(1, endTime);\n }\n}\n\n/**\n * Disconnects all nodes and closes the `AudioContext`.\n * Safe to call multiple times; errors during disconnect are silently ignored.\n */\nexport function teardownCrossfadeGraph(graph: CrossfadeGraph): void {\n const nodes: AudioNode[] = [graph.outSource, graph.inSource, graph.outGain, graph.inGain];\n for (const node of nodes) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n void graph.context.close();\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeEndedTransition } from \"../core/transitions\";\nimport {\n type CrossfadeCurve,\n type CrossfadeGraph,\n attachCrossfadeGraph,\n scheduleCrossfade,\n teardownCrossfadeGraph,\n} from \"./crossfadeGraph\";\n\nexport type { CrossfadeCurve };\n\nexport type UseGingerCrossfadeOptions = {\n /**\n * Duration of the crossfade in seconds.\n * The hook begins the fade when `timeRemaining ≤ duration`.\n * @default 3\n */\n duration?: number;\n /**\n * Gain curve shape applied to both gain nodes.\n * `\"equal-power\"` uses a cosine/sine curve to maintain consistent perceived\n * loudness; `\"linear\"` is a straight ramp that may dip slightly at the midpoint.\n * @default \"equal-power\"\n */\n curve?: CrossfadeCurve;\n /**\n * `crossOrigin` attribute for the incoming `<audio>` element.\n * Match this to the `crossOrigin` prop on `Ginger.Player` when serving\n * cross-origin audio so the browser can reuse the cached resource.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\";\n /**\n * When `false`, crossfade is completely disabled and playback falls back to\n * the default hard-cut transition.\n * @default true\n */\n enabled?: boolean;\n};\n\nexport type UseGingerCrossfadeResult = {\n /** `true` while a crossfade transition is actively in progress. */\n isCrossfading: boolean;\n /**\n * Progress of the current crossfade from `0` (start) to `1` (complete).\n * Always `0` when idle.\n */\n crossfadeProgress: number;\n};\n\ntype CrossfadeSession = {\n graph: CrossfadeGraph;\n incomingAudio: HTMLAudioElement;\n startedAtIndex: number;\n startTime: number;\n fadeDurationMs: number;\n timeoutId: ReturnType<typeof setTimeout>;\n rafId: number;\n aborted: boolean;\n};\n\n/**\n * Smoothly crossfades between consecutive tracks using the Web Audio API.\n *\n * When the remaining time on the current track falls below `duration`, the hook:\n * 1. Creates a hidden `<audio>` element and begins loading the next track.\n * 2. Routes both the outgoing and incoming elements through `GainNode`s in a\n * shared `AudioContext`.\n * 3. Schedules gain ramps so the outgoing track fades out while the incoming\n * track fades in simultaneously.\n * 4. Dispatches `SET_INDEX` once the ramp completes so the Ginger queue\n * advances to the new track.\n *\n * **Limitations:**\n * - Incompatible with `useGingerEqualizer` and `useGingerLiveAnalyzer` on the\n * same element — the browser only permits one `MediaElementAudioSourceNode`\n * per `<audio>` element.\n * - Requires a prior user gesture before `AudioContext` can be resumed (standard\n * Web Audio policy).\n * - When `repeatMode` is `\"one\"`, the crossfade replays the same track from the\n * beginning, matching the standard `notifyEnded` behaviour.\n *\n * Available as a subpath import:\n * ```ts\n * import { useGingerCrossfade } from \"@lucaismyname/ginger/crossfade\";\n * ```\n */\nexport function useGingerCrossfade(\n options: UseGingerCrossfadeOptions = {},\n): UseGingerCrossfadeResult {\n const { duration = 3, curve = \"equal-power\", crossOrigin, enabled = true } = options;\n\n const { tracks, currentIndex, isPaused, repeatMode, playbackMode, dispatch } =\n useGingerPlayback();\n const { currentTime, duration: trackDuration, audioRef, muted, volume } = useGingerMedia();\n\n const [isCrossfading, setIsCrossfading] = useState(false);\n const [crossfadeProgress, setCrossfadeProgress] = useState(0);\n\n const sessionRef = useRef<CrossfadeSession | null>(null);\n\n const abort = useCallback(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.aborted = true;\n clearTimeout(session.timeoutId);\n cancelAnimationFrame(session.rafId);\n teardownCrossfadeGraph(session.graph);\n session.incomingAudio.pause();\n session.incomingAudio.removeAttribute(\"src\");\n session.incomingAudio.load();\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, []);\n\n // Abort if the user pauses or manually advances/changes the track mid-fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n if (isPaused || currentIndex !== session.startedAtIndex) {\n abort();\n }\n }, [isPaused, currentIndex, abort]);\n\n // Clean up on unmount.\n // biome-ignore lint/correctness/useExhaustiveDependencies: abort is stable; intentional unmount-only cleanup\n useEffect(() => () => abort(), []);\n\n // Keep the incoming element's volume/muted in sync with Ginger state so that\n // the user's volume control applies to the incoming track during the fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.incomingAudio.volume = volume;\n session.incomingAudio.muted = muted;\n }, [volume, muted]);\n\n // Main trigger: start a crossfade when time remaining ≤ duration.\n useEffect(() => {\n if (!enabled) return;\n if (sessionRef.current) return; // already in progress\n if (isPaused) return;\n if (!(trackDuration > 0)) return;\n\n const timeRemaining = trackDuration - currentTime;\n if (timeRemaining > duration || timeRemaining <= 0) return;\n\n // Determine what comes next using the same logic as notifyEnded.\n const transition = computeEndedTransition({ tracks, currentIndex, repeatMode, playbackMode });\n if (transition.kind === \"stop\") return; // queue ends — nothing to crossfade into\n\n const nextIndex = transition.kind === \"replay_same\" ? currentIndex : transition.nextIndex;\n const nextTrack = tracks[nextIndex];\n if (!nextTrack?.fileUrl) return;\n\n const mainEl = audioRef.current;\n if (!mainEl) return;\n\n // Create the incoming element before attaching the graph so that both\n // elements are ready when createMediaElementSource is called.\n const incomingAudio = document.createElement(\"audio\");\n incomingAudio.preload = \"auto\";\n incomingAudio.volume = volume;\n incomingAudio.muted = muted;\n if (crossOrigin) incomingAudio.crossOrigin = crossOrigin;\n incomingAudio.src = nextTrack.fileUrl;\n\n let graph: CrossfadeGraph;\n try {\n graph = attachCrossfadeGraph(mainEl, incomingAudio);\n } catch (e) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. \" +\n \"This may be because the audio element is already connected to a Web Audio graph \" +\n \"(e.g. via useGingerEqualizer or useGingerLiveAnalyzer). \" +\n \"These features are incompatible with useGingerCrossfade.\",\n e,\n );\n }\n return;\n }\n\n // Browsers suspend AudioContext until a user gesture has occurred.\n void graph.context.resume();\n\n incomingAudio.load();\n void incomingAudio.play().catch(() => {\n // Autoplay may be blocked; the gain ramps continue regardless and the\n // incoming audio will play once the browser permits it.\n });\n\n scheduleCrossfade(graph, timeRemaining, curve);\n\n const startTime = performance.now();\n const fadeDurationMs = timeRemaining * 1000;\n\n setIsCrossfading(true);\n setCrossfadeProgress(0);\n\n // rAF loop: drive crossfadeProgress for consumers (visualisers, UI indicators).\n let rafId = 0;\n const tick = () => {\n const elapsed = performance.now() - startTime;\n const progress = Math.min(1, elapsed / fadeDurationMs);\n setCrossfadeProgress(progress);\n if (progress < 1) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n\n // Once the gain ramps complete, advance the Ginger queue.\n const timeoutId = setTimeout(() => {\n const session = sessionRef.current;\n if (!session || session.aborted) return;\n\n // GingerPlayer will swap the src on the main element and resume playback.\n dispatch({ type: \"SET_INDEX\", payload: { index: nextIndex, autoPlay: true } });\n\n teardownCrossfadeGraph(graph);\n incomingAudio.pause();\n incomingAudio.removeAttribute(\"src\");\n incomingAudio.load();\n\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, fadeDurationMs);\n\n sessionRef.current = {\n graph,\n incomingAudio,\n startedAtIndex: currentIndex,\n startTime,\n fadeDurationMs,\n timeoutId,\n rafId,\n aborted: false,\n };\n }, [\n enabled,\n isPaused,\n trackDuration,\n currentTime,\n duration,\n curve,\n crossOrigin,\n tracks,\n currentIndex,\n repeatMode,\n playbackMode,\n audioRef,\n volume,\n muted,\n dispatch,\n ]);\n\n return { isCrossfading, crossfadeProgress };\n}\n"],"names":["EQUAL_POWER_CURVE_LENGTH","buildEqualPowerCurves","outCurve","inCurve","i","t","getAudioContextCtor","attachCrossfadeGraph","outgoing","incoming","Ctor","context","outSource","inSource","outGain","inGain","scheduleCrossfade","graph","durationSec","curve","startTime","endTime","teardownCrossfadeGraph","nodes","node","useGingerCrossfade","options","duration","crossOrigin","enabled","tracks","currentIndex","isPaused","repeatMode","playbackMode","dispatch","useGingerPlayback","currentTime","trackDuration","audioRef","muted","volume","useGingerMedia","isCrossfading","setIsCrossfading","useState","crossfadeProgress","setCrossfadeProgress","sessionRef","useRef","abort","useCallback","session","useEffect","timeRemaining","transition","computeEndedTransition","nextIndex","nextTrack","mainEl","incomingAudio","e","fadeDurationMs","rafId","tick","elapsed","progress","timeoutId"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { attachCrossfadeGraph, scheduleCrossfade, teardownCrossfadeGraph, } from './crossfadeGraph';
2
+ export type { CrossfadeCurve, CrossfadeGraph } from './crossfadeGraph';
3
+ export { useGingerCrossfade } from './useGingerCrossfade';
4
+ export type { UseGingerCrossfadeOptions, UseGingerCrossfadeResult, } from './useGingerCrossfade';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/crossfade/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EACV,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,124 @@
1
+ import { useState as S, useRef as U, useCallback as L, useEffect as h } from "react";
2
+ import { b as W, u as z } from "../GingerSplitContexts-BzBExb95.js";
3
+ import { c as H } from "../transitions-CmNkf3sd.js";
4
+ const w = 256;
5
+ function Q() {
6
+ const r = new Float32Array(w), i = new Float32Array(w);
7
+ for (let o = 0; o < w; o++) {
8
+ const n = o / (w - 1);
9
+ r[o] = Math.cos(n * (Math.PI / 2)), i[o] = Math.sin(n * (Math.PI / 2));
10
+ }
11
+ return { outCurve: r, inCurve: i };
12
+ }
13
+ function X() {
14
+ if (!(typeof window > "u"))
15
+ return window.AudioContext ?? window.webkitAudioContext;
16
+ }
17
+ function j(r, i) {
18
+ const o = X();
19
+ if (!o)
20
+ throw new Error(
21
+ "[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment."
22
+ );
23
+ const n = new o(), c = n.createMediaElementSource(r), u = n.createMediaElementSource(i), t = n.createGain(), s = n.createGain();
24
+ return t.gain.value = 1, s.gain.value = 0, c.connect(t), t.connect(n.destination), u.connect(s), s.connect(n.destination), { context: n, outGain: t, inGain: s, outSource: c, inSource: u };
25
+ }
26
+ function B(r, i, o) {
27
+ const { context: n, outGain: c, inGain: u } = r, t = n.currentTime, s = t + i;
28
+ if (o === "equal-power") {
29
+ const { outCurve: f, inCurve: g } = Q();
30
+ c.gain.setValueCurveAtTime(f, t, i), u.gain.setValueCurveAtTime(g, t, i);
31
+ } else
32
+ c.gain.setValueAtTime(1, t), c.gain.linearRampToValueAtTime(0, s), u.gain.setValueAtTime(0, t), u.gain.linearRampToValueAtTime(1, s);
33
+ }
34
+ function _(r) {
35
+ const i = [r.outSource, r.inSource, r.outGain, r.inGain];
36
+ for (const o of i)
37
+ try {
38
+ o.disconnect();
39
+ } catch {
40
+ }
41
+ r.context.close();
42
+ }
43
+ function Z(r = {}) {
44
+ const { duration: i = 3, curve: o = "equal-power", crossOrigin: n, enabled: c = !0 } = r, { tracks: u, currentIndex: t, isPaused: s, repeatMode: f, playbackMode: g, dispatch: I } = W(), { currentTime: M, duration: b, audioRef: P, muted: p, volume: A } = z(), [D, T] = S(!1), [N, v] = S(0), d = U(null), y = L(() => {
45
+ const e = d.current;
46
+ e && (e.aborted = !0, clearTimeout(e.timeoutId), cancelAnimationFrame(e.rafId), _(e.graph), e.incomingAudio.pause(), e.incomingAudio.removeAttribute("src"), e.incomingAudio.load(), d.current = null, T(!1), v(0));
47
+ }, []);
48
+ return h(() => {
49
+ const e = d.current;
50
+ e && (s || t !== e.startedAtIndex) && y();
51
+ }, [s, t, y]), h(() => () => y(), []), h(() => {
52
+ const e = d.current;
53
+ e && (e.incomingAudio.volume = A, e.incomingAudio.muted = p);
54
+ }, [A, p]), h(() => {
55
+ if (!c || d.current || s || !(b > 0)) return;
56
+ const e = b - M;
57
+ if (e > i || e <= 0) return;
58
+ const E = H({ tracks: u, currentIndex: t, repeatMode: f, playbackMode: g });
59
+ if (E.kind === "stop") return;
60
+ const k = E.kind === "replay_same" ? t : E.nextIndex, C = u[k];
61
+ if (!(C != null && C.fileUrl)) return;
62
+ const R = P.current;
63
+ if (!R) return;
64
+ const a = document.createElement("audio");
65
+ a.preload = "auto", a.volume = A, a.muted = p, n && (a.crossOrigin = n), a.src = C.fileUrl;
66
+ let m;
67
+ try {
68
+ m = j(R, a);
69
+ } catch (l) {
70
+ process.env.NODE_ENV !== "production" && console.warn(
71
+ "[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. This may be because the audio element is already connected to a Web Audio graph (e.g. via useGingerEqualizer or useGingerLiveAnalyzer). These features are incompatible with useGingerCrossfade.",
72
+ l
73
+ );
74
+ return;
75
+ }
76
+ m.context.resume(), a.load(), a.play().catch(() => {
77
+ }), B(m, e, o);
78
+ const V = performance.now(), G = e * 1e3;
79
+ T(!0), v(0);
80
+ let x = 0;
81
+ const q = () => {
82
+ const l = performance.now() - V, F = Math.min(1, l / G);
83
+ v(F), F < 1 && (x = requestAnimationFrame(q));
84
+ };
85
+ x = requestAnimationFrame(q);
86
+ const O = setTimeout(() => {
87
+ const l = d.current;
88
+ !l || l.aborted || (I({ type: "SET_INDEX", payload: { index: k, autoPlay: !0 } }), _(m), a.pause(), a.removeAttribute("src"), a.load(), d.current = null, T(!1), v(0));
89
+ }, G);
90
+ d.current = {
91
+ graph: m,
92
+ incomingAudio: a,
93
+ startedAtIndex: t,
94
+ startTime: V,
95
+ fadeDurationMs: G,
96
+ timeoutId: O,
97
+ rafId: x,
98
+ aborted: !1
99
+ };
100
+ }, [
101
+ c,
102
+ s,
103
+ b,
104
+ M,
105
+ i,
106
+ o,
107
+ n,
108
+ u,
109
+ t,
110
+ f,
111
+ g,
112
+ P,
113
+ A,
114
+ p,
115
+ I
116
+ ]), { isCrossfading: D, crossfadeProgress: N };
117
+ }
118
+ export {
119
+ j as attachCrossfadeGraph,
120
+ B as scheduleCrossfade,
121
+ _ as teardownCrossfadeGraph,
122
+ Z as useGingerCrossfade
123
+ };
124
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/crossfade/crossfadeGraph.ts","../../src/crossfade/useGingerCrossfade.ts"],"sourcesContent":["/**\n * Web Audio graph management for crossfade transitions.\n *\n * Creates a shared `AudioContext` that routes both the outgoing and incoming\n * `HTMLAudioElement` through individual `GainNode`s into the same destination.\n * Scheduling the gain ramps on both nodes produces the crossfade effect.\n *\n * **Compatibility note:** because the browser only permits one\n * `MediaElementAudioSourceNode` per `HTMLAudioElement`, this module is\n * incompatible with `liveAudioGraph`-based features (`useGingerEqualizer`,\n * `useGingerLiveAnalyzer`) on the same element. Using both simultaneously will\n * throw a `DOMException` when the second source node is requested.\n */\n\nexport type CrossfadeCurve = \"linear\" | \"equal-power\";\n\nexport type CrossfadeGraph = {\n context: AudioContext;\n outGain: GainNode;\n inGain: GainNode;\n outSource: MediaElementAudioSourceNode;\n inSource: MediaElementAudioSourceNode;\n};\n\nconst EQUAL_POWER_CURVE_LENGTH = 256;\n\nfunction buildEqualPowerCurves(): { outCurve: Float32Array; inCurve: Float32Array } {\n const outCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n const inCurve = new Float32Array(EQUAL_POWER_CURVE_LENGTH);\n for (let i = 0; i < EQUAL_POWER_CURVE_LENGTH; i++) {\n const t = i / (EQUAL_POWER_CURVE_LENGTH - 1);\n outCurve[i] = Math.cos(t * (Math.PI / 2));\n inCurve[i] = Math.sin(t * (Math.PI / 2));\n }\n return { outCurve, inCurve };\n}\n\nfunction getAudioContextCtor(): (new (options?: AudioContextOptions) => AudioContext) | undefined {\n if (typeof window === \"undefined\") return undefined;\n return (\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext\n );\n}\n\n/**\n * Creates a shared `AudioContext` and connects both the outgoing and incoming\n * audio elements to it via individual `GainNode`s.\n *\n * The outgoing gain starts at 1, the incoming gain starts at 0.\n * Call `scheduleCrossfade` immediately after to begin the ramps.\n *\n * @throws `DOMException` if either element already has a `MediaElementAudioSourceNode`\n * in another context (e.g. created by `liveAudioGraph`).\n * @throws `Error` if the Web Audio API is unavailable in this environment.\n */\nexport function attachCrossfadeGraph(\n outgoing: HTMLAudioElement,\n incoming: HTMLAudioElement,\n): CrossfadeGraph {\n const Ctor = getAudioContextCtor();\n if (!Ctor) {\n throw new Error(\n \"[@lucaismyname/ginger/crossfade] Web Audio API is not available in this environment.\",\n );\n }\n\n const context = new Ctor();\n\n const outSource = context.createMediaElementSource(outgoing);\n const inSource = context.createMediaElementSource(incoming);\n\n const outGain = context.createGain();\n const inGain = context.createGain();\n\n outGain.gain.value = 1;\n inGain.gain.value = 0;\n\n outSource.connect(outGain);\n outGain.connect(context.destination);\n\n inSource.connect(inGain);\n inGain.connect(context.destination);\n\n return { context, outGain, inGain, outSource, inSource };\n}\n\n/**\n * Schedules gain ramps on both gain nodes so that `outGain` fades from 1 → 0\n * and `inGain` fades from 0 → 1 over `durationSec` seconds starting immediately.\n *\n * For `\"equal-power\"`, a cosine/sine curve is applied via `setValueCurveAtTime`\n * to maintain consistent perceived loudness throughout the transition.\n */\nexport function scheduleCrossfade(\n graph: CrossfadeGraph,\n durationSec: number,\n curve: CrossfadeCurve,\n): void {\n const { context, outGain, inGain } = graph;\n const startTime = context.currentTime;\n const endTime = startTime + durationSec;\n\n if (curve === \"equal-power\") {\n const { outCurve, inCurve } = buildEqualPowerCurves();\n outGain.gain.setValueCurveAtTime(outCurve, startTime, durationSec);\n inGain.gain.setValueCurveAtTime(inCurve, startTime, durationSec);\n } else {\n outGain.gain.setValueAtTime(1, startTime);\n outGain.gain.linearRampToValueAtTime(0, endTime);\n inGain.gain.setValueAtTime(0, startTime);\n inGain.gain.linearRampToValueAtTime(1, endTime);\n }\n}\n\n/**\n * Disconnects all nodes and closes the `AudioContext`.\n * Safe to call multiple times; errors during disconnect are silently ignored.\n */\nexport function teardownCrossfadeGraph(graph: CrossfadeGraph): void {\n const nodes: AudioNode[] = [graph.outSource, graph.inSource, graph.outGain, graph.inGain];\n for (const node of nodes) {\n try {\n node.disconnect();\n } catch {\n // ignore\n }\n }\n void graph.context.close();\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeEndedTransition } from \"../core/transitions\";\nimport {\n type CrossfadeCurve,\n type CrossfadeGraph,\n attachCrossfadeGraph,\n scheduleCrossfade,\n teardownCrossfadeGraph,\n} from \"./crossfadeGraph\";\n\nexport type { CrossfadeCurve };\n\nexport type UseGingerCrossfadeOptions = {\n /**\n * Duration of the crossfade in seconds.\n * The hook begins the fade when `timeRemaining ≤ duration`.\n * @default 3\n */\n duration?: number;\n /**\n * Gain curve shape applied to both gain nodes.\n * `\"equal-power\"` uses a cosine/sine curve to maintain consistent perceived\n * loudness; `\"linear\"` is a straight ramp that may dip slightly at the midpoint.\n * @default \"equal-power\"\n */\n curve?: CrossfadeCurve;\n /**\n * `crossOrigin` attribute for the incoming `<audio>` element.\n * Match this to the `crossOrigin` prop on `Ginger.Player` when serving\n * cross-origin audio so the browser can reuse the cached resource.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\";\n /**\n * When `false`, crossfade is completely disabled and playback falls back to\n * the default hard-cut transition.\n * @default true\n */\n enabled?: boolean;\n};\n\nexport type UseGingerCrossfadeResult = {\n /** `true` while a crossfade transition is actively in progress. */\n isCrossfading: boolean;\n /**\n * Progress of the current crossfade from `0` (start) to `1` (complete).\n * Always `0` when idle.\n */\n crossfadeProgress: number;\n};\n\ntype CrossfadeSession = {\n graph: CrossfadeGraph;\n incomingAudio: HTMLAudioElement;\n startedAtIndex: number;\n startTime: number;\n fadeDurationMs: number;\n timeoutId: ReturnType<typeof setTimeout>;\n rafId: number;\n aborted: boolean;\n};\n\n/**\n * Smoothly crossfades between consecutive tracks using the Web Audio API.\n *\n * When the remaining time on the current track falls below `duration`, the hook:\n * 1. Creates a hidden `<audio>` element and begins loading the next track.\n * 2. Routes both the outgoing and incoming elements through `GainNode`s in a\n * shared `AudioContext`.\n * 3. Schedules gain ramps so the outgoing track fades out while the incoming\n * track fades in simultaneously.\n * 4. Dispatches `SET_INDEX` once the ramp completes so the Ginger queue\n * advances to the new track.\n *\n * **Limitations:**\n * - Incompatible with `useGingerEqualizer` and `useGingerLiveAnalyzer` on the\n * same element — the browser only permits one `MediaElementAudioSourceNode`\n * per `<audio>` element.\n * - Requires a prior user gesture before `AudioContext` can be resumed (standard\n * Web Audio policy).\n * - When `repeatMode` is `\"one\"`, the crossfade replays the same track from the\n * beginning, matching the standard `notifyEnded` behaviour.\n *\n * Available as a subpath import:\n * ```ts\n * import { useGingerCrossfade } from \"@lucaismyname/ginger/crossfade\";\n * ```\n */\nexport function useGingerCrossfade(\n options: UseGingerCrossfadeOptions = {},\n): UseGingerCrossfadeResult {\n const { duration = 3, curve = \"equal-power\", crossOrigin, enabled = true } = options;\n\n const { tracks, currentIndex, isPaused, repeatMode, playbackMode, dispatch } =\n useGingerPlayback();\n const { currentTime, duration: trackDuration, audioRef, muted, volume } = useGingerMedia();\n\n const [isCrossfading, setIsCrossfading] = useState(false);\n const [crossfadeProgress, setCrossfadeProgress] = useState(0);\n\n const sessionRef = useRef<CrossfadeSession | null>(null);\n\n const abort = useCallback(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.aborted = true;\n clearTimeout(session.timeoutId);\n cancelAnimationFrame(session.rafId);\n teardownCrossfadeGraph(session.graph);\n session.incomingAudio.pause();\n session.incomingAudio.removeAttribute(\"src\");\n session.incomingAudio.load();\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, []);\n\n // Abort if the user pauses or manually advances/changes the track mid-fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n if (isPaused || currentIndex !== session.startedAtIndex) {\n abort();\n }\n }, [isPaused, currentIndex, abort]);\n\n // Clean up on unmount.\n // biome-ignore lint/correctness/useExhaustiveDependencies: abort is stable; intentional unmount-only cleanup\n useEffect(() => () => abort(), []);\n\n // Keep the incoming element's volume/muted in sync with Ginger state so that\n // the user's volume control applies to the incoming track during the fade.\n useEffect(() => {\n const session = sessionRef.current;\n if (!session) return;\n session.incomingAudio.volume = volume;\n session.incomingAudio.muted = muted;\n }, [volume, muted]);\n\n // Main trigger: start a crossfade when time remaining ≤ duration.\n useEffect(() => {\n if (!enabled) return;\n if (sessionRef.current) return; // already in progress\n if (isPaused) return;\n if (!(trackDuration > 0)) return;\n\n const timeRemaining = trackDuration - currentTime;\n if (timeRemaining > duration || timeRemaining <= 0) return;\n\n // Determine what comes next using the same logic as notifyEnded.\n const transition = computeEndedTransition({ tracks, currentIndex, repeatMode, playbackMode });\n if (transition.kind === \"stop\") return; // queue ends — nothing to crossfade into\n\n const nextIndex = transition.kind === \"replay_same\" ? currentIndex : transition.nextIndex;\n const nextTrack = tracks[nextIndex];\n if (!nextTrack?.fileUrl) return;\n\n const mainEl = audioRef.current;\n if (!mainEl) return;\n\n // Create the incoming element before attaching the graph so that both\n // elements are ready when createMediaElementSource is called.\n const incomingAudio = document.createElement(\"audio\");\n incomingAudio.preload = \"auto\";\n incomingAudio.volume = volume;\n incomingAudio.muted = muted;\n if (crossOrigin) incomingAudio.crossOrigin = crossOrigin;\n incomingAudio.src = nextTrack.fileUrl;\n\n let graph: CrossfadeGraph;\n try {\n graph = attachCrossfadeGraph(mainEl, incomingAudio);\n } catch (e) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[@lucaismyname/ginger/crossfade] Failed to attach crossfade graph. \" +\n \"This may be because the audio element is already connected to a Web Audio graph \" +\n \"(e.g. via useGingerEqualizer or useGingerLiveAnalyzer). \" +\n \"These features are incompatible with useGingerCrossfade.\",\n e,\n );\n }\n return;\n }\n\n // Browsers suspend AudioContext until a user gesture has occurred.\n void graph.context.resume();\n\n incomingAudio.load();\n void incomingAudio.play().catch(() => {\n // Autoplay may be blocked; the gain ramps continue regardless and the\n // incoming audio will play once the browser permits it.\n });\n\n scheduleCrossfade(graph, timeRemaining, curve);\n\n const startTime = performance.now();\n const fadeDurationMs = timeRemaining * 1000;\n\n setIsCrossfading(true);\n setCrossfadeProgress(0);\n\n // rAF loop: drive crossfadeProgress for consumers (visualisers, UI indicators).\n let rafId = 0;\n const tick = () => {\n const elapsed = performance.now() - startTime;\n const progress = Math.min(1, elapsed / fadeDurationMs);\n setCrossfadeProgress(progress);\n if (progress < 1) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n\n // Once the gain ramps complete, advance the Ginger queue.\n const timeoutId = setTimeout(() => {\n const session = sessionRef.current;\n if (!session || session.aborted) return;\n\n // GingerPlayer will swap the src on the main element and resume playback.\n dispatch({ type: \"SET_INDEX\", payload: { index: nextIndex, autoPlay: true } });\n\n teardownCrossfadeGraph(graph);\n incomingAudio.pause();\n incomingAudio.removeAttribute(\"src\");\n incomingAudio.load();\n\n sessionRef.current = null;\n setIsCrossfading(false);\n setCrossfadeProgress(0);\n }, fadeDurationMs);\n\n sessionRef.current = {\n graph,\n incomingAudio,\n startedAtIndex: currentIndex,\n startTime,\n fadeDurationMs,\n timeoutId,\n rafId,\n aborted: false,\n };\n }, [\n enabled,\n isPaused,\n trackDuration,\n currentTime,\n duration,\n curve,\n crossOrigin,\n tracks,\n currentIndex,\n repeatMode,\n playbackMode,\n audioRef,\n volume,\n muted,\n dispatch,\n ]);\n\n return { isCrossfading, crossfadeProgress };\n}\n"],"names":["EQUAL_POWER_CURVE_LENGTH","buildEqualPowerCurves","outCurve","inCurve","i","t","getAudioContextCtor","attachCrossfadeGraph","outgoing","incoming","Ctor","context","outSource","inSource","outGain","inGain","scheduleCrossfade","graph","durationSec","curve","startTime","endTime","teardownCrossfadeGraph","nodes","node","useGingerCrossfade","options","duration","crossOrigin","enabled","tracks","currentIndex","isPaused","repeatMode","playbackMode","dispatch","useGingerPlayback","currentTime","trackDuration","audioRef","muted","volume","useGingerMedia","isCrossfading","setIsCrossfading","useState","crossfadeProgress","setCrossfadeProgress","sessionRef","useRef","abort","useCallback","session","useEffect","timeRemaining","transition","computeEndedTransition","nextIndex","nextTrack","mainEl","incomingAudio","e","fadeDurationMs","rafId","tick","elapsed","progress","timeoutId"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { CrossfadeCurve } from './crossfadeGraph';
2
+ export type { CrossfadeCurve };
3
+ export type UseGingerCrossfadeOptions = {
4
+ /**
5
+ * Duration of the crossfade in seconds.
6
+ * The hook begins the fade when `timeRemaining ≤ duration`.
7
+ * @default 3
8
+ */
9
+ duration?: number;
10
+ /**
11
+ * Gain curve shape applied to both gain nodes.
12
+ * `"equal-power"` uses a cosine/sine curve to maintain consistent perceived
13
+ * loudness; `"linear"` is a straight ramp that may dip slightly at the midpoint.
14
+ * @default "equal-power"
15
+ */
16
+ curve?: CrossfadeCurve;
17
+ /**
18
+ * `crossOrigin` attribute for the incoming `<audio>` element.
19
+ * Match this to the `crossOrigin` prop on `Ginger.Player` when serving
20
+ * cross-origin audio so the browser can reuse the cached resource.
21
+ */
22
+ crossOrigin?: "" | "anonymous" | "use-credentials";
23
+ /**
24
+ * When `false`, crossfade is completely disabled and playback falls back to
25
+ * the default hard-cut transition.
26
+ * @default true
27
+ */
28
+ enabled?: boolean;
29
+ };
30
+ export type UseGingerCrossfadeResult = {
31
+ /** `true` while a crossfade transition is actively in progress. */
32
+ isCrossfading: boolean;
33
+ /**
34
+ * Progress of the current crossfade from `0` (start) to `1` (complete).
35
+ * Always `0` when idle.
36
+ */
37
+ crossfadeProgress: number;
38
+ };
39
+ /**
40
+ * Smoothly crossfades between consecutive tracks using the Web Audio API.
41
+ *
42
+ * When the remaining time on the current track falls below `duration`, the hook:
43
+ * 1. Creates a hidden `<audio>` element and begins loading the next track.
44
+ * 2. Routes both the outgoing and incoming elements through `GainNode`s in a
45
+ * shared `AudioContext`.
46
+ * 3. Schedules gain ramps so the outgoing track fades out while the incoming
47
+ * track fades in simultaneously.
48
+ * 4. Dispatches `SET_INDEX` once the ramp completes so the Ginger queue
49
+ * advances to the new track.
50
+ *
51
+ * **Limitations:**
52
+ * - Incompatible with `useGingerEqualizer` and `useGingerLiveAnalyzer` on the
53
+ * same element — the browser only permits one `MediaElementAudioSourceNode`
54
+ * per `<audio>` element.
55
+ * - Requires a prior user gesture before `AudioContext` can be resumed (standard
56
+ * Web Audio policy).
57
+ * - When `repeatMode` is `"one"`, the crossfade replays the same track from the
58
+ * beginning, matching the standard `notifyEnded` behaviour.
59
+ *
60
+ * Available as a subpath import:
61
+ * ```ts
62
+ * import { useGingerCrossfade } from "@lucaismyname/ginger/crossfade";
63
+ * ```
64
+ */
65
+ export declare function useGingerCrossfade(options?: UseGingerCrossfadeOptions): UseGingerCrossfadeResult;
66
+ //# sourceMappingURL=useGingerCrossfade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGingerCrossfade.d.ts","sourceRoot":"","sources":["../../src/crossfade/useGingerCrossfade.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,GAAG,WAAW,GAAG,iBAAiB,CAAC;IACnD;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,mEAAmE;IACnE,aAAa,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAaF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,wBAAwB,CA2K1B"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react"),a=require("../liveAudioGraph-0cpHD_Ic.cjs"),G=require("../useGinger-BXgia32v.cjs"),R=[{frequency:60},{frequency:250},{frequency:1e3},{frequency:4e3},{frequency:16e3}];function b(p={}){const{enabled:y=!0,bands:h=R}=p,{audioRef:l,state:v}=G.useGinger(),[s,g]=t.useState(h),[S,q]=t.useState(null),c=t.useRef([]),m=t.useRef(s);m.current=s;const B=t.useMemo(()=>s.map(e=>`${e.frequency}:${e.type??"peaking"}:${e.q??1}`).join("|"),[s]);t.useEffect(()=>{const e=l.current;if(!(!e||typeof window>"u")){if(!y){a.setProcessingChain(e,[]),c.current=[];return}try{const n=a.attachLiveAnalyser(e,{fftSize:32,smoothingTimeConstant:0,minDecibels:-100,maxDecibels:0}),{context:r,id:f}=n,i=m.current.map(o=>{const u=r.createBiquadFilter();return u.type=o.type??"peaking",u.frequency.value=o.frequency,u.gain.value=o.gain??0,u.Q.value=o.q??1,u});c.current=i,a.setProcessingChain(e,i),a.detachLiveAnalyser(e,f),q(null)}catch(n){const r=n instanceof Error?n.message:"Failed to create equalizer";q(r),c.current=[]}return()=>{const n=l.current;n&&a.setProcessingChain(n,[]),c.current=[]}}},[y,B,l,v.currentIndex]);const C=t.useCallback((e,n)=>{const r=c.current[e];r&&(r.gain.value=n),g(f=>f.map((d,i)=>i===e?{...d,gain:n}:d))},[]),E=t.useCallback(e=>{g(e)},[]);return{setBandGain:C,setBands:E,bands:s,error:S}}exports.useGingerEqualizer=b;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react"),a=require("../liveAudioGraph-0cpHD_Ic.cjs"),G=require("../useGinger-4uvPoChz.cjs"),R=[{frequency:60},{frequency:250},{frequency:1e3},{frequency:4e3},{frequency:16e3}];function b(p={}){const{enabled:y=!0,bands:h=R}=p,{audioRef:l,state:v}=G.useGinger(),[s,g]=t.useState(h),[S,q]=t.useState(null),c=t.useRef([]),m=t.useRef(s);m.current=s;const B=t.useMemo(()=>s.map(e=>`${e.frequency}:${e.type??"peaking"}:${e.q??1}`).join("|"),[s]);t.useEffect(()=>{const e=l.current;if(!(!e||typeof window>"u")){if(!y){a.setProcessingChain(e,[]),c.current=[];return}try{const n=a.attachLiveAnalyser(e,{fftSize:32,smoothingTimeConstant:0,minDecibels:-100,maxDecibels:0}),{context:r,id:f}=n,i=m.current.map(o=>{const u=r.createBiquadFilter();return u.type=o.type??"peaking",u.frequency.value=o.frequency,u.gain.value=o.gain??0,u.Q.value=o.q??1,u});c.current=i,a.setProcessingChain(e,i),a.detachLiveAnalyser(e,f),q(null)}catch(n){const r=n instanceof Error?n.message:"Failed to create equalizer";q(r),c.current=[]}return()=>{const n=l.current;n&&a.setProcessingChain(n,[]),c.current=[]}}},[y,B,l,v.currentIndex]);const C=t.useCallback((e,n)=>{const r=c.current[e];r&&(r.gain.value=n),g(f=>f.map((d,i)=>i===e?{...d,gain:n}:d))},[]),E=t.useCallback(e=>{g(e)},[]);return{setBandGain:C,setBands:E,bands:s,error:S}}exports.useGingerEqualizer=b;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1,6 +1,6 @@
1
1
  import { useState as q, useRef as g, useMemo as R, useEffect as k, useCallback as h } from "react";
2
2
  import { s as l, a as x, d as z } from "../liveAudioGraph-DvPaxBCP.js";
3
- import { u as C } from "../useGinger-hpp2pAGY.js";
3
+ import { u as C } from "../useGinger-Dz0cPyD1.js";
4
4
  const D = [
5
5
  { frequency: 60 },
6
6
  { frequency: 250 },
@@ -1,7 +1,8 @@
1
1
  import { jsx as c, jsxs as w, Fragment as ir } from "react/jsx-runtime";
2
2
  import { useContext as $e, createContext as He, useRef as L, useState as on, useEffect as I, useMemo as A, useReducer as cn, useCallback as E, Children as sn, isValidElement as un, cloneElement as ln } from "react";
3
3
  import { b as D, u as X, g as or, c as P, G as dn, d as pn } from "./GingerSplitContexts-BzBExb95.js";
4
- import { b as Ye, g as K, r as gn, a as fn, p as cr, e as mn, d as sr, t as Be, f as yn, c as hn, s as ur, h as Ue, i as H, j as vn, k as Ze, m as kn, l as er, n as rr, o as bn, q as nr } from "./selectors-BalBCc7X.js";
4
+ import { b as Ye, g as K, r as gn, a as fn, p as cr, e as mn, d as sr } from "./selectors-BT3WSsKN.js";
5
+ import { t as Be, d as yn, b as hn, s as ur, f as Ue, e as H, g as vn, h as Ze, m as kn, i as er, j as rr, r as bn, c as nr } from "./transitions-CmNkf3sd.js";
5
6
  const lr = He(null);
6
7
  function Tn() {
7
8
  const e = $e(lr);
@@ -1964,7 +1965,7 @@ function Jn(e, r) {
1964
1965
  const t = [e, r].filter(Boolean).join(" ");
1965
1966
  return t === "" ? void 0 : t;
1966
1967
  }
1967
- const tt = {
1968
+ const at = {
1968
1969
  Provider: qn,
1969
1970
  Player: xn,
1970
1971
  Current: {
@@ -2028,7 +2029,7 @@ const tt = {
2028
2029
  };
2029
2030
  export {
2030
2031
  Gr as C,
2031
- tt as G,
2032
+ at as G,
2032
2033
  Nr as L,
2033
2034
  pr as P,
2034
2035
  fr as R,
@@ -2052,4 +2053,4 @@ export {
2052
2053
  Un as p,
2053
2054
  Bn as u
2054
2055
  };
2055
- //# sourceMappingURL=ginger-CgHqHrrG.js.map
2056
+ //# sourceMappingURL=ginger-B2DgE-2a.js.map