@arraypress/waveform-player 1.7.1 → 1.8.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/README.md +179 -366
- package/dist/waveform-player.cjs +2207 -0
- package/dist/waveform-player.cjs.map +7 -0
- package/dist/waveform-player.css +1 -1
- package/dist/waveform-player.esm.js +8 -8
- package/dist/waveform-player.esm.js.map +7 -0
- package/dist/waveform-player.js +676 -260
- package/dist/waveform-player.min.js +8 -8
- package/dist/waveform-player.min.js.map +7 -0
- package/index.d.ts +344 -0
- package/package.json +18 -8
- package/src/css/waveform-player.css +26 -3
- package/src/js/audio.js +61 -25
- package/src/js/bpm.js +26 -5
- package/src/js/core.js +557 -170
- package/src/js/drawing.js +208 -44
- package/src/js/index.js +56 -11
- package/src/js/themes.js +95 -47
- package/src/js/utils.js +231 -65
package/README.md
CHANGED
|
@@ -1,173 +1,122 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
A lightweight, customizable audio player with waveform visualization. Under 8KB gzipped.
|
|
1
|
+
<div align="center">
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
*[NPM Package](https://www.npmjs.com/package/@arraypress/waveform-player)**
|
|
3
|
+
# WaveformPlayer
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-

|
|
11
|
-

|
|
5
|
+
**Zero-config audio waveforms for the web.**
|
|
6
|
+
Add a `data-` attribute to a `<div>` — get a real, interactive waveform player. No build step, no dependencies, ~10KB.
|
|
12
7
|
|
|
13
|
-
](https://www.npmjs.com/package/@arraypress/waveform-player)
|
|
9
|
+
[](https://bundlephobia.com/package/@arraypress/waveform-player)
|
|
10
|
+
[](https://www.npmjs.com/package/@arraypress/waveform-player)
|
|
11
|
+
[](./index.d.ts)
|
|
12
|
+
[](./LICENSE)
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
[Live demo](https://waveformplayer.com) · [Docs](https://waveformplayer.com/#docs) · [npm](https://www.npmjs.com/package/@arraypress/waveform-player)
|
|
16
15
|
|
|
17
|
-
-
|
|
18
|
-
- **Tiny** - ~8KB gzipped vs 40KB+ for alternatives
|
|
19
|
-
- **Real Waveforms** - Actual audio analysis, not fake waves
|
|
20
|
-
- **No Dependencies** - No jQuery, no bloat, pure vanilla JS
|
|
21
|
-
- **Works Everywhere** - WordPress, Shopify, React, Vue, or plain HTML
|
|
22
|
-
- **Ecosystem** - Optional [WaveformBar](https://github.com/arraypress/waveform-bar) persistent player addon available
|
|
16
|
+

|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
</div>
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
---
|
|
27
21
|
|
|
28
|
-
## Quick
|
|
22
|
+
## Quick start
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
No build tools. No initialization. Drop in two files and add a `<div>`.
|
|
31
25
|
|
|
32
26
|
```html
|
|
33
|
-
|
|
34
|
-
<
|
|
27
|
+
<link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-player/dist/waveform-player.css">
|
|
28
|
+
<script src="https://unpkg.com/@arraypress/waveform-player/dist/waveform-player.min.js"></script>
|
|
29
|
+
|
|
30
|
+
<div data-waveform-player data-url="track.mp3" data-title="My Song"></div>
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
It auto-initializes every `[data-waveform-player]` on the page when the DOM is ready. That's it.
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
## Install
|
|
40
36
|
|
|
41
37
|
```bash
|
|
42
38
|
npm install @arraypress/waveform-player
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
```js
|
|
42
|
+
import WaveformPlayer from '@arraypress/waveform-player';
|
|
43
|
+
import '@arraypress/waveform-player/styles.css';
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
<script src="https://unpkg.com/@arraypress/waveform-player@latest/dist/waveform-player.min.js"></script>
|
|
45
|
+
const player = new WaveformPlayer('#player', { url: 'track.mp3' });
|
|
51
46
|
```
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
Ships ESM + CommonJS + a standalone IIFE for `<script>` tags, with bundled TypeScript definitions.
|
|
54
49
|
|
|
55
|
-
|
|
50
|
+
## Why
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
| | WaveformPlayer | WaveSurfer.js | Amplitude.js |
|
|
53
|
+
| ------------------- | :------------: | :-----------: | :----------: |
|
|
54
|
+
| Size (gzipped) | **~10KB** | 40KB+ | 35KB+ |
|
|
55
|
+
| Zero-config (HTML) | ✓ | — | — |
|
|
56
|
+
| Dependencies | None | None | None |
|
|
57
|
+
| Real waveforms | ✓ | ✓ | — |
|
|
58
|
+
| TypeScript types | ✓ | ✓ | — |
|
|
59
|
+
| Keyboard + ARIA | ✓ | partial | — |
|
|
60
|
+
| Media Session API | ✓ | — | — |
|
|
60
61
|
|
|
61
62
|
## Features
|
|
62
63
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- 💾 **Waveform Caching** - Pre-generate waveforms for performance
|
|
71
|
-
- 🌐 **Framework Agnostic** - Works with React, Vue, Angular, or vanilla JS
|
|
72
|
-
- ⌨️ **Keyboard Controls** - Full keyboard navigation support
|
|
73
|
-
- 📱 **Media Session API** - System media controls, lock screen integration
|
|
74
|
-
- ⏩ **Speed Control** - Adjustable playback rate for podcasts/audiobooks
|
|
75
|
-
- 📍 **Chapter Markers** - Add clickable markers for navigation
|
|
76
|
-
- 🔄 **Dynamic Loading** - Load new tracks without page refresh
|
|
77
|
-
- 🎨 **Auto Theme Detection** - Adapts to light/dark themes automatically
|
|
64
|
+
- **Zero-config** — works from plain HTML via `data-` attributes; no JS required.
|
|
65
|
+
- **6 visual styles** — bars, mirror, line, blocks, dots, seekbar — plus rounded caps and gradient fills.
|
|
66
|
+
- **Real audio analysis** — decodes peaks with the Web Audio API, or uses pre-generated data.
|
|
67
|
+
- **Accessible** — the waveform is a keyboard-operable ARIA slider out of the box.
|
|
68
|
+
- **Framework-ready** — first-party [React](https://github.com/arraypress/waveform-player-react) and [Astro](https://github.com/arraypress/waveform-player-astro) wrappers; works anywhere otherwise.
|
|
69
|
+
- **Batteries included** — chapter markers, BPM detection, Media Session, speed control, auto theme detection.
|
|
70
|
+
- **Tiny** — ~10KB gzipped, zero dependencies, TypeScript types bundled.
|
|
78
71
|
|
|
79
|
-
##
|
|
72
|
+
## Visual styles
|
|
80
73
|
|
|
81
|
-
|
|
74
|
+
Set with `data-style` (or the longer `data-waveform-style`), or the `style` / `waveformStyle` option.
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
| Style | Look |
|
|
77
|
+
| --------- | ----------------------------------- |
|
|
78
|
+
| `mirror` | Symmetrical, SoundCloud-style (default) |
|
|
79
|
+
| `bars` | Classic bottom-anchored bars |
|
|
80
|
+
| `line` | Smooth oscilloscope line |
|
|
81
|
+
| `blocks` | Segmented LED meter |
|
|
82
|
+
| `dots` | Circular points |
|
|
83
|
+
| `seekbar` | Minimal progress bar, no peaks |
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<div data-wb-play
|
|
89
|
-
data-url="song.mp3"
|
|
90
|
-
data-title="My Track"
|
|
91
|
-
data-artist="Artist"
|
|
92
|
-
data-bpm="128"
|
|
93
|
-
data-key="Am">
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<script>
|
|
97
|
-
WaveformBar.init();
|
|
98
|
-
</script>
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
[Learn more →](https://github.com/arraypress/waveform-bar)
|
|
102
|
-
|
|
103
|
-
### WaveformPlaylist (Optional Addon)
|
|
104
|
-
|
|
105
|
-
Add playlist and chapter support with zero JavaScript:
|
|
106
|
-
|
|
107
|
-
```html
|
|
108
|
-
|
|
109
|
-
<div data-waveform-playlist data-continuous="true">
|
|
110
|
-
<div data-track data-url="song1.mp3" data-title="Track 1">
|
|
111
|
-
<div data-chapter data-time="0:00">Intro</div>
|
|
112
|
-
<div data-chapter data-time="2:30">Verse</div>
|
|
113
|
-
</div>
|
|
114
|
-
<div data-track data-url="song2.mp3" data-title="Track 2"></div>
|
|
115
|
-
</div>
|
|
116
|
-
```
|
|
85
|
+
**Modern caps & gradients** (bundle-neutral):
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
WaveformTracker.init({
|
|
126
|
-
endpoint: '/api/analytics',
|
|
127
|
-
events: {
|
|
128
|
-
listen: 30 // Track after 30 seconds of listening
|
|
129
|
-
}
|
|
87
|
+
```js
|
|
88
|
+
new WaveformPlayer('#player', {
|
|
89
|
+
url: 'track.mp3',
|
|
90
|
+
waveformStyle: 'mirror',
|
|
91
|
+
barRadius: 3, // rounded bar caps
|
|
92
|
+
waveformColor: ['#fafafa', '#71717a'], // vertical gradient (array of stops)
|
|
93
|
+
progressColor: ['#ffffff', '#a1a1aa'],
|
|
130
94
|
});
|
|
131
95
|
```
|
|
132
96
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
## Comparison
|
|
136
|
-
|
|
137
|
-
| Feature | WaveformPlayer | WaveSurfer.js | Amplitude.js |
|
|
138
|
-
|-------------------|----------------|---------------|--------------|
|
|
139
|
-
| Size (gzipped) | ~8KB | 40KB+ | 35KB+ |
|
|
140
|
-
| Zero Config | ✅ | ❌ | ❌ |
|
|
141
|
-
| Dependencies | None | None | None |
|
|
142
|
-
| Waveform Styles | 6 | 3 | N/A |
|
|
143
|
-
| Setup Time | 30 seconds | 5+ minutes | 5+ minutes |
|
|
144
|
-
| Real Waveforms | ✅ | ✅ | ❌ |
|
|
145
|
-
| Keyboard Controls | ✅ | ✅ | ❌ |
|
|
146
|
-
| Media Session API | ✅ | ❌ | ❌ |
|
|
147
|
-
| Speed Control | ✅ | ✅ | ❌ |
|
|
97
|
+
In zero-config markup, pass a gradient as a JSON array: `data-waveform-color='["#fafafa","#71717a"]'`.
|
|
148
98
|
|
|
149
99
|
## Usage
|
|
150
100
|
|
|
151
|
-
### HTML (
|
|
101
|
+
### HTML (zero JavaScript)
|
|
152
102
|
|
|
153
103
|
```html
|
|
154
|
-
|
|
155
104
|
<div data-waveform-player
|
|
156
|
-
data-url="
|
|
105
|
+
data-url="track.mp3"
|
|
157
106
|
data-title="My Song"
|
|
158
107
|
data-subtitle="Artist Name"
|
|
159
108
|
data-waveform-style="mirror"
|
|
109
|
+
data-bar-radius="3"
|
|
160
110
|
data-show-playback-speed="true"
|
|
161
111
|
data-markers='[{"time": 30, "label": "Chorus"}]'>
|
|
162
112
|
</div>
|
|
163
113
|
```
|
|
164
114
|
|
|
165
|
-
### Waveform
|
|
115
|
+
### Waveform only (custom UI)
|
|
166
116
|
|
|
167
117
|
```html
|
|
168
|
-
<!-- Hide built-in controls for custom UI integration -->
|
|
169
118
|
<div data-waveform-player
|
|
170
|
-
data-url="
|
|
119
|
+
data-url="track.mp3"
|
|
171
120
|
data-show-controls="false"
|
|
172
121
|
data-show-info="false">
|
|
173
122
|
</div>
|
|
@@ -175,301 +124,165 @@ WaveformTracker.init({
|
|
|
175
124
|
|
|
176
125
|
### JavaScript API
|
|
177
126
|
|
|
178
|
-
```
|
|
127
|
+
```js
|
|
179
128
|
import WaveformPlayer from '@arraypress/waveform-player';
|
|
180
129
|
|
|
181
130
|
const player = new WaveformPlayer('#player', {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{time: 60, label: 'Chorus', color: '#f59e0b'}
|
|
190
|
-
]
|
|
131
|
+
url: 'track.mp3',
|
|
132
|
+
waveformStyle: 'mirror',
|
|
133
|
+
height: 80,
|
|
134
|
+
markers: [
|
|
135
|
+
{ time: 30, label: 'Verse', color: '#4ade80' },
|
|
136
|
+
{ time: 60, label: 'Chorus', color: '#f59e0b' },
|
|
137
|
+
],
|
|
191
138
|
});
|
|
192
139
|
```
|
|
193
140
|
|
|
194
|
-
## Visual Styles
|
|
195
|
-
|
|
196
|
-
Choose from 6 built-in styles:
|
|
197
|
-
|
|
198
|
-
- **bars** - Classic waveform bars
|
|
199
|
-
- **mirror** - SoundCloud-style mirrored waveform
|
|
200
|
-
- **line** - Smooth oscilloscope line
|
|
201
|
-
- **blocks** - LED meter blocks
|
|
202
|
-
- **dots** - Circular dots
|
|
203
|
-
- **seekbar** - Minimal progress bar
|
|
204
|
-
|
|
205
141
|
## Options
|
|
206
142
|
|
|
207
|
-
| Option | Type
|
|
208
|
-
|
|
209
|
-
| `url` | string
|
|
210
|
-
| `waveformStyle` | string
|
|
211
|
-
| `height` | number
|
|
212
|
-
| `barWidth` | number
|
|
213
|
-
| `barSpacing` | number
|
|
214
|
-
| `
|
|
215
|
-
| `waveformColor` | string |
|
|
216
|
-
| `progressColor` | string |
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
data-wb-title="..."></div>
|
|
247
|
-
```
|
|
143
|
+
| Option | Type | Default | Description |
|
|
144
|
+
| -------------------- | --------------------- | ----------- | ------------------------------------------------------- |
|
|
145
|
+
| `url` | `string` | `''` | Audio file URL (alias: `src` / `data-src`) |
|
|
146
|
+
| `waveformStyle` | `string` | `'mirror'` | `bars` · `mirror` · `line` · `blocks` · `dots` · `seekbar` |
|
|
147
|
+
| `height` | `number` | `60` | Waveform height (px) |
|
|
148
|
+
| `barWidth` | `number` | style-based | Bar width (px) |
|
|
149
|
+
| `barSpacing` | `number` | style-based | Gap between bars (px) |
|
|
150
|
+
| `barRadius` | `number` | `0` | Rounded bar-cap radius (px) — bars/mirror |
|
|
151
|
+
| `waveformColor` | `string \| string[]` | preset | Unplayed colour; array = vertical gradient |
|
|
152
|
+
| `progressColor` | `string \| string[]` | preset | Played colour; array = vertical gradient |
|
|
153
|
+
| `colorPreset` | `'dark' \| 'light' \| null` | `null` | Force a theme, or `null` to auto-detect |
|
|
154
|
+
| `samples` | `number` | `200` | Peak samples to extract |
|
|
155
|
+
| `waveform` | `number[] \| string` | `null` | Pre-generated peaks (array, `.json` URL, or JSON) |
|
|
156
|
+
| `audioMode` | `'self' \| 'external'`| `'self'` | Own the `<audio>`, or delegate (see below) |
|
|
157
|
+
| `markers` | `WaveformMarker[]` | `[]` | Chapter markers |
|
|
158
|
+
| `showControls` | `boolean` | `true` | Show the play/pause button |
|
|
159
|
+
| `showInfo` | `boolean` | `true` | Show title/subtitle/time |
|
|
160
|
+
| `showPlaybackSpeed` | `boolean` | `false` | Show the speed menu |
|
|
161
|
+
| `showBPM` | `boolean` | `false` | Detect + display BPM |
|
|
162
|
+
| `autoplay` | `boolean` | `false` | Play on load |
|
|
163
|
+
| `enableMediaSession` | `boolean` | `true` | System media controls (self mode) |
|
|
164
|
+
| `accessibleSeek` | `boolean` | `true` | Expose the waveform as a keyboard ARIA slider |
|
|
165
|
+
| `seekLabel` | `string` | `null` | Accessible name for the slider (falls back to title) |
|
|
166
|
+
|
|
167
|
+
Full option types ship in [`index.d.ts`](./index.d.ts).
|
|
168
|
+
|
|
169
|
+
## Accessibility
|
|
170
|
+
|
|
171
|
+
By default the waveform is a keyboard-operable [ARIA slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/): `.waveform-container` gets `role="slider"`, joins the tab order, and reports `aria-valuemin`/`max`/`now` plus a readable `aria-valuetext` (e.g. `"0:30 of 2:00"`). When focused:
|
|
172
|
+
|
|
173
|
+
| Key | Action |
|
|
174
|
+
| -------------------------------------------- | ------------ |
|
|
175
|
+
| <kbd>←</kbd> / <kbd>↓</kbd> · <kbd>→</kbd> / <kbd>↑</kbd> | Seek ∓5s |
|
|
176
|
+
| <kbd>Page Down</kbd> / <kbd>Page Up</kbd> | Seek ∓10s |
|
|
177
|
+
| <kbd>Home</kbd> / <kbd>End</kbd> | Start / end |
|
|
178
|
+
|
|
179
|
+
Works in both audio modes, respects `prefers-reduced-motion`, and announces load errors to screen readers. Opt out with `accessibleSeek: false`; localize with `seekLabel`.
|
|
180
|
+
|
|
181
|
+
## API
|
|
248
182
|
|
|
249
183
|
```js
|
|
250
|
-
//
|
|
251
|
-
const player = new WaveformPlayer(el, { audioMode: 'external' });
|
|
252
|
-
|
|
253
|
-
// Listen for the play action and route to your own audio source:
|
|
254
|
-
el.addEventListener('waveformplayer:request-play', (e) => {
|
|
255
|
-
// e.detail = { url, title, subtitle, artist, artwork, id, player }
|
|
256
|
-
yourAudio.src = e.detail.url;
|
|
257
|
-
yourAudio.play();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Drive the visualization from your audio's timeupdate:
|
|
261
|
-
yourAudio.addEventListener('timeupdate', () => {
|
|
262
|
-
player.setProgress(yourAudio.currentTime, yourAudio.duration);
|
|
263
|
-
});
|
|
264
|
-
yourAudio.addEventListener('play', () => player.setPlayingState(true));
|
|
265
|
-
yourAudio.addEventListener('pause', () => player.setPlayingState(false));
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Events dispatched in external mode (all bubble; all cancelable):
|
|
269
|
-
|
|
270
|
-
| Event | Detail |
|
|
271
|
-
|------------------------------------|-----------------------------------------------------|
|
|
272
|
-
| `waveformplayer:request-play` | `{ url, title, subtitle, artist, artwork, id, player }` |
|
|
273
|
-
| `waveformplayer:request-pause` | Same shape as request-play |
|
|
274
|
-
| `waveformplayer:request-seek` | Same shape + `{ percent: 0..1 }` |
|
|
275
|
-
|
|
276
|
-
## API Methods
|
|
277
|
-
|
|
278
|
-
```javascript
|
|
279
|
-
// Control playback
|
|
280
|
-
player.play();
|
|
184
|
+
player.play(); // returns the <audio>.play() promise in self mode
|
|
281
185
|
player.pause();
|
|
282
186
|
player.togglePlay();
|
|
283
187
|
|
|
284
|
-
//
|
|
285
|
-
player.
|
|
286
|
-
player.
|
|
287
|
-
|
|
288
|
-
// Volume
|
|
289
|
-
player.setVolume(0.8); // 80% volume
|
|
290
|
-
|
|
291
|
-
// Speed
|
|
292
|
-
player.setPlaybackRate(1.5); // 1.5x speed
|
|
293
|
-
|
|
294
|
-
// External-mode state pumps (only useful when audioMode === 'external')
|
|
295
|
-
player.setPlayingState(true); // toggle play/pause UI state
|
|
296
|
-
player.setProgress(currentTime, duration); // sync canvas + time displays
|
|
188
|
+
player.seekTo(30); // seconds
|
|
189
|
+
player.seekToPercent(0.5);
|
|
190
|
+
player.setVolume(0.8);
|
|
191
|
+
player.setPlaybackRate(1.5);
|
|
297
192
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
markers: [{time: 30, label: 'Chorus'}],
|
|
301
|
-
artwork: 'cover.jpg',
|
|
302
|
-
waveform: [0.2, 0.5, 0.8, 0.3] // Optional pre-generated data
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Destroy
|
|
306
|
-
player.destroy();
|
|
307
|
-
```
|
|
193
|
+
await player.loadTrack('next.mp3', 'Title', 'Artist', { artwork: 'cover.jpg' });
|
|
194
|
+
player.destroy(); // removes all listeners + DOM
|
|
308
195
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
onPlay: (player) => console.log('Playing'),
|
|
316
|
-
onPause: (player) => console.log('Paused'),
|
|
317
|
-
onEnd: (player) => console.log('Ended'),
|
|
318
|
-
onTimeUpdate: (current, total, player) => {
|
|
319
|
-
console.log(`${current}/${total}`);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
196
|
+
// Statics
|
|
197
|
+
WaveformPlayer.getInstance('id');
|
|
198
|
+
WaveformPlayer.getAllInstances();
|
|
199
|
+
WaveformPlayer.destroyAll();
|
|
200
|
+
const peaks = await WaveformPlayer.generateWaveformData('track.mp3');
|
|
201
|
+
const jsonUrl = WaveformPlayer.getPeaksUrl('track.mp3'); // -> 'track.json'
|
|
322
202
|
```
|
|
323
203
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
When a player is focused (click on it):
|
|
327
|
-
|
|
328
|
-
- `Space` - Play/pause
|
|
329
|
-
- `←/→` - Seek backward/forward 5 seconds
|
|
330
|
-
- `↑/↓` - Volume up/down
|
|
331
|
-
- `M` - Mute/unmute
|
|
332
|
-
- `0-9` - Jump to 0%-90% of track
|
|
333
|
-
|
|
334
|
-
## Advanced Usage
|
|
335
|
-
|
|
336
|
-
### Pre-generated Waveforms
|
|
337
|
-
|
|
338
|
-
For better performance, generate waveform data server-side:
|
|
204
|
+
### Events
|
|
339
205
|
|
|
340
|
-
|
|
341
|
-
// Generate waveform data once
|
|
342
|
-
const waveformData = await WaveformPlayer.generateWaveformData('audio.mp3');
|
|
206
|
+
Lifecycle callbacks via options, or DOM events on the container (all bubble, `e.detail` is typed):
|
|
343
207
|
|
|
344
|
-
|
|
208
|
+
```js
|
|
345
209
|
new WaveformPlayer('#player', {
|
|
346
|
-
|
|
347
|
-
|
|
210
|
+
url: 'track.mp3',
|
|
211
|
+
onPlay: (player) => {},
|
|
212
|
+
onPause: (player) => {},
|
|
213
|
+
onEnd: (player) => {},
|
|
214
|
+
onTimeUpdate: (currentTime, duration, player) => {},
|
|
348
215
|
});
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
**Build-time pipeline:** when you generate peaks ahead of time with [`@arraypress/waveform-gen`](https://github.com/arraypress/waveform-gen) and store the JSON alongside each MP3 (`/audio/track.mp3` ↔ `/audio/track.json`), `WaveformPlayer.getPeaksUrl()` derives the JSON URL by swapping the extension:
|
|
352
216
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
url: track.audioUrl,
|
|
356
|
-
waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),
|
|
217
|
+
el.addEventListener('waveformplayer:timeupdate', (e) => {
|
|
218
|
+
const { currentTime, duration, progress } = e.detail;
|
|
357
219
|
});
|
|
358
220
|
```
|
|
359
221
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
### Multiple Players
|
|
363
|
-
|
|
364
|
-
```javascript
|
|
365
|
-
// Get all instances
|
|
366
|
-
const players = WaveformPlayer.getAllInstances();
|
|
222
|
+
## External audio mode
|
|
367
223
|
|
|
368
|
-
|
|
369
|
-
const player = WaveformPlayer.getInstance('my-player');
|
|
224
|
+
Set `audioMode: 'external'` and the player becomes a visualization-only surface: play/pause/seek dispatch cancelable events, and you drive the canvas back with `setPlayingState()` / `setProgress()`. This is how [`@arraypress/waveform-bar`](https://github.com/arraypress/waveform-bar) turns many inline players into one persistent bar.
|
|
370
225
|
|
|
371
|
-
|
|
372
|
-
WaveformPlayer
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Custom Styling
|
|
376
|
-
|
|
377
|
-
```css
|
|
378
|
-
.waveform-player {
|
|
379
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
380
|
-
border-radius: 12px;
|
|
381
|
-
}
|
|
226
|
+
```js
|
|
227
|
+
const player = new WaveformPlayer(el, { audioMode: 'external' });
|
|
382
228
|
|
|
383
|
-
.
|
|
384
|
-
|
|
385
|
-
|
|
229
|
+
el.addEventListener('waveformplayer:request-play', (e) => {
|
|
230
|
+
audio.src = e.detail.url; // e.detail = { url, title, subtitle, artist, artwork, id, player }
|
|
231
|
+
audio.play();
|
|
232
|
+
});
|
|
233
|
+
audio.addEventListener('timeupdate', () => player.setProgress(audio.currentTime, audio.duration));
|
|
234
|
+
audio.addEventListener('play', () => player.setPlayingState(true));
|
|
235
|
+
audio.addEventListener('pause', () => player.setPlayingState(false));
|
|
386
236
|
```
|
|
387
237
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
import {useEffect, useRef} from 'react';
|
|
394
|
-
import WaveformPlayer from '@arraypress/waveform-player';
|
|
395
|
-
|
|
396
|
-
function AudioPlayer({url}) {
|
|
397
|
-
const playerRef = useRef();
|
|
238
|
+
| Event | Detail |
|
|
239
|
+
| ------------------------------ | --------------------------------------------------- |
|
|
240
|
+
| `waveformplayer:request-play` | `{ url, title, subtitle, artist, artwork, id, player }` |
|
|
241
|
+
| `waveformplayer:request-pause` | Same shape |
|
|
242
|
+
| `waveformplayer:request-seek` | Same shape + `{ percent: 0..1 }` |
|
|
398
243
|
|
|
399
|
-
|
|
400
|
-
const player = new WaveformPlayer(playerRef.current, {url});
|
|
401
|
-
return () => player.destroy();
|
|
402
|
-
}, [url]);
|
|
244
|
+
## Frameworks
|
|
403
245
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
246
|
+
| Environment | Package |
|
|
247
|
+
| ----------- | ------- |
|
|
248
|
+
| React | [`@arraypress/waveform-player-react`](https://github.com/arraypress/waveform-player-react) |
|
|
249
|
+
| Astro | [`@arraypress/waveform-player-astro`](https://github.com/arraypress/waveform-player-astro) |
|
|
250
|
+
| Vanilla / anything else | this package — `new WaveformPlayer(el, opts)` and `player.destroy()` on teardown |
|
|
407
251
|
|
|
408
|
-
|
|
252
|
+
## Ecosystem
|
|
409
253
|
|
|
410
|
-
|
|
254
|
+
- **[WaveformBar](https://github.com/arraypress/waveform-bar)** — persistent bottom-bar player with queue, favorites, and cross-page session.
|
|
255
|
+
- **[WaveformPlaylist](https://github.com/arraypress/waveform-playlist)** — zero-JS playlists and chapters.
|
|
256
|
+
- **[WaveformGen](https://github.com/arraypress/waveform-gen)** — pre-generate peak JSON at build time.
|
|
257
|
+
- **[WaveformTracker](https://github.com/arraypress/waveform-tracker)** — audio engagement analytics.
|
|
411
258
|
|
|
412
|
-
|
|
413
|
-
<div ref="player"></div>
|
|
414
|
-
</template>
|
|
259
|
+
## Pre-generated waveforms
|
|
415
260
|
|
|
416
|
-
|
|
417
|
-
import WaveformPlayer from '@arraypress/waveform-player';
|
|
261
|
+
Skip client-side decoding by storing peaks next to each file (`track.mp3` ↔ `track.json`):
|
|
418
262
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
},
|
|
425
|
-
beforeDestroy() {
|
|
426
|
-
this.player?.destroy();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
</script>
|
|
263
|
+
```js
|
|
264
|
+
new WaveformPlayer('#player', {
|
|
265
|
+
url: track.audioUrl,
|
|
266
|
+
waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),
|
|
267
|
+
});
|
|
430
268
|
```
|
|
431
269
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
- Chrome/Edge 90+
|
|
435
|
-
- Firefox 88+
|
|
436
|
-
- Safari 14+
|
|
437
|
-
- Mobile browsers (iOS Safari, Chrome Android)
|
|
270
|
+
`getPeaksUrl()` swaps a known audio extension (`mp3`, `wav`, `ogg`, `flac`, `m4a`, `aac`) for `.json`, preserving query/hash, and returns `undefined` for anything else (so the player falls back to live decoding).
|
|
438
271
|
|
|
439
|
-
##
|
|
272
|
+
## Browser support
|
|
440
273
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
- All visual styles
|
|
444
|
-
- Custom styling examples
|
|
445
|
-
- Event handling
|
|
446
|
-
- Player builder
|
|
447
|
-
- BPM detection
|
|
448
|
-
- Pre-generated waveforms
|
|
449
|
-
- Keyboard navigation
|
|
450
|
-
- Speed controls
|
|
451
|
-
- Chapter markers
|
|
274
|
+
Chrome/Edge 90+, Firefox 88+, Safari 14+, and modern mobile browsers. Rounded bar caps use `roundRect` where available (Safari 16+) and fall back to square bars elsewhere.
|
|
452
275
|
|
|
453
276
|
## Development
|
|
454
277
|
|
|
455
278
|
```bash
|
|
456
|
-
# Install dependencies
|
|
457
279
|
npm install
|
|
458
|
-
|
|
459
|
-
#
|
|
460
|
-
npm run
|
|
461
|
-
|
|
462
|
-
# Build
|
|
463
|
-
npm run build
|
|
464
|
-
|
|
465
|
-
# Check size
|
|
466
|
-
npm run size
|
|
280
|
+
npm run dev # watch build
|
|
281
|
+
npm test # vitest
|
|
282
|
+
npm run build # all bundles
|
|
283
|
+
npm run size # gzipped size
|
|
467
284
|
```
|
|
468
285
|
|
|
469
286
|
## License
|
|
470
287
|
|
|
471
|
-
MIT © [ArrayPress](https://github.com/arraypress)
|
|
472
|
-
|
|
473
|
-
## Credits
|
|
474
|
-
|
|
475
|
-
Created by [David Sherlock](https://github.com/arraypress)
|
|
288
|
+
MIT © [ArrayPress](https://github.com/arraypress) · created by [David Sherlock](https://github.com/arraypress)
|