@djangocfg/ui-nextjs 2.1.82 → 2.1.84
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/README.md +108 -242
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/{SimpleAudioPlayer.tsx → HybridSimplePlayer.tsx} +61 -69
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +5 -5
- package/src/tools/AudioPlayer/components/index.ts +7 -6
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -6
- package/src/tools/AudioPlayer/hooks/index.ts +14 -10
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/{useAudioAnalysis.ts → useHybridAudioAnalysis.ts} +23 -38
- package/src/tools/AudioPlayer/index.ts +37 -70
- package/src/tools/AudioPlayer/types/index.ts +10 -18
- package/src/tools/index.ts +60 -43
- 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
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +0 -200
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +0 -231
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +0 -99
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +0 -64
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +0 -371
- package/src/tools/AudioPlayer/context/selectors.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +0 -150
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +0 -155
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +0 -106
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +0 -295
- package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +0 -381
- package/src/tools/AudioPlayer/progressive/index.ts +0 -40
- package/src/tools/AudioPlayer/progressive/peaks.ts +0 -234
- package/src/tools/AudioPlayer/progressive/types.ts +0 -179
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +0 -340
- package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +0 -267
- package/src/tools/AudioPlayer/types/audio.ts +0 -121
- package/src/tools/AudioPlayer/types/components.ts +0 -98
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WaveformCanvas - Canvas-based waveform visualization
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Progressive rendering as peaks load
|
|
8
|
-
* - Playback progress overlay
|
|
9
|
-
* - Buffered ranges indicator
|
|
10
|
-
* - Interactive seek (click/drag)
|
|
11
|
-
* - Hover preview
|
|
12
|
-
* - High DPI support
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
useRef,
|
|
17
|
-
useEffect,
|
|
18
|
-
useCallback,
|
|
19
|
-
useState,
|
|
20
|
-
forwardRef,
|
|
21
|
-
useImperativeHandle,
|
|
22
|
-
} from 'react';
|
|
23
|
-
import { cn } from '@djangocfg/ui-core';
|
|
24
|
-
import type { WaveformStyle, LoadedRange } from './types';
|
|
25
|
-
|
|
26
|
-
// =============================================================================
|
|
27
|
-
// TYPES
|
|
28
|
-
// =============================================================================
|
|
29
|
-
|
|
30
|
-
interface WaveformCanvasProps {
|
|
31
|
-
/** Peaks data (0-1 normalized) */
|
|
32
|
-
peaks: number[];
|
|
33
|
-
/** Current playback time (seconds) */
|
|
34
|
-
currentTime: number;
|
|
35
|
-
/** Total duration (seconds) */
|
|
36
|
-
duration: number;
|
|
37
|
-
/** Buffered ranges from audio element */
|
|
38
|
-
buffered?: TimeRanges | null;
|
|
39
|
-
/** Loaded ranges (for progressive loading indicator) */
|
|
40
|
-
loadedRanges?: LoadedRange[];
|
|
41
|
-
/** Loading progress (0-100) */
|
|
42
|
-
loadingPercent?: number;
|
|
43
|
-
/** Seek callback */
|
|
44
|
-
onSeek?: (time: number) => void;
|
|
45
|
-
/** Hover callback */
|
|
46
|
-
onHover?: (time: number | null) => void;
|
|
47
|
-
/** Is interactive */
|
|
48
|
-
interactive?: boolean;
|
|
49
|
-
/** Style options */
|
|
50
|
-
style?: WaveformStyle;
|
|
51
|
-
/** Additional class */
|
|
52
|
-
className?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface WaveformCanvasRef {
|
|
56
|
-
redraw: () => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// DEFAULT STYLES
|
|
61
|
-
// =============================================================================
|
|
62
|
-
|
|
63
|
-
const DEFAULT_STYLE: Required<WaveformStyle> = {
|
|
64
|
-
waveColor: 'hsl(217 91% 60% / 0.3)',
|
|
65
|
-
progressColor: 'hsl(217 91% 60%)',
|
|
66
|
-
loadingColor: 'hsl(217 91% 60% / 0.1)',
|
|
67
|
-
cursorColor: 'hsl(217 91% 60%)',
|
|
68
|
-
barWidth: 3,
|
|
69
|
-
barGap: 2,
|
|
70
|
-
barRadius: 2,
|
|
71
|
-
height: 64,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// =============================================================================
|
|
75
|
-
// COMPONENT
|
|
76
|
-
// =============================================================================
|
|
77
|
-
|
|
78
|
-
export const WaveformCanvas = forwardRef<WaveformCanvasRef, WaveformCanvasProps>(
|
|
79
|
-
function WaveformCanvas(props, ref) {
|
|
80
|
-
const {
|
|
81
|
-
peaks,
|
|
82
|
-
currentTime,
|
|
83
|
-
duration,
|
|
84
|
-
buffered,
|
|
85
|
-
loadedRanges = [],
|
|
86
|
-
loadingPercent = 100,
|
|
87
|
-
onSeek,
|
|
88
|
-
onHover,
|
|
89
|
-
interactive = true,
|
|
90
|
-
style = {},
|
|
91
|
-
className,
|
|
92
|
-
} = props;
|
|
93
|
-
|
|
94
|
-
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
95
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
96
|
-
const [hoverX, setHoverX] = useState<number | null>(null);
|
|
97
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
98
|
-
|
|
99
|
-
// Merge styles
|
|
100
|
-
const {
|
|
101
|
-
waveColor,
|
|
102
|
-
progressColor,
|
|
103
|
-
loadingColor,
|
|
104
|
-
cursorColor,
|
|
105
|
-
barWidth,
|
|
106
|
-
barGap,
|
|
107
|
-
barRadius,
|
|
108
|
-
height,
|
|
109
|
-
} = { ...DEFAULT_STYLE, ...style };
|
|
110
|
-
|
|
111
|
-
// ==========================================================================
|
|
112
|
-
// DRAWING
|
|
113
|
-
// ==========================================================================
|
|
114
|
-
|
|
115
|
-
const draw = useCallback(() => {
|
|
116
|
-
const canvas = canvasRef.current;
|
|
117
|
-
const container = containerRef.current;
|
|
118
|
-
if (!canvas || !container) return;
|
|
119
|
-
|
|
120
|
-
const ctx = canvas.getContext('2d');
|
|
121
|
-
if (!ctx) return;
|
|
122
|
-
|
|
123
|
-
// Get dimensions
|
|
124
|
-
const rect = container.getBoundingClientRect();
|
|
125
|
-
const width = rect.width;
|
|
126
|
-
const dpr = window.devicePixelRatio || 1;
|
|
127
|
-
|
|
128
|
-
// Set canvas size with DPI scaling
|
|
129
|
-
canvas.width = width * dpr;
|
|
130
|
-
canvas.height = height * dpr;
|
|
131
|
-
canvas.style.width = `${width}px`;
|
|
132
|
-
canvas.style.height = `${height}px`;
|
|
133
|
-
ctx.scale(dpr, dpr);
|
|
134
|
-
|
|
135
|
-
// Clear
|
|
136
|
-
ctx.clearRect(0, 0, width, height);
|
|
137
|
-
|
|
138
|
-
// Calculate bar positions
|
|
139
|
-
const totalBarSpace = barWidth + barGap;
|
|
140
|
-
const barCount = Math.floor(width / totalBarSpace);
|
|
141
|
-
|
|
142
|
-
if (barCount === 0) return;
|
|
143
|
-
|
|
144
|
-
// Progress position
|
|
145
|
-
const progress = duration > 0 ? currentTime / duration : 0;
|
|
146
|
-
const progressX = progress * width;
|
|
147
|
-
|
|
148
|
-
// Hover position
|
|
149
|
-
const hoverProgress = hoverX !== null ? hoverX / width : null;
|
|
150
|
-
|
|
151
|
-
// Draw loading placeholder for unloaded area
|
|
152
|
-
if (loadingPercent < 100) {
|
|
153
|
-
const loadedX = (loadingPercent / 100) * width;
|
|
154
|
-
ctx.fillStyle = loadingColor;
|
|
155
|
-
ctx.fillRect(loadedX, 0, width - loadedX, height);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Resample peaks to match bar count
|
|
159
|
-
const resampledPeaks = resamplePeaks(peaks, barCount);
|
|
160
|
-
|
|
161
|
-
// Draw bars
|
|
162
|
-
for (let i = 0; i < barCount; i++) {
|
|
163
|
-
const x = i * totalBarSpace;
|
|
164
|
-
const peakValue = resampledPeaks[i] || 0;
|
|
165
|
-
|
|
166
|
-
// Bar height (minimum 2px for visibility)
|
|
167
|
-
const barHeight = Math.max(2, peakValue * (height - 4));
|
|
168
|
-
const y = (height - barHeight) / 2;
|
|
169
|
-
|
|
170
|
-
// Determine color based on position
|
|
171
|
-
const barProgress = (x + barWidth / 2) / width;
|
|
172
|
-
let color = waveColor;
|
|
173
|
-
|
|
174
|
-
if (barProgress <= progress) {
|
|
175
|
-
color = progressColor;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Draw bar
|
|
179
|
-
ctx.fillStyle = color;
|
|
180
|
-
if (barRadius > 0) {
|
|
181
|
-
roundRect(ctx, x, y, barWidth, barHeight, barRadius);
|
|
182
|
-
} else {
|
|
183
|
-
ctx.fillRect(x, y, barWidth, barHeight);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Draw cursor
|
|
188
|
-
if (duration > 0) {
|
|
189
|
-
ctx.fillStyle = cursorColor;
|
|
190
|
-
ctx.fillRect(progressX - 1, 0, 2, height);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Draw hover indicator
|
|
194
|
-
if (hoverProgress !== null && interactive) {
|
|
195
|
-
ctx.fillStyle = `${cursorColor}80`; // 50% opacity
|
|
196
|
-
const hoverPx = hoverProgress * width;
|
|
197
|
-
ctx.fillRect(hoverPx - 1, 0, 2, height);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Draw buffered indicator (thin line at bottom)
|
|
201
|
-
if (buffered && buffered.length > 0 && duration > 0) {
|
|
202
|
-
ctx.fillStyle = `${progressColor}40`;
|
|
203
|
-
for (let i = 0; i < buffered.length; i++) {
|
|
204
|
-
const start = (buffered.start(i) / duration) * width;
|
|
205
|
-
const end = (buffered.end(i) / duration) * width;
|
|
206
|
-
ctx.fillRect(start, height - 2, end - start, 2);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}, [
|
|
210
|
-
peaks,
|
|
211
|
-
currentTime,
|
|
212
|
-
duration,
|
|
213
|
-
buffered,
|
|
214
|
-
loadingPercent,
|
|
215
|
-
hoverX,
|
|
216
|
-
waveColor,
|
|
217
|
-
progressColor,
|
|
218
|
-
loadingColor,
|
|
219
|
-
cursorColor,
|
|
220
|
-
barWidth,
|
|
221
|
-
barGap,
|
|
222
|
-
barRadius,
|
|
223
|
-
height,
|
|
224
|
-
interactive,
|
|
225
|
-
]);
|
|
226
|
-
|
|
227
|
-
// ==========================================================================
|
|
228
|
-
// INTERACTION HANDLERS
|
|
229
|
-
// ==========================================================================
|
|
230
|
-
|
|
231
|
-
const getTimeFromX = useCallback((clientX: number): number => {
|
|
232
|
-
const container = containerRef.current;
|
|
233
|
-
if (!container || duration <= 0) return 0;
|
|
234
|
-
|
|
235
|
-
const rect = container.getBoundingClientRect();
|
|
236
|
-
const x = clientX - rect.left;
|
|
237
|
-
const progress = Math.max(0, Math.min(1, x / rect.width));
|
|
238
|
-
return progress * duration;
|
|
239
|
-
}, [duration]);
|
|
240
|
-
|
|
241
|
-
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
242
|
-
if (!interactive) return;
|
|
243
|
-
|
|
244
|
-
const container = containerRef.current;
|
|
245
|
-
if (!container) return;
|
|
246
|
-
|
|
247
|
-
const rect = container.getBoundingClientRect();
|
|
248
|
-
const x = e.clientX - rect.left;
|
|
249
|
-
setHoverX(x);
|
|
250
|
-
|
|
251
|
-
const time = getTimeFromX(e.clientX);
|
|
252
|
-
onHover?.(time);
|
|
253
|
-
|
|
254
|
-
if (isDragging) {
|
|
255
|
-
onSeek?.(time);
|
|
256
|
-
}
|
|
257
|
-
}, [interactive, isDragging, getTimeFromX, onHover, onSeek]);
|
|
258
|
-
|
|
259
|
-
const handleMouseLeave = useCallback(() => {
|
|
260
|
-
setHoverX(null);
|
|
261
|
-
onHover?.(null);
|
|
262
|
-
setIsDragging(false);
|
|
263
|
-
}, [onHover]);
|
|
264
|
-
|
|
265
|
-
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
266
|
-
if (!interactive) return;
|
|
267
|
-
e.preventDefault();
|
|
268
|
-
setIsDragging(true);
|
|
269
|
-
const time = getTimeFromX(e.clientX);
|
|
270
|
-
onSeek?.(time);
|
|
271
|
-
}, [interactive, getTimeFromX, onSeek]);
|
|
272
|
-
|
|
273
|
-
const handleMouseUp = useCallback(() => {
|
|
274
|
-
setIsDragging(false);
|
|
275
|
-
}, []);
|
|
276
|
-
|
|
277
|
-
// Global mouseup for drag release
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
if (!isDragging) return;
|
|
280
|
-
|
|
281
|
-
const handleGlobalMouseUp = () => setIsDragging(false);
|
|
282
|
-
window.addEventListener('mouseup', handleGlobalMouseUp);
|
|
283
|
-
return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
284
|
-
}, [isDragging]);
|
|
285
|
-
|
|
286
|
-
// ==========================================================================
|
|
287
|
-
// EFFECTS
|
|
288
|
-
// ==========================================================================
|
|
289
|
-
|
|
290
|
-
// Redraw on any change
|
|
291
|
-
useEffect(() => {
|
|
292
|
-
draw();
|
|
293
|
-
}, [draw]);
|
|
294
|
-
|
|
295
|
-
// Redraw on resize
|
|
296
|
-
useEffect(() => {
|
|
297
|
-
const handleResize = () => draw();
|
|
298
|
-
window.addEventListener('resize', handleResize);
|
|
299
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
300
|
-
}, [draw]);
|
|
301
|
-
|
|
302
|
-
// Expose redraw method
|
|
303
|
-
useImperativeHandle(ref, () => ({
|
|
304
|
-
redraw: draw,
|
|
305
|
-
}), [draw]);
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
<div
|
|
309
|
-
ref={containerRef}
|
|
310
|
-
className={cn(
|
|
311
|
-
'relative w-full cursor-pointer select-none',
|
|
312
|
-
!interactive && 'cursor-default',
|
|
313
|
-
className
|
|
314
|
-
)}
|
|
315
|
-
style={{ height }}
|
|
316
|
-
onMouseMove={handleMouseMove}
|
|
317
|
-
onMouseLeave={handleMouseLeave}
|
|
318
|
-
onMouseDown={handleMouseDown}
|
|
319
|
-
onMouseUp={handleMouseUp}
|
|
320
|
-
>
|
|
321
|
-
<canvas
|
|
322
|
-
ref={canvasRef}
|
|
323
|
-
className="absolute inset-0"
|
|
324
|
-
/>
|
|
325
|
-
</div>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
// =============================================================================
|
|
331
|
-
// HELPERS
|
|
332
|
-
// =============================================================================
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Draw rounded rectangle
|
|
336
|
-
*/
|
|
337
|
-
function roundRect(
|
|
338
|
-
ctx: CanvasRenderingContext2D,
|
|
339
|
-
x: number,
|
|
340
|
-
y: number,
|
|
341
|
-
width: number,
|
|
342
|
-
height: number,
|
|
343
|
-
radius: number
|
|
344
|
-
) {
|
|
345
|
-
ctx.beginPath();
|
|
346
|
-
ctx.moveTo(x + radius, y);
|
|
347
|
-
ctx.lineTo(x + width - radius, y);
|
|
348
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
349
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
350
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
351
|
-
ctx.lineTo(x + radius, y + height);
|
|
352
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
353
|
-
ctx.lineTo(x, y + radius);
|
|
354
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
355
|
-
ctx.closePath();
|
|
356
|
-
ctx.fill();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Resample peaks to target length
|
|
361
|
-
*/
|
|
362
|
-
function resamplePeaks(peaks: number[], targetLength: number): number[] {
|
|
363
|
-
if (peaks.length === 0) return new Array(targetLength).fill(0);
|
|
364
|
-
if (peaks.length === targetLength) return peaks;
|
|
365
|
-
|
|
366
|
-
const result = new Array<number>(targetLength);
|
|
367
|
-
const ratio = peaks.length / targetLength;
|
|
368
|
-
|
|
369
|
-
for (let i = 0; i < targetLength; i++) {
|
|
370
|
-
const start = Math.floor(i * ratio);
|
|
371
|
-
const end = Math.min(Math.ceil((i + 1) * ratio), peaks.length);
|
|
372
|
-
|
|
373
|
-
let max = 0;
|
|
374
|
-
for (let j = start; j < end; j++) {
|
|
375
|
-
if (peaks[j] > max) max = peaks[j];
|
|
376
|
-
}
|
|
377
|
-
result[i] = max;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return result;
|
|
381
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progressive Audio Player
|
|
3
|
-
*
|
|
4
|
-
* Audio player with progressive waveform loading.
|
|
5
|
-
* Uses native HTML5 audio (supports Range requests) and custom canvas visualization.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Components
|
|
9
|
-
export { ProgressiveAudioPlayer } from './ProgressiveAudioPlayer';
|
|
10
|
-
export { WaveformCanvas } from './WaveformCanvas';
|
|
11
|
-
|
|
12
|
-
// Hooks
|
|
13
|
-
export { useAudioElement } from './useAudioElement';
|
|
14
|
-
export { useProgressiveWaveform } from './useProgressiveWaveform';
|
|
15
|
-
|
|
16
|
-
// Utilities
|
|
17
|
-
export {
|
|
18
|
-
extractPeaks,
|
|
19
|
-
mergePeaks,
|
|
20
|
-
resamplePeaks,
|
|
21
|
-
smoothPeaks,
|
|
22
|
-
calculatePeaksCount,
|
|
23
|
-
timeToIndex,
|
|
24
|
-
indexToTime,
|
|
25
|
-
} from './peaks';
|
|
26
|
-
|
|
27
|
-
// Types
|
|
28
|
-
export type {
|
|
29
|
-
WaveformData,
|
|
30
|
-
LoadedRange,
|
|
31
|
-
WaveformLoadingState,
|
|
32
|
-
AudioState,
|
|
33
|
-
AudioControls,
|
|
34
|
-
WaveformStyle,
|
|
35
|
-
WaveformInteraction,
|
|
36
|
-
ProgressiveAudioPlayerProps,
|
|
37
|
-
ChunkInfo,
|
|
38
|
-
DecodeResult,
|
|
39
|
-
DecoderOptions,
|
|
40
|
-
} from './types';
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audio Peaks Extraction Utilities
|
|
3
|
-
*
|
|
4
|
-
* Extracts waveform peaks from AudioBuffer for visualization.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// =============================================================================
|
|
8
|
-
// TYPES
|
|
9
|
-
// =============================================================================
|
|
10
|
-
|
|
11
|
-
interface ExtractPeaksOptions {
|
|
12
|
-
/** Number of peaks to extract */
|
|
13
|
-
length: number;
|
|
14
|
-
/** Channel to use (0 = left, 1 = right, -1 = mix) */
|
|
15
|
-
channel?: number;
|
|
16
|
-
/** Normalize peaks to 0-1 range */
|
|
17
|
-
normalize?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// PEAK EXTRACTION
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Extract peaks from an AudioBuffer
|
|
26
|
-
*
|
|
27
|
-
* @param audioBuffer - Decoded audio buffer
|
|
28
|
-
* @param options - Extraction options
|
|
29
|
-
* @returns Array of normalized peak values (0-1)
|
|
30
|
-
*/
|
|
31
|
-
export function extractPeaks(
|
|
32
|
-
audioBuffer: AudioBuffer,
|
|
33
|
-
options: ExtractPeaksOptions
|
|
34
|
-
): number[] {
|
|
35
|
-
const { length, channel = -1, normalize = true } = options;
|
|
36
|
-
|
|
37
|
-
// Get channel data
|
|
38
|
-
let data: Float32Array;
|
|
39
|
-
if (channel === -1) {
|
|
40
|
-
// Mix all channels
|
|
41
|
-
data = mixChannels(audioBuffer);
|
|
42
|
-
} else {
|
|
43
|
-
data = audioBuffer.getChannelData(Math.min(channel, audioBuffer.numberOfChannels - 1));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const peaks = new Array<number>(length);
|
|
47
|
-
const samplesPerPeak = Math.floor(data.length / length);
|
|
48
|
-
|
|
49
|
-
let maxPeak = 0;
|
|
50
|
-
|
|
51
|
-
for (let i = 0; i < length; i++) {
|
|
52
|
-
const start = i * samplesPerPeak;
|
|
53
|
-
const end = Math.min(start + samplesPerPeak, data.length);
|
|
54
|
-
|
|
55
|
-
let max = 0;
|
|
56
|
-
for (let j = start; j < end; j++) {
|
|
57
|
-
const abs = Math.abs(data[j]);
|
|
58
|
-
if (abs > max) max = abs;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
peaks[i] = max;
|
|
62
|
-
if (max > maxPeak) maxPeak = max;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Normalize to 0-1
|
|
66
|
-
if (normalize && maxPeak > 0) {
|
|
67
|
-
for (let i = 0; i < length; i++) {
|
|
68
|
-
peaks[i] = peaks[i] / maxPeak;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return peaks;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Mix all channels into mono
|
|
77
|
-
*/
|
|
78
|
-
function mixChannels(audioBuffer: AudioBuffer): Float32Array {
|
|
79
|
-
const numChannels = audioBuffer.numberOfChannels;
|
|
80
|
-
const length = audioBuffer.length;
|
|
81
|
-
const mixed = new Float32Array(length);
|
|
82
|
-
|
|
83
|
-
for (let ch = 0; ch < numChannels; ch++) {
|
|
84
|
-
const channelData = audioBuffer.getChannelData(ch);
|
|
85
|
-
for (let i = 0; i < length; i++) {
|
|
86
|
-
mixed[i] += channelData[i];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Average
|
|
91
|
-
const divisor = numChannels;
|
|
92
|
-
for (let i = 0; i < length; i++) {
|
|
93
|
-
mixed[i] /= divisor;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return mixed;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// =============================================================================
|
|
100
|
-
// PEAK MERGING
|
|
101
|
-
// =============================================================================
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Merge two peaks arrays (for progressive loading)
|
|
105
|
-
*
|
|
106
|
-
* @param existing - Existing peaks array
|
|
107
|
-
* @param newPeaks - New peaks to merge
|
|
108
|
-
* @param startIndex - Start index for new peaks
|
|
109
|
-
* @returns Merged peaks array
|
|
110
|
-
*/
|
|
111
|
-
export function mergePeaks(
|
|
112
|
-
existing: number[],
|
|
113
|
-
newPeaks: number[],
|
|
114
|
-
startIndex: number
|
|
115
|
-
): number[] {
|
|
116
|
-
const result = [...existing];
|
|
117
|
-
|
|
118
|
-
// Extend if needed
|
|
119
|
-
const neededLength = startIndex + newPeaks.length;
|
|
120
|
-
while (result.length < neededLength) {
|
|
121
|
-
result.push(0);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Merge
|
|
125
|
-
for (let i = 0; i < newPeaks.length; i++) {
|
|
126
|
-
result[startIndex + i] = newPeaks[i];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// =============================================================================
|
|
133
|
-
// PEAK RESAMPLING
|
|
134
|
-
// =============================================================================
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Resample peaks to a different length
|
|
138
|
-
*
|
|
139
|
-
* @param peaks - Source peaks
|
|
140
|
-
* @param targetLength - Target number of peaks
|
|
141
|
-
* @returns Resampled peaks
|
|
142
|
-
*/
|
|
143
|
-
export function resamplePeaks(peaks: number[], targetLength: number): number[] {
|
|
144
|
-
if (peaks.length === 0) return new Array(targetLength).fill(0);
|
|
145
|
-
if (peaks.length === targetLength) return [...peaks];
|
|
146
|
-
|
|
147
|
-
const result = new Array<number>(targetLength);
|
|
148
|
-
const ratio = peaks.length / targetLength;
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < targetLength; i++) {
|
|
151
|
-
const start = Math.floor(i * ratio);
|
|
152
|
-
const end = Math.min(Math.ceil((i + 1) * ratio), peaks.length);
|
|
153
|
-
|
|
154
|
-
let max = 0;
|
|
155
|
-
for (let j = start; j < end; j++) {
|
|
156
|
-
if (peaks[j] > max) max = peaks[j];
|
|
157
|
-
}
|
|
158
|
-
result[i] = max;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// =============================================================================
|
|
165
|
-
// PEAK SMOOTHING
|
|
166
|
-
// =============================================================================
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Smooth peaks using moving average
|
|
170
|
-
*
|
|
171
|
-
* @param peaks - Source peaks
|
|
172
|
-
* @param windowSize - Smoothing window size
|
|
173
|
-
* @returns Smoothed peaks
|
|
174
|
-
*/
|
|
175
|
-
export function smoothPeaks(peaks: number[], windowSize: number = 3): number[] {
|
|
176
|
-
if (windowSize < 2 || peaks.length < windowSize) return [...peaks];
|
|
177
|
-
|
|
178
|
-
const result = new Array<number>(peaks.length);
|
|
179
|
-
const halfWindow = Math.floor(windowSize / 2);
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < peaks.length; i++) {
|
|
182
|
-
const start = Math.max(0, i - halfWindow);
|
|
183
|
-
const end = Math.min(peaks.length, i + halfWindow + 1);
|
|
184
|
-
|
|
185
|
-
let sum = 0;
|
|
186
|
-
for (let j = start; j < end; j++) {
|
|
187
|
-
sum += peaks[j];
|
|
188
|
-
}
|
|
189
|
-
result[i] = sum / (end - start);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// =============================================================================
|
|
196
|
-
// PEAK CALCULATIONS
|
|
197
|
-
// =============================================================================
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Calculate peaks per second based on width and duration
|
|
201
|
-
*/
|
|
202
|
-
export function calculatePeaksCount(
|
|
203
|
-
width: number,
|
|
204
|
-
barWidth: number,
|
|
205
|
-
barGap: number
|
|
206
|
-
): number {
|
|
207
|
-
const totalBarSpace = barWidth + barGap;
|
|
208
|
-
return Math.floor(width / totalBarSpace);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Calculate which peak index corresponds to a time
|
|
213
|
-
*/
|
|
214
|
-
export function timeToIndex(
|
|
215
|
-
time: number,
|
|
216
|
-
duration: number,
|
|
217
|
-
peaksCount: number
|
|
218
|
-
): number {
|
|
219
|
-
if (duration <= 0) return 0;
|
|
220
|
-
const progress = time / duration;
|
|
221
|
-
return Math.floor(progress * peaksCount);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Calculate time from peak index
|
|
226
|
-
*/
|
|
227
|
-
export function indexToTime(
|
|
228
|
-
index: number,
|
|
229
|
-
peaksCount: number,
|
|
230
|
-
duration: number
|
|
231
|
-
): number {
|
|
232
|
-
if (peaksCount <= 0) return 0;
|
|
233
|
-
return (index / peaksCount) * duration;
|
|
234
|
-
}
|