wavesurfer.js 7.12.2 → 7.12.3
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/__tests__/regions.test.d.ts +1 -0
- package/dist/__tests__/regions.test.js +61 -0
- package/dist/__tests__/timeline.test.d.ts +1 -0
- package/dist/__tests__/timeline.test.js +46 -0
- package/dist/plugins/envelope.js +426 -1
- package/dist/plugins/hover.js +131 -1
- package/dist/plugins/minimap.js +229 -1
- package/dist/plugins/record.js +319 -1
- package/dist/plugins/regions.d.ts +2 -0
- package/dist/plugins/regions.js +699 -1
- package/dist/plugins/spectrogram-windowed.js +967 -1
- package/dist/plugins/timeline.cjs +1 -1
- package/dist/plugins/timeline.esm.js +1 -1
- package/dist/plugins/timeline.js +1 -1
- package/dist/plugins/timeline.min.js +1 -1
- package/package.json +2 -4
- package/dist/plugins/envelope.cjs +0 -1
- package/dist/plugins/envelope.esm.js +0 -1
- package/dist/plugins/envelope.min.js +0 -1
- package/dist/plugins/hover.cjs +0 -1
- package/dist/plugins/hover.esm.js +0 -1
- package/dist/plugins/hover.min.js +0 -1
- package/dist/plugins/minimap.cjs +0 -1
- package/dist/plugins/minimap.esm.js +0 -1
- package/dist/plugins/minimap.min.js +0 -1
- package/dist/plugins/record.cjs +0 -1
- package/dist/plugins/record.esm.js +0 -1
- package/dist/plugins/record.min.js +0 -1
- package/dist/plugins/regions.cjs +0 -1
- package/dist/plugins/regions.esm.js +0 -1
- package/dist/plugins/regions.min.js +0 -1
- package/dist/plugins/spectrogram-windowed.cjs +0 -1
- package/dist/plugins/spectrogram-windowed.esm.js +0 -1
- package/dist/plugins/spectrogram-windowed.min.js +0 -1
- package/dist/plugins/spectrogram.cjs +0 -1
- package/dist/plugins/spectrogram.esm.js +0 -1
- package/dist/plugins/spectrogram.min.js +0 -1
package/dist/plugins/record.js
CHANGED
|
@@ -1 +1,319 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Record audio from the microphone with a real-time waveform preview
|
|
3
|
+
*/
|
|
4
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
5
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
6
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
7
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
8
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
9
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
10
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
import BasePlugin from '../base-plugin.js';
|
|
14
|
+
import Timer from '../timer.js';
|
|
15
|
+
const DEFAULT_BITS_PER_SECOND = 128000;
|
|
16
|
+
const DEFAULT_SCROLLING_WAVEFORM_WINDOW = 5;
|
|
17
|
+
const FPS = 100;
|
|
18
|
+
const MIME_TYPES = ['audio/webm', 'audio/wav', 'audio/mpeg', 'audio/mp4', 'audio/mp3'];
|
|
19
|
+
const findSupportedMimeType = () => MIME_TYPES.find((mimeType) => MediaRecorder.isTypeSupported(mimeType));
|
|
20
|
+
class RecordPlugin extends BasePlugin {
|
|
21
|
+
/** Create an instance of the Record plugin */
|
|
22
|
+
constructor(options) {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f;
|
|
24
|
+
super(Object.assign(Object.assign({}, options), { audioBitsPerSecond: (_a = options.audioBitsPerSecond) !== null && _a !== void 0 ? _a : DEFAULT_BITS_PER_SECOND, scrollingWaveform: (_b = options.scrollingWaveform) !== null && _b !== void 0 ? _b : false, scrollingWaveformWindow: (_c = options.scrollingWaveformWindow) !== null && _c !== void 0 ? _c : DEFAULT_SCROLLING_WAVEFORM_WINDOW, continuousWaveform: (_d = options.continuousWaveform) !== null && _d !== void 0 ? _d : false, renderRecordedAudio: (_e = options.renderRecordedAudio) !== null && _e !== void 0 ? _e : true, mediaRecorderTimeslice: (_f = options.mediaRecorderTimeslice) !== null && _f !== void 0 ? _f : undefined }));
|
|
25
|
+
this.stream = null;
|
|
26
|
+
this.mediaRecorder = null;
|
|
27
|
+
this.dataWindow = null;
|
|
28
|
+
this.isWaveformPaused = false;
|
|
29
|
+
this.lastStartTime = 0;
|
|
30
|
+
this.lastDuration = 0;
|
|
31
|
+
this.duration = 0;
|
|
32
|
+
this.micStream = null;
|
|
33
|
+
this.recordedBlobUrl = null;
|
|
34
|
+
this.timer = new Timer();
|
|
35
|
+
this.subscriptions.push(this.timer.on('tick', () => {
|
|
36
|
+
const currentTime = performance.now() - this.lastStartTime;
|
|
37
|
+
this.duration = this.isPaused() ? this.duration : this.lastDuration + currentTime;
|
|
38
|
+
this.emit('record-progress', this.duration);
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/** Create an instance of the Record plugin */
|
|
42
|
+
static create(options) {
|
|
43
|
+
return new RecordPlugin(options || {});
|
|
44
|
+
}
|
|
45
|
+
renderMicStream(stream) {
|
|
46
|
+
var _a;
|
|
47
|
+
const audioContext = new AudioContext();
|
|
48
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
49
|
+
const analyser = audioContext.createAnalyser();
|
|
50
|
+
source.connect(analyser);
|
|
51
|
+
// Use smaller FFT size for more responsive peak detection
|
|
52
|
+
if (this.options.continuousWaveform || this.options.scrollingWaveform) {
|
|
53
|
+
analyser.fftSize = 32;
|
|
54
|
+
}
|
|
55
|
+
const bufferLength = analyser.frequencyBinCount;
|
|
56
|
+
const dataArray = new Float32Array(bufferLength);
|
|
57
|
+
let sampleIdx = 0;
|
|
58
|
+
if (this.wavesurfer) {
|
|
59
|
+
(_a = this.originalOptions) !== null && _a !== void 0 ? _a : (this.originalOptions = Object.assign({}, this.wavesurfer.options));
|
|
60
|
+
this.wavesurfer.options.interact = false;
|
|
61
|
+
if (this.options.scrollingWaveform) {
|
|
62
|
+
this.wavesurfer.options.cursorWidth = 0;
|
|
63
|
+
// Use fixed max peak in scrolling mode to prevent "dancing" waveform
|
|
64
|
+
this.wavesurfer.options.normalize = true;
|
|
65
|
+
this.wavesurfer.options.maxPeak = 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const drawWaveform = () => {
|
|
69
|
+
var _a, _b, _c, _d;
|
|
70
|
+
if (this.isWaveformPaused)
|
|
71
|
+
return;
|
|
72
|
+
analyser.getFloatTimeDomainData(dataArray);
|
|
73
|
+
if (this.options.scrollingWaveform) {
|
|
74
|
+
// Scrolling waveform - use peak values for smooth rendering
|
|
75
|
+
const windowSize = Math.floor((this.options.scrollingWaveformWindow || 0) * FPS);
|
|
76
|
+
// Calculate peak value from the current buffer
|
|
77
|
+
let maxValue = 0;
|
|
78
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
79
|
+
const value = Math.abs(dataArray[i]);
|
|
80
|
+
if (value > maxValue) {
|
|
81
|
+
maxValue = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!this.dataWindow) {
|
|
85
|
+
this.dataWindow = new Float32Array(windowSize);
|
|
86
|
+
}
|
|
87
|
+
const tempArray = new Float32Array(windowSize);
|
|
88
|
+
if (this.dataWindow && this.dataWindow.length > 0) {
|
|
89
|
+
// Shift old data to the left, dropping the oldest sample
|
|
90
|
+
const keepLength = windowSize - 1;
|
|
91
|
+
const oldData = this.dataWindow.slice(-keepLength);
|
|
92
|
+
tempArray.set(oldData, 0);
|
|
93
|
+
}
|
|
94
|
+
// Add new peak value at the end
|
|
95
|
+
tempArray[windowSize - 1] = maxValue;
|
|
96
|
+
this.dataWindow = tempArray;
|
|
97
|
+
}
|
|
98
|
+
else if (this.options.continuousWaveform) {
|
|
99
|
+
// Continuous waveform
|
|
100
|
+
if (!this.dataWindow) {
|
|
101
|
+
const size = this.options.continuousWaveformDuration
|
|
102
|
+
? Math.round(this.options.continuousWaveformDuration * FPS)
|
|
103
|
+
: ((_b = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getWidth()) !== null && _b !== void 0 ? _b : 0) * window.devicePixelRatio;
|
|
104
|
+
this.dataWindow = new Float32Array(size);
|
|
105
|
+
}
|
|
106
|
+
let maxValue = 0;
|
|
107
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
108
|
+
const value = Math.abs(dataArray[i]);
|
|
109
|
+
if (value > maxValue) {
|
|
110
|
+
maxValue = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (sampleIdx + 1 > this.dataWindow.length) {
|
|
114
|
+
const tempArray = new Float32Array(this.dataWindow.length * 2);
|
|
115
|
+
tempArray.set(this.dataWindow, 0);
|
|
116
|
+
this.dataWindow = tempArray;
|
|
117
|
+
}
|
|
118
|
+
this.dataWindow[sampleIdx] = maxValue;
|
|
119
|
+
sampleIdx++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.dataWindow = dataArray;
|
|
123
|
+
}
|
|
124
|
+
// Render the waveform
|
|
125
|
+
if (this.wavesurfer) {
|
|
126
|
+
const totalDuration = ((_d = (_c = this.dataWindow) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) / FPS;
|
|
127
|
+
this.wavesurfer
|
|
128
|
+
.load('', [this.dataWindow], this.options.scrollingWaveform ? this.options.scrollingWaveformWindow : totalDuration)
|
|
129
|
+
.then(() => {
|
|
130
|
+
if (this.wavesurfer && this.options.continuousWaveform) {
|
|
131
|
+
this.wavesurfer.setTime(this.getDuration() / 1000);
|
|
132
|
+
if (!this.wavesurfer.options.minPxPerSec) {
|
|
133
|
+
this.wavesurfer.setOptions({
|
|
134
|
+
minPxPerSec: this.wavesurfer.getWidth() / this.wavesurfer.getDuration(),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch((err) => {
|
|
140
|
+
console.error('Error rendering real-time recording data:', err);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const intervalId = setInterval(drawWaveform, 1000 / FPS);
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
clearInterval(intervalId);
|
|
147
|
+
source === null || source === void 0 ? void 0 : source.disconnect();
|
|
148
|
+
audioContext === null || audioContext === void 0 ? void 0 : audioContext.close();
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
onDestroy: cleanup,
|
|
152
|
+
onEnd: () => {
|
|
153
|
+
this.isWaveformPaused = true;
|
|
154
|
+
this.stopMic();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Request access to the microphone and start monitoring incoming audio */
|
|
159
|
+
startMic(options) {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
// Stop previous mic stream if exists to clean up AudioContext
|
|
162
|
+
if (this.micStream) {
|
|
163
|
+
this.stopMic();
|
|
164
|
+
}
|
|
165
|
+
let stream;
|
|
166
|
+
try {
|
|
167
|
+
stream = yield navigator.mediaDevices.getUserMedia({
|
|
168
|
+
audio: options !== null && options !== void 0 ? options : true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
throw new Error('Error accessing the microphone: ' + err.message);
|
|
173
|
+
}
|
|
174
|
+
const micStream = this.renderMicStream(stream);
|
|
175
|
+
this.micStream = micStream;
|
|
176
|
+
this.unsubscribeDestroy = this.once('destroy', micStream.onDestroy);
|
|
177
|
+
this.unsubscribeRecordEnd = this.once('record-end', micStream.onEnd);
|
|
178
|
+
this.stream = stream;
|
|
179
|
+
return stream;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Stop monitoring incoming audio */
|
|
183
|
+
stopMic() {
|
|
184
|
+
var _a, _b, _c;
|
|
185
|
+
(_a = this.micStream) === null || _a === void 0 ? void 0 : _a.onDestroy();
|
|
186
|
+
(_b = this.unsubscribeDestroy) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
187
|
+
(_c = this.unsubscribeRecordEnd) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
188
|
+
this.micStream = null;
|
|
189
|
+
this.unsubscribeDestroy = undefined;
|
|
190
|
+
this.unsubscribeRecordEnd = undefined;
|
|
191
|
+
if (!this.stream)
|
|
192
|
+
return;
|
|
193
|
+
this.stream.getTracks().forEach((track) => track.stop());
|
|
194
|
+
this.stream = null;
|
|
195
|
+
this.mediaRecorder = null;
|
|
196
|
+
}
|
|
197
|
+
/** Start recording audio from the microphone */
|
|
198
|
+
startRecording(options) {
|
|
199
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
const stream = this.stream || (yield this.startMic(options));
|
|
201
|
+
this.dataWindow = null;
|
|
202
|
+
const mediaRecorder = this.mediaRecorder ||
|
|
203
|
+
new MediaRecorder(stream, {
|
|
204
|
+
mimeType: this.options.mimeType || findSupportedMimeType(),
|
|
205
|
+
audioBitsPerSecond: this.options.audioBitsPerSecond,
|
|
206
|
+
});
|
|
207
|
+
this.mediaRecorder = mediaRecorder;
|
|
208
|
+
this.stopRecording();
|
|
209
|
+
const recordedChunks = [];
|
|
210
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
211
|
+
if (event.data.size > 0) {
|
|
212
|
+
recordedChunks.push(event.data);
|
|
213
|
+
}
|
|
214
|
+
this.emit('record-data-available', event.data);
|
|
215
|
+
};
|
|
216
|
+
const emitWithBlob = (ev) => {
|
|
217
|
+
var _a;
|
|
218
|
+
const blob = new Blob(recordedChunks, { type: mediaRecorder.mimeType });
|
|
219
|
+
this.emit(ev, blob);
|
|
220
|
+
if (this.options.renderRecordedAudio) {
|
|
221
|
+
this.applyOriginalOptionsIfNeeded();
|
|
222
|
+
// Revoke previous blob URL before creating a new one
|
|
223
|
+
if (this.recordedBlobUrl) {
|
|
224
|
+
URL.revokeObjectURL(this.recordedBlobUrl);
|
|
225
|
+
}
|
|
226
|
+
this.recordedBlobUrl = URL.createObjectURL(blob);
|
|
227
|
+
(_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.load(this.recordedBlobUrl);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
mediaRecorder.onpause = () => emitWithBlob('record-pause');
|
|
231
|
+
mediaRecorder.onstop = () => emitWithBlob('record-end');
|
|
232
|
+
mediaRecorder.start(this.options.mediaRecorderTimeslice);
|
|
233
|
+
this.lastStartTime = performance.now();
|
|
234
|
+
this.lastDuration = 0;
|
|
235
|
+
this.duration = 0;
|
|
236
|
+
this.isWaveformPaused = false;
|
|
237
|
+
this.timer.start();
|
|
238
|
+
this.emit('record-start');
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Get the duration of the recording */
|
|
242
|
+
getDuration() {
|
|
243
|
+
return this.duration;
|
|
244
|
+
}
|
|
245
|
+
/** Check if the audio is being recorded */
|
|
246
|
+
isRecording() {
|
|
247
|
+
var _a;
|
|
248
|
+
return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'recording';
|
|
249
|
+
}
|
|
250
|
+
isPaused() {
|
|
251
|
+
var _a;
|
|
252
|
+
return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'paused';
|
|
253
|
+
}
|
|
254
|
+
isActive() {
|
|
255
|
+
var _a;
|
|
256
|
+
return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) !== 'inactive';
|
|
257
|
+
}
|
|
258
|
+
/** Stop the recording */
|
|
259
|
+
stopRecording() {
|
|
260
|
+
var _a;
|
|
261
|
+
if (this.isActive()) {
|
|
262
|
+
(_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.stop();
|
|
263
|
+
this.timer.stop();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Pause the recording */
|
|
267
|
+
pauseRecording() {
|
|
268
|
+
var _a, _b;
|
|
269
|
+
if (this.isRecording()) {
|
|
270
|
+
this.isWaveformPaused = true;
|
|
271
|
+
(_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.requestData();
|
|
272
|
+
(_b = this.mediaRecorder) === null || _b === void 0 ? void 0 : _b.pause();
|
|
273
|
+
this.timer.stop();
|
|
274
|
+
this.lastDuration = this.duration;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Resume the recording */
|
|
278
|
+
resumeRecording() {
|
|
279
|
+
var _a;
|
|
280
|
+
if (this.isPaused()) {
|
|
281
|
+
this.isWaveformPaused = false;
|
|
282
|
+
(_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.resume();
|
|
283
|
+
this.timer.start();
|
|
284
|
+
this.lastStartTime = performance.now();
|
|
285
|
+
this.emit('record-resume');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Get a list of available audio devices
|
|
289
|
+
* You can use this to get the device ID of the microphone to use with the startMic and startRecording methods
|
|
290
|
+
* Will return an empty array if the browser doesn't support the MediaDevices API or if the user has not granted access to the microphone
|
|
291
|
+
* You can ask for permission to the microphone by calling startMic
|
|
292
|
+
*/
|
|
293
|
+
static getAvailableAudioDevices() {
|
|
294
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
295
|
+
return navigator.mediaDevices
|
|
296
|
+
.enumerateDevices()
|
|
297
|
+
.then((devices) => devices.filter((device) => device.kind === 'audioinput'));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** Destroy the plugin */
|
|
301
|
+
destroy() {
|
|
302
|
+
this.applyOriginalOptionsIfNeeded();
|
|
303
|
+
super.destroy();
|
|
304
|
+
this.stopRecording();
|
|
305
|
+
this.stopMic();
|
|
306
|
+
// Revoke blob URL to free memory
|
|
307
|
+
if (this.recordedBlobUrl) {
|
|
308
|
+
URL.revokeObjectURL(this.recordedBlobUrl);
|
|
309
|
+
this.recordedBlobUrl = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
applyOriginalOptionsIfNeeded() {
|
|
313
|
+
if (this.wavesurfer && this.originalOptions) {
|
|
314
|
+
this.wavesurfer.setOptions(this.originalOptions);
|
|
315
|
+
delete this.originalOptions;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export default RecordPlugin;
|
|
@@ -36,6 +36,8 @@ export type RegionEvents = {
|
|
|
36
36
|
update: [side?: UpdateSide];
|
|
37
37
|
/** When dragging or resizing is finished */
|
|
38
38
|
'update-end': [side?: UpdateSide];
|
|
39
|
+
/** When the region needs to be re-rendered */
|
|
40
|
+
render: [];
|
|
39
41
|
/** On play */
|
|
40
42
|
play: [end?: number];
|
|
41
43
|
/** On mouse click */
|