wavesurfer.js
Advanced tools
Comparing version 0.1.8 to 1.0.4
@@ -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 @@ }; |
338
README.md
@@ -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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
5644656
105
0
266
8
7418
1