@djangocfg/ui-nextjs 2.1.82 → 2.1.83
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/package.json +4 -4
- package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
- package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
- package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
- package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
- package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
- package/src/tools/AudioPlayer/README.md +177 -205
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
- package/src/tools/AudioPlayer/components/index.ts +6 -1
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -3
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
- package/src/tools/AudioPlayer/context/index.ts +14 -2
- package/src/tools/AudioPlayer/hooks/index.ts +11 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
- package/src/tools/AudioPlayer/index.ts +31 -0
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
- package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
- package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridWaveform - Real-time frequency visualization for hybrid player.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* - 'frequency': Real-time frequency bars (default, requires playing audio)
|
|
8
|
+
* - 'static': Static progress bar (for when no analyser is available)
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Shows buffered regions
|
|
12
|
+
* - Click to seek
|
|
13
|
+
* - Responsive width
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useRef, useEffect, useCallback, memo } from 'react';
|
|
17
|
+
import { useHybridAudioContext } from '../context/HybridAudioProvider';
|
|
18
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export interface HybridWaveformProps {
|
|
25
|
+
/** Visualization mode */
|
|
26
|
+
mode?: 'frequency' | 'static';
|
|
27
|
+
/** Canvas height in pixels */
|
|
28
|
+
height?: number;
|
|
29
|
+
/** Bar width in pixels */
|
|
30
|
+
barWidth?: number;
|
|
31
|
+
/** Gap between bars in pixels */
|
|
32
|
+
barGap?: number;
|
|
33
|
+
/** Bar border radius */
|
|
34
|
+
barRadius?: number;
|
|
35
|
+
/** Color for played portion */
|
|
36
|
+
progressColor?: string;
|
|
37
|
+
/** Color for unplayed portion */
|
|
38
|
+
waveColor?: string;
|
|
39
|
+
/** Color for buffered regions indicator */
|
|
40
|
+
bufferedColor?: string;
|
|
41
|
+
/** Additional CSS class */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Callback when user seeks */
|
|
44
|
+
onSeek?: (time: number) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// COMPONENT
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export const HybridWaveform = memo(function HybridWaveform({
|
|
52
|
+
mode = 'frequency',
|
|
53
|
+
height = 64,
|
|
54
|
+
barWidth = 3,
|
|
55
|
+
barGap = 2,
|
|
56
|
+
barRadius = 2,
|
|
57
|
+
progressColor = 'hsl(217 91% 60%)',
|
|
58
|
+
waveColor = 'hsl(217 91% 60% / 0.3)',
|
|
59
|
+
bufferedColor = 'hsl(217 91% 60% / 0.15)',
|
|
60
|
+
className,
|
|
61
|
+
onSeek,
|
|
62
|
+
}: HybridWaveformProps) {
|
|
63
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const animationRef = useRef<number | null>(null);
|
|
66
|
+
const { state, controls, webAudio } = useHybridAudioContext();
|
|
67
|
+
|
|
68
|
+
// Handle click to seek
|
|
69
|
+
const handleClick = useCallback(
|
|
70
|
+
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
71
|
+
const canvas = canvasRef.current;
|
|
72
|
+
if (!canvas || !state.duration) return;
|
|
73
|
+
|
|
74
|
+
const rect = canvas.getBoundingClientRect();
|
|
75
|
+
const x = e.clientX - rect.left;
|
|
76
|
+
const progress = x / rect.width;
|
|
77
|
+
const time = state.duration * progress;
|
|
78
|
+
|
|
79
|
+
controls.seek(time);
|
|
80
|
+
onSeek?.(time);
|
|
81
|
+
},
|
|
82
|
+
[state.duration, controls, onSeek]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Render frequency bars (real-time visualization)
|
|
86
|
+
const renderFrequency = useCallback(() => {
|
|
87
|
+
const canvas = canvasRef.current;
|
|
88
|
+
const analyser = webAudio.analyser;
|
|
89
|
+
if (!canvas) return;
|
|
90
|
+
|
|
91
|
+
const ctx = canvas.getContext('2d');
|
|
92
|
+
if (!ctx) return;
|
|
93
|
+
|
|
94
|
+
const { width, height: canvasHeight } = canvas;
|
|
95
|
+
const dpr = window.devicePixelRatio || 1;
|
|
96
|
+
const displayWidth = width / dpr;
|
|
97
|
+
|
|
98
|
+
// Get frequency data if analyser is available
|
|
99
|
+
let dataArray: Uint8Array<ArrayBuffer> | null = null;
|
|
100
|
+
if (analyser) {
|
|
101
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
|
|
102
|
+
analyser.getByteFrequencyData(dataArray);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ctx.clearRect(0, 0, width, canvasHeight);
|
|
106
|
+
|
|
107
|
+
// Draw buffered regions at bottom
|
|
108
|
+
if (state.buffered && state.duration > 0) {
|
|
109
|
+
ctx.fillStyle = bufferedColor;
|
|
110
|
+
for (let i = 0; i < state.buffered.length; i++) {
|
|
111
|
+
const start = (state.buffered.start(i) / state.duration) * width;
|
|
112
|
+
const end = (state.buffered.end(i) / state.duration) * width;
|
|
113
|
+
ctx.fillRect(start, canvasHeight - 3 * dpr, end - start, 3 * dpr);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate bar count based on available width
|
|
118
|
+
const barCount = Math.floor(displayWidth / (barWidth + barGap));
|
|
119
|
+
const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
|
|
120
|
+
const progressX = width * progress;
|
|
121
|
+
|
|
122
|
+
// Draw bars
|
|
123
|
+
for (let i = 0; i < barCount; i++) {
|
|
124
|
+
let barHeight: number;
|
|
125
|
+
|
|
126
|
+
if (dataArray && state.isPlaying) {
|
|
127
|
+
// Real-time frequency data
|
|
128
|
+
const step = Math.floor(dataArray.length / barCount);
|
|
129
|
+
const value = dataArray[i * step] / 255;
|
|
130
|
+
barHeight = Math.max(4 * dpr, value * (canvasHeight - 6 * dpr) * 0.9);
|
|
131
|
+
} else {
|
|
132
|
+
// Static fallback - small bars
|
|
133
|
+
barHeight = 8 * dpr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const x = i * (barWidth + barGap) * dpr;
|
|
137
|
+
const y = (canvasHeight - barHeight) / 2;
|
|
138
|
+
|
|
139
|
+
ctx.fillStyle = x < progressX ? progressColor : waveColor;
|
|
140
|
+
|
|
141
|
+
// Draw rounded rect
|
|
142
|
+
const radius = barRadius * dpr;
|
|
143
|
+
const rectWidth = barWidth * dpr;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.roundRect(x, y, rectWidth, barHeight, radius);
|
|
146
|
+
ctx.fill();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Continue animation if playing
|
|
150
|
+
if (state.isPlaying) {
|
|
151
|
+
animationRef.current = requestAnimationFrame(renderFrequency);
|
|
152
|
+
}
|
|
153
|
+
}, [
|
|
154
|
+
webAudio.analyser,
|
|
155
|
+
state.buffered,
|
|
156
|
+
state.duration,
|
|
157
|
+
state.currentTime,
|
|
158
|
+
state.isPlaying,
|
|
159
|
+
barWidth,
|
|
160
|
+
barGap,
|
|
161
|
+
barRadius,
|
|
162
|
+
progressColor,
|
|
163
|
+
waveColor,
|
|
164
|
+
bufferedColor,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
// Render static progress bar
|
|
168
|
+
const renderStatic = useCallback(() => {
|
|
169
|
+
const canvas = canvasRef.current;
|
|
170
|
+
if (!canvas) return;
|
|
171
|
+
|
|
172
|
+
const ctx = canvas.getContext('2d');
|
|
173
|
+
if (!ctx) return;
|
|
174
|
+
|
|
175
|
+
const { width, height: canvasHeight } = canvas;
|
|
176
|
+
const dpr = window.devicePixelRatio || 1;
|
|
177
|
+
|
|
178
|
+
ctx.clearRect(0, 0, width, canvasHeight);
|
|
179
|
+
|
|
180
|
+
// Draw buffered regions
|
|
181
|
+
if (state.buffered && state.duration > 0) {
|
|
182
|
+
ctx.fillStyle = bufferedColor;
|
|
183
|
+
for (let i = 0; i < state.buffered.length; i++) {
|
|
184
|
+
const start = (state.buffered.start(i) / state.duration) * width;
|
|
185
|
+
const end = (state.buffered.end(i) / state.duration) * width;
|
|
186
|
+
ctx.fillRect(start, 0, end - start, canvasHeight);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Draw progress bar
|
|
191
|
+
const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
|
|
192
|
+
const progressWidth = width * progress;
|
|
193
|
+
|
|
194
|
+
// Background
|
|
195
|
+
ctx.fillStyle = waveColor;
|
|
196
|
+
ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, width, 4 * dpr);
|
|
197
|
+
|
|
198
|
+
// Progress
|
|
199
|
+
ctx.fillStyle = progressColor;
|
|
200
|
+
ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, progressWidth, 4 * dpr);
|
|
201
|
+
|
|
202
|
+
// Handle
|
|
203
|
+
if (progress > 0) {
|
|
204
|
+
ctx.beginPath();
|
|
205
|
+
ctx.arc(progressWidth, canvasHeight / 2, 6 * dpr, 0, Math.PI * 2);
|
|
206
|
+
ctx.fill();
|
|
207
|
+
}
|
|
208
|
+
}, [state.buffered, state.duration, state.currentTime, progressColor, waveColor, bufferedColor]);
|
|
209
|
+
|
|
210
|
+
// Resize canvas to match container
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const container = containerRef.current;
|
|
213
|
+
const canvas = canvasRef.current;
|
|
214
|
+
if (!container || !canvas) return;
|
|
215
|
+
|
|
216
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
217
|
+
const entry = entries[0];
|
|
218
|
+
if (!entry) return;
|
|
219
|
+
|
|
220
|
+
const dpr = window.devicePixelRatio || 1;
|
|
221
|
+
const displayWidth = entry.contentRect.width;
|
|
222
|
+
const displayHeight = height;
|
|
223
|
+
|
|
224
|
+
canvas.width = displayWidth * dpr;
|
|
225
|
+
canvas.height = displayHeight * dpr;
|
|
226
|
+
canvas.style.width = `${displayWidth}px`;
|
|
227
|
+
canvas.style.height = `${displayHeight}px`;
|
|
228
|
+
|
|
229
|
+
// Re-render after resize
|
|
230
|
+
if (mode === 'frequency') {
|
|
231
|
+
renderFrequency();
|
|
232
|
+
} else {
|
|
233
|
+
renderStatic();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
resizeObserver.observe(container);
|
|
238
|
+
return () => resizeObserver.disconnect();
|
|
239
|
+
}, [height, mode, renderFrequency, renderStatic]);
|
|
240
|
+
|
|
241
|
+
// Animation loop for frequency mode
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (mode === 'frequency') {
|
|
244
|
+
renderFrequency();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
if (animationRef.current) {
|
|
249
|
+
cancelAnimationFrame(animationRef.current);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}, [mode, renderFrequency]);
|
|
253
|
+
|
|
254
|
+
// Re-render on time change for static mode
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (mode === 'static') {
|
|
257
|
+
renderStatic();
|
|
258
|
+
}
|
|
259
|
+
}, [mode, state.currentTime, renderStatic]);
|
|
260
|
+
|
|
261
|
+
// Re-render frequency when playback state changes
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (mode === 'frequency' && !state.isPlaying) {
|
|
264
|
+
// One final render when stopped
|
|
265
|
+
renderFrequency();
|
|
266
|
+
}
|
|
267
|
+
}, [mode, state.isPlaying, renderFrequency]);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div ref={containerRef} className={cn('w-full', className)}>
|
|
271
|
+
<canvas
|
|
272
|
+
ref={canvasRef}
|
|
273
|
+
onClick={handleClick}
|
|
274
|
+
className="cursor-pointer"
|
|
275
|
+
style={{ width: '100%', height }}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SimpleAudioPlayer -
|
|
4
|
+
* SimpleAudioPlayer - WaveSurfer-based audio player (Legacy)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* @deprecated Consider using `HybridSimplePlayer` instead for better streaming
|
|
7
|
+
* support, lower memory usage, and guaranteed no audio crackling.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* <SimpleAudioPlayer src="https://example.com/audio.mp3" />
|
|
9
|
+
* Use SimpleAudioPlayer only when you specifically need the static waveform
|
|
10
|
+
* visualization that shows the full audio amplitude shape.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* title="Track Title"
|
|
18
|
-
* artist="Artist Name"
|
|
19
|
-
* coverArt="/path/to/cover.jpg"
|
|
20
|
-
* />
|
|
12
|
+
* Migration:
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Before
|
|
15
|
+
* <SimpleAudioPlayer src={url} prefetch reactiveCover />
|
|
21
16
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* showEqualizer={false}
|
|
30
|
-
* reactiveCover
|
|
31
|
-
* variant="spotlight"
|
|
32
|
-
* autoPlay={false}
|
|
33
|
-
* />
|
|
17
|
+
* // After
|
|
18
|
+
* <HybridSimplePlayer src={url} reactiveCover />
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Key differences:
|
|
22
|
+
* - HybridSimplePlayer: Real-time frequency bars, native streaming, no prefetch needed
|
|
23
|
+
* - SimpleAudioPlayer: Static waveform shape, requires prefetch for streaming URLs
|
|
34
24
|
*/
|
|
35
25
|
|
|
36
26
|
import { useRef, type ReactNode } from 'react';
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* AudioPlayer components - Public API
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// WaveSurfer-based components (original)
|
|
6
6
|
export { AudioPlayer } from './AudioPlayer';
|
|
7
7
|
export { SimpleAudioPlayer, type SimpleAudioPlayerProps } from './SimpleAudioPlayer';
|
|
8
8
|
export { AudioEqualizer } from './AudioEqualizer';
|
|
9
9
|
export { AudioShortcutsPopover } from './AudioShortcutsPopover';
|
|
10
10
|
export { VisualizationToggle, type VisualizationToggleProps } from './VisualizationToggle';
|
|
11
11
|
|
|
12
|
+
// Hybrid player components (HTML5 audio + Web Audio for visualization)
|
|
13
|
+
export { HybridAudioPlayer, type HybridAudioPlayerProps } from './HybridAudioPlayer';
|
|
14
|
+
export { HybridSimplePlayer, type HybridSimplePlayerProps } from './HybridSimplePlayer';
|
|
15
|
+
export { HybridWaveform, type HybridWaveformProps } from './HybridWaveform';
|
|
16
|
+
|
|
12
17
|
// ReactiveCover
|
|
13
18
|
export {
|
|
14
19
|
AudioReactiveCover,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* AudioProvider -
|
|
4
|
+
* AudioProvider - WaveSurfer-based audio context (Legacy)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* @deprecated Consider using `HybridAudioProvider` instead for better streaming
|
|
7
|
+
* support, lower memory usage, and guaranteed no audio crackling.
|
|
8
|
+
*
|
|
9
|
+
* Provides centralized audio state management using WaveSurfer.js.
|
|
10
|
+
* All child components can access playback state and controls.
|
|
11
|
+
*
|
|
12
|
+
* @see HybridAudioProvider for the recommended alternative
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridAudioProvider - Context provider for hybrid audio player.
|
|
5
|
+
*
|
|
6
|
+
* Provides audio state, controls, and analysis data to child components.
|
|
7
|
+
* Uses native HTML5 audio for playback with Web Audio API for visualization only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
11
|
+
import {
|
|
12
|
+
useHybridAudio,
|
|
13
|
+
type UseHybridAudioOptions,
|
|
14
|
+
type HybridAudioState,
|
|
15
|
+
type HybridAudioControls,
|
|
16
|
+
type HybridWebAudioAPI,
|
|
17
|
+
} from '../hooks/useHybridAudio';
|
|
18
|
+
import { useHybridAudioAnalysis } from '../hooks/useHybridAudioAnalysis';
|
|
19
|
+
import type { AudioLevels } from '../effects';
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// TYPES
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export interface HybridAudioContextValue {
|
|
26
|
+
// Audio state
|
|
27
|
+
state: HybridAudioState;
|
|
28
|
+
|
|
29
|
+
// Controls
|
|
30
|
+
controls: HybridAudioControls;
|
|
31
|
+
|
|
32
|
+
// Web Audio (for visualizations)
|
|
33
|
+
webAudio: HybridWebAudioAPI;
|
|
34
|
+
|
|
35
|
+
// Audio levels (for reactive effects)
|
|
36
|
+
audioLevels: AudioLevels;
|
|
37
|
+
|
|
38
|
+
// Audio element ref (for custom integrations)
|
|
39
|
+
audioRef: React.RefObject<HTMLAudioElement | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// CONTEXT
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
const HybridAudioContext = createContext<HybridAudioContextValue | null>(null);
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// PROVIDER
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export interface HybridAudioProviderProps extends UseHybridAudioOptions {
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function HybridAudioProvider({ children, ...options }: HybridAudioProviderProps) {
|
|
57
|
+
const { audioRef, state, controls, webAudio } = useHybridAudio(options);
|
|
58
|
+
|
|
59
|
+
// Audio analysis for reactive effects
|
|
60
|
+
const audioLevels = useHybridAudioAnalysis(webAudio.analyser, state.isPlaying);
|
|
61
|
+
|
|
62
|
+
const value = useMemo<HybridAudioContextValue>(
|
|
63
|
+
() => ({
|
|
64
|
+
state,
|
|
65
|
+
controls,
|
|
66
|
+
webAudio,
|
|
67
|
+
audioLevels,
|
|
68
|
+
audioRef,
|
|
69
|
+
}),
|
|
70
|
+
[state, controls, webAudio, audioLevels, audioRef]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return <HybridAudioContext.Provider value={value}>{children}</HybridAudioContext.Provider>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// HOOKS
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Access full hybrid audio context
|
|
82
|
+
*/
|
|
83
|
+
export function useHybridAudioContext(): HybridAudioContextValue {
|
|
84
|
+
const context = useContext(HybridAudioContext);
|
|
85
|
+
if (!context) {
|
|
86
|
+
throw new Error('useHybridAudioContext must be used within HybridAudioProvider');
|
|
87
|
+
}
|
|
88
|
+
return context;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Access audio state only (read-only)
|
|
93
|
+
*/
|
|
94
|
+
export function useHybridAudioState(): HybridAudioState {
|
|
95
|
+
const { state } = useHybridAudioContext();
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Access audio controls only (no state re-renders)
|
|
101
|
+
*/
|
|
102
|
+
export function useHybridAudioControls(): HybridAudioControls {
|
|
103
|
+
const { controls } = useHybridAudioContext();
|
|
104
|
+
return controls;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Access audio levels for reactive effects
|
|
109
|
+
*/
|
|
110
|
+
export function useHybridAudioLevels(): AudioLevels {
|
|
111
|
+
const { audioLevels } = useHybridAudioContext();
|
|
112
|
+
return audioLevels;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Access Web Audio API for custom visualizations
|
|
117
|
+
*/
|
|
118
|
+
export function useHybridWebAudio(): HybridWebAudioAPI {
|
|
119
|
+
const { webAudio } = useHybridAudioContext();
|
|
120
|
+
return webAudio;
|
|
121
|
+
}
|
|
@@ -4,8 +4,20 @@
|
|
|
4
4
|
* Re-exports provider and selector hooks
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// WaveSurfer-based provider (original)
|
|
8
8
|
export { AudioProvider, AudioPlayerContext } from './AudioProvider';
|
|
9
9
|
|
|
10
|
-
// Selector hooks
|
|
10
|
+
// Selector hooks for WaveSurfer provider
|
|
11
11
|
export { useAudio, useAudioControls, useAudioState, useAudioElement } from './selectors';
|
|
12
|
+
|
|
13
|
+
// Hybrid provider (HTML5 audio + Web Audio for visualization)
|
|
14
|
+
export {
|
|
15
|
+
HybridAudioProvider,
|
|
16
|
+
useHybridAudioContext,
|
|
17
|
+
useHybridAudioState,
|
|
18
|
+
useHybridAudioControls,
|
|
19
|
+
useHybridAudioLevels,
|
|
20
|
+
useHybridWebAudio,
|
|
21
|
+
type HybridAudioContextValue,
|
|
22
|
+
type HybridAudioProviderProps,
|
|
23
|
+
} from './HybridAudioProvider';
|
|
@@ -8,6 +8,17 @@ export { useAudioAnalysis } from './useAudioAnalysis';
|
|
|
8
8
|
export { useAudioSource } from './useAudioSource';
|
|
9
9
|
export type { UseAudioSourceResult } from './useAudioSource';
|
|
10
10
|
|
|
11
|
+
// Hybrid player hooks
|
|
12
|
+
export { useHybridAudio } from './useHybridAudio';
|
|
13
|
+
export type {
|
|
14
|
+
UseHybridAudioOptions,
|
|
15
|
+
HybridAudioState,
|
|
16
|
+
HybridAudioControls,
|
|
17
|
+
HybridWebAudioAPI,
|
|
18
|
+
UseHybridAudioReturn,
|
|
19
|
+
} from './useHybridAudio';
|
|
20
|
+
export { useHybridAudioAnalysis } from './useHybridAudioAnalysis';
|
|
21
|
+
|
|
11
22
|
// Public hooks
|
|
12
23
|
export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
|
|
13
24
|
export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';
|