New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

wavesurfer.js

Package Overview
Dependencies
Maintainers
1
Versions
303
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wavesurfer.js - npm Package Compare versions

Comparing version 0.1.8 to 1.0.4

.idea/.name

52

example/elan/app.js

@@ -77,6 +77,4 @@ 'use strict';

var prevAnnotation, prevRow;
var onProgress = function () {
var duration = wavesurfer.backend.getDuration();
var time = wavesurfer.backend.getCurrentTime();
var prevAnnotation, prevRow, region;
var onProgress = function (time) {
var annotation = elan.getRenderedAnnotation(time);

@@ -87,2 +85,5 @@

region && region.remove();
region = null;
if (annotation) {

@@ -99,9 +100,9 @@ // Highlight annotation table row

// Selection
wavesurfer.updateSelection({
startPercentage: annotation.start / duration,
endPercentage: annotation.end / duration
// Region
region = wavesurfer.addRegion({
start: annotation.start,
end: annotation.end,
resize: false,
color: 'rgba(223, 240, 216, 0.7)'
});
} else {
wavesurfer.clearSelection();
}

@@ -111,32 +112,3 @@ }

wavesurfer.on('progress', onProgress);
wavesurfer.on('audioprocess', onProgress);
});
// Bind buttons and keypresses
wavesurfer.on('ready', function () {
var handlers = {
'play': function () {
wavesurfer.playPause();
}
};
var map = {
32: 'play' // spacebar
};
document.addEventListener('keydown', function (e) {
if (e.keyCode in map) {
e.preventDefault();
var handler = handlers[map[e.keyCode]];
handler && handler(e);
}
});
document.addEventListener('click', function (e) {
var action = e.target.dataset && e.target.dataset.action;
if (action && action in handlers) {
handlers[action](e);
}
});
});

@@ -23,30 +23,30 @@ 'use strict';

f: 32,
type: 'LOWSHELF'
type: 'lowshelf'
}, {
f: 64,
type: 'PEAKING'
type: 'peaking'
}, {
f: 125,
type: 'PEAKING'
type: 'peaking'
}, {
f: 250,
type: 'PEAKING'
type: 'peaking'
}, {
f: 500,
type: 'PEAKING'
type: 'peaking'
}, {
f: 1000,
type: 'PEAKING'
type: 'peaking'
}, {
f: 2000,
type: 'PEAKING'
type: 'peaking'
}, {
f: 4000,
type: 'PEAKING'
type: 'peaking'
}, {
f: 8000,
type: 'PEAKING'
type: 'peaking'
}, {
f: 16000,
type: 'HIGHSHELF'
type: 'highshelf'
}

@@ -58,3 +58,3 @@ ];

var filter = wavesurfer.backend.ac.createBiquadFilter();
filter.type = filter[band.type];
filter.type = band.type;
filter.gain.value = 0;

@@ -66,12 +66,5 @@ filter.Q.value = 1;

// Connect filters in a series
filters.forEach(function (filter, index) {
if (index < filters.length - 1) {
filter.connect(filters[index + 1]);
}
});
// Connect filters to wavesurfer
wavesurfer.backend.setFilters(filters);
// Connect the last filter to wavesurfer
wavesurfer.backend.setFilter(filters[0], filters[filters.length - 1]);
// Bind filters to vertical range sliders

@@ -88,2 +81,3 @@ var container = document.querySelector('#equalizer');

});
input.style.display = 'inline-block';
input.setAttribute('orient', 'vertical');

@@ -90,0 +84,0 @@ wavesurfer.drawer.style(input, {

@@ -12,5 +12,3 @@ 'use strict';

progressColor : 'purple',
loaderColor : 'purple',
cursorColor : 'navy',
markerWidth : 2
cursorColor : 'navy'
};

@@ -23,26 +21,2 @@

if (location.search.match('normalize')) {
options.normalize = true;
}
/* Progress bar */
(function () {
var progressDiv = document.querySelector('#progress-bar');
var progressBar = progressDiv.querySelector('.progress-bar');
var showProgress = function (percent) {
progressDiv.style.display = 'block';
progressBar.style.width = percent + '%';
};
var hideProgress = function () {
progressDiv.style.display = 'none';
};
wavesurfer.on('loading', showProgress);
wavesurfer.on('ready', hideProgress);
wavesurfer.on('destroy', hideProgress);
wavesurfer.on('error', hideProgress);
}());
// Init

@@ -52,2 +26,9 @@ wavesurfer.init(options);

wavesurfer.load('example/media/demo.wav');
// Regions
if (wavesurfer.enableDragSelection) {
wavesurfer.enableDragSelection({
color: 'rgba(0, 255, 0, 0.1)'
});
}
});

@@ -61,2 +42,7 @@

// Report errors
wavesurfer.on('error', function (err) {
console.error(err);
});
// Do something when the clip is over

