@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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/waveform-player.css +185 -0
- package/dist/waveform-player.esm.js +42 -0
- package/dist/waveform-player.js +1087 -0
- package/dist/waveform-player.min.js +42 -0
- package/package.json +46 -0
- package/src/audio.js +94 -0
- package/src/bpm.js +92 -0
- package/src/core.js +680 -0
- package/src/drawing.js +365 -0
- package/src/index.js +51 -0
- package/src/themes.js +94 -0
- package/src/utils.js +202 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Waveform Player
|
|
2
|
+
|
|
3
|
+
A lightweight, customizable audio player with waveform visualization. Under 6KB gzipped.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🎨 **6 Visual Styles** - Bars, mirror, line, blocks, dots
|
|
12
|
+
- 🎯 **Tiny Footprint** - Under 6KB gzipped
|
|
13
|
+
- ⚡ **Zero Dependencies** - Pure JavaScript
|
|
14
|
+
- 🎭 **Fully Customizable** - Colors, sizes, styles
|
|
15
|
+
- 📱 **Responsive** - Works on all devices
|
|
16
|
+
- 🎵 **BPM Detection** - Automatic tempo detection (optional)
|
|
17
|
+
- 💾 **Waveform Caching** - Pre-generate waveforms for performance
|
|
18
|
+
- 🌐 **Framework Agnostic** - Works with React, Vue, Angular, or vanilla JS
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### NPM
|
|
23
|
+
```bash
|
|
24
|
+
npm install waveform-player
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### CDN
|
|
28
|
+
```html
|
|
29
|
+
<link rel="stylesheet" href="https://unpkg.com/waveform-player/dist/waveform-player.css">
|
|
30
|
+
<script src="https://unpkg.com/waveform-player/dist/waveform-player.min.js"></script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### HTML
|
|
36
|
+
```html
|
|
37
|
+
<div data-waveform-player
|
|
38
|
+
data-url="audio.mp3"
|
|
39
|
+
data-title="My Song">
|
|
40
|
+
</div>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### JavaScript
|
|
44
|
+
```javascript
|
|
45
|
+
import WaveformPlayer from 'waveform-player';
|
|
46
|
+
|
|
47
|
+
const player = new WaveformPlayer('#player', {
|
|
48
|
+
url: 'audio.mp3',
|
|
49
|
+
waveformStyle: 'mirror',
|
|
50
|
+
height: 80
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
| Option | Type | Default | Description |
|
|
57
|
+
|--------|------|---------|-------------|
|
|
58
|
+
| `url` | string | `''` | Audio file URL |
|
|
59
|
+
| `waveformStyle` | string | `'bars'` | Visual style: bars, mirror, line, blocks, dots |
|
|
60
|
+
| `height` | number | `60` | Waveform height in pixels |
|
|
61
|
+
| `barWidth` | number | `3` | Width of waveform bars |
|
|
62
|
+
| `barSpacing` | number | `1` | Space between bars |
|
|
63
|
+
| `samples` | number | `200` | Number of waveform samples |
|
|
64
|
+
| `waveformColor` | string | `'rgba(255,255,255,0.3)'` | Waveform color |
|
|
65
|
+
| `progressColor` | string | `'rgba(255,255,255,0.9)'` | Progress color |
|
|
66
|
+
| `showTime` | boolean | `true` | Show time display |
|
|
67
|
+
| `showBPM` | boolean | `false` | Enable BPM detection |
|
|
68
|
+
| `autoplay` | boolean | `false` | Autoplay on load |
|
|
69
|
+
| `title` | string | `''` | Track title |
|
|
70
|
+
| `subtitle` | string | `''` | Track subtitle |
|
|
71
|
+
|
|
72
|
+
## API Methods
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// Control playback
|
|
76
|
+
player.play();
|
|
77
|
+
player.pause();
|
|
78
|
+
player.togglePlay();
|
|
79
|
+
|
|
80
|
+
// Seek
|
|
81
|
+
player.seekTo(30); // Seek to 30 seconds
|
|
82
|
+
player.seekToPercent(0.5); // Seek to 50%
|
|
83
|
+
|
|
84
|
+
// Volume
|
|
85
|
+
player.setVolume(0.8); // 80% volume
|
|
86
|
+
|
|
87
|
+
// Destroy
|
|
88
|
+
player.destroy();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Events
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
new WaveformPlayer('#player', {
|
|
95
|
+
url: 'audio.mp3',
|
|
96
|
+
onLoad: (player) => console.log('Loaded'),
|
|
97
|
+
onPlay: (player) => console.log('Playing'),
|
|
98
|
+
onPause: (player) => console.log('Paused'),
|
|
99
|
+
onEnd: (player) => console.log('Ended'),
|
|
100
|
+
onTimeUpdate: (current, total, player) => {
|
|
101
|
+
console.log(`${current}/${total}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Advanced Usage
|
|
107
|
+
|
|
108
|
+
### Pre-generated Waveforms
|
|
109
|
+
|
|
110
|
+
For better performance, generate waveform data server-side:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// Generate waveform data
|
|
114
|
+
const waveformData = await WaveformPlayer.generateWaveformData('audio.mp3');
|
|
115
|
+
|
|
116
|
+
// Use pre-generated data
|
|
117
|
+
new WaveformPlayer('#player', {
|
|
118
|
+
url: 'audio.mp3',
|
|
119
|
+
waveform: waveformData // Bypass client-side processing
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Multiple Players
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Pause all players
|
|
127
|
+
WaveformPlayer.pauseAll();
|
|
128
|
+
|
|
129
|
+
// Get all instances
|
|
130
|
+
const players = WaveformPlayer.getAllInstances();
|
|
131
|
+
|
|
132
|
+
// Find specific player
|
|
133
|
+
const player = WaveformPlayer.getInstance('my-player');
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Custom Styling
|
|
137
|
+
|
|
138
|
+
```css
|
|
139
|
+
.waveform-player {
|
|
140
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
141
|
+
border-radius: 12px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.waveform-btn {
|
|
145
|
+
border-color: #fff;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Browser Support
|
|
150
|
+
|
|
151
|
+
- Chrome/Edge 90+
|
|
152
|
+
- Firefox 88+
|
|
153
|
+
- Safari 14+
|
|
154
|
+
- Mobile browsers
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
Check the `/examples` directory for:
|
|
159
|
+
- Basic player setup
|
|
160
|
+
- Multiple players
|
|
161
|
+
- Custom styling
|
|
162
|
+
- Event handling
|
|
163
|
+
- Pre-generated waveforms
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Install dependencies
|
|
169
|
+
npm install
|
|
170
|
+
|
|
171
|
+
# Development mode
|
|
172
|
+
npm run dev
|
|
173
|
+
|
|
174
|
+
# Build
|
|
175
|
+
npm run build
|
|
176
|
+
|
|
177
|
+
# Check size
|
|
178
|
+
npm run size
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT © David Sherlock / ArrayPress
|
|
184
|
+
|
|
185
|
+
## Credits
|
|
186
|
+
|
|
187
|
+
Created by [David Sherlock](https://github.com/arraypress)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaveformPlayer.css
|
|
3
|
+
* Ultra-minimal styles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Base structure */
|
|
7
|
+
.waveform-player {
|
|
8
|
+
font-family: inherit;
|
|
9
|
+
color: inherit;
|
|
10
|
+
line-height: 1.4;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.waveform-player * {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.waveform-player-inner {
|
|
18
|
+
padding: 12px;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Body container */
|
|
23
|
+
.waveform-body {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Track row */
|
|
30
|
+
.waveform-track {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
position: relative;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Play button */
|
|
38
|
+
.waveform-btn {
|
|
39
|
+
width: 36px;
|
|
40
|
+
height: 36px;
|
|
41
|
+
min-width: 36px;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
border: 2px solid currentColor;
|
|
44
|
+
background: transparent;
|
|
45
|
+
color: inherit;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
transition: all 0.2s ease;
|
|
51
|
+
padding: 0;
|
|
52
|
+
opacity: 0.9;
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.waveform-btn:hover:not(:disabled) {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: scale(1.05);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.waveform-btn:disabled {
|
|
62
|
+
cursor: not-allowed;
|
|
63
|
+
opacity: 0.3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Icons */
|
|
67
|
+
.waveform-btn > * {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: 100%;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.waveform-btn svg {
|
|
76
|
+
width: 16px;
|
|
77
|
+
height: 16px;
|
|
78
|
+
fill: currentColor;
|
|
79
|
+
display: block;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.waveform-icon-play svg {
|
|
83
|
+
margin-left: 1px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Waveform container */
|
|
87
|
+
.waveform-container {
|
|
88
|
+
flex: 1;
|
|
89
|
+
position: relative;
|
|
90
|
+
min-height: 60px;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
width: 100%;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.waveform-container canvas {
|
|
98
|
+
display: block;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
max-width: 100%;
|
|
102
|
+
transition: opacity 0.3s ease;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Info section */
|
|
106
|
+
.waveform-info {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
min-height: 20px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.waveform-text {
|
|
115
|
+
flex: 1;
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
gap: 2px;
|
|
119
|
+
min-width: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.waveform-title {
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
text-overflow: ellipsis;
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.waveform-subtitle {
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
text-overflow: ellipsis;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.waveform-time {
|
|
137
|
+
font-size: 11px;
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
flex-shrink: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Loading state - simplified */
|
|
143
|
+
.waveform-loading {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
background: rgba(0, 0, 0, 0.1);
|
|
147
|
+
z-index: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Error state */
|
|
151
|
+
.waveform-error {
|
|
152
|
+
position: absolute;
|
|
153
|
+
inset: 0;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
background: rgba(0, 0, 0, 0.2);
|
|
158
|
+
z-index: 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.waveform-error-text {
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
opacity: 0.7;
|
|
164
|
+
text-align: center;
|
|
165
|
+
padding: 0 20px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Minimal responsive */
|
|
169
|
+
@media (max-width: 480px) {
|
|
170
|
+
.waveform-btn {
|
|
171
|
+
width: 32px;
|
|
172
|
+
height: 32px;
|
|
173
|
+
min-width: 32px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.waveform-container {
|
|
177
|
+
min-height: 50px;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Accessibility */
|
|
182
|
+
.waveform-btn:focus-visible {
|
|
183
|
+
outline: 2px solid currentColor;
|
|
184
|
+
outline-offset: 2px;
|
|
185
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function T(t){let e={};return t.dataset.url&&(e.url=t.dataset.url),t.dataset.height&&(e.height=parseInt(t.dataset.height)),t.dataset.samples&&(e.samples=parseInt(t.dataset.samples)),t.dataset.waveformStyle&&(e.waveformStyle=t.dataset.waveformStyle),t.dataset.barWidth&&(e.barWidth=parseInt(t.dataset.barWidth)),t.dataset.barSpacing&&(e.barSpacing=parseInt(t.dataset.barSpacing)),t.dataset.colorPreset&&(e.colorPreset=t.dataset.colorPreset),t.dataset.waveformColor&&(e.waveformColor=t.dataset.waveformColor),t.dataset.progressColor&&(e.progressColor=t.dataset.progressColor),t.dataset.buttonColor&&(e.buttonColor=t.dataset.buttonColor),t.dataset.buttonHoverColor&&(e.buttonHoverColor=t.dataset.buttonHoverColor),t.dataset.textColor&&(e.textColor=t.dataset.textColor),t.dataset.textSecondaryColor&&(e.textSecondaryColor=t.dataset.textSecondaryColor),t.dataset.backgroundColor&&(e.backgroundColor=t.dataset.backgroundColor),t.dataset.borderColor&&(e.borderColor=t.dataset.borderColor),t.dataset.color&&(e.waveformColor=t.dataset.color),t.dataset.theme&&(e.colorPreset=t.dataset.theme),t.dataset.autoplay&&(e.autoplay=t.dataset.autoplay==="true"),t.dataset.showTime&&(e.showTime=t.dataset.showTime==="true"),t.dataset.showHoverTime&&(e.showHoverTime=t.dataset.showHoverTime==="true"),t.dataset.showBpm&&(e.showBPM=t.dataset.showBpm==="true"),t.dataset.singlePlay&&(e.singlePlay=t.dataset.singlePlay==="true"),t.dataset.playOnSeek&&(e.playOnSeek=t.dataset.playOnSeek==="true"),t.dataset.title&&(e.title=t.dataset.title),t.dataset.subtitle&&(e.subtitle=t.dataset.subtitle),t.dataset.waveform&&(e.waveform=t.dataset.waveform),e}function P(t){if(!t||isNaN(t))return"0:00";let e=Math.floor(t/60),o=Math.floor(t%60);return`${e}:${o.toString().padStart(2,"0")}`}function k(t){let e=t||Math.random().toString();return btoa(e.substring(0,10)).replace(/[^a-zA-Z0-9]/g,"")}function W(t){if(!t)return"Audio";let e=t.split("/");return e[e.length-1].split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,s=>s.toUpperCase())}function B(...t){let e={};for(let o of t)for(let r in o)o[r]!==null&&o[r]!==void 0&&(e[r]=o[r]);return e}function L(t,e){let o;return function(...s){let n=()=>{clearTimeout(o),t(...s)};clearTimeout(o),o=setTimeout(n,e)}}function S(t,e){if(t.length===e)return t;if(t.length===0||e===0)return[];let o=[];if(e>t.length){let r=(t.length-1)/(e-1);for(let s=0;s<e;s++){let n=s*r,a=Math.floor(n),i=Math.ceil(n),d=n-a;if(i>=t.length)o.push(t[t.length-1]);else if(a===i)o.push(t[a]);else{let l=t[a]*(1-d)+t[i]*d;o.push(l)}}}else{let r=t.length/e;for(let s=0;s<e;s++){let n=Math.floor(s*r),a=Math.floor((s+1)*r),i=0,d=0;for(let l=n;l<=a&&l<t.length;l++)t[l]>i&&(i=t[l]),d++;if(d===0){let l=Math.min(Math.round(s*r),t.length-1);i=t[l]}o.push(i)}}return o}function I(t,e,o,r,s){let n=window.devicePixelRatio||1,a=s.barWidth*n,i=s.barSpacing*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=r*e.width;t.clearRect(0,0,e.width,e.height);for(let m=0;m<l.length;m++){let p=m*(a+i);if(p+a>e.width)break;let c=l[m]*h*.9,g=h-c;t.fillStyle=s.color,t.fillRect(p,g,a,c)}t.save(),t.beginPath(),t.rect(0,0,f,h),t.clip();for(let m=0;m<l.length;m++){let p=m*(a+i);if(p>f)break;let c=l[m]*h*.9,g=h-c;t.fillStyle=s.progressColor,t.fillRect(p,g,a,c)}t.restore()}function x(t,e,o,r,s){let n=window.devicePixelRatio||1,a=s.barWidth*n,i=s.barSpacing*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=h/2,m=r*e.width;t.clearRect(0,0,e.width,e.height);for(let p=0;p<l.length;p++){let c=p*(a+i);if(c+a>e.width)break;let g=l[p]*h*.45;t.fillStyle=s.color,t.fillRect(c,f-g,a,g),t.fillRect(c,f,a,g)}t.save(),t.beginPath(),t.rect(0,0,m,h),t.clip();for(let p=0;p<l.length;p++){let c=p*(a+i);if(c>m)break;let g=l[p]*h*.45;t.fillStyle=s.progressColor,t.fillRect(c,f-g,a,g),t.fillRect(c,f,a,g)}t.restore()}function H(t,e,o,r,s){let n=e.width,a=e.height,i=a/2,d=a*.35;t.clearRect(0,0,n,a);let l=(h,f,m=1,p=!1)=>{p&&(t.shadowBlur=12,t.shadowColor=h),t.strokeStyle=h,t.lineWidth=f,t.lineCap="round",t.lineJoin="round",t.beginPath(),t.moveTo(0,i);let c=[],g=Math.floor(o.length*m);for(let u=0;u<g;u++){let v=u/(o.length-1)*n,C=o[u],y=Math.sin(u*.1)*C,w=i+y*d;c.push({x:v,y:w})}for(let u=0;u<c.length-1;u++){let v=c[u].x+(c[u+1].x-c[u].x)*.5,C=c[u].y,y=c[u+1].x-(c[u+1].x-c[u].x)*.5,w=c[u+1].y;t.bezierCurveTo(v,C,y,w,c[u+1].x,c[u+1].y)}t.stroke(),p&&(t.shadowBlur=0)};t.strokeStyle="rgba(255, 255, 255, 0.03)",t.lineWidth=.5,t.beginPath(),t.moveTo(0,i),t.lineTo(n,i),t.stroke();for(let h=0;h<=10;h++){let f=n/10*h;t.beginPath(),t.moveTo(f,0),t.lineTo(f,a),t.stroke()}l(s.color,2,1,!1),r>0&&l(s.progressColor,3,r,!0)}function q(t,e,o,r,s){let n=window.devicePixelRatio||1,a=(s.barWidth||3)*n,i=(s.barSpacing||1)*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=4*n,m=2*n,p=r*e.width,c=h/2;t.clearRect(0,0,e.width,e.height);for(let g=0;g<l.length;g++){let u=g*(a+i);if(u+a>e.width)break;let v=l[g]*h*.9,C=Math.floor(v/(f+m));t.fillStyle=u<p?s.progressColor:s.color;for(let y=0;y<C;y++){let w=y*(f+m);t.fillRect(u,c-w-f,a,f),y>0&&t.fillRect(u,c+w,a,f)}}}function U(t,e,o,r,s){let n=window.devicePixelRatio||1,a=(s.barWidth||2)*n,i=(s.barSpacing||3)*n,d=Math.floor(e.width/(a+i)),l=S(o,d),h=e.height,f=Math.max(1.5*n,a/2),m=r*e.width,p=h/2;t.clearRect(0,0,e.width,e.height);for(let c=0;c<l.length;c++){let g=c*(a+i)+a/2;if(g>e.width)break;let u=l[c]*h*.9;t.fillStyle=g<m?s.progressColor:s.color,t.beginPath(),t.arc(g,p-u/2,f,0,Math.PI*2),t.fill(),t.beginPath(),t.arc(g,p+u/2,f,0,Math.PI*2),t.fill()}}function F(t,e,o,r,s){let n=e.width,a=e.height,i=a/2,d=4,l=d/2;if(t.clearRect(0,0,n,a),t.fillStyle=s.color||"rgba(255, 255, 255, 0.2)",t.beginPath(),t.moveTo(l,i-d/2),t.lineTo(n-l,i-d/2),t.arc(n-l,i,d/2,-Math.PI/2,Math.PI/2),t.lineTo(l,i+d/2),t.arc(l,i,d/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),r>0){let h=Math.max(l*2,r*n);t.shadowBlur=8,t.shadowColor=s.progressColor,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.moveTo(l,i-d/2),t.lineTo(h-l,i-d/2),t.arc(h-l,i,d/2,-Math.PI/2,Math.PI/2),t.lineTo(l,i+d/2),t.arc(l,i,d/2,Math.PI/2,-Math.PI/2),t.closePath(),t.fill(),t.shadowBlur=0;let f=8,m=h;t.shadowBlur=4,t.shadowColor="rgba(0, 0, 0, 0.3)",t.shadowOffsetY=2,t.fillStyle="#ffffff",t.beginPath(),t.arc(m,i,f,0,Math.PI*2),t.fill(),t.shadowBlur=0,t.shadowOffsetY=0,t.fillStyle=s.progressColor||"rgba(255, 255, 255, 0.9)",t.beginPath(),t.arc(m,i,f*.4,0,Math.PI*2),t.fill()}}var $={bars:I,mirror:x,line:H,blocks:q,dots:U,seekbar:F};function z(t,e,o,r,s){($[s.waveformStyle]||I)(t,e,o,r,s)}function R(t){try{let e=t.getChannelData(0),o=t.sampleRate,r=Y(e,o);if(r.length<2)return 120;let s=[];for(let d=1;d<r.length;d++)s.push((r[d]-r[d-1])/o);let n={};s.forEach(d=>{let l=60/d,h=Math.round(l/3)*3;h>60&&h<200&&(n[h]=(n[h]||0)+1)});let a=0,i=120;for(let[d,l]of Object.entries(n))l>a&&(a=l,i=parseInt(d));return i<70&&n[i*2]?i*=2:i>160&&n[Math.round(i/2)]&&(i=Math.round(i/2)),i-1}catch(e){return console.warn("BPM detection failed:",e),null}}function Y(t,e){let s=[],n=0;for(let a=0;a<t.length-2048;a+=1024){let i=0;for(let h=a;h<a+2048;h++)i+=t[h]*t[h];i=i/2048;let d=i-n,l=n*1.8+.01;if(d>l&&i>.01){let h=s[s.length-1]||0,f=e*.15;a-h>f&&s.push(a)}n=i*.8+n*.2}return s}function N(t,e=200){let o=t.length/e,r=~~(o/10)||1,s=t.numberOfChannels,n=[];for(let i=0;i<s;i++){let d=t.getChannelData(i);for(let l=0;l<e;l++){let h=~~(l*o),f=~~(h+o),m=0,p=0;for(let g=h;g<f;g+=r){let u=d[g];u>p&&(p=u),u<m&&(m=u)}let c=Math.max(Math.abs(p),Math.abs(m));(i===0||c>n[l])&&(n[l]=c)}}let a=Math.max(...n);return a>0?n.map(i=>i/a):n}async function M(t,e=200,o=!1){let r=await fetch(t);if(!r.ok)throw new Error(`HTTP error! status: ${r.status}`);let s=await r.arrayBuffer(),n=window.AudioContext||window.webkitAudioContext,a=new n;try{let i=await a.decodeAudioData(s),l={peaks:N(i,e)};return o&&(l.bpm=R(i)),l}finally{await a.close()}}function A(t=200){let e=[];for(let o=0;o<t;o++){let r=Math.random()*.5+.3,s=Math.sin(o/t*Math.PI*4)*.2;e.push(Math.max(.1,Math.min(1,r+s)))}return e}var D={url:"",height:60,samples:200,waveformStyle:"mirror",barWidth:2,barSpacing:0,colorPreset:"dark",waveformColor:null,progressColor:null,buttonColor:null,buttonHoverColor:null,textColor:null,textSecondaryColor:null,backgroundColor:null,borderColor:null,autoplay:!1,showTime:!0,showHoverTime:!1,showBPM:!1,singlePlay:!0,playOnSeek:!0,title:null,subtitle:null,playIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',pauseIcon:'<svg viewBox="0 0 24 24" width="16" height="16"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',onLoad:null,onPlay:null,onPause:null,onEnd:null,onError:null,onTimeUpdate:null},O={bars:{barWidth:3,barSpacing:1},mirror:{barWidth:2,barSpacing:0},line:{barWidth:2,barSpacing:0},blocks:{barWidth:4,barSpacing:2},dots:{barWidth:3,barSpacing:3},seekbar:{barWidth:1,barSpacing:0}};var b=class t{static instances=new Map;static currentlyPlaying=null;constructor(e,o={}){if(this.container=typeof e=="string"?document.querySelector(e):e,!this.container)throw new Error("WaveformPlayer: Container element not found");let r=T(this.container);this.options=B(D,r,o);let s=O[this.options.waveformStyle];s&&(r.barWidth===void 0&&o.barWidth===void 0&&(this.options.barWidth=s.barWidth),r.barSpacing===void 0&&o.barSpacing===void 0&&(this.options.barSpacing=s.barSpacing)),this.options.waveformColor=this.options.waveformColor||"rgba(255, 255, 255, 0.3)",this.options.progressColor=this.options.progressColor||"rgba(255, 255, 255, 0.9)",this.options.buttonColor=this.options.buttonColor||"rgba(255, 255, 255, 0.9)",this.options.textColor=this.options.textColor||"#ffffff",this.options.textSecondaryColor=this.options.textSecondaryColor||"rgba(255, 255, 255, 0.6)",this.audio=null,this.canvas=null,this.ctx=null,this.waveformData=[],this.progress=0,this.isPlaying=!1,this.isLoading=!1,this.hasError=!1,this.updateTimer=null,this.resizeObserver=null,this.id=this.container.id||k(this.options.url),t.instances.set(this.id,this),this.init()}init(){this.createDOM(),this.createAudio(),this.bindEvents(),this.setupResizeObserver(),requestAnimationFrame(()=>{this.resizeCanvas(),this.options.url&&this.load(this.options.url).then(()=>{this.options.autoplay&&this.play()}).catch(e=>{console.error("Failed to load audio:",e)})})}createDOM(){this.container.innerHTML="",this.container.className="waveform-player",this.container.innerHTML=`
|
|
2
|
+
<div class="waveform-player-inner">
|
|
3
|
+
<div class="waveform-body">
|
|
4
|
+
<div class="waveform-track">
|
|
5
|
+
<button class="waveform-btn" aria-label="Play/Pause" style="
|
|
6
|
+
border-color: ${this.options.buttonColor};
|
|
7
|
+
color: ${this.options.buttonColor};
|
|
8
|
+
">
|
|
9
|
+
<span class="waveform-icon-play">${this.options.playIcon}</span>
|
|
10
|
+
<span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
|
|
11
|
+
</button>
|
|
12
|
+
|
|
13
|
+
<div class="waveform-container">
|
|
14
|
+
<canvas></canvas>
|
|
15
|
+
<div class="waveform-loading" style="display:none;"></div>
|
|
16
|
+
<div class="waveform-error" style="display:none;">
|
|
17
|
+
<span class="waveform-error-text">Unable to load audio</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="waveform-info">
|
|
23
|
+
<div class="waveform-text">
|
|
24
|
+
<span class="waveform-title" style="color: ${this.options.textColor};"></span>
|
|
25
|
+
${this.options.subtitle?`<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>`:""}
|
|
26
|
+
</div>
|
|
27
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
28
|
+
${this.options.showBPM?`
|
|
29
|
+
<span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
|
|
30
|
+
<span class="bpm-value">--</span> BPM
|
|
31
|
+
</span>
|
|
32
|
+
`:""}
|
|
33
|
+
${this.options.showTime?`
|
|
34
|
+
<span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
|
|
35
|
+
<span class="time-current">0:00</span> / <span class="time-total">0:00</span>
|
|
36
|
+
</span>
|
|
37
|
+
`:""}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
`,this.playBtn=this.container.querySelector(".waveform-btn"),this.canvas=this.container.querySelector("canvas"),this.ctx=this.canvas.getContext("2d"),this.titleEl=this.container.querySelector(".waveform-title"),this.subtitleEl=this.container.querySelector(".waveform-subtitle"),this.currentTimeEl=this.container.querySelector(".time-current"),this.totalTimeEl=this.container.querySelector(".time-total"),this.bpmEl=this.container.querySelector(".waveform-bpm"),this.bpmValueEl=this.container.querySelector(".bpm-value"),this.loadingEl=this.container.querySelector(".waveform-loading"),this.errorEl=this.container.querySelector(".waveform-error"),this.resizeCanvas()}createAudio(){this.audio=new Audio,this.audio.preload="metadata",this.audio.crossOrigin="anonymous"}bindEvents(){this.playBtn.addEventListener("click",()=>this.togglePlay()),this.audio.addEventListener("loadstart",()=>this.setLoading(!0)),this.audio.addEventListener("loadedmetadata",()=>this.onMetadataLoaded()),this.audio.addEventListener("canplay",()=>this.setLoading(!1)),this.audio.addEventListener("play",()=>this.onPlay()),this.audio.addEventListener("pause",()=>this.onPause()),this.audio.addEventListener("ended",()=>this.onEnded()),this.audio.addEventListener("error",e=>this.onError(e)),this.canvas.addEventListener("click",e=>this.handleCanvasClick(e)),window.addEventListener("resize",L(()=>this.resizeCanvas(),100))}setupResizeObserver(){"ResizeObserver"in window&&(this.resizeObserver=new ResizeObserver(()=>{this.resizeCanvas()}),this.canvas?.parentElement&&this.resizeObserver.observe(this.canvas.parentElement))}async load(e){try{this.setLoading(!0),this.progress=0,this.hasError=!1,this.audio.src=e,await new Promise((r,s)=>{let n=()=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",a),r()},a=i=>{this.audio.removeEventListener("loadedmetadata",n),this.audio.removeEventListener("error",a),s(i)};this.audio.addEventListener("loadedmetadata",n),this.audio.addEventListener("error",a)});let o=this.options.title||W(e);if(this.titleEl&&(this.titleEl.textContent=o),this.options.waveform)this.setWaveformData(this.options.waveform);else try{let r=await M(e,this.options.samples,this.options.showBPM);this.waveformData=r.peaks,r.bpm&&(this.detectedBPM=r.bpm,this.updateBPMDisplay())}catch(r){console.warn("Using placeholder waveform:",r),this.waveformData=A(this.options.samples)}this.drawWaveform(),this.options.onLoad&&this.options.onLoad(this)}catch(o){console.error("Failed to load audio:",o),this.onError(o)}finally{this.setLoading(!1)}}setWaveformData(e){if(typeof e=="string")try{let o=JSON.parse(e);this.waveformData=Array.isArray(o)?o:[]}catch{this.waveformData=e.split(",").map(Number)}else this.waveformData=Array.isArray(e)?e:[];this.drawWaveform()}drawWaveform(){!this.ctx||this.waveformData.length===0||z(this.ctx,this.canvas,this.waveformData,this.progress,{...this.options,waveformStyle:this.options.waveformStyle||"bars",color:this.options.waveformColor,progressColor:this.options.progressColor})}resizeCanvas(){let e=window.devicePixelRatio||1,o=this.canvas.getBoundingClientRect();this.canvas.width=o.width*e,this.canvas.height=this.options.height*e,this.canvas.style.height=this.options.height+"px",this.canvas.parentElement.style.height=this.options.height+"px",this.drawWaveform()}handleCanvasClick(e){if(!this.audio.duration)return;let o=this.canvas.getBoundingClientRect(),r=e.clientX-o.left,s=Math.max(0,Math.min(1,r/o.width));this.seekToPercent(s)}setLoading(e){this.isLoading=e,this.loadingEl&&(this.loadingEl.style.display=e?"block":"none")}onMetadataLoaded(){this.totalTimeEl&&(this.totalTimeEl.textContent=P(this.audio.duration))}onPlay(){this.isPlaying=!0,this.playBtn.classList.add("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),o=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="none"),o&&(o.style.display="flex"),this.startSmoothUpdate(),this.options.onPlay&&this.options.onPlay(this)}onPause(){this.isPlaying=!1,this.playBtn.classList.remove("playing");let e=this.playBtn.querySelector(".waveform-icon-play"),o=this.playBtn.querySelector(".waveform-icon-pause");e&&(e.style.display="flex"),o&&(o.style.display="none"),this.stopSmoothUpdate(),this.options.onPause&&this.options.onPause(this)}onEnded(){this.progress=0,this.audio.currentTime=0,this.drawWaveform(),this.currentTimeEl&&(this.currentTimeEl.textContent="0:00"),this.onPause(),this.options.onEnd&&this.options.onEnd(this)}onError(e){console.error("Audio error:",e),this.hasError=!0,this.setLoading(!1),this.errorEl&&(this.errorEl.style.display="flex"),this.canvas&&(this.canvas.style.opacity="0.2"),this.playBtn&&(this.playBtn.disabled=!0),this.options.onError&&this.options.onError(e,this)}startSmoothUpdate(){this.stopSmoothUpdate();let e=()=>{this.isPlaying&&this.audio.duration&&(this.updateProgress(),this.updateTimer=requestAnimationFrame(e))};this.updateTimer=requestAnimationFrame(e)}stopSmoothUpdate(){this.updateTimer&&(cancelAnimationFrame(this.updateTimer),this.updateTimer=null)}updateProgress(){if(!this.audio.duration)return;let e=this.audio.currentTime/this.audio.duration;Math.abs(e-this.progress)>.001&&(this.progress=e,this.drawWaveform()),this.currentTimeEl&&(this.currentTimeEl.textContent=P(this.audio.currentTime)),this.options.onTimeUpdate&&this.options.onTimeUpdate(this.audio.currentTime,this.audio.duration,this)}updateBPMDisplay(){this.bpmEl&&this.bpmValueEl&&this.detectedBPM&&(this.bpmValueEl.textContent=Math.round(this.detectedBPM),this.bpmEl.style.display="inline-flex")}play(){this.options.singlePlay&&t.currentlyPlaying&&t.currentlyPlaying!==this&&t.currentlyPlaying.pause(),t.currentlyPlaying=this,this.audio.play()}pause(){t.currentlyPlaying===this&&(t.currentlyPlaying=null),this.audio.pause()}togglePlay(){this.isPlaying?this.pause():this.play()}seekToPercent(e){this.audio&&this.audio.duration&&(this.audio.currentTime=this.audio.duration*Math.max(0,Math.min(1,e)),this.updateProgress())}setVolume(e){this.audio&&(this.audio.volume=Math.max(0,Math.min(1,e)))}destroy(){this.pause(),this.stopSmoothUpdate(),this.resizeObserver&&this.resizeObserver.disconnect(),t.instances.delete(this.id),this.audio&&(this.audio.src=""),this.container.innerHTML=""}static getInstance(e){if(typeof e=="string"){let o=this.instances.get(e);if(o)return o;let r=document.getElementById(e);if(r)return Array.from(this.instances.values()).find(s=>s.container===r)}if(e instanceof HTMLElement)return Array.from(this.instances.values()).find(o=>o.container===e)}static getAllInstances(){return Array.from(this.instances.values())}static destroyAll(){this.instances.forEach(e=>e.destroy()),this.instances.clear()}static async generateWaveformData(e,o=200){try{return(await M(e,o)).peaks}catch(r){throw console.error("Failed to generate waveform:",r),r}}};function E(){if(typeof document>"u")return;document.querySelectorAll("[data-waveform-player]").forEach(e=>{if(e.dataset.waveformInitialized!=="true")try{new b(e),e.dataset.waveformInitialized="true"}catch(o){console.error("Failed to initialize WaveformPlayer:",o,e)}})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",E):E());b.init=E;typeof window<"u"&&(window.WaveformPlayer=b);var st=b;export{b as WaveformPlayer,st as default};
|