wavesurfer 0.0.1

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.
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ WaveSurfer.MediaElement = Object.create(WaveSurfer.WebAudio);
4
+
5
+ WaveSurfer.util.extend(WaveSurfer.MediaElement, {
6
+ init: function (params) {
7
+ this.params = params;
8
+
9
+ // Dummy media to catch errors
10
+ this.media = {
11
+ currentTime: 0,
12
+ duration: 0,
13
+ paused: true,
14
+ playbackRate: 1,
15
+ play: function () {},
16
+ pause: function () {}
17
+ };
18
+
19
+ this.mediaType = params.mediaType.toLowerCase();
20
+ this.elementPosition = params.elementPosition;
21
+ },
22
+
23
+ load: function (url, container, peaks) {
24
+ var my = this;
25
+
26
+ var media = document.createElement(this.mediaType);
27
+ media.controls = false;
28
+ media.autoplay = false;
29
+ media.preload = 'auto';
30
+ media.src = url;
31
+
32
+ media.addEventListener('error', function () {
33
+ my.fireEvent('error', 'Error loading media element');
34
+ });
35
+
36
+ media.addEventListener('canplay', function () {
37
+ my.fireEvent('canplay');
38
+ });
39
+
40
+ media.addEventListener('ended', function () {
41
+ my.fireEvent('finish');
42
+ });
43
+
44
+ media.addEventListener('timeupdate', function () {
45
+ my.fireEvent('audioprocess', my.getCurrentTime());
46
+ });
47
+
48
+ var prevMedia = container.querySelector(this.mediaType);
49
+ if (prevMedia) {
50
+ container.removeChild(prevMedia);
51
+ }
52
+ container.appendChild(media);
53
+
54
+ this.media = media;
55
+ this.peaks = peaks;
56
+ this.onPlayEnd = null;
57
+ this.buffer = null;
58
+ this.setPlaybackRate(this.playbackRate);
59
+ },
60
+
61
+ isPaused: function () {
62
+ return this.media.paused;
63
+ },
64
+
65
+ getDuration: function () {
66
+ var duration = this.media.duration;
67
+ if (duration >= Infinity) { // streaming audio
68
+ duration = this.media.seekable.end();
69
+ }
70
+ return duration;
71
+ },
72
+
73
+ getCurrentTime: function () {
74
+ return this.media.currentTime;
75
+ },
76
+
77
+ getPlayedPercents: function () {
78
+ return (this.getCurrentTime() / this.getDuration()) || 0;
79
+ },
80
+
81
+ /**
82
+ * Set the audio source playback rate.
83
+ */
84
+ setPlaybackRate: function (value) {
85
+ this.playbackRate = value || 1;
86
+ this.media.playbackRate = this.playbackRate;
87
+ },
88
+
89
+ seekTo: function (start) {
90
+ if (start != null) {
91
+ this.media.currentTime = start;
92
+ }
93
+ this.clearPlayEnd();
94
+ },
95
+
96
+ /**
97
+ * Plays the loaded audio region.
98
+ *
99
+ * @param {Number} start Start offset in seconds,
100
+ * relative to the beginning of a clip.
101
+ * @param {Number} end End offset in seconds,
102
+ * relative to the beginning of a clip.
103
+ */
104
+ play: function (start, end) {
105
+ this.seekTo(start);
106
+ this.media.play();
107
+ end && this.setPlayEnd(end);
108
+ },
109
+
110
+ /**
111
+ * Pauses the loaded audio.
112
+ */
113
+ pause: function () {
114
+ this.media.pause();
115
+ this.clearPlayEnd();
116
+ },
117
+
118
+ setPlayEnd: function (end) {
119
+ var my = this;
120
+ this.onPlayEnd = function (time) {
121
+ if (time >= end) {
122
+ my.pause();
123
+ my.seekTo(end);
124
+ }
125
+ };
126
+ this.on('audioprocess', this.onPlayEnd);
127
+ },
128
+
129
+ clearPlayEnd: function () {
130
+ if (this.onPlayEnd) {
131
+ this.un('audioprocess', this.onPlayEnd);
132
+ this.onPlayEnd = null;
133
+ }
134
+ },
135
+
136
+ getPeaks: function (length) {
137
+ if (this.buffer) {
138
+ return WaveSurfer.WebAudio.getPeaks.call(this, length);
139
+ }
140
+ return this.peaks || [];
141
+ },
142
+
143
+ getVolume: function () {
144
+ return this.media.volume;
145
+ },
146
+
147
+ setVolume: function (val) {
148
+ this.media.volume = val;
149
+ },
150
+
151
+ destroy: function () {
152
+ this.pause();
153
+ this.unAll();
154
+ this.media.parentNode && this.media.parentNode.removeChild(this.media);
155
+ this.media = null;
156
+ }
157
+ });
158
+
159
+ //For backwards compatibility
160
+ WaveSurfer.AudioElement = WaveSurfer.MediaElement;
@@ -0,0 +1,132 @@
1
+ /* Common utilities */
2
+ WaveSurfer.util = {
3
+ extend: function (dest) {
4
+ var sources = Array.prototype.slice.call(arguments, 1);
5
+ sources.forEach(function (source) {
6
+ Object.keys(source).forEach(function (key) {
7
+ dest[key] = source[key];
8
+ });
9
+ });
10
+ return dest;
11
+ },
12
+
13
+ getId: function () {
14
+ return 'wavesurfer_' + Math.random().toString(32).substring(2);
15
+ },
16
+
17
+ ajax: function (options) {
18
+ var ajax = Object.create(WaveSurfer.Observer);
19
+ var xhr = new XMLHttpRequest();
20
+ var fired100 = false;
21
+
22
+ xhr.open(options.method || 'GET', options.url, true);
23
+ xhr.responseType = options.responseType || 'json';
24
+
25
+ xhr.addEventListener('progress', function (e) {
26
+ ajax.fireEvent('progress', e);
27
+ if (e.lengthComputable && e.loaded == e.total) {
28
+ fired100 = true;
29
+ }
30
+ });
31
+
32
+ xhr.addEventListener('load', function (e) {
33
+ if (!fired100) {
34
+ ajax.fireEvent('progress', e);
35
+ }
36
+ ajax.fireEvent('load', e);
37
+
38
+ if (200 == xhr.status || 206 == xhr.status) {
39
+ ajax.fireEvent('success', xhr.response, e);
40
+ } else {
41
+ ajax.fireEvent('error', e);
42
+ }
43
+ });
44
+
45
+ xhr.addEventListener('error', function (e) {
46
+ ajax.fireEvent('error', e);
47
+ });
48
+
49
+ xhr.send();
50
+ ajax.xhr = xhr;
51
+ return ajax;
52
+ }
53
+ };
54
+
55
+
56
+ /* Observer */
57
+ WaveSurfer.Observer = {
58
+ /**
59
+ * Attach a handler function for an event.
60
+ */
61
+ on: function (event, fn) {
62
+ if (!this.handlers) { this.handlers = {}; }
63
+
64
+ var handlers = this.handlers[event];
65
+ if (!handlers) {
66
+ handlers = this.handlers[event] = [];
67
+ }
68
+ handlers.push(fn);
69
+
70
+ // Return an event descriptor
71
+ return {
72
+ name: event,
73
+ callback: fn,
74
+ un: this.un.bind(this, event, fn)
75
+ };
76
+ },
77
+
78
+ /**
79
+ * Remove an event handler.
80
+ */
81
+ un: function (event, fn) {
82
+ if (!this.handlers) { return; }
83
+
84
+ var handlers = this.handlers[event];
85
+ if (handlers) {
86
+ if (fn) {
87
+ for (var i = handlers.length - 1; i >= 0; i--) {
88
+ if (handlers[i] == fn) {
89
+ handlers.splice(i, 1);
90
+ }
91
+ }
92
+ } else {
93
+ handlers.length = 0;
94
+ }
95
+ }
96
+ },
97
+
98
+ /**
99
+ * Remove all event handlers.
100
+ */
101
+ unAll: function () {
102
+ this.handlers = null;
103
+ },
104
+
105
+ /**
106
+ * Attach a handler to an event. The handler is executed at most once per
107
+ * event type.
108
+ */
109
+ once: function (event, handler) {
110
+ var my = this;
111
+ var fn = function () {
112
+ handler.apply(this, arguments);
113
+ setTimeout(function () {
114
+ my.un(event, fn);
115
+ }, 0);
116
+ };
117
+ return this.on(event, fn);
118
+ },
119
+
120
+ fireEvent: function (event) {
121
+ if (!this.handlers) { return; }
122
+ var handlers = this.handlers[event];
123
+ var args = Array.prototype.slice.call(arguments, 1);
124
+ handlers && handlers.forEach(function (fn) {
125
+ fn.apply(null, args);
126
+ });
127
+ }
128
+ };
129
+
130
+
131
+ /* Make the main WaveSurfer object an observer */
132
+ WaveSurfer.util.extend(WaveSurfer, WaveSurfer.Observer);
@@ -0,0 +1,440 @@
1
+ /**
2
+ * wavesurfer.js
3
+ *
4
+ * https://github.com/katspaugh/wavesurfer.js
5
+ *
6
+ * This work is licensed under a Creative Commons Attribution 3.0 Unported License.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ var WaveSurfer = {
12
+ defaultParams: {
13
+ height : 128,
14
+ waveColor : '#999',
15
+ progressColor : '#555',
16
+ cursorColor : '#333',
17
+ cursorWidth : 1,
18
+ skipLength : 2,
19
+ minPxPerSec : 20,
20
+ pixelRatio : window.devicePixelRatio,
21
+ fillParent : true,
22
+ scrollParent : false,
23
+ hideScrollbar : false,
24
+ normalize : false,
25
+ audioContext : null,
26
+ container : null,
27
+ dragSelection : true,
28
+ loopSelection : true,
29
+ audioRate : 1,
30
+ interact : true,
31
+ splitChannels : false,
32
+ renderer : 'Canvas',
33
+ backend : 'WebAudio',
34
+ mediaType : 'audio'
35
+ },
36
+
37
+ init: function (params) {
38
+ // Extract relevant parameters (or defaults)
39
+ this.params = WaveSurfer.util.extend({}, this.defaultParams, params);
40
+
41
+ this.container = 'string' == typeof params.container ?
42
+ document.querySelector(this.params.container) :
43
+ this.params.container;
44
+
45
+ if (!this.container) {
46
+ throw new Error('Container element not found');
47
+ }
48
+
49
+ if (typeof this.params.mediaContainer == 'undefined') {
50
+ this.mediaContainer = this.container;
51
+ } else if (typeof this.params.mediaContainer == 'string') {
52
+ this.mediaContainer = document.querySelector(this.params.mediaContainer);
53
+ } else {
54
+ this.mediaContainer = this.params.mediaContainer;
55
+ }
56
+
57
+ if (!this.mediaContainer) {
58
+ throw new Error('Media Container element not found');
59
+ }
60
+
61
+ // Used to save the current volume when muting so we can
62
+ // restore once unmuted
63
+ this.savedVolume = 0;
64
+ // The current muted state
65
+ this.isMuted = false;
66
+ // Will hold a list of event descriptors that need to be
67
+ // cancelled on subsequent loads of audio
68
+ this.tmpEvents = [];
69
+
70
+ this.createDrawer();
71
+ this.createBackend();
72
+ },
73
+
74
+ createDrawer: function () {
75
+ var my = this;
76
+
77
+ this.drawer = Object.create(WaveSurfer.Drawer[this.params.renderer]);
78
+ this.drawer.init(this.container, this.params);
79
+
80
+ this.drawer.on('redraw', function () {
81
+ my.drawBuffer();
82
+ my.drawer.progress(my.backend.getPlayedPercents());
83
+ });
84
+
85
+ // Click-to-seek
86
+ this.drawer.on('click', function (e, progress) {
87
+ setTimeout(function () {
88
+ my.seekTo(progress);
89
+ }, 0);
90
+ });
91
+
92
+ // Relay the scroll event from the drawer
93
+ this.drawer.on('scroll', function (e) {
94
+ my.fireEvent('scroll', e);
95
+ });
96
+ },
97
+
98
+ createBackend: function () {
99
+ var my = this;
100
+
101
+ if (this.backend) {
102
+ this.backend.destroy();
103
+ }
104
+
105
+ // Back compat
106
+ if (this.params.backend == 'AudioElement') {
107
+ this.params.backend = 'MediaElement';
108
+ }
109
+
110
+ if (this.params.backend == 'WebAudio' && !WaveSurfer.WebAudio.supportsWebAudio()) {
111
+ this.params.backend = 'MediaElement';
112
+ }
113
+
114
+ this.backend = Object.create(WaveSurfer[this.params.backend]);
115
+ this.backend.init(this.params);
116
+
117
+ this.backend.on('finish', function () {
118
+ my.fireEvent('finish');
119
+ });
120
+
121
+ this.backend.on('audioprocess', function (time) {
122
+ my.drawer.progress(my.backend.getPlayedPercents());
123
+ my.fireEvent('audioprocess', time);
124
+ });
125
+ },
126
+
127
+ getDuration: function () {
128
+ return this.backend.getDuration();
129
+ },
130
+
131
+ getCurrentTime: function () {
132
+ return this.backend.getCurrentTime();
133
+ },
134
+
135
+ play: function (start, end) {
136
+ this.backend.play(start, end);
137
+ this.fireEvent('play');
138
+ },
139
+
140
+ pause: function () {
141
+ this.backend.pause();
142
+ this.fireEvent('pause');
143
+ },
144
+
145
+ playPause: function () {
146
+ this.backend.isPaused() ? this.play() : this.pause();
147
+ },
148
+
149
+ skipBackward: function (seconds) {
150
+ this.skip(-seconds || -this.params.skipLength);
151
+ },
152
+
153
+ skipForward: function (seconds) {
154
+ this.skip(seconds || this.params.skipLength);
155
+ },
156
+
157
+ skip: function (offset) {
158
+ var position = this.getCurrentTime() || 0;
159
+ var duration = this.getDuration() || 1;
160
+ position = Math.max(0, Math.min(duration, position + (offset || 0)));
161
+ this.seekAndCenter(position / duration);
162
+ },
163
+
164
+ seekAndCenter: function (progress) {
165
+ this.seekTo(progress);
166
+ this.drawer.recenter(progress);
167
+ },
168
+
169
+ seekTo: function (progress) {
170
+ var paused = this.backend.isPaused();
171
+ // avoid small scrolls while paused seeking
172
+ var oldScrollParent = this.params.scrollParent;
173
+ if (paused) {
174
+ this.params.scrollParent = false;
175
+ }
176
+ this.backend.seekTo(progress * this.getDuration());
177
+ this.drawer.progress(this.backend.getPlayedPercents());
178
+
179
+ if (!paused) {
180
+ this.backend.pause();
181
+ this.backend.play();
182
+ }
183
+ this.params.scrollParent = oldScrollParent;
184
+ this.fireEvent('seek', progress);
185
+ },
186
+
187
+ stop: function () {
188
+ this.pause();
189
+ this.seekTo(0);
190
+ this.drawer.progress(0);
191
+ },
192
+
193
+ /**
194
+ * Set the playback volume.
195
+ *
196
+ * @param {Number} newVolume A value between 0 and 1, 0 being no
197
+ * volume and 1 being full volume.
198
+ */
199
+ setVolume: function (newVolume) {
200
+ this.backend.setVolume(newVolume);
201
+ },
202
+
203
+ /**
204
+ * Set the playback rate.
205
+ *
206
+ * @param {Number} rate A positive number. E.g. 0.5 means half the
207
+ * normal speed, 2 means double speed and so on.
208
+ */
209
+ setPlaybackRate: function (rate) {
210
+ this.backend.setPlaybackRate(rate);
211
+ },
212
+
213
+ /**
214
+ * Toggle the volume on and off. It not currenly muted it will
215
+ * save the current volume value and turn the volume off.
216
+ * If currently muted then it will restore the volume to the saved
217
+ * value, and then rest the saved value.
218
+ */
219
+ toggleMute: function () {
220
+ if (this.isMuted) {
221
+ // If currently muted then restore to the saved volume
222
+ // and update the mute properties
223
+ this.backend.setVolume(this.savedVolume);
224
+ this.isMuted = false;
225
+ } else {
226
+ // If currently not muted then save current volume,
227
+ // turn off the volume and update the mute properties
228
+ this.savedVolume = this.backend.getVolume();
229
+ this.backend.setVolume(0);
230
+ this.isMuted = true;
231
+ }
232
+ },
233
+
234
+ toggleScroll: function () {
235
+ this.params.scrollParent = !this.params.scrollParent;
236
+ this.drawBuffer();
237
+ },
238
+
239
+ toggleInteraction: function () {
240
+ this.params.interact = !this.params.interact;
241
+ },
242
+
243
+ drawBuffer: function () {
244
+ var nominalWidth = Math.round(
245
+ this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
246
+ );
247
+ var parentWidth = this.drawer.getWidth();
248
+ var width = nominalWidth;
249
+
250
+ // Fill container
251
+ if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
252
+ width = parentWidth;
253
+ }
254
+
255
+ var peaks = this.backend.getPeaks(width);
256
+ this.drawer.drawPeaks(peaks, width);
257
+ this.fireEvent('redraw', peaks, width);
258
+ },
259
+
260
+ /**
261
+ * Internal method.
262
+ */
263
+ loadArrayBuffer: function (arraybuffer) {
264
+ this.decodeArrayBuffer(arraybuffer, function (data) {
265
+ this.loadDecodedBuffer(data);
266
+ }.bind(this));
267
+ },
268
+
269
+ /**
270
+ * Directly load an externally decoded AudioBuffer.
271
+ */
272
+ loadDecodedBuffer: function (buffer) {
273
+ this.backend.load(buffer);
274
+ this.drawBuffer();
275
+ this.fireEvent('ready');
276
+ },
277
+
278
+ /**
279
+ * Loads audio data from a Blob or File object.
280
+ *
281
+ * @param {Blob|File} blob Audio data.
282
+ */
283
+ loadBlob: function (blob) {
284
+ var my = this;
285
+ // Create file reader
286
+ var reader = new FileReader();
287
+ reader.addEventListener('progress', function (e) {
288
+ my.onProgress(e);
289
+ });
290
+ reader.addEventListener('load', function (e) {
291
+ my.loadArrayBuffer(e.target.result);
292
+ });
293
+ reader.addEventListener('error', function () {
294
+ my.fireEvent('error', 'Error reading file');
295
+ });
296
+ reader.readAsArrayBuffer(blob);
297
+ this.empty();
298
+ },
299
+
300
+ /**
301
+ * Loads audio and rerenders the waveform.
302
+ */
303
+ load: function (url, peaks) {
304
+ switch (this.params.backend) {
305
+ case 'WebAudio': return this.loadBuffer(url);
306
+ case 'MediaElement': return this.loadMediaElement(url, peaks);
307
+ }
308
+ },
309
+
310
+ /**
311
+ * Loads audio using Web Audio buffer backend.
312
+ */
313
+ loadBuffer: function (url) {
314
+ this.empty();
315
+ // load via XHR and render all at once
316
+ return this.getArrayBuffer(url, this.loadArrayBuffer.bind(this));
317
+ },
318
+
319
+ loadMediaElement: function (url, peaks) {
320
+ this.empty();
321
+ this.backend.load(url, this.mediaContainer, peaks);
322
+
323
+ this.tmpEvents.push(
324
+ this.backend.once('canplay', (function () {
325
+ this.drawBuffer();
326
+ this.fireEvent('ready');
327
+ }).bind(this)),
328
+
329
+ this.backend.once('error', (function (err) {
330
+ this.fireEvent('error', err);
331
+ }).bind(this))
332
+ );
333
+
334
+
335
+ // If no pre-decoded peaks provided, attempt to download the
336
+ // audio file and decode it with Web Audio.
337
+ if (!peaks && this.backend.supportsWebAudio()) {
338
+ this.getArrayBuffer(url, (function (arraybuffer) {
339
+ this.decodeArrayBuffer(arraybuffer, (function (buffer) {
340
+ this.backend.buffer = buffer;
341
+ this.drawBuffer();
342
+ }).bind(this));
343
+ }).bind(this));
344
+ }
345
+ },
346
+
347
+ decodeArrayBuffer: function (arraybuffer, callback) {
348
+ this.backend.decodeArrayBuffer(
349
+ arraybuffer,
350
+ this.fireEvent.bind(this, 'decoded'),
351
+ this.fireEvent.bind(this, 'error', 'Error decoding audiobuffer')
352
+ );
353
+ this.tmpEvents.push(
354
+ this.once('decoded', callback)
355
+ );
356
+ },
357
+
358
+ getArrayBuffer: function (url, callback) {
359
+ var my = this;
360
+ var ajax = WaveSurfer.util.ajax({
361
+ url: url,
362
+ responseType: 'arraybuffer'
363
+ });
364
+ this.tmpEvents.push(
365
+ ajax.on('progress', function (e) {
366
+ my.onProgress(e);
367
+ }),
368
+ ajax.on('success', callback),
369
+ ajax.on('error', function (e) {
370
+ my.fireEvent('error', 'XHR error: ' + e.target.statusText);
371
+ })
372
+ );
373
+ return ajax;
374
+ },
375
+
376
+ onProgress: function (e) {
377
+ if (e.lengthComputable) {
378
+ var percentComplete = e.loaded / e.total;
379
+ } else {
380
+ // Approximate progress with an asymptotic
381
+ // function, and assume downloads in the 1-3 MB range.
382
+ percentComplete = e.loaded / (e.loaded + 1000000);
383
+ }
384
+ this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
385
+ },
386
+
387
+ /**
388
+ * Exports PCM data into a JSON array and opens in a new window.
389
+ */
390
+ exportPCM: function (length, accuracy, noWindow) {
391
+ length = length || 1024;
392
+ accuracy = accuracy || 10000;
393
+ noWindow = noWindow || false;
394
+ var peaks = this.backend.getPeaks(length, accuracy);
395
+ var arr = [].map.call(peaks, function (val) {
396
+ return Math.round(val * accuracy) / accuracy;
397
+ });
398
+ var json = JSON.stringify(arr);
399
+ if (!noWindow) {
400
+ window.open('data:application/json;charset=utf-8,' +
401
+ encodeURIComponent(json));
402
+ }
403
+ return json;
404
+ },
405
+
406
+ clearTmpEvents: function () {
407
+ this.tmpEvents.forEach(function (e) { e.un(); });
408
+ },
409
+
410
+ /**
411
+ * Display empty waveform.
412
+ */
413
+ empty: function () {
414
+ if (!this.backend.isPaused()) {
415
+ this.stop();
416
+ this.backend.disconnectSource();
417
+ }
418
+ this.clearTmpEvents();
419
+ this.drawer.progress(0);
420
+ this.drawer.setWidth(0);
421
+ this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
422
+ },
423
+
424
+ /**
425
+ * Remove events, elements and disconnect WebAudio nodes.
426
+ */
427
+ destroy: function () {
428
+ this.fireEvent('destroy');
429
+ this.clearTmpEvents();
430
+ this.unAll();
431
+ this.backend.destroy();
432
+ this.drawer.destroy();
433
+ }
434
+ };
435
+
436
+ WaveSurfer.create = function (params) {
437
+ var wavesurfer = Object.create(WaveSurfer);
438
+ wavesurfer.init(params);
439
+ return wavesurfer;
440
+ };