wavesurfer.js 7.8.4-beta.0 → 7.8.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  import EventEmitter from './event-emitter.js';
2
2
  /** Base class for wavesurfer plugins */
3
3
  export class BasePlugin extends EventEmitter {
4
+ wavesurfer;
5
+ subscriptions = [];
6
+ options;
4
7
  /** Create a plugin instance */
5
8
  constructor(options) {
6
9
  super();
7
- this.subscriptions = [];
8
10
  this.options = options;
9
11
  }
10
12
  /** Called after this.wavesurfer is available */
package/dist/decoder.js CHANGED
@@ -1,19 +1,8 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  /** Decode an array buffer into an audio buffer */
11
- function decode(audioData, sampleRate) {
12
- return __awaiter(this, void 0, void 0, function* () {
13
- const audioCtx = new AudioContext({ sampleRate });
14
- const decode = audioCtx.decodeAudioData(audioData);
15
- return decode.finally(() => audioCtx.close());
16
- });
2
+ async function decode(audioData, sampleRate) {
3
+ const audioCtx = new AudioContext({ sampleRate });
4
+ const decode = audioCtx.decodeAudioData(audioData);
5
+ return decode.finally(() => audioCtx.close());
17
6
  }
18
7
  /** Normalize peaks to -1..1 */
19
8
  function normalize(channelData) {
@@ -46,7 +35,7 @@ function createBuffer(channelData, duration) {
46
35
  length: channelData[0].length,
47
36
  sampleRate: channelData[0].length / duration,
48
37
  numberOfChannels: channelData.length,
49
- getChannelData: (i) => channelData === null || channelData === void 0 ? void 0 : channelData[i],
38
+ getChannelData: (i) => channelData?.[i],
50
39
  copyFromChannel: AudioBuffer.prototype.copyFromChannel,
51
40
  copyToChannel: AudioBuffer.prototype.copyToChannel,
52
41
  };
package/dist/dom.js CHANGED
@@ -27,7 +27,7 @@ function renderNode(tagName, content) {
27
27
  }
28
28
  export function createElement(tagName, content, container) {
29
29
  const el = renderNode(tagName, content || {});
30
- container === null || container === void 0 ? void 0 : container.appendChild(el);
30
+ container?.appendChild(el);
31
31
  return el;
32
32
  }
33
33
  export default createElement;
package/dist/draggable.js CHANGED
@@ -25,7 +25,7 @@ export function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mo
25
25
  const rect = element.getBoundingClientRect();
26
26
  const { left, top } = rect;
27
27
  if (!isDragging) {
28
- onStart === null || onStart === void 0 ? void 0 : onStart(startX - left, startY - top);
28
+ onStart?.(startX - left, startY - top);
29
29
  isDragging = true;
30
30
  }
31
31
  onDrag(dx, dy, x - left, y - top);
@@ -39,7 +39,7 @@ export function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mo
39
39
  const y = event.clientY;
40
40
  const rect = element.getBoundingClientRect();
41
41
  const { left, top } = rect;
42
- onEnd === null || onEnd === void 0 ? void 0 : onEnd(x - left, y - top);
42
+ onEnd?.(x - left, y - top);
43
43
  }
44
44
  unsubscribeDocument();
45
45
  };
@@ -1,15 +1,13 @@
1
1
  /** A simple event emitter that can be used to listen to and emit events. */
2
2
  class EventEmitter {
3
- constructor() {
4
- this.listeners = {};
5
- }
3
+ listeners = {};
6
4
  /** Subscribe to an event. Returns an unsubscribe function. */
7
5
  on(event, listener, options) {
8
6
  if (!this.listeners[event]) {
9
7
  this.listeners[event] = new Set();
10
8
  }
11
9
  this.listeners[event].add(listener);
12
- if (options === null || options === void 0 ? void 0 : options.once) {
10
+ if (options?.once) {
13
11
  const unsubscribeOnce = () => {
14
12
  this.un(event, unsubscribeOnce);
15
13
  this.un(event, listener);
@@ -21,8 +19,7 @@ class EventEmitter {
21
19
  }
22
20
  /** Unsubscribe from an event */
23
21
  un(event, listener) {
24
- var _a;
25
- (_a = this.listeners[event]) === null || _a === void 0 ? void 0 : _a.delete(listener);
22
+ this.listeners[event]?.delete(listener);
26
23
  }
27
24
  /** Subscribe to an event only once */
28
25
  once(event, listener) {
package/dist/fetcher.js CHANGED
@@ -1,55 +1,42 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- function watchProgress(response, progressCallback) {
11
- return __awaiter(this, void 0, void 0, function* () {
12
- if (!response.body || !response.headers)
1
+ async function watchProgress(response, progressCallback) {
2
+ if (!response.body || !response.headers)
3
+ return;
4
+ const reader = response.body.getReader();
5
+ const contentLength = Number(response.headers.get('Content-Length')) || 0;
6
+ let receivedLength = 0;
7
+ // Process the data
8
+ const processChunk = async (value) => {
9
+ // Add to the received length
10
+ receivedLength += value?.length || 0;
11
+ const percentage = Math.round((receivedLength / contentLength) * 100);
12
+ progressCallback(percentage);
13
+ };
14
+ const read = async () => {
15
+ let data;
16
+ try {
17
+ data = await reader.read();
18
+ }
19
+ catch {
20
+ // Ignore errors because we can only handle the main response
13
21
  return;
14
- const reader = response.body.getReader();
15
- const contentLength = Number(response.headers.get('Content-Length')) || 0;
16
- let receivedLength = 0;
17
- // Process the data
18
- const processChunk = (value) => __awaiter(this, void 0, void 0, function* () {
19
- // Add to the received length
20
- receivedLength += (value === null || value === void 0 ? void 0 : value.length) || 0;
21
- const percentage = Math.round((receivedLength / contentLength) * 100);
22
- progressCallback(percentage);
23
- });
24
- const read = () => __awaiter(this, void 0, void 0, function* () {
25
- let data;
26
- try {
27
- data = yield reader.read();
28
- }
29
- catch (_a) {
30
- // Ignore errors because we can only handle the main response
31
- return;
32
- }
33
- // Continue reading data until done
34
- if (!data.done) {
35
- processChunk(data.value);
36
- yield read();
37
- }
38
- });
39
- read();
40
- });
41
- }
42
- function fetchBlob(url, progressCallback, requestInit) {
43
- return __awaiter(this, void 0, void 0, function* () {
44
- // Fetch the resource
45
- const response = yield fetch(url, requestInit);
46
- if (response.status >= 400) {
47
- throw new Error(`Failed to fetch ${url}: ${response.status} (${response.statusText})`);
48
22
  }
49
- // Read the data to track progress
50
- watchProgress(response.clone(), progressCallback);
51
- return response.blob();
52
- });
23
+ // Continue reading data until done
24
+ if (!data.done) {
25
+ processChunk(data.value);
26
+ await read();
27
+ }
28
+ };
29
+ read();
30
+ }
31
+ async function fetchBlob(url, progressCallback, requestInit) {
32
+ // Fetch the resource
33
+ const response = await fetch(url, requestInit);
34
+ if (response.status >= 400) {
35
+ throw new Error(`Failed to fetch ${url}: ${response.status} (${response.statusText})`);
36
+ }
37
+ // Read the data to track progress
38
+ watchProgress(response.clone(), progressCallback);
39
+ return response.blob();
53
40
  }
54
41
  const Fetcher = {
55
42
  fetchBlob,
package/dist/player.js CHANGED
@@ -1,17 +1,9 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import EventEmitter from './event-emitter.js';
11
2
  class Player extends EventEmitter {
3
+ media;
4
+ isExternalMedia = false;
12
5
  constructor(options) {
13
6
  super();
14
- this.isExternalMedia = false;
15
7
  if (options.media) {
16
8
  this.media = options.media;
17
9
  this.isExternalMedia = true;
@@ -79,10 +71,8 @@ class Player extends EventEmitter {
79
71
  this.media = element;
80
72
  }
81
73
  /** Start playing the audio */
82
- play() {
83
- return __awaiter(this, void 0, void 0, function* () {
84
- return this.media.play();
85
- });
74
+ async play() {
75
+ return this.media.play();
86
76
  }
87
77
  /** Pause the audio */
88
78
  pause() {
@@ -1 +1,355 @@
1
- class t{constructor(){this.listeners={}}on(t,e,i){if(this.listeners[t]||(this.listeners[t]=new Set),this.listeners[t].add(e),null==i?void 0:i.once){const i=()=>{this.un(t,i),this.un(t,e)};return this.on(t,i),i}return()=>this.un(t,e)}un(t,e){var i;null===(i=this.listeners[t])||void 0===i||i.delete(e)}once(t,e){return this.on(t,e,{once:!0})}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}}class e extends t{constructor(t){super(),this.subscriptions=[],this.options=t}onInit(){}_init(t){this.wavesurfer=t,this.onInit()}destroy(){this.emit("destroy"),this.subscriptions.forEach((t=>t()))}}function i(t,e,i,o,n=3,s=0,r=100){if(!t)return()=>{};const l=matchMedia("(pointer: coarse)").matches;let a=()=>{};const h=h=>{if(h.button!==s)return;h.preventDefault(),h.stopPropagation();let u=h.clientX,c=h.clientY,d=!1;const p=Date.now(),m=o=>{if(o.preventDefault(),o.stopPropagation(),l&&Date.now()-p<r)return;const s=o.clientX,a=o.clientY,h=s-u,m=a-c;if(d||Math.abs(h)>n||Math.abs(m)>n){const o=t.getBoundingClientRect(),{left:n,top:r}=o;d||(null==i||i(u-n,c-r),d=!0),e(h,m,s-n,a-r),u=s,c=a}},v=e=>{if(d){const i=e.clientX,n=e.clientY,s=t.getBoundingClientRect(),{left:r,top:l}=s;null==o||o(i-r,n-l)}a()},g=t=>{t.relatedTarget&&t.relatedTarget!==document.documentElement||v(t)},f=t=>{d&&(t.stopPropagation(),t.preventDefault())},y=t=>{d&&t.preventDefault()};document.addEventListener("pointermove",m),document.addEventListener("pointerup",v),document.addEventListener("pointerout",g),document.addEventListener("pointercancel",g),document.addEventListener("touchmove",y,{passive:!1}),document.addEventListener("click",f,{capture:!0}),a=()=>{document.removeEventListener("pointermove",m),document.removeEventListener("pointerup",v),document.removeEventListener("pointerout",g),document.removeEventListener("pointercancel",g),document.removeEventListener("touchmove",y),setTimeout((()=>{document.removeEventListener("click",f,{capture:!0})}),10)}};return t.addEventListener("pointerdown",h),()=>{a(),t.removeEventListener("pointerdown",h)}}function o(t,e){const i=e.xmlns?document.createElementNS(e.xmlns,t):document.createElement(t);for(const[t,n]of Object.entries(e))if("children"===t)for(const[t,n]of Object.entries(e))"string"==typeof n?i.appendChild(document.createTextNode(n)):i.appendChild(o(t,n));else"style"===t?Object.assign(i.style,n):"textContent"===t?i.textContent=n:i.setAttribute(t,n.toString());return i}function n(t,e,i){const n=o(t,e||{});return null==i||i.appendChild(n),n}const s={points:[],lineWidth:4,lineColor:"rgba(0, 0, 255, 0.5)",dragPointSize:10,dragPointFill:"rgba(255, 255, 255, 0.8)",dragPointStroke:"rgba(255, 255, 255, 0.8)"};class r extends t{constructor(t,e){super(),this.subscriptions=[],this.subscriptions=[],this.options=t,this.polyPoints=new Map;const o=e.clientWidth,s=e.clientHeight,r=n("svg",{xmlns:"http://www.w3.org/2000/svg",width:"100%",height:"100%",viewBox:`0 0 ${o} ${s}`,preserveAspectRatio:"none",style:{position:"absolute",left:"0",top:"0",zIndex:"4"},part:"envelope"},e);this.svg=r;const l=n("polyline",{xmlns:"http://www.w3.org/2000/svg",points:`0,${s} ${o},${s}`,stroke:t.lineColor,"stroke-width":t.lineWidth,fill:"none",part:"polyline",style:t.dragLine?{cursor:"row-resize",pointerEvents:"stroke"}:{}},r);t.dragLine&&this.subscriptions.push(i(l,((t,e)=>{const{height:i}=r.viewBox.baseVal,{points:o}=l;for(let t=1;t<o.numberOfItems-1;t++){const n=o.getItem(t);n.y=Math.min(i,Math.max(0,n.y+e))}const n=r.querySelectorAll("ellipse");Array.from(n).forEach((t=>{const o=Math.min(i,Math.max(0,Number(t.getAttribute("cy"))+e));t.setAttribute("cy",o.toString())})),this.emit("line-move",e/i)}))),r.addEventListener("dblclick",(t=>{const e=r.getBoundingClientRect(),i=t.clientX-e.left,o=t.clientY-e.top;this.emit("point-create",i/e.width,o/e.height)}));{let t;const e=()=>clearTimeout(t);r.addEventListener("touchstart",(i=>{1===i.touches.length?t=window.setTimeout((()=>{i.preventDefault();const t=r.getBoundingClientRect(),e=i.touches[0].clientX-t.left,o=i.touches[0].clientY-t.top;this.emit("point-create",e/t.width,o/t.height)}),500):e()})),r.addEventListener("touchmove",e),r.addEventListener("touchend",e)}}makeDraggable(t,e){this.subscriptions.push(i(t,e,(()=>t.style.cursor="grabbing"),(()=>t.style.cursor="grab"),1))}createCircle(t,e){const i=this.options.dragPointSize/2;return n("ellipse",{xmlns:"http://www.w3.org/2000/svg",cx:t,cy:e,rx:i,ry:i,fill:this.options.dragPointFill,stroke:this.options.dragPointStroke,"stroke-width":"2",style:{cursor:"grab",pointerEvents:"all"},part:"envelope-circle"},this.svg)}removePolyPoint(t){const e=this.polyPoints.get(t);if(!e)return;const{polyPoint:i,circle:o}=e,{points:n}=this.svg.querySelector("polyline"),s=Array.from(n).findIndex((t=>t.x===i.x&&t.y===i.y));n.removeItem(s),o.remove(),this.polyPoints.delete(t)}addPolyPoint(t,e,i){const{svg:o}=this,{width:n,height:s}=o.viewBox.baseVal,r=t*n,l=s-e*s,a=this.options.dragPointSize/2,h=o.createSVGPoint();h.x=t*n,h.y=s-e*s;const u=this.createCircle(r,l),{points:c}=o.querySelector("polyline"),d=Array.from(c).findIndex((t=>t.x>=r));c.insertItemBefore(h,Math.max(d,1)),this.polyPoints.set(i,{polyPoint:h,circle:u}),this.makeDraggable(u,((t,e)=>{const o=h.x+t,r=h.y+e;if(o<-a||r<-a||o>n+a||r>s+a)return void this.emit("point-dragout",i);const l=Array.from(c).find((t=>t.x>h.x)),d=Array.from(c).findLast((t=>t.x<h.x));l&&o>=l.x||d&&o<=d.x||(h.x=o,h.y=r,u.setAttribute("cx",o.toString()),u.setAttribute("cy",r.toString()),this.emit("point-move",i,o/n,r/s))}))}update(){const{svg:t}=this,e=t.viewBox.baseVal.width/t.clientWidth,i=t.viewBox.baseVal.height/t.clientHeight;t.querySelectorAll("ellipse").forEach((t=>{const o=this.options.dragPointSize/2,n=o*e,s=o*i;t.setAttribute("rx",n.toString()),t.setAttribute("ry",s.toString())}))}destroy(){this.subscriptions.forEach((t=>t())),this.polyPoints.clear(),this.svg.remove()}}class l extends e{constructor(t){super(t),this.polyline=null,this.throttleTimeout=null,this.volume=1,this.points=t.points||[],this.options=Object.assign({},s,t),this.options.lineColor=this.options.lineColor||s.lineColor,this.options.dragPointFill=this.options.dragPointFill||s.dragPointFill,this.options.dragPointStroke=this.options.dragPointStroke||s.dragPointStroke,this.options.dragPointSize=this.options.dragPointSize||s.dragPointSize}static create(t){return new l(t)}addPoint(t){var e;t.id||(t.id=Math.random().toString(36).slice(2));const i=this.points.findLastIndex((e=>e.time<t.time));this.points.splice(i+1,0,t),this.emitPoints();const o=null===(e=this.wavesurfer)||void 0===e?void 0:e.getDuration();o&&this.addPolyPoint(t,o)}removePoint(t){var e;const i=this.points.indexOf(t);i>-1&&(this.points.splice(i,1),null===(e=this.polyline)||void 0===e||e.removePolyPoint(t),this.emitPoints())}getPoints(){return this.points}setPoints(t){this.points.slice().forEach((t=>this.removePoint(t))),t.forEach((t=>this.addPoint(t)))}destroy(){var t;null===(t=this.polyline)||void 0===t||t.destroy(),super.destroy()}getCurrentVolume(){return this.volume}setVolume(t){var e;this.volume=t,null===(e=this.wavesurfer)||void 0===e||e.setVolume(t)}onInit(){var t;if(!this.wavesurfer)throw Error("WaveSurfer is not initialized");const{options:e}=this;e.volume=null!==(t=e.volume)&&void 0!==t?t:this.wavesurfer.getVolume(),this.setVolume(e.volume),this.subscriptions.push(this.wavesurfer.on("decode",(t=>{this.initPolyline(),this.points.forEach((e=>{this.addPolyPoint(e,t)}))})),this.wavesurfer.on("redraw",(()=>{var t;null===(t=this.polyline)||void 0===t||t.update()})),this.wavesurfer.on("timeupdate",(t=>{this.onTimeUpdate(t)})))}emitPoints(){this.throttleTimeout&&clearTimeout(this.throttleTimeout),this.throttleTimeout=setTimeout((()=>{this.emit("points-change",this.points)}),200)}initPolyline(){if(this.polyline&&this.polyline.destroy(),!this.wavesurfer)return;const t=this.wavesurfer.getWrapper();this.polyline=new r(this.options,t),this.subscriptions.push(this.polyline.on("point-move",((t,e,i)=>{var o;const n=(null===(o=this.wavesurfer)||void 0===o?void 0:o.getDuration())||0;t.time=e*n,t.volume=1-i,this.emitPoints()})),this.polyline.on("point-dragout",(t=>{this.removePoint(t)})),this.polyline.on("point-create",((t,e)=>{var i;this.addPoint({time:t*((null===(i=this.wavesurfer)||void 0===i?void 0:i.getDuration())||0),volume:1-e})})),this.polyline.on("line-move",(t=>{var e;this.points.forEach((e=>{e.volume=Math.min(1,Math.max(0,e.volume-t))})),this.emitPoints(),this.onTimeUpdate((null===(e=this.wavesurfer)||void 0===e?void 0:e.getCurrentTime())||0)})))}addPolyPoint(t,e){var i;null===(i=this.polyline)||void 0===i||i.addPolyPoint(t.time/e,t.volume,t)}onTimeUpdate(t){if(!this.wavesurfer)return;let e=this.points.find((e=>e.time>t));e||(e={time:this.wavesurfer.getDuration()||0,volume:0});let i=this.points.findLast((e=>e.time<=t));i||(i={time:0,volume:0});const o=e.time-i.time,n=e.volume-i.volume,s=i.volume+(t-i.time)*(n/o),r=Math.min(1,Math.max(0,s)),l=Math.round(100*r)/100;l!==this.getCurrentVolume()&&(this.setVolume(l),this.emit("volume-change",l))}}export{l as default};
1
+ /**
2
+ * Envelope is a visual UI for controlling the audio volume and add fade-in and fade-out effects.
3
+ */
4
+ import BasePlugin from '../base-plugin.js';
5
+ import { makeDraggable } from '../draggable.js';
6
+ import EventEmitter from '../event-emitter.js';
7
+ import createElement from '../dom.js';
8
+ const defaultOptions = {
9
+ points: [],
10
+ lineWidth: 4,
11
+ lineColor: 'rgba(0, 0, 255, 0.5)',
12
+ dragPointSize: 10,
13
+ dragPointFill: 'rgba(255, 255, 255, 0.8)',
14
+ dragPointStroke: 'rgba(255, 255, 255, 0.8)',
15
+ };
16
+ class Polyline extends EventEmitter {
17
+ svg;
18
+ options;
19
+ polyPoints;
20
+ subscriptions = [];
21
+ constructor(options, wrapper) {
22
+ super();
23
+ this.subscriptions = [];
24
+ this.options = options;
25
+ this.polyPoints = new Map();
26
+ const width = wrapper.clientWidth;
27
+ const height = wrapper.clientHeight;
28
+ // SVG element
29
+ const svg = createElement('svg', {
30
+ xmlns: 'http://www.w3.org/2000/svg',
31
+ width: '100%',
32
+ height: '100%',
33
+ viewBox: `0 0 ${width} ${height}`,
34
+ preserveAspectRatio: 'none',
35
+ style: {
36
+ position: 'absolute',
37
+ left: '0',
38
+ top: '0',
39
+ zIndex: '4',
40
+ },
41
+ part: 'envelope',
42
+ }, wrapper);
43
+ this.svg = svg;
44
+ // A polyline representing the envelope
45
+ const polyline = createElement('polyline', {
46
+ xmlns: 'http://www.w3.org/2000/svg',
47
+ points: `0,${height} ${width},${height}`,
48
+ stroke: options.lineColor,
49
+ 'stroke-width': options.lineWidth,
50
+ fill: 'none',
51
+ part: 'polyline',
52
+ style: options.dragLine
53
+ ? {
54
+ cursor: 'row-resize',
55
+ pointerEvents: 'stroke',
56
+ }
57
+ : {},
58
+ }, svg);
59
+ // Make the polyline draggable along the Y axis
60
+ if (options.dragLine) {
61
+ this.subscriptions.push(makeDraggable(polyline, (_, dy) => {
62
+ const { height } = svg.viewBox.baseVal;
63
+ const { points } = polyline;
64
+ for (let i = 1; i < points.numberOfItems - 1; i++) {
65
+ const point = points.getItem(i);
66
+ point.y = Math.min(height, Math.max(0, point.y + dy));
67
+ }
68
+ const circles = svg.querySelectorAll('ellipse');
69
+ Array.from(circles).forEach((circle) => {
70
+ const newY = Math.min(height, Math.max(0, Number(circle.getAttribute('cy')) + dy));
71
+ circle.setAttribute('cy', newY.toString());
72
+ });
73
+ this.emit('line-move', dy / height);
74
+ }));
75
+ }
76
+ // Listen to double click to add a new point
77
+ svg.addEventListener('dblclick', (e) => {
78
+ const rect = svg.getBoundingClientRect();
79
+ const x = e.clientX - rect.left;
80
+ const y = e.clientY - rect.top;
81
+ this.emit('point-create', x / rect.width, y / rect.height);
82
+ });
83
+ // Long press on touch devices
84
+ {
85
+ let pressTimer;
86
+ const clearTimer = () => clearTimeout(pressTimer);
87
+ svg.addEventListener('touchstart', (e) => {
88
+ if (e.touches.length === 1) {
89
+ pressTimer = window.setTimeout(() => {
90
+ e.preventDefault();
91
+ const rect = svg.getBoundingClientRect();
92
+ const x = e.touches[0].clientX - rect.left;
93
+ const y = e.touches[0].clientY - rect.top;
94
+ this.emit('point-create', x / rect.width, y / rect.height);
95
+ }, 500);
96
+ }
97
+ else {
98
+ clearTimer();
99
+ }
100
+ });
101
+ svg.addEventListener('touchmove', clearTimer);
102
+ svg.addEventListener('touchend', clearTimer);
103
+ }
104
+ }
105
+ makeDraggable(draggable, onDrag) {
106
+ this.subscriptions.push(makeDraggable(draggable, onDrag, () => (draggable.style.cursor = 'grabbing'), () => (draggable.style.cursor = 'grab'), 1));
107
+ }
108
+ createCircle(x, y) {
109
+ const size = this.options.dragPointSize;
110
+ const radius = size / 2;
111
+ return createElement('ellipse', {
112
+ xmlns: 'http://www.w3.org/2000/svg',
113
+ cx: x,
114
+ cy: y,
115
+ rx: radius,
116
+ ry: radius,
117
+ fill: this.options.dragPointFill,
118
+ stroke: this.options.dragPointStroke,
119
+ 'stroke-width': '2',
120
+ style: {
121
+ cursor: 'grab',
122
+ pointerEvents: 'all',
123
+ },
124
+ part: 'envelope-circle',
125
+ }, this.svg);
126
+ }
127
+ removePolyPoint(point) {
128
+ const item = this.polyPoints.get(point);
129
+ if (!item)
130
+ return;
131
+ const { polyPoint, circle } = item;
132
+ const { points } = this.svg.querySelector('polyline');
133
+ const index = Array.from(points).findIndex((p) => p.x === polyPoint.x && p.y === polyPoint.y);
134
+ points.removeItem(index);
135
+ circle.remove();
136
+ this.polyPoints.delete(point);
137
+ }
138
+ addPolyPoint(relX, relY, refPoint) {
139
+ const { svg } = this;
140
+ const { width, height } = svg.viewBox.baseVal;
141
+ const x = relX * width;
142
+ const y = height - relY * height;
143
+ const threshold = this.options.dragPointSize / 2;
144
+ const newPoint = svg.createSVGPoint();
145
+ newPoint.x = relX * width;
146
+ newPoint.y = height - relY * height;
147
+ const circle = this.createCircle(x, y);
148
+ const { points } = svg.querySelector('polyline');
149
+ const newIndex = Array.from(points).findIndex((point) => point.x >= x);
150
+ points.insertItemBefore(newPoint, Math.max(newIndex, 1));
151
+ this.polyPoints.set(refPoint, { polyPoint: newPoint, circle });
152
+ this.makeDraggable(circle, (dx, dy) => {
153
+ const newX = newPoint.x + dx;
154
+ const newY = newPoint.y + dy;
155
+ // Remove the point if it's dragged out of the SVG
156
+ if (newX < -threshold || newY < -threshold || newX > width + threshold || newY > height + threshold) {
157
+ this.emit('point-dragout', refPoint);
158
+ return;
159
+ }
160
+ // Don't allow to drag past the next or previous point
161
+ const next = Array.from(points).find((point) => point.x > newPoint.x);
162
+ const prev = Array.from(points).findLast((point) => point.x < newPoint.x);
163
+ if ((next && newX >= next.x) || (prev && newX <= prev.x)) {
164
+ return;
165
+ }
166
+ // Update the point and the circle position
167
+ newPoint.x = newX;
168
+ newPoint.y = newY;
169
+ circle.setAttribute('cx', newX.toString());
170
+ circle.setAttribute('cy', newY.toString());
171
+ // Emit the event passing the point and new relative coordinates
172
+ this.emit('point-move', refPoint, newX / width, newY / height);
173
+ });
174
+ }
175
+ update() {
176
+ const { svg } = this;
177
+ const aspectRatioX = svg.viewBox.baseVal.width / svg.clientWidth;
178
+ const aspectRatioY = svg.viewBox.baseVal.height / svg.clientHeight;
179
+ const circles = svg.querySelectorAll('ellipse');
180
+ circles.forEach((circle) => {
181
+ const radius = this.options.dragPointSize / 2;
182
+ const rx = radius * aspectRatioX;
183
+ const ry = radius * aspectRatioY;
184
+ circle.setAttribute('rx', rx.toString());
185
+ circle.setAttribute('ry', ry.toString());
186
+ });
187
+ }
188
+ destroy() {
189
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
190
+ this.polyPoints.clear();
191
+ this.svg.remove();
192
+ }
193
+ }
194
+ const randomId = () => Math.random().toString(36).slice(2);
195
+ class EnvelopePlugin extends BasePlugin {
196
+ options;
197
+ polyline = null;
198
+ points;
199
+ throttleTimeout = null;
200
+ volume = 1;
201
+ /**
202
+ * Create a new Envelope plugin.
203
+ */
204
+ constructor(options) {
205
+ super(options);
206
+ this.points = options.points || [];
207
+ this.options = Object.assign({}, defaultOptions, options);
208
+ this.options.lineColor = this.options.lineColor || defaultOptions.lineColor;
209
+ this.options.dragPointFill = this.options.dragPointFill || defaultOptions.dragPointFill;
210
+ this.options.dragPointStroke = this.options.dragPointStroke || defaultOptions.dragPointStroke;
211
+ this.options.dragPointSize = this.options.dragPointSize || defaultOptions.dragPointSize;
212
+ }
213
+ static create(options) {
214
+ return new EnvelopePlugin(options);
215
+ }
216
+ /**
217
+ * Add an envelope point with a given time and volume.
218
+ */
219
+ addPoint(point) {
220
+ if (!point.id)
221
+ point.id = randomId();
222
+ // Insert the point in the correct position to keep the array sorted
223
+ const index = this.points.findLastIndex((p) => p.time < point.time);
224
+ this.points.splice(index + 1, 0, point);
225
+ this.emitPoints();
226
+ // Add the point to the polyline if the duration is available
227
+ const duration = this.wavesurfer?.getDuration();
228
+ if (duration) {
229
+ this.addPolyPoint(point, duration);
230
+ }
231
+ }
232
+ /**
233
+ * Remove an envelope point.
234
+ */
235
+ removePoint(point) {
236
+ const index = this.points.indexOf(point);
237
+ if (index > -1) {
238
+ this.points.splice(index, 1);
239
+ this.polyline?.removePolyPoint(point);
240
+ this.emitPoints();
241
+ }
242
+ }
243
+ /**
244
+ * Get all envelope points. Should not be modified directly.
245
+ */
246
+ getPoints() {
247
+ return this.points;
248
+ }
249
+ /**
250
+ * Set new envelope points.
251
+ */
252
+ setPoints(newPoints) {
253
+ this.points.slice().forEach((point) => this.removePoint(point));
254
+ newPoints.forEach((point) => this.addPoint(point));
255
+ }
256
+ /**
257
+ * Destroy the plugin instance.
258
+ */
259
+ destroy() {
260
+ this.polyline?.destroy();
261
+ super.destroy();
262
+ }
263
+ /**
264
+ * Get the envelope volume.
265
+ */
266
+ getCurrentVolume() {
267
+ return this.volume;
268
+ }
269
+ /**
270
+ * Set the envelope volume. 0..1 (more than 1 will boost the volume).
271
+ */
272
+ setVolume(floatValue) {
273
+ this.volume = floatValue;
274
+ this.wavesurfer?.setVolume(floatValue);
275
+ }
276
+ /** Called by wavesurfer, don't call manually */
277
+ onInit() {
278
+ if (!this.wavesurfer) {
279
+ throw Error('WaveSurfer is not initialized');
280
+ }
281
+ const { options } = this;
282
+ options.volume = options.volume ?? this.wavesurfer.getVolume();
283
+ this.setVolume(options.volume);
284
+ this.subscriptions.push(this.wavesurfer.on('decode', (duration) => {
285
+ this.initPolyline();
286
+ this.points.forEach((point) => {
287
+ this.addPolyPoint(point, duration);
288
+ });
289
+ }), this.wavesurfer.on('redraw', () => {
290
+ this.polyline?.update();
291
+ }), this.wavesurfer.on('timeupdate', (time) => {
292
+ this.onTimeUpdate(time);
293
+ }));
294
+ }
295
+ emitPoints() {
296
+ if (this.throttleTimeout) {
297
+ clearTimeout(this.throttleTimeout);
298
+ }
299
+ this.throttleTimeout = setTimeout(() => {
300
+ this.emit('points-change', this.points);
301
+ }, 200);
302
+ }
303
+ initPolyline() {
304
+ if (this.polyline)
305
+ this.polyline.destroy();
306
+ if (!this.wavesurfer)
307
+ return;
308
+ const wrapper = this.wavesurfer.getWrapper();
309
+ this.polyline = new Polyline(this.options, wrapper);
310
+ this.subscriptions.push(this.polyline.on('point-move', (point, relativeX, relativeY) => {
311
+ const duration = this.wavesurfer?.getDuration() || 0;
312
+ point.time = relativeX * duration;
313
+ point.volume = 1 - relativeY;
314
+ this.emitPoints();
315
+ }), this.polyline.on('point-dragout', (point) => {
316
+ this.removePoint(point);
317
+ }), this.polyline.on('point-create', (relativeX, relativeY) => {
318
+ this.addPoint({
319
+ time: relativeX * (this.wavesurfer?.getDuration() || 0),
320
+ volume: 1 - relativeY,
321
+ });
322
+ }), this.polyline.on('line-move', (relativeY) => {
323
+ this.points.forEach((point) => {
324
+ point.volume = Math.min(1, Math.max(0, point.volume - relativeY));
325
+ });
326
+ this.emitPoints();
327
+ this.onTimeUpdate(this.wavesurfer?.getCurrentTime() || 0);
328
+ }));
329
+ }
330
+ addPolyPoint(point, duration) {
331
+ this.polyline?.addPolyPoint(point.time / duration, point.volume, point);
332
+ }
333
+ onTimeUpdate(time) {
334
+ if (!this.wavesurfer)
335
+ return;
336
+ let nextPoint = this.points.find((point) => point.time > time);
337
+ if (!nextPoint) {
338
+ nextPoint = { time: this.wavesurfer.getDuration() || 0, volume: 0 };
339
+ }
340
+ let prevPoint = this.points.findLast((point) => point.time <= time);
341
+ if (!prevPoint) {
342
+ prevPoint = { time: 0, volume: 0 };
343
+ }
344
+ const timeDiff = nextPoint.time - prevPoint.time;
345
+ const volumeDiff = nextPoint.volume - prevPoint.volume;
346
+ const newVolume = prevPoint.volume + (time - prevPoint.time) * (volumeDiff / timeDiff);
347
+ const clampedVolume = Math.min(1, Math.max(0, newVolume));
348
+ const roundedVolume = Math.round(clampedVolume * 100) / 100;
349
+ if (roundedVolume !== this.getCurrentVolume()) {
350
+ this.setVolume(roundedVolume);
351
+ this.emit('volume-change', roundedVolume);
352
+ }
353
+ }
354
+ }
355
+ export default EnvelopePlugin;