@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.
@@ -0,0 +1,1087 @@
1
+ (() => {
2
+ // src/utils.js
3
+ function parseDataAttributes(element) {
4
+ const options = {};
5
+ if (element.dataset.url) options.url = element.dataset.url;
6
+ if (element.dataset.height) options.height = parseInt(element.dataset.height);
7
+ if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
8
+ if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
9
+ if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
10
+ if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
11
+ if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
12
+ if (element.dataset.waveformColor) options.waveformColor = element.dataset.waveformColor;
13
+ if (element.dataset.progressColor) options.progressColor = element.dataset.progressColor;
14
+ if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;
15
+ if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;
16
+ if (element.dataset.textColor) options.textColor = element.dataset.textColor;
17
+ if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;
18
+ if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;
19
+ if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;
20
+ if (element.dataset.color) options.waveformColor = element.dataset.color;
21
+ if (element.dataset.theme) options.colorPreset = element.dataset.theme;
22
+ if (element.dataset.autoplay) options.autoplay = element.dataset.autoplay === "true";
23
+ if (element.dataset.showTime) options.showTime = element.dataset.showTime === "true";
24
+ if (element.dataset.showHoverTime) options.showHoverTime = element.dataset.showHoverTime === "true";
25
+ if (element.dataset.showBpm) options.showBPM = element.dataset.showBpm === "true";
26
+ if (element.dataset.singlePlay) options.singlePlay = element.dataset.singlePlay === "true";
27
+ if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === "true";
28
+ if (element.dataset.title) options.title = element.dataset.title;
29
+ if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
30
+ if (element.dataset.waveform) options.waveform = element.dataset.waveform;
31
+ return options;
32
+ }
33
+ function formatTime(seconds) {
34
+ if (!seconds || isNaN(seconds)) return "0:00";
35
+ const mins = Math.floor(seconds / 60);
36
+ const secs = Math.floor(seconds % 60);
37
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
38
+ }
39
+ function generateId(url) {
40
+ const str = url || Math.random().toString();
41
+ return btoa(str.substring(0, 10)).replace(/[^a-zA-Z0-9]/g, "");
42
+ }
43
+ function extractTitleFromUrl(url) {
44
+ if (!url) return "Audio";
45
+ const parts = url.split("/");
46
+ const filename = parts[parts.length - 1];
47
+ const name = filename.split(".")[0];
48
+ return name.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
49
+ }
50
+ function mergeOptions(...sources) {
51
+ const result = {};
52
+ for (const source of sources) {
53
+ for (const key in source) {
54
+ if (source[key] !== null && source[key] !== void 0) {
55
+ result[key] = source[key];
56
+ }
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ function debounce(func, wait) {
62
+ let timeout;
63
+ return function executedFunction(...args) {
64
+ const later = () => {
65
+ clearTimeout(timeout);
66
+ func(...args);
67
+ };
68
+ clearTimeout(timeout);
69
+ timeout = setTimeout(later, wait);
70
+ };
71
+ }
72
+ function resampleData(data, targetLength) {
73
+ if (data.length === targetLength) return data;
74
+ if (data.length === 0 || targetLength === 0) return [];
75
+ const result = [];
76
+ if (targetLength > data.length) {
77
+ const ratio = (data.length - 1) / (targetLength - 1);
78
+ for (let i = 0; i < targetLength; i++) {
79
+ const index = i * ratio;
80
+ const lower = Math.floor(index);
81
+ const upper = Math.ceil(index);
82
+ const fraction = index - lower;
83
+ if (upper >= data.length) {
84
+ result.push(data[data.length - 1]);
85
+ } else if (lower === upper) {
86
+ result.push(data[lower]);
87
+ } else {
88
+ const value = data[lower] * (1 - fraction) + data[upper] * fraction;
89
+ result.push(value);
90
+ }
91
+ }
92
+ } else {
93
+ const bucketSize = data.length / targetLength;
94
+ for (let i = 0; i < targetLength; i++) {
95
+ const start = Math.floor(i * bucketSize);
96
+ const end = Math.floor((i + 1) * bucketSize);
97
+ let max = 0;
98
+ let count = 0;
99
+ for (let j = start; j <= end && j < data.length; j++) {
100
+ if (data[j] > max) {
101
+ max = data[j];
102
+ }
103
+ count++;
104
+ }
105
+ if (count === 0) {
106
+ const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);
107
+ max = data[nearestIndex];
108
+ }
109
+ result.push(max);
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+
115
+ // src/drawing.js
116
+ function drawBars(ctx, canvas, peaks, progress, options) {
117
+ const dpr = window.devicePixelRatio || 1;
118
+ const barWidth = options.barWidth * dpr;
119
+ const barSpacing = options.barSpacing * dpr;
120
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
121
+ const resampledPeaks = resampleData(peaks, barCount);
122
+ const height = canvas.height;
123
+ const progressWidth = progress * canvas.width;
124
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
125
+ for (let i = 0; i < resampledPeaks.length; i++) {
126
+ const x = i * (barWidth + barSpacing);
127
+ if (x + barWidth > canvas.width) break;
128
+ const peakHeight = resampledPeaks[i] * height * 0.9;
129
+ const y = height - peakHeight;
130
+ ctx.fillStyle = options.color;
131
+ ctx.fillRect(x, y, barWidth, peakHeight);
132
+ }
133
+ ctx.save();
134
+ ctx.beginPath();
135
+ ctx.rect(0, 0, progressWidth, height);
136
+ ctx.clip();
137
+ for (let i = 0; i < resampledPeaks.length; i++) {
138
+ const x = i * (barWidth + barSpacing);
139
+ if (x > progressWidth) break;
140
+ const peakHeight = resampledPeaks[i] * height * 0.9;
141
+ const y = height - peakHeight;
142
+ ctx.fillStyle = options.progressColor;
143
+ ctx.fillRect(x, y, barWidth, peakHeight);
144
+ }
145
+ ctx.restore();
146
+ }
147
+ function drawMirror(ctx, canvas, peaks, progress, options) {
148
+ const dpr = window.devicePixelRatio || 1;
149
+ const barWidth = options.barWidth * dpr;
150
+ const barSpacing = options.barSpacing * dpr;
151
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
152
+ const resampledPeaks = resampleData(peaks, barCount);
153
+ const height = canvas.height;
154
+ const centerY = height / 2;
155
+ const progressWidth = progress * canvas.width;
156
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
157
+ for (let i = 0; i < resampledPeaks.length; i++) {
158
+ const x = i * (barWidth + barSpacing);
159
+ if (x + barWidth > canvas.width) break;
160
+ const peakHeight = resampledPeaks[i] * height * 0.45;
161
+ ctx.fillStyle = options.color;
162
+ ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
163
+ ctx.fillRect(x, centerY, barWidth, peakHeight);
164
+ }
165
+ ctx.save();
166
+ ctx.beginPath();
167
+ ctx.rect(0, 0, progressWidth, height);
168
+ ctx.clip();
169
+ for (let i = 0; i < resampledPeaks.length; i++) {
170
+ const x = i * (barWidth + barSpacing);
171
+ if (x > progressWidth) break;
172
+ const peakHeight = resampledPeaks[i] * height * 0.45;
173
+ ctx.fillStyle = options.progressColor;
174
+ ctx.fillRect(x, centerY - peakHeight, barWidth, peakHeight);
175
+ ctx.fillRect(x, centerY, barWidth, peakHeight);
176
+ }
177
+ ctx.restore();
178
+ }
179
+ function drawLine(ctx, canvas, peaks, progress, options) {
180
+ const width = canvas.width;
181
+ const height = canvas.height;
182
+ const centerY = height / 2;
183
+ const amplitude = height * 0.35;
184
+ ctx.clearRect(0, 0, width, height);
185
+ const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {
186
+ if (addGlow) {
187
+ ctx.shadowBlur = 12;
188
+ ctx.shadowColor = color;
189
+ }
190
+ ctx.strokeStyle = color;
191
+ ctx.lineWidth = lineWidth;
192
+ ctx.lineCap = "round";
193
+ ctx.lineJoin = "round";
194
+ ctx.beginPath();
195
+ ctx.moveTo(0, centerY);
196
+ const points = [];
197
+ const samples = Math.floor(peaks.length * endProgress);
198
+ for (let i = 0; i < samples; i++) {
199
+ const x = i / (peaks.length - 1) * width;
200
+ const peakValue = peaks[i];
201
+ const waveOffset = Math.sin(i * 0.1) * peakValue;
202
+ const y = centerY + waveOffset * amplitude;
203
+ points.push({ x, y });
204
+ }
205
+ for (let i = 0; i < points.length - 1; i++) {
206
+ const cp1x = points[i].x + (points[i + 1].x - points[i].x) * 0.5;
207
+ const cp1y = points[i].y;
208
+ const cp2x = points[i + 1].x - (points[i + 1].x - points[i].x) * 0.5;
209
+ const cp2y = points[i + 1].y;
210
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, points[i + 1].x, points[i + 1].y);
211
+ }
212
+ ctx.stroke();
213
+ if (addGlow) {
214
+ ctx.shadowBlur = 0;
215
+ }
216
+ };
217
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.03)";
218
+ ctx.lineWidth = 0.5;
219
+ ctx.beginPath();
220
+ ctx.moveTo(0, centerY);
221
+ ctx.lineTo(width, centerY);
222
+ ctx.stroke();
223
+ for (let i = 0; i <= 10; i++) {
224
+ const x = width / 10 * i;
225
+ ctx.beginPath();
226
+ ctx.moveTo(x, 0);
227
+ ctx.lineTo(x, height);
228
+ ctx.stroke();
229
+ }
230
+ drawCurve(options.color, 2, 1, false);
231
+ if (progress > 0) {
232
+ drawCurve(options.progressColor, 3, progress, true);
233
+ }
234
+ }
235
+ function drawBlocks(ctx, canvas, peaks, progress, options) {
236
+ const dpr = window.devicePixelRatio || 1;
237
+ const barWidth = (options.barWidth || 3) * dpr;
238
+ const barSpacing = (options.barSpacing || 1) * dpr;
239
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
240
+ const resampledPeaks = resampleData(peaks, barCount);
241
+ const height = canvas.height;
242
+ const blockSize = 4 * dpr;
243
+ const blockGap = 2 * dpr;
244
+ const progressWidth = progress * canvas.width;
245
+ const centerY = height / 2;
246
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
247
+ for (let i = 0; i < resampledPeaks.length; i++) {
248
+ const x = i * (barWidth + barSpacing);
249
+ if (x + barWidth > canvas.width) break;
250
+ const peakHeight = resampledPeaks[i] * height * 0.9;
251
+ const blockCount = Math.floor(peakHeight / (blockSize + blockGap));
252
+ ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
253
+ for (let j = 0; j < blockCount; j++) {
254
+ const blockOffset = j * (blockSize + blockGap);
255
+ ctx.fillRect(x, centerY - blockOffset - blockSize, barWidth, blockSize);
256
+ if (j > 0) {
257
+ ctx.fillRect(x, centerY + blockOffset, barWidth, blockSize);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ function drawDots(ctx, canvas, peaks, progress, options) {
263
+ const dpr = window.devicePixelRatio || 1;
264
+ const barWidth = (options.barWidth || 2) * dpr;
265
+ const barSpacing = (options.barSpacing || 3) * dpr;
266
+ const barCount = Math.floor(canvas.width / (barWidth + barSpacing));
267
+ const resampledPeaks = resampleData(peaks, barCount);
268
+ const height = canvas.height;
269
+ const dotRadius = Math.max(1.5 * dpr, barWidth / 2);
270
+ const progressWidth = progress * canvas.width;
271
+ const centerY = height / 2;
272
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
273
+ for (let i = 0; i < resampledPeaks.length; i++) {
274
+ const x = i * (barWidth + barSpacing) + barWidth / 2;
275
+ if (x > canvas.width) break;
276
+ const peakHeight = resampledPeaks[i] * height * 0.9;
277
+ ctx.fillStyle = x < progressWidth ? options.progressColor : options.color;
278
+ ctx.beginPath();
279
+ ctx.arc(x, centerY - peakHeight / 2, dotRadius, 0, Math.PI * 2);
280
+ ctx.fill();
281
+ ctx.beginPath();
282
+ ctx.arc(x, centerY + peakHeight / 2, dotRadius, 0, Math.PI * 2);
283
+ ctx.fill();
284
+ }
285
+ }
286
+ function drawSeekbar(ctx, canvas, peaks, progress, options) {
287
+ const width = canvas.width;
288
+ const height = canvas.height;
289
+ const centerY = height / 2;
290
+ const barHeight = 4;
291
+ const borderRadius = barHeight / 2;
292
+ ctx.clearRect(0, 0, width, height);
293
+ ctx.fillStyle = options.color || "rgba(255, 255, 255, 0.2)";
294
+ ctx.beginPath();
295
+ ctx.moveTo(borderRadius, centerY - barHeight / 2);
296
+ ctx.lineTo(width - borderRadius, centerY - barHeight / 2);
297
+ ctx.arc(width - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
298
+ ctx.lineTo(borderRadius, centerY + barHeight / 2);
299
+ ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
300
+ ctx.closePath();
301
+ ctx.fill();
302
+ if (progress > 0) {
303
+ const progressWidth = Math.max(borderRadius * 2, progress * width);
304
+ ctx.shadowBlur = 8;
305
+ ctx.shadowColor = options.progressColor;
306
+ ctx.fillStyle = options.progressColor || "rgba(255, 255, 255, 0.9)";
307
+ ctx.beginPath();
308
+ ctx.moveTo(borderRadius, centerY - barHeight / 2);
309
+ ctx.lineTo(progressWidth - borderRadius, centerY - barHeight / 2);
310
+ ctx.arc(progressWidth - borderRadius, centerY, barHeight / 2, -Math.PI / 2, Math.PI / 2);
311
+ ctx.lineTo(borderRadius, centerY + barHeight / 2);
312
+ ctx.arc(borderRadius, centerY, barHeight / 2, Math.PI / 2, -Math.PI / 2);
313
+ ctx.closePath();
314
+ ctx.fill();
315
+ ctx.shadowBlur = 0;
316
+ const handleRadius = 8;
317
+ const handleX = progressWidth;
318
+ ctx.shadowBlur = 4;
319
+ ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
320
+ ctx.shadowOffsetY = 2;
321
+ ctx.fillStyle = "#ffffff";
322
+ ctx.beginPath();
323
+ ctx.arc(handleX, centerY, handleRadius, 0, Math.PI * 2);
324
+ ctx.fill();
325
+ ctx.shadowBlur = 0;
326
+ ctx.shadowOffsetY = 0;
327
+ ctx.fillStyle = options.progressColor || "rgba(255, 255, 255, 0.9)";
328
+ ctx.beginPath();
329
+ ctx.arc(handleX, centerY, handleRadius * 0.4, 0, Math.PI * 2);
330
+ ctx.fill();
331
+ }
332
+ }
333
+ var DRAWING_STYLES = {
334
+ "bars": drawBars,
335
+ // Classic vertical bars
336
+ "mirror": drawMirror,
337
+ // SoundCloud-style symmetrical
338
+ "line": drawLine,
339
+ // Smooth oscilloscope wave
340
+ "blocks": drawBlocks,
341
+ // LED meter segmented
342
+ "dots": drawDots,
343
+ // Circular points
344
+ "seekbar": drawSeekbar
345
+ // Simple progress bar (no waveform)
346
+ };
347
+ function draw(ctx, canvas, peaks, progress, options) {
348
+ const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;
349
+ drawFunc(ctx, canvas, peaks, progress, options);
350
+ }
351
+
352
+ // src/bpm.js
353
+ function detectBPM(buffer) {
354
+ try {
355
+ const channelData = buffer.getChannelData(0);
356
+ const sampleRate = buffer.sampleRate;
357
+ const onsets = detectOnsets(channelData, sampleRate);
358
+ if (onsets.length < 2) return 120;
359
+ const intervals = [];
360
+ for (let i = 1; i < onsets.length; i++) {
361
+ intervals.push((onsets[i] - onsets[i - 1]) / sampleRate);
362
+ }
363
+ const tempoGroups = {};
364
+ intervals.forEach((interval) => {
365
+ const tempo = 60 / interval;
366
+ const bucket = Math.round(tempo / 3) * 3;
367
+ if (bucket > 60 && bucket < 200) {
368
+ tempoGroups[bucket] = (tempoGroups[bucket] || 0) + 1;
369
+ }
370
+ });
371
+ let maxCount = 0;
372
+ let detectedBPM = 120;
373
+ for (const [tempo, count] of Object.entries(tempoGroups)) {
374
+ if (count > maxCount) {
375
+ maxCount = count;
376
+ detectedBPM = parseInt(tempo);
377
+ }
378
+ }
379
+ if (detectedBPM < 70 && tempoGroups[detectedBPM * 2]) {
380
+ detectedBPM *= 2;
381
+ } else if (detectedBPM > 160 && tempoGroups[Math.round(detectedBPM / 2)]) {
382
+ detectedBPM = Math.round(detectedBPM / 2);
383
+ }
384
+ return detectedBPM - 1;
385
+ } catch (e) {
386
+ console.warn("BPM detection failed:", e);
387
+ return null;
388
+ }
389
+ }
390
+ function detectOnsets(channelData, sampleRate) {
391
+ const windowSize = 2048;
392
+ const hopSize = windowSize / 2;
393
+ const onsets = [];
394
+ let previousEnergy = 0;
395
+ for (let i = 0; i < channelData.length - windowSize; i += hopSize) {
396
+ let energy = 0;
397
+ for (let j = i; j < i + windowSize; j++) {
398
+ energy += channelData[j] * channelData[j];
399
+ }
400
+ energy = energy / windowSize;
401
+ const energyDiff = energy - previousEnergy;
402
+ const threshold = previousEnergy * 1.8 + 0.01;
403
+ if (energyDiff > threshold && energy > 0.01) {
404
+ const lastOnset = onsets[onsets.length - 1] || 0;
405
+ const minDistance = sampleRate * 0.15;
406
+ if (i - lastOnset > minDistance) {
407
+ onsets.push(i);
408
+ }
409
+ }
410
+ previousEnergy = energy * 0.8 + previousEnergy * 0.2;
411
+ }
412
+ return onsets;
413
+ }
414
+
415
+ // src/audio.js
416
+ function extractPeaks(buffer, samples = 200) {
417
+ const sampleSize = buffer.length / samples;
418
+ const sampleStep = ~~(sampleSize / 10) || 1;
419
+ const channels = buffer.numberOfChannels;
420
+ const peaks = [];
421
+ for (let c = 0; c < channels; c++) {
422
+ const chan = buffer.getChannelData(c);
423
+ for (let i = 0; i < samples; i++) {
424
+ const start = ~~(i * sampleSize);
425
+ const end = ~~(start + sampleSize);
426
+ let min = 0;
427
+ let max = 0;
428
+ for (let j = start; j < end; j += sampleStep) {
429
+ const value = chan[j];
430
+ if (value > max) max = value;
431
+ if (value < min) min = value;
432
+ }
433
+ const peak = Math.max(Math.abs(max), Math.abs(min));
434
+ if (c === 0 || peak > peaks[i]) {
435
+ peaks[i] = peak;
436
+ }
437
+ }
438
+ }
439
+ const maxPeak = Math.max(...peaks);
440
+ return maxPeak > 0 ? peaks.map((peak) => peak / maxPeak) : peaks;
441
+ }
442
+ async function generateWaveform(url, samples = 200, includeBPM = false) {
443
+ const response = await fetch(url);
444
+ if (!response.ok) {
445
+ throw new Error(`HTTP error! status: ${response.status}`);
446
+ }
447
+ const arrayBuffer = await response.arrayBuffer();
448
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
449
+ const audioContext = new AudioContextClass();
450
+ try {
451
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
452
+ const peaks = extractPeaks(audioBuffer, samples);
453
+ const result = { peaks };
454
+ if (includeBPM) {
455
+ result.bpm = detectBPM(audioBuffer);
456
+ }
457
+ return result;
458
+ } finally {
459
+ await audioContext.close();
460
+ }
461
+ }
462
+ function generatePlaceholderWaveform(samples = 200) {
463
+ const data = [];
464
+ for (let i = 0; i < samples; i++) {
465
+ const base = Math.random() * 0.5 + 0.3;
466
+ const variation = Math.sin(i / samples * Math.PI * 4) * 0.2;
467
+ data.push(Math.max(0.1, Math.min(1, base + variation)));
468
+ }
469
+ return data;
470
+ }
471
+
472
+ // src/themes.js
473
+ var DEFAULT_OPTIONS = {
474
+ // Core settings
475
+ url: "",
476
+ height: 60,
477
+ samples: 200,
478
+ // Default waveform style
479
+ waveformStyle: "mirror",
480
+ barWidth: 2,
481
+ barSpacing: 0,
482
+ // Color preset (dark/light or null for custom)
483
+ colorPreset: "dark",
484
+ // Individual color overrides (null means use preset)
485
+ waveformColor: null,
486
+ progressColor: null,
487
+ buttonColor: null,
488
+ buttonHoverColor: null,
489
+ textColor: null,
490
+ textSecondaryColor: null,
491
+ backgroundColor: null,
492
+ borderColor: null,
493
+ // Features
494
+ autoplay: false,
495
+ showTime: true,
496
+ showHoverTime: false,
497
+ showBPM: false,
498
+ singlePlay: true,
499
+ playOnSeek: true,
500
+ // Content
501
+ title: null,
502
+ subtitle: null,
503
+ // Icons (SVG)
504
+ playIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',
505
+ pauseIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',
506
+ // Callbacks
507
+ onLoad: null,
508
+ onPlay: null,
509
+ onPause: null,
510
+ onEnd: null,
511
+ onError: null,
512
+ onTimeUpdate: null
513
+ };
514
+ var STYLE_DEFAULTS = {
515
+ bars: { barWidth: 3, barSpacing: 1 },
516
+ mirror: { barWidth: 2, barSpacing: 0 },
517
+ line: { barWidth: 2, barSpacing: 0 },
518
+ blocks: { barWidth: 4, barSpacing: 2 },
519
+ dots: { barWidth: 3, barSpacing: 3 },
520
+ seekbar: { barWidth: 1, barSpacing: 0 }
521
+ };
522
+
523
+ // src/core.js
524
+ var WaveformPlayer = class _WaveformPlayer {
525
+ /** @type {Map<string, WaveformPlayer>} */
526
+ static instances = /* @__PURE__ */ new Map();
527
+ /** @type {WaveformPlayer|null} */
528
+ static currentlyPlaying = null;
529
+ /**
530
+ * Create a new WaveformPlayer instance
531
+ * @param {string|HTMLElement} container - Container element or selector
532
+ * @param {Object} options - Player options
533
+ */
534
+ constructor(container, options = {}) {
535
+ this.container = typeof container === "string" ? document.querySelector(container) : container;
536
+ if (!this.container) {
537
+ throw new Error("WaveformPlayer: Container element not found");
538
+ }
539
+ const dataOptions = parseDataAttributes(this.container);
540
+ this.options = mergeOptions(DEFAULT_OPTIONS, dataOptions, options);
541
+ const styleDefaults = STYLE_DEFAULTS[this.options.waveformStyle];
542
+ if (styleDefaults) {
543
+ if (dataOptions.barWidth === void 0 && options.barWidth === void 0) {
544
+ this.options.barWidth = styleDefaults.barWidth;
545
+ }
546
+ if (dataOptions.barSpacing === void 0 && options.barSpacing === void 0) {
547
+ this.options.barSpacing = styleDefaults.barSpacing;
548
+ }
549
+ }
550
+ this.options.waveformColor = this.options.waveformColor || "rgba(255, 255, 255, 0.3)";
551
+ this.options.progressColor = this.options.progressColor || "rgba(255, 255, 255, 0.9)";
552
+ this.options.buttonColor = this.options.buttonColor || "rgba(255, 255, 255, 0.9)";
553
+ this.options.textColor = this.options.textColor || "#ffffff";
554
+ this.options.textSecondaryColor = this.options.textSecondaryColor || "rgba(255, 255, 255, 0.6)";
555
+ this.audio = null;
556
+ this.canvas = null;
557
+ this.ctx = null;
558
+ this.waveformData = [];
559
+ this.progress = 0;
560
+ this.isPlaying = false;
561
+ this.isLoading = false;
562
+ this.hasError = false;
563
+ this.updateTimer = null;
564
+ this.resizeObserver = null;
565
+ this.id = this.container.id || generateId(this.options.url);
566
+ _WaveformPlayer.instances.set(this.id, this);
567
+ this.init();
568
+ }
569
+ /**
570
+ * Initialize the player
571
+ * @private
572
+ */
573
+ init() {
574
+ this.createDOM();
575
+ this.createAudio();
576
+ this.bindEvents();
577
+ this.setupResizeObserver();
578
+ requestAnimationFrame(() => {
579
+ this.resizeCanvas();
580
+ if (this.options.url) {
581
+ this.load(this.options.url).then(() => {
582
+ if (this.options.autoplay) {
583
+ this.play();
584
+ }
585
+ }).catch((error) => {
586
+ console.error("Failed to load audio:", error);
587
+ });
588
+ }
589
+ });
590
+ }
591
+ /**
592
+ * Create DOM elements
593
+ * @private
594
+ */
595
+ createDOM() {
596
+ this.container.innerHTML = "";
597
+ this.container.className = "waveform-player";
598
+ this.container.innerHTML = `
599
+ <div class="waveform-player-inner">
600
+ <div class="waveform-body">
601
+ <div class="waveform-track">
602
+ <button class="waveform-btn" aria-label="Play/Pause" style="
603
+ border-color: ${this.options.buttonColor};
604
+ color: ${this.options.buttonColor};
605
+ ">
606
+ <span class="waveform-icon-play">${this.options.playIcon}</span>
607
+ <span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
608
+ </button>
609
+
610
+ <div class="waveform-container">
611
+ <canvas></canvas>
612
+ <div class="waveform-loading" style="display:none;"></div>
613
+ <div class="waveform-error" style="display:none;">
614
+ <span class="waveform-error-text">Unable to load audio</span>
615
+ </div>
616
+ </div>
617
+ </div>
618
+
619
+ <div class="waveform-info">
620
+ <div class="waveform-text">
621
+ <span class="waveform-title" style="color: ${this.options.textColor};"></span>
622
+ ${this.options.subtitle ? `<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>` : ""}
623
+ </div>
624
+ <div style="display: flex; align-items: center; gap: 1rem;">
625
+ ${this.options.showBPM ? `
626
+ <span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
627
+ <span class="bpm-value">--</span> BPM
628
+ </span>
629
+ ` : ""}
630
+ ${this.options.showTime ? `
631
+ <span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
632
+ <span class="time-current">0:00</span> / <span class="time-total">0:00</span>
633
+ </span>
634
+ ` : ""}
635
+ </div>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ `;
640
+ this.playBtn = this.container.querySelector(".waveform-btn");
641
+ this.canvas = this.container.querySelector("canvas");
642
+ this.ctx = this.canvas.getContext("2d");
643
+ this.titleEl = this.container.querySelector(".waveform-title");
644
+ this.subtitleEl = this.container.querySelector(".waveform-subtitle");
645
+ this.currentTimeEl = this.container.querySelector(".time-current");
646
+ this.totalTimeEl = this.container.querySelector(".time-total");
647
+ this.bpmEl = this.container.querySelector(".waveform-bpm");
648
+ this.bpmValueEl = this.container.querySelector(".bpm-value");
649
+ this.loadingEl = this.container.querySelector(".waveform-loading");
650
+ this.errorEl = this.container.querySelector(".waveform-error");
651
+ this.resizeCanvas();
652
+ }
653
+ /**
654
+ * Create audio element
655
+ * @private
656
+ */
657
+ createAudio() {
658
+ this.audio = new Audio();
659
+ this.audio.preload = "metadata";
660
+ this.audio.crossOrigin = "anonymous";
661
+ }
662
+ /**
663
+ * Bind event listeners
664
+ * @private
665
+ */
666
+ bindEvents() {
667
+ this.playBtn.addEventListener("click", () => this.togglePlay());
668
+ this.audio.addEventListener("loadstart", () => this.setLoading(true));
669
+ this.audio.addEventListener("loadedmetadata", () => this.onMetadataLoaded());
670
+ this.audio.addEventListener("canplay", () => this.setLoading(false));
671
+ this.audio.addEventListener("play", () => this.onPlay());
672
+ this.audio.addEventListener("pause", () => this.onPause());
673
+ this.audio.addEventListener("ended", () => this.onEnded());
674
+ this.audio.addEventListener("error", (e) => this.onError(e));
675
+ this.canvas.addEventListener("click", (e) => this.handleCanvasClick(e));
676
+ window.addEventListener("resize", debounce(() => this.resizeCanvas(), 100));
677
+ }
678
+ /**
679
+ * Setup resize observer
680
+ * @private
681
+ */
682
+ setupResizeObserver() {
683
+ if ("ResizeObserver" in window) {
684
+ this.resizeObserver = new ResizeObserver(() => {
685
+ this.resizeCanvas();
686
+ });
687
+ if (this.canvas?.parentElement) {
688
+ this.resizeObserver.observe(this.canvas.parentElement);
689
+ }
690
+ }
691
+ }
692
+ /**
693
+ * Load audio file
694
+ * @param {string} url - Audio URL
695
+ * @returns {Promise<void>}
696
+ */
697
+ async load(url) {
698
+ try {
699
+ this.setLoading(true);
700
+ this.progress = 0;
701
+ this.hasError = false;
702
+ this.audio.src = url;
703
+ await new Promise((resolve, reject) => {
704
+ const metadataHandler = () => {
705
+ this.audio.removeEventListener("loadedmetadata", metadataHandler);
706
+ this.audio.removeEventListener("error", errorHandler);
707
+ resolve();
708
+ };
709
+ const errorHandler = (e) => {
710
+ this.audio.removeEventListener("loadedmetadata", metadataHandler);
711
+ this.audio.removeEventListener("error", errorHandler);
712
+ reject(e);
713
+ };
714
+ this.audio.addEventListener("loadedmetadata", metadataHandler);
715
+ this.audio.addEventListener("error", errorHandler);
716
+ });
717
+ const title = this.options.title || extractTitleFromUrl(url);
718
+ if (this.titleEl) {
719
+ this.titleEl.textContent = title;
720
+ }
721
+ if (this.options.waveform) {
722
+ this.setWaveformData(this.options.waveform);
723
+ } else {
724
+ try {
725
+ const result = await generateWaveform(url, this.options.samples, this.options.showBPM);
726
+ this.waveformData = result.peaks;
727
+ if (result.bpm) {
728
+ this.detectedBPM = result.bpm;
729
+ this.updateBPMDisplay();
730
+ }
731
+ } catch (error) {
732
+ console.warn("Using placeholder waveform:", error);
733
+ this.waveformData = generatePlaceholderWaveform(this.options.samples);
734
+ }
735
+ }
736
+ this.drawWaveform();
737
+ if (this.options.onLoad) {
738
+ this.options.onLoad(this);
739
+ }
740
+ } catch (error) {
741
+ console.error("Failed to load audio:", error);
742
+ this.onError(error);
743
+ } finally {
744
+ this.setLoading(false);
745
+ }
746
+ }
747
+ /**
748
+ * Set waveform data
749
+ * @private
750
+ */
751
+ setWaveformData(data) {
752
+ if (typeof data === "string") {
753
+ try {
754
+ const parsed = JSON.parse(data);
755
+ this.waveformData = Array.isArray(parsed) ? parsed : [];
756
+ } catch {
757
+ this.waveformData = data.split(",").map(Number);
758
+ }
759
+ } else {
760
+ this.waveformData = Array.isArray(data) ? data : [];
761
+ }
762
+ this.drawWaveform();
763
+ }
764
+ /**
765
+ * Draw waveform
766
+ * @private
767
+ */
768
+ drawWaveform() {
769
+ if (!this.ctx || this.waveformData.length === 0) return;
770
+ draw(this.ctx, this.canvas, this.waveformData, this.progress, {
771
+ ...this.options,
772
+ waveformStyle: this.options.waveformStyle || "bars",
773
+ color: this.options.waveformColor,
774
+ progressColor: this.options.progressColor
775
+ });
776
+ }
777
+ /**
778
+ * Resize canvas
779
+ * @private
780
+ */
781
+ resizeCanvas() {
782
+ const dpr = window.devicePixelRatio || 1;
783
+ const rect = this.canvas.getBoundingClientRect();
784
+ this.canvas.width = rect.width * dpr;
785
+ this.canvas.height = this.options.height * dpr;
786
+ this.canvas.style.height = this.options.height + "px";
787
+ this.canvas.parentElement.style.height = this.options.height + "px";
788
+ this.drawWaveform();
789
+ }
790
+ /**
791
+ * Handle canvas click
792
+ * @private
793
+ */
794
+ handleCanvasClick(event) {
795
+ if (!this.audio.duration) return;
796
+ const rect = this.canvas.getBoundingClientRect();
797
+ const x = event.clientX - rect.left;
798
+ const targetPercent = Math.max(0, Math.min(1, x / rect.width));
799
+ this.seekToPercent(targetPercent);
800
+ }
801
+ /**
802
+ * Set loading state
803
+ * @private
804
+ */
805
+ setLoading(loading) {
806
+ this.isLoading = loading;
807
+ if (this.loadingEl) {
808
+ this.loadingEl.style.display = loading ? "block" : "none";
809
+ }
810
+ }
811
+ /**
812
+ * Handle metadata loaded
813
+ * @private
814
+ */
815
+ onMetadataLoaded() {
816
+ if (this.totalTimeEl) {
817
+ this.totalTimeEl.textContent = formatTime(this.audio.duration);
818
+ }
819
+ }
820
+ /**
821
+ * Handle play event
822
+ * @private
823
+ */
824
+ onPlay() {
825
+ this.isPlaying = true;
826
+ this.playBtn.classList.add("playing");
827
+ const playIcon = this.playBtn.querySelector(".waveform-icon-play");
828
+ const pauseIcon = this.playBtn.querySelector(".waveform-icon-pause");
829
+ if (playIcon) playIcon.style.display = "none";
830
+ if (pauseIcon) pauseIcon.style.display = "flex";
831
+ this.startSmoothUpdate();
832
+ if (this.options.onPlay) {
833
+ this.options.onPlay(this);
834
+ }
835
+ }
836
+ /**
837
+ * Handle pause event
838
+ * @private
839
+ */
840
+ onPause() {
841
+ this.isPlaying = false;
842
+ this.playBtn.classList.remove("playing");
843
+ const playIcon = this.playBtn.querySelector(".waveform-icon-play");
844
+ const pauseIcon = this.playBtn.querySelector(".waveform-icon-pause");
845
+ if (playIcon) playIcon.style.display = "flex";
846
+ if (pauseIcon) pauseIcon.style.display = "none";
847
+ this.stopSmoothUpdate();
848
+ if (this.options.onPause) {
849
+ this.options.onPause(this);
850
+ }
851
+ }
852
+ /**
853
+ * Handle ended event
854
+ * @private
855
+ */
856
+ onEnded() {
857
+ this.progress = 0;
858
+ this.audio.currentTime = 0;
859
+ this.drawWaveform();
860
+ if (this.currentTimeEl) {
861
+ this.currentTimeEl.textContent = "0:00";
862
+ }
863
+ this.onPause();
864
+ if (this.options.onEnd) {
865
+ this.options.onEnd(this);
866
+ }
867
+ }
868
+ /**
869
+ * Handle error event
870
+ * @private
871
+ */
872
+ onError(error) {
873
+ console.error("Audio error:", error);
874
+ this.hasError = true;
875
+ this.setLoading(false);
876
+ if (this.errorEl) {
877
+ this.errorEl.style.display = "flex";
878
+ }
879
+ if (this.canvas) {
880
+ this.canvas.style.opacity = "0.2";
881
+ }
882
+ if (this.playBtn) {
883
+ this.playBtn.disabled = true;
884
+ }
885
+ if (this.options.onError) {
886
+ this.options.onError(error, this);
887
+ }
888
+ }
889
+ /**
890
+ * Start smooth update animation
891
+ * @private
892
+ */
893
+ startSmoothUpdate() {
894
+ this.stopSmoothUpdate();
895
+ const update = () => {
896
+ if (this.isPlaying && this.audio.duration) {
897
+ this.updateProgress();
898
+ this.updateTimer = requestAnimationFrame(update);
899
+ }
900
+ };
901
+ this.updateTimer = requestAnimationFrame(update);
902
+ }
903
+ /**
904
+ * Stop smooth update animation
905
+ * @private
906
+ */
907
+ stopSmoothUpdate() {
908
+ if (this.updateTimer) {
909
+ cancelAnimationFrame(this.updateTimer);
910
+ this.updateTimer = null;
911
+ }
912
+ }
913
+ /**
914
+ * Update progress
915
+ * @private
916
+ */
917
+ updateProgress() {
918
+ if (!this.audio.duration) return;
919
+ const newProgress = this.audio.currentTime / this.audio.duration;
920
+ if (Math.abs(newProgress - this.progress) > 1e-3) {
921
+ this.progress = newProgress;
922
+ this.drawWaveform();
923
+ }
924
+ if (this.currentTimeEl) {
925
+ this.currentTimeEl.textContent = formatTime(this.audio.currentTime);
926
+ }
927
+ if (this.options.onTimeUpdate) {
928
+ this.options.onTimeUpdate(this.audio.currentTime, this.audio.duration, this);
929
+ }
930
+ }
931
+ /**
932
+ * Update BPM display
933
+ * @private
934
+ */
935
+ updateBPMDisplay() {
936
+ if (this.bpmEl && this.bpmValueEl && this.detectedBPM) {
937
+ this.bpmValueEl.textContent = Math.round(this.detectedBPM);
938
+ this.bpmEl.style.display = "inline-flex";
939
+ }
940
+ }
941
+ // ============================================
942
+ // Public API
943
+ // ============================================
944
+ /**
945
+ * Play audio
946
+ */
947
+ play() {
948
+ if (this.options.singlePlay && _WaveformPlayer.currentlyPlaying && _WaveformPlayer.currentlyPlaying !== this) {
949
+ _WaveformPlayer.currentlyPlaying.pause();
950
+ }
951
+ _WaveformPlayer.currentlyPlaying = this;
952
+ this.audio.play();
953
+ }
954
+ /**
955
+ * Pause audio
956
+ */
957
+ pause() {
958
+ if (_WaveformPlayer.currentlyPlaying === this) {
959
+ _WaveformPlayer.currentlyPlaying = null;
960
+ }
961
+ this.audio.pause();
962
+ }
963
+ /**
964
+ * Toggle play/pause
965
+ */
966
+ togglePlay() {
967
+ if (this.isPlaying) {
968
+ this.pause();
969
+ } else {
970
+ this.play();
971
+ }
972
+ }
973
+ /**
974
+ * Seek to percentage
975
+ * @param {number} percent - Percentage (0-1)
976
+ */
977
+ seekToPercent(percent) {
978
+ if (this.audio && this.audio.duration) {
979
+ this.audio.currentTime = this.audio.duration * Math.max(0, Math.min(1, percent));
980
+ this.updateProgress();
981
+ }
982
+ }
983
+ /**
984
+ * Set volume
985
+ * @param {number} volume - Volume (0-1)
986
+ */
987
+ setVolume(volume) {
988
+ if (this.audio) {
989
+ this.audio.volume = Math.max(0, Math.min(1, volume));
990
+ }
991
+ }
992
+ /**
993
+ * Destroy player instance
994
+ */
995
+ destroy() {
996
+ this.pause();
997
+ this.stopSmoothUpdate();
998
+ if (this.resizeObserver) {
999
+ this.resizeObserver.disconnect();
1000
+ }
1001
+ _WaveformPlayer.instances.delete(this.id);
1002
+ if (this.audio) {
1003
+ this.audio.src = "";
1004
+ }
1005
+ this.container.innerHTML = "";
1006
+ }
1007
+ // ============================================
1008
+ // Static methods
1009
+ // ============================================
1010
+ /**
1011
+ * Get player instance by ID, element, or element ID
1012
+ * @param {string|HTMLElement} idOrElement - Player ID, element, or element ID
1013
+ * @returns {WaveformPlayer|undefined}
1014
+ */
1015
+ static getInstance(idOrElement) {
1016
+ if (typeof idOrElement === "string") {
1017
+ const instance = this.instances.get(idOrElement);
1018
+ if (instance) return instance;
1019
+ const element = document.getElementById(idOrElement);
1020
+ if (element) {
1021
+ return Array.from(this.instances.values()).find((p) => p.container === element);
1022
+ }
1023
+ }
1024
+ if (idOrElement instanceof HTMLElement) {
1025
+ return Array.from(this.instances.values()).find((p) => p.container === idOrElement);
1026
+ }
1027
+ return void 0;
1028
+ }
1029
+ /**
1030
+ * Get all player instances
1031
+ * @returns {WaveformPlayer[]}
1032
+ */
1033
+ static getAllInstances() {
1034
+ return Array.from(this.instances.values());
1035
+ }
1036
+ /**
1037
+ * Destroy all player instances
1038
+ */
1039
+ static destroyAll() {
1040
+ this.instances.forEach((player) => player.destroy());
1041
+ this.instances.clear();
1042
+ }
1043
+ /**
1044
+ * Generate waveform data from audio URL
1045
+ * @static
1046
+ * @param {string} url - Audio URL
1047
+ * @param {number} samples - Number of samples
1048
+ * @returns {Promise<number[]>} Waveform peak data
1049
+ */
1050
+ static async generateWaveformData(url, samples = 200) {
1051
+ try {
1052
+ const result = await generateWaveform(url, samples);
1053
+ return result.peaks;
1054
+ } catch (error) {
1055
+ console.error("Failed to generate waveform:", error);
1056
+ throw error;
1057
+ }
1058
+ }
1059
+ };
1060
+
1061
+ // src/index.js
1062
+ function autoInit() {
1063
+ if (typeof document === "undefined") return;
1064
+ const elements = document.querySelectorAll("[data-waveform-player]");
1065
+ elements.forEach((element) => {
1066
+ if (element.dataset.waveformInitialized === "true") return;
1067
+ try {
1068
+ new WaveformPlayer(element);
1069
+ element.dataset.waveformInitialized = "true";
1070
+ } catch (error) {
1071
+ console.error("Failed to initialize WaveformPlayer:", error, element);
1072
+ }
1073
+ });
1074
+ }
1075
+ if (typeof document !== "undefined") {
1076
+ if (document.readyState === "loading") {
1077
+ document.addEventListener("DOMContentLoaded", autoInit);
1078
+ } else {
1079
+ autoInit();
1080
+ }
1081
+ }
1082
+ WaveformPlayer.init = autoInit;
1083
+ if (typeof window !== "undefined") {
1084
+ window.WaveformPlayer = WaveformPlayer;
1085
+ }
1086
+ var index_default = WaveformPlayer;
1087
+ })();