@djangocfg/ui-tools 2.1.310 → 2.1.313
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/README.md +38 -22
- package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
- package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
- package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
- package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
- package/dist/Player-M3GC3VPE.mjs +4 -0
- package/dist/Player-M3GC3VPE.mjs.map +1 -0
- package/dist/Player-ZGQKKOWI.css +65 -0
- package/dist/Player-ZGQKKOWI.css.map +1 -0
- package/dist/Player-ZL2X5LGG.cjs +13 -0
- package/dist/Player-ZL2X5LGG.cjs.map +1 -0
- package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
- package/dist/chunk-DFTVB66S.cjs.map +1 -0
- package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
- package/dist/chunk-EUADAUBQ.mjs.map +1 -0
- package/dist/chunk-FX2QFYWF.mjs +2059 -0
- package/dist/chunk-FX2QFYWF.mjs.map +1 -0
- package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
- package/dist/chunk-GBLQTHWT.mjs.map +1 -0
- package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
- package/dist/chunk-S44PW6NK.cjs.map +1 -0
- package/dist/chunk-ZLQHUZDU.cjs +2061 -0
- package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
- package/dist/components-WYEZL5TE.cjs +26 -0
- package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
- package/dist/components-ZAGG2PBO.mjs +5 -0
- package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
- package/dist/index.cjs +36 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +65 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +44 -500
- package/dist/index.d.ts +44 -500
- package/dist/index.mjs +16 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
- package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
- package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
- package/src/components/markdown/MarkdownMessage/README.md +72 -0
- package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
- package/src/components/markdown/MarkdownMessage/index.ts +6 -0
- package/src/index.ts +2 -11
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
- package/src/tools/AudioPlayer/Player.tsx +80 -0
- package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
- package/src/tools/AudioPlayer/README.md +139 -204
- package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
- package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
- package/src/tools/AudioPlayer/audio/index.ts +4 -0
- package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
- package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
- package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
- package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
- package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -15
- package/src/tools/AudioPlayer/context/selectors.ts +36 -0
- package/src/tools/AudioPlayer/hooks/index.ts +12 -39
- package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
- package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
- package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
- package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
- package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
- package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
- package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
- package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
- package/src/tools/AudioPlayer/index.ts +63 -134
- package/src/tools/AudioPlayer/lazy.tsx +8 -97
- package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
- package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
- package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
- package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
- package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
- package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
- package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
- package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
- package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
- package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
- package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
- package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
- package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
- package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
- package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
- package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
- package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
- package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
- package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
- package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
- package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
- package/src/tools/AudioPlayer/parts/index.ts +1 -0
- package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
- package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
- package/src/tools/AudioPlayer/store/index.ts +16 -0
- package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
- package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
- package/src/tools/AudioPlayer/types.ts +95 -0
- package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
- package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
- package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
- package/src/tools/AudioPlayer/utils/index.ts +4 -5
- package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
- package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
- package/src/tools/index.ts +5 -75
- package/dist/chunk-2QY3LJR6.mjs.map +0 -1
- package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
- package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
- package/dist/chunk-F2N7P5XU.cjs +0 -30
- package/dist/chunk-F2N7P5XU.cjs.map +0 -1
- package/dist/chunk-FYLR232K.cjs.map +0 -1
- package/dist/chunk-HMHIVEMS.mjs +0 -1619
- package/dist/chunk-HMHIVEMS.mjs.map +0 -1
- package/dist/chunk-JWB2EWQO.mjs +0 -5
- package/dist/chunk-JWB2EWQO.mjs.map +0 -1
- package/dist/chunk-YZX6FH3H.cjs +0 -1656
- package/dist/chunk-YZX6FH3H.cjs.map +0 -1
- package/dist/components-3RTH76CV.cjs +0 -27
- package/dist/components-5GVVL2Q6.mjs +0 -5
- package/dist/components-CPHOUQ5F.cjs +0 -46
- package/dist/components-CPHOUQ5F.cjs.map +0 -1
- package/dist/components-OTK43IMD.mjs +0 -6
- package/dist/components-OTK43IMD.mjs.map +0 -1
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
- package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
- package/src/tools/AudioPlayer/components/index.ts +0 -23
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
- package/src/tools/AudioPlayer/effects/index.ts +0 -412
- package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
- package/src/tools/AudioPlayer/types/effects.ts +0 -73
- package/src/tools/AudioPlayer/types/index.ts +0 -27
- package/src/tools/AudioPlayer/utils/debug.ts +0 -14
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useAudioBus — Global audio exclusivity via Zustand store.
|
|
5
|
-
*
|
|
6
|
-
* Ensures only one audio player plays at a time across the entire page.
|
|
7
|
-
* No provider needed — singleton store, works anywhere in the tree.
|
|
8
|
-
*
|
|
9
|
-
* Also integrates with @djangocfg/ui-tools AudioPlayer automatically
|
|
10
|
-
* (wired into useHybridAudio).
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* const { announce } = useAudioBus('my-player-id', () => pause());
|
|
14
|
-
*
|
|
15
|
-
* // When playback starts:
|
|
16
|
-
* announce(); // all other players will pause
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useEffect, useRef, useCallback } from 'react';
|
|
20
|
-
import { create } from 'zustand';
|
|
21
|
-
|
|
22
|
-
// =============================================================================
|
|
23
|
-
// Store
|
|
24
|
-
// =============================================================================
|
|
25
|
-
|
|
26
|
-
interface AudioBusStore {
|
|
27
|
-
/** ID of the currently active (playing) player, null if none */
|
|
28
|
-
activeId: string | null;
|
|
29
|
-
setActiveId: (id: string | null) => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const useAudioBusStore = create<AudioBusStore>((set) => ({
|
|
33
|
-
activeId: null,
|
|
34
|
-
setActiveId: (id) => set({ activeId: id }),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
// =============================================================================
|
|
38
|
-
// Hook
|
|
39
|
-
// =============================================================================
|
|
40
|
-
|
|
41
|
-
export interface UseAudioBusReturn {
|
|
42
|
-
/** Broadcast: this player is now playing — all others should stop */
|
|
43
|
-
announce: () => void;
|
|
44
|
-
/** Release: this player stopped (clears activeId if it was ours) */
|
|
45
|
-
release: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param playerId Stable unique ID for this player instance
|
|
50
|
-
* @param onStop Called when another player announces — should pause this player
|
|
51
|
-
*/
|
|
52
|
-
export function useAudioBus(playerId: string, onStop: () => void): UseAudioBusReturn {
|
|
53
|
-
const onStopRef = useRef(onStop);
|
|
54
|
-
onStopRef.current = onStop;
|
|
55
|
-
|
|
56
|
-
const setActiveId = useAudioBusStore((s) => s.setActiveId);
|
|
57
|
-
|
|
58
|
-
// Subscribe to store changes — if another player becomes active, stop this one
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
return useAudioBusStore.subscribe((state) => {
|
|
61
|
-
if (state.activeId !== null && state.activeId !== playerId) {
|
|
62
|
-
onStopRef.current();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
}, [playerId]);
|
|
66
|
-
|
|
67
|
-
const announce = useCallback(() => {
|
|
68
|
-
setActiveId(playerId);
|
|
69
|
-
}, [playerId, setActiveId]);
|
|
70
|
-
|
|
71
|
-
const release = useCallback(() => {
|
|
72
|
-
useAudioBusStore.setState((s) => s.activeId === playerId ? { activeId: null } : s);
|
|
73
|
-
}, [playerId]);
|
|
74
|
-
|
|
75
|
-
return { announce, release };
|
|
76
|
-
}
|
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useHybridAudio - Hybrid audio hook combining HTML5 audio + Web Audio API
|
|
5
|
-
*
|
|
6
|
-
* Uses native HTML5 <audio> for playback (no crackling, native streaming)
|
|
7
|
-
* and Web Audio API only for visualization (AnalyserNode).
|
|
8
|
-
*
|
|
9
|
-
* Audio routing:
|
|
10
|
-
* source -> destination (single path for playback)
|
|
11
|
-
* source -> analyser (parallel path for visualization only, no output)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
15
|
-
import { useAudioBus } from './useAudioBus';
|
|
16
|
-
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// TYPES
|
|
19
|
-
// =============================================================================
|
|
20
|
-
|
|
21
|
-
export interface UseHybridAudioOptions {
|
|
22
|
-
src: string;
|
|
23
|
-
autoPlay?: boolean;
|
|
24
|
-
initialVolume?: number;
|
|
25
|
-
loop?: boolean;
|
|
26
|
-
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
27
|
-
/** Set to true to opt out of global audio bus (player won't stop others and won't be stopped) */
|
|
28
|
-
excludeFromBus?: boolean;
|
|
29
|
-
onPlay?: () => void;
|
|
30
|
-
onPause?: () => void;
|
|
31
|
-
onEnded?: () => void;
|
|
32
|
-
onTimeUpdate?: (time: number) => void;
|
|
33
|
-
onError?: (error: Error) => void;
|
|
34
|
-
onReady?: () => void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface HybridAudioState {
|
|
38
|
-
isReady: boolean;
|
|
39
|
-
isPlaying: boolean;
|
|
40
|
-
currentTime: number;
|
|
41
|
-
duration: number;
|
|
42
|
-
volume: number;
|
|
43
|
-
isMuted: boolean;
|
|
44
|
-
isLooping: boolean;
|
|
45
|
-
buffered: TimeRanges | null;
|
|
46
|
-
error: Error | null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface HybridAudioControls {
|
|
50
|
-
play: () => Promise<void>;
|
|
51
|
-
pause: () => void;
|
|
52
|
-
togglePlay: () => void;
|
|
53
|
-
seek: (time: number) => void;
|
|
54
|
-
seekTo: (progress: number) => void;
|
|
55
|
-
skip: (seconds: number) => void;
|
|
56
|
-
setVolume: (vol: number) => void;
|
|
57
|
-
toggleMute: () => void;
|
|
58
|
-
toggleLoop: () => void;
|
|
59
|
-
setLoop: (enabled: boolean) => void;
|
|
60
|
-
restart: () => void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface HybridWebAudioAPI {
|
|
64
|
-
context: AudioContext | null;
|
|
65
|
-
analyser: AnalyserNode | null;
|
|
66
|
-
sourceNode: MediaElementAudioSourceNode | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface UseHybridAudioReturn {
|
|
70
|
-
audioRef: React.RefObject<HTMLAudioElement | null>;
|
|
71
|
-
state: HybridAudioState;
|
|
72
|
-
controls: HybridAudioControls;
|
|
73
|
-
webAudio: HybridWebAudioAPI;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// =============================================================================
|
|
77
|
-
// HOOK
|
|
78
|
-
// =============================================================================
|
|
79
|
-
|
|
80
|
-
export function useHybridAudio(options: UseHybridAudioOptions): UseHybridAudioReturn {
|
|
81
|
-
const {
|
|
82
|
-
src,
|
|
83
|
-
autoPlay = false,
|
|
84
|
-
initialVolume = 1,
|
|
85
|
-
loop = false,
|
|
86
|
-
crossOrigin = 'anonymous',
|
|
87
|
-
excludeFromBus = false,
|
|
88
|
-
onPlay,
|
|
89
|
-
onPause,
|
|
90
|
-
onEnded,
|
|
91
|
-
onTimeUpdate,
|
|
92
|
-
onError,
|
|
93
|
-
onReady,
|
|
94
|
-
} = options;
|
|
95
|
-
|
|
96
|
-
const playerId = useId();
|
|
97
|
-
|
|
98
|
-
// Refs
|
|
99
|
-
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
100
|
-
const audioContextRef = useRef<AudioContext | null>(null);
|
|
101
|
-
const sourceNodeRef = useRef<MediaElementAudioSourceNode | null>(null);
|
|
102
|
-
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
103
|
-
const connectedElementRef = useRef<HTMLMediaElement | null>(null);
|
|
104
|
-
|
|
105
|
-
// State
|
|
106
|
-
const [state, setState] = useState<HybridAudioState>({
|
|
107
|
-
isReady: false,
|
|
108
|
-
isPlaying: false,
|
|
109
|
-
currentTime: 0,
|
|
110
|
-
duration: 0,
|
|
111
|
-
volume: initialVolume,
|
|
112
|
-
isMuted: false,
|
|
113
|
-
isLooping: loop,
|
|
114
|
-
buffered: null,
|
|
115
|
-
error: null,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Initialize Web Audio for visualization (lazy, on first play)
|
|
119
|
-
const initWebAudio = useCallback(() => {
|
|
120
|
-
const audio = audioRef.current;
|
|
121
|
-
if (!audio) return;
|
|
122
|
-
|
|
123
|
-
// Already connected to this element
|
|
124
|
-
if (connectedElementRef.current === audio && audioContextRef.current) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
// Create AudioContext if needed
|
|
130
|
-
if (!audioContextRef.current) {
|
|
131
|
-
const AudioContextClass =
|
|
132
|
-
window.AudioContext ||
|
|
133
|
-
(window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
|
134
|
-
audioContextRef.current = new AudioContextClass();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const ctx = audioContextRef.current;
|
|
138
|
-
|
|
139
|
-
// Disconnect old source if any
|
|
140
|
-
if (sourceNodeRef.current) {
|
|
141
|
-
try {
|
|
142
|
-
sourceNodeRef.current.disconnect();
|
|
143
|
-
} catch {
|
|
144
|
-
/* ignore */
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Create source from audio element
|
|
149
|
-
const source = ctx.createMediaElementSource(audio);
|
|
150
|
-
sourceNodeRef.current = source;
|
|
151
|
-
|
|
152
|
-
// Create analyser for visualization
|
|
153
|
-
const analyser = ctx.createAnalyser();
|
|
154
|
-
analyser.fftSize = 256;
|
|
155
|
-
analyser.smoothingTimeConstant = 0.85;
|
|
156
|
-
analyserRef.current = analyser;
|
|
157
|
-
|
|
158
|
-
// Audio routing:
|
|
159
|
-
// 1. source -> destination (for playback)
|
|
160
|
-
// 2. source -> analyser (for visualization only - NO output!)
|
|
161
|
-
source.connect(ctx.destination);
|
|
162
|
-
source.connect(analyser);
|
|
163
|
-
// NOTE: analyser does NOT connect to destination - prevents double audio!
|
|
164
|
-
|
|
165
|
-
connectedElementRef.current = audio;
|
|
166
|
-
} catch (error) {
|
|
167
|
-
console.warn('[useHybridAudio] Web Audio init failed:', error);
|
|
168
|
-
}
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
// Audio bus — stop other players when this one plays
|
|
172
|
-
const { announce: busAnnounce, release: busRelease } = useAudioBus(
|
|
173
|
-
playerId,
|
|
174
|
-
useCallback(() => {
|
|
175
|
-
if (!excludeFromBus) audioRef.current?.pause();
|
|
176
|
-
}, [excludeFromBus])
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// Resume AudioContext on user interaction
|
|
180
|
-
const resumeAudioContext = useCallback(async () => {
|
|
181
|
-
const ctx = audioContextRef.current;
|
|
182
|
-
if (ctx && ctx.state === 'suspended') {
|
|
183
|
-
await ctx.resume();
|
|
184
|
-
}
|
|
185
|
-
}, []);
|
|
186
|
-
|
|
187
|
-
// Controls
|
|
188
|
-
const play = useCallback(async () => {
|
|
189
|
-
const audio = audioRef.current;
|
|
190
|
-
if (!audio) return;
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
if (!excludeFromBus) busAnnounce();
|
|
194
|
-
initWebAudio();
|
|
195
|
-
await resumeAudioContext();
|
|
196
|
-
await audio.play();
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error('[useHybridAudio] Play failed:', error);
|
|
199
|
-
onError?.(error as Error);
|
|
200
|
-
}
|
|
201
|
-
}, [excludeFromBus, busAnnounce, initWebAudio, resumeAudioContext, onError]);
|
|
202
|
-
|
|
203
|
-
const pause = useCallback(() => {
|
|
204
|
-
audioRef.current?.pause();
|
|
205
|
-
}, []);
|
|
206
|
-
|
|
207
|
-
const togglePlay = useCallback(() => {
|
|
208
|
-
if (state.isPlaying) {
|
|
209
|
-
pause();
|
|
210
|
-
} else {
|
|
211
|
-
play();
|
|
212
|
-
}
|
|
213
|
-
}, [state.isPlaying, play, pause]);
|
|
214
|
-
|
|
215
|
-
const seek = useCallback(
|
|
216
|
-
(time: number) => {
|
|
217
|
-
const audio = audioRef.current;
|
|
218
|
-
if (audio && isFinite(time)) {
|
|
219
|
-
audio.currentTime = Math.max(0, Math.min(time, state.duration || audio.duration || 0));
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
[state.duration]
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
const seekTo = useCallback(
|
|
226
|
-
(progress: number) => {
|
|
227
|
-
const duration = state.duration || audioRef.current?.duration || 0;
|
|
228
|
-
seek(duration * Math.max(0, Math.min(1, progress)));
|
|
229
|
-
},
|
|
230
|
-
[state.duration, seek]
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
const skip = useCallback(
|
|
234
|
-
(seconds: number) => {
|
|
235
|
-
seek(state.currentTime + seconds);
|
|
236
|
-
},
|
|
237
|
-
[state.currentTime, seek]
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
const setVolume = useCallback((vol: number) => {
|
|
241
|
-
const audio = audioRef.current;
|
|
242
|
-
if (audio) {
|
|
243
|
-
const clampedVol = Math.max(0, Math.min(1, vol));
|
|
244
|
-
audio.volume = clampedVol;
|
|
245
|
-
setState((prev) => ({ ...prev, volume: clampedVol }));
|
|
246
|
-
}
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
const toggleMute = useCallback(() => {
|
|
250
|
-
const audio = audioRef.current;
|
|
251
|
-
if (audio) {
|
|
252
|
-
audio.muted = !audio.muted;
|
|
253
|
-
setState((prev) => ({ ...prev, isMuted: audio.muted }));
|
|
254
|
-
}
|
|
255
|
-
}, []);
|
|
256
|
-
|
|
257
|
-
const toggleLoop = useCallback(() => {
|
|
258
|
-
const audio = audioRef.current;
|
|
259
|
-
if (audio) {
|
|
260
|
-
audio.loop = !audio.loop;
|
|
261
|
-
setState((prev) => ({ ...prev, isLooping: audio.loop }));
|
|
262
|
-
}
|
|
263
|
-
}, []);
|
|
264
|
-
|
|
265
|
-
const setLoop = useCallback((enabled: boolean) => {
|
|
266
|
-
const audio = audioRef.current;
|
|
267
|
-
if (audio) {
|
|
268
|
-
audio.loop = enabled;
|
|
269
|
-
setState((prev) => ({ ...prev, isLooping: enabled }));
|
|
270
|
-
}
|
|
271
|
-
}, []);
|
|
272
|
-
|
|
273
|
-
const restart = useCallback(() => {
|
|
274
|
-
seek(0);
|
|
275
|
-
play();
|
|
276
|
-
}, [seek, play]);
|
|
277
|
-
|
|
278
|
-
const controls: HybridAudioControls = {
|
|
279
|
-
play,
|
|
280
|
-
pause,
|
|
281
|
-
togglePlay,
|
|
282
|
-
seek,
|
|
283
|
-
seekTo,
|
|
284
|
-
skip,
|
|
285
|
-
setVolume,
|
|
286
|
-
toggleMute,
|
|
287
|
-
toggleLoop,
|
|
288
|
-
setLoop,
|
|
289
|
-
restart,
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// Create audio element on mount
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
const audio = document.createElement('audio');
|
|
295
|
-
audio.preload = 'metadata';
|
|
296
|
-
audio.crossOrigin = crossOrigin;
|
|
297
|
-
audio.volume = initialVolume;
|
|
298
|
-
audio.loop = loop;
|
|
299
|
-
audioRef.current = audio;
|
|
300
|
-
|
|
301
|
-
return () => {
|
|
302
|
-
audio.pause();
|
|
303
|
-
audio.src = '';
|
|
304
|
-
busRelease();
|
|
305
|
-
if (audioContextRef.current) {
|
|
306
|
-
audioContextRef.current.close().catch(() => {});
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
310
|
-
}, []);
|
|
311
|
-
|
|
312
|
-
// Event handlers
|
|
313
|
-
useEffect(() => {
|
|
314
|
-
const audio = audioRef.current;
|
|
315
|
-
if (!audio) return;
|
|
316
|
-
|
|
317
|
-
const handlers = {
|
|
318
|
-
loadedmetadata: () => {
|
|
319
|
-
setState((prev) => ({
|
|
320
|
-
...prev,
|
|
321
|
-
duration: audio.duration,
|
|
322
|
-
isReady: true,
|
|
323
|
-
}));
|
|
324
|
-
onReady?.();
|
|
325
|
-
},
|
|
326
|
-
canplay: () => {
|
|
327
|
-
setState((prev) => ({ ...prev, isReady: true }));
|
|
328
|
-
if (autoPlay) {
|
|
329
|
-
play();
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
play: () => {
|
|
333
|
-
setState((prev) => ({ ...prev, isPlaying: true }));
|
|
334
|
-
onPlay?.();
|
|
335
|
-
},
|
|
336
|
-
pause: () => {
|
|
337
|
-
setState((prev) => ({ ...prev, isPlaying: false }));
|
|
338
|
-
onPause?.();
|
|
339
|
-
},
|
|
340
|
-
ended: () => {
|
|
341
|
-
setState((prev) => ({ ...prev, isPlaying: false }));
|
|
342
|
-
onEnded?.();
|
|
343
|
-
},
|
|
344
|
-
timeupdate: () => {
|
|
345
|
-
setState((prev) => ({ ...prev, currentTime: audio.currentTime }));
|
|
346
|
-
onTimeUpdate?.(audio.currentTime);
|
|
347
|
-
},
|
|
348
|
-
progress: () => {
|
|
349
|
-
setState((prev) => ({ ...prev, buffered: audio.buffered }));
|
|
350
|
-
},
|
|
351
|
-
error: () => {
|
|
352
|
-
const error = new Error(audio.error?.message || 'Audio error');
|
|
353
|
-
setState((prev) => ({ ...prev, error }));
|
|
354
|
-
onError?.(error);
|
|
355
|
-
},
|
|
356
|
-
volumechange: () => {
|
|
357
|
-
setState((prev) => ({
|
|
358
|
-
...prev,
|
|
359
|
-
volume: audio.volume,
|
|
360
|
-
isMuted: audio.muted,
|
|
361
|
-
}));
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
Object.entries(handlers).forEach(([event, handler]) => {
|
|
366
|
-
audio.addEventListener(event, handler);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
return () => {
|
|
370
|
-
Object.entries(handlers).forEach(([event, handler]) => {
|
|
371
|
-
audio.removeEventListener(event, handler);
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
}, [autoPlay, onPlay, onPause, onEnded, onTimeUpdate, onError, onReady, play]);
|
|
375
|
-
|
|
376
|
-
// Load new source
|
|
377
|
-
useEffect(() => {
|
|
378
|
-
const audio = audioRef.current;
|
|
379
|
-
if (!audio || !src) return;
|
|
380
|
-
|
|
381
|
-
setState((prev) => ({
|
|
382
|
-
...prev,
|
|
383
|
-
isReady: false,
|
|
384
|
-
currentTime: 0,
|
|
385
|
-
duration: 0,
|
|
386
|
-
error: null,
|
|
387
|
-
}));
|
|
388
|
-
|
|
389
|
-
audio.src = src;
|
|
390
|
-
audio.load();
|
|
391
|
-
}, [src]);
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
audioRef,
|
|
395
|
-
state,
|
|
396
|
-
controls,
|
|
397
|
-
webAudio: {
|
|
398
|
-
context: audioContextRef.current,
|
|
399
|
-
analyser: analyserRef.current,
|
|
400
|
-
sourceNode: sourceNodeRef.current,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useHybridAudioAnalysis - Audio frequency analysis for hybrid player.
|
|
5
|
-
*
|
|
6
|
-
* Simplified version of useAudioAnalysis that works directly with AnalyserNode
|
|
7
|
-
* instead of SharedWebAudioContext.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
-
|
|
12
|
-
import type { AudioLevels } from '../effects';
|
|
13
|
-
|
|
14
|
-
export function useHybridAudioAnalysis(
|
|
15
|
-
analyser: AnalyserNode | null,
|
|
16
|
-
isPlaying: boolean
|
|
17
|
-
): AudioLevels {
|
|
18
|
-
const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
|
|
19
|
-
const animationRef = useRef<number | null>(null);
|
|
20
|
-
const dataArrayRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
|
|
21
|
-
|
|
22
|
-
const cleanup = useCallback(() => {
|
|
23
|
-
if (animationRef.current) {
|
|
24
|
-
cancelAnimationFrame(animationRef.current);
|
|
25
|
-
animationRef.current = null;
|
|
26
|
-
}
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
// Initialize data array when analyser is available
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (analyser && !dataArrayRef.current) {
|
|
32
|
-
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
|
|
33
|
-
}
|
|
34
|
-
}, [analyser]);
|
|
35
|
-
|
|
36
|
-
// Animation loop
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!isPlaying || !analyser || !dataArrayRef.current) {
|
|
39
|
-
cleanup();
|
|
40
|
-
// Smooth fade out when stopped
|
|
41
|
-
setLevels((prev) => ({
|
|
42
|
-
bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
|
|
43
|
-
mid: prev.mid * 0.95 < 0.01 ? 0 : prev.mid * 0.95,
|
|
44
|
-
high: prev.high * 0.95 < 0.01 ? 0 : prev.high * 0.95,
|
|
45
|
-
overall: prev.overall * 0.95 < 0.01 ? 0 : prev.overall * 0.95,
|
|
46
|
-
}));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const dataArray = dataArrayRef.current;
|
|
51
|
-
|
|
52
|
-
const animate = () => {
|
|
53
|
-
analyser.getByteFrequencyData(dataArray);
|
|
54
|
-
const binCount = dataArray.length;
|
|
55
|
-
|
|
56
|
-
// Bass (0-15% of frequency range)
|
|
57
|
-
const bassEnd = Math.floor(binCount * 0.15);
|
|
58
|
-
let bassSum = 0;
|
|
59
|
-
for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
|
|
60
|
-
const bass = bassSum / bassEnd / 255;
|
|
61
|
-
|
|
62
|
-
// Mids (15-50% of frequency range)
|
|
63
|
-
const midStart = bassEnd;
|
|
64
|
-
const midEnd = Math.floor(binCount * 0.5);
|
|
65
|
-
let midSum = 0;
|
|
66
|
-
for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
|
|
67
|
-
const mid = midSum / (midEnd - midStart) / 255;
|
|
68
|
-
|
|
69
|
-
// Highs (50-100% of frequency range)
|
|
70
|
-
const highStart = midEnd;
|
|
71
|
-
let highSum = 0;
|
|
72
|
-
for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
|
|
73
|
-
const high = highSum / (binCount - highStart) / 255;
|
|
74
|
-
|
|
75
|
-
// Overall
|
|
76
|
-
let totalSum = 0;
|
|
77
|
-
for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
|
|
78
|
-
const overall = totalSum / binCount / 255;
|
|
79
|
-
|
|
80
|
-
// Smooth with lerp for natural feel
|
|
81
|
-
setLevels((prev) => ({
|
|
82
|
-
bass: prev.bass * 0.7 + bass * 0.3,
|
|
83
|
-
mid: prev.mid * 0.7 + mid * 0.3,
|
|
84
|
-
high: prev.high * 0.7 + high * 0.3,
|
|
85
|
-
overall: prev.overall * 0.7 + overall * 0.3,
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
animationRef.current = requestAnimationFrame(animate);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
animationRef.current = requestAnimationFrame(animate);
|
|
92
|
-
return cleanup;
|
|
93
|
-
}, [analyser, isPlaying, cleanup]);
|
|
94
|
-
|
|
95
|
-
return levels;
|
|
96
|
-
}
|