@djangocfg/ui-nextjs 2.1.83 → 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.
Files changed (36) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/README.md +60 -166
  3. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -35
  4. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -11
  5. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +5 -5
  6. package/src/tools/AudioPlayer/components/index.ts +4 -8
  7. package/src/tools/AudioPlayer/context/index.ts +1 -8
  8. package/src/tools/AudioPlayer/hooks/index.ts +6 -13
  9. package/src/tools/AudioPlayer/index.ts +25 -89
  10. package/src/tools/AudioPlayer/types/index.ts +10 -18
  11. package/src/tools/index.ts +51 -56
  12. package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +0 -1146
  13. package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +0 -611
  14. package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +0 -560
  15. package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +0 -769
  16. package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +0 -373
  17. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +0 -200
  18. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +0 -236
  19. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +0 -99
  20. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +0 -278
  21. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +0 -64
  22. package/src/tools/AudioPlayer/context/AudioProvider.tsx +0 -376
  23. package/src/tools/AudioPlayer/context/selectors.ts +0 -96
  24. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +0 -110
  25. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +0 -150
  26. package/src/tools/AudioPlayer/hooks/useAudioSource.ts +0 -155
  27. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +0 -109
  28. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +0 -303
  29. package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +0 -381
  30. package/src/tools/AudioPlayer/progressive/index.ts +0 -40
  31. package/src/tools/AudioPlayer/progressive/peaks.ts +0 -234
  32. package/src/tools/AudioPlayer/progressive/types.ts +0 -179
  33. package/src/tools/AudioPlayer/progressive/useAudioElement.ts +0 -340
  34. package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +0 -267
  35. package/src/tools/AudioPlayer/types/audio.ts +0 -121
  36. package/src/tools/AudioPlayer/types/components.ts +0 -98
