@arraypress/waveform-player 1.0.1 → 1.1.1
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/dist/waveform-player.css +1 -1
- package/dist/waveform-player.esm.js +58 -38
- package/dist/waveform-player.js +390 -54
- package/dist/waveform-player.min.js +58 -38
- package/package.json +2 -2
- package/src/css/waveform-player.css +197 -10
- package/src/js/audio.js +37 -19
- package/src/js/core.js +414 -40
- package/src/js/themes.js +16 -0
- package/src/js/utils.js +36 -1
package/dist/waveform-player.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
if (element.dataset.url) options.url = element.dataset.url;
|
|
6
6
|
if (element.dataset.height) options.height = parseInt(element.dataset.height);
|
|
7
7
|
if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
|
|
8
|
+
if (element.dataset.preload) {
|
|
9
|
+
options.preload = element.dataset.preload;
|
|
10
|
+
}
|
|
8
11
|
if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
|
|
9
12
|
if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
|
|
10
13
|
if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
|
|
14
|
+
if (element.dataset.buttonAlign) options.buttonAlign = element.dataset.buttonAlign;
|
|
11
15
|
if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
|
|
12
16
|
if (element.dataset.waveformColor) options.waveformColor = element.dataset.waveformColor;
|
|
13
17
|
if (element.dataset.progressColor) options.progressColor = element.dataset.progressColor;
|
|
@@ -27,7 +31,32 @@
|
|
|
27
31
|
if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === "true";
|
|
28
32
|
if (element.dataset.title) options.title = element.dataset.title;
|
|
29
33
|
if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
|
|
34
|
+
if (element.dataset.album) options.album = element.dataset.album;
|
|
35
|
+
if (element.dataset.artwork) options.artwork = element.dataset.artwork;
|
|
30
36
|
if (element.dataset.waveform) options.waveform = element.dataset.waveform;
|
|
37
|
+
if (element.dataset.markers) {
|
|
38
|
+
try {
|
|
39
|
+
options.markers = JSON.parse(element.dataset.markers);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn("Invalid markers JSON:", e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (element.dataset.playbackRate) {
|
|
45
|
+
options.playbackRate = parseFloat(element.dataset.playbackRate);
|
|
46
|
+
}
|
|
47
|
+
if (element.dataset.showPlaybackSpeed !== void 0) {
|
|
48
|
+
options.showPlaybackSpeed = element.dataset.showPlaybackSpeed === "true";
|
|
49
|
+
}
|
|
50
|
+
if (element.dataset.playbackRates) {
|
|
51
|
+
try {
|
|
52
|
+
options.playbackRates = JSON.parse(element.dataset.playbackRates);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn("Invalid playbackRates JSON:", e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (element.dataset.enableMediaSession !== void 0) {
|
|
58
|
+
options.enableMediaSession = element.dataset.enableMediaSession === "true";
|
|
59
|
+
}
|
|
31
60
|
return options;
|
|
32
61
|
}
|
|
33
62
|
function formatTime(seconds) {
|
|
@@ -439,24 +468,23 @@
|
|
|
439
468
|
const maxPeak = Math.max(...peaks);
|
|
440
469
|
return maxPeak > 0 ? peaks.map((peak) => peak / maxPeak) : peaks;
|
|
441
470
|
}
|
|
442
|
-
async function generateWaveform(url, samples = 200,
|
|
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();
|
|
471
|
+
async function generateWaveform(url, samples = 200, shouldDetectBPM = false) {
|
|
450
472
|
try {
|
|
473
|
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
474
|
+
const response = await fetch(url);
|
|
475
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
451
476
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
477
|
+
let peaks = extractPeaks(audioBuffer, samples);
|
|
478
|
+
peaks = normalizePeaks(peaks);
|
|
479
|
+
let bpm = null;
|
|
480
|
+
if (shouldDetectBPM) {
|
|
481
|
+
bpm = await detectBPM(audioBuffer);
|
|
456
482
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
483
|
+
audioContext.close();
|
|
484
|
+
return { peaks, bpm };
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error("Failed to generate waveform:", error);
|
|
487
|
+
throw error;
|
|
460
488
|
}
|
|
461
489
|
}
|
|
462
490
|
function generatePlaceholderWaveform(samples = 200) {
|
|
@@ -468,6 +496,12 @@
|
|
|
468
496
|
}
|
|
469
497
|
return data;
|
|
470
498
|
}
|
|
499
|
+
function normalizePeaks(peaks, targetMax = 0.95) {
|
|
500
|
+
const maxPeak = Math.max(...peaks);
|
|
501
|
+
if (maxPeak === 0 || maxPeak > targetMax) return peaks;
|
|
502
|
+
const scaleFactor = targetMax / maxPeak;
|
|
503
|
+
return peaks.map((peak) => peak * scaleFactor);
|
|
504
|
+
}
|
|
471
505
|
|
|
472
506
|
// src/js/themes.js
|
|
473
507
|
var DEFAULT_OPTIONS = {
|
|
@@ -475,6 +509,15 @@
|
|
|
475
509
|
url: "",
|
|
476
510
|
height: 60,
|
|
477
511
|
samples: 200,
|
|
512
|
+
preload: "metadata",
|
|
513
|
+
// Playback
|
|
514
|
+
playbackRate: 1,
|
|
515
|
+
showPlaybackSpeed: false,
|
|
516
|
+
playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
|
517
|
+
// Available speeds
|
|
518
|
+
// Layout Options
|
|
519
|
+
buttonAlign: "auto",
|
|
520
|
+
// 'auto', 'top', 'center', 'bottom'
|
|
478
521
|
// Default waveform style
|
|
479
522
|
waveformStyle: "mirror",
|
|
480
523
|
barWidth: 2,
|
|
@@ -497,9 +540,15 @@
|
|
|
497
540
|
showBPM: false,
|
|
498
541
|
singlePlay: true,
|
|
499
542
|
playOnSeek: true,
|
|
543
|
+
enableMediaSession: true,
|
|
544
|
+
// Markers
|
|
545
|
+
markers: [],
|
|
546
|
+
showMarkers: true,
|
|
500
547
|
// Content
|
|
501
548
|
title: null,
|
|
502
549
|
subtitle: null,
|
|
550
|
+
artwork: null,
|
|
551
|
+
album: "",
|
|
503
552
|
// Icons (SVG)
|
|
504
553
|
playIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',
|
|
505
554
|
pauseIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',
|
|
@@ -566,6 +615,9 @@
|
|
|
566
615
|
_WaveformPlayer.instances.set(this.id, this);
|
|
567
616
|
this.init();
|
|
568
617
|
}
|
|
618
|
+
// ============================================
|
|
619
|
+
// Initialization
|
|
620
|
+
// ============================================
|
|
569
621
|
/**
|
|
570
622
|
* Initialize the player
|
|
571
623
|
* @private
|
|
@@ -573,6 +625,8 @@
|
|
|
573
625
|
init() {
|
|
574
626
|
this.createDOM();
|
|
575
627
|
this.createAudio();
|
|
628
|
+
this.initPlaybackSpeed();
|
|
629
|
+
this.initKeyboardControls();
|
|
576
630
|
this.bindEvents();
|
|
577
631
|
this.setupResizeObserver();
|
|
578
632
|
requestAnimationFrame(() => {
|
|
@@ -595,59 +649,94 @@
|
|
|
595
649
|
createDOM() {
|
|
596
650
|
this.container.innerHTML = "";
|
|
597
651
|
this.container.className = "waveform-player";
|
|
652
|
+
let buttonAlign = this.options.buttonAlign;
|
|
653
|
+
if (buttonAlign === "auto") {
|
|
654
|
+
const style = this.options.waveformStyle;
|
|
655
|
+
if (style === "bars") {
|
|
656
|
+
buttonAlign = "bottom";
|
|
657
|
+
} else {
|
|
658
|
+
buttonAlign = "center";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
598
661
|
this.container.innerHTML = `
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
</div>
|
|
662
|
+
<div class="waveform-player-inner">
|
|
663
|
+
<div class="waveform-body">
|
|
664
|
+
<div class="waveform-track waveform-align-${buttonAlign}">
|
|
665
|
+
<button class="waveform-btn" aria-label="Play/Pause" style="
|
|
666
|
+
border-color: ${this.options.buttonColor};
|
|
667
|
+
color: ${this.options.buttonColor};
|
|
668
|
+
">
|
|
669
|
+
<span class="waveform-icon-play">${this.options.playIcon}</span>
|
|
670
|
+
<span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
|
|
671
|
+
</button>
|
|
672
|
+
|
|
673
|
+
<div class="waveform-container">
|
|
674
|
+
<canvas></canvas>
|
|
675
|
+
<div class="waveform-markers"></div>
|
|
676
|
+
<div class="waveform-loading" style="display:none;"></div>
|
|
677
|
+
<div class="waveform-error" style="display:none;">
|
|
678
|
+
<span class="waveform-error-text">Unable to load audio</span>
|
|
617
679
|
</div>
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<div class="waveform-info">
|
|
684
|
+
${this.options.artwork ? `
|
|
685
|
+
<img class="waveform-artwork" src="${this.options.artwork}" alt="Album artwork" style="
|
|
686
|
+
width: 40px;
|
|
687
|
+
height: 40px;
|
|
688
|
+
border-radius: 4px;
|
|
689
|
+
object-fit: cover;
|
|
690
|
+
flex-shrink: 0;
|
|
691
|
+
">
|
|
692
|
+
` : ""}
|
|
693
|
+
<div class="waveform-text">
|
|
694
|
+
<span class="waveform-title" style="color: ${this.options.textColor};"></span>
|
|
695
|
+
${this.options.subtitle ? `<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>` : ""}
|
|
696
|
+
</div>
|
|
697
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
698
|
+
${this.options.showBPM ? `
|
|
699
|
+
<span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
|
|
700
|
+
<span class="bpm-value">--</span> BPM
|
|
701
|
+
</span>
|
|
702
|
+
` : ""}
|
|
703
|
+
${this.options.showPlaybackSpeed ? `
|
|
704
|
+
<div class="waveform-speed">
|
|
705
|
+
<button class="speed-btn" aria-label="Playback speed">
|
|
706
|
+
<span class="speed-value">1x</span>
|
|
707
|
+
</button>
|
|
708
|
+
<div class="speed-menu" style="display: none;">
|
|
709
|
+
${this.options.playbackRates.map(
|
|
710
|
+
(rate) => `<button class="speed-option" data-rate="${rate}">${rate}x</button>`
|
|
711
|
+
).join("")}
|
|
712
|
+
</div>
|
|
635
713
|
</div>
|
|
636
|
-
|
|
714
|
+
` : ""}
|
|
715
|
+
${this.options.showTime ? `
|
|
716
|
+
<span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
|
|
717
|
+
<span class="time-current">0:00</span> / <span class="time-total">0:00</span>
|
|
718
|
+
</span>
|
|
719
|
+
` : ""}
|
|
637
720
|
</div>
|
|
638
721
|
</div>
|
|
639
|
-
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
`;
|
|
640
725
|
this.playBtn = this.container.querySelector(".waveform-btn");
|
|
641
726
|
this.canvas = this.container.querySelector("canvas");
|
|
642
727
|
this.ctx = this.canvas.getContext("2d");
|
|
643
728
|
this.titleEl = this.container.querySelector(".waveform-title");
|
|
644
729
|
this.subtitleEl = this.container.querySelector(".waveform-subtitle");
|
|
730
|
+
this.artworkEl = this.container.querySelector(".waveform-artwork");
|
|
645
731
|
this.currentTimeEl = this.container.querySelector(".time-current");
|
|
646
732
|
this.totalTimeEl = this.container.querySelector(".time-total");
|
|
647
733
|
this.bpmEl = this.container.querySelector(".waveform-bpm");
|
|
648
734
|
this.bpmValueEl = this.container.querySelector(".bpm-value");
|
|
649
735
|
this.loadingEl = this.container.querySelector(".waveform-loading");
|
|
650
736
|
this.errorEl = this.container.querySelector(".waveform-error");
|
|
737
|
+
this.markersContainer = this.container.querySelector(".waveform-markers");
|
|
738
|
+
this.speedBtn = this.container.querySelector(".speed-btn");
|
|
739
|
+
this.speedMenu = this.container.querySelector(".speed-menu");
|
|
651
740
|
this.resizeCanvas();
|
|
652
741
|
}
|
|
653
742
|
/**
|
|
@@ -656,9 +745,119 @@
|
|
|
656
745
|
*/
|
|
657
746
|
createAudio() {
|
|
658
747
|
this.audio = new Audio();
|
|
659
|
-
this.audio.preload = "metadata";
|
|
748
|
+
this.audio.preload = this.options.preload || "metadata";
|
|
660
749
|
this.audio.crossOrigin = "anonymous";
|
|
661
750
|
}
|
|
751
|
+
// ============================================
|
|
752
|
+
// Feature Initialization
|
|
753
|
+
// ============================================
|
|
754
|
+
/**
|
|
755
|
+
* Initialize playback speed controls
|
|
756
|
+
* @private
|
|
757
|
+
*/
|
|
758
|
+
initPlaybackSpeed() {
|
|
759
|
+
if (this.options.playbackRate && this.options.playbackRate !== 1) {
|
|
760
|
+
this.audio.playbackRate = this.options.playbackRate;
|
|
761
|
+
}
|
|
762
|
+
if (this.options.showPlaybackSpeed) {
|
|
763
|
+
this.initSpeedControls();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Initialize speed control UI
|
|
768
|
+
* @private
|
|
769
|
+
*/
|
|
770
|
+
initSpeedControls() {
|
|
771
|
+
const speedBtn = this.container.querySelector(".speed-btn");
|
|
772
|
+
const speedMenu = this.container.querySelector(".speed-menu");
|
|
773
|
+
if (!speedBtn || !speedMenu) return;
|
|
774
|
+
speedBtn.addEventListener("click", (e) => {
|
|
775
|
+
e.stopPropagation();
|
|
776
|
+
speedMenu.style.display = speedMenu.style.display === "none" ? "block" : "none";
|
|
777
|
+
});
|
|
778
|
+
document.addEventListener("click", () => {
|
|
779
|
+
speedMenu.style.display = "none";
|
|
780
|
+
});
|
|
781
|
+
speedMenu.addEventListener("click", (e) => {
|
|
782
|
+
e.stopPropagation();
|
|
783
|
+
if (e.target.classList.contains("speed-option")) {
|
|
784
|
+
const rate = parseFloat(e.target.dataset.rate);
|
|
785
|
+
this.setPlaybackRate(rate);
|
|
786
|
+
speedMenu.style.display = "none";
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
this.updateSpeedUI();
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Initialize keyboard controls
|
|
793
|
+
* @private
|
|
794
|
+
*/
|
|
795
|
+
initKeyboardControls() {
|
|
796
|
+
this.container.setAttribute("tabindex", "-1");
|
|
797
|
+
this.container.addEventListener("click", () => {
|
|
798
|
+
_WaveformPlayer.getAllInstances().forEach((player) => {
|
|
799
|
+
if (player !== this) {
|
|
800
|
+
player.container.setAttribute("tabindex", "-1");
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
this.container.setAttribute("tabindex", "0");
|
|
804
|
+
this.container.focus();
|
|
805
|
+
});
|
|
806
|
+
this.container.addEventListener("keydown", (e) => {
|
|
807
|
+
if (document.activeElement !== this.container) return;
|
|
808
|
+
const key = e.key;
|
|
809
|
+
const currentTime = this.audio.currentTime;
|
|
810
|
+
if (key >= "0" && key <= "9") {
|
|
811
|
+
e.preventDefault();
|
|
812
|
+
this.seekToPercent(parseInt(key) / 10);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const actions = {
|
|
816
|
+
" ": () => this.togglePlay(),
|
|
817
|
+
"ArrowLeft": () => this.seekTo(Math.max(0, currentTime - 5)),
|
|
818
|
+
"ArrowRight": () => this.seekTo(Math.min(this.audio.duration, currentTime + 5)),
|
|
819
|
+
"ArrowUp": () => this.setVolume(Math.min(1, this.audio.volume + 0.1)),
|
|
820
|
+
"ArrowDown": () => this.setVolume(Math.max(0, this.audio.volume - 0.1)),
|
|
821
|
+
"m": () => this.audio.muted = !this.audio.muted,
|
|
822
|
+
"M": () => this.audio.muted = !this.audio.muted
|
|
823
|
+
};
|
|
824
|
+
if (actions[key]) {
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
actions[key]();
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Initialize Media Session API for system media controls
|
|
832
|
+
* @private
|
|
833
|
+
*/
|
|
834
|
+
initMediaSession() {
|
|
835
|
+
if (!("mediaSession" in navigator) || !this.options.enableMediaSession) return;
|
|
836
|
+
navigator.mediaSession.metadata = new MediaMetadata({
|
|
837
|
+
title: this.options.title || "Unknown Track",
|
|
838
|
+
artist: this.options.subtitle || "",
|
|
839
|
+
album: this.options.album || "",
|
|
840
|
+
artwork: this.options.artwork ? [
|
|
841
|
+
{ src: this.options.artwork, sizes: "512x512", type: "image/jpeg" }
|
|
842
|
+
] : []
|
|
843
|
+
});
|
|
844
|
+
navigator.mediaSession.setActionHandler("play", () => this.play());
|
|
845
|
+
navigator.mediaSession.setActionHandler("pause", () => this.pause());
|
|
846
|
+
navigator.mediaSession.setActionHandler("seekbackward", () => {
|
|
847
|
+
this.seekTo(Math.max(0, this.audio.currentTime - 10));
|
|
848
|
+
});
|
|
849
|
+
navigator.mediaSession.setActionHandler("seekforward", () => {
|
|
850
|
+
this.seekTo(Math.min(this.audio.duration, this.audio.currentTime + 10));
|
|
851
|
+
});
|
|
852
|
+
navigator.mediaSession.setActionHandler("seekto", (details) => {
|
|
853
|
+
if (details.seekTime !== null) {
|
|
854
|
+
this.seekTo(details.seekTime);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// ============================================
|
|
859
|
+
// Event Binding
|
|
860
|
+
// ============================================
|
|
662
861
|
/**
|
|
663
862
|
* Bind event listeners
|
|
664
863
|
* @private
|
|
@@ -689,6 +888,9 @@
|
|
|
689
888
|
}
|
|
690
889
|
}
|
|
691
890
|
}
|
|
891
|
+
// ============================================
|
|
892
|
+
// Audio Loading
|
|
893
|
+
// ============================================
|
|
692
894
|
/**
|
|
693
895
|
* Load audio file
|
|
694
896
|
* @param {string} url - Audio URL
|
|
@@ -734,6 +936,8 @@
|
|
|
734
936
|
}
|
|
735
937
|
}
|
|
736
938
|
this.drawWaveform();
|
|
939
|
+
this.renderMarkers();
|
|
940
|
+
this.initMediaSession();
|
|
737
941
|
if (this.options.onLoad) {
|
|
738
942
|
this.options.onLoad(this);
|
|
739
943
|
}
|
|
@@ -744,6 +948,61 @@
|
|
|
744
948
|
this.setLoading(false);
|
|
745
949
|
}
|
|
746
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Load a new track
|
|
953
|
+
* @param {string} url - Audio URL
|
|
954
|
+
* @param {string} [title] - Track title
|
|
955
|
+
* @param {string} [subtitle] - Track subtitle
|
|
956
|
+
* @param {Object} [options] - Additional options
|
|
957
|
+
* @returns {Promise<void>}
|
|
958
|
+
*/
|
|
959
|
+
async loadTrack(url, title = null, subtitle = null, options = {}) {
|
|
960
|
+
if (this.isPlaying) {
|
|
961
|
+
this.pause();
|
|
962
|
+
}
|
|
963
|
+
this.audio.src = "";
|
|
964
|
+
this.audio.load();
|
|
965
|
+
this.hasError = false;
|
|
966
|
+
if (this.errorEl) {
|
|
967
|
+
this.errorEl.style.display = "none";
|
|
968
|
+
}
|
|
969
|
+
if (this.canvas) {
|
|
970
|
+
this.canvas.style.opacity = "1";
|
|
971
|
+
}
|
|
972
|
+
if (this.playBtn) {
|
|
973
|
+
this.playBtn.disabled = false;
|
|
974
|
+
}
|
|
975
|
+
this.progress = 0;
|
|
976
|
+
this.waveformData = [];
|
|
977
|
+
this.options = mergeOptions(this.options, {
|
|
978
|
+
url,
|
|
979
|
+
title: title || this.options.title,
|
|
980
|
+
subtitle: subtitle || this.options.subtitle,
|
|
981
|
+
...options
|
|
982
|
+
});
|
|
983
|
+
if (options.preload) {
|
|
984
|
+
this.audio.preload = options.preload;
|
|
985
|
+
}
|
|
986
|
+
if (this.subtitleEl) {
|
|
987
|
+
if (subtitle) {
|
|
988
|
+
this.subtitleEl.textContent = subtitle;
|
|
989
|
+
this.subtitleEl.style.display = "";
|
|
990
|
+
} else if (subtitle === "") {
|
|
991
|
+
this.subtitleEl.style.display = "none";
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (options.artwork && this.artworkEl) {
|
|
995
|
+
this.artworkEl.src = options.artwork;
|
|
996
|
+
}
|
|
997
|
+
if (options.markers) {
|
|
998
|
+
this.options.markers = options.markers;
|
|
999
|
+
}
|
|
1000
|
+
await this.load(url);
|
|
1001
|
+
this.play();
|
|
1002
|
+
}
|
|
1003
|
+
// ============================================
|
|
1004
|
+
// Visualization
|
|
1005
|
+
// ============================================
|
|
747
1006
|
/**
|
|
748
1007
|
* Set waveform data
|
|
749
1008
|
* @private
|
|
@@ -787,6 +1046,41 @@
|
|
|
787
1046
|
this.canvas.parentElement.style.height = this.options.height + "px";
|
|
788
1047
|
this.drawWaveform();
|
|
789
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Render markers on the waveform
|
|
1051
|
+
* @private
|
|
1052
|
+
*/
|
|
1053
|
+
renderMarkers() {
|
|
1054
|
+
if (!this.options.showMarkers || !this.options.markers?.length || !this.markersContainer) return;
|
|
1055
|
+
this.markersContainer.innerHTML = "";
|
|
1056
|
+
if (!this.audio || !this.audio.duration || this.audio.duration === 0) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.options.markers.forEach((marker, index) => {
|
|
1060
|
+
const position = marker.time / this.audio.duration * 100;
|
|
1061
|
+
const markerEl = document.createElement("button");
|
|
1062
|
+
markerEl.className = "waveform-marker";
|
|
1063
|
+
markerEl.style.left = `${position}%`;
|
|
1064
|
+
markerEl.style.backgroundColor = marker.color || "rgba(255, 255, 255, 0.5)";
|
|
1065
|
+
markerEl.setAttribute("aria-label", marker.label);
|
|
1066
|
+
markerEl.setAttribute("data-time", marker.time);
|
|
1067
|
+
const tooltip = document.createElement("span");
|
|
1068
|
+
tooltip.className = "waveform-marker-tooltip";
|
|
1069
|
+
tooltip.textContent = marker.label;
|
|
1070
|
+
markerEl.appendChild(tooltip);
|
|
1071
|
+
markerEl.addEventListener("click", (e) => {
|
|
1072
|
+
e.stopPropagation();
|
|
1073
|
+
this.seekTo(marker.time);
|
|
1074
|
+
if (this.options.playOnSeek && !this.isPlaying) {
|
|
1075
|
+
this.play();
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
this.markersContainer.appendChild(markerEl);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
// ============================================
|
|
1082
|
+
// Event Handlers
|
|
1083
|
+
// ============================================
|
|
790
1084
|
/**
|
|
791
1085
|
* Handle canvas click
|
|
792
1086
|
* @private
|
|
@@ -816,6 +1110,7 @@
|
|
|
816
1110
|
if (this.totalTimeEl) {
|
|
817
1111
|
this.totalTimeEl.textContent = formatTime(this.audio.duration);
|
|
818
1112
|
}
|
|
1113
|
+
this.renderMarkers();
|
|
819
1114
|
}
|
|
820
1115
|
/**
|
|
821
1116
|
* Handle play event
|
|
@@ -886,6 +1181,9 @@
|
|
|
886
1181
|
this.options.onError(error, this);
|
|
887
1182
|
}
|
|
888
1183
|
}
|
|
1184
|
+
// ============================================
|
|
1185
|
+
// Progress Updates
|
|
1186
|
+
// ============================================
|
|
889
1187
|
/**
|
|
890
1188
|
* Start smooth update animation
|
|
891
1189
|
* @private
|
|
@@ -928,6 +1226,9 @@
|
|
|
928
1226
|
this.options.onTimeUpdate(this.audio.currentTime, this.audio.duration, this);
|
|
929
1227
|
}
|
|
930
1228
|
}
|
|
1229
|
+
// ============================================
|
|
1230
|
+
// UI Updates
|
|
1231
|
+
// ============================================
|
|
931
1232
|
/**
|
|
932
1233
|
* Update BPM display
|
|
933
1234
|
* @private
|
|
@@ -938,6 +1239,20 @@
|
|
|
938
1239
|
this.bpmEl.style.display = "inline-flex";
|
|
939
1240
|
}
|
|
940
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Update speed UI to reflect current rate
|
|
1244
|
+
* @private
|
|
1245
|
+
*/
|
|
1246
|
+
updateSpeedUI() {
|
|
1247
|
+
const speedValue = this.container.querySelector(".speed-value");
|
|
1248
|
+
if (speedValue) {
|
|
1249
|
+
const rate = this.audio.playbackRate;
|
|
1250
|
+
speedValue.textContent = rate === 1 ? "1x" : `${rate}x`;
|
|
1251
|
+
}
|
|
1252
|
+
this.container.querySelectorAll(".speed-option").forEach((btn) => {
|
|
1253
|
+
btn.classList.toggle("active", parseFloat(btn.dataset.rate) === this.audio.playbackRate);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
941
1256
|
// ============================================
|
|
942
1257
|
// Public API
|
|
943
1258
|
// ============================================
|
|
@@ -970,6 +1285,16 @@
|
|
|
970
1285
|
this.play();
|
|
971
1286
|
}
|
|
972
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Seek to time in seconds
|
|
1290
|
+
* @param {number} seconds - Time in seconds
|
|
1291
|
+
*/
|
|
1292
|
+
seekTo(seconds) {
|
|
1293
|
+
if (this.audio && this.audio.duration) {
|
|
1294
|
+
this.audio.currentTime = Math.max(0, Math.min(seconds, this.audio.duration));
|
|
1295
|
+
this.updateProgress();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
973
1298
|
/**
|
|
974
1299
|
* Seek to percentage
|
|
975
1300
|
* @param {number} percent - Percentage (0-1)
|
|
@@ -989,6 +1314,17 @@
|
|
|
989
1314
|
this.audio.volume = Math.max(0, Math.min(1, volume));
|
|
990
1315
|
}
|
|
991
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Set playback rate
|
|
1319
|
+
* @param {number} rate - Playback rate (0.5 to 2)
|
|
1320
|
+
*/
|
|
1321
|
+
setPlaybackRate(rate) {
|
|
1322
|
+
if (!this.audio) return;
|
|
1323
|
+
const clampedRate = Math.max(0.5, Math.min(2, rate));
|
|
1324
|
+
this.audio.playbackRate = clampedRate;
|
|
1325
|
+
this.options.playbackRate = clampedRate;
|
|
1326
|
+
this.updateSpeedUI();
|
|
1327
|
+
}
|
|
992
1328
|
/**
|
|
993
1329
|
* Destroy player instance
|
|
994
1330
|
*/
|
|
@@ -1005,7 +1341,7 @@
|
|
|
1005
1341
|
this.container.innerHTML = "";
|
|
1006
1342
|
}
|
|
1007
1343
|
// ============================================
|
|
1008
|
-
// Static
|
|
1344
|
+
// Static Methods
|
|
1009
1345
|
// ============================================
|
|
1010
1346
|
/**
|
|
1011
1347
|
* Get player instance by ID, element, or element ID
|