regions.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. /**
  2. * (Single) Region plugin class
  3. *
  4. * Must be turned into an observer before instantiating. This is done in
  5. * RegionsPlugin (main plugin class)
  6. *
  7. * @extends {Observer}
  8. */
  9. class Region {
  10. constructor(params, ws) {
  11. this.wavesurfer = ws;
  12. this.wrapper = ws.drawer.wrapper;
  13. this.style = ws.util.style;
  14. this.id = params.id == null ? ws.util.getId() : params.id;
  15. this.start = Number(params.start) || 0;
  16. this.end = params.end == null ?
  17. // small marker-like region
  18. this.start + (4 / this.wrapper.scrollWidth) * this.wavesurfer.getDuration() :
  19. Number(params.end);
  20. this.resize = params.resize === undefined ? true : Boolean(params.resize);
  21. this.drag = params.drag === undefined ? true : Boolean(params.drag);
  22. this.loop = Boolean(params.loop);
  23. this.color = params.color || 'rgba(0, 0, 0, 0.1)';
  24. this.data = params.data || {};
  25. this.attributes = params.attributes || {};
  26. this.maxLength = params.maxLength;
  27. this.minLength = params.minLength;
  28. this.bindInOut();
  29. this.render();
  30. this.onZoom = this.updateRender.bind(this);
  31. this.wavesurfer.on('zoom', this.onZoom);
  32. this.wavesurfer.fireEvent('region-created', this);
  33. }
  34. /* Update region params. */
  35. update(params) {
  36. if (null != params.start) {
  37. this.start = Number(params.start);
  38. }
  39. if (null != params.end) {
  40. this.end = Number(params.end);
  41. }
  42. if (null != params.loop) {
  43. this.loop = Boolean(params.loop);
  44. }
  45. if (null != params.color) {
  46. this.color = params.color;
  47. }
  48. if (null != params.data) {
  49. this.data = params.data;
  50. }
  51. if (null != params.resize) {
  52. this.resize = Boolean(params.resize);
  53. }
  54. if (null != params.drag) {
  55. this.drag = Boolean(params.drag);
  56. }
  57. if (null != params.maxLength) {
  58. this.maxLength = Number(params.maxLength);
  59. }
  60. if (null != params.minLength) {
  61. this.minLength = Number(params.minLength);
  62. }
  63. if (null != params.attributes) {
  64. this.attributes = params.attributes;
  65. }
  66. this.updateRender();
  67. this.fireEvent('update');
  68. this.wavesurfer.fireEvent('region-updated', this);
  69. }
  70. /* Remove a single region. */
  71. remove() {
  72. if (this.element) {
  73. this.wrapper.removeChild(this.element);
  74. this.element = null;
  75. this.fireEvent('remove');
  76. this.wavesurfer.un('zoom', this.onZoom);
  77. this.wavesurfer.fireEvent('region-removed', this);
  78. }
  79. }
  80. /* Play the audio region. */
  81. play() {
  82. this.wavesurfer.play(this.start, this.end);
  83. this.fireEvent('play');
  84. this.wavesurfer.fireEvent('region-play', this);
  85. }
  86. /* Play the region in loop. */
  87. playLoop() {
  88. this.play();
  89. this.once('out', () => this.playLoop());
  90. }
  91. /* Render a region as a DOM element. */
  92. render() {
  93. const regionEl = document.createElement('region');
  94. regionEl.className = 'wavesurfer-region';
  95. regionEl.title = this.formatTime(this.start, this.end);
  96. regionEl.setAttribute('data-id', this.id);
  97. for (const attrname in this.attributes) {
  98. regionEl.setAttribute('data-region-' + attrname, this.attributes[attrname]);
  99. }
  100. const width = this.wrapper.scrollWidth;
  101. this.style(regionEl, {
  102. position: 'absolute',
  103. zIndex: 2,
  104. height: '100%',
  105. top: '0px'
  106. });
  107. /* Resize handles */
  108. if (this.resize) {
  109. const handleLeft = regionEl.appendChild(document.createElement('handle'));
  110. const handleRight = regionEl.appendChild(document.createElement('handle'));
  111. handleLeft.className = 'wavesurfer-handle wavesurfer-handle-start';
  112. handleRight.className = 'wavesurfer-handle wavesurfer-handle-end';
  113. const css = {
  114. cursor: 'col-resize',
  115. position: 'absolute',
  116. left: '0px',
  117. top: '0px',
  118. width: '1%',
  119. maxWidth: '4px',
  120. height: '100%'
  121. };
  122. this.style(handleLeft, css);
  123. this.style(handleRight, css);
  124. this.style(handleRight, {
  125. left: '100%'
  126. });
  127. }
  128. this.element = this.wrapper.appendChild(regionEl);
  129. this.updateRender();
  130. this.bindEvents(regionEl);
  131. }
  132. formatTime(start, end) {
  133. return (start == end ? [start] : [start, end]).map(time => [
  134. Math.floor((time % 3600) / 60), // minutes
  135. ('00' + Math.floor(time % 60)).slice(-2) // seconds
  136. ].join(':')).join('-');
  137. }
  138. getWidth() {
  139. return this.wavesurfer.drawer.width / this.wavesurfer.params.pixelRatio;
  140. }
  141. /* Update element's position, width, color. */
  142. updateRender() {
  143. const dur = this.wavesurfer.getDuration();
  144. const width = this.getWidth();
  145. if (this.start < 0) {
  146. this.start = 0;
  147. this.end = this.end - this.start;
  148. }
  149. if (this.end > dur) {
  150. this.end = dur;
  151. this.start = dur - (this.end - this.start);
  152. }
  153. if (this.minLength != null) {
  154. this.end = Math.max(this.start + this.minLength, this.end);
  155. }
  156. if (this.maxLength != null) {
  157. this.end = Math.min(this.start + this.maxLength, this.end);
  158. }
  159. if (this.element != null) {
  160. // Calculate the left and width values of the region such that
  161. // no gaps appear between regions.
  162. const left = Math.round(this.start / dur * width);
  163. const regionWidth =
  164. Math.round(this.end / dur * width) - left;
  165. this.style(this.element, {
  166. left: left + 'px',
  167. width: regionWidth + 'px',
  168. backgroundColor: this.color,
  169. cursor: this.drag ? 'move' : 'default'
  170. });
  171. for (const attrname in this.attributes) {
  172. this.element.setAttribute('data-region-' + attrname, this.attributes[attrname]);
  173. }
  174. this.element.title = this.formatTime(this.start, this.end);
  175. }
  176. }
  177. /* Bind audio events. */
  178. bindInOut() {
  179. this.firedIn = false;
  180. this.firedOut = false;
  181. const onProcess = time => {
  182. if (!this.firedOut && this.firedIn && (this.start >= Math.round(time * 100) / 100 || this.end <= Math.round(time * 100) / 100)) {
  183. this.firedOut = true;
  184. this.firedIn = false;
  185. this.fireEvent('out');
  186. this.wavesurfer.fireEvent('region-out', this);
  187. }
  188. if (!this.firedIn && this.start <= time && this.end > time) {
  189. this.firedIn = true;
  190. this.firedOut = false;
  191. this.fireEvent('in');
  192. this.wavesurfer.fireEvent('region-in', this);
  193. }
  194. };
  195. this.wavesurfer.backend.on('audioprocess', onProcess);
  196. this.on('remove', () => {
  197. this.wavesurfer.backend.un('audioprocess', onProcess);
  198. });
  199. /* Loop playback. */
  200. this.on('out', () => {
  201. if (this.loop) {
  202. this.wavesurfer.play(this.start);
  203. }
  204. });
  205. }
  206. /* Bind DOM events. */
  207. bindEvents() {
  208. this.element.addEventListener('mouseenter', e => {
  209. this.fireEvent('mouseenter', e);
  210. this.wavesurfer.fireEvent('region-mouseenter', this, e);
  211. });
  212. this.element.addEventListener('mouseleave', e => {
  213. this.fireEvent('mouseleave', e);
  214. this.wavesurfer.fireEvent('region-mouseleave', this, e);
  215. });
  216. this.element.addEventListener('click', e => {
  217. e.preventDefault();
  218. this.fireEvent('click', e);
  219. this.wavesurfer.fireEvent('region-click', this, e);
  220. });
  221. this.element.addEventListener('dblclick', e => {
  222. e.stopPropagation();
  223. e.preventDefault();
  224. this.fireEvent('dblclick', e);
  225. this.wavesurfer.fireEvent('region-dblclick', this, e);
  226. });
  227. /* Drag or resize on mousemove. */
  228. (this.drag || this.resize) && (() => {
  229. const duration = this.wavesurfer.getDuration();
  230. let startTime;
  231. let touchId;
  232. let drag;
  233. let resize;
  234. const onDown = e => {
  235. if (e.touches && e.touches.length > 1) { return; }
  236. touchId = e.targetTouches ? e.targetTouches[0].identifier : null;
  237. e.stopPropagation();
  238. startTime = this.wavesurfer.drawer.handleEvent(e, true) * duration;
  239. if (e.target.tagName.toLowerCase() == 'handle') {
  240. if (e.target.classList.contains('wavesurfer-handle-start')) {
  241. resize = 'start';
  242. } else {
  243. resize = 'end';
  244. }
  245. } else {
  246. drag = true;
  247. resize = false;
  248. }
  249. };
  250. const onUp = e => {
  251. if (e.touches && e.touches.length > 1) { return; }
  252. if (drag || resize) {
  253. drag = false;
  254. resize = false;
  255. this.fireEvent('update-end', e);
  256. this.wavesurfer.fireEvent('region-update-end', this, e);
  257. }
  258. };
  259. const onMove = e => {
  260. if (e.touches && e.touches.length > 1) { return; }
  261. if (e.targetTouches && e.targetTouches[0].identifier != touchId) { return; }
  262. if (drag || resize) {
  263. const time = this.wavesurfer.drawer.handleEvent(e) * duration;
  264. const delta = time - startTime;
  265. startTime = time;
  266. // Drag
  267. if (this.drag && drag) {
  268. this.onDrag(delta);
  269. }
  270. // Resize
  271. if (this.resize && resize) {
  272. this.onResize(delta, resize);
  273. }
  274. }
  275. };
  276. this.element.addEventListener('mousedown', onDown);
  277. this.element.addEventListener('touchstart', onDown);
  278. this.wrapper.addEventListener('mousemove', onMove);
  279. this.wrapper.addEventListener('touchmove', onMove);
  280. document.body.addEventListener('mouseup', onUp);
  281. document.body.addEventListener('touchend', onUp);
  282. this.on('remove', () => {
  283. document.body.removeEventListener('mouseup', onUp);
  284. document.body.removeEventListener('touchend', onUp);
  285. this.wrapper.removeEventListener('mousemove', onMove);
  286. this.wrapper.removeEventListener('touchmove', onMove);
  287. });
  288. this.wavesurfer.on('destroy', () => {
  289. document.body.removeEventListener('mouseup', onUp);
  290. document.body.removeEventListener('touchend', onUp);
  291. });
  292. })();
  293. }
  294. onDrag(delta) {
  295. const maxEnd = this.wavesurfer.getDuration();
  296. if ((this.end + delta) > maxEnd || (this.start + delta) < 0) {
  297. return;
  298. }
  299. this.update({
  300. start: this.start + delta,
  301. end: this.end + delta
  302. });
  303. }
  304. onResize(delta, direction) {
  305. if (direction == 'start') {
  306. this.update({
  307. start: Math.min(this.start + delta, this.end),
  308. end: Math.max(this.start + delta, this.end)
  309. });
  310. } else {
  311. this.update({
  312. start: Math.min(this.end + delta, this.start),
  313. end: Math.max(this.end + delta, this.start)
  314. });
  315. }
  316. }
  317. }
  318. /**
  319. * @typedef {Object} RegionsPluginParams
  320. * @property {?boolean} dragSelection Enable creating regions by dragging wih
  321. * the mouse
  322. * @property {?RegionParams[]} regions Regions that should be added upon
  323. * initialisation
  324. * @property {number} slop=2 The sensitivity of the mouse dragging
  325. * @property {?boolean} deferInit Set to true to manually call
  326. * `initPlugin('regions')`
  327. */
  328. /**
  329. * @typedef {Object} RegionParams
  330. * @desc The parameters used to describe a region.
  331. * @example wavesurfer.addRegion(regionParams);
  332. * @property {string} id=→random The id of the region
  333. * @property {number} start=0 The start position of the region (in seconds).
  334. * @property {number} end=0 The end position of the region (in seconds).
  335. * @property {?boolean} loop Whether to loop the region when played back.
  336. * @property {boolean} drag=true Allow/dissallow dragging the region.
  337. * @property {boolean} resize=true Allow/dissallow resizing the region.
  338. * @property {string} [color='rgba(0, 0, 0, 0.1)'] HTML color code.
  339. */
  340. /**
  341. * Regions are visual overlays on waveform that can be used to play and loop
  342. * portions of audio. Regions can be dragged and resized.
  343. *
  344. * Visual customization is possible via CSS (using the selectors
  345. * `.wavesurfer-region` and `.wavesurfer-handle`).
  346. *
  347. * @implements {PluginClass}
  348. * @extends {Observer}
  349. *
  350. * @example
  351. * // es6
  352. * import RegionsPlugin from 'wavesurfer.regions.js';
  353. *
  354. * // commonjs
  355. * var RegionsPlugin = require('wavesurfer.regions.js');
  356. *
  357. * // if you are using <script> tags
  358. * var RegionsPlugin = window.WaveSurfer.regions;
  359. *
  360. * // ... initialising wavesurfer with the plugin
  361. * var wavesurfer = WaveSurfer.create({
  362. * // wavesurfer options ...
  363. * plugins: [
  364. * RegionsPlugin.create({
  365. * // plugin options ...
  366. * })
  367. * ]
  368. * });
  369. */
  370. export default class RegionsPlugin {
  371. /**
  372. * Regions plugin definition factory
  373. *
  374. * This function must be used to create a plugin definition which can be
  375. * used by wavesurfer to correctly instantiate the plugin.
  376. *
  377. * @param {RegionsPluginParams} params parameters use to initialise the plugin
  378. * @return {PluginDefinition} an object representing the plugin
  379. */
  380. static create(params) {
  381. return {
  382. name: 'regions',
  383. deferInit: params && params.deferInit ? params.deferInit : false,
  384. params: params,
  385. staticProps: {
  386. initRegions() {
  387. console.warn('Deprecated initRegions! Use wavesurfer.initPlugins("regions") instead!');
  388. this.initPlugin('regions');
  389. },
  390. addRegion(options) {
  391. if (!this.initialisedPluginList.regions) {
  392. this.initPlugin('regions');
  393. }
  394. return this.regions.add(options);
  395. },
  396. clearRegions() {
  397. this.regions && this.regions.clear();
  398. },
  399. enableDragSelection(options) {
  400. if (!this.initialisedPluginList.regions) {
  401. this.initPlugin('regions');
  402. }
  403. this.regions.enableDragSelection(options);
  404. },
  405. disableDragSelection() {
  406. this.regions.disableDragSelection();
  407. }
  408. },
  409. instance: RegionsPlugin
  410. };
  411. }
  412. constructor(params, ws) {
  413. this.params = params;
  414. this.wavesurfer = ws;
  415. this.util = ws.util;
  416. // turn the plugin instance into an observer
  417. const observerPrototypeKeys = Object.getOwnPropertyNames(this.util.Observer.prototype);
  418. observerPrototypeKeys.forEach(key => {
  419. Region.prototype[key] = this.util.Observer.prototype[key];
  420. });
  421. this.wavesurfer.Region = Region;
  422. // Id-based hash of regions.
  423. this.list = {};
  424. this._onReady = () => {
  425. this.wrapper = this.wavesurfer.drawer.wrapper;
  426. if (this.params.regions) {
  427. this.params.regions.forEach(region => {
  428. this.add(region);
  429. });
  430. }
  431. if (this.params.dragSelection) {
  432. this.enableDragSelection(this.params);
  433. }
  434. };
  435. }
  436. init() {
  437. // Check if ws is ready
  438. if (this.wavesurfer.isReady) {
  439. this._onReady();
  440. }
  441. this.wavesurfer.on('ready', this._onReady);
  442. }
  443. destroy() {
  444. this.wavesurfer.un('ready', this._onReady);
  445. this.disableDragSelection();
  446. this.clear();
  447. }
  448. /* Add a region. */
  449. add(params) {
  450. const region = new this.wavesurfer.Region(params, this.wavesurfer);
  451. this.list[region.id] = region;
  452. region.on('remove', () => {
  453. delete this.list[region.id];
  454. });
  455. return region;
  456. }
  457. /* Remove all regions. */
  458. clear() {
  459. Object.keys(this.list).forEach(id => {
  460. this.list[id].remove();
  461. });
  462. }
  463. enableDragSelection(params) {
  464. const slop = params.slop || 2;
  465. let drag;
  466. let start;
  467. let region;
  468. let touchId;
  469. let pxMove = 0;
  470. const eventDown = e => {
  471. if (e.touches && e.touches.length > 1) { return; }
  472. touchId = e.targetTouches ? e.targetTouches[0].identifier : null;
  473. drag = true;
  474. start = this.wavesurfer.drawer.handleEvent(e, true);
  475. region = null;
  476. };
  477. this.wrapper.addEventListener('mousedown', eventDown);
  478. this.wrapper.addEventListener('touchstart', eventDown);
  479. this.on('disable-drag-selection', () => {
  480. this.wrapper.removeEventListener('touchstart', eventDown);
  481. this.wrapper.removeEventListener('mousedown', eventDown);
  482. });
  483. const eventUp = e => {
  484. if (e.touches && e.touches.length > 1) { return; }
  485. drag = false;
  486. pxMove = 0;
  487. if (region) {
  488. region.fireEvent('update-end', e);
  489. this.wavesurfer.fireEvent('region-update-end', region, e);
  490. }
  491. region = null;
  492. };
  493. this.wrapper.addEventListener('mouseup', eventUp);
  494. this.wrapper.addEventListener('touchend', eventUp);
  495. this.on('disable-drag-selection', () => {
  496. this.wrapper.removeEventListener('touchend', eventUp);
  497. this.wrapper.removeEventListener('mouseup', eventUp);
  498. });
  499. const eventMove = e => {
  500. if (!drag) { return; }
  501. if (++pxMove <= slop) { return; }
  502. if (e.touches && e.touches.length > 1) { return; }
  503. if (e.targetTouches && e.targetTouches[0].identifier != touchId) { return; }
  504. if (!region) {
  505. region = this.add(params || {});
  506. }
  507. const duration = this.wavesurfer.getDuration();
  508. const end = this.wavesurfer.drawer.handleEvent(e);
  509. region.update({
  510. start: Math.min(end * duration, start * duration),
  511. end: Math.max(end * duration, start * duration)
  512. });
  513. };
  514. this.wrapper.addEventListener('mousemove', eventMove);
  515. this.wrapper.addEventListener('touchmove', eventMove);
  516. this.on('disable-drag-selection', () => {
  517. this.wrapper.removeEventListener('touchmove', eventMove);
  518. this.wrapper.removeEventListener('mousemove', eventMove);
  519. });
  520. }
  521. disableDragSelection() {
  522. this.fireEvent('disable-drag-selection');
  523. }
  524. /* Get current region
  525. * The smallest region that contains the current time.
  526. * If several such regions exist, we take the first.
  527. * Return null if none exist. */
  528. getCurrentRegion() {
  529. const time = this.wavesurfer.getCurrentTime();
  530. let min = null;
  531. Object.keys(this.list).forEach(id => {
  532. const cur = this.list[id];
  533. if (cur.start <= time && cur.end >= time) {
  534. if (!min || ((cur.end - cur.start) < (min.end - min.start))) {
  535. min = cur;
  536. }
  537. }
  538. });
  539. return min;
  540. }
  541. }