@@ -1,303 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * ProgressiveAudioPlayer - Audio player with progressive waveform loading
5
- *
6
- * Uses native HTML5 audio for playback (supports Range requests)
7
- * and custom canvas for waveform visualization.
8
- *
9
- * Features:
10
- * - Progressive waveform loading (no need to load entire file)
11
- * - Smooth seek on any position
12
- * - Buffered ranges visualization
13
- * - Keyboard shortcuts
14
- * - Volume control
15
- * - Loop mode
16
- *
17
- * NOTE: This player does NOT support reactive cover effects.
18
- * For reactive effects with streaming support, use `HybridSimplePlayer` instead.
19
- *
20
- * Use this player when you need:
21
- * - Progressive waveform that shows the actual audio amplitude shape
22
- * - Visualization of buffered/loaded ranges
23
- * - Large file streaming without reactive effects
24
- */
25
-
26
- import { useRef, useCallback } from 'react';
27
- import {
28
- Play,
29
- Pause,
30
- RotateCcw,
31
- SkipBack,
32
- SkipForward,
33
- Volume2,
34
- VolumeX,
35
- Loader2,
36
- Repeat,
37
- AlertCircle,
38
- } from 'lucide-react';
39
- import { cn, Button, Slider } from '@djangocfg/ui-core';
40
-
41
- import { useAudioElement } from './useAudioElement';
42
- import { useProgressiveWaveform } from './useProgressiveWaveform';
43
- import { WaveformCanvas } from './WaveformCanvas';
44
- import { formatTime } from '../utils';
45
- import type { ProgressiveAudioPlayerProps, WaveformStyle } from './types';
46
-
47
- // =============================================================================
48
- // COMPONENT
49
- // =============================================================================
50
-
51
- export function ProgressiveAudioPlayer({
52
- src,
53
- title,
54
- artist,
55
- coverArt,
56
- showWaveform = true,
57
- showControls = true,
58
- showTimer = true,
59
- showVolume = true,
60
- showLoop = true,
61
- autoPlay = false,
62
- waveformStyle,
63
- className,
64
- onPlay,
65
- onPause,
66
- onEnded,
67
- onTimeUpdate,
68
- onError,
69
- }: ProgressiveAudioPlayerProps) {
70
- const audioRef = useRef<HTMLAudioElement>(null);
71
-
72
- // Audio element hook
73
- const audio = useAudioElement({
74
- src,
75
- autoPlay,
76
- onPlay,
77
- onPause,
78
- onEnded,
79
- onTimeUpdate,
80
- onError,
81
- });
82
-
83
- // Progressive waveform loading
84
- const waveform = useProgressiveWaveform({
85
- url: src,
86
- duration: audio.duration,
87
- enabled: showWaveform,
88
- });
89
-
90
- // Handlers
91
- const handleSeek = useCallback((time: number) => {
92
- audio.seek(time);
93
- }, [audio]);
94
-
95
- const handleVolumeChange = useCallback((value: number[]) => {
96
- audio.setVolume(value[0] / 100);
97
- }, [audio]);
98
-
99
- // Loading state
100
- const isLoading = !audio.isReady;
101
- const hasError = audio.error || waveform.error;
102
-
103
- // Default waveform style
104
- const defaultStyle: WaveformStyle = {
105
- waveColor: 'hsl(217 91% 60% / 0.3)',
106
- progressColor: 'hsl(217 91% 60%)',
107
- loadingColor: 'hsl(217 91% 60% / 0.1)',
108
- cursorColor: 'hsl(217 91% 60%)',
109
- barWidth: 3,
110
- barGap: 2,
111
- barRadius: 2,
112
- height: 64,
113
- ...waveformStyle,
114
- };
115
-
116
- return (
117
- <div className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}>
118
- {/* Hidden audio element */}
119
- <audio
120
- ref={(el) => {
121
- (audioRef as any).current = el;
122
- (audio.audioRef as any).current = el;
123
- }}
124
- preload="metadata"
125
- crossOrigin="anonymous"
126
- />
127
-
128
- {/* Header with title/artist */}
129
- {(title || artist) && (
130
- <div className="text-center">
131
- {title && <h3 className="text-sm font-medium truncate">{title}</h3>}
132
- {artist && <p className="text-xs text-muted-foreground truncate">{artist}</p>}
133
- </div>
134
- )}
135
-
136
- {/* Waveform */}
137
- {showWaveform && (
138
- <div className="relative">
139
- <WaveformCanvas
140
- peaks={waveform.peaks}
141
- currentTime={audio.currentTime}
142
- duration={audio.duration}
143
- buffered={audio.buffered}
144
- loadingPercent={waveform.loadedPercent}
145
- onSeek={handleSeek}
146
- interactive={audio.isReady}
147
- style={defaultStyle}
148
- className={cn(
149
- 'rounded transition-opacity',
150
- isLoading && 'opacity-50'
151
- )}
152
- />
153
-
154
- {/* Loading overlay */}
155
- {isLoading && (
156
- <div className="absolute inset-0 flex items-center justify-center">
157
- <Loader2 className="h-6 w-6 animate-spin text-primary" />
158
- </div>
159
- )}
160
-
161
- {/* Error overlay */}
162
- {hasError && (
163
- <div className="absolute inset-0 flex items-center justify-center bg-destructive/10 rounded">
164
- <div className="flex items-center gap-2 text-destructive text-sm">
165
- <AlertCircle className="h-4 w-4" />
166
- <span>Failed to load</span>
167
- <Button
168
- variant="ghost"
169
- size="sm"
170
- onClick={waveform.retry}
171
- className="h-6 px-2"
172
- >
173
- Retry
174
- </Button>
175
- </div>
176
- </div>
177
- )}
178
-
179
- {/* Loading progress indicator */}
180
- {waveform.isLoading && !isLoading && (
181
- <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted overflow-hidden rounded-full">
182
- <div
183
- className="h-full bg-primary transition-all duration-300"
184
- style={{ width: `${waveform.loadedPercent}%` }}
185
- />
186
- </div>
187
- )}
188
- </div>
189
- )}
190
-
191
- {/* Timer */}
192
- {showTimer && (
193
- <div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
194
- <span>{formatTime(audio.currentTime)}</span>
195
- <span>{formatTime(audio.duration)}</span>
196
- </div>
197
- )}
198
-
199
- {/* Controls */}
200
- {showControls && (
201
- <div className="flex items-center justify-center gap-1">
202
- {/* Restart */}
203
- <Button
204
- variant="ghost"
205
- size="icon"
206
- className="h-9 w-9"
207
- onClick={() => audio.seek(0)}
208
- disabled={!audio.isReady}
209
- title="Restart"
210
- >
211
- <RotateCcw className="h-4 w-4" />
212
- </Button>
213
-
214
- {/* Skip back 5s */}
215
- <Button
216
- variant="ghost"
217
- size="icon"
218
- className="h-9 w-9"
219
- onClick={() => audio.skip(-5)}
220
- disabled={!audio.isReady}
221
- title="Back 5 seconds"
222
- >
223
- <SkipBack className="h-4 w-4" />
224
- </Button>
225
-
226
- {/* Play/Pause */}
227
- <Button
228
- variant="default"
229
- size="icon"
230
- className="h-12 w-12 rounded-full"
231
- onClick={audio.togglePlay}
232
- disabled={!audio.isReady && !isLoading}
233
- title={audio.isPlaying ? 'Pause' : 'Play'}
234
- >
235
- {isLoading ? (
236
- <Loader2 className="h-5 w-5 animate-spin" />
237
- ) : audio.isPlaying ? (
238
- <Pause className="h-5 w-5" />
239
- ) : (
240
- <Play className="h-5 w-5 ml-0.5" />
241
- )}
242
- </Button>
243
-
244
- {/* Skip forward 5s */}
245
- <Button
246
- variant="ghost"
247
- size="icon"
248
- className="h-9 w-9"
249
- onClick={() => audio.skip(5)}
250
- disabled={!audio.isReady}
251
- title="Forward 5 seconds"
252
- >
253
- <SkipForward className="h-4 w-4" />
254
- </Button>
255
-
256
- {/* Volume */}
257
- {showVolume && (
258
- <>
259
- <Button
260
- variant="ghost"
261
- size="icon"
262
- className="h-9 w-9"
263
- onClick={audio.toggleMute}
264
- title={audio.isMuted ? 'Unmute' : 'Mute'}
265
- >
266
- {audio.isMuted || audio.volume === 0 ? (
267
- <VolumeX className="h-4 w-4" />
268
- ) : (
269
- <Volume2 className="h-4 w-4" />
270
- )}
271
- </Button>
272
-
273
- <Slider
274
- value={[audio.isMuted ? 0 : audio.volume * 100]}
275
- max={100}
276
- step={1}
277
- onValueChange={handleVolumeChange}
278
- className="w-20"
279
- aria-label="Volume"
280
- />
281
- </>
282
- )}
283
-
284
- {/* Loop/Repeat */}
285
- {showLoop && (
286
- <Button
287
- variant="ghost"
288
- size="icon"
289
- className={cn('h-9 w-9', audio.isLooping && 'text-primary')}
290
- onClick={audio.toggleLoop}
291
- disabled={!audio.isReady}
292
- title={audio.isLooping ? 'Disable loop' : 'Enable loop'}
293
- >
294
- <Repeat className="h-4 w-4" />
295
- </Button>
296
- )}
297
- </div>
298
- )}
299
- </div>
300
- );
301
- }
302
-
303
- export default ProgressiveAudioPlayer;
@@ -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
- }