123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- /**
- * (Single) Region plugin class
- *
- * Must be turned into an observer before instantiating. This is done in
- * RegionsPlugin (main plugin class)
- *
- * @extends {Observer}
- */
- class Region {
- constructor(params, ws) {
- this.wavesurfer = ws;
- this.wrapper = ws.drawer.wrapper;
- this.style = ws.util.style;
- this.id = params.id == null ? ws.util.getId() : params.id;
- this.start = Number(params.start) || 0;
- this.end = params.end == null ?
- // small marker-like region
- this.start + (4 / this.wrapper.scrollWidth) * this.wavesurfer.getDuration() :
- Number(params.end);
- this.resize = params.resize === undefined ? true : Boolean(params.resize);
- this.drag = params.drag === undefined ? true : Boolean(params.drag);
- this.loop = Boolean(params.loop);
- this.color = params.color || 'rgba(0, 0, 0, 0.1)';
- this.data = params.data || {};
- this.attributes = params.attributes || {};
- this.maxLength = params.maxLength;
- this.minLength = params.minLength;
- this.bindInOut();
- this.render();
- this.onZoom = this.updateRender.bind(this);
- this.wavesurfer.on('zoom', this.onZoom);
- this.wavesurfer.fireEvent('region-created', this);
- }
- /* Update region params. */
- update(params) {
- if (null != params.start) {
- this.start = Number(params.start);
- }
- if (null != params.end) {
- this.end = Number(params.end);
- }
- if (null != params.loop) {
- this.loop = Boolean(params.loop);
- }
- if (null != params.color) {
- this.color = params.color;
- }
- if (null != params.data) {
- this.data = params.data;
- }
- if (null != params.resize) {
- this.resize = Boolean(params.resize);
- }
- if (null != params.drag) {
- this.drag = Boolean(params.drag);
- }
- if (null != params.maxLength) {
- this.maxLength = Number(params.maxLength);
- }
- if (null != params.minLength) {
- this.minLength = Number(params.minLength);
- }
- if (null != params.attributes) {
- this.attributes = params.attributes;
- }
- this.updateRender();
- this.fireEvent('update');
- this.wavesurfer.fireEvent('region-updated', this);
- }
- /* Remove a single region. */
- remove() {
- if (this.element) {
- this.wrapper.removeChild(this.element);
- this.element = null;
- this.fireEvent('remove');
- this.wavesurfer.un('zoom', this.onZoom);
- this.wavesurfer.fireEvent('region-removed', this);
- }
- }
- /* Play the audio region. */
- play() {
- this.wavesurfer.play(this.start, this.end);
- this.fireEvent('play');
- this.wavesurfer.fireEvent('region-play', this);
- }
- /* Play the region in loop. */
- playLoop() {
- this.play();
- this.once('out', () => this.playLoop());
- }
- /* Render a region as a DOM element. */
- render() {
- const regionEl = document.createElement('region');
- regionEl.className = 'wavesurfer-region';
- regionEl.title = this.formatTime(this.start, this.end);
- regionEl.setAttribute('data-id', this.id);
- for (const attrname in this.attributes) {
- regionEl.setAttribute('data-region-' + attrname, this.attributes[attrname]);
- }
- const width = this.wrapper.scrollWidth;
- this.style(regionEl, {
- position: 'absolute',
- zIndex: 2,
- height: '100%',
- top: '0px'
- });
- /* Resize handles */
- if (this.resize) {
- const handleLeft = regionEl.appendChild(document.createElement('handle'));
- const handleRight = regionEl.appendChild(document.createElement('handle'));
- handleLeft.className = 'wavesurfer-handle wavesurfer-handle-start';
- handleRight.className = 'wavesurfer-handle wavesurfer-handle-end';
- const css = {
- cursor: 'col-resize',
- position: 'absolute',
- left: '0px',
- top: '0px',
- width: '1%',
- maxWidth: '4px',
- height: '100%'
- };
- this.style(handleLeft, css);
- this.style(handleRight, css);
- this.style(handleRight, {
- left: '100%'
- });
- }
- this.element = this.wrapper.appendChild(regionEl);
- this.updateRender();
- this.bindEvents(regionEl);
- }
- formatTime(start, end) {
- return (start == end ? [start] : [start, end]).map(time => [
- Math.floor((time % 3600) / 60), // minutes
- ('00' + Math.floor(time % 60)).slice(-2) // seconds
- ].join(':')).join('-');
- }
- getWidth() {
- return this.wavesurfer.drawer.width / this.wavesurfer.params.pixelRatio;
- }
- /* Update element's position, width, color. */
- updateRender() {
- const dur = this.wavesurfer.getDuration();
- const width = this.getWidth();
- if (this.start < 0) {
- this.start = 0;
- this.end = this.end - this.start;
- }
- if (this.end > dur) {
- this.end = dur;
- this.start = dur - (this.end - this.start);
- }
- if (this.minLength != null) {
- this.end = Math.max(this.start + this.minLength, this.end);
- }
- if (this.maxLength != null) {
- this.end = Math.min(this.start + this.maxLength, this.end);
- }
- if (this.element != null) {
- // Calculate the left and width values of the region such that
- // no gaps appear between regions.
- const left = Math.round(this.start / dur * width);
- const regionWidth =
- Math.round(this.end / dur * width) - left;
- this.style(this.element, {
- left: left + 'px',
- width: regionWidth + 'px',
- backgroundColor: this.color,
- cursor: this.drag ? 'move' : 'default'
- });
- for (const attrname in this.attributes) {
- this.element.setAttribute('data-region-' + attrname, this.attributes[attrname]);
- }
- this.element.title = this.formatTime(this.start, this.end);
- }
- }
- /* Bind audio events. */
- bindInOut() {
- this.firedIn = false;
- this.firedOut = false;
- const onProcess = time => {
- if (!this.firedOut && this.firedIn && (this.start >= Math.round(time * 100) / 100 || this.end <= Math.round(time * 100) / 100)) {
- this.firedOut = true;
- this.firedIn = false;
- this.fireEvent('out');
- this.wavesurfer.fireEvent('region-out', this);
- }
- if (!this.firedIn && this.start <= time && this.end > time) {
- this.firedIn = true;
- this.firedOut = false;
- this.fireEvent('in');
- this.wavesurfer.fireEvent('region-in', this);
- }
- };
- this.wavesurfer.backend.on('audioprocess', onProcess);
- this.on('remove', () => {
- this.wavesurfer.backend.un('audioprocess', onProcess);
- });
- /* Loop playback. */
- this.on('out', () => {
- if (this.loop) {
- this.wavesurfer.play(this.start);
- }
- });
- }
- /* Bind DOM events. */
- bindEvents() {
- this.element.addEventListener('mouseenter', e => {
- this.fireEvent('mouseenter', e);
- this.wavesurfer.fireEvent('region-mouseenter', this, e);
- });
- this.element.addEventListener('mouseleave', e => {
- this.fireEvent('mouseleave', e);
- this.wavesurfer.fireEvent('region-mouseleave', this, e);
- });
- this.element.addEventListener('click', e => {
- e.preventDefault();
- this.fireEvent('click', e);
- this.wavesurfer.fireEvent('region-click', this, e);
- });
- this.element.addEventListener('dblclick', e => {
- e.stopPropagation();
- e.preventDefault();
- this.fireEvent('dblclick', e);
- this.wavesurfer.fireEvent('region-dblclick', this, e);
- });
- /* Drag or resize on mousemove. */
- (this.drag || this.resize) && (() => {
- const duration = this.wavesurfer.getDuration();
- let startTime;
- let touchId;
- let drag;
- let resize;
- const onDown = e => {
- if (e.touches && e.touches.length > 1) { return; }
- touchId = e.targetTouches ? e.targetTouches[0].identifier : null;
- e.stopPropagation();
- startTime = this.wavesurfer.drawer.handleEvent(e, true) * duration;
- if (e.target.tagName.toLowerCase() == 'handle') {
- if (e.target.classList.contains('wavesurfer-handle-start')) {
- resize = 'start';
- } else {
- resize = 'end';
- }
- } else {
- drag = true;
- resize = false;
- }
- };
- const onUp = e => {
- if (e.touches && e.touches.length > 1) { return; }
- if (drag || resize) {
- drag = false;
- resize = false;
- this.fireEvent('update-end', e);
- this.wavesurfer.fireEvent('region-update-end', this, e);
- }
- };
- const onMove = e => {
- if (e.touches && e.touches.length > 1) { return; }
- if (e.targetTouches && e.targetTouches[0].identifier != touchId) { return; }
- if (drag || resize) {
- const time = this.wavesurfer.drawer.handleEvent(e) * duration;
- const delta = time - startTime;
- startTime = time;
- // Drag
- if (this.drag && drag) {
- this.onDrag(delta);
- }
- // Resize
- if (this.resize && resize) {
- this.onResize(delta, resize);
- }
- }
- };
- this.element.addEventListener('mousedown', onDown);
- this.element.addEventListener('touchstart', onDown);
- this.wrapper.addEventListener('mousemove', onMove);
- this.wrapper.addEventListener('touchmove', onMove);
- document.body.addEventListener('mouseup', onUp);
- document.body.addEventListener('touchend', onUp);
- this.on('remove', () => {
- document.body.removeEventListener('mouseup', onUp);
- document.body.removeEventListener('touchend', onUp);
- this.wrapper.removeEventListener('mousemove', onMove);
- this.wrapper.removeEventListener('touchmove', onMove);
- });
- this.wavesurfer.on('destroy', () => {
- document.body.removeEventListener('mouseup', onUp);
- document.body.removeEventListener('touchend', onUp);
- });
- })();
- }
- onDrag(delta) {
- const maxEnd = this.wavesurfer.getDuration();
- if ((this.end + delta) > maxEnd || (this.start + delta) < 0) {
- return;
- }
- this.update({
- start: this.start + delta,
- end: this.end + delta
- });
- }
- onResize(delta, direction) {
- if (direction == 'start') {
- this.update({
- start: Math.min(this.start + delta, this.end),
- end: Math.max(this.start + delta, this.end)
- });
- } else {
- this.update({
- start: Math.min(this.end + delta, this.start),
- end: Math.max(this.end + delta, this.start)
- });
- }
- }
- }
- /**
- * @typedef {Object} RegionsPluginParams
- * @property {?boolean} dragSelection Enable creating regions by dragging wih
- * the mouse
- * @property {?RegionParams[]} regions Regions that should be added upon
- * initialisation
- * @property {number} slop=2 The sensitivity of the mouse dragging
- * @property {?boolean} deferInit Set to true to manually call
- * `initPlugin('regions')`
- */
- /**
- * @typedef {Object} RegionParams
- * @desc The parameters used to describe a region.
- * @example wavesurfer.addRegion(regionParams);
- * @property {string} id=→random The id of the region
- * @property {number} start=0 The start position of the region (in seconds).
- * @property {number} end=0 The end position of the region (in seconds).
- * @property {?boolean} loop Whether to loop the region when played back.
- * @property {boolean} drag=true Allow/dissallow dragging the region.
- * @property {boolean} resize=true Allow/dissallow resizing the region.
- * @property {string} [color='rgba(0, 0, 0, 0.1)'] HTML color code.
- */
- /**
- * 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`).
- *
- * @implements {PluginClass}
- * @extends {Observer}
- *
- * @example
- * // es6
- * import RegionsPlugin from 'wavesurfer.regions.js';
- *
- * // commonjs
- * var RegionsPlugin = require('wavesurfer.regions.js');
- *
- * // if you are using <script> tags
- * var RegionsPlugin = window.WaveSurfer.regions;
- *
- * // ... initialising wavesurfer with the plugin
- * var wavesurfer = WaveSurfer.create({
- * // wavesurfer options ...
- * plugins: [
- * RegionsPlugin.create({
- * // plugin options ...
- * })
- * ]
- * });
- */
- export default class RegionsPlugin {
- /**
- * Regions plugin definition factory
- *
- * This function must be used to create a plugin definition which can be
- * used by wavesurfer to correctly instantiate the plugin.
- *
- * @param {RegionsPluginParams} params parameters use to initialise the plugin
- * @return {PluginDefinition} an object representing the plugin
- */
- static create(params) {
- return {
- name: 'regions',
- deferInit: params && params.deferInit ? params.deferInit : false,
- params: params,
- staticProps: {
- initRegions() {
- console.warn('Deprecated initRegions! Use wavesurfer.initPlugins("regions") instead!');
- this.initPlugin('regions');
- },
- addRegion(options) {
- if (!this.initialisedPluginList.regions) {
- this.initPlugin('regions');
- }
- return this.regions.add(options);
- },
- clearRegions() {
- this.regions && this.regions.clear();
- },
- enableDragSelection(options) {
- if (!this.initialisedPluginList.regions) {
- this.initPlugin('regions');
- }
- this.regions.enableDragSelection(options);
- },
- disableDragSelection() {
- this.regions.disableDragSelection();
- }
- },
- instance: RegionsPlugin
- };
- }
- constructor(params, ws) {
- this.params = params;
- this.wavesurfer = ws;
- this.util = ws.util;
- // turn the plugin instance into an observer
- const observerPrototypeKeys = Object.getOwnPropertyNames(this.util.Observer.prototype);
- observerPrototypeKeys.forEach(key => {
- Region.prototype[key] = this.util.Observer.prototype[key];
- });
- this.wavesurfer.Region = Region;
- // Id-based hash of regions.
- this.list = {};
- this._onReady = () => {
- this.wrapper = this.wavesurfer.drawer.wrapper;
- if (this.params.regions) {
- this.params.regions.forEach(region => {
- this.add(region);
- });
- }
- if (this.params.dragSelection) {
- this.enableDragSelection(this.params);
- }
- };
- }
- init() {
- // Check if ws is ready
- if (this.wavesurfer.isReady) {
- this._onReady();
- }
- this.wavesurfer.on('ready', this._onReady);
- }
- destroy() {
- this.wavesurfer.un('ready', this._onReady);
- this.disableDragSelection();
- this.clear();
- }
- /* Add a region. */
- add(params) {
- const region = new this.wavesurfer.Region(params, this.wavesurfer);
- this.list[region.id] = region;
- region.on('remove', () => {
- delete this.list[region.id];
- });
- return region;
- }
- /* Remove all regions. */
- clear() {
- Object.keys(this.list).forEach(id => {
- this.list[id].remove();
- });
- }
- enableDragSelection(params) {
- const slop = params.slop || 2;
- let drag;
- let start;
- let region;
- let touchId;
- let pxMove = 0;
- const eventDown = e => {
- if (e.touches && e.touches.length > 1) { return; }
- touchId = e.targetTouches ? e.targetTouches[0].identifier : null;
- drag = true;
- start = this.wavesurfer.drawer.handleEvent(e, true);
- region = null;
- };
- this.wrapper.addEventListener('mousedown', eventDown);
- this.wrapper.addEventListener('touchstart', eventDown);
- this.on('disable-drag-selection', () => {
- this.wrapper.removeEventListener('touchstart', eventDown);
- this.wrapper.removeEventListener('mousedown', eventDown);
- });
- const eventUp = e => {
- if (e.touches && e.touches.length > 1) { return; }
- drag = false;
- pxMove = 0;
- if (region) {
- region.fireEvent('update-end', e);
- this.wavesurfer.fireEvent('region-update-end', region, e);
- }
- region = null;
- };
- this.wrapper.addEventListener('mouseup', eventUp);
- this.wrapper.addEventListener('touchend', eventUp);
- this.on('disable-drag-selection', () => {
- this.wrapper.removeEventListener('touchend', eventUp);
- this.wrapper.removeEventListener('mouseup', eventUp);
- });
- const eventMove = e => {
- if (!drag) { return; }
- if (++pxMove <= slop) { return; }
- if (e.touches && e.touches.length > 1) { return; }
- if (e.targetTouches && e.targetTouches[0].identifier != touchId) { return; }
- if (!region) {
- region = this.add(params || {});
- }
- const duration = this.wavesurfer.getDuration();
- const end = this.wavesurfer.drawer.handleEvent(e);
- region.update({
- start: Math.min(end * duration, start * duration),
- end: Math.max(end * duration, start * duration)
- });
- };
- this.wrapper.addEventListener('mousemove', eventMove);
- this.wrapper.addEventListener('touchmove', eventMove);
- this.on('disable-drag-selection', () => {
- this.wrapper.removeEventListener('touchmove', eventMove);
- this.wrapper.removeEventListener('mousemove', eventMove);
- });
- }
- disableDragSelection() {
- this.fireEvent('disable-drag-selection');
- }
- /* Get current region
- * The smallest region that contains the current time.
- * If several such regions exist, we take the first.
- * Return null if none exist. */
- getCurrentRegion() {
- const time = this.wavesurfer.getCurrentTime();
- let min = null;
- Object.keys(this.list).forEach(id => {
- const cur = this.list[id];
- if (cur.start <= time && cur.end >= time) {
- if (!min || ((cur.end - cur.start) < (min.end - min.start))) {
- min = cur;
- }
- }
- });
- return min;
- }
- }
|