wavesurfer.js 7.12.1 → 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 (46) hide show
  1. package/dist/__tests__/minimap.test.d.ts +1 -0
  2. package/dist/__tests__/minimap.test.js +92 -0
  3. package/dist/__tests__/regions.test.d.ts +1 -0
  4. package/dist/__tests__/regions.test.js +61 -0
  5. package/dist/__tests__/timeline.test.d.ts +1 -0
  6. package/dist/__tests__/timeline.test.js +46 -0
  7. package/dist/plugins/envelope.js +426 -1
  8. package/dist/plugins/hover.js +131 -1
  9. package/dist/plugins/minimap.d.ts +7 -0
  10. package/dist/plugins/minimap.js +229 -1
  11. package/dist/plugins/record.js +319 -1
  12. package/dist/plugins/regions.d.ts +3 -1
  13. package/dist/plugins/regions.js +699 -1
  14. package/dist/plugins/spectrogram-windowed.js +967 -1
  15. package/dist/plugins/timeline.cjs +1 -1
  16. package/dist/plugins/timeline.esm.js +1 -1
  17. package/dist/plugins/timeline.js +1 -1
  18. package/dist/plugins/timeline.min.js +1 -1
  19. package/dist/renderer.js +5 -0
  20. package/dist/wavesurfer.cjs +1 -1
  21. package/dist/wavesurfer.esm.js +1 -1
  22. package/dist/wavesurfer.min.js +1 -1
  23. package/dist/webaudio.d.ts +1 -1
  24. package/dist/webaudio.js +13 -9
  25. package/package.json +1 -2
  26. package/dist/plugins/envelope.cjs +0 -1
  27. package/dist/plugins/envelope.esm.js +0 -1
  28. package/dist/plugins/envelope.min.js +0 -1
  29. package/dist/plugins/hover.cjs +0 -1
  30. package/dist/plugins/hover.esm.js +0 -1
  31. package/dist/plugins/hover.min.js +0 -1
  32. package/dist/plugins/minimap.cjs +0 -1
  33. package/dist/plugins/minimap.esm.js +0 -1
  34. package/dist/plugins/minimap.min.js +0 -1
  35. package/dist/plugins/record.cjs +0 -1
  36. package/dist/plugins/record.esm.js +0 -1
  37. package/dist/plugins/record.min.js +0 -1
  38. package/dist/plugins/regions.cjs +0 -1
  39. package/dist/plugins/regions.esm.js +0 -1
  40. package/dist/plugins/regions.min.js +0 -1
  41. package/dist/plugins/spectrogram-windowed.cjs +0 -1
  42. package/dist/plugins/spectrogram-windowed.esm.js +0 -1
  43. package/dist/plugins/spectrogram-windowed.min.js +0 -1
  44. package/dist/plugins/spectrogram.cjs +0 -1
  45. package/dist/plugins/spectrogram.esm.js +0 -1
  46. package/dist/plugins/spectrogram.min.js +0 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
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
+ jest.mock('../wavesurfer.js', () => ({
11
+ __esModule: true,
12
+ default: {
13
+ create: jest.fn(),
14
+ },
15
+ }));
16
+ import MinimapPlugin from '../plugins/minimap.js';
17
+ import WaveSurfer from '../wavesurfer.js';
18
+ const createEmitter = () => {
19
+ const listeners = new Map();
20
+ return {
21
+ on: jest.fn((event, listener) => {
22
+ if (!listeners.has(event)) {
23
+ listeners.set(event, new Set());
24
+ }
25
+ listeners.get(event).add(listener);
26
+ return () => { var _a; return (_a = listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(listener); };
27
+ }),
28
+ emit: (event, ...args) => {
29
+ var _a;
30
+ (_a = listeners.get(event)) === null || _a === void 0 ? void 0 : _a.forEach((listener) => listener(...args));
31
+ },
32
+ };
33
+ };
34
+ const createMiniWaveSurfer = (duration = 30) => {
35
+ const emitter = createEmitter();
36
+ const renderer = { renderProgress: jest.fn() };
37
+ return Object.assign(Object.assign({}, emitter), { destroy: jest.fn(() => emitter.emit('destroy')), getDuration: jest.fn(() => duration), getRenderer: jest.fn(() => renderer), setTime: jest.fn(), renderer });
38
+ };
39
+ const createMainWaveSurfer = (dragToSeek = true) => {
40
+ const emitter = createEmitter();
41
+ const wrapperParent = document.createElement('div');
42
+ const wrapper = document.createElement('div');
43
+ const renderer = { renderProgress: jest.fn() };
44
+ const channelData = new Float32Array([0, 1, -1]);
45
+ wrapperParent.appendChild(wrapper);
46
+ return Object.assign(Object.assign({}, emitter), { options: { dragToSeek }, getCurrentTime: jest.fn(() => 12), getDecodedData: jest.fn(() => ({
47
+ duration: 30,
48
+ numberOfChannels: 1,
49
+ getChannelData: jest.fn(() => channelData),
50
+ })), getDuration: jest.fn(() => 30), getRenderer: jest.fn(() => renderer), getWrapper: jest.fn(() => wrapper), isPlaying: jest.fn(() => false), seekTo: jest.fn(), renderer });
51
+ };
52
+ const createMock = WaveSurfer.create;
53
+ describe('MinimapPlugin', () => {
54
+ afterEach(() => {
55
+ jest.clearAllMocks();
56
+ jest.useRealTimers();
57
+ });
58
+ test('renders the minimap from peaks without sharing the main media element', () => __awaiter(void 0, void 0, void 0, function* () {
59
+ const miniWaveSurfer = createMiniWaveSurfer();
60
+ const mainWaveSurfer = createMainWaveSurfer();
61
+ createMock.mockReturnValue(miniWaveSurfer);
62
+ const plugin = MinimapPlugin.create({ height: 20 });
63
+ plugin._init(mainWaveSurfer);
64
+ yield Promise.resolve();
65
+ expect(createMock).toHaveBeenCalledWith(expect.objectContaining({
66
+ container: expect.any(HTMLElement),
67
+ duration: 30,
68
+ media: undefined,
69
+ url: undefined,
70
+ }));
71
+ expect(createMock.mock.calls[0][0].peaks).toEqual([new Float32Array([0, 1, -1])]);
72
+ mainWaveSurfer.emit('timeupdate', 5);
73
+ expect(miniWaveSurfer.setTime).toHaveBeenLastCalledWith(5);
74
+ mainWaveSurfer.emit('drag', 0.25);
75
+ expect(miniWaveSurfer.renderer.renderProgress).toHaveBeenCalledWith(0.25, false);
76
+ }));
77
+ test('syncs the main waveform immediately when interacting with the minimap', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ jest.useFakeTimers();
79
+ const miniWaveSurfer = createMiniWaveSurfer();
80
+ const mainWaveSurfer = createMainWaveSurfer(true);
81
+ createMock.mockReturnValue(miniWaveSurfer);
82
+ const plugin = MinimapPlugin.create({ dragToSeek: true });
83
+ plugin._init(mainWaveSurfer);
84
+ yield Promise.resolve();
85
+ miniWaveSurfer.emit('click', 0.5, 0.1);
86
+ expect(mainWaveSurfer.seekTo).toHaveBeenCalledWith(0.5);
87
+ miniWaveSurfer.emit('drag', 0.75);
88
+ expect(mainWaveSurfer.renderer.renderProgress).toHaveBeenCalledWith(0.75, false);
89
+ jest.advanceTimersByTime(200);
90
+ expect(mainWaveSurfer.seekTo).toHaveBeenLastCalledWith(0.75);
91
+ }));
92
+ });
@@ -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;