@checksub_team/peaks_timeline 1.4.17
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/CHANGELOG.md +530 -0
- package/COPYING +165 -0
- package/README.md +1184 -0
- package/package.json +88 -0
- package/peaks.js +20174 -0
- package/peaks.js.d.ts +332 -0
- package/src/data-retriever.js +90 -0
- package/src/data.js +56 -0
- package/src/default-segment-marker.js +132 -0
- package/src/keyboard-handler.js +112 -0
- package/src/line-indicator.js +312 -0
- package/src/line.js +629 -0
- package/src/lines.js +356 -0
- package/src/main.js +663 -0
- package/src/marker-factories.js +91 -0
- package/src/mode-layer.js +361 -0
- package/src/mouse-drag-handler.js +207 -0
- package/src/player.js +178 -0
- package/src/playhead-layer.js +413 -0
- package/src/segment-marker.js +155 -0
- package/src/segment-shape.js +345 -0
- package/src/segment.js +229 -0
- package/src/segments-group.js +697 -0
- package/src/source-group.js +975 -0
- package/src/source.js +688 -0
- package/src/sources-layer.js +509 -0
- package/src/timeline-axis.js +238 -0
- package/src/timeline-segments.js +389 -0
- package/src/timeline-sources.js +431 -0
- package/src/timeline-zoomview.js +866 -0
- package/src/utils.js +339 -0
- package/src/waveform-builder.js +458 -0
- package/src/waveform-shape.js +223 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
*
|
|
4
|
+
* Defines the {@link WaveformBuilder} class.
|
|
5
|
+
*
|
|
6
|
+
* @module waveform-builder
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
define([
|
|
10
|
+
'waveform-data',
|
|
11
|
+
'./utils'
|
|
12
|
+
], function(
|
|
13
|
+
WaveformData,
|
|
14
|
+
Utils) {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var isXhr2 = ('withCredentials' in new XMLHttpRequest());
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates and returns a WaveformData object, either by requesting the
|
|
21
|
+
* waveform data from the server, or by creating the waveform data using the
|
|
22
|
+
* Web Audio API.
|
|
23
|
+
*
|
|
24
|
+
* @class
|
|
25
|
+
* @alias WaveformBuilder
|
|
26
|
+
*
|
|
27
|
+
* @param {Peaks} peaks
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function WaveformBuilder(peaks) {
|
|
31
|
+
this._peaks = peaks;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for requesting remote waveform data.
|
|
36
|
+
*
|
|
37
|
+
* @typedef {Object} RemoteWaveformDataOptions
|
|
38
|
+
* @global
|
|
39
|
+
* @property {String=} arraybuffer
|
|
40
|
+
* @property {String=} json
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Options for supplying local waveform data.
|
|
45
|
+
*
|
|
46
|
+
* @typedef {Object} LocalWaveformDataOptions
|
|
47
|
+
* @global
|
|
48
|
+
* @property {ArrayBuffer=} arraybuffer
|
|
49
|
+
* @property {Object=} json
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options for the Web Audio waveform builder.
|
|
54
|
+
*
|
|
55
|
+
* @typedef {Object} WaveformBuilderWebAudioOptions
|
|
56
|
+
* @global
|
|
57
|
+
* @property {AudioContext} audioContext
|
|
58
|
+
* @property {AudioBuffer=} audioBuffer
|
|
59
|
+
* @property {Number=} scale
|
|
60
|
+
* @property {Boolean=} multiChannel
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for [WaveformBuilder.init]{@link WaveformBuilder#init}.
|
|
65
|
+
*
|
|
66
|
+
* @typedef {Object} WaveformBuilderInitOptions
|
|
67
|
+
* @global
|
|
68
|
+
* @property {RemoteWaveformDataOptions=} dataUri
|
|
69
|
+
* @property {LocalWaveformDataOptions=} waveformData
|
|
70
|
+
* @property {WaveformBuilderWebAudioOptions=} webAudio
|
|
71
|
+
* @property {Boolean=} withCredentials
|
|
72
|
+
* @property {Array<Number>=} zoomLevels
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Callback for receiving the waveform data.
|
|
77
|
+
*
|
|
78
|
+
* @callback WaveformBuilderInitCallback
|
|
79
|
+
* @global
|
|
80
|
+
* @param {Error} error
|
|
81
|
+
* @param {WaveformData} waveformData
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Loads or creates the waveform data.
|
|
86
|
+
*
|
|
87
|
+
* @private
|
|
88
|
+
* @param {WaveformBuilderInitOptions} options
|
|
89
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
WaveformBuilder.prototype.init = function(options, callback) {
|
|
93
|
+
if ((options.dataUri && (options.webAudio || options.audioContext)) ||
|
|
94
|
+
(options.waveformData && (options.webAudio || options.audioContext)) ||
|
|
95
|
+
(options.dataUri && options.waveformData)) {
|
|
96
|
+
// eslint-disable-next-line max-len
|
|
97
|
+
callback(new TypeError('Peaks.init(): You may only pass one source (webAudio, dataUri, or waveformData) to render waveform data.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.audioContext) {
|
|
102
|
+
// eslint-disable-next-line max-len
|
|
103
|
+
this._peaks.options.deprecationLogger('Peaks.init(): The audioContext option is deprecated, please pass a webAudio object instead');
|
|
104
|
+
|
|
105
|
+
options.webAudio = {
|
|
106
|
+
audioContext: options.audioContext
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.objectUrl) {
|
|
111
|
+
return this._buildWaveformDataFromObjectUrl(options, callback);
|
|
112
|
+
}
|
|
113
|
+
else if (options.dataUri) {
|
|
114
|
+
return this._getRemoteWaveformData(options, callback);
|
|
115
|
+
}
|
|
116
|
+
else if (options.waveformData) {
|
|
117
|
+
return this._buildWaveformFromLocalData(options, callback);
|
|
118
|
+
}
|
|
119
|
+
else if (options.webAudio) {
|
|
120
|
+
if (options.webAudio.audioBuffer) {
|
|
121
|
+
return this._buildWaveformDataFromAudioBuffer(options, callback);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return this._buildWaveformDataUsingWebAudio(options, callback);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// eslint-disable-next-line max-len
|
|
129
|
+
callback(new Error('Peaks.init(): You must pass an audioContext, or dataUri, or waveformData to render waveform data'));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/* eslint-disable max-len */
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetches waveform data, based on the given options.
|
|
137
|
+
*
|
|
138
|
+
* @private
|
|
139
|
+
* @param {Object} options
|
|
140
|
+
* @param {String|Object} options.dataUri
|
|
141
|
+
* @param {String} options.dataUri.arraybuffer Waveform data URL
|
|
142
|
+
* (binary format)
|
|
143
|
+
* @param {String} options.dataUri.json Waveform data URL (JSON format)
|
|
144
|
+
* @param {String} options.defaultUriFormat Either 'arraybuffer' (for binary
|
|
145
|
+
* data) or 'json'
|
|
146
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
147
|
+
*
|
|
148
|
+
* @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
|
|
149
|
+
* for details of the binary and JSON waveform data formats.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/* eslint-enable max-len */
|
|
153
|
+
|
|
154
|
+
WaveformBuilder.prototype._getRemoteWaveformData = function(options, callback) {
|
|
155
|
+
var self = this;
|
|
156
|
+
var dataUri = null;
|
|
157
|
+
var requestType = null;
|
|
158
|
+
var url;
|
|
159
|
+
|
|
160
|
+
if (Utils.isObject(options.dataUri)) {
|
|
161
|
+
dataUri = options.dataUri;
|
|
162
|
+
}
|
|
163
|
+
else if (Utils.isString(options.dataUri)) {
|
|
164
|
+
// Backward compatibility
|
|
165
|
+
dataUri = {};
|
|
166
|
+
dataUri[options.dataUriDefaultFormat || 'json'] = options.dataUri;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
callback(new TypeError('Peaks.init(): The dataUri option must be an object'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
['ArrayBuffer', 'JSON'].some(function(connector) {
|
|
174
|
+
if (window[connector]) {
|
|
175
|
+
requestType = connector.toLowerCase();
|
|
176
|
+
url = dataUri[requestType];
|
|
177
|
+
|
|
178
|
+
return Boolean(url);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!url) {
|
|
183
|
+
// eslint-disable-next-line max-len
|
|
184
|
+
callback(new Error('Peaks.init(): Unable to determine a compatible dataUri format for this browser'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
|
|
189
|
+
if (this.readyState !== 4) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (this.status !== 200) {
|
|
194
|
+
callback(
|
|
195
|
+
new Error('Unable to fetch remote data. HTTP status ' + this.status)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var waveformData = WaveformData.create(event.target.response);
|
|
202
|
+
|
|
203
|
+
if (waveformData.channels !== 1 && waveformData.channels !== 2) {
|
|
204
|
+
callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
callback(null, waveformData);
|
|
209
|
+
},
|
|
210
|
+
function() {
|
|
211
|
+
callback(new Error('XHR Failed'));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
xhr.send();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/* eslint-disable max-len */
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Creates a waveform from given data, based on the given options.
|
|
221
|
+
*
|
|
222
|
+
* @private
|
|
223
|
+
* @param {Object} options
|
|
224
|
+
* @param {Object} options.waveformData
|
|
225
|
+
* @param {ArrayBuffer} options.waveformData.arraybuffer Waveform data (binary format)
|
|
226
|
+
* @param {Object} options.waveformData.json Waveform data (JSON format)
|
|
227
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
228
|
+
*
|
|
229
|
+
* @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
|
|
230
|
+
* for details of the binary and JSON waveform data formats.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/* eslint-enable max-len */
|
|
234
|
+
|
|
235
|
+
WaveformBuilder.prototype._buildWaveformFromLocalData = function(options, callback) {
|
|
236
|
+
var waveformData = null;
|
|
237
|
+
var data = null;
|
|
238
|
+
|
|
239
|
+
if (Utils.isObject(options.waveformData)) {
|
|
240
|
+
waveformData = options.waveformData;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
callback(new Error('Peaks.init(): The waveformData option must be an object'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (Utils.isObject(waveformData.json)) {
|
|
248
|
+
data = waveformData.json;
|
|
249
|
+
}
|
|
250
|
+
else if (Utils.isArrayBuffer(waveformData.arraybuffer)) {
|
|
251
|
+
data = waveformData.arraybuffer;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!data) {
|
|
255
|
+
// eslint-disable-next-line max-len
|
|
256
|
+
callback(new Error('Peaks.init(): Unable to determine a compatible waveformData format'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
var createdWaveformData = WaveformData.create(data);
|
|
262
|
+
|
|
263
|
+
if (createdWaveformData.channels !== 1 && createdWaveformData.channels !== 2) {
|
|
264
|
+
callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
callback(null, createdWaveformData);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
callback(err);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Creates waveform data using the Web Audio API.
|
|
277
|
+
*
|
|
278
|
+
* @private
|
|
279
|
+
* @param {Object} options
|
|
280
|
+
* @param {AudioContext} options.webAudio.audioContext
|
|
281
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
WaveformBuilder.prototype._buildWaveformDataUsingWebAudio = function(options, callback) {
|
|
285
|
+
var self = this;
|
|
286
|
+
|
|
287
|
+
var audioContext = window.AudioContext || window.webkitAudioContext;
|
|
288
|
+
|
|
289
|
+
if (!(options.webAudio.audioContext instanceof audioContext)) {
|
|
290
|
+
// eslint-disable-next-line max-len
|
|
291
|
+
callback(new TypeError('Peaks.init(): The webAudio.audioContext option must be a valid AudioContext'));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
var webAudioOptions = options.webAudio;
|
|
296
|
+
|
|
297
|
+
if (webAudioOptions.scale !== options.zoomLevels[0]) {
|
|
298
|
+
webAudioOptions.scale = options.zoomLevels[0];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// If the media element has already selected which source to play, its
|
|
302
|
+
// currentSrc attribute will contain the source media URL. Otherwise,
|
|
303
|
+
// we wait for a canplay event to tell us when the media is ready.
|
|
304
|
+
|
|
305
|
+
var mediaSourceUrl = self._peaks.player.getCurrentSource();
|
|
306
|
+
|
|
307
|
+
if (mediaSourceUrl) {
|
|
308
|
+
self._requestAudioAndBuildWaveformData(
|
|
309
|
+
mediaSourceUrl,
|
|
310
|
+
webAudioOptions,
|
|
311
|
+
options.withCredentials,
|
|
312
|
+
callback
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
self._peaks.once('player_canplay', function(player) {
|
|
317
|
+
self._requestAudioAndBuildWaveformData(
|
|
318
|
+
player.getCurrentSource(),
|
|
319
|
+
webAudioOptions,
|
|
320
|
+
options.withCredentials,
|
|
321
|
+
callback
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Creates waveform data using the Web Audio API.
|
|
329
|
+
*
|
|
330
|
+
* @private
|
|
331
|
+
* @param {Object} options
|
|
332
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
WaveformBuilder.prototype._buildWaveformDataFromObjectUrl = function(options, callback) {
|
|
336
|
+
var self = this;
|
|
337
|
+
|
|
338
|
+
var webAudioOptions = {
|
|
339
|
+
scale: options.minScale
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
webAudioOptions.audioContext = new window.AudioContext();
|
|
343
|
+
|
|
344
|
+
if (options.objectUrl) {
|
|
345
|
+
self._requestAudioAndBuildWaveformData(
|
|
346
|
+
options.objectUrl,
|
|
347
|
+
webAudioOptions,
|
|
348
|
+
options.withCredentials,
|
|
349
|
+
callback
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
WaveformBuilder.prototype._buildWaveformDataFromAudioBuffer = function(options, callback) {
|
|
355
|
+
var webAudioOptions = options.webAudio;
|
|
356
|
+
|
|
357
|
+
if (webAudioOptions.scale !== options.zoomLevels[0]) {
|
|
358
|
+
webAudioOptions.scale = options.zoomLevels[0];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
var webAudioBuilderOptions = {
|
|
362
|
+
audio_buffer: webAudioOptions.audioBuffer,
|
|
363
|
+
split_channels: webAudioOptions.multiChannel,
|
|
364
|
+
scale: webAudioOptions.scale
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
WaveformData.createFromAudio(webAudioBuilderOptions, callback);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Fetches the audio content, based on the given options, and creates waveform
|
|
372
|
+
* data using the Web Audio API.
|
|
373
|
+
*
|
|
374
|
+
* @private
|
|
375
|
+
* @param {url} The media source URL
|
|
376
|
+
* @param {WaveformBuilderWebAudioOptions} webAudio
|
|
377
|
+
* @param {Boolean} withCredentials
|
|
378
|
+
* @param {WaveformBuilderInitCallback} callback
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
WaveformBuilder.prototype._requestAudioAndBuildWaveformData = function(url,
|
|
382
|
+
webAudio, withCredentials, callback) {
|
|
383
|
+
var self = this;
|
|
384
|
+
|
|
385
|
+
if (!url) {
|
|
386
|
+
self._peaks.logger('Peaks.init(): The mediaElement src is invalid');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
var xhr = self._createXHR(url, 'arraybuffer', withCredentials, function(event) {
|
|
391
|
+
if (this.readyState !== 4) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (this.status !== 200) {
|
|
396
|
+
callback(
|
|
397
|
+
new Error('Unable to fetch remote data. HTTP status ' + this.status)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
var webAudioBuilderOptions = {
|
|
404
|
+
audio_context: webAudio.audioContext,
|
|
405
|
+
array_buffer: event.target.response,
|
|
406
|
+
split_channels: webAudio.multiChannel,
|
|
407
|
+
scale: webAudio.scale
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
WaveformData.createFromAudio(webAudioBuilderOptions, callback);
|
|
411
|
+
},
|
|
412
|
+
function() {
|
|
413
|
+
callback(new Error('XHR Failed'));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
xhr.send();
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @private
|
|
421
|
+
* @param {String} url
|
|
422
|
+
* @param {String} requestType
|
|
423
|
+
* @param {Boolean} withCredentials
|
|
424
|
+
* @param {Function} onLoad
|
|
425
|
+
* @param {Function} onError
|
|
426
|
+
*
|
|
427
|
+
* @returns {XMLHttpRequest}
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
WaveformBuilder.prototype._createXHR = function(url, requestType,
|
|
431
|
+
withCredentials, onLoad, onError) {
|
|
432
|
+
var xhr = new XMLHttpRequest();
|
|
433
|
+
|
|
434
|
+
// open an XHR request to the data source file
|
|
435
|
+
xhr.open('GET', url, true);
|
|
436
|
+
|
|
437
|
+
if (isXhr2) {
|
|
438
|
+
try {
|
|
439
|
+
xhr.responseType = requestType;
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
// Some browsers like Safari 6 do handle XHR2 but not the json
|
|
443
|
+
// response type, doing only a try/catch fails in IE9
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
xhr.onload = onLoad;
|
|
448
|
+
xhr.onerror = onError;
|
|
449
|
+
|
|
450
|
+
if (isXhr2 && withCredentials) {
|
|
451
|
+
xhr.withCredentials = true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return xhr;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return WaveformBuilder;
|
|
458
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
*
|
|
4
|
+
* Defines the {@link WaveformShape} class.
|
|
5
|
+
*
|
|
6
|
+
* @module waveform-shape
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
define(['./utils', 'konva'], function(Utils, Konva) {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scales the waveform data for drawing on a canvas context.
|
|
14
|
+
*
|
|
15
|
+
* @param {Number} amplitude The waveform data point amplitude.
|
|
16
|
+
* @param {Number} height The height of the waveform, in pixels.
|
|
17
|
+
* @param {Number} scale Amplitude scaling factor.
|
|
18
|
+
* @returns {Number} The scaled waveform data point.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function scaleY(amplitude, height, scale) {
|
|
22
|
+
var range = 256;
|
|
23
|
+
var offset = 128;
|
|
24
|
+
|
|
25
|
+
var scaledAmplitude = (amplitude * scale + offset) * height / range;
|
|
26
|
+
|
|
27
|
+
return height - Utils.clamp(height - scaledAmplitude, 0, height);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Waveform shape options.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} WaveformShapeOptions
|
|
34
|
+
* @global
|
|
35
|
+
* @property {String} color Waveform color.
|
|
36
|
+
* @property {WaveformOverview|WaveformZoomView} view The view object
|
|
37
|
+
* that contains the waveform shape.
|
|
38
|
+
* @property {Segment?} segment If given, render a waveform image
|
|
39
|
+
* covering the segment's time range. Otherwise, render the entire
|
|
40
|
+
* waveform duration.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a Konva.Shape object that renders a waveform image.
|
|
45
|
+
*
|
|
46
|
+
* @class
|
|
47
|
+
* @alias WaveformShape
|
|
48
|
+
*
|
|
49
|
+
* @param {WaveformShapeOptions} options
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
function WaveformShape(options) {
|
|
53
|
+
Konva.Shape.call(this, {
|
|
54
|
+
fill: options.color
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this._view = options.view;
|
|
58
|
+
this._source = options.source;
|
|
59
|
+
this._originalWaveformData = options.waveformData;
|
|
60
|
+
this._waveformData = options.waveformData;
|
|
61
|
+
this._height = options.height;
|
|
62
|
+
|
|
63
|
+
this.rescale();
|
|
64
|
+
|
|
65
|
+
this.sceneFunc(this._sceneFunc);
|
|
66
|
+
|
|
67
|
+
this.hitFunc(this._waveformShapeHitFunc);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
WaveformShape.prototype = Object.create(Konva.Shape.prototype);
|
|
71
|
+
|
|
72
|
+
WaveformShape.prototype.rescale = function() {
|
|
73
|
+
this._waveformData = this._originalWaveformData.resample({
|
|
74
|
+
scale: this._waveformData.sample_rate / this._view.getTimeToPixelsScale()
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
WaveformShape.prototype.setWaveformColor = function(color) {
|
|
79
|
+
this.fill(color);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
WaveformShape.prototype._sceneFunc = function(context) {
|
|
83
|
+
var width = this._view.getWidth();
|
|
84
|
+
|
|
85
|
+
var startPixels = 0, startOffset = 0;
|
|
86
|
+
|
|
87
|
+
if (this._source) {
|
|
88
|
+
startPixels = this._view.timeToPixels(this._source.mediaStartTime) + Math.max(
|
|
89
|
+
this._view.getFrameOffset() - this._view.timeToPixels(this._source.startTime),
|
|
90
|
+
0
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
startOffset = this._view.timeToPixels(this._source.mediaStartTime);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
var endPixels = width;
|
|
97
|
+
|
|
98
|
+
if (this._source) {
|
|
99
|
+
endPixels = Math.min(
|
|
100
|
+
this._view.timeToPixels(this._source.mediaEndTime) - Math.max(
|
|
101
|
+
this._view.timeToPixels(this._source.endTime)
|
|
102
|
+
- this._view.getFrameOffset()
|
|
103
|
+
- this._view.getWidth(),
|
|
104
|
+
0
|
|
105
|
+
),
|
|
106
|
+
this._waveformData.length
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._drawWaveform(
|
|
111
|
+
context,
|
|
112
|
+
this._waveformData,
|
|
113
|
+
startPixels,
|
|
114
|
+
startOffset,
|
|
115
|
+
endPixels,
|
|
116
|
+
this._height
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Draws a waveform on a canvas context.
|
|
122
|
+
*
|
|
123
|
+
* @param {Konva.Context} context The canvas context to draw on.
|
|
124
|
+
* @param {WaveformData} waveformData The waveform data to draw.
|
|
125
|
+
* @param {Number} frameOffset The start position of the waveform shown
|
|
126
|
+
* in the view, in pixels.
|
|
127
|
+
* @param {Number} startPixels The start position of the waveform to draw,
|
|
128
|
+
* in pixels.
|
|
129
|
+
* @param {Number} endPixels The end position of the waveform to draw,
|
|
130
|
+
* in pixels.
|
|
131
|
+
* @param {Number} width The width of the waveform area, in pixels.
|
|
132
|
+
* @param {Number} height The height of the waveform area, in pixels.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
WaveformShape.prototype._drawWaveform = function(context, waveformData,
|
|
136
|
+
startPixels, startOffset, endPixels, height) {
|
|
137
|
+
var channels = waveformData.channels;
|
|
138
|
+
|
|
139
|
+
var waveformTop = 0;
|
|
140
|
+
var waveformHeight = Math.floor(height / channels);
|
|
141
|
+
|
|
142
|
+
for (var i = 0; i < channels; i++) {
|
|
143
|
+
if (i === channels - 1) {
|
|
144
|
+
waveformHeight = height - (channels - 1) * waveformHeight;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this._drawChannel(
|
|
148
|
+
context,
|
|
149
|
+
waveformData.channel(i),
|
|
150
|
+
startPixels,
|
|
151
|
+
startOffset,
|
|
152
|
+
endPixels,
|
|
153
|
+
waveformTop,
|
|
154
|
+
waveformHeight
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
waveformTop += waveformHeight;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
WaveformShape.prototype._drawChannel = function(context, channel,
|
|
162
|
+
startPixels, startOffset, endPixels, top, height) {
|
|
163
|
+
var x, val;
|
|
164
|
+
|
|
165
|
+
var amplitudeScale = this._view.getAmplitudeScale();
|
|
166
|
+
|
|
167
|
+
context.beginPath();
|
|
168
|
+
|
|
169
|
+
for (x = startPixels; x < endPixels; x++) {
|
|
170
|
+
val = channel.min_sample(x);
|
|
171
|
+
|
|
172
|
+
context.lineTo(x - startOffset + 0.5, top + scaleY(val, height, amplitudeScale) + 0.5);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (x = endPixels - 1; x >= startPixels; x--) {
|
|
176
|
+
val = channel.max_sample(x);
|
|
177
|
+
|
|
178
|
+
context.lineTo(x - startOffset + 0.5, top + scaleY(val, height, amplitudeScale) + 0.5);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
context.closePath();
|
|
182
|
+
|
|
183
|
+
context.fillShape(this);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
WaveformShape.prototype._waveformShapeHitFunc = function(context) {
|
|
187
|
+
if (!this._source) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var frameOffset = this._view.getFrameOffset();
|
|
192
|
+
var viewWidth = this._view.getWidth();
|
|
193
|
+
|
|
194
|
+
var startPixels = this._view.timeToPixels(this._source.startTime);
|
|
195
|
+
var endPixels = this._view.timeToPixels(this._source.endTime);
|
|
196
|
+
|
|
197
|
+
var offsetY = 10;
|
|
198
|
+
var hitRectHeight = this._height;
|
|
199
|
+
|
|
200
|
+
if (hitRectHeight < 0) {
|
|
201
|
+
hitRectHeight = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var hitRectLeft = startPixels - frameOffset;
|
|
205
|
+
var hitRectWidth = endPixels - startPixels;
|
|
206
|
+
|
|
207
|
+
if (hitRectLeft < 0) {
|
|
208
|
+
hitRectWidth -= -hitRectLeft;
|
|
209
|
+
hitRectLeft = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (hitRectLeft + hitRectWidth > viewWidth) {
|
|
213
|
+
hitRectWidth -= hitRectLeft + hitRectWidth - viewWidth;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
context.beginPath();
|
|
217
|
+
context.rect(hitRectLeft, offsetY, hitRectWidth, hitRectHeight);
|
|
218
|
+
context.closePath();
|
|
219
|
+
context.fillStrokeShape(this);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return WaveformShape;
|
|
223
|
+
});
|