wavesurfer.js 7.12.2 → 7.12.3

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.
Files changed (37) hide show
  1. package/dist/__tests__/regions.test.d.ts +1 -0
  2. package/dist/__tests__/regions.test.js +61 -0
  3. package/dist/__tests__/timeline.test.d.ts +1 -0
  4. package/dist/__tests__/timeline.test.js +46 -0
  5. package/dist/plugins/envelope.js +426 -1
  6. package/dist/plugins/hover.js +131 -1
  7. package/dist/plugins/minimap.js +229 -1
  8. package/dist/plugins/record.js +319 -1
  9. package/dist/plugins/regions.d.ts +2 -0
  10. package/dist/plugins/regions.js +699 -1
  11. package/dist/plugins/spectrogram-windowed.js +967 -1
  12. package/dist/plugins/timeline.cjs +1 -1
  13. package/dist/plugins/timeline.esm.js +1 -1
  14. package/dist/plugins/timeline.js +1 -1
  15. package/dist/plugins/timeline.min.js +1 -1
  16. package/package.json +2 -4
  17. package/dist/plugins/envelope.cjs +0 -1
  18. package/dist/plugins/envelope.esm.js +0 -1
  19. package/dist/plugins/envelope.min.js +0 -1
  20. package/dist/plugins/hover.cjs +0 -1
  21. package/dist/plugins/hover.esm.js +0 -1
  22. package/dist/plugins/hover.min.js +0 -1
  23. package/dist/plugins/minimap.cjs +0 -1
  24. package/dist/plugins/minimap.esm.js +0 -1
  25. package/dist/plugins/minimap.min.js +0 -1
  26. package/dist/plugins/record.cjs +0 -1
  27. package/dist/plugins/record.esm.js +0 -1
  28. package/dist/plugins/record.min.js +0 -1
  29. package/dist/plugins/regions.cjs +0 -1
  30. package/dist/plugins/regions.esm.js +0 -1
  31. package/dist/plugins/regions.min.js +0 -1
  32. package/dist/plugins/spectrogram-windowed.cjs +0 -1
  33. package/dist/plugins/spectrogram-windowed.esm.js +0 -1
  34. package/dist/plugins/spectrogram-windowed.min.js +0 -1
  35. package/dist/plugins/spectrogram.cjs +0 -1
  36. package/dist/plugins/spectrogram.esm.js +0 -1
  37. package/dist/plugins/spectrogram.min.js +0 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import RegionsPlugin from '../plugins/regions.js';
