diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..4634e3bac --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,26 @@ +# Hey, thank you for testing and contributing to wavesurfer.js! + +## Please make sure you can check all of these points below before opening an issue: + +(You don't have to post this section) + +- [ ] I have checked [the FAQ](https://wavesurfer-js.org/faq/) and it doesn't solve my problem. +- [ ] I have checked [the documentation](https://wavesurfer-js.org/docs/) and it doesn't solve my problem +- [ ] I have searched for [already open issues](https://github.com/katspaugh/wavesurfer.js/issues) which desribe my problem. +- [ ] The issue I'm having is related to and caused by wavesurfer.js, not by other software (which maybe packages and uses wavesurfer incorrectly) – In that case you should open the issue on the respective project pages. + +## Please make sure you provide the following information (if applicable): + +### Wavesurfer.js version(s): + + +### Browser version(s): + + +### Code needed to reproduce the issue: + +(Please reduce your code as much as possible and only post the minimum code needed to reproduce the issue. [A Code pen](http://codepen.io/) is an excellent way to share such code) + + +### Use behaviour needed to reproduce the issue: + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f9002b067 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +# Hey, thank you for contributing to wavesurfer.js! + +To review/merge open PRs it is very helpful to know as much as possible about the changes which are being introduced. Reviewing PRs is very time consuming, please be patient, it can take some time to do properly. + +**Title:** Please make sure the name of your PR is as descriptive as possible (Describe the feature that is introduced or the bug that is being fixed). + +## Please make sure you provide the information below: + +### Short description of changes: + + +### Breaking in the external API: + + +### Breaking changes in the internal API: + + +### Todos/Notes: + + +### Related Issues and other PRs: diff --git a/.gitignore b/.gitignore index 561f4da4f..8d493c83d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist/plugin/*.js /_SpecRunner.html .DS_Store .idea +.project diff --git a/.npmignore b/.npmignore index 8e8929995..0f62610c4 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ -/dist/wavesurfer.min.js.map -/dist/wavesurfer.amd.js -/dist/wavesurfer.min.js +# exclude everything except dist and src +**/* +!dist/** +!src/** diff --git a/src/drawer.multicanvas.js b/src/drawer.multicanvas.js index 562b6812f..54d50e15a 100644 --- a/src/drawer.multicanvas.js +++ b/src/drawer.multicanvas.js @@ -473,6 +473,18 @@ export default class MultiCanvas extends Drawer { } } + /** + * Return image data of the waveform + * + * @param {string} type='image/png' An optional value of a format type. + * @param {number} quality=0.92 An optional value between 0 and 1. + * @return {string|string[]} images A data URL or an array of data URLs + */ + getImage(type, quality) { + const images = this.canvases.map(entry => entry.wave.toDataURL(type, quality)); + return images.length > 1 ? images : images[0]; + } + /** * Render the new progress * diff --git a/src/mediaelement.js b/src/mediaelement.js index 8d2700ba4..c5af189b9 100755 --- a/src/mediaelement.js +++ b/src/mediaelement.js @@ -154,7 +154,7 @@ export default class MediaElement extends WebAudio { * @return {number} */ getDuration() { - let duration = this.media.duration; + var duration = (this.buffer || this.media).duration; if (duration >= Infinity) { // streaming audio duration = this.media.seekable.end(0); } diff --git a/src/plugin/minimap.js b/src/plugin/minimap.js index 8afa64a75..98a2c0d48 100644 --- a/src/plugin/minimap.js +++ b/src/plugin/minimap.js @@ -85,13 +85,13 @@ export default function(params = {}) { } }; let prevWidth = 0; - this._onResize = () => { + this._onResize = wavesurfer.util.debounce(() => { if (prevWidth != this.wrapper.clientWidth) { prevWidth = this.wrapper.clientWidth; this.render(); this.progress(this.wavesurfer.backend.getPlayedPercents()); } - }; + }); } init() { @@ -103,6 +103,7 @@ export default function(params = {}) { destroy() { window.removeEventListener('resize', this._onResize, true); + window.removeEventListener('orientationchange', this._onResize, true); this.wavesurfer.drawer.wrapper.removeEventListener('mouseover', this._onMouseover); this.wavesurfer.un('ready', this._onReady); this.wavesurfer.un('seek', this._onSeek); @@ -177,6 +178,7 @@ export default function(params = {}) { bindWaveSurferEvents() { window.addEventListener('resize', this._onResize, true); + window.addEventListener('orientationchange', this._onResize, true); this.wavesurfer.on('audioprocess', this._onAudioprocess); this.wavesurfer.on('seek', this._onSeek); if (this.params.showOverview) { diff --git a/src/plugin/spectrogram.js b/src/plugin/spectrogram.js index 1eaedd3de..64aa10005 100644 --- a/src/plugin/spectrogram.js +++ b/src/plugin/spectrogram.js @@ -271,12 +271,22 @@ export default function spectrogram(params = {}) { if (prevSpectrogram) { this.container.removeChild(prevSpectrogram); } - const wsParams = this.wavesurfer.params; + this.wrapper = document.createElement('spectrogram'); + // if labels are active + if (this.params.labels) { + const labelsEl = this.labelsEl = document.createElement('canvas'); + labelsEl.classList.add('spec-labels'); + this.drawer.style(labelsEl, { + left: 0, + position: 'absolute', + zIndex: 9 + }); + this.wrapper.appendChild(labelsEl); + // can be customized in next version + this.loadLabels('rgba(68,68,68,0.5)', '12px', '10px', '', '#fff', '#f7f7f7', 'center', '#specLabels'); + } - this.wrapper = this.container.appendChild( - document.createElement('spectrogram') - ); this.drawer.style(this.wrapper, { display: 'block', position: 'relative', @@ -292,6 +302,7 @@ export default function spectrogram(params = {}) { overflowY: 'hidden' }); } + this.container.appendChild(this.wrapper); this.wrapper.addEventListener('click', e => { e.preventDefault(); @@ -394,6 +405,69 @@ export default function spectrogram(params = {}) { return ajax; } + freqType(freq) { + return (freq >= 1000 ? (freq / 1000).toFixed(1) : Math.round(freq)); + } + + unitType(freq) { + return (freq >= 1000 ? 'KHz' : 'Hz'); + } + + loadLabels(bgFill, fontSizeFreq, fontSizeUnit, fontType, textColorFreq, textColorUnit, textAlign, container) { + const frequenciesHeight = this.height; + bgFill = bgFill || 'rgba(68,68,68,0)'; + fontSizeFreq = fontSizeFreq || '12px'; + fontSizeUnit = fontSizeUnit || '10px'; + fontType = fontType || 'Helvetica'; + textColorFreq = textColorFreq || '#fff'; + textColorUnit = textColorUnit || '#fff'; + textAlign = textAlign || 'center'; + container = container || '#specLabels'; + const getMaxY = frequenciesHeight || 512; + const labelIndex = 5 * (getMaxY / 256); + const freqStart = 0; + const step = ((this.wavesurfer.backend.ac.sampleRate / 2) - freqStart) / labelIndex; + + const ctx = this.labelsEl.getContext('2d'); + this.labelsEl.height = this.height / this.pixelRatio; + this.labelsEl.width = 55; + + ctx.fillStyle = bgFill; + ctx.fillRect(0, 0, 55, getMaxY); + ctx.fill(); + let i; + + for (i = 0; i <= labelIndex; i++) { + ctx.textAlign = textAlign; + ctx.textBaseline = 'middle'; + + const freq = freqStart + (step * i); + const index = Math.round(freq / (this.sampleRate / 2) * this.fftSamples); + const percent = index / this.fftSamples / 2; + const y = (1 - percent) * this.height; + const label = this.freqType(freq); + const units = this.unitType(freq); + const x = 16; + const yLabelOffset = 2; + + if (i == 0) { + ctx.fillStyle = textColorUnit; + ctx.font = fontSizeUnit + ' ' + fontType; + ctx.fillText(units, x + 24, getMaxY + i - 10); + ctx.fillStyle = textColorFreq; + ctx.font = fontSizeFreq + ' ' + fontType; + ctx.fillText(label, x, getMaxY + i - 10); + } else { + ctx.fillStyle = textColorUnit; + ctx.font = fontSizeUnit + ' ' + fontType; + ctx.fillText(units, x + 24, getMaxY - i * 50 + yLabelOffset); + ctx.fillStyle = textColorFreq; + ctx.font = fontSizeFreq + ' ' + fontType; + ctx.fillText(label, x, getMaxY - i * 50 + yLabelOffset); + } + } + } + updateScroll(e) { this.wrapper.scrollLeft = e.target.scrollLeft; } diff --git a/src/wavesurfer.js b/src/wavesurfer.js index 557b92a5b..dbbd296cf 100755 --- a/src/wavesurfer.js +++ b/src/wavesurfer.js @@ -23,6 +23,8 @@ import PeakCache from './peakcache'; * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'` In most cases * you don't have to set this manually. MediaElement is a fallback for * unsupported browsers. + * @property {boolean} closeAudioContext=false Close and nullify all audio + * contexts when the destroy method is called. * @property {!string|HTMLElement} container CSS selector or HTML element where * the waveform should be drawn. This is the only required parameter. * @property {string} cursorColor='#333' The fill color of the cursor indicating @@ -30,6 +32,8 @@ import PeakCache from './peakcache'; * @property {number} cursorWidth=1 Measured in pixels. * @property {boolean} fillParent=true Whether to fill the entire container or * draw only according to `minPxPerSec`. + * @property {boolean} forceDecode=false Force decoding of audio using web audio + * when zooming to get a more detailed waveform. * @property {number} height=128 The height of the waveform. Measured in * pixels. * @property {boolean} hideScrollbar=false Whether to hide the horizontal @@ -131,6 +135,7 @@ export default class WaveSurfer extends util.Observer { cursorWidth : 1, dragSelection : true, fillParent : true, + forceDecode : true, height : 128, hideScrollbar : false, interact : true, @@ -585,7 +590,7 @@ export default class WaveSurfer extends util.Observer { * @example wavesurfer.pause(); */ pause() { - this.backend.pause(); + this.backend.isPaused() || this.backend.pause(); } /** @@ -710,6 +715,16 @@ export default class WaveSurfer extends util.Observer { this.backend.setVolume(newVolume); } + /** + * Get the playback volume. + * + * @return {number} A value between 0 and 1, 0 being no + * volume and 1 being full volume. + */ + getVolume () { + return this.backend.getVolume(); + } + /** * Set the playback rate. * @@ -721,6 +736,15 @@ export default class WaveSurfer extends util.Observer { this.backend.setPlaybackRate(rate); } + /** + * Get the playback rate. + * + * @return {number} + */ + getPlaybackRate() { + return this.backend.getPlaybackRate(); + } + /** * Toggle the volume on and off. It not currenly muted it will save the * current volume value and turn the volume off. If currently muted then it @@ -761,6 +785,16 @@ export default class WaveSurfer extends util.Observer { } } + /** + * Get the current mute status. + * + * @example const isMuted = wavesurfer.getMute(); + * @return {boolean} + */ + getMute() { + return this.isMuted; + } + /** * Toggles `scrollParent` and redraws * @@ -970,14 +1004,18 @@ export default class WaveSurfer extends util.Observer { this.backend.once('error', err => this.fireEvent('error', err)) ); - // If no pre-decoded peaks provided, attempt to download the audio file - // and decode it with Web Audio. + // If no pre-decoded peaks provided or pre-decoded peaks are + // provided with forceDecode flag, attempt to download the + // audio file and decode it with Web Audio. if (peaks) { this.backend.setPeaks(peaks); - } else if (this.backend.supportsWebAudio()) { + } + + if ((!peaks || this.params.forceDecode) && this.backend.supportsWebAudio()) { this.getArrayBuffer(url, arraybuffer => { this.decodeArrayBuffer(arraybuffer, buffer => { this.backend.buffer = buffer; + this.backend.setPeaks(null); this.drawBuffer(); this.fireEvent('waveform-ready'); }); @@ -1154,5 +1192,6 @@ export default class WaveSurfer extends util.Observer { this.backend.destroy(); this.drawer.destroy(); this.isDestroyed = true; + this.arraybuffer = null; } } diff --git a/src/webaudio.js b/src/webaudio.js index c39c5179f..4aa11dd6d 100755 --- a/src/webaudio.js +++ b/src/webaudio.js @@ -72,12 +72,12 @@ export default class WebAudio extends util.Observer { * @return {AudioContext} */ getAudioContext() { - if (!this.audioContext) { - this.audioContext = new ( + if (!window.WaveSurferAudioContext) { + window.WaveSurferAudioContext = new ( window.AudioContext || window.webkitAudioContext ); } - return this.audioContext; + return window.WaveSurferAudioContext; } /** @@ -87,12 +87,12 @@ export default class WebAudio extends util.Observer { * @return {OfflineAudioContext} */ getOfflineAudioContext(sampleRate) { - if (!this.offlineAudioContext) { - this.offlineAudioContext = new ( + if (!window.WaveSurferOfflineAudioContext) { + window.WaveSurferOfflineAudioContext = new ( window.OfflineAudioContext || window.webkitOfflineAudioContext )(1, 2, sampleRate); } - return this.offlineAudioContext; + return window.WaveSurferOfflineAudioContext; } /** @@ -420,6 +420,25 @@ export default class WebAudio extends util.Observer { this.gainNode.disconnect(); this.scriptNode.disconnect(); this.analyser.disconnect(); + + // close the audioContext if closeAudioContext option is set to true + if (this.params.closeAudioContext) { + // check if browser supports AudioContext.close() + if (typeof this.ac.close === 'function') { + this.ac.close(); + } + // clear the reference to the audiocontext + this.ac = null; + // clear the actual audiocontext, either passed as param or the + // global singleton + if (!this.params.audioContext) { + window.WaveSurferAudioContext = null; + } else { + this.params.audioContext = null; + } + // clear the offlineAudioContext + window.WaveSurferOfflineAudioContext = null; + } } /**