wavesurfer.js 7.0.0-alpha.9 → 7.0.0-beta.10
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 +29 -0
- package/README.md +137 -20
- package/dist/base-plugin.d.ts +6 -4
- package/dist/base-plugin.js +9 -3
- package/dist/decoder.d.ts +8 -7
- package/dist/decoder.js +43 -38
- package/dist/draggable.d.ts +1 -0
- package/dist/draggable.js +59 -0
- package/dist/event-emitter.d.ts +16 -10
- package/dist/event-emitter.js +38 -16
- package/dist/fetcher.d.ts +6 -3
- package/dist/fetcher.js +9 -15
- package/dist/player.d.ts +31 -11
- package/dist/player.js +58 -31
- package/dist/plugins/envelope.d.ts +71 -0
- package/dist/plugins/envelope.js +326 -0
- package/dist/plugins/envelope.min.cjs +1 -0
- package/dist/plugins/minimap.d.ts +39 -0
- package/dist/plugins/minimap.js +114 -0
- package/dist/plugins/minimap.min.cjs +1 -0
- package/dist/plugins/record.d.ts +28 -0
- package/dist/plugins/record.js +132 -0
- package/dist/plugins/record.min.cjs +1 -0
- package/dist/plugins/regions.d.ts +84 -43
- package/dist/plugins/regions.js +330 -195
- package/dist/plugins/regions.min.cjs +1 -0
- package/dist/plugins/spectrogram-fft.d.ts +9 -0
- package/dist/plugins/spectrogram-fft.js +150 -0
- package/dist/plugins/spectrogram.d.ts +72 -0
- package/dist/plugins/spectrogram.js +341 -0
- package/dist/plugins/spectrogram.min.cjs +1 -0
- package/dist/plugins/timeline.d.ts +25 -9
- package/dist/plugins/timeline.js +65 -24
- package/dist/plugins/timeline.min.cjs +1 -0
- package/dist/renderer.d.ts +32 -26
- package/dist/renderer.js +363 -167
- package/dist/timer.d.ts +1 -1
- package/dist/wavesurfer.d.ts +154 -0
- package/dist/wavesurfer.js +230 -0
- package/dist/wavesurfer.min.cjs +1 -0
- package/package.json +61 -28
- package/dist/index.d.ts +0 -117
- package/dist/index.js +0 -227
- package/dist/player-webaudio.d.ts +0 -8
- package/dist/player-webaudio.js +0 -32
- package/dist/plugins/multitrack.d.ts +0 -44
- package/dist/plugins/multitrack.js +0 -260
- package/dist/plugins/xmultitrack.d.ts +0 -44
- package/dist/plugins/xmultitrack.js +0 -260
- package/dist/react/useWavesurfer.d.ts +0 -5
- package/dist/react/useWavesurfer.js +0 -20
- package/dist/wavesurfer.Multitrack.min.js +0 -1
- package/dist/wavesurfer.Regions.min.js +0 -1
- package/dist/wavesurfer.Timeline.min.js +0 -1
- package/dist/wavesurfer.min.js +0 -1
package/dist/renderer.js
CHANGED
|
@@ -1,28 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
1
|
+
import { makeDraggable } from './draggable.js';
|
|
10
2
|
import EventEmitter from './event-emitter.js';
|
|
11
3
|
class Renderer extends EventEmitter {
|
|
12
4
|
constructor(options) {
|
|
13
5
|
super();
|
|
14
|
-
this.
|
|
6
|
+
this.timeouts = [];
|
|
7
|
+
this.isScrolling = false;
|
|
8
|
+
this.audioData = null;
|
|
9
|
+
this.resizeObserver = null;
|
|
10
|
+
this.isDragging = false;
|
|
15
11
|
this.options = options;
|
|
16
|
-
let
|
|
17
|
-
if (typeof
|
|
18
|
-
|
|
12
|
+
let parent;
|
|
13
|
+
if (typeof options.container === 'string') {
|
|
14
|
+
parent = document.querySelector(options.container);
|
|
19
15
|
}
|
|
20
|
-
else if (
|
|
21
|
-
|
|
16
|
+
else if (options.container instanceof HTMLElement) {
|
|
17
|
+
parent = options.container;
|
|
22
18
|
}
|
|
23
|
-
if (!
|
|
19
|
+
if (!parent) {
|
|
24
20
|
throw new Error('Container not found');
|
|
25
21
|
}
|
|
22
|
+
this.parent = parent;
|
|
23
|
+
const [div, shadow] = this.initHtml();
|
|
24
|
+
parent.appendChild(div);
|
|
25
|
+
this.container = div;
|
|
26
|
+
this.scrollContainer = shadow.querySelector('.scroll');
|
|
27
|
+
this.wrapper = shadow.querySelector('.wrapper');
|
|
28
|
+
this.canvasWrapper = shadow.querySelector('.canvases');
|
|
29
|
+
this.progressWrapper = shadow.querySelector('.progress');
|
|
30
|
+
this.cursor = shadow.querySelector('.cursor');
|
|
31
|
+
this.initEvents();
|
|
32
|
+
}
|
|
33
|
+
initEvents() {
|
|
34
|
+
// Add a click listener
|
|
35
|
+
this.wrapper.addEventListener('click', (e) => {
|
|
36
|
+
const rect = this.wrapper.getBoundingClientRect();
|
|
37
|
+
const x = e.clientX - rect.left;
|
|
38
|
+
const relativeX = x / rect.width;
|
|
39
|
+
this.emit('click', relativeX);
|
|
40
|
+
});
|
|
41
|
+
// Drag
|
|
42
|
+
this.initDrag();
|
|
43
|
+
// Add a scroll listener
|
|
44
|
+
this.scrollContainer.addEventListener('scroll', () => {
|
|
45
|
+
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
|
|
46
|
+
const startX = scrollLeft / scrollWidth;
|
|
47
|
+
const endX = (scrollLeft + clientWidth) / scrollWidth;
|
|
48
|
+
this.emit('scroll', startX, endX);
|
|
49
|
+
});
|
|
50
|
+
// Re-render the waveform on container resize
|
|
51
|
+
const delay = this.createDelay(100);
|
|
52
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
53
|
+
delay(() => this.reRender());
|
|
54
|
+
});
|
|
55
|
+
this.resizeObserver.observe(this.scrollContainer);
|
|
56
|
+
}
|
|
57
|
+
initDrag() {
|
|
58
|
+
makeDraggable(this.wrapper,
|
|
59
|
+
// On drag
|
|
60
|
+
(_, __, x) => {
|
|
61
|
+
this.emit('drag', Math.max(0, Math.min(1, x / this.wrapper.clientWidth)));
|
|
62
|
+
},
|
|
63
|
+
// On start drag
|
|
64
|
+
() => (this.isDragging = true),
|
|
65
|
+
// On end drag
|
|
66
|
+
() => (this.isDragging = false));
|
|
67
|
+
}
|
|
68
|
+
getHeight() {
|
|
69
|
+
const defaultHeight = 128;
|
|
70
|
+
if (this.options.height == null)
|
|
71
|
+
return defaultHeight;
|
|
72
|
+
if (!isNaN(Number(this.options.height)))
|
|
73
|
+
return Number(this.options.height);
|
|
74
|
+
if (this.options.height === 'auto')
|
|
75
|
+
return this.parent.clientHeight || defaultHeight;
|
|
76
|
+
return defaultHeight;
|
|
77
|
+
}
|
|
78
|
+
initHtml() {
|
|
26
79
|
const div = document.createElement('div');
|
|
27
80
|
const shadow = div.attachShadow({ mode: 'open' });
|
|
28
81
|
shadow.innerHTML = `
|
|
@@ -36,204 +89,347 @@ class Renderer extends EventEmitter {
|
|
|
36
89
|
width: 100%;
|
|
37
90
|
position: relative;
|
|
38
91
|
}
|
|
92
|
+
:host .noScrollbar {
|
|
93
|
+
scrollbar-color: transparent;
|
|
94
|
+
scrollbar-width: none;
|
|
95
|
+
}
|
|
96
|
+
:host .noScrollbar::-webkit-scrollbar {
|
|
97
|
+
display: none;
|
|
98
|
+
-webkit-appearance: none;
|
|
99
|
+
}
|
|
39
100
|
:host .wrapper {
|
|
40
101
|
position: relative;
|
|
41
|
-
|
|
42
|
-
min-width: 100%;
|
|
102
|
+
overflow: visible;
|
|
43
103
|
z-index: 2;
|
|
44
104
|
}
|
|
105
|
+
:host .canvases {
|
|
106
|
+
min-height: ${this.getHeight()}px;
|
|
107
|
+
}
|
|
108
|
+
:host .canvases > div {
|
|
109
|
+
position: relative;
|
|
110
|
+
}
|
|
45
111
|
:host canvas {
|
|
46
112
|
display: block;
|
|
47
|
-
|
|
48
|
-
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 0;
|
|
49
115
|
image-rendering: pixelated;
|
|
50
116
|
}
|
|
51
117
|
:host .progress {
|
|
118
|
+
pointer-events: none;
|
|
52
119
|
position: absolute;
|
|
53
120
|
z-index: 2;
|
|
54
121
|
top: 0;
|
|
55
122
|
left: 0;
|
|
56
|
-
|
|
57
|
-
|
|
123
|
+
width: 0;
|
|
124
|
+
height: 100%;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
:host .progress > div {
|
|
128
|
+
position: relative;
|
|
58
129
|
}
|
|
59
130
|
:host .cursor {
|
|
131
|
+
pointer-events: none;
|
|
60
132
|
position: absolute;
|
|
61
|
-
z-index:
|
|
133
|
+
z-index: 5;
|
|
62
134
|
top: 0;
|
|
63
135
|
left: 0;
|
|
64
136
|
height: 100%;
|
|
65
|
-
border-
|
|
66
|
-
margin-left: -1px;
|
|
137
|
+
border-radius: 2px;
|
|
67
138
|
}
|
|
68
139
|
</style>
|
|
69
140
|
|
|
70
|
-
<div class="scroll">
|
|
141
|
+
<div class="scroll" part="scroll">
|
|
71
142
|
<div class="wrapper">
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
<div class="cursor"></div>
|
|
143
|
+
<div class="canvases"></div>
|
|
144
|
+
<div class="progress" part="progress"></div>
|
|
145
|
+
<div class="cursor" part="cursor"></div>
|
|
75
146
|
</div>
|
|
76
147
|
</div>
|
|
77
148
|
`;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const x = e.clientX - rect.left;
|
|
88
|
-
const relativeX = x / rect.width;
|
|
89
|
-
this.emit('click', { relativeX });
|
|
90
|
-
});
|
|
149
|
+
return [div, shadow];
|
|
150
|
+
}
|
|
151
|
+
setOptions(options) {
|
|
152
|
+
this.options = options;
|
|
153
|
+
// Re-render the waveform
|
|
154
|
+
this.reRender();
|
|
155
|
+
}
|
|
156
|
+
getWrapper() {
|
|
157
|
+
return this.wrapper;
|
|
91
158
|
}
|
|
92
|
-
|
|
93
|
-
return this.scrollContainer.
|
|
159
|
+
getScroll() {
|
|
160
|
+
return this.scrollContainer.scrollLeft;
|
|
94
161
|
}
|
|
95
162
|
destroy() {
|
|
96
163
|
this.container.remove();
|
|
164
|
+
this.resizeObserver?.disconnect();
|
|
97
165
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
createDelay(delayMs = 10) {
|
|
167
|
+
const context = {};
|
|
168
|
+
this.timeouts.push(context);
|
|
169
|
+
return (callback) => {
|
|
170
|
+
context.timeout && clearTimeout(context.timeout);
|
|
171
|
+
context.timeout = setTimeout(callback, delayMs);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// Convert array of color values to linear gradient
|
|
175
|
+
convertColorValues(color) {
|
|
176
|
+
if (!Array.isArray(color))
|
|
177
|
+
return color || '';
|
|
178
|
+
if (color.length < 2)
|
|
179
|
+
return color[0] || '';
|
|
180
|
+
const canvasElement = document.createElement('canvas');
|
|
181
|
+
const ctx = canvasElement.getContext('2d');
|
|
182
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, canvasElement.height);
|
|
183
|
+
const colorStopPercentage = 1 / (color.length - 1);
|
|
184
|
+
color.forEach((color, index) => {
|
|
185
|
+
const offset = index * colorStopPercentage;
|
|
186
|
+
gradient.addColorStop(offset, color);
|
|
106
187
|
});
|
|
188
|
+
return gradient;
|
|
107
189
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
for (let i = start; i < end; i++) {
|
|
134
|
-
const barIndex = Math.round(i * barIndexScale);
|
|
135
|
-
if (barIndex > prevX) {
|
|
136
|
-
const leftBarHeight = Math.round(prevLeft * halfHeight);
|
|
137
|
-
const rightBarHeight = Math.round(prevRight * halfHeight);
|
|
138
|
-
ctx.roundRect(prevX * (barWidth + barGap), halfHeight - leftBarHeight, barWidth, leftBarHeight + rightBarHeight || 1, barRadius);
|
|
139
|
-
prevX = barIndex;
|
|
140
|
-
prevLeft = 0;
|
|
141
|
-
prevRight = 0;
|
|
142
|
-
}
|
|
143
|
-
const leftValue = useNegative ? leftChannel[i] : Math.abs(leftChannel[i]);
|
|
144
|
-
const rightValue = useNegative ? rightChannel[i] : Math.abs(rightChannel[i]);
|
|
145
|
-
if (leftValue > prevLeft) {
|
|
146
|
-
prevLeft = leftValue;
|
|
147
|
-
}
|
|
148
|
-
// If stereo, both channels are drawn as max values
|
|
149
|
-
// If mono with negative values, the bottom channel will be the min negative values
|
|
150
|
-
if (useNegative ? rightValue < -prevRight : rightValue > prevRight) {
|
|
151
|
-
prevRight = rightValue < 0 ? -rightValue : rightValue;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
ctx.fill();
|
|
155
|
-
ctx.closePath();
|
|
156
|
-
};
|
|
157
|
-
// Clear the canvas
|
|
158
|
-
ctx.clearRect(0, 0, width, height);
|
|
159
|
-
// Draw the currently visible part of the waveform
|
|
160
|
-
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
|
|
161
|
-
const scale = len / scrollWidth;
|
|
162
|
-
const start = Math.floor(scrollLeft * scale);
|
|
163
|
-
const end = Math.ceil((scrollLeft + clientWidth) * scale);
|
|
164
|
-
draw(start, end);
|
|
165
|
-
// Draw the progress mask
|
|
166
|
-
this.createProgressMask();
|
|
167
|
-
// Draw the rest of the waveform with a timeout for better performance
|
|
168
|
-
if (start > 0) {
|
|
169
|
-
yield this.delay(() => {
|
|
170
|
-
draw(0, start);
|
|
171
|
-
});
|
|
190
|
+
renderBars(channelData, options, ctx) {
|
|
191
|
+
ctx.fillStyle = this.convertColorValues(options.waveColor);
|
|
192
|
+
// Custom rendering function
|
|
193
|
+
if (options.renderFunction) {
|
|
194
|
+
options.renderFunction(channelData, ctx);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const topChannel = channelData[0];
|
|
198
|
+
const bottomChannel = channelData[1] || channelData[0];
|
|
199
|
+
const length = topChannel.length;
|
|
200
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
201
|
+
const { width, height } = ctx.canvas;
|
|
202
|
+
const halfHeight = height / 2;
|
|
203
|
+
const barHeight = options.barHeight || 1;
|
|
204
|
+
const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
|
|
205
|
+
const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
|
|
206
|
+
const barRadius = options.barRadius || 0;
|
|
207
|
+
const barIndexScale = width / (barWidth + barGap) / length;
|
|
208
|
+
let max = 1;
|
|
209
|
+
if (options.normalize) {
|
|
210
|
+
max = 0;
|
|
211
|
+
for (let i = 0; i < length; i++) {
|
|
212
|
+
const value = Math.abs(topChannel[i]);
|
|
213
|
+
if (value > max)
|
|
214
|
+
max = value;
|
|
172
215
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
216
|
+
}
|
|
217
|
+
const vScale = (halfHeight / max) * barHeight;
|
|
218
|
+
ctx.beginPath();
|
|
219
|
+
let prevX = 0;
|
|
220
|
+
let maxTop = 0;
|
|
221
|
+
let maxBottom = 0;
|
|
222
|
+
for (let i = 0; i <= length; i++) {
|
|
223
|
+
const x = Math.round(i * barIndexScale);
|
|
224
|
+
if (x > prevX) {
|
|
225
|
+
const leftBarHeight = Math.round(maxTop * vScale);
|
|
226
|
+
const rightBarHeight = Math.round(maxBottom * vScale);
|
|
227
|
+
const barHeight = leftBarHeight + rightBarHeight || 1;
|
|
228
|
+
// Vertical alignment
|
|
229
|
+
let y = halfHeight - leftBarHeight;
|
|
230
|
+
if (options.barAlign === 'top')
|
|
231
|
+
y = 0;
|
|
232
|
+
else if (options.barAlign === 'bottom')
|
|
233
|
+
y = height - barHeight;
|
|
234
|
+
ctx.roundRect(prevX * (barWidth + barGap), y, barWidth, barHeight, barRadius);
|
|
235
|
+
prevX = x;
|
|
236
|
+
maxTop = 0;
|
|
237
|
+
maxBottom = 0;
|
|
177
238
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
239
|
+
const magnitudeTop = Math.abs(topChannel[i] || 0);
|
|
240
|
+
const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
|
|
241
|
+
if (magnitudeTop > maxTop)
|
|
242
|
+
maxTop = magnitudeTop;
|
|
243
|
+
if (magnitudeBottom > maxBottom)
|
|
244
|
+
maxBottom = magnitudeBottom;
|
|
245
|
+
}
|
|
246
|
+
ctx.fill();
|
|
247
|
+
ctx.closePath();
|
|
183
248
|
}
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
249
|
+
renderSingleCanvas(channelData, options, width, height, start, end, canvasContainer, progressContainer) {
|
|
250
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
251
|
+
const canvas = document.createElement('canvas');
|
|
252
|
+
const length = channelData[0].length;
|
|
253
|
+
canvas.width = Math.round((width * (end - start)) / length);
|
|
254
|
+
canvas.height = height * pixelRatio;
|
|
255
|
+
canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`;
|
|
256
|
+
canvas.style.height = `${height}px`;
|
|
257
|
+
canvas.style.left = `${Math.floor((start * width) / pixelRatio / length)}px`;
|
|
258
|
+
canvasContainer.appendChild(canvas);
|
|
259
|
+
const ctx = canvas.getContext('2d');
|
|
260
|
+
this.renderBars(channelData.map((channel) => channel.slice(start, end)), options, ctx);
|
|
261
|
+
// Draw a progress canvas
|
|
262
|
+
const progressCanvas = canvas.cloneNode();
|
|
263
|
+
progressContainer.appendChild(progressCanvas);
|
|
264
|
+
const progressCtx = progressCanvas.getContext('2d');
|
|
265
|
+
if (canvas.width > 0 && canvas.height > 0) {
|
|
266
|
+
progressCtx.drawImage(canvas, 0, 0);
|
|
267
|
+
}
|
|
194
268
|
// Set the composition method to draw only where the waveform is drawn
|
|
195
269
|
progressCtx.globalCompositeOperation = 'source-in';
|
|
196
|
-
progressCtx.fillStyle = this.options.progressColor;
|
|
270
|
+
progressCtx.fillStyle = this.convertColorValues(options.progressColor);
|
|
197
271
|
// This rectangle acts as a mask thanks to the composition method
|
|
198
|
-
progressCtx.fillRect(0, 0,
|
|
272
|
+
progressCtx.fillRect(0, 0, canvas.width, canvas.height);
|
|
273
|
+
}
|
|
274
|
+
renderWaveform(channelData, options, width) {
|
|
275
|
+
// A container for canvases
|
|
276
|
+
const canvasContainer = document.createElement('div');
|
|
277
|
+
const height = this.getHeight();
|
|
278
|
+
canvasContainer.style.height = `${height}px`;
|
|
279
|
+
this.canvasWrapper.style.minHeight = `${height}px`;
|
|
280
|
+
this.canvasWrapper.appendChild(canvasContainer);
|
|
281
|
+
// A container for progress canvases
|
|
282
|
+
const progressContainer = canvasContainer.cloneNode();
|
|
283
|
+
this.progressWrapper.appendChild(progressContainer);
|
|
284
|
+
// Determine the currently visible part of the waveform
|
|
285
|
+
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
|
|
286
|
+
const len = channelData[0].length;
|
|
287
|
+
const scale = len / scrollWidth;
|
|
288
|
+
let viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth);
|
|
289
|
+
// Adjust width to avoid gaps between canvases when using bars
|
|
290
|
+
if (options.barWidth || options.barGap) {
|
|
291
|
+
const barWidth = options.barWidth || 0.5;
|
|
292
|
+
const barGap = options.barGap || barWidth / 2;
|
|
293
|
+
const totalBarWidth = barWidth + barGap;
|
|
294
|
+
if (viewportWidth % totalBarWidth !== 0) {
|
|
295
|
+
viewportWidth = Math.floor(viewportWidth / totalBarWidth) * totalBarWidth;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const start = Math.floor(Math.abs(scrollLeft) * scale);
|
|
299
|
+
const end = Math.floor(start + viewportWidth * scale);
|
|
300
|
+
const viewportLen = end - start;
|
|
301
|
+
// Draw a portion of the waveform from start peak to end peak
|
|
302
|
+
const draw = (start, end) => {
|
|
303
|
+
this.renderSingleCanvas(channelData, options, width, height, Math.max(0, start), Math.min(end, len), canvasContainer, progressContainer);
|
|
304
|
+
};
|
|
305
|
+
// Draw the waveform in viewport chunks, each with a delay
|
|
306
|
+
const headDelay = this.createDelay();
|
|
307
|
+
const tailDelay = this.createDelay();
|
|
308
|
+
const renderHead = (fromIndex, toIndex) => {
|
|
309
|
+
draw(fromIndex, toIndex);
|
|
310
|
+
if (fromIndex > 0) {
|
|
311
|
+
headDelay(() => {
|
|
312
|
+
renderHead(fromIndex - viewportLen, toIndex - viewportLen);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const renderTail = (fromIndex, toIndex) => {
|
|
317
|
+
draw(fromIndex, toIndex);
|
|
318
|
+
if (toIndex < len) {
|
|
319
|
+
tailDelay(() => {
|
|
320
|
+
renderTail(fromIndex + viewportLen, toIndex + viewportLen);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
renderHead(start, end);
|
|
325
|
+
if (end < len) {
|
|
326
|
+
renderTail(end, end + viewportLen);
|
|
327
|
+
}
|
|
199
328
|
}
|
|
200
329
|
render(audioData) {
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
this.
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
330
|
+
// Clear previous timeouts
|
|
331
|
+
this.timeouts.forEach((context) => context.timeout && clearTimeout(context.timeout));
|
|
332
|
+
this.timeouts = [];
|
|
333
|
+
// Determine the width of the waveform
|
|
334
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
335
|
+
const parentWidth = this.scrollContainer.clientWidth;
|
|
336
|
+
const scrollWidth = Math.ceil(audioData.duration * (this.options.minPxPerSec || 0));
|
|
337
|
+
// Whether the container should scroll
|
|
338
|
+
this.isScrolling = scrollWidth > parentWidth;
|
|
339
|
+
const useParentWidth = this.options.fillParent && !this.isScrolling;
|
|
340
|
+
// Width and height of the waveform in pixels
|
|
341
|
+
const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
|
|
342
|
+
// Set the width of the wrapper
|
|
343
|
+
this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
|
|
344
|
+
// Set additional styles
|
|
345
|
+
this.scrollContainer.style.overflowX = this.isScrolling ? 'auto' : 'hidden';
|
|
346
|
+
this.scrollContainer.classList.toggle('noScrollbar', !!this.options.hideScrollbar);
|
|
347
|
+
this.cursor.style.backgroundColor = `${this.options.cursorColor || this.options.progressColor}`;
|
|
348
|
+
this.cursor.style.width = `${this.options.cursorWidth}px`;
|
|
349
|
+
// Clear the canvases
|
|
350
|
+
this.canvasWrapper.innerHTML = '';
|
|
351
|
+
this.progressWrapper.innerHTML = '';
|
|
352
|
+
// Render the waveform
|
|
353
|
+
if (this.options.splitChannels) {
|
|
354
|
+
// Render a waveform for each channel
|
|
355
|
+
for (let i = 0; i < audioData.numberOfChannels; i++) {
|
|
356
|
+
const options = { ...this.options, ...this.options.splitChannels[i] };
|
|
357
|
+
this.renderWaveform([audioData.getChannelData(i)], options, width);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Render a single waveform for the first two channels (left and right)
|
|
362
|
+
const channels = [audioData.getChannelData(0)];
|
|
363
|
+
if (audioData.numberOfChannels > 1)
|
|
364
|
+
channels.push(audioData.getChannelData(1));
|
|
365
|
+
this.renderWaveform(channels, this.options, width);
|
|
366
|
+
}
|
|
367
|
+
this.audioData = audioData;
|
|
368
|
+
this.emit('render');
|
|
369
|
+
}
|
|
370
|
+
reRender() {
|
|
371
|
+
// Return if the waveform has not been rendered yet
|
|
372
|
+
if (!this.audioData)
|
|
373
|
+
return;
|
|
219
374
|
// Remember the current cursor position
|
|
220
|
-
const oldCursorPosition = this.
|
|
221
|
-
|
|
222
|
-
this.render(audioData);
|
|
375
|
+
const oldCursorPosition = this.progressWrapper.clientWidth;
|
|
376
|
+
// Set the new zoom level and re-render the waveform
|
|
377
|
+
this.render(this.audioData);
|
|
223
378
|
// Adjust the scroll position so that the cursor stays in the same place
|
|
224
|
-
const newCursortPosition = this.
|
|
379
|
+
const newCursortPosition = this.progressWrapper.clientWidth;
|
|
225
380
|
this.scrollContainer.scrollLeft += newCursortPosition - oldCursorPosition;
|
|
226
381
|
}
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
382
|
+
zoom(minPxPerSec) {
|
|
383
|
+
this.options.minPxPerSec = minPxPerSec;
|
|
384
|
+
this.reRender();
|
|
385
|
+
}
|
|
386
|
+
scrollIntoView(progress, isPlaying = false) {
|
|
387
|
+
const { clientWidth, scrollLeft, scrollWidth } = this.scrollContainer;
|
|
388
|
+
const progressWidth = scrollWidth * progress;
|
|
389
|
+
const center = clientWidth / 2;
|
|
390
|
+
const minScroll = isPlaying && this.options.autoCenter && !this.isDragging ? center : clientWidth;
|
|
391
|
+
if (progressWidth > scrollLeft + minScroll || progressWidth < scrollLeft) {
|
|
392
|
+
// Scroll to the center
|
|
393
|
+
if (this.options.autoCenter && !this.isDragging) {
|
|
394
|
+
// If the cursor is in viewport but not centered, scroll to the center slowly
|
|
395
|
+
const minDiff = center / 20;
|
|
396
|
+
if (progressWidth - (scrollLeft + center) >= minDiff && progressWidth < scrollLeft + clientWidth) {
|
|
397
|
+
this.scrollContainer.scrollLeft += minDiff;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Otherwise, scroll to the center immediately
|
|
401
|
+
this.scrollContainer.scrollLeft = progressWidth - center;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else if (this.isDragging) {
|
|
405
|
+
// Scroll just a little bit to allow for some space between the cursor and the edge
|
|
406
|
+
const gap = 10;
|
|
407
|
+
this.scrollContainer.scrollLeft =
|
|
408
|
+
progressWidth < scrollLeft ? progressWidth - gap : progressWidth - clientWidth + gap;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Scroll to the beginning
|
|
412
|
+
this.scrollContainer.scrollLeft = progressWidth;
|
|
235
413
|
}
|
|
236
414
|
}
|
|
415
|
+
// Emit the scroll event
|
|
416
|
+
{
|
|
417
|
+
const { scrollLeft } = this.scrollContainer;
|
|
418
|
+
const startX = scrollLeft / scrollWidth;
|
|
419
|
+
const endX = (scrollLeft + clientWidth) / scrollWidth;
|
|
420
|
+
this.emit('scroll', startX, endX);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
renderProgress(progress, isPlaying) {
|
|
424
|
+
if (isNaN(progress))
|
|
425
|
+
return;
|
|
426
|
+
this.progressWrapper.style.width = `${progress * 100}%`;
|
|
427
|
+
this.cursor.style.left = `${progress * 100}%`;
|
|
428
|
+
this.cursor.style.marginLeft = Math.round(progress * 100) === 100 ? `-${this.options.cursorWidth}px` : '';
|
|
429
|
+
if (this.isScrolling && this.options.autoScroll) {
|
|
430
|
+
this.scrollIntoView(progress, isPlaying);
|
|
431
|
+
}
|
|
237
432
|
}
|
|
238
433
|
}
|
|
434
|
+
Renderer.MAX_CANVAS_WIDTH = 4000;
|
|
239
435
|
export default Renderer;
|