2
+ const createEmitter = () => {
3
+ const listeners = new Map();
4
+ return {
5
+ on: jest.fn((event, listener) => {
6
+ if (!listeners.has(event)) {
7
+ listeners.set(event, new Set());
8
+ }
9
+ listeners.get(event).add(listener);
10
+ return () => { var _a; return (_a = listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(listener); };
11
+ }),
12
+ };
13
+ };
14
+ const createWaveSurfer = (duration = 10, width = 100, scroll = 0) => {
15
+ const emitter = createEmitter();
16
+ const wrapper = document.createElement('div');
17
+ document.body.appendChild(wrapper);
18
+ return Object.assign(Object.assign({}, emitter), { getDecodedData: jest.fn(() => ({ numberOfChannels: 1 })), getDuration: jest.fn(() => duration), getScroll: jest.fn(() => scroll), getWidth: jest.fn(() => width), getWrapper: jest.fn(() => wrapper) });
19
+ };
20
+ describe('RegionsPlugin', () => {
21
+ beforeEach(() => {
22
+ jest.useFakeTimers();
23
+ Object.defineProperty(window, 'matchMedia', {
24
+ writable: true,
25
+ value: jest.fn().mockImplementation((query) => ({
26
+ matches: false,
27
+ media: query,
28
+ onchange: null,
29
+ addListener: jest.fn(),
30
+ removeListener: jest.fn(),
31
+ addEventListener: jest.fn(),
32
+ removeEventListener: jest.fn(),
33
+ dispatchEvent: jest.fn(),
34
+ })),
35
+ });
36
+ });
37
+ afterEach(() => {
38
+ jest.runOnlyPendingTimers();
39
+ jest.useRealTimers();
40
+ document.body.innerHTML = '';
41
+ jest.clearAllMocks();
42
+ });
43
+ test('re-renders a lazily detached region when setOptions moves it into view', () => {
44
+ var _a, _b, _c, _d, _e;
45
+ const wavesurfer = createWaveSurfer();
46
+ const plugin = RegionsPlugin.create();
47
+ plugin._init(wavesurfer);
48
+ const regionsContainer = wavesurfer.getWrapper().querySelector('[part="regions-container"]');
49
+ expect(regionsContainer).toBeTruthy();
50
+ Object.defineProperty(regionsContainer, 'clientWidth', { configurable: true, value: 1000 });
51
+ const region = plugin.addRegion({ start: 8, end: 9 });
52
+ jest.runOnlyPendingTimers();
53
+ expect((_a = region.element) === null || _a === void 0 ? void 0 : _a.parentElement).toBeNull();
54
+ region.setOptions({ start: 0.5, end: 1.5 });
55
+ expect((_b = region.element) === null || _b === void 0 ? void 0 : _b.parentElement).toBe(regionsContainer);
56
+ expect((_c = region.element) === null || _c === void 0 ? void 0 : _c.style.left).toBe('5%');
57
+ expect((_d = region.element) === null || _d === void 0 ? void 0 : _d.style.right).toBe('85%');
58
+ region.setOptions({ start: 8, end: 9 });
59
+ expect((_e = region.element) === null || _e === void 0 ? void 0 : _e.parentElement).toBeNull();
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import TimelinePlugin from '../plugins/timeline.js';
2
+ import { signal } from '../reactive/store.js';
3
+ const createEmitter = () => {
4
+ const listeners = new Map();
5
+ return {
6
+ on: jest.fn((event, listener) => {
7
+ if (!listeners.has(event)) {
8
+ listeners.set(event, new Set());
9
+ }
10
+ listeners.get(event).add(listener);
11
+ return () => { var _a; return (_a = listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(listener); };
12
+ }),
13
+ };
14
+ };
15
+ const createWaveSurfer = (duration = 1, scrollWidth = 100) => {
16
+ const emitter = createEmitter();
17
+ const durationSignal = signal(duration);
18
+ const wrapper = document.createElement('div');
19
+ Object.defineProperty(wrapper, 'scrollWidth', { configurable: true, value: scrollWidth });
20
+ document.body.appendChild(wrapper);
21
+ return Object.assign(Object.assign({}, emitter), { getDuration: jest.fn(() => duration), getScroll: jest.fn(() => 0), getState: jest.fn(() => ({ duration: durationSignal })), getWidth: jest.fn(() => scrollWidth * 2), getWrapper: jest.fn(() => wrapper) });
22
+ };
23
+ describe('TimelinePlugin', () => {
24
+ afterEach(() => {
25
+ document.body.innerHTML = '';
26
+ jest.clearAllMocks();
27
+ });
28
+ test('preserves high precision offsets for notch positions', () => {
29
+ const wavesurfer = createWaveSurfer(1, 100);
30
+ const plugin = TimelinePlugin.create({
31
+ duration: 1,
32
+ timeInterval: 0.333,
33
+ timeOffset: 0.001,
34
+ primaryLabelInterval: 10,
35
+ secondaryLabelInterval: 10,
36
+ });
37
+ plugin._init(wavesurfer);
38
+ const notches = wavesurfer.getWrapper().querySelectorAll('[part^="timeline-notch"]');
39
+ expect(notches).toHaveLength(4);
40
+ const offsets = Array.from(notches, (notch) => parseFloat(notch.style.left));
41
+ expect(offsets[0]).toBeCloseTo(0.1);
42
+ expect(offsets[1]).toBeCloseTo(33.4);
43
+ expect(offsets[2]).toBeCloseTo(66.7);
44
+ expect(offsets[3]).toBeCloseTo(100);
45
+ });
46
+ });
@@ -1 +1,426 @@
1
- class t{constructor(){this.listeners={}}on(t,e,i){if(this.listeners[t]||(this.listeners[t]=new Set),null==i?void 0:i.once){const i=(...s)=>{this.un(t,i),e(...s)};return this.listeners[t].add(i),()=>this.un(t,i)}return this.listeners[t].add(e),()=>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.isDestroyed=!1,this.options=t}onInit(){}_init(t){this.isDestroyed&&(this.subscriptions=[],this.isDestroyed=!1),this.wavesurfer=t,this.onInit()}destroy(){this.emit("destroy"),this.subscriptions.forEach((t=>t())),this.subscriptions=[],this.isDestroyed=!0,this.wavesurfer=void 0}}function i(t,e){const s=e.xmlns?document.createElementNS(e.xmlns,t):document.createElement(t);for(const[t,o]of Object.entries(e))if("children"===t&&o)for(const[t,e]of Object.entries(o))e instanceof Node?s.appendChild(e):"string"==typeof e?s.appendChild(document.createTextNode(e)):s.appendChild(i(t,e));else"style"===t?Object.assign(s.style,o):"textContent"===t?s.textContent=o:s.setAttribute(t,o.toString());return s}function s(t,e,s){const o=i(t,e||{});return null==s||s.appendChild(o),o}function o(t,e){let i;const s=()=>{i&&(i(),i=void 0),i=t()},o=e.map((t=>t.subscribe(s)));return s(),()=>{i&&(i(),i=void 0),o.forEach((t=>t()))}}function n(t,e={}){const{threshold:i=3,mouseButton:s=0,touchDelay:o=100}=e,n=function(t){let e=t;const i=new Set;return{get value(){return e},set(t){Object.is(e,t)||(e=t,i.forEach((t=>t(e))))},update(t){this.set(t(e))},subscribe:t=>(i.add(t),()=>i.delete(t))}}(null),r=new Map,l=matchMedia("(pointer: coarse)").matches;let h=()=>{};const a=e=>{if(e.button!==s)return;if(r.set(e.pointerId,e),r.size>1)return;let a=e.clientX,u=e.clientY,c=!1;const d=Date.now(),p=t.getBoundingClientRect(),{left:v,top:m}=p,g=t=>{if(t.defaultPrevented||r.size>1)return;if(l&&Date.now()-d<o)return;const e=t.clientX,s=t.clientY,h=e-a,p=s-u;(c||Math.abs(h)>i||Math.abs(p)>i)&&(t.preventDefault(),t.stopPropagation(),c||(n.set({type:"start",x:a-v,y:u-m}),c=!0),n.set({type:"move",x:e-v,y:s-m,deltaX:h,deltaY:p}),a=e,u=s)},f=t=>{if(r.delete(t.pointerId),c){const e=t.clientX,i=t.clientY;n.set({type:"end",x:e-v,y:i-m})}h()},y=t=>{r.delete(t.pointerId),t.relatedTarget&&t.relatedTarget!==document.documentElement||f(t)},P=t=>{c&&(t.stopPropagation(),t.preventDefault())},w=t=>{t.defaultPrevented||r.size>1||c&&t.preventDefault()};document.addEventListener("pointermove",g),document.addEventListener("pointerup",f),document.addEventListener("pointerout",y),document.addEventListener("pointercancel",y),document.addEventListener("touchmove",w,{passive:!1}),document.addEventListener("click",P,{capture:!0}),h=()=>{document.removeEventListener("pointermove",g),document.removeEventListener("pointerup",f),document.removeEventListener("pointerout",y),document.removeEventListener("pointercancel",y),document.removeEventListener("touchmove",w),setTimeout((()=>{document.removeEventListener("click",P,{capture:!0})}),10)}};t.addEventListener("pointerdown",a);return{signal:n,cleanup:()=>{h(),t.removeEventListener("pointerdown",a),r.clear(),function(t){const e=t._cleanup;"function"==typeof e&&e()}(n)}}}const r={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 l extends t{constructor(t,e){super(),this.subscriptions=[],this.subscriptions=[],this.options=t,this.polyPoints=new Map;const i=e.clientWidth,r=e.clientHeight,l=s("svg",{xmlns:"http://www.w3.org/2000/svg",width:"100%",height:"100%",viewBox:`0 0 ${i} ${r}`,preserveAspectRatio:"none",style:{position:"absolute",left:"0",top:"0",zIndex:"4"},part:"envelope"},e);this.svg=l;const h=s("polyline",{xmlns:"http://www.w3.org/2000/svg",points:`0,${r} ${i},${r}`,stroke:t.lineColor,"stroke-width":t.lineWidth,fill:"none",part:"polyline",style:t.dragLine?{cursor:"row-resize",pointerEvents:"stroke"}:{}},l);if(t.dragLine){const t=n(h),e=o((()=>{const e=t.signal.value;if(!e||"move"!==e.type||void 0===e.deltaY)return;const i=e.deltaY,{height:s}=l.viewBox.baseVal,{points:o}=h;for(let t=1;t<o.numberOfItems-1;t++){const e=o.getItem(t);e.y=Math.min(s,Math.max(0,e.y+i))}const n=l.querySelectorAll("ellipse");Array.from(n).forEach((t=>{const e=Math.min(s,Math.max(0,Number(t.getAttribute("cy"))+i));t.setAttribute("cy",e.toString())})),this.emit("line-move",i/s)}),[t.signal]);this.subscriptions.push((()=>{e(),t.cleanup()}))}this.dblClickListener=t=>{const e=l.getBoundingClientRect(),i=t.clientX-e.left,s=t.clientY-e.top;this.emit("point-create",i/e.width,s/e.height)},l.addEventListener("dblclick",this.dblClickListener);const a=()=>{void 0!==this.pressTimer&&(clearTimeout(this.pressTimer),this.pressTimer=void 0)};this.touchStartListener=t=>{1===t.touches.length?this.pressTimer=window.setTimeout((()=>{t.preventDefault();const e=l.getBoundingClientRect(),i=t.touches[0].clientX-e.left,s=t.touches[0].clientY-e.top;this.emit("point-create",i/e.width,s/e.height)}),500):a()},this.touchMoveListener=a,this.touchEndListener=a,l.addEventListener("touchstart",this.touchStartListener),l.addEventListener("touchmove",this.touchMoveListener),l.addEventListener("touchend",this.touchEndListener)}makeDraggable(t,e){const i=n(t,{threshold:1}),s=o((()=>{const s=i.signal.value;s&&("start"===s.type?t.style.cursor="grabbing":"move"===s.type&&void 0!==s.deltaX&&void 0!==s.deltaY?e(s.deltaX,s.deltaY):"end"===s.type&&(t.style.cursor="grab"))}),[i.signal]);this.subscriptions.push((()=>{s(),i.cleanup()}))}createCircle(t,e){const i=this.options.dragPointSize/2;return s("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:s}=e,{points:o}=this.svg.querySelector("polyline"),n=Array.from(o).findIndex((t=>t.x===i.x&&t.y===i.y));o.removeItem(n),s.remove(),this.polyPoints.delete(t)}addPolyPoint(t,e,i){const{svg:s}=this,{width:o,height:n}=s.viewBox.baseVal,r=t*o,l=n-e*n,h=this.options.dragPointSize/2,a=s.createSVGPoint();a.x=t*o,a.y=n-e*n;const u=this.createCircle(r,l),{points:c}=s.querySelector("polyline"),d=Array.from(c).findIndex((t=>t.x>=r));c.insertItemBefore(a,Math.max(d,1)),this.polyPoints.set(i,{polyPoint:a,circle:u}),this.makeDraggable(u,((t,e)=>{const s=a.x+t,r=a.y+e;if(s<-h||r<-h||s>o+h||r>n+h)return void this.emit("point-dragout",i);const l=Array.from(c).find((t=>t.x>a.x)),d=Array.from(c).findLast((t=>t.x<a.x));l&&s>=l.x||d&&s<=d.x||(a.x=s,a.y=r,u.setAttribute("cx",s.toString()),u.setAttribute("cy",r.toString()),this.emit("point-move",i,s/o,r/n))}))}update(){const{svg:t}=this,{clientWidth:e,clientHeight:i}=t;if(!e||!i)return;const s=t.viewBox.baseVal.width/e,o=t.viewBox.baseVal.height/i;t.querySelectorAll("ellipse").forEach((t=>{const e=this.options.dragPointSize/2,i=e*s,n=e*o;t.setAttribute("rx",i.toString()),t.setAttribute("ry",n.toString())}))}destroy(){void 0!==this.pressTimer&&(clearTimeout(this.pressTimer),this.pressTimer=void 0),this.dblClickListener&&(this.svg.removeEventListener("dblclick",this.dblClickListener),this.dblClickListener=void 0),this.touchStartListener&&(this.svg.removeEventListener("touchstart",this.touchStartListener),this.touchStartListener=void 0),this.touchMoveListener&&(this.svg.removeEventListener("touchmove",this.touchMoveListener),this.touchMoveListener=void 0),this.touchEndListener&&(this.svg.removeEventListener("touchend",this.touchEndListener),this.touchEndListener=void 0),this.subscriptions.forEach((t=>t())),this.polyPoints.clear(),this.svg.remove()}}class h extends e{constructor(t){super(t),this.polyline=null,this.throttleTimeout=null,this.volume=1,this.points=t.points||[],this.options=Object.assign({},r,t),this.options.lineColor=this.options.lineColor||r.lineColor,this.options.dragPointFill=this.options.dragPointFill||r.dragPointFill,this.options.dragPointStroke=this.options.dragPointStroke||r.dragPointStroke,this.options.dragPointSize=this.options.dragPointSize||r.dragPointSize}static create(t){return new h(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 s=null===(e=this.wavesurfer)||void 0===e?void 0:e.getDuration();s&&this.addPolyPoint(t,s)}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;this.throttleTimeout&&(clearTimeout(this.throttleTimeout),this.throttleTimeout=null),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 l(this.options,t),this.subscriptions.push(this.polyline.on("point-move",((t,e,i)=>{var s;const o=(null===(s=this.wavesurfer)||void 0===s?void 0:s.getDuration())||0;t.time=e*o,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 s=e.time-i.time,o=e.volume-i.volume,n=i.volume+(t-i.time)*(o/s),r=Math.min(1,Math.max(0,n)),l=Math.round(100*r)/100;l!==this.getCurrentVolume()&&(this.setVolume(l),this.emit("volume-change",l))}}export{h 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 EventEmitter from '../event-emitter.js';
6
+ import createElement from '../dom.js';
7
+ import { createDragStream } from '../reactive/drag-stream.js';
8
+ import { effect } from '../reactive/store.js';
9
+ const defaultOptions = {
10
+ points: [],
11
+ lineWidth: 4,
12
+ lineColor: 'rgba(0, 0, 255, 0.5)',
13
+ dragPointSize: 10,
14
+ dragPointFill: 'rgba(255, 255, 255, 0.8)',
15
+ dragPointStroke: 'rgba(255, 255, 255, 0.8)',
16
+ };
17
+ class Polyline extends EventEmitter {
18
+ constructor(options, wrapper) {
19
+ super();
20
+ this.subscriptions = [];
21
+ this.subscriptions = [];
22
+ this.options = options;
23
+ this.polyPoints = new Map();
24
+ const width = wrapper.clientWidth;
25
+ const height = wrapper.clientHeight;
26
+ // SVG element
27
+ const svg = createElement('svg', {
28
+ xmlns: 'http://www.w3.org/2000/svg',
29
+ width: '100%',
30
+ height: '100%',
31
+ viewBox: `0 0 ${width} ${height}`,
32
+ preserveAspectRatio: 'none',
33
+ style: {
34
+ position: 'absolute',
35
+ left: '0',
36
+ top: '0',
37
+ zIndex: '4',
38
+ },
39
+ part: 'envelope',
40
+ }, wrapper);
41
+ this.svg = svg;
42
+ // A polyline representing the envelope
43
+ const polyline = createElement('polyline', {
44
+ xmlns: 'http://www.w3.org/2000/svg',
45
+ points: `0,${height} ${width},${height}`,
46
+ stroke: options.lineColor,
47
+ 'stroke-width': options.lineWidth,
48
+ fill: 'none',
49
+ part: 'polyline',
50
+ style: options.dragLine
51
+ ? {
52
+ cursor: 'row-resize',
53
+ pointerEvents: 'stroke',
54
+ }
55
+ : {},
56
+ }, svg);
57
+ // Make the polyline draggable along the Y axis
58
+ if (options.dragLine) {
59
+ const dragStream = createDragStream(polyline);
60
+ const unsubscribe = effect(() => {
61
+ const drag = dragStream.signal.value;
62
+ if (!drag || drag.type !== 'move' || drag.deltaY === undefined)
63
+ return;
64
+ const deltaY = drag.deltaY;
65
+ const { height } = svg.viewBox.baseVal;
66
+ const { points } = polyline;
67
+ for (let i = 1; i < points.numberOfItems - 1; i++) {
68
+ const point = points.getItem(i);
69
+ point.y = Math.min(height, Math.max(0, point.y + deltaY));
70
+ }
71
+ const circles = svg.querySelectorAll('ellipse');
72
+ Array.from(circles).forEach((circle) => {
73
+ const newY = Math.min(height, Math.max(0, Number(circle.getAttribute('cy')) + deltaY));
74
+ circle.setAttribute('cy', newY.toString());
75
+ });
76
+ this.emit('line-move', deltaY / height);
77
+ }, [dragStream.signal]);
78
+ this.subscriptions.push(() => {
79
+ unsubscribe();
80
+ dragStream.cleanup();
81
+ });
82
+ }
83
+ // Listen to double click to add a new point
84
+ this.dblClickListener = (e) => {
85
+ const rect = svg.getBoundingClientRect();
86
+ const x = e.clientX - rect.left;
87
+ const y = e.clientY - rect.top;
88
+ this.emit('point-create', x / rect.width, y / rect.height);
89
+ };
90
+ svg.addEventListener('dblclick', this.dblClickListener);
91
+ // Long press on touch devices
92
+ const clearTimer = () => {
93
+ if (this.pressTimer !== undefined) {
94
+ clearTimeout(this.pressTimer);
95
+ this.pressTimer = undefined;
96
+ }
97
+ };
98
+ this.touchStartListener = (e) => {
99
+ if (e.touches.length === 1) {
100
+ this.pressTimer = window.setTimeout(() => {
101
+ e.preventDefault();
102
+ const rect = svg.getBoundingClientRect();
103
+ const x = e.touches[0].clientX - rect.left;
104
+ const y = e.touches[0].clientY - rect.top;
105
+ this.emit('point-create', x / rect.width, y / rect.height);
106
+ }, 500);
107
+ }
108
+ else {
109
+ clearTimer();
110
+ }
111
+ };
112
+ this.touchMoveListener = clearTimer;
113
+ this.touchEndListener = clearTimer;
114
+ svg.addEventListener('touchstart', this.touchStartListener);
115
+ svg.addEventListener('touchmove', this.touchMoveListener);
116
+ svg.addEventListener('touchend', this.touchEndListener);
117
+ }
118
+ makeDraggable(draggable, onDrag) {
119
+ const dragStream = createDragStream(draggable, { threshold: 1 });
120
+ const unsubscribe = effect(() => {
121
+ const drag = dragStream.signal.value;
122
+ if (!drag)
123
+ return;
124
+ if (drag.type === 'start') {
125
+ draggable.style.cursor = 'grabbing';
126
+ }
127
+ else if (drag.type === 'move' && drag.deltaX !== undefined && drag.deltaY !== undefined) {
128
+ onDrag(drag.deltaX, drag.deltaY);
129
+ }
130
+ else if (drag.type === 'end') {
131
+ draggable.style.cursor = 'grab';
132
+ }
133
+ }, [dragStream.signal]);
134
+ this.subscriptions.push(() => {
135
+ unsubscribe();
136
+ dragStream.cleanup();
137
+ });
138
+ }
139
+ createCircle(x, y) {
140
+ const size = this.options.dragPointSize;
141
+ const radius = size / 2;
142
+ return createElement('ellipse', {
143
+ xmlns: 'http://www.w3.org/2000/svg',
144
+ cx: x,
145
+ cy: y,
146
+ rx: radius,
147
+ ry: radius,
148
+ fill: this.options.dragPointFill,
149
+ stroke: this.options.dragPointStroke,
150
+ 'stroke-width': '2',
151
+ style: {
152
+ cursor: 'grab',
153
+ pointerEvents: 'all',
154
+ },
155
+ part: 'envelope-circle',
156
+ }, this.svg);
157
+ }
158
+ removePolyPoint(point) {
159
+ const item = this.polyPoints.get(point);
160
+ if (!item)
161
+ return;
162
+ const { polyPoint, circle } = item;
163
+ const { points } = this.svg.querySelector('polyline');
164
+ const index = Array.from(points).findIndex((p) => p.x === polyPoint.x && p.y === polyPoint.y);
165
+ points.removeItem(index);
166
+ circle.remove();
167
+ this.polyPoints.delete(point);
168
+ }
169
+ addPolyPoint(relX, relY, refPoint) {
170
+ const { svg } = this;
171
+ const { width, height } = svg.viewBox.baseVal;
172
+ const x = relX * width;
173
+ const y = height - relY * height;
174
+ const threshold = this.options.dragPointSize / 2;
175
+ const newPoint = svg.createSVGPoint();
176
+ newPoint.x = relX * width;
177
+ newPoint.y = height - relY * height;
178
+ const circle = this.createCircle(x, y);
179
+ const { points } = svg.querySelector('polyline');
180
+ const newIndex = Array.from(points).findIndex((point) => point.x >= x);
181
+ points.insertItemBefore(newPoint, Math.max(newIndex, 1));
182
+ this.polyPoints.set(refPoint, { polyPoint: newPoint, circle });
183
+ this.makeDraggable(circle, (dx, dy) => {
184
+ const newX = newPoint.x + dx;
185
+ const newY = newPoint.y + dy;
186
+ // Remove the point if it's dragged out of the SVG
187
+ if (newX < -threshold || newY < -threshold || newX > width + threshold || newY > height + threshold) {
188
+ this.emit('point-dragout', refPoint);
189
+ return;
190
+ }
191
+ // Don't allow to drag past the next or previous point
192
+ const next = Array.from(points).find((point) => point.x > newPoint.x);
193
+ const prev = Array.from(points).findLast((point) => point.x < newPoint.x);
194
+ if ((next && newX >= next.x) || (prev && newX <= prev.x)) {
195
+ return;
196
+ }
197
+ // Update the point and the circle position
198
+ newPoint.x = newX;
199
+ newPoint.y = newY;
200
+ circle.setAttribute('cx', newX.toString());
201
+ circle.setAttribute('cy', newY.toString());
202
+ // Emit the event passing the point and new relative coordinates
203
+ this.emit('point-move', refPoint, newX / width, newY / height);
204
+ });
205
+ }
206
+ update() {
207
+ const { svg } = this;
208
+ // Skip the update if the container is hidden
209
+ const { clientWidth, clientHeight } = svg;
210
+ if (!clientWidth || !clientHeight) {
211
+ return;
212
+ }
213
+ const aspectRatioX = svg.viewBox.baseVal.width / clientWidth;
214
+ const aspectRatioY = svg.viewBox.baseVal.height / clientHeight;
215
+ const circles = svg.querySelectorAll('ellipse');
216
+ circles.forEach((circle) => {
217
+ const radius = this.options.dragPointSize / 2;
218
+ const rx = radius * aspectRatioX;
219
+ const ry = radius * aspectRatioY;
220
+ circle.setAttribute('rx', rx.toString());
221
+ circle.setAttribute('ry', ry.toString());
222
+ });
223
+ }
224
+ destroy() {
225
+ // Clear pending press timer
226
+ if (this.pressTimer !== undefined) {
227
+ clearTimeout(this.pressTimer);
228
+ this.pressTimer = undefined;
229
+ }
230
+ // Remove event listeners
231
+ if (this.dblClickListener) {
232
+ this.svg.removeEventListener('dblclick', this.dblClickListener);
233
+ this.dblClickListener = undefined;
234
+ }
235
+ if (this.touchStartListener) {
236
+ this.svg.removeEventListener('touchstart', this.touchStartListener);
237
+ this.touchStartListener = undefined;
238
+ }
239
+ if (this.touchMoveListener) {
240
+ this.svg.removeEventListener('touchmove', this.touchMoveListener);
241
+ this.touchMoveListener = undefined;
242
+ }
243
+ if (this.touchEndListener) {
244
+ this.svg.removeEventListener('touchend', this.touchEndListener);
245
+ this.touchEndListener = undefined;
246
+ }
247
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
248
+ this.polyPoints.clear();
249
+ this.svg.remove();
250
+ }
251
+ }
252
+ const randomId = () => Math.random().toString(36).slice(2);
253
+ class EnvelopePlugin extends BasePlugin {
254
+ /**
255
+ * Create a new Envelope plugin.
256
+ */
257
+ constructor(options) {
258
+ super(options);
259
+ this.polyline = null;
260
+ this.throttleTimeout = null;
261
+ this.volume = 1;
262
+ this.points = options.points || [];
263
+ this.options = Object.assign({}, defaultOptions, options);
264
+ this.options.lineColor = this.options.lineColor || defaultOptions.lineColor;
265
+ this.options.dragPointFill = this.options.dragPointFill || defaultOptions.dragPointFill;
266
+ this.options.dragPointStroke = this.options.dragPointStroke || defaultOptions.dragPointStroke;
267
+ this.options.dragPointSize = this.options.dragPointSize || defaultOptions.dragPointSize;
268
+ }
269
+ static create(options) {
270
+ return new EnvelopePlugin(options);
271
+ }
272
+ /**
273
+ * Add an envelope point with a given time and volume.
274
+ */
275
+ addPoint(point) {
276
+ var _a;
277
+ if (!point.id)
278
+ point.id = randomId();
279
+ // Insert the point in the correct position to keep the array sorted
280
+ const index = this.points.findLastIndex((p) => p.time < point.time);
281
+ this.points.splice(index + 1, 0, point);
282
+ this.emitPoints();
283
+ // Add the point to the polyline if the duration is available
284
+ const duration = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getDuration();
285
+ if (duration) {
286
+ this.addPolyPoint(point, duration);
287
+ }
288
+ }
289
+ /**
290
+ * Remove an envelope point.
291
+ */
292
+ removePoint(point) {
293
+ var _a;
294
+ const index = this.points.indexOf(point);
295
+ if (index > -1) {
296
+ this.points.splice(index, 1);
297
+ (_a = this.polyline) === null || _a === void 0 ? void 0 : _a.removePolyPoint(point);
298
+ this.emitPoints();
299
+ }
300
+ }
301
+ /**
302
+ * Get all envelope points. Should not be modified directly.
303
+ */
304
+ getPoints() {
305
+ return this.points;
306
+ }
307
+ /**
308
+ * Set new envelope points.
309
+ */
310
+ setPoints(newPoints) {
311
+ this.points.slice().forEach((point) => this.removePoint(point));
312
+ newPoints.forEach((point) => this.addPoint(point));
313
+ }
314
+ /**
315
+ * Destroy the plugin instance.
316
+ */
317
+ destroy() {
318
+ var _a;
319
+ // Clear pending throttle timeout
320
+ if (this.throttleTimeout) {
321
+ clearTimeout(this.throttleTimeout);
322
+ this.throttleTimeout = null;
323
+ }
324
+ (_a = this.polyline) === null || _a === void 0 ? void 0 : _a.destroy();
325
+ super.destroy();
326
+ }
327
+ /**
328
+ * Get the envelope volume.
329
+ */
330
+ getCurrentVolume() {
331
+ return this.volume;
332
+ }
333
+ /**
334
+ * Set the envelope volume. 0..1 (more than 1 will boost the volume).
335
+ */
336
+ setVolume(floatValue) {
337
+ var _a;
338
+ this.volume = floatValue;
339
+ (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.setVolume(floatValue);
340
+ }
341
+ /** Called by wavesurfer, don't call manually */
342
+ onInit() {
343
+ var _a;
344
+ if (!this.wavesurfer) {
345
+ throw Error('WaveSurfer is not initialized');
346
+ }
347
+ const { options } = this;
348
+ options.volume = (_a = options.volume) !== null && _a !== void 0 ? _a : this.wavesurfer.getVolume();
349
+ this.setVolume(options.volume);
350
+ this.subscriptions.push(this.wavesurfer.on('decode', (duration) => {
351
+ this.initPolyline();
352
+ this.points.forEach((point) => {
353
+ this.addPolyPoint(point, duration);
354
+ });
355
+ }), this.wavesurfer.on('redraw', () => {
356
+ var _a;
357
+ (_a = this.polyline) === null || _a === void 0 ? void 0 : _a.update();
358
+ }), this.wavesurfer.on('timeupdate', (time) => {
359
+ this.onTimeUpdate(time);
360
+ }));
361
+ }
362
+ emitPoints() {
363
+ if (this.throttleTimeout) {
364
+ clearTimeout(this.throttleTimeout);
365
+ }
366
+ this.throttleTimeout = setTimeout(() => {
367
+ this.emit('points-change', this.points);
368
+ }, 200);
369
+ }
370
+ initPolyline() {
371
+ if (this.polyline)
372
+ this.polyline.destroy();
373
+ if (!this.wavesurfer)
374
+ return;
375
+ const wrapper = this.wavesurfer.getWrapper();
376
+ this.polyline = new Polyline(this.options, wrapper);
377
+ this.subscriptions.push(this.polyline.on('point-move', (point, relativeX, relativeY) => {
378
+ var _a;
379
+ const duration = ((_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getDuration()) || 0;
380
+ point.time = relativeX * duration;
381
+ point.volume = 1 - relativeY;
382
+ this.emitPoints();
383
+ }), this.polyline.on('point-dragout', (point) => {
384
+ this.removePoint(point);
385
+ }), this.polyline.on('point-create', (relativeX, relativeY) => {
386
+ var _a;
387
+ this.addPoint({
388
+ time: relativeX * (((_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getDuration()) || 0),
389
+ volume: 1 - relativeY,
390
+ });
391
+ }), this.polyline.on('line-move', (relativeY) => {
392
+ var _a;
393
+ this.points.forEach((point) => {
394
+ point.volume = Math.min(1, Math.max(0, point.volume - relativeY));
395
+ });
396
+ this.emitPoints();
397
+ this.onTimeUpdate(((_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getCurrentTime()) || 0);
398
+ }));
399
+ }
400
+ addPolyPoint(point, duration) {
401
+ var _a;
402
+ (_a = this.polyline) === null || _a === void 0 ? void 0 : _a.addPolyPoint(point.time / duration, point.volume, point);
403
+ }
404
+ onTimeUpdate(time) {
405
+ if (!this.wavesurfer)
406
+ return;
407
+ let nextPoint = this.points.find((point) => point.time > time);
408
+ if (!nextPoint) {
409
+ nextPoint = { time: this.wavesurfer.getDuration() || 0, volume: 0 };
410
+ }
411
+ let prevPoint = this.points.findLast((point) => point.time <= time);
412
+ if (!prevPoint) {
413
+ prevPoint = { time: 0, volume: 0 };
414
+ }
415
+ const timeDiff = nextPoint.time - prevPoint.time;
416
+ const volumeDiff = nextPoint.volume - prevPoint.volume;
417
+ const newVolume = prevPoint.volume + (time - prevPoint.time) * (volumeDiff / timeDiff);
418
+ const clampedVolume = Math.min(1, Math.max(0, newVolume));
419
+ const roundedVolume = Math.round(clampedVolume * 100) / 100;
420
+ if (roundedVolume !== this.getCurrentVolume()) {
421
+ this.setVolume(roundedVolume);
422
+ this.emit('volume-change', roundedVolume);
423
+ }
424
+ }
425
+ }
426
+ export default EnvelopePlugin;