@@ -67,79 +53,23 @@ wavesurfer.on('finish', function () {

// Bind buttons and keypresses
(function () {
var eventHandlers = {
'play': function () {
wavesurfer.playPause();
},
'green-mark': function () {
wavesurfer.mark({
id: 'up',
color: 'rgba(0, 255, 0, 0.5)',
position: wavesurfer.getCurrentTime()
});
},
/* Progress bar */
document.addEventListener('DOMContentLoaded', function () {
var progressDiv = document.querySelector('#progress-bar');
var progressBar = progressDiv.querySelector('.progress-bar');
'red-mark': function () {
wavesurfer.mark({
id: 'down',
color: 'rgba(255, 0, 0, 0.5)',
position: wavesurfer.getCurrentTime()
});
},
var showProgress = function (percent) {
progressDiv.style.display = 'block';
progressBar.style.width = percent + '%';
};
'back': function () {
wavesurfer.skipBackward();
},
'forth': function () {
wavesurfer.skipForward();
},
'toggle-mute': function () {
wavesurfer.toggleMute();
}
var hideProgress = function () {
progressDiv.style.display = 'none';
};
document.addEventListener('keydown', function (e) {
var map = {
32: 'play', // space
38: 'green-mark', // up
40: 'red-mark', // down
37: 'back', // left
39: 'forth' // right
};
if (e.keyCode in map) {
var handler = eventHandlers[map[e.keyCode]];
e.preventDefault();
handler && handler(e);
}
});
document.addEventListener('click', function (e) {
var action = e.target.dataset && e.target.dataset.action;
if (action && action in eventHandlers) {
eventHandlers[action](e);
}
});
}());
// Flash mark when it's played over
wavesurfer.on('mark', function (marker) {
if (marker.timer) { return; }
marker.timer = setTimeout(function () {
var origColor = marker.color;
marker.update({ color: 'yellow' });
setTimeout(function () {
marker.update({ color: origColor });
delete marker.timer;
}, 100);
}, 100);
wavesurfer.on('loading', showProgress);
wavesurfer.on('ready', hideProgress);
wavesurfer.on('destroy', hideProgress);
wavesurfer.on('error', hideProgress);
});
wavesurfer.on('error', function (err) {
console.error(err);
});

@@ -146,0 +76,0 @@ // Drag'n'drop

@@ -8,27 +8,33 @@ 'use strict';

document.addEventListener('DOMContentLoaded', function () {
var options = {
container : '#waveform',
waveColor : 'black',
loopSelection : false,
cursorWidth : 0
};
var options = {
container : '#waveform',
waveColor : 'black',
interact : false,
cursorWidth : 0
};
var micBtn = document.querySelector('#micBtn');
// Init wavesurfer
wavesurfer.init(options);
// Init wavesurfer
wavesurfer.init(options);
// Init Microphone plugin
var microphone = Object.create(WaveSurfer.Microphone);
microphone.init({
wavesurfer: wavesurfer
});
// Init Microphone plugin
var microphone = Object.create(WaveSurfer.Microphone);
microphone.init({
wavesurfer: wavesurfer
});
microphone.on('deviceReady', function() {
console.info('Device ready!');
});
microphone.on('deviceError', function(code) {
console.warn('Device error: ' + code);
});
// start/stop mic on click
micBtn.onclick = function() {
if (microphone.active) {
microphone.stop();
} else {
microphone.start();
}
};
});
// start/stop mic on button click
micBtn.onclick = function() {
if (microphone.active) {
microphone.stop();
} else {
microphone.start();
}
};
});

@@ -60,30 +60,1 @@ 'use strict';

});
// Bind buttons and keypresses
wavesurfer.on('ready', function () {
var handlers = {
'play': function () {
wavesurfer.playPause();
}
};
var map = {
32: 'play' // spacebar
};
document.addEventListener('keydown', function (e) {
if (e.keyCode in map) {
e.preventDefault();
var handler = handlers[map[e.keyCode]];
handler && handler(e);
}
});
document.addEventListener('click', function (e) {
var action = e.target.dataset && e.target.dataset.action;
if (action && action in handlers) {
handlers[action](e);
}
});
});

@@ -1,7 +0,61 @@

window.addEventListener('load', function () {
if (!(window.AudioContext || window.webkitAudioContext)) {
document.querySelector('#demo').innerHTML =
'<img src="/example/screenshot.png" />';
var GLOBAL_ACTIONS = {
'play': function () {
wavesurfer.playPause();
},
'back': function () {
wavesurfer.skipBackward();
},
'forth': function () {
wavesurfer.skipForward();
},
'toggle-mute': function () {
wavesurfer.toggleMute();
}
};
// Bind actions to buttons and keypresses
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('keydown', function (e) {
var map = {
32: 'play', // space
37: 'back', // left
39: 'forth' // right
};
var action = map[e.keyCode];
if (action in GLOBAL_ACTIONS) {
if (document == e.target || document.body == e.target) {
e.preventDefault();
}
GLOBAL_ACTIONS[action](e);
}
});
[].forEach.call(document.querySelectorAll('[data-action]'), function (el) {
el.addEventListener('click', function (e) {
var action = e.currentTarget.dataset.action;
if (action in GLOBAL_ACTIONS) {
e.preventDefault();
GLOBAL_ACTIONS[action](e);
}
});
});
});
// Misc
document.addEventListener('DOMContentLoaded', function () {
// Web Audio not supported
if (!window.AudioContext && !window.webkitAudioContext) {
var demo = document.querySelector('#demo');
if (demo) {
demo.innerHTML = '<img src="/example/screenshot.png" />';
}
}
// Navbar links
var ul = document.querySelector('.nav-pills');

@@ -8,0 +62,0 @@ var pills = ul.querySelectorAll('li');

{
"name": "wavesurfer.js",
"version": "0.1.8",
"version": "1.0.4",
"description": "Interactive navigable audio visualization using Web Audio and Canvas",
"main": "build/wavesurfer.cjs.js",
"main": "dist/wavesurfer.min.js",
"directories": {

@@ -10,5 +10,4 @@ "example": "example"

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "make cjs",
"update": "make cjs"
"start": "grunt; ruby -run -e httpd . -p 8080",
"test": "grunt test"
},

@@ -20,7 +19,17 @@ "repository": {

"author": "",
"license": "CC-BY 3.0",
"license": "CC-BY-3.0",
"bugs": {
"url": "https://github.com/katspaugh/wavesurfer.js/issues"
},
"devDependencies": {
"grunt": ">=0.4.5",
"grunt-contrib-concat": ">=0.4.0",
"grunt-contrib-uglify": ">=0.5.0",
"grunt-contrib-jshint": ">=0.10.0",
"grunt-contrib-jasmine": ">=0.8.2",
"grunt-template-jasmine-istanbul": ">=0.3.3",
"jasmine-expect": ">=1.22.3",
"grunt-coveralls": ">=1.0.0"
},
"homepage": "https://github.com/katspaugh/wavesurfer.js"
}

@@ -17,11 +17,30 @@ (function (root, factory) {

if (!this.wavesurfer) {
throw Error('No WaveSurfer instance provided');
throw new Error('No WaveSurfer instance provided');
}
this.active = false;
this.getUserMedia = (navigator.getUserMedia ||
this.paused = false;
// cross-browser getUserMedia
this.getUserMedia = (
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia).bind(navigator);
navigator.msGetUserMedia
).bind(navigator);
// The buffer size in units of sample-frames.
// If specified, the bufferSize must be one of the following values:
// 256, 512, 1024, 2048, 4096, 8192, 16384. Defaults to 4096.
this.bufferSize = this.params.bufferSize || 4096;
// Integer specifying the number of channels for this node's input,
// defaults to 1. Values of up to 32 are supported.
this.numberOfInputChannels = this.params.numberOfInputChannels || 1;
// Integer specifying the number of channels for this node's output,
// defaults to 1. Values of up to 32 are supported.
this.numberOfOutputChannels = this.params.numberOfOutputChannels || 1;
// wavesurfer's AudioContext where we'll route the mic signal to
this.micContext = this.wavesurfer.backend.getAudioContext();

@@ -31,3 +50,4 @@ },

/**
* Allow user to select audio input device, eg. microphone.
* Allow user to select audio input device, eg. microphone, and
* start the visualization.
*/

@@ -40,27 +60,80 @@ start: function() {

this.gotStream.bind(this),
this.streamError.bind(this));
this.deviceError.bind(this));
},
/**
* Stop the microphone.
* Pause/resume visualization.
*/
togglePlay: function() {
if (!this.active) {
// start it first
this.start();
} else {
// toggle paused
this.paused = !this.paused;
if (this.paused) {
// disconnect sources so they can be used elsewhere
// (eg. during audio playback)
this.disconnect();
} else {
// resume visualization
this.connect();
}
}
},
/**
* Stop the microphone and visualization.
*/
stop: function() {
if (this.active) {
this.active = false;
if (this.active) {
this.active = false;
if (this.stream) {
this.stream.stop();
}
this.mediaStreamSource.disconnect();
this.levelChecker.disconnect();
this.wavesurfer.empty();
}
if (this.stream) {
this.stream.stop();
}
this.disconnect();
this.wavesurfer.empty();
}
},
/**
* Connect the media sources that feed the visualization.
*/
connect: function() {
if (this.stream !== undefined) {
// Create an AudioNode from the stream.
this.mediaStreamSource = this.micContext.createMediaStreamSource(this.stream);
this.levelChecker = this.micContext.createScriptProcessor(
this.bufferSize, this.numberOfInputChannels, this.numberOfOutputChannels);
this.mediaStreamSource.connect(this.levelChecker);
this.levelChecker.connect(this.micContext.destination);
this.levelChecker.onaudioprocess = this.reloadBuffer.bind(this);
}
},
/**
* Disconnect the media sources that feed the visualization.
*/
disconnect: function() {
if (this.mediaStreamSource !== undefined) {
this.mediaStreamSource.disconnect();
}
if (this.levelChecker !== undefined) {
this.levelChecker.disconnect();
}
},
/**
* Redraw the waveform.
*/
reloadBuffer: function(event) {
this.wavesurfer.empty();
this.wavesurfer.loadDecodedBuffer(event.inputBuffer);
if (!this.paused) {
this.wavesurfer.empty();
this.wavesurfer.loadDecodedBuffer(event.inputBuffer);
}
},

@@ -70,23 +143,28 @@

* Audio input device is ready.
*
* @param {LocalMediaStream} stream: the microphone's media stream.
*/
gotStream: function(stream) {
this.stream = stream;
this.active = true;
this.stream = stream;
this.active = true;
// Create an AudioNode from the stream.
this.mediaStreamSource = this.micContext.createMediaStreamSource(stream);
this.connect();
// Connect it to the destination to hear yourself (or any other node for processing!)
//this.mediaStreamSource.connect(this.audioContext.destination);
// notify listeners
this.fireEvent('deviceReady', stream);
},
this.levelChecker = this.micContext.createScriptProcessor(4096, 1 ,1);
this.mediaStreamSource.connect(this.levelChecker);
/**
* Destroy the microphone plugin.
*/
destroy: function(event) {
this.stop();
},
this.levelChecker.connect(this.micContext.destination);
this.levelChecker.onaudioprocess = this.reloadBuffer.bind(this);
},
streamError: function(error)
{
console.warn('error', error);
/**
* Device error callback.
*/
deviceError: function(code) {
// notify listeners
this.fireEvent('deviceError', code);
}

@@ -93,0 +171,0 @@

@@ -44,8 +44,22 @@ (function (root, factory) {

wavesurfer.on('redraw', this.render.bind(this));
wavesurfer.on('destroy', this.destroy.bind(this));
},
destroy: function () {
this.unAll();
if (this.wrapper && this.wrapper.parentNode) {
this.wrapper.parentNode.removeChild(this.wrapper);
this.wrapper = null;
}
},
createWrapper: function () {
var prevTimeline = this.container.querySelector('timeline');
if (prevTimeline) {
this.container.removeChild(prevTimeline);
}
var wsParams = this.wavesurfer.params;
this.wrapper = this.container.appendChild(
document.createElement('wave')
document.createElement('timeline')
);

@@ -72,3 +86,3 @@ this.drawer.style(this.wrapper, {

var relX = 'offsetX' in e ? e.offsetX : e.layerX;
my.fireEvent('click', (relX / my.scrollWidth) || 0);
my.fireEvent('click', (relX / my.wrapper.scrollWidth) || 0);
});

@@ -96,6 +110,7 @@ },

updateCanvasStyle: function () {
var width = Math.round(this.drawer.scrollWidth / this.drawer.pixelRatio);
this.canvas.width = width;
this.canvas.height = this.height;
var width = this.drawer.wrapper.scrollWidth;
this.canvas.width = width * this.wavesurfer.params.pixelRatio;
this.canvas.height = this.height * this.wavesurfer.params.pixelRatio;
this.canvas.style.width = width + 'px';
this.canvas.style.height = this.height + 'px';
},

@@ -110,10 +125,7 @@

var width = this.drawer.getWidth();
var pixelsPerSecond = width/duration;
} else {
var width = backend.getDuration() * wsParams.minPxPerSec;
var pixelsPerSecond = wsParams.minPxPerSec;
width = this.drawer.wrapper.scrollWidth * wsParams.pixelRatio;
}
var pixelsPerSecond = width/duration;
pixelsPerSecond = pixelsPerSecond / this.wavesurfer.drawer.pixelRatio;
if (duration > 0) {

@@ -123,3 +135,2 @@ var curPixel = 0,

totalSeconds = parseInt(duration, 10) + 1,
timeInterval = (pixelsPerSecond < 10) ? 10 : 1,
formatTime = function(seconds) {

@@ -136,16 +147,35 @@ if (seconds/60 > 1) {

if (pixelsPerSecond * 1 >= 25) {
var timeInterval = 1;
var primaryLabelInterval = 10;
var secondaryLabelInterval = 5;
} else if (pixelsPerSecond * 5 >= 25) {
var timeInterval = 5;
var primaryLabelInterval = 6;
var secondaryLabelInterval = 2;
} else if (pixelsPerSecond * 15 >= 25) {
var timeInterval = 15;
var primaryLabelInterval = 4;
var secondaryLabelInterval = 2;
} else {
var timeInterval = 60;
var primaryLabelInterval = 4;
var secondaryLabelInterval = 2;
}
var height1 = this.height - 4,
height2 = (this.height * (this.notchPercentHeight / 100.0)) - 4;
height2 = (this.height * (this.notchPercentHeight / 100.0)) - 4,
fontSize = this.fontSize * wsParams.pixelRatio;
for (var i = 0; i < totalSeconds/timeInterval; i++) {
if (i % 10 == 0) {
if (i % primaryLabelInterval == 0) {
this.timeCc.fillStyle = this.primaryColor;
this.timeCc.fillRect(curPixel, 0, 1, height1);
this.timeCc.font = this.fontSize + 'px ' + this.fontFamily;
this.timeCc.font = fontSize + 'px ' + this.fontFamily;
this.timeCc.fillStyle = this.primaryFontColor;
this.timeCc.fillText(formatTime(curSeconds), curPixel + 5, height1);
} else if (i % 10 == 5) {
} else if (i % secondaryLabelInterval == 0) {
this.timeCc.fillStyle = this.secondaryColor;
this.timeCc.fillRect(curPixel, 0, 1, height1);
this.timeCc.font = this.fontSize + 'px ' + this.fontFamily;
this.timeCc.font = fontSize + 'px ' + this.fontFamily;
this.timeCc.fillStyle = this.secondaryFontColor;

@@ -164,4 +194,4 @@ this.timeCc.fillText(formatTime(curSeconds), curPixel + 5, height1);

updateScroll: function(e){
this.wrapper.scrollLeft = e.target.scrollLeft;
updateScroll: function () {
this.wrapper.scrollLeft = this.drawer.wrapper.scrollLeft;
}

@@ -168,0 +198,0 @@ };

@@ -1,3 +0,2 @@

wavesurfer.js
=============
# wavesurfer.js

@@ -8,7 +7,22 @@ Interactive navigable audio visualization using

![Imgur](http://i.imgur.com/dnH8q.png)
![Screenshot](example/screenshot.png?raw=true "Screenshot")
API in examples
===============
## Browser support
wavesurfer.js works only in modern browsers supporting Web Audio
(Chrome, Firefox, Safari, Opera etc).
It will fallback to Audio Element in other browsers (without
graphics). You can also try
[wavesurfer.swf](https://github.com/laurentvd/wavesurfer.swf) which is
a Flash-based fallback with graphics.
## FAQ
### Can the audio start playing before the waveform is drawn?
Yes, if you use the `backend: 'MediaElement'` option. See here: http://wavesurfer-js.org/example/audio-element/. The audio will start playing as you press play. A thin line will be displayed until the whole audio file is downloaded and decoded to draw the waveform.
### Can drawing be done as file loads?
No. Web Audio needs the whole file to decode it in the browser. You can however load pre-decoded waveform data to draw the waveform immediately. See here: http://wavesurfer-js.org/example/audio-element/ (the "Pre-recoded Peaks" section).
## API in examples
Create an instance:

@@ -23,4 +37,7 @@

```javascript
wavesurfer.init({ container: '#wave', waveColor: 'violet',
progressColor: 'purple' });
wavesurfer.init({
container: '#wave',
waveColor: 'violet',
progressColor: 'purple'
});
```

@@ -31,3 +48,5 @@

```javascript
wavesurfer.on('ready', function () { wavesurfer.play(); });
wavesurfer.on('ready', function () {
wavesurfer.play();
});
```

@@ -41,103 +60,70 @@

See the example code
[here](https://github.com/katspaugh/wavesurfer.js/blob/master/example/main.js).
See the example code [here](/example/main.js).
Options
=======
For a list of other projects using wavesurfer.js, check out
[the wiki](https://github.com/katspaugh/wavesurfer.js/wiki/Projects)
where you can also add your own project.
* `container` – CSS-selector or HTML-element where the waveform
should be drawn. This is the only required parameter.
* `height` – the height of the waveform. `128` by default.
* `skipLength` – number of seconds to skip with the `skipForward()`
and `skipBackward()` methods (`2` by default).
* `minPxPerSec` – minimum number of pixels per second of audio (`1`
by default).
* `fillParent` – whether to fill the entire container or draw only
according to `minPxPerSec` (`true` by default).
* `scrollParent` – whether to scroll the container with a lengthy
waveform. Otherwise the waveform is shrinked to container width
(see `fillParent`).
* `normalize` – if `true`, normalize by the maximum peak instead of
1.0 (`false` by default).
* `pixelRatio` – equals `window.devicePixelRatio` by default, but
you can set it to `1` for faster rendering.
* `audioContext` – use your own previously initialized
`AudioContext` or leave blank.
* `cursorWidth` – 1 px by default.
* `markerWidth` – 1 px by default.
* `waveColor` – the fill color of the waveform after the cursor.
* `progressColor` – the fill color of the part of the waveform
behind the cursor.
* `cursorColor` – the fill color of the cursor indicating the
playhead position.
* `dragSelection` – enable drag selection (`true` by default).
* `loopSelection` – whether playback should loop inside the selected
region (`true` by default). Has no effect if `dragSelection` is
`false`.
* `interact` – whether the mouse interaction will enabled at
initialisation (`true` by default).
## WaveSurfer Options
Methods
=======
| option | type | default | description |
| --- | --- | --- | --- |
| `audioContext` | string | `null` | Use your own previously initialized `AudioContext` or leave blank. |
| `audioRate` | float | `1` | Speed at which to play audio. Lower number is slower. |
| `backend` | string | `WebAudio` | `WebAudio` or `MediaElement`. In most cases you don't have to set this manually. `MediaElement` is a fallback for unsupported browsers. |
| `barWidth` | number | If specified, the waveform will be drawn in bars like this: ▁ ▂ ▇ ▃ ▅ ▂
| `container` | mixed | _none_ | CSS-selector or HTML-element where the waveform should be drawn. This is the only required parameter. |
| `cursorColor` | string | `#333` | The fill color of the cursor indicating the playhead position. |
| `cursorWidth` | integer | `1` | Measured in pixels. |
| `fillParent` | boolean | `true` | Whether to fill the entire container or draw only according to `minPxPerSec`. |
| `height` | integer | `128` | The height of the waveform. Measured in pixels. |
| `hideScrollbar` | boolean | `false` | Whether to hide the horizontal scrollbar when one would normally be shown. |
| `interact` | boolean | `true` | Whether the mouse interaction will be enabled at initialization. You can switch this parameter at any time later on. |
| `minPxPerSec` | integer | `50` | Minimum number of pixels per second of audio. |
| `normalize` | boolean | `false` | If `true`, normalize by the maximum peak instead of 1.0. |
| `pixelRatio` | integer | `window.devicePixelRatio` | Can be set to `1` for faster rendering. |
| `progressColor` | string | `#555` | The fill color of the part of the waveform behind the cursor. |
| `scrollParent` | boolean | `false` | Whether to scroll the container with a lengthy waveform. Otherwise the waveform is shrunk to the container width (see `fillParent`). |
| `skipLength` | float | `2` | Number of seconds to skip with the `skipForward()` and `skipBackward()` methods. |
| `waveColor` | string | `#999` | The fill color of the waveform after the cursor. |
All methods are intentionally public, but the most readily available
are the following:
## WaveSurfer Methods
* `init(params)` – initializes with the options listed above.
* `on(eventName, callback)` – subscribes to an event.
* `load(url)` – loads an audio from URL via XHR. Returns XHR object.
* `getDuration()` – returns the duration of an audio clip in seconds.
* `getCurrentTime()` – returns current progress in seconds.
* `play()` – starts playback from the current position.
* `pause()` – stops playback.
* `playPause()` – plays if paused, pauses if playing.
* `stop()` – stops and goes to the beginning.
* `skipForward()`
* `skipBackward()`
* `seekTo(progress)` – seeks to a progress [0..1].
* `seekAndCenter(progress)` – seeks to a progress and centers view [0..1].
* `skip(offset)` – skips a number of seconds from the current
position (use a negative value to go backwards).
* `setVolume(newVolume)` – sets the playback volume to a new value
(use a floating point value between 0 and 1, 0 being no volume and
1 being full volume).
* `toggleMute()` – toggles the volume on and off.
* `mark(options)` – creates a visual marker on the waveform. Options
are `id` (random if not set), `position` (in seconds), `color` and
`width` (defaults to the global option `markerWidth`). Returns a
marker object which you can update later.
(`marker.update(options)`).
* `clearMarks()` – removes all markers.
* `clearRegions()` – removes all regions.
* `empty()` – clears the waveform as if a zero-length audio is
loaded.
* `destroy()` – removes events, elements and disconnects Web Audio
nodes.
* `region(options)` – creates a region on the waveform. Options are `id`
(random if not set), `startPosition` (in seconds), `endPosition`
(in seconds) and `color`. Returns a region object which you can
update later.
* `toggleLoopSelection()` – toggles whether playback should loop
inside the selection.
* `toggleScroll()` – toggles scroll on parent
* `getSelection()` – returns an object representing the current
selection. This object will have the following keys:
`startPercentage` (float between 0 and 1), `startPosition` (in
seconds), `endPercentage` (float between 0 and 1) and `endPosition`
(in seconds). Returns `null` if no selection is present.
* `updateSelection({ startPercentage, endPercentage })` – create or
update a visual selection.
* `enableInteraction()` – Enable mouse interaction
* `disableInteraction()` – Disable mouse interaction
* `toggleInteraction()` – Toggle mouse interaction
* `setPlaybackRate(rate)` – sets the speed of playback (`0.5` is half
normal speed, `2` is double speed and so on).
* `playPauseSelection()` – plays selection if paused, pauses if playing.
All methods are intentionally public, but the most readily available are the following:
Connecting filters
==================
* `init(options)` – Initializes with the options listed above.
* `destroy()` – Removes events, elements and disconnects Web Audio nodes.
* `empty()` – Clears the waveform as if a zero-length audio is loaded.
* `getCurrentTime()` – Returns current progress in seconds.
* `getDuration()` – Returns the duration of an audio clip in seconds.
* `isPlaying()` – Returns true if currently playing, false otherwise.
* `load(url)` – Loads audio from URL via XHR. Returns XHR object.
* `loadBlob(url)` – Loads audio from a `Blob` or `File` object.
* `on(eventName, callback)` – Subscribes to an event. See [WaveSurfer Events](#wavesurfer-events) section below for a list.
* `un(eventName, callback)` – Unsubscribes from an event.
* `unAll()` – Unsubscribes from all events.
* `pause()` – Stops playback.
* `play([start[, end]])` – Starts playback from the current position. Optional `start` and `end` measured in seconds can be used to set the range of audio to play.
* `playPause()` – Plays if paused, pauses if playing.
* `seekAndCenter(progress)` – Seeks to a progress and centers view `[0..1]` (0 = beginning, 1 = end).
* `seekTo(progress)` – Seeks to a progress `[0..1]` (0=beginning, 1=end).
* `setFilter(filters)` - For inserting your own WebAudio nodes into the graph. See [Connecting Filters](#connecting-filters) below.
* `setPlaybackRate(rate)` – Sets the speed of playback (`0.5` is half speed, `1` is normal speed, `2` is double speed and so on).
* `setVolume(newVolume)` – Sets the playback volume to a new value `[0..1]` (0 = silent, 1 = maximum).
* `skip(offset)` – Skip a number of seconds from the current position (use a negative value to go backwards).
* `skipBackward()` - Rewind `skipLength` seconds.
* `skipForward()` - Skip ahead `skipLength` seconds.
* `stop()` – Stops and goes to the beginning.
* `toggleMute()` – Toggles the volume on and off.
* `toggleInteraction()` – Toggle mouse interaction.
* `toggleScroll()` – Toggles `scrollParent`.
* `zoom(pxPerSec)` – Horiontally zooms the waveform in and out. The
parameter is a number of horizontal pixels per second of audio. It
also changes the parameter `minPxPerSec` and enables the
`scrollParent` option.
You can insert your own Web Audio nodes into the graph using the
method `setFilter`. Example:
##### Connecting Filters
You can insert your own Web Audio nodes into the graph using the method `setFilter()`. Example:
```javascript

@@ -148,33 +134,128 @@ var lowpass = wavesurfer.backend.ac.createBiquadFilter();

Events
======
## WaveSurfer Events
You can listen to the following events:
General events:
* `ready` – when audio is loaded, decoded and the waveform drawn.
* `loading` – fires continuously when loading via XHR or
drag'n'drop. Callback recieves loading progress in percents (from 0
to 100) and the event target.
* `seek` – on seeking.
* `play` – when it starts playing.
* `finish` – when it finishes playing.
* `progress` – fires continuously during playback.
* `mark` – when a mark is reached. Passes the mark object.
* `marked` – when a mark is created.
* `mark-update` – when a mark is updated.
* `mark-removed` – when a mark is removed.
* `region-in` – when entering a region.
* `region-out`– when leaving a region.
* `region-created` – when a region is created.
* `region-updated` – when a region is updated.
* `region-removed` – when a region is removed.
* `selection-update` – when a selection is updated. Has an object parameter
containig selection information or null if the selection is cleared.
* `error` – on error, passes an error message.
* `error` – Occurs on error. Callback will receive (string) error message.
* `finish` – When it finishes playing.
* `loading` – Fires continuously when loading via XHR or drag'n'drop. Callback will receive (integer) loading progress in percents [0..100] and (object) event target.
* `mouseup` - When a mouse button goes up. Callback will receive `MouseEvent` object.
* `pause` – When audio is paused.
* `play` – When play starts.
* `ready` – When audio is loaded, decoded and the waveform drawn.
* `scroll` - When the scrollbar is moved. Callback will receive a `ScrollEvent` object.
* `seek` – On seeking. Callback will receive (float) progress [0..1].
Each of mark objects also fire the event `reached` when played over.
Region events (exposed by the Regions plugin):
Credits
=======
* `region-in` – When playback enters a region. Callback will receive the `Region` object.
* `region-out`– When playback leaves a region. Callback will receive the `Region` object.
* `region-mouseenter` - When the mouse moves over a region. Callback will receive the `Region` object, and a `MouseEvent` object.
* `region-mouseleave` - When the mouse leaves a region. Callback will receive the `Region` object, and a `MouseEvent` object.
* `region-click` - When the mouse clicks on a region. Callback will receive the `Region` object, and a `MouseEvent` object.
* `region-dblclick` - When the mouse double-clicks on a region. Callback will receive the `Region` object, and a `MouseEvent` object.
* `region-created` – When a region is created. Callback will receive the `Region` object.
* `region-updated` – When a region is updated. Callback will receive the `Region` object.
* `region-update-end` – When dragging or resizing is finished. Callback will receive the `Region` object.
* `region-removed` – When a region is removed. Callback will receive the `Region` object.
## Regions Plugin
Regions are visual overlays on waveform that can be used to play and
loop portions of audio. Regions can be dragged and resized.
Visual customization is possible via CSS (using the selectors
`.wavesurfer-region` and `.wavesurfer-handle`).
To enable the plugin, add the script `plugin/wavesurfer.regions.js` to
your page.
After doing that, use `wavesurfer.addRegion()` to create Region objects.
### Exposed Methods
* `addRegion(options)` – Creates a region on the waveform. Returns a `Region` object. See [Region Options](#region-options), [Region Methods](#region-methods) and [Region Events](#region-events) below.
* `clearRegions()` – Removes all regions.
* `enableDragSelection(options)` – Lets you create regions by selecting.
areas of the waveform with mouse. `options` are Region objects' params (see [below](#region-options)).
### Region Options
| option | type | default | description |
| --- | --- | --- | --- |
| `id` | string | random | The id of the region. |
| `start` | float | `0` | The start position of the region (in seconds). |
| `end` | float | `0` | The end position of the region (in seconds). |
| `loop` | boolean | `false` | Whether to loop the region when played back. |
| `drag` | boolean | `true` | Allow/dissallow dragging the region. |
| `resize` | boolean | `true` | Allow/dissallow resizing the region. |
| `color` | string | `"rgba(0, 0, 0, 0.1)"` | HTML color code. |
### Region Methods
* `remove()` - Remove the region object.
* `update(options)` - Modify the settings of the region.
* `play()` - Play the audio region from the start to end position.
### Region Events
General events:
* `in` - When playback enters the region.
* `out` - When playback leaves the region.
* `remove` - Happens just before the region is removed.
* `update` - When the region's options are updated.
Mouse events:
* `click` - When the mouse clicks on the region. Callback will receive a `MouseEvent`.
* `dblclick` - When the mouse double-clicks on the region. Callback will receive a `MouseEvent`.
* `over` - When mouse moves over the region. Callback will receive a `MouseEvent`.
* `leave` - When mouse leaves the region. Callback will receive a `MouseEvent`.
## Development
[![npm version](https://img.shields.io/npm/v/wavesurfer.js.svg?style=flat)](https://www.npmjs.com/package/wavesurfer.js)
[![npm](https://img.shields.io/npm/dm/wavesurfer.js.svg)]()
[![Build Status](https://travis-ci.org/katspaugh/wavesurfer.js.svg?branch=master)](https://travis-ci.org/katspaugh/wavesurfer.js)
[![Coverage Status](https://coveralls.io/repos/katspaugh/wavesurfer.js/badge.svg)](https://coveralls.io/r/katspaugh/wavesurfer.js)
Install `grunt-cli` using npm:
```
npm install -g grunt-cli
```
Install development dependencies:
```
npm install
```
Build a minified version of the library and plugins. This command also checks
for code-style mistakes and runs the tests:
```
grunt
```
Generated files are placed in the `dist` directory.
Running tests only:
```
grunt test
```
Creating a coverage report:
```
grunt coverage
```
The HTML report can be found in `coverage/html/index.html`.
## Credits
Initial idea by [Alex Khokhulin](https://github.com/xoxulin). Many

@@ -184,4 +265,3 @@ thanks to

License
=======
## License

@@ -188,0 +268,0 @@ ![cc-by](http://i.creativecommons.org/l/by/3.0/88x31.png)

@@ -10,5 +10,9 @@ 'use strict';

position: 'absolute',
zIndex: 1
zIndex: 1,
left: 0,
top: 0,
bottom: 0
})
);
this.waveCc = waveCanvas.getContext('2d');

@@ -19,47 +23,38 @@ this.progressWave = this.wrapper.appendChild(

zIndex: 2,
left: 0,
top: 0,
bottom: 0,
overflow: 'hidden',
width: '0',
height: this.params.height + 'px',
borderRight: [
this.params.cursorWidth + 'px',
'solid',
this.params.cursorColor
].join(' ')
display: 'none',
boxSizing: 'border-box',
borderRightStyle: 'solid',
borderRightWidth: this.params.cursorWidth + 'px',
borderRightColor: this.params.cursorColor
})
);
var progressCanvas = this.progressWave.appendChild(
document.createElement('canvas')
);
if (this.params.waveColor != this.params.progressColor) {
var progressCanvas = this.progressWave.appendChild(
document.createElement('canvas')
);
this.progressCc = progressCanvas.getContext('2d');
}
},
var selectionZIndex = 0;
updateSize: function () {
var width = Math.round(this.width / this.params.pixelRatio);
if (this.params.selectionForeground) {
selectionZIndex = 3;
}
this.waveCc.canvas.width = this.width;
this.waveCc.canvas.height = this.height;
this.style(this.waveCc.canvas, { width: width + 'px'});
var selectionCanvas = this.wrapper.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
zIndex: selectionZIndex
})
);
this.style(this.progressWave, { display: 'block'});
this.waveCc = waveCanvas.getContext('2d');
this.progressCc = progressCanvas.getContext('2d');
this.selectionCc = selectionCanvas.getContext('2d');
},
if (this.progressCc) {
this.progressCc.canvas.width = this.width;
this.progressCc.canvas.height = this.height;
this.style(this.progressCc.canvas, { width: width + 'px'});
}
updateWidth: function () {
var width = Math.round(this.width / this.pixelRatio);
[
this.waveCc,
this.progressCc,
this.selectionCc
].forEach(function (cc) {
cc.canvas.width = this.width;
cc.canvas.height = this.height;
this.style(cc.canvas, { width: width + 'px'});
}, this);
this.clearWave();

@@ -70,199 +65,137 @@ },

this.waveCc.clearRect(0, 0, this.width, this.height);
this.progressCc.clearRect(0, 0, this.width, this.height);
if (this.progressCc) {
this.progressCc.clearRect(0, 0, this.width, this.height);
}
},
drawWave: function (peaks, max) {
drawBars: function (peaks, channelIndex) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(this.drawBars, this);
return;
} else {
peaks = channels[0];
}
}
// A half-pixel offset makes lines crisp
var $ = 0.5 / this.pixelRatio;
this.waveCc.fillStyle = this.params.waveColor;
this.progressCc.fillStyle = this.params.progressColor;
var halfH = this.height / 2;
var coef = halfH / max;
var scale = this.width / peaks.length;
var $ = 0.5 / this.params.pixelRatio;
var width = this.width;
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = ~~(peaks.length / 2);
var bar = this.params.barWidth * this.params.pixelRatio;
var gap = Math.max(this.params.pixelRatio, ~~(bar / 4));
var step = bar + gap;
this.waveCc.beginPath();
this.waveCc.moveTo($, halfH);
this.progressCc.beginPath();
this.progressCc.moveTo($, halfH);
for (var i = 0; i < this.width; i++) {
var h = Math.round(peaks[~~(i * scale)] * coef);
this.waveCc.lineTo(i + $, halfH + h);
this.progressCc.lineTo(i + $, halfH + h);
var absmax = 1;
if (this.params.normalize) {
var min, max;
max = Math.max.apply(Math, peaks);
min = Math.min.apply(Math, peaks);
absmax = max;
if (-min > absmax) {
absmax = -min;
}
}
this.waveCc.lineTo(this.width + $, halfH);
this.progressCc.lineTo(this.width + $, halfH);
this.waveCc.moveTo($, halfH);
this.progressCc.moveTo($, halfH);
for (var i = 0; i < this.width; i++) {
var h = Math.round(peaks[~~(i * scale)] * coef);
this.waveCc.lineTo(i + $, halfH - h);
this.progressCc.lineTo(i + $, halfH - h);
var scale = length / width;
this.waveCc.fillStyle = this.params.waveColor;
if (this.progressCc) {
this.progressCc.fillStyle = this.params.progressColor;
}
this.waveCc.lineTo(this.width + $, halfH);
this.waveCc.fill();
this.progressCc.lineTo(this.width + $, halfH);
this.progressCc.fill();
},
[ this.waveCc, this.progressCc ].forEach(function (cc) {
if (!cc) { return; }
updateProgress: function (progress) {
var pos = Math.round(
this.width * progress
) / this.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[2 * i * scale] / absmax * halfH);
cc.fillRect(i + $, halfH - h + offsetY, bar + $, h);
}
for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[2 * i * scale + 1] / absmax * halfH);
cc.fillRect(i + $, halfH - h + offsetY, bar + $, h);
}
}, this);
},
addMark: function (mark) {
var my = this;
var markEl = document.createElement('mark');
markEl.id = mark.id;
this.wrapper.appendChild(markEl);
var handler;
if (mark.draggable) {
handler = document.createElement('handler');
handler.id = mark.id + '-handler';
handler.className = 'wavesurfer-handler';
markEl.appendChild(handler);
drawWave: function (peaks, channelIndex) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(this.drawWave, this);
return;
} else {
peaks = channels[0];
}
}
markEl.addEventListener('mouseover', function (e) {
my.fireEvent('mark-over', mark, e);
});
markEl.addEventListener('mouseleave', function (e) {
my.fireEvent('mark-leave', mark, e);
});
markEl.addEventListener('click', function (e) {
my.fireEvent('mark-click', mark, e);
});
// A half-pixel offset makes lines crisp
var $ = 0.5 / this.params.pixelRatio;
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = ~~(peaks.length / 2);
mark.draggable && (function () {
var drag = {};
var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = this.width / length;
}
var onMouseUp = function (e) {
e.stopPropagation();
drag.startPercentage = drag.endPercentage = null;
};
document.addEventListener('mouseup', onMouseUp);
my.on('destroy', function () {
document.removeEventListener('mouseup', onMouseUp);
});
handler.addEventListener('mousedown', function (e) {
e.stopPropagation();
drag.startPercentage = my.handleEvent(e);
});
my.wrapper.addEventListener('mousemove', WaveSurfer.util.throttle(function (e) {
e.stopPropagation();
if (drag.startPercentage != null) {
drag.endPercentage = my.handleEvent(e);
my.fireEvent('drag-mark', drag, mark);
}
}, 30));
}());
this.updateMark(mark);
if (mark.draggable) {
this.style(handler, {
position: 'absolute',
cursor: 'col-resize',
width: '12px',
height: '15px'
});
this.style(handler, {
left: handler.offsetWidth / 2 * -1 + 'px',
top: markEl.offsetHeight / 2 - handler.offsetHeight / 2 + 'px',
backgroundColor: mark.color
});
var absmax = 1;
if (this.params.normalize) {
var min, max;
max = Math.max.apply(Math, peaks);
min = Math.min.apply(Math, peaks);
absmax = max;
if (-min > absmax) {
absmax = -min;
}
}
},
updateMark: function (mark) {
var markEl = document.getElementById(mark.id);
markEl.title = mark.getTitle();
this.style(markEl, {
height: '100%',
position: 'absolute',
zIndex: 4,
width: mark.width + 'px',
left: Math.max(0, Math.round(
mark.percentage * this.scrollWidth - mark.width / 2
)) + 'px',
backgroundColor: mark.color
});
},
removeMark: function (mark) {
var markEl = document.getElementById(mark.id);
if (markEl) {
this.wrapper.removeChild(markEl);
this.waveCc.fillStyle = this.params.waveColor;
if (this.progressCc) {
this.progressCc.fillStyle = this.params.progressColor;
}
},
addRegion: function (region) {
var my = this;
var regionEl = document.createElement('region');
regionEl.id = region.id;
this.wrapper.appendChild(regionEl);
[ this.waveCc, this.progressCc ].forEach(function (cc) {
if (!cc) { return; }
regionEl.addEventListener('mouseover', function (e) {
my.fireEvent('region-over', region, e);
});
regionEl.addEventListener('mouseleave', function (e) {
my.fireEvent('region-leave', region, e);
});
regionEl.addEventListener('click', function (e) {
my.fireEvent('region-click', region, e);
});
cc.beginPath();
cc.moveTo($, halfH + offsetY);
this.updateRegion(region);
},
for (var i = 0; i < length; i++) {
var h = Math.round(peaks[2 * i] / absmax * halfH);
cc.lineTo(i * scale + $, halfH - h + offsetY);
}
updateRegion: function (region) {
var regionEl = document.getElementById(region.id);
var left = Math.max(0, Math.round(
region.startPercentage * this.scrollWidth));
var width = Math.max(0, Math.round(
region.endPercentage * this.scrollWidth)) - left;
// Draw the bottom edge going backwards, to make a single
// closed hull to fill.
for (var i = length - 1; i >= 0; i--) {
var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
cc.lineTo(i * scale + $, halfH - h + offsetY);
}
this.style(regionEl, {
height: '100%',
position: 'absolute',
zIndex: 4,
left: left + 'px',
top: '0px',
width: width + 'px',
backgroundColor: region.color
});
},
cc.closePath();
cc.fill();
removeRegion: function (region) {
var regionEl = document.getElementById(region.id);
if (regionEl) {
this.wrapper.removeChild(regionEl);
}
// Always draw a median line
cc.fillRect(0, halfH + offsetY - $, this.width, $);
}, this);
},
drawSelection: function () {
this.eraseSelection();
this.selectionCc.fillStyle = this.params.selectionColor;
var x = this.startPercent * this.width;
var width = this.endPercent * this.width - x;
this.selectionCc.fillRect(x, 0, width, this.height);
},
eraseSelection: function () {
this.selectionCc.clearRect(0, 0, this.width, this.height);
},
eraseSelectionMarks: function (mark0, mark1) {
this.removeMark(mark0);
this.removeMark(mark1);
updateProgress: function (progress) {
var pos = Math.round(
this.width * progress
) / this.params.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
}
});

@@ -7,8 +7,5 @@ 'use strict';

this.params = params;
this.pixelRatio = this.params.pixelRatio;
this.width = 0;
this.height = params.height * this.pixelRatio;
this.containerWidth = this.container.clientWidth;
this.interact = this.params.interact;
this.height = params.height * this.params.pixelRatio;

@@ -25,2 +22,3 @@ this.lastPos = 0;

);
this.style(this.wrapper, {

@@ -37,3 +35,3 @@ display: 'block',

width: '100%',
overflowX: this.params.scrollParent ? 'scroll' : 'hidden',
overflowX: this.params.hideScrollbar ? 'hidden' : 'auto',
overflowY: 'hidden'

@@ -47,5 +45,5 @@ });

handleEvent: function (e) {
e.preventDefault();
var bbox = this.wrapper.getBoundingClientRect();
return ((e.clientX - bbox.left + this.wrapper.scrollLeft) / this.scrollWidth) || 0;
e.preventDefault();
var bbox = this.wrapper.getBoundingClientRect();
return ((e.clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0;
},

@@ -56,41 +54,21 @@

this.wrapper.addEventListener('mousedown', function (e) {
if (my.interact) {
my.fireEvent('mousedown', my.handleEvent(e), e);
this.wrapper.addEventListener('click', function (e) {
var scrollbarHeight = my.wrapper.offsetHeight - my.wrapper.clientHeight;
if (scrollbarHeight != 0) {
// scrollbar is visible. Check if click was on it
var bbox = my.wrapper.getBoundingClientRect();
if (e.clientY >= bbox.bottom - scrollbarHeight) {
// ignore mousedown as it was on the scrollbar
return;
}
}
});
this.wrapper.addEventListener('mouseup', function (e) {
if (my.interact) {
my.fireEvent('mouseup', e);
if (my.params.interact) {
my.fireEvent('click', e, my.handleEvent(e));
}
});
this.params.dragSelection && (function () {
var drag = {};
var onMouseUp = function () {
drag.startPercentage = drag.endPercentage = null;
};
document.addEventListener('mouseup', onMouseUp);
my.on('destroy', function () {
document.removeEventListener('mouseup', onMouseUp);
});
my.wrapper.addEventListener('mousedown', function (e) {
drag.startPercentage = my.handleEvent(e);
});
my.wrapper.addEventListener('mousemove', WaveSurfer.util.throttle(function (e) {
e.stopPropagation();
if (drag.startPercentage != null) {
drag.endPercentage = my.handleEvent(e);
my.fireEvent('drag', drag);
}
}, 30));
my.wrapper.addEventListener('dblclick', function () {
my.fireEvent('drag-clear', drag);
});
}());
this.wrapper.addEventListener('scroll', function (e) {
my.fireEvent('scroll', e);
});
},

@@ -101,8 +79,6 @@

this.setWidth(length);
if (this.params.normalize) {
var max = WaveSurfer.util.max(peaks);
} else {
max = 1;
}
this.drawWave(peaks, max);
this.params.barWidth ?
this.drawBars(peaks) :
this.drawWave(peaks);
},

@@ -112,3 +88,3 @@

Object.keys(styles).forEach(function (prop) {
if (el.style[prop] != styles[prop]) {
if (el.style[prop] !== styles[prop]) {
el.style[prop] = styles[prop];

@@ -121,7 +97,9 @@ }

resetScroll: function () {
this.wrapper.scrollLeft = 0;
if (this.wrapper !== null) {
this.wrapper.scrollLeft = 0;
}
},
recenter: function (percent) {
var position = this.scrollWidth * percent;
var position = this.wrapper.scrollWidth * percent;
this.recenterOnPosition(position, true);

@@ -132,8 +110,14 @@ },

var scrollLeft = this.wrapper.scrollLeft;
var half = ~~(this.containerWidth / 2);
var half = ~~(this.wrapper.clientWidth / 2);
var target = position - half;
var offset = target - scrollLeft;
var maxScroll = this.wrapper.scrollWidth - this.wrapper.clientWidth;
if (maxScroll == 0) {
// no need to continue if scrollbar is not there
return;
}
// if the cursor is currently visible...
if (!immediate && offset >= -half && offset < half) {
if (!immediate && -half <= offset && offset < half) {
// we'll limit the "re-center" rate.

@@ -145,9 +129,13 @@ var rate = 5;

if (offset != 0) {
// limit target to valid range (0 to maxScroll)
target = Math.max(0, Math.min(maxScroll, target));
// no use attempting to scroll if we're not moving
if (target != scrollLeft) {
this.wrapper.scrollLeft = target;
}
},
getWidth: function () {
return Math.round(this.containerWidth * this.pixelRatio);
return Math.round(this.container.clientWidth * this.params.pixelRatio);
},

@@ -159,16 +147,27 @@

this.width = width;
this.scrollWidth = ~~(this.width / this.pixelRatio);
this.containerWidth = this.container.clientWidth;
if (!this.params.fillParent && !this.params.scrollParent) {
if (this.params.fillParent || this.params.scrollParent) {
this.style(this.wrapper, {
width: this.scrollWidth + 'px'
width: ''
});
} else {
this.style(this.wrapper, {
width: ~~(this.width / this.params.pixelRatio) + 'px'
});
}
this.updateWidth();
this.updateSize();
},
setHeight: function (height) {
if (height == this.height) { return; }
this.height = height;
this.style(this.wrapper, {
height: ~~(this.height / this.params.pixelRatio) + 'px'
});
this.updateSize();
},
progress: function (progress) {
var minPxDelta = 1 / this.pixelRatio;
var minPxDelta = 1 / this.params.pixelRatio;
var pos = Math.round(progress * this.width) * minPxDelta;

@@ -180,3 +179,3 @@

if (this.params.scrollParent) {
var newPos = ~~(this.scrollWidth * progress);
var newPos = ~~(this.wrapper.scrollWidth * progress);
this.recenterOnPosition(newPos);

@@ -191,25 +190,12 @@ }

this.unAll();
this.container.removeChild(this.wrapper);
this.wrapper = null;
if (this.wrapper) {
this.container.removeChild(this.wrapper);
this.wrapper = null;
}
},
updateSelection: function (startPercent, endPercent) {
this.startPercent = startPercent;
this.endPercent = endPercent;
this.drawSelection();
},
clearSelection: function (mark0, mark1) {
this.startPercent = null;
this.endPercent = null;
this.eraseSelection();
this.eraseSelectionMarks(mark0, mark1);
},
/* Renderer-specific methods */
createElements: function () {},
updateWidth: function () {},
updateSize: function () {},

@@ -220,23 +206,5 @@ drawWave: function (peaks, max) {},

updateProgress: function (position) {},
addMark: function (mark) {},
removeMark: function (mark) {},
updateMark: function (mark) {},
addRegion: function (region) {},
removeRegion: function (region) {},
updateRegion: function (region) {},
drawSelection: function () {},
eraseSelection: function () {},
eraseSelectionMarks: function (mark0, mark1) {}
updateProgress: function (position) {}
};
WaveSurfer.util.extend(WaveSurfer.Drawer, WaveSurfer.Observer);

@@ -0,1 +1,9 @@

/**
* wavesurfer.js
*
* https://github.com/katspaugh/wavesurfer.js
*
* This work is licensed under a Creative Commons Attribution 3.0 Unported License.
*/
'use strict';

@@ -9,22 +17,20 @@

cursorColor : '#333',
selectionColor: '#0fc',
selectionBorder: false,
selectionForeground: false,
selectionBorderColor: '#000',
cursorWidth : 1,
markerWidth : 2,
skipLength : 2,
minPxPerSec : 10,
samples : 3,
minPxPerSec : 20,
pixelRatio : window.devicePixelRatio,
fillParent : true,
scrollParent : false,
hideScrollbar : false,
normalize : false,
audioContext : null,
container : null,
renderer : 'Canvas',
dragSelection : true,
loopSelection : true,
audioRate : 1,
interact : true
interact : true,
splitChannels : false,
renderer : 'Canvas',
backend : 'WebAudio',
mediaType : 'audio'
},

@@ -41,12 +47,16 @@

if (!this.container) {
throw new Error('wavesurfer.js: container element not found');
throw new Error('Container element not found');
}
// Marker objects
this.markers = {};
this.once('marked', this.bindMarks.bind(this));
this.once('region-created', this.bindRegions.bind(this));
if (typeof this.params.mediaContainer == 'undefined') {
this.mediaContainer = this.container;
} else if (typeof this.params.mediaContainer == 'string') {
this.mediaContainer = document.querySelector(this.params.mediaContainer);
} else {
this.mediaContainer = this.params.mediaContainer;
}
// Region objects
this.regions = {};
if (!this.mediaContainer) {
throw new Error('Media Container element not found');
}

@@ -58,7 +68,6 @@ // Used to save the current volume when muting so we can

this.isMuted = false;
// Will hold a list of event descriptors that need to be
// cancelled on subsequent loads of audio
this.tmpEvents = [];
this.loopSelection = this.params.loopSelection;
this.minPxPerSec = this.params.minPxPerSec;
this.bindUserAction();
this.createDrawer();

@@ -68,40 +77,2 @@ this.createBackend();

bindUserAction: function () {
// iOS requires user input to start loading audio
var my = this;
var onUserAction = function () {
my.fireEvent('user-action');
};
document.addEventListener('mousedown', onUserAction);
document.addEventListener('keydown', onUserAction);
this.on('destroy', function () {
document.removeEventListener('mousedown', onUserAction);
document.removeEventListener('keydown', onUserAction);
});
},
/**
* Used with loadStream.
*/
createMedia: function (url) {
var my = this;
var media = document.createElement('audio');
media.controls = false;
media.autoplay = false;
media.src = url;
media.addEventListener('error', function () {
my.fireEvent('error', 'Error loading media element');
});
var prevMedia = this.container.querySelector('audio');
if (prevMedia) {
this.container.removeChild(prevMedia);
}
this.container.appendChild(media);
return media;
},
createDrawer: function () {

@@ -118,8 +89,4 @@ var my = this;

this.on('progress', function (progress) {
my.drawer.progress(progress);
});
// Click-to-seek
this.drawer.on('mousedown', function (progress) {
this.drawer.on('click', function (e, progress) {
setTimeout(function () {

@@ -130,22 +97,6 @@ my.seekTo(progress);

// Drag selection or marker events
if (this.params.dragSelection) {
this.drawer.on('drag', function (drag) {
my.dragging = true;
my.updateSelection(drag);
});
this.drawer.on('drag-clear', function () {
my.clearSelection();
});
}
this.drawer.on('drag-mark', function (drag, mark) {
mark.fireEvent('drag', drag);
// Relay the scroll event from the drawer
this.drawer.on('scroll', function (e) {
my.fireEvent('scroll', e);
});
// Mouseup for plugins
this.drawer.on('mouseup', function (e) {
my.fireEvent('mouseup', e);
my.dragging = false;
});
},

@@ -156,12 +107,18 @@

this.backend = Object.create(WaveSurfer.WebAudio);
if (this.backend) {
this.backend.destroy();
}
this.backend.on('play', function () {
my.fireEvent('play');
});
// Back compat
if (this.params.backend == 'AudioElement') {
this.params.backend = 'MediaElement';
}
this.on('play', function () {
my.restartAnimationLoop();
});
if (this.params.backend == 'WebAudio' && !WaveSurfer.WebAudio.supportsWebAudio()) {
this.params.backend = 'MediaElement';
}
this.backend = Object.create(WaveSurfer[this.params.backend]);
this.backend.init(this.params);
this.backend.on('finish', function () {

@@ -171,18 +128,8 @@ my.fireEvent('finish');

this.backend.init(this.params);
this.backend.on('audioprocess', function (time) {
my.drawer.progress(my.backend.getPlayedPercents());
my.fireEvent('audioprocess', time);
});
},
restartAnimationLoop: function () {
var my = this;
var requestFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame;
var frame = function () {
if (!my.backend.isPaused()) {
my.fireEvent('progress', my.backend.getPlayedPercents());
requestFrame(frame);
}
};
frame();
},
getDuration: function () {

@@ -198,2 +145,3 @@ return this.backend.getDuration();

this.backend.play(start, end);
this.fireEvent('play');
},

@@ -203,2 +151,3 @@

this.backend.pause();
this.fireEvent('pause');
},

@@ -210,12 +159,8 @@

playPauseSelection: function () {
var sel = this.getSelection();
if (sel !== null) {
this.seekTo(sel.startPercentage);
this.playPause();
}
isPlaying: function () {
return !this.backend.isPaused();
},
skipBackward: function (seconds) {
this.skip(seconds || -this.params.skipLength);
this.skip(-seconds || -this.params.skipLength);
},

@@ -228,6 +173,6 @@

skip: function (offset) {
var timings = this.timings(offset);
var progress = timings[0] / timings[1];
this.seekTo(progress);
var position = this.getCurrentTime() || 0;
var duration = this.getDuration() || 1;
position = Math.max(0, Math.min(duration, position + (offset || 0)));
this.seekAndCenter(position / duration);
},

@@ -246,10 +191,9 @@

this.params.scrollParent = false;
// avoid noise while seeking
this.savedVolume = this.backend.getVolume();
this.backend.setVolume(0);
}
this.play((progress * this.drawer.width) / this.realPxPerSec);
if (paused) {
this.pause();
this.backend.setVolume(this.savedVolume);
this.backend.seekTo(progress * this.getDuration());
this.drawer.progress(this.backend.getPlayedPercents());
if (!paused) {
this.backend.pause();
this.backend.play();
}

@@ -277,2 +221,12 @@ this.params.scrollParent = oldScrollParent;

/**
* Set the playback rate.
*
* @param {Number} rate A positive number. E.g. 0.5 means half the
* normal speed, 2 means double speed and so on.
*/
setPlaybackRate: function (rate) {
this.backend.setPlaybackRate(rate);
},
/**
* Toggle the volume on and off. It not currenly muted it will

@@ -300,191 +254,36 @@ * save the current volume value and turn the volume off.

this.params.scrollParent = !this.params.scrollParent;
this.drawBuffer();
},
mark: function (options) {
var my = this;
var opts = WaveSurfer.util.extend({
id: WaveSurfer.util.getId(),
width: this.params.markerWidth
}, options);
if (opts.percentage && !opts.position) {
opts.position = opts.percentage * this.getDuration();
}
opts.percentage = opts.position / this.getDuration();
// If exists, just update and exit early
if (opts.id in this.markers) {
return this.markers[opts.id].update(opts);
}
// Ensure position for a new marker
if (!opts.position) {
opts.position = this.getCurrentTime();
opts.percentage = opts.position / this.getDuration();
}
var mark = Object.create(WaveSurfer.Mark);
mark.init(opts);
// If we create marker while dragging we are creating selMarks
if (this.dragging) {
mark.on('drag', function(drag){
my.updateSelectionByMark(drag, mark);
});
} else {
mark.on('drag', function(drag){
my.moveMark(drag, mark);
});
}
mark.on('update', function () {
my.drawer.updateMark(mark);
my.fireEvent('mark-updated', mark);
});
mark.on('remove', function () {
my.drawer.removeMark(mark);
delete my.markers[mark.id];
my.fireEvent('mark-removed', mark);
});
this.drawer.addMark(mark);
this.drawer.on('mark-over', function (mark, e) {
mark.fireEvent('over', e);
my.fireEvent('mark-over', mark, e);
});
this.drawer.on('mark-leave', function (mark, e) {
mark.fireEvent('leave', e);
my.fireEvent('mark-leave', mark, e);
});
this.drawer.on('mark-click', function (mark, e) {
mark.fireEvent('click', e);
my.fireEvent('mark-click', mark, e);
});
this.markers[mark.id] = mark;
this.fireEvent('marked', mark);
return mark;
toggleInteraction: function () {
this.params.interact = !this.params.interact;
},
clearMarks: function () {
Object.keys(this.markers).forEach(function (id) {
this.markers[id].remove();
}, this);
this.markers = {};
},
drawBuffer: function () {
var nominalWidth = Math.round(
this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
);
var parentWidth = this.drawer.getWidth();
var width = nominalWidth;
redrawRegions: function () {
Object.keys(this.regions).forEach(function (id) {
this.region(this.regions[id]);
}, this);
},
clearRegions: function() {
Object.keys(this.regions).forEach(function (id) {
this.regions[id].remove();
}, this);
this.regions = {};
},
region: function(options) {
var my = this;
var opts = WaveSurfer.util.extend({
id: WaveSurfer.util.getId()
}, options);
opts.startPercentage = opts.startPosition / this.getDuration();
opts.endPercentage = opts.endPosition / this.getDuration();
// If exists, just update and exit early
if (opts.id in this.regions) {
return this.regions[opts.id].update(opts);
// Fill container
if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
width = parentWidth;
}
var region = Object.create(WaveSurfer.Region);
region.init(opts);
region.on('update', function () {
my.drawer.updateRegion(region);
my.fireEvent('region-updated', region);
});
region.on('remove', function () {
my.drawer.removeRegion(region);
my.fireEvent('region-removed', region);
delete my.regions[region.id];
});
this.drawer.addRegion(region);
this.drawer.on('region-over', function (region, e) {
region.fireEvent('over', e);
my.fireEvent('region-over', region, e);
});
this.drawer.on('region-leave', function (region, e) {
region.fireEvent('leave', e);
my.fireEvent('region-leave', region, e);
});
this.drawer.on('region-click', function (region, e) {
region.fireEvent('click', e);
my.fireEvent('region-click', region, e);
});
this.regions[region.id] = region;
this.fireEvent('region-created', region);
return region;
var peaks = this.backend.getPeaks(width);
this.drawer.drawPeaks(peaks, width);
this.fireEvent('redraw', peaks, width);
},
timings: function (offset) {
var position = this.getCurrentTime() || 0;
var duration = this.getDuration() || 1;
position = Math.max(0, Math.min(duration, position + (offset || 0)));
return [ position, duration ];
},
zoom: function (pxPerSec) {
this.params.minPxPerSec = pxPerSec;
drawBuffer: function () {
if (this.params.fillParent && !this.params.scrollParent) {
var length = this.drawer.getWidth();
} else {
length = Math.round(this.getDuration() * this.minPxPerSec * this.params.pixelRatio);
}
this.realPxPerSec = length / this.getDuration();
this.params.scrollParent = true;
this.drawer.drawPeaks(this.backend.getPeaks(length), length);
this.fireEvent('redraw');
},
this.drawBuffer();
drawAsItPlays: function () {
var my = this;
this.realPxPerSec = this.minPxPerSec * this.params.pixelRatio;
var frameTime = 1 / this.realPxPerSec;
var prevTime = 0;
var peaks;
this.drawFrame = function (time) {
if (time > prevTime && time - prevTime < frameTime) {
return;
}
prevTime = time;
var duration = my.getDuration();
if (duration < Infinity) {
var length = Math.round(duration * my.realPxPerSec);
peaks = peaks || new Uint8Array(length);
} else {
peaks = peaks || [];
length = peaks.length;
}
var index = ~~(my.backend.getPlayedPercents() * length);
if (!peaks[index]) {
peaks[index] = WaveSurfer.util.max(my.backend.waveform(), 128);
my.drawer.setWidth(length);
my.drawer.clearWave();
my.drawer.drawWave(peaks, 128);
}
};
this.backend.on('audioprocess', this.drawFrame);
this.seekAndCenter(
this.getCurrentTime() / this.getDuration()
);
},

@@ -496,10 +295,5 @@

loadArrayBuffer: function (arraybuffer) {
var my = this;
this.backend.decodeArrayBuffer(arraybuffer, function (data) {
my.backend.loadBuffer(data);
my.drawBuffer();
my.fireEvent('ready');
}, function () {
my.fireEvent('error', 'Error decoding audiobuffer');
});
this.decodeArrayBuffer(arraybuffer, function (data) {
this.loadDecodedBuffer(data);
}.bind(this));
},

@@ -511,4 +305,3 @@

loadDecodedBuffer: function (buffer) {
this.empty();
this.backend.loadBuffer(buffer);
this.backend.load(buffer);
this.drawBuffer();

@@ -531,3 +324,2 @@ this.fireEvent('ready');

reader.addEventListener('load', function (e) {
my.empty();
my.loadArrayBuffer(e.target.result);

@@ -539,33 +331,64 @@ });

reader.readAsArrayBuffer(blob);
this.empty();
},
/**
* Loads audio and prerenders its waveform.
* Loads audio and rerenders the waveform.
*/
load: function (url) {
this.empty();
// load via XHR and render all at once
return this.downloadArrayBuffer(url, this.loadArrayBuffer.bind(this));
load: function (url, peaks) {
switch (this.params.backend) {
case 'WebAudio': return this.loadBuffer(url);
case 'MediaElement': return this.loadMediaElement(url, peaks);
}
},
/**
* Load audio stream and render its waveform as it plays.
* Loads audio using Web Audio buffer backend.
*/
loadStream: function (url) {
var my = this;
loadBuffer: function (url) {
this.empty();
// load via XHR and render all at once
return this.getArrayBuffer(url, this.loadArrayBuffer.bind(this));
},
loadMediaElement: function (url, peaks) {
this.empty();
this.drawAsItPlays();
this.media = this.createMedia(url);
this.backend.load(url, this.mediaContainer, peaks);
// iOS requires a touch to start loading audio
this.once('user-action', function () {
// Assume media.readyState >= media.HAVE_ENOUGH_DATA
my.backend.loadMedia(my.media);
});
this.tmpEvents.push(
this.backend.once('canplay', (function () {
this.drawBuffer();
this.fireEvent('ready');
}).bind(this)),
setTimeout(this.fireEvent.bind(this, 'ready'), 0);
this.backend.once('error', (function (err) {
this.fireEvent('error', err);
}).bind(this))
);
// If no pre-decoded peaks provided, attempt to download the
// audio file and decode it with Web Audio.
if (!peaks && this.backend.supportsWebAudio()) {
this.getArrayBuffer(url, (function (arraybuffer) {
this.decodeArrayBuffer(arraybuffer, (function (buffer) {
this.backend.buffer = buffer;
this.drawBuffer();
}).bind(this));
}).bind(this));
}
},
downloadArrayBuffer: function (url, callback) {
decodeArrayBuffer: function (arraybuffer, callback) {
this.backend.decodeArrayBuffer(
arraybuffer,
this.fireEvent.bind(this, 'decoded'),
this.fireEvent.bind(this, 'error', 'Error decoding audiobuffer')
);
this.tmpEvents.push(
this.once('decoded', callback)
);
},
getArrayBuffer: function (url, callback) {
var my = this;

@@ -576,9 +399,11 @@ var ajax = WaveSurfer.util.ajax({

});
ajax.on('progress', function (e) {
my.onProgress(e);
});
ajax.on('success', callback);
ajax.on('error', function (e) {
my.fireEvent('error', 'XHR error: ' + e.target.statusText);
});
this.tmpEvents.push(
ajax.on('progress', function (e) {
my.onProgress(e);
}),
ajax.on('success', callback),
ajax.on('error', function (e) {
my.fireEvent('error', 'XHR error: ' + e.target.statusText);
})
);
return ajax;

@@ -598,53 +423,23 @@ },

bindMarks: function () {
var my = this;
var prevTime = 0;
this.backend.on('play', function () {
// Reset marker events
Object.keys(my.markers).forEach(function (id) {
my.markers[id].played = false;
});
/**
* Exports PCM data into a JSON array and opens in a new window.
*/
exportPCM: function (length, accuracy, noWindow) {
length = length || 1024;
accuracy = accuracy || 10000;
noWindow = noWindow || false;
var peaks = this.backend.getPeaks(length, accuracy);
var arr = [].map.call(peaks, function (val) {
return Math.round(val * accuracy) / accuracy;
});
this.backend.on('audioprocess', function (time) {
Object.keys(my.markers).forEach(function (id) {
var marker = my.markers[id];
if (!marker.played) {
if (marker.position <= time && marker.position >= prevTime) {
// Prevent firing the event more than once per playback
marker.played = true;
my.fireEvent('mark', marker);
marker.fireEvent('reached');
}
}
});
prevTime = time;
});
var json = JSON.stringify(arr);
if (!noWindow) {
window.open('data:application/json;charset=utf-8,' +
encodeURIComponent(json));
}
return json;
},
bindRegions: function() {
var my = this;
this.backend.on('play', function () {
Object.keys(my.regions).forEach(function (id) {
my.regions[id].fired_in = false;
my.regions[id].fired_out = false;
});
});
this.backend.on('audioprocess', function (time) {
Object.keys(my.regions).forEach(function (id) {
var region = my.regions[id];
if (!region.fired_in && region.startPosition <= time && region.endPosition >= time) {
my.fireEvent('region-in', region);
region.fireEvent('in');
region.fired_in = true;
}
if (!region.fired_out && region.endPosition < time) {
my.fireEvent('region-out', region);
region.fireEvent('out');
region.fired_out = true;
}
});
});
clearTmpEvents: function () {
this.tmpEvents.forEach(function (e) { e.un(); });
},

@@ -656,13 +451,8 @@

empty: function () {
if (this.drawFrame) {
this.un('progress', this.drawFrame);
this.drawFrame = null;
}
if (this.backend && !this.backend.isPaused()) {
if (!this.backend.isPaused()) {
this.stop();
this.backend.disconnectSource();
}
this.clearMarks();
this.clearRegions();
this.clearTmpEvents();
this.drawer.progress(0);
this.drawer.setWidth(0);

@@ -677,369 +467,13 @@ this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);

this.fireEvent('destroy');
this.clearMarks();
this.clearRegions();
this.clearTmpEvents();
this.unAll();
this.backend.destroy();
this.drawer.destroy();
if (this.media) {
this.container.removeChild(this.media);
}
},
updateSelectionByMark: function (markDrag, mark) {
var selection;
if (mark.id == this.selMark0.id) {
selection = {
'startPercentage': markDrag.endPercentage,
'endPercentage': this.selMark1.percentage
};
} else {
selection = {
'startPercentage': this.selMark0.percentage,
'endPercentage': markDrag.endPercentage
};
}
this.updateSelection(selection);
},
updateSelection: function (selection) {
var my = this;
var percent0 = selection.startPercentage;
var percent1 = selection.endPercentage;
var color = this.params.selectionColor;
var width = 0;
if (this.params.selectionBorder) {
color = this.params.selectionBorderColor;
width = 2; // parametrize?
}
if (percent0 > percent1) {
var tmpPercent = percent0;
percent0 = percent1;
percent1 = tmpPercent;
}
if (this.selMark0) {
this.selMark0.update({
percentage: percent0,
position: percent0 * this.getDuration()
});
} else {
this.selMark0 = this.mark({
width: width,
percentage: percent0,
position: percent0 * this.getDuration(),
color: color,
draggable: my.params.selectionBorder
});
}
if (this.selMark1) {
this.selMark1.update({
percentage: percent1,
position: percent1 * this.getDuration()
});
} else {
this.selMark1 = this.mark({
width: width,
percentage: percent1,
position: percent1 * this.getDuration(),
color: color,
draggable: my.params.selectionBorder
});
}
this.drawer.updateSelection(percent0, percent1);
if (this.loopSelection) {
this.backend.updateSelection(percent0, percent1);
}
my.fireEvent('selection-update', this.getSelection());
},
moveMark: function (drag, mark) {
mark.update({
percentage: drag.endPercentage,
position: drag.endPercentage * this.getDuration()
});
this.markers[mark.id] = mark;
},
clearSelection: function () {
this.drawer.clearSelection(this.selMark0, this.selMark1);
if (this.selMark0) {
this.selMark0.remove();
this.selMark0 = null;
}
if (this.selMark1) {
this.selMark1.remove();
this.selMark1 = null;
}
if (this.loopSelection) {
this.backend.clearSelection();
}
this.fireEvent('selection-update', this.getSelection());
},
toggleLoopSelection: function () {
this.loopSelection = !this.loopSelection;
if (this.selMark0) this.selectionPercent0 = this.selMark0.percentage;
if (this.selMark1) this.selectionPercent1 = this.selMark1.percentage;
this.updateSelection();
this.selectionPercent0 = null;
this.selectionPercent1 = null;
},
getSelection: function () {
if (!this.selMark0 || !this.selMark1) return null;
return {
startPercentage: this.selMark0.percentage,
startPosition: this.selMark0.position,
endPercentage: this.selMark1.percentage,
endPosition: this.selMark1.position,
startTime: this.selMark0.getTitle(),
endTime: this.selMark1.getTitle()
};
},
enableInteraction: function () {
this.drawer.interact = true;
},
disableInteraction: function () {
this.drawer.interact = false;
},
toggleInteraction: function () {
this.drawer.interact = !this.drawer.interact;
}
};
/* Mark */
WaveSurfer.Mark = {
defaultParams: {
id: null,
position: 0,
percentage: 0,
width: 1,
color: '#333',
draggable: false
},
init: function (options) {
this.apply(
WaveSurfer.util.extend({}, this.defaultParams, options)
);
return this;
},
getTitle: function () {
return [
~~(this.position / 60), // minutes
('00' + ~~(this.position % 60)).slice(-2) // seconds
].join(':');
},
apply: function (options) {
Object.keys(options).forEach(function (key) {
if (key in this.defaultParams) {
this[key] = options[key];
}
}, this);
},
update: function (options) {
this.apply(options);
this.fireEvent('update');
},
remove: function () {
this.fireEvent('remove');
this.unAll();
}
WaveSurfer.create = function (params) {
var wavesurfer = Object.create(WaveSurfer);
wavesurfer.init(params);
return wavesurfer;
};
/* Region */
WaveSurfer.Region = {
defaultParams: {
id: null,
startPosition: 0,
endPosition: 0,
startPercentage: 0,
endPercentage: 0,
color: 'rgba(0, 0, 255, 0.2)'
},
init: function (options) {
this.apply(
WaveSurfer.util.extend({}, this.defaultParams, options)
);
return this;
},
apply: function (options) {
Object.keys(options).forEach(function (key) {
if (key in this.defaultParams) {
this[key] = options[key];
}
}, this);
},
update: function (options) {
this.apply(options);
this.fireEvent('update');
},
remove: function () {
this.fireEvent('remove');
this.unAll();
}
};
/* Observer */
WaveSurfer.Observer = {
on: function (event, fn) {
if (!this.handlers) { this.handlers = {}; }
var handlers = this.handlers[event];
if (!handlers) {
handlers = this.handlers[event] = [];
}
handlers.push(fn);
},
un: function (event, fn) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
if (handlers) {
if (fn) {
for (var i = handlers.length - 1; i >= 0; i--) {
if (handlers[i] == fn) {
handlers.splice(i, 1);
}
}
} else {
handlers.length = 0;
}
}
},
unAll: function () {
this.handlers = null;
},
once: function (event, handler) {
var my = this;
var fn = function () {
handler();
setTimeout(function () {
my.un(event, fn);
}, 0);
};
this.on(event, fn);
},
fireEvent: function (event) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
var args = Array.prototype.slice.call(arguments, 1);
handlers && handlers.forEach(function (fn) {
fn.apply(null, args);
});
}
};
/* Common utilities */
WaveSurfer.util = {
extend: function (dest) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function (source) {
Object.keys(source).forEach(function (key) {
dest[key] = source[key];
});
});
return dest;
},
getId: function () {
return 'wavesurfer_' + Math.random().toString(32).substring(2);
},
max: function (values, min) {
var max = -Infinity;
for (var i = 0, len = values.length; i < len; i++) {
var val = values[i];
if (min != null) {
val = Math.abs(val - min);
}
if (val > max) { max = val; }
}
return max;
},
ajax: function (options) {
var ajax = Object.create(WaveSurfer.Observer);
var xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', options.url, true);
xhr.responseType = options.responseType;
xhr.addEventListener('progress', function (e) {
ajax.fireEvent('progress', e);
});
xhr.addEventListener('load', function (e) {
ajax.fireEvent('load', e);
if (200 == xhr.status || 206 == xhr.status) {
ajax.fireEvent('success', xhr.response, e);
} else {
ajax.fireEvent('error', e);
}
});
xhr.addEventListener('error', function (e) {
ajax.fireEvent('error', e);
});
xhr.send();
ajax.xhr = xhr;
return ajax;
},
/**
* @see http://underscorejs.org/#throttle
*/
throttle: function (func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
options || (options = {});
var later = function () {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
context = args = null;
};
return function () {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
};
WaveSurfer.util.extend(WaveSurfer, WaveSurfer.Observer);
WaveSurfer.util.extend(WaveSurfer.Mark, WaveSurfer.Observer);
WaveSurfer.util.extend(WaveSurfer.Region, WaveSurfer.Observer);

@@ -5,11 +5,11 @@ 'use strict';

scriptBufferSize: 256,
fftSize: 128,
PLAYING_STATE: 0,
PAUSED_STATE: 1,
FINISHED_STATE: 2,
supportsWebAudio: function () {
return !!(window.AudioContext || window.webkitAudioContext);
},
getAudioContext: function () {
if (!(window.AudioContext || window.webkitAudioContext)) {
throw new Error(
'wavesurfer.js: your browser doesn\'t support WebAudio'
);
}
if (!WaveSurfer.WebAudio.audioContext) {

@@ -23,2 +23,11 @@ WaveSurfer.WebAudio.audioContext = new (

getOfflineAudioContext: function (sampleRate) {
if (!WaveSurfer.WebAudio.offlineAudioContext) {
WaveSurfer.WebAudio.offlineAudioContext = new (
window.OfflineAudioContext || window.webkitOfflineAudioContext
)(1, 2, sampleRate);
}
return WaveSurfer.WebAudio.offlineAudioContext;
},
init: function (params) {

@@ -28,60 +37,87 @@ this.params = params;

this.loop = false;
this.prevFrameTime = 0;
this.lastPlay = this.ac.currentTime;
this.startPosition = 0;
this.scheduledPause = null;
this.states = [
Object.create(WaveSurfer.WebAudio.state.playing),
Object.create(WaveSurfer.WebAudio.state.paused),
Object.create(WaveSurfer.WebAudio.state.finished)
];
this.createVolumeNode();
this.createScriptNode();
this.createAnalyserNode();
this.setState(this.PAUSED_STATE);
this.setPlaybackRate(this.params.audioRate);
},
loadBuffer: function (buffer) {
WaveSurfer.util.extend(this, WaveSurfer.WebAudio.Buffer);
this.postInit();
this.load(buffer);
disconnectFilters: function () {
if (this.filters) {
this.filters.forEach(function (filter) {
filter && filter.disconnect();
});
this.filters = null;
// Reconnect direct path
this.analyser.connect(this.gainNode);
}
},
loadMedia: function (media) {
WaveSurfer.util.extend(this, WaveSurfer.WebAudio.Media);
this.postInit();
this.load(media);
setState: function (state) {
if (this.state !== this.states[state]) {
this.state = this.states[state];
this.state.init.call(this);
}
},
disconnectFilters: function () {
if (this.inputFilter) {
this.inputFilter.disconnect();
}
if (this.outputFilter) {
this.outputFilter.disconnect();
}
// Unpacked filters
setFilter: function () {
this.setFilters([].slice.call(arguments));
},
setFilter: function (inputFilter, outputFilter) {
/**
* @param {Array} filters Packed ilters array
*/
setFilters: function (filters) {
// Remove existing filters
this.disconnectFilters();
this.inputFilter = inputFilter;
this.outputFilter = outputFilter || inputFilter;
// Insert filters if filter array not empty
if (filters && filters.length) {
this.filters = filters;
if (this.inputFilter && this.outputFilter) {
this.analyser.connect(this.inputFilter);
this.outputFilter.connect(this.gainNode);
} else {
this.analyser.connect(this.gainNode);
// Disconnect direct path before inserting filters
this.analyser.disconnect();
// Connect each filter in turn
filters.reduce(function (prev, curr) {
prev.connect(curr);
return curr;
}, this.analyser).connect(this.gainNode);
}
},
createScriptNode: function () {
var my = this;
var bufferSize = this.scriptBufferSize;
if (this.ac.createScriptProcessor) {
this.scriptNode = this.ac.createScriptProcessor(bufferSize);
this.scriptNode = this.ac.createScriptProcessor(this.scriptBufferSize);
} else {
this.scriptNode = this.ac.createJavaScriptNode(bufferSize);
this.scriptNode = this.ac.createJavaScriptNode(this.scriptBufferSize);
}
this.scriptNode.connect(this.ac.destination);
},
addOnAudioProcess: function () {
var my = this;
this.scriptNode.onaudioprocess = function () {
if (!my.isPaused()) {
var time = my.getCurrentTime();
my.onPlayFrame(time);
var time = my.getCurrentTime();
if (time >= my.getDuration()) {
my.setState(my.FINISHED_STATE);
} else if (time >= my.scheduledPause) {
my.setState(my.PAUSED_STATE);
} else if (my.state === my.states[my.PLAYING_STATE]) {
my.fireEvent('audioprocess', time);

@@ -92,20 +128,4 @@ }

onPlayFrame: function (time) {
if (this.scheduledPause != null) {
if (this.prevFrameTime >= this.scheduledPause) {
this.pause();
}
}
if (this.loop) {
if (
this.prevFrameTime > this.loopStart &&
this.prevFrameTime <= this.loopEnd &&
time > this.loopEnd
) {
this.play(this.loopStart);
}
}
this.prevFrameTime = time;
removeOnAudioProcess: function () {
this.scriptNode.onaudioprocess = null;
},

@@ -115,4 +135,2 @@

this.analyser = this.ac.createAnalyser();
this.analyser.fftSize = this.fftSize;
this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.connect(this.gainNode);

@@ -156,46 +174,64 @@ },

decodeArrayBuffer: function (arraybuffer, callback, errback) {
var my = this;
this.ac.decodeAudioData(arraybuffer, function (data) {
my.buffer = data;
if (!this.offlineAc) {
this.offlineAc = this.getOfflineAudioContext(this.ac ? this.ac.sampleRate : 44100);
}
this.offlineAc.decodeAudioData(arraybuffer, (function (data) {
callback(data);
}, errback);
}).bind(this), errback);
},
/**
* @returns {Float32Array} Array of peaks.
* Compute the max and min value of the waveform when broken into
* <length> subranges.
* @param {Number} How many subranges to break the waveform into.
* @returns {Array} Array of 2*<length> peaks or array of arrays
* of peaks consisting of (max, min) values for each subrange.
*/
getPeaks: function (length) {
var buffer = this.buffer;
var sampleSize = buffer.length / length;
var sampleSize = this.buffer.length / length;
var sampleStep = ~~(sampleSize / 10) || 1;
var channels = buffer.numberOfChannels;
var peaks = new Float32Array(length);
var channels = this.buffer.numberOfChannels;
var splitPeaks = [];
var mergedPeaks = [];
for (var c = 0; c < channels; c++) {
var chan = buffer.getChannelData(c);
var peaks = splitPeaks[c] = [];
var chan = this.buffer.getChannelData(c);
for (var i = 0; i < length; i++) {
var start = ~~(i * sampleSize);
var end = ~~(start + sampleSize);
var max = 0;
var min = chan[0];
var max = chan[0];
for (var j = start; j < end; j += sampleStep) {
var value = chan[j];
if (value > max) {
max = value;
// faster than Math.abs
} else if (-value > max) {
max = -value;
}
if (value < min) {
min = value;
}
}
if (c == 0 || max > peaks[i]) {
peaks[i] = max;
peaks[2 * i] = max;
peaks[2 * i + 1] = min;
if (c == 0 || max > mergedPeaks[2 * i]) {
mergedPeaks[2 * i] = max;
}
if (c == 0 || min < mergedPeaks[2 * i + 1]) {
mergedPeaks[2 * i + 1] = min;
}
}
}
return peaks;
return this.params.splitChannels ? splitPeaks : mergedPeaks;
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
return this.state.getPlayedPercents.call(this);
},

@@ -210,3 +246,5 @@

destroy: function () {
this.pause();
if (!this.isPaused()) {
this.pause();
}
this.unAll();

@@ -221,58 +259,58 @@ this.buffer = null;

updateSelection: function (startPercent, endPercent) {
var duration = this.getDuration();
this.loop = true;
this.loopStart = duration * startPercent;
this.loopEnd = duration * endPercent;
load: function (buffer) {
this.startPosition = 0;
this.lastPlay = this.ac.currentTime;
this.buffer = buffer;
this.createSource();
},
clearSelection: function () {
this.loop = false;
this.loopStart = 0;
this.loopEnd = 0;
createSource: function () {
this.disconnectSource();
this.source = this.ac.createBufferSource();
//adjust for old browsers.
this.source.start = this.source.start || this.source.noteGrainOn;
this.source.stop = this.source.stop || this.source.noteOff;
this.source.playbackRate.value = this.playbackRate;
this.source.buffer = this.buffer;
this.source.connect(this.analyser);
},
/**
* Returns the real-time waveform data.
*
* @return {Uint8Array} The frequency data.
* Values range from 0 to 255.
*/
waveform: function () {
this.analyser.getByteTimeDomainData(this.analyserData);
return this.analyserData;
isPaused: function () {
return this.state !== this.states[this.PLAYING_STATE];
},
getDuration: function () {
if (!this.buffer) {
return 0;
}
return this.buffer.duration;
},
/* Dummy methods */
seekTo: function (start, end) {
this.scheduledPause = null;
postInit: function () {},
load: function () {},
if (start == null) {
start = this.getCurrentTime();
if (start >= this.getDuration()) {
start = 0;
}
}
if (end == null) {
end = this.getDuration();
}
/**
* Get current position in seconds.
*/
getCurrentTime: function () {
return 0;
},
this.startPosition = start;
this.lastPlay = this.ac.currentTime;
/**
* @returns {Boolean}
*/
isPaused: function () {
return true;
},
if (this.state === this.states[this.FINISHED_STATE]) {
this.setState(this.PAUSED_STATE);
}
/**
* Get duration in seconds.
*/
getDuration: function () {
return 0;
return { start: start, end: end };
},
/**
* Set the audio source playback rate.
*/
setPlaybackRate: function (value) {
this.playbackRate = value || 1;
getPlayedTime: function () {
return (this.ac.currentTime - this.lastPlay) * this.playbackRate;
},

@@ -288,10 +326,93 @@

*/
play: function (start, end) {},
play: function (start, end) {
// need to re-create source on each playback
this.createSource();
var adjustedTime = this.seekTo(start, end);
start = adjustedTime.start;
end = adjustedTime.end;
this.scheduledPause = end;
this.source.start(0, start, end - start);
this.setState(this.PLAYING_STATE);
},
/**
* Pauses the loaded audio.
*/
pause: function () {}
pause: function () {
this.scheduledPause = null;
this.startPosition += this.getPlayedTime();
this.source && this.source.stop(0);
this.setState(this.PAUSED_STATE);
},
/**
* Returns the current time in seconds relative to the audioclip's duration.
*/
getCurrentTime: function () {
return this.state.getCurrentTime.call(this);
},
/**
* Set the audio source playback rate.
*/
setPlaybackRate: function (value) {
value = value || 1;
if (this.isPaused()) {
this.playbackRate = value;
} else {
this.pause();
this.playbackRate = value;
this.play();
}
}
};
WaveSurfer.WebAudio.state = {};
WaveSurfer.WebAudio.state.playing = {
init: function () {
this.addOnAudioProcess();
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition + this.getPlayedTime();
}
};
WaveSurfer.WebAudio.state.paused = {
init: function () {
this.removeOnAudioProcess();
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition;
}
};
WaveSurfer.WebAudio.state.finished = {
init: function () {
this.removeOnAudioProcess();
this.fireEvent('finish');
},
getPlayedPercents: function () {
return 1;
},
getCurrentTime: function () {
return this.getDuration();
}
};
WaveSurfer.util.extend(WaveSurfer.WebAudio, WaveSurfer.Observer);

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc