@arraypress/waveform-player 1.0.0

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/src/drawing.js ADDED
@@ -0,0 +1,365 @@
1
+ /**
2
+ * @module drawing
3
+ * @description Core waveform drawing styles optimized for visual distinction at all sizes
4
+ */
5
+
6
+ import {resampleData} from './utils.js';
7
+
8
+ /**
9
+ * Draw standard bars waveform - Classic vertical bars
10
+ */
11
+ export function drawBars(ctx, canvas, peaks, progress, options) {
12
+ const dpr = window.devicePixelRatio || 1;
13
+ const barWidth = options.barWidth * dpr;
14
+ const barSpacing = options.barSpacing * dpr;
15
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
16
+ const resampledPeaks = resampleData(peaks, barCount);
17
+ const height = canvas.height;
18
+ const progressWidth = progress * canvas.width;
19
+
20
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
21
+
22
+ // Draw all bars first
23
+ for (let i = 0; i < resampledPeaks.length; i++) {
24
+ const x = i * (barWidth + barSpacing);
25
+ if (x + barWidth > canvas.width) break;
26
+
27
+ const peakHeight = resampledPeaks[i] * height * 0.9;
28
+ // Draw from bottom up, not centered
29
+ const y = height - peakHeight;
30
+
31
+ ctx.fillStyle = options.color;
32
+ ctx.fillRect(x, y, barWidth, peakHeight);
33
+ }
34
+
35
+ // Progress overlay
36
+ ctx.save();
37
+ ctx.beginPath();
38
+ ctx.rect(0, 0, progressWidth, height);
39
+ ctx.clip();
40
+
41
+ for (let i = 0; i < resampledPeaks.length; i++) {
42
+ const x = i * (barWidth + barSpacing);
43
+ if (x > progressWidth) break;
44
+
45
+ const peakHeight = resampledPeaks[i] * height * 0.9;
46
+ // Draw from bottom up, not centered
47
+ const y = height - peakHeight;
48
+
49
+ ctx.fillStyle = options.progressColor;
50
+ ctx.fillRect(x, y, barWidth, peakHeight);
51
+ }
52
+
53
+ ctx.restore();
54
+ }
55
+
56
+ /**
57
+ * Draw mirror/SoundCloud style waveform - Symmetrical bars
58
+ */
59
+ /**
60
+ * Draw mirror/SoundCloud style waveform - Symmetrical bars
61
+ */
62
+ export function drawMirror(ctx, canvas, peaks, progress, options) {
63
+ const dpr = window.devicePixelRatio || 1;
64
+ const barWidth = options.barWidth * dpr;
65
+ const barSpacing = options.barSpacing * dpr;
66
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
67
+ const resampledPeaks = resampleData(peaks, barCount);
68
+ const height = canvas.height;
69
+ const centerY = height / 2;
70
+ const progressWidth = progress * canvas.width;
71
+
72
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
73
+
74
+ // Draw all bars
75
+ for (let i = 0; i < resampledPeaks.length; i++) {
76
+ const x = i * (barWidth + barSpacing);
77
+ if (x + barWidth > canvas.width) break;
78
+
79
+ const peakHeight = resampledPeaks[i] * height * 0.45;
80
+
81
+ ctx.fillStyle = options.color;
82
+ ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
83
+ ctx.fillRect(x, centerY, barWidth, peakHeight);
84
+ }
85
+
86
+ // Progress overlay
87
+ ctx.save();
88
+ ctx.beginPath();
89
+ ctx.rect(0, 0, progressWidth, height);
90
+ ctx.clip();
91
+
92
+ for (let i = 0; i < resampledPeaks.length; i++) {
93
+ const x = i * (barWidth + barSpacing);
94
+ if (x > progressWidth) break;
95
+
96
+ const peakHeight = resampledPeaks[i] * height * 0.45;
97
+
98
+ ctx.fillStyle = options.progressColor;
99
+ ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
100
+ ctx.fillRect(x, centerY, barWidth, peakHeight);
101
+ }
102
+
103
+ ctx.restore();
104
+ }
105
+
106
+ /**
107
+ * Draw line/oscilloscope style waveform - Smooth flowing wave with glow
108
+ */
109
+ export function drawLine(ctx, canvas, peaks, progress, options) {
110
+ const width = canvas.width;
111
+ const height = canvas.height;
112
+ const centerY = height / 2;
113
+ const amplitude = height * 0.35;
114
+
115
+ ctx.clearRect(0, 0, width, height);
116
+
117
+ // Helper to draw a smooth curve through the peaks
118
+ const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {
119
+ if (addGlow) {
120
+ ctx.shadowBlur = 12;
121
+ ctx.shadowColor = color;
122
+ }
123
+
124
+ ctx.strokeStyle = color;
125
+ ctx.lineWidth = lineWidth;
126
+ ctx.lineCap = 'round';
127
+ ctx.lineJoin = 'round';
128
+
129
+ ctx.beginPath();
130
+ ctx.moveTo(0, centerY);
131
+
132
+ const points = [];
133
+ const samples = Math.floor(peaks.length * endProgress);
134
+
135
+ // Calculate smoothed points
136
+ for (let i = 0; i < samples; i++) {
137
+ const x = (i / (peaks.length - 1)) * width;
138
+ const peakValue = peaks[i];
139
+
140
+ // Create a smooth wave motion
141
+ const waveOffset = Math.sin(i * 0.1) * peakValue;
142
+ const y = centerY + (waveOffset * amplitude);
143
+
144
+ points.push({x, y});
145
+ }
146
+
147
+ // Draw smooth curve through points using bezier curves
148
+ for (let i = 0; i < points.length - 1; i++) {
149
+ const cp1x = points[i].x + (points[i + 1].x - points[i].x) * 0.5;
150
+ const cp1y = points[i].y;
151
+ const cp2x = points[i + 1].x - (points[i + 1].x - points[i].x) * 0.5;
152
+ const cp2y = points[i + 1].y;
153
+
154
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, points[i + 1].x, points[i + 1].y);
155
+ }
156
+
157
+ ctx.stroke();
158
+
159
+ if (addGlow) {
160
+ ctx.shadowBlur = 0;
161
+ }
162
+ };
163
+
164
+ // Draw subtle grid for oscilloscope feel
165
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
166
+ ctx.lineWidth = 0.5;
167
+
168
+ // Horizontal center line
169
+ ctx.beginPath();
170
+ ctx.moveTo(0, centerY);
171
+ ctx.lineTo(width, centerY);
172
+ ctx.stroke();
173
+
174
+ // Vertical grid lines
175
+ for (let i = 0; i <= 10; i++) {
176
+ const x = (width / 10) * i;
177
+ ctx.beginPath();
178
+ ctx.moveTo(x, 0);
179
+ ctx.lineTo(x, height);
180
+ ctx.stroke();
181
+ }
182
+
183
+ // Draw background wave
184
+ drawCurve(options.color, 2, 1, false);
185
+
186
+ // Draw progress with glow
187
+ if (progress > 0) {
188
+ drawCurve(options.progressColor, 3, progress, true);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Draw blocks/LED meter style waveform - Segmented blocks
194
+ */
195
+ export function drawBlocks(ctx, canvas, peaks, progress, options) {
196
+ const dpr = window.devicePixelRatio || 1;
197
+ const barWidth = (options.barWidth || 3) * dpr;
198
+ const barSpacing = (options.barSpacing || 1) * dpr;
199
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
200
+ const resampledPeaks = resampleData(peaks, barCount);
201
+ const height = canvas.height;
202
+ const blockSize = 4 * dpr;
203
+ const blockGap = 2 * dpr;
204
+ const progressWidth = progress * canvas.width;
205
+ const centerY = height / 2;
206
+
207
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
208
+
209
+ for (let i = 0; i < resampledPeaks.length; i++) {
210
+ const x = i * (barWidth + barSpacing);
211
+ if (x + barWidth > canvas.width) break;
212
+
213
+ const peakHeight = resampledPeaks[i] * height * 0.9;
214
+ const blockCount = Math.floor(peakHeight / (blockSize + blockGap));
215
+
216
+ ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
217
+
218
+ // Draw blocks from center outward
219
+ for (let j = 0; j < blockCount; j++) {
220
+ const blockOffset = j * (blockSize + blockGap);
221
+
222
+ // Upper blocks
223
+ ctx.fillRect(x, centerY - blockOffset - blockSize, barWidth, blockSize);
224
+
225
+ // Lower blocks (skip the center block)
226
+ if (j > 0) {
227
+ ctx.fillRect(x, centerY + blockOffset, barWidth, blockSize);
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Draw dots style waveform - Circular points
235
+ */
236
+ export function drawDots(ctx, canvas, peaks, progress, options) {
237
+ const dpr = window.devicePixelRatio || 1;
238
+ const barWidth = (options.barWidth || 2) * dpr;
239
+ const barSpacing = (options.barSpacing || 3) * dpr;
240
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
241
+ const resampledPeaks = resampleData(peaks, barCount);
242
+ const height = canvas.height;
243
+ const dotRadius = Math.max(1.5 * dpr, barWidth / 2);
244
+ const progressWidth = progress * canvas.width;
245
+ const centerY = height / 2;
246
+
247
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
248
+
249
+ for (let i = 0; i < resampledPeaks.length; i++) {
250
+ const x = i * (barWidth + barSpacing) + barWidth / 2;
251
+ if (x > canvas.width) break;
252
+
253
+ const peakHeight = resampledPeaks[i] * height * 0.9;
254
+
255
+ ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
256
+
257
+ // Draw upper dot
258
+ ctx.beginPath();
259
+ ctx.arc(x, centerY - peakHeight / 2, dotRadius, 0, Math.PI * 2);
260
+ ctx.fill();
261
+
262
+ // Draw lower dot
263
+ ctx.beginPath();
264
+ ctx.arc(x, centerY + peakHeight / 2, dotRadius, 0, Math.PI * 2);
265
+ ctx.fill();
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Draw seekbar style - Simple progress bar without waveform
271
+ */
272
+ export function drawSeekbar(ctx, canvas, peaks, progress, options) {
273
+ const width = canvas.width;
274
+ const height = canvas.height;
275
+ const centerY = height / 2;
276
+ const barHeight = 4; // Height of the seekbar in pixels
277
+ const borderRadius = barHeight / 2;
278
+
279
+ ctx.clearRect(0, 0, width, height);
280
+
281
+ // Draw background track
282
+ ctx.fillStyle = options.color || 'rgba(255, 255, 255, 0.2)';
283
+
284
+ // Create rounded rectangle for background
285
+ ctx.beginPath();
286
+ ctx.moveTo(borderRadius, centerY - barHeight / 2);
287
+ ctx.lineTo(width - borderRadius, centerY - barHeight / 2);
288
+ ctx.arc(width - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
289
+ ctx.lineTo(borderRadius, centerY + barHeight / 2);
290
+ ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
291
+ ctx.closePath();
292
+ ctx.fill();
293
+
294
+ // Draw progress
295
+ if (progress > 0) {
296
+ const progressWidth = Math.max(borderRadius * 2, progress * width);
297
+
298
+ // Add subtle glow effect
299
+ ctx.shadowBlur = 8;
300
+ ctx.shadowColor = options.progressColor;
301
+
302
+ ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';
303
+
304
+ // Create rounded rectangle for progress
305
+ ctx.beginPath();
306
+ ctx.moveTo(borderRadius, centerY - barHeight / 2);
307
+ ctx.lineTo(progressWidth - borderRadius, centerY - barHeight / 2);
308
+ ctx.arc(progressWidth - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
309
+ ctx.lineTo(borderRadius, centerY + barHeight / 2);
310
+ ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
311
+ ctx.closePath();
312
+ ctx.fill();
313
+
314
+ ctx.shadowBlur = 0;
315
+
316
+ // Draw progress handle/thumb
317
+ const handleRadius = 8;
318
+ const handleX = progressWidth;
319
+
320
+ // Handle shadow
321
+ ctx.shadowBlur = 4;
322
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
323
+ ctx.shadowOffsetY = 2;
324
+
325
+ // Handle circle
326
+ ctx.fillStyle = '#ffffff';
327
+ ctx.beginPath();
328
+ ctx.arc(handleX, centerY, handleRadius, 0, Math.PI * 2);
329
+ ctx.fill();
330
+
331
+ // Handle inner circle (for depth)
332
+ ctx.shadowBlur = 0;
333
+ ctx.shadowOffsetY = 0;
334
+ ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';
335
+ ctx.beginPath();
336
+ ctx.arc(handleX, centerY, handleRadius * 0.4, 0, Math.PI * 2);
337
+ ctx.fill();
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Map of style names to drawing functions
343
+ * 6 visually distinct styles including a simple seekbar
344
+ */
345
+ export const DRAWING_STYLES = {
346
+ 'bars': drawBars, // Classic vertical bars
347
+ 'mirror': drawMirror, // SoundCloud-style symmetrical
348
+ 'line': drawLine, // Smooth oscilloscope wave
349
+ 'blocks': drawBlocks, // LED meter segmented
350
+ 'dots': drawDots, // Circular points
351
+ 'seekbar': drawSeekbar // Simple progress bar (no waveform)
352
+ };
353
+
354
+ /**
355
+ * Main drawing function that delegates to appropriate style
356
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
357
+ * @param {HTMLCanvasElement} canvas - Canvas element
358
+ * @param {number[]} peaks - Waveform peak data
359
+ * @param {number} progress - Progress (0-1)
360
+ * @param {Object} options - Drawing options
361
+ */
362
+ export function draw(ctx, canvas, peaks, progress, options) {
363
+ const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;
364
+ drawFunc(ctx, canvas, peaks, progress, options);
365
+ }
package/src/index.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WaveformPlayer
3
+ *
4
+ * Modern audio player with waveform visualization
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ // Import the main class
10
+ import {WaveformPlayer} from './core.js';
11
+
12
+ // Auto-initialization for data attributes
13
+ function autoInit() {
14
+ if (typeof document === 'undefined') return;
15
+
16
+ const elements = document.querySelectorAll('[data-waveform-player]');
17
+
18
+ elements.forEach(element => {
19
+ if (element.dataset.waveformInitialized === 'true') return;
20
+
21
+ try {
22
+ new WaveformPlayer(element);
23
+ element.dataset.waveformInitialized = 'true';
24
+ } catch (error) {
25
+ console.error('Failed to initialize WaveformPlayer:', error, element);
26
+ }
27
+ });
28
+ }
29
+
30
+ // Initialize when DOM is ready
31
+ if (typeof document !== 'undefined') {
32
+ if (document.readyState === 'loading') {
33
+ document.addEventListener('DOMContentLoaded', autoInit);
34
+ } else {
35
+ autoInit();
36
+ }
37
+ }
38
+
39
+ // Add init method
40
+ WaveformPlayer.init = autoInit;
41
+
42
+ // For CDN/browser usage
43
+ if (typeof window !== 'undefined') {
44
+ window.WaveformPlayer = WaveformPlayer;
45
+ }
46
+
47
+ // Default export for ES modules
48
+ export default WaveformPlayer;
49
+
50
+ // Named exports
51
+ export {WaveformPlayer};
package/src/themes.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @module themes
3
+ * @description Color presets and default options for WaveformPlayer
4
+ */
5
+
6
+ /**
7
+ * Color presets - simple dark/light defaults that can be overridden
8
+ */
9
+ export const COLOR_PRESETS = {
10
+ dark: {
11
+ waveformColor: 'rgba(255, 255, 255, 0.3)',
12
+ progressColor: 'rgba(255, 255, 255, 0.9)',
13
+ buttonColor: 'rgba(255, 255, 255, 0.9)',
14
+ buttonHoverColor: 'rgba(255, 255, 255, 1)',
15
+ textColor: '#ffffff',
16
+ textSecondaryColor: 'rgba(255, 255, 255, 0.6)',
17
+ backgroundColor: 'rgba(255, 255, 255, 0.03)',
18
+ borderColor: 'rgba(255, 255, 255, 0.1)'
19
+ },
20
+ light: {
21
+ waveformColor: 'rgba(0, 0, 0, 0.2)',
22
+ progressColor: 'rgba(0, 0, 0, 0.8)',
23
+ buttonColor: 'rgba(0, 0, 0, 0.8)',
24
+ buttonHoverColor: 'rgba(0, 0, 0, 0.9)',
25
+ textColor: '#333333',
26
+ textSecondaryColor: 'rgba(0, 0, 0, 0.6)',
27
+ backgroundColor: 'rgba(0, 0, 0, 0.02)',
28
+ borderColor: 'rgba(0, 0, 0, 0.1)'
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Default player options
34
+ */
35
+ export const DEFAULT_OPTIONS = {
36
+ // Core settings
37
+ url: '',
38
+ height: 60,
39
+ samples: 200,
40
+
41
+ // Default waveform style
42
+ waveformStyle: 'mirror',
43
+ barWidth: 2,
44
+ barSpacing: 0,
45
+
46
+ // Color preset (dark/light or null for custom)
47
+ colorPreset: 'dark',
48
+
49
+ // Individual color overrides (null means use preset)
50
+ waveformColor: null,
51
+ progressColor: null,
52
+ buttonColor: null,
53
+ buttonHoverColor: null,
54
+ textColor: null,
55
+ textSecondaryColor: null,
56
+ backgroundColor: null,
57
+ borderColor: null,
58
+
59
+ // Features
60
+ autoplay: false,
61
+ showTime: true,
62
+ showHoverTime: false,
63
+ showBPM: false,
64
+ singlePlay: true,
65
+ playOnSeek: true,
66
+
67
+ // Content
68
+ title: null,
69
+ subtitle: null,
70
+
71
+ // Icons (SVG)
72
+ playIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',
73
+ pauseIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',
74
+
75
+ // Callbacks
76
+ onLoad: null,
77
+ onPlay: null,
78
+ onPause: null,
79
+ onEnd: null,
80
+ onError: null,
81
+ onTimeUpdate: null
82
+ };
83
+
84
+ /**
85
+ * Style defaults
86
+ */
87
+ export const STYLE_DEFAULTS = {
88
+ bars: { barWidth: 3, barSpacing: 1 },
89
+ mirror: { barWidth: 2, barSpacing: 0 },
90
+ line: { barWidth: 2, barSpacing: 0 },
91
+ blocks: { barWidth: 4, barSpacing: 2 },
92
+ dots: { barWidth: 3, barSpacing: 3 },
93
+ seekbar: { barWidth: 1, barSpacing: 0 }
94
+ };
package/src/utils.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @module utils
3
+ * @description Utility functions for WaveformPlayer
4
+ */
5
+
6
+ /**
7
+ * Parse data attributes from element
8
+ * @param {HTMLElement} element - Element with data attributes
9
+ * @returns {Object} Parsed options
10
+ */
11
+ export function parseDataAttributes(element) {
12
+ const options = {};
13
+
14
+ // Core attributes
15
+ if (element.dataset.url) options.url = element.dataset.url;
16
+ if (element.dataset.height) options.height = parseInt(element.dataset.height);
17
+ if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
18
+
19
+ // Waveform style attributes
20
+ if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
21
+ if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
22
+ if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
23
+
24
+ // Color preset
25
+ if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
26
+
27
+ // Individual color customization
28
+ if (element.dataset.waveformColor) options.waveformColor = element.dataset.waveformColor;
29
+ if (element.dataset.progressColor) options.progressColor = element.dataset.progressColor;
30
+ if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;
31
+ if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;
32
+ if (element.dataset.textColor) options.textColor = element.dataset.textColor;
33
+ if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;
34
+ if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;
35
+ if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;
36
+
37
+ // Legacy support for old attribute names
38
+ if (element.dataset.color) options.waveformColor = element.dataset.color;
39
+ if (element.dataset.theme) options.colorPreset = element.dataset.theme;
40
+
41
+ // Feature flags
42
+ if (element.dataset.autoplay) options.autoplay = element.dataset.autoplay === 'true';
43
+ if (element.dataset.showTime) options.showTime = element.dataset.showTime === 'true';
44
+ if (element.dataset.showHoverTime) options.showHoverTime = element.dataset.showHoverTime === 'true';
45
+ if (element.dataset.showBpm) options.showBPM = element.dataset.showBpm === 'true';
46
+ if (element.dataset.singlePlay) options.singlePlay = element.dataset.singlePlay === 'true';
47
+ if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === 'true';
48
+
49
+ // Content
50
+ if (element.dataset.title) options.title = element.dataset.title;
51
+ if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
52
+
53
+ // Waveform data
54
+ if (element.dataset.waveform) options.waveform = element.dataset.waveform;
55
+
56
+ return options;
57
+ }
58
+
59
+ /**
60
+ * Format time in MM:SS format
61
+ * @param {number} seconds - Time in seconds
62
+ * @returns {string} Formatted time
63
+ */
64
+ export function formatTime(seconds) {
65
+ if (!seconds || isNaN(seconds)) return '0:00';
66
+
67
+ const mins = Math.floor(seconds / 60);
68
+ const secs = Math.floor(seconds % 60);
69
+
70
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
71
+ }
72
+
73
+ /**
74
+ * Generate unique ID from URL
75
+ * @param {string} url - Audio URL
76
+ * @returns {string} Base64 encoded ID
77
+ */
78
+ export function generateId(url) {
79
+ const str = url || Math.random().toString();
80
+ return btoa(str.substring(0, 10)).replace(/[^a-zA-Z0-9]/g, '');
81
+ }
82
+
83
+ /**
84
+ * Extract title from URL
85
+ * @param {string} url - Audio URL
86
+ * @returns {string} Extracted title
87
+ */
88
+ export function extractTitleFromUrl(url) {
89
+ if (!url) return 'Audio';
90
+
91
+ const parts = url.split('/');
92
+ const filename = parts[parts.length - 1];
93
+ const name = filename.split('.')[0];
94
+
95
+ // Clean up common separators
96
+ return name
97
+ .replace(/[-_]/g, ' ')
98
+ .replace(/\b\w/g, l => l.toUpperCase());
99
+ }
100
+
101
+ /**
102
+ * Merge multiple option objects
103
+ * @param {...Object} sources - Option objects to merge
104
+ * @returns {Object} Merged options
105
+ */
106
+ export function mergeOptions(...sources) {
107
+ const result = {};
108
+
109
+ for (const source of sources) {
110
+ for (const key in source) {
111
+ if (source[key] !== null && source[key] !== undefined) {
112
+ result[key] = source[key];
113
+ }
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Debounce function
122
+ * @param {Function} func - Function to debounce
123
+ * @param {number} wait - Wait time in ms
124
+ * @returns {Function} Debounced function
125
+ */
126
+ export function debounce(func, wait) {
127
+ let timeout;
128
+
129
+ return function executedFunction(...args) {
130
+ const later = () => {
131
+ clearTimeout(timeout);
132
+ func(...args);
133
+ };
134
+
135
+ clearTimeout(timeout);
136
+ timeout = setTimeout(later, wait);
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Resample array data
142
+ * @param {number[]} data - Original data
143
+ * @param {number} targetLength - Target length
144
+ * @returns {number[]} Resampled data
145
+ */
146
+ export function resampleData(data, targetLength) {
147
+ if (data.length === targetLength) return data;
148
+ if (data.length === 0 || targetLength === 0) return [];
149
+
150
+ const result = [];
151
+
152
+ // If upsampling (target is larger than source)
153
+ if (targetLength > data.length) {
154
+ const ratio = (data.length - 1) / (targetLength - 1);
155
+
156
+ for (let i = 0; i < targetLength; i++) {
157
+ const index = i * ratio;
158
+ const lower = Math.floor(index);
159
+ const upper = Math.ceil(index);
160
+ const fraction = index - lower;
161
+
162
+ // Linear interpolation between samples
163
+ if (upper >= data.length) {
164
+ result.push(data[data.length - 1]);
165
+ } else if (lower === upper) {
166
+ result.push(data[lower]);
167
+ } else {
168
+ const value = data[lower] * (1 - fraction) + data[upper] * fraction;
169
+ result.push(value);
170
+ }
171
+ }
172
+ } else {
173
+ // Downsampling (target is smaller than source)
174
+ const bucketSize = data.length / targetLength;
175
+
176
+ for (let i = 0; i < targetLength; i++) {
177
+ const start = Math.floor(i * bucketSize);
178
+ const end = Math.floor((i + 1) * bucketSize);
179
+
180
+ // Find the maximum value in this bucket
181
+ let max = 0;
182
+ let count = 0;
183
+
184
+ for (let j = start; j <= end && j < data.length; j++) {
185
+ if (data[j] > max) {
186
+ max = data[j];
187
+ }
188
+ count++;
189
+ }
190
+
191
+ // If no samples were found in this bucket, use nearest neighbor
192
+ if (count === 0) {
193
+ const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);
194
+ max = data[nearestIndex];
195
+ }
196
+
197
+ result.push(max);
198
+ }
199
+ }
200
+
201
+ return result;
202
+ }