wavesurfer.js 7.0.0-alpha.3 → 7.0.0-alpha.31
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.
- package/README.md +58 -26
- package/dist/base-plugin.d.ts +9 -5
- package/dist/base-plugin.js +11 -3
- package/dist/decoder.d.ts +2 -4
- package/dist/decoder.js +20 -31
- package/dist/event-emitter.d.ts +1 -1
- package/dist/event-emitter.js +4 -7
- package/dist/fetcher.js +2 -13
- package/dist/legacy-adapter.d.ts +7 -0
- package/dist/legacy-adapter.js +15 -0
- package/dist/player.d.ts +39 -9
- package/dist/player.js +90 -15
- package/dist/plugin/wavesurfer.envelope.min.js +1 -0
- package/dist/plugin/wavesurfer.minimap.min.js +1 -0
- package/dist/plugin/wavesurfer.multitrack.min.js +1 -0
- package/dist/plugin/wavesurfer.regions.min.js +1 -0
- package/dist/plugin/wavesurfer.timeline.min.js +1 -0
- package/dist/plugins/envelope.d.ts +60 -0
- package/dist/plugins/envelope.js +302 -0
- package/dist/plugins/envelope.min.js +1 -0
- package/dist/plugins/minimap.d.ts +27 -0
- package/dist/plugins/minimap.js +83 -0
- package/dist/plugins/minimap.min.js +1 -0
- package/dist/plugins/multitrack.d.ts +113 -0
- package/dist/plugins/multitrack.js +504 -0
- package/dist/plugins/multitrack.min.js +1 -0
- package/dist/plugins/record.d.ts +23 -0
- package/dist/plugins/record.js +119 -0
- package/dist/plugins/record.min.js +1 -0
- package/dist/plugins/regions.d.ts +68 -32
- package/dist/plugins/regions.js +291 -158
- package/dist/plugins/regions.min.js +1 -0
- package/dist/plugins/spectrogram.d.ts +24 -0
- package/dist/plugins/spectrogram.js +165 -0
- package/dist/plugins/timeline.d.ts +38 -0
- package/dist/plugins/timeline.js +134 -0
- package/dist/plugins/timeline.min.js +1 -0
- package/dist/renderer.d.ts +27 -12
- package/dist/renderer.js +223 -137
- package/dist/timer.d.ts +2 -1
- package/dist/timer.js +5 -3
- package/dist/wavesurfer.d.ts +132 -0
- package/dist/wavesurfer.js +220 -0
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +51 -20
- package/dist/index.d.ts +0 -92
- package/dist/index.js +0 -166
- package/dist/player-webaudio.d.ts +0 -8
- package/dist/player-webaudio.js +0 -30
- package/dist/wavesurfer.Regions.min.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Minimap=e():t.Minimap=e()}(WaveSurfer,(()=>(()=>{"use strict";var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>p});const i=class{eventTarget;constructor(){this.eventTarget=new EventTarget}emit(t,e){const i=new CustomEvent(String(t),{detail:e});this.eventTarget.dispatchEvent(i)}on(t,e,i){const s=t=>{t instanceof CustomEvent&&e(t.detail)},r=String(t);return this.eventTarget.addEventListener(r,s,{once:i}),()=>this.eventTarget.removeEventListener(r,s)}once(t,e){return this.on(t,e,!0)}},s=class extends i{wavesurfer;container;wrapper;subscriptions=[];options;constructor(t){super(),this.options=t}init(t){this.wavesurfer=t.wavesurfer,this.container=t.container,this.wrapper=t.wrapper}destroy(){this.subscriptions.forEach((t=>t()))}},r=class extends i{options={height:0};container;scrollContainer;wrapper;canvasWrapper;progressWrapper;timeout=null;isScrolling=!1;channelData=null;duration=null;resizeObserver=null;constructor(t,e){super(),this.options={...e};let i=null;if("string"==typeof t.container?i=document.querySelector(t.container):t.container instanceof HTMLElement&&(i=t.container),!i)throw new Error("Container not found");const[s,r]=this.initHtml();i.appendChild(s),this.container=s,this.scrollContainer=r.querySelector(".scroll"),this.wrapper=r.querySelector(".wrapper"),this.canvasWrapper=r.querySelector(".canvases"),this.progressWrapper=r.querySelector(".progress"),this.wrapper.addEventListener("click",(t=>{const e=this.wrapper.getBoundingClientRect(),i=(t.clientX-e.left)/e.width;this.emit("click",{relativeX:i})})),this.resizeObserver=new ResizeObserver((()=>{this.delay((()=>this.reRender()),100)})),this.resizeObserver.observe(this.scrollContainer)}initHtml(){const t=document.createElement("div"),e=t.attachShadow({mode:"open"});return e.innerHTML=`\n <style>\n :host {\n user-select: none;\n }\n :host .scroll {\n overflow-x: auto;\n overflow-y: hidden;\n width: 100%;\n position: relative;\n }\n :host .noScrollbar {\n scrollbar-color: transparent;\n scrollbar-width: none;\n }\n :host .noScrollbar::-webkit-scrollbar {\n display: none;\n -webkit-appearance: none;\n }\n :host .wrapper {\n position: relative;\n overflow: visible;\n z-index: 2;\n }\n :host .canvases {\n position: relative;\n height: ${this.options.height}px;\n }\n :host canvas {\n display: block;\n position: absolute;\n top: 0;\n image-rendering: pixelated;\n height: ${this.options.height}px;\n }\n :host .progress {\n pointer-events: none;\n position: absolute;\n z-index: 2;\n top: 0;\n left: 0;\n width: 0;\n height: 100%;\n overflow: hidden;\n box-sizing: border-box;\n }\n </style>\n\n <div class="scroll">\n <div class="wrapper">\n <div class="canvases"></div>\n <div class="progress"></div>\n </div>\n </div>\n `,[t,e]}setOptions(t){this.options=t,this.reRender()}getContainer(){return this.scrollContainer}getWrapper(){return this.wrapper}destroy(){this.container.remove(),this.resizeObserver?.disconnect()}delay(t,e=10){return this.timeout&&clearTimeout(this.timeout),new Promise((i=>{this.timeout=setTimeout((()=>{i(t())}),e)}))}async renderPeaks(t,e,i,s){const r=null!=this.options.barWidth?this.options.barWidth*s:1,n=null!=this.options.barGap?this.options.barGap*s:this.options.barWidth?r/2:0,o=this.options.barRadius??0,a=t[0],h=a.length,l=Math.floor(e/(r+n))/h,d=i/2,c=1===t.length,p=c?a:t[1],u=c&&p.some((t=>t<0)),m=(t,i)=>{let c=0,m=0,g=0;const v=document.createElement("canvas");v.width=Math.round(e*(i-t)/h),v.height=this.options.height,v.style.width=`${Math.floor(v.width/s)}px`,v.style.height=`${this.options.height}px`,v.style.left=`${Math.floor(t*e/s/h)}px`,this.canvasWrapper.appendChild(v);const y=v.getContext("2d",{desynchronized:!0});y.beginPath(),y.fillStyle=this.options.waveColor??"",y.roundRect||(y.roundRect=y.fillRect);for(let e=t;e<i;e++){const i=Math.round((e-t)*l);if(i>c){const t=Math.round(m*d),e=Math.round(g*d);y.roundRect(c*(r+n),d-t,r,t+e||1,o),c=i,m=0,g=0}const s=u?a[e]:Math.abs(a[e]),h=u?p[e]:Math.abs(p[e]);s>m&&(m=s),(u?h<-g:h>g)&&(g=h<0?-h:h)}y.fill(),y.closePath();const f=v.cloneNode();this.progressWrapper.appendChild(f);const b=f.getContext("2d",{desynchronized:!0});v.width>0&&v.height>0&&b.drawImage(v,0,0),b.globalCompositeOperation="source-in",b.fillStyle=this.options.progressColor??"",b.fillRect(0,0,v.width,v.height)};this.canvasWrapper.innerHTML="",this.progressWrapper.innerHTML="";const{scrollLeft:g,scrollWidth:v,clientWidth:y}=this.scrollContainer,f=h/v,b=Math.floor(g*f),w=Math.ceil(Math.min(v,g+y)*f);m(b,w);const C=w-b;for(let t=w;t<h;t+=C)await this.delay((()=>{m(t,Math.min(h,t+C))}));for(let t=b-1;t>=0;t-=C)await this.delay((()=>{m(Math.max(0,t-C),t)}))}render(t,e){const i=window.devicePixelRatio||1,s=this.scrollContainer.clientWidth,r=Math.ceil(e*(this.options.minPxPerSec||0));this.isScrolling=r>s;const n=this.options.fillParent&&!this.isScrolling,o=(n?s:r)*i,{height:a}=this.options;this.wrapper.style.width=n?"100%":`${r}px`,this.scrollContainer.style.overflowX=this.isScrolling?"auto":"hidden",this.scrollContainer.classList.toggle("noScrollbar",!!this.options.hideScrollbar),this.progressWrapper.style.borderRightStyle="solid",this.progressWrapper.style.borderRightColor=`${this.options.cursorColor||this.options.progressColor}`,this.progressWrapper.style.borderRightWidth=`${this.options.cursorWidth}px`,this.canvasWrapper.style.height=`${this.options.height}px`,this.renderPeaks(t,o,a,i),this.channelData=t,this.duration=e}reRender(){if(!this.channelData||!this.duration)return;const t=this.progressWrapper.clientWidth;this.render(this.channelData,this.duration);const e=this.progressWrapper.clientWidth;this.scrollContainer.scrollLeft+=e-t}zoom(t){this.options.minPxPerSec=t,this.reRender()}renderProgress(t,e=!1){if(this.progressWrapper.style.width=100*t+"%",this.isScrolling&&this.options.autoCenter){const{clientWidth:i,scrollLeft:s,scrollWidth:r}=this.scrollContainer,n=r*t,o=i/2,a=o/20;(n>s+(e?o:i)||n<s)&&(n-(s+o)>=a&&n<s+i?this.scrollContainer.scrollLeft+=a:this.scrollContainer.scrollLeft=n-o)}}},n=class extends i{media;subscriptions=[];isExternalMedia=!1;hasPlayedOnce=!1;constructor(t){super(),t.media?(this.media=t.media,this.isExternalMedia=!0):this.media=document.createElement("audio"),this.subscriptions.push(this.onceMediaEvent("play",(()=>{this.hasPlayedOnce=!0}))),t.autoplay&&(this.media.autoplay=!0),null!=t.playbackRate&&(this.media.playbackRate=t.playbackRate)}onMediaEvent(t,e,i){return this.media.addEventListener(t,e,i),()=>this.media.removeEventListener(t,e)}onceMediaEvent(t,e){return this.onMediaEvent(t,e,{once:!0})}loadUrl(t){this.media.src=t}destroy(){this.media.pause(),this.subscriptions.forEach((t=>t())),this.isExternalMedia||this.media.remove()}play(){return this.media.play()}pause(){this.media.pause()}isPlaying(){return this.media.currentTime>0&&!this.media.paused&&!this.media.ended}setTime(t){this.hasPlayedOnce||this.media.play()?.then?.((()=>{setTimeout((()=>this.media.pause()),10)})),this.media.currentTime=t}getDuration(){return this.media.duration}getCurrentTime(){return this.media.currentTime}getVolume(){return this.media.volume}setVolume(t){this.media.volume=t}getMuted(){return this.media.muted}setMuted(t){this.media.muted=t}getPlaybackRate(){return this.media.playbackRate}setPlaybackRate(t,e){null!=e&&(this.media.preservesPitch=e),this.media.playbackRate=t}getMediaElement(){return this.media}setSinkId(t){return this.media.setSinkId(t)}},o=class extends i{unsubscribe=()=>{};start(){this.unsubscribe=this.on("tick",(()=>{requestAnimationFrame((()=>{this.emit("tick")}))})),this.emit("tick")}stop(){this.unsubscribe()}destroy(){this.unsubscribe()}},a={height:128,waveColor:"#999",progressColor:"#555",cursorWidth:1,minPxPerSec:0,fillParent:!0,interact:!0,autoCenter:!0};class h extends n{options;fetcher;decoder;renderer;timer;plugins=[];decodedData=null;canPlay=!1;static create(t){return new h(t)}constructor(t){super({media:t.media,autoplay:t.autoplay,playbackRate:t.audioRate}),this.options=Object.assign({},a,t),this.fetcher=new class{async load(t){return fetch(t).then((t=>t.arrayBuffer()))}},this.decoder=new class{audioCtx=null;initAudioContext(t){this.audioCtx=new AudioContext({latencyHint:"playback",sampleRate:t})}constructor(){try{this.initAudioContext(3e3)}catch(t){this.initAudioContext(8e3)}}async decode(t){if(!this.audioCtx)throw new Error("AudioContext is not initialized");return this.audioCtx.decodeAudioData(t)}destroy(){this.audioCtx?.close(),this.audioCtx=null}},this.timer=new o,this.renderer=new r({container:this.options.container},this.options),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents(),this.initReadyEvent(),this.initPlugins();const e=this.options.url||this.options.media?.src;e&&this.load(e,this.options.peaks,this.options.duration)}setOptions(t){this.options={...this.options,...t},this.renderer.setOptions(this.options)}initPlayerEvents(){this.subscriptions.push(this.onMediaEvent("timeupdate",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.getDuration(),this.isPlaying()),this.emit("timeupdate",{currentTime:t})})),this.onMediaEvent("play",(()=>{this.emit("play"),this.timer.start()})),this.onMediaEvent("pause",(()=>{this.emit("pause"),this.timer.stop(),this.getCurrentTime()>=this.getDuration()&&this.emit("finish")})),this.onMediaEvent("canplay",(()=>{this.canPlay=!0,this.emit("canplay",{duration:this.getDuration()})})),this.onMediaEvent("seeking",(()=>{this.emit("seeking",{currentTime:this.getCurrentTime()})})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",(({relativeX:t})=>{this.options.interact&&(this.seekTo(t),this.emit("interaction"))})))}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.getDuration(),!0),this.emit("timeupdate",{currentTime:t})})))}initReadyEvent(){const t=()=>{this.decodedData&&this.canPlay&&this.emit("ready",{duration:this.getDuration()})};this.subscriptions.push(this.on("decode",t),this.on("canplay",t))}initPlugins(){this.options.plugins?.length&&this.options.plugins.forEach((t=>{this.registerPlugin(t)}))}registerPlugin(t){return t.init({wavesurfer:this,container:this.renderer.getContainer(),wrapper:this.renderer.getWrapper()}),this.plugins.push(t),t}async load(t,e,i){if(this.decodedData=null,this.canPlay=!1,this.loadUrl(t),null==e){const e=await this.fetcher.load(t),i=await this.decoder.decode(e);this.decodedData=i}else i||(i=await new Promise((t=>{this.onceMediaEvent("loadedmetadata",(()=>t(this.getDuration())))}))||0),"number"==typeof e[0]&&(e=[e]),this.decodedData={duration:i,numberOfChannels:e.length,sampleRate:e[0].length/i,getChannelData:t=>e?.[t]};this.renderAudio(),this.emit("decode",{duration:this.getDuration()})}renderAudio(){if(!this.decodedData)return;const t=[this.decodedData.getChannelData(0)];this.decodedData.numberOfChannels>1&&t.push(this.decodedData.getChannelData(1));const e=this.getDuration();this.renderer.render(t,e)}zoom(t){if(!this.decodedData)throw new Error("No audio loaded");this.renderer.zoom(t),this.emit("zoom",{minPxPerSec:t})}getDecodedData(){return this.decodedData}getDuration(){const t=this.getMediaElement()?.duration;return t>0&&t<1/0?t:this.decodedData?.duration||0}toggleInteraction(t){this.options.interact=t}seekTo(t){const e=this.getDuration()*t;this.setTime(e)}playPause(){return this.isPlaying()?(this.pause(),Promise.resolve()):this.play()}stop(){this.pause(),this.setTime(0)}skip(t){this.setTime(this.getCurrentTime()+t)}empty(){this.load("",[[0]],.001)}destroy(){this.emit("destroy"),this.plugins.forEach((t=>t.destroy())),this.timer.destroy(),this.decoder.destroy(),this.renderer.destroy(),super.destroy()}}const l=h,d={height:50,overlayColor:"rgba(100, 100, 100, 0.1)"};class c extends s{options;minimapWrapper;miniWavesurfer=null;overlay;constructor(t){super(t),this.options=Object.assign({},d,t),this.minimapWrapper=this.initMinimapWrapper(),this.overlay=this.initOverlay()}static create(t){return new c(t)}init(t){if(super.init(t),!this.wavesurfer)throw Error("WaveSurfer is not initialized");this.container?.insertAdjacentElement("afterend",this.minimapWrapper),this.subscriptions.push(this.wavesurfer.on("decode",(()=>{this.initMinimap()})))}initMinimapWrapper(){const t=document.createElement("div");return t.style.position="relative",t}initOverlay(){const t=document.createElement("div");return t.setAttribute("style","position: absolute; z-index: 2; left: 0; top: 0; bottom: 0;"),t.style.backgroundColor=this.options.overlayColor,this.minimapWrapper.appendChild(t),t}initMinimap(){if(!this.wavesurfer||!this.wrapper)return;const t=this.wavesurfer.getDecodedData(),e=this.wavesurfer.getMediaElement();if(!t||!e)return;this.miniWavesurfer=l.create({...this.options,container:this.minimapWrapper,minPxPerSec:1,fillParent:!0,media:e,url:e.src,peaks:[t.getChannelData(0)],duration:t.duration});const i=Math.round(this.minimapWrapper.clientWidth/this.wrapper.clientWidth*100);this.overlay.style.width=`${i}%`,this.subscriptions.push(this.wavesurfer.on("timeupdate",(({currentTime:e})=>{const s=Math.max(0,Math.min(e/t.duration*100-i/2,100-i)).toFixed(2);this.overlay.style.left=`${s}%`}))),this.subscriptions.push(this.miniWavesurfer.on("interaction",(()=>{this.wavesurfer&&this.miniWavesurfer&&this.wavesurfer.setTime(this.miniWavesurfer.getCurrentTime()),this.container&&(this.container.scrollLeft=this.minimapWrapper.scrollLeft/this.minimapWrapper.scrollWidth*this.container.scrollWidth)})))}destroy(){this.miniWavesurfer?.destroy(),this.minimapWrapper.remove(),super.destroy()}}const p=c;return e.default})()));
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type WaveSurferOptions } from '../wavesurfer.js';
|
|
2
|
+
import { type EnvelopePluginOptions } from './envelope.js';
|
|
3
|
+
import EventEmitter from '../event-emitter.js';
|
|
4
|
+
export type TrackId = string | number;
|
|
5
|
+
export type TrackOptions = {
|
|
6
|
+
id: TrackId;
|
|
7
|
+
url?: string;
|
|
8
|
+
peaks?: WaveSurferOptions['peaks'];
|
|
9
|
+
draggable?: boolean;
|
|
10
|
+
startPosition: number;
|
|
11
|
+
startCue?: number;
|
|
12
|
+
endCue?: number;
|
|
13
|
+
fadeInEnd?: number;
|
|
14
|
+
fadeOutStart?: number;
|
|
15
|
+
volume?: number;
|
|
16
|
+
markers?: Array<{
|
|
17
|
+
time: number;
|
|
18
|
+
label?: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
}>;
|
|
21
|
+
intro?: {
|
|
22
|
+
endTime: number;
|
|
23
|
+
label?: string;
|
|
24
|
+
color?: string;
|
|
25
|
+
};
|
|
26
|
+
options?: WaveSurferOptions;
|
|
27
|
+
};
|
|
28
|
+
export type MultitrackOptions = {
|
|
29
|
+
container: HTMLElement;
|
|
30
|
+
minPxPerSec?: number;
|
|
31
|
+
cursorColor?: string;
|
|
32
|
+
cursorWidth?: number;
|
|
33
|
+
trackBackground?: string;
|
|
34
|
+
trackBorderColor?: string;
|
|
35
|
+
rightButtonDrag?: boolean;
|
|
36
|
+
envelopeOptions?: EnvelopePluginOptions;
|
|
37
|
+
};
|
|
38
|
+
export type MultitrackEvents = {
|
|
39
|
+
canplay: void;
|
|
40
|
+
'start-position-change': {
|
|
41
|
+
id: TrackId;
|
|
42
|
+
startPosition: number;
|
|
43
|
+
};
|
|
44
|
+
'start-cue-change': {
|
|
45
|
+
id: TrackId;
|
|
46
|
+
startCue: number;
|
|
47
|
+
};
|
|
48
|
+
'end-cue-change': {
|
|
49
|
+
id: TrackId;
|
|
50
|
+
endCue: number;
|
|
51
|
+
};
|
|
52
|
+
'fade-in-change': {
|
|
53
|
+
id: TrackId;
|
|
54
|
+
fadeInEnd: number;
|
|
55
|
+
};
|
|
56
|
+
'fade-out-change': {
|
|
57
|
+
id: TrackId;
|
|
58
|
+
fadeOutStart: number;
|
|
59
|
+
};
|
|
60
|
+
'volume-change': {
|
|
61
|
+
id: TrackId;
|
|
62
|
+
volume: number;
|
|
63
|
+
};
|
|
64
|
+
'intro-end-change': {
|
|
65
|
+
id: TrackId;
|
|
66
|
+
endTime: number;
|
|
67
|
+
};
|
|
68
|
+
drop: {
|
|
69
|
+
id: TrackId;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export type MultitrackTracks = Array<TrackOptions>;
|
|
73
|
+
declare class MultiTrack extends EventEmitter<MultitrackEvents> {
|
|
74
|
+
private tracks;
|
|
75
|
+
private options;
|
|
76
|
+
private audios;
|
|
77
|
+
private wavesurfers;
|
|
78
|
+
private durations;
|
|
79
|
+
private currentTime;
|
|
80
|
+
private maxDuration;
|
|
81
|
+
private rendering;
|
|
82
|
+
private isDragging;
|
|
83
|
+
private frameRequest;
|
|
84
|
+
private timer;
|
|
85
|
+
private subscriptions;
|
|
86
|
+
private timeline;
|
|
87
|
+
static create(tracks: MultitrackTracks, options: MultitrackOptions): MultiTrack;
|
|
88
|
+
constructor(tracks: MultitrackTracks, options: MultitrackOptions);
|
|
89
|
+
private initDurations;
|
|
90
|
+
private initAudio;
|
|
91
|
+
private initAllAudios;
|
|
92
|
+
private initWavesurfer;
|
|
93
|
+
private initAllWavesurfers;
|
|
94
|
+
private initTimeline;
|
|
95
|
+
private updatePosition;
|
|
96
|
+
private setIsDragging;
|
|
97
|
+
private onDrag;
|
|
98
|
+
private findCurrentTracks;
|
|
99
|
+
private startSync;
|
|
100
|
+
play(): void;
|
|
101
|
+
pause(): void;
|
|
102
|
+
isPlaying(): boolean;
|
|
103
|
+
getCurrentTime(): number;
|
|
104
|
+
/** Position percentage from 0 to 1 */
|
|
105
|
+
seekTo(position: number): void;
|
|
106
|
+
/** Set time in seconds */
|
|
107
|
+
setTime(time: number): void;
|
|
108
|
+
zoom(pxPerSec: number): void;
|
|
109
|
+
addTrack(track: TrackOptions): void;
|
|
110
|
+
destroy(): void;
|
|
111
|
+
setSinkId(sinkId: string): Promise<void[]>;
|
|
112
|
+
}
|
|
113
|
+
export default MultiTrack;
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import WaveSurfer from '../wavesurfer.js';
|
|
2
|
+
import RegionsPlugin from './regions.js';
|
|
3
|
+
import TimelinePlugin from './timeline.js';
|
|
4
|
+
import EnvelopePlugin from './envelope.js';
|
|
5
|
+
import EventEmitter from '../event-emitter.js';
|
|
6
|
+
class MultiTrack extends EventEmitter {
|
|
7
|
+
tracks;
|
|
8
|
+
options;
|
|
9
|
+
audios = [];
|
|
10
|
+
wavesurfers = [];
|
|
11
|
+
durations = [];
|
|
12
|
+
currentTime = 0;
|
|
13
|
+
maxDuration = 0;
|
|
14
|
+
rendering;
|
|
15
|
+
isDragging = false;
|
|
16
|
+
frameRequest = null;
|
|
17
|
+
timer = null;
|
|
18
|
+
subscriptions = [];
|
|
19
|
+
timeline = null;
|
|
20
|
+
static create(tracks, options) {
|
|
21
|
+
return new MultiTrack(tracks, options);
|
|
22
|
+
}
|
|
23
|
+
constructor(tracks, options) {
|
|
24
|
+
super();
|
|
25
|
+
this.tracks = tracks.map((track) => ({
|
|
26
|
+
...track,
|
|
27
|
+
startPosition: track.startPosition || 0,
|
|
28
|
+
peaks: track.peaks || (track.url ? undefined : [new Float32Array()]),
|
|
29
|
+
}));
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.rendering = initRendering(this.tracks, this.options);
|
|
32
|
+
this.rendering.addDropHandler((trackId) => {
|
|
33
|
+
this.emit('drop', { id: trackId });
|
|
34
|
+
});
|
|
35
|
+
this.initAllAudios().then((durations) => {
|
|
36
|
+
this.initDurations(durations);
|
|
37
|
+
this.initAllWavesurfers();
|
|
38
|
+
this.rendering.containers.forEach((container, index) => {
|
|
39
|
+
const drag = initDragging(container, (delta) => this.onDrag(index, delta), options.rightButtonDrag);
|
|
40
|
+
this.wavesurfers[index].once('destroy', () => drag?.destroy());
|
|
41
|
+
});
|
|
42
|
+
this.rendering.addClickHandler((position) => {
|
|
43
|
+
if (this.isDragging)
|
|
44
|
+
return;
|
|
45
|
+
this.seekTo(position);
|
|
46
|
+
});
|
|
47
|
+
this.emit('canplay');
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
initDurations(durations) {
|
|
51
|
+
this.durations = durations;
|
|
52
|
+
this.maxDuration = this.tracks.reduce((max, track, index) => {
|
|
53
|
+
return Math.max(max, track.startPosition + durations[index]);
|
|
54
|
+
}, 0);
|
|
55
|
+
this.rendering.setMainWidth(durations, this.maxDuration);
|
|
56
|
+
}
|
|
57
|
+
initAudio(track) {
|
|
58
|
+
const audio = new Audio(track.url);
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
if (!audio.src)
|
|
61
|
+
return resolve(audio);
|
|
62
|
+
audio.addEventListener('loadedmetadata', () => resolve(audio), { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async initAllAudios() {
|
|
66
|
+
this.audios = await Promise.all(this.tracks.map((track) => this.initAudio(track)));
|
|
67
|
+
return this.audios.map((a) => (a.src ? a.duration : 0));
|
|
68
|
+
}
|
|
69
|
+
initWavesurfer(track, index) {
|
|
70
|
+
const container = this.rendering.containers[index];
|
|
71
|
+
// Create a wavesurfer instance
|
|
72
|
+
const ws = WaveSurfer.create({
|
|
73
|
+
...track.options,
|
|
74
|
+
container,
|
|
75
|
+
minPxPerSec: 0,
|
|
76
|
+
media: this.audios[index],
|
|
77
|
+
peaks: track.peaks,
|
|
78
|
+
cursorColor: 'transparent',
|
|
79
|
+
cursorWidth: 0,
|
|
80
|
+
interact: false,
|
|
81
|
+
});
|
|
82
|
+
// Regions and markers
|
|
83
|
+
const wsRegions = ws.registerPlugin(RegionsPlugin.create());
|
|
84
|
+
this.subscriptions.push(ws.once('decode', () => {
|
|
85
|
+
// Start and end cues
|
|
86
|
+
if (track.startCue != null || track.endCue != null) {
|
|
87
|
+
const { startCue = 0, endCue = this.durations[index] } = track;
|
|
88
|
+
const startCueRegion = wsRegions.addRegion({
|
|
89
|
+
start: 0,
|
|
90
|
+
end: startCue,
|
|
91
|
+
color: 'rgba(0, 0, 0, 0.7)',
|
|
92
|
+
drag: false,
|
|
93
|
+
});
|
|
94
|
+
const endCueRegion = wsRegions.addRegion({
|
|
95
|
+
start: endCue,
|
|
96
|
+
end: endCue + this.durations[index],
|
|
97
|
+
color: 'rgba(0, 0, 0, 0.7)',
|
|
98
|
+
drag: false,
|
|
99
|
+
});
|
|
100
|
+
// Allow resizing only from one side
|
|
101
|
+
startCueRegion.element.firstElementChild?.remove();
|
|
102
|
+
endCueRegion.element.lastChild?.remove();
|
|
103
|
+
// Prevent clicks when dragging
|
|
104
|
+
// Update the start and end cues on resize
|
|
105
|
+
this.subscriptions.push(startCueRegion.on('update-end', () => {
|
|
106
|
+
track.startCue = startCueRegion.end;
|
|
107
|
+
this.emit('start-cue-change', { id: track.id, startCue: track.startCue });
|
|
108
|
+
}), endCueRegion.on('update-end', () => {
|
|
109
|
+
track.endCue = endCueRegion.start;
|
|
110
|
+
this.emit('end-cue-change', { id: track.id, endCue: track.endCue });
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// Intro
|
|
114
|
+
if (track.intro) {
|
|
115
|
+
const introRegion = wsRegions.addRegion({
|
|
116
|
+
start: 0,
|
|
117
|
+
end: track.intro.endTime,
|
|
118
|
+
content: track.intro.label,
|
|
119
|
+
color: this.options.trackBackground,
|
|
120
|
+
drag: false,
|
|
121
|
+
});
|
|
122
|
+
introRegion.element.querySelector('[data-resize="left"]')?.remove();
|
|
123
|
+
introRegion.element.parentElement.style.mixBlendMode = 'plus-lighter';
|
|
124
|
+
if (track.intro.color) {
|
|
125
|
+
;
|
|
126
|
+
introRegion.element.querySelector('[data-resize="right"]').style.borderColor =
|
|
127
|
+
track.intro.color;
|
|
128
|
+
}
|
|
129
|
+
this.subscriptions.push(introRegion.on('update-end', () => {
|
|
130
|
+
this.emit('intro-end-change', { id: track.id, endTime: introRegion.end });
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
// Render markers
|
|
134
|
+
if (track.markers) {
|
|
135
|
+
track.markers.forEach((marker) => {
|
|
136
|
+
wsRegions.addRegion({
|
|
137
|
+
start: marker.time,
|
|
138
|
+
content: marker.label,
|
|
139
|
+
color: marker.color,
|
|
140
|
+
resize: false,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
// Envelope
|
|
146
|
+
const envelope = ws.registerPlugin(EnvelopePlugin.create({
|
|
147
|
+
...this.options.envelopeOptions,
|
|
148
|
+
fadeInStart: track.startCue,
|
|
149
|
+
fadeInEnd: track.fadeInEnd,
|
|
150
|
+
fadeOutStart: track.fadeOutStart,
|
|
151
|
+
fadeOutEnd: track.endCue,
|
|
152
|
+
volume: track.volume,
|
|
153
|
+
}));
|
|
154
|
+
this.subscriptions.push(envelope.on('volume-change', ({ volume }) => {
|
|
155
|
+
this.setIsDragging();
|
|
156
|
+
this.emit('volume-change', { id: track.id, volume });
|
|
157
|
+
}), envelope.on('fade-in-change', ({ time }) => {
|
|
158
|
+
this.setIsDragging();
|
|
159
|
+
this.emit('fade-in-change', { id: track.id, fadeInEnd: time });
|
|
160
|
+
}), envelope.on('fade-out-change', ({ time }) => {
|
|
161
|
+
this.setIsDragging();
|
|
162
|
+
this.emit('fade-out-change', { id: track.id, fadeOutStart: time });
|
|
163
|
+
}), this.on('start-cue-change', ({ id, startCue }) => {
|
|
164
|
+
if (id === track.id) {
|
|
165
|
+
envelope.setStartTime(startCue);
|
|
166
|
+
}
|
|
167
|
+
}), this.on('end-cue-change', ({ id, endCue }) => {
|
|
168
|
+
if (id === track.id) {
|
|
169
|
+
envelope.setEndTime(endCue);
|
|
170
|
+
}
|
|
171
|
+
}));
|
|
172
|
+
return ws;
|
|
173
|
+
}
|
|
174
|
+
initAllWavesurfers() {
|
|
175
|
+
const wavesurfers = this.tracks.map((track, index) => {
|
|
176
|
+
return this.initWavesurfer(track, index);
|
|
177
|
+
});
|
|
178
|
+
this.wavesurfers = wavesurfers;
|
|
179
|
+
this.initTimeline();
|
|
180
|
+
}
|
|
181
|
+
initTimeline() {
|
|
182
|
+
if (this.timeline)
|
|
183
|
+
this.timeline.destroy();
|
|
184
|
+
this.timeline = this.wavesurfers[0].registerPlugin(TimelinePlugin.create({
|
|
185
|
+
duration: this.maxDuration,
|
|
186
|
+
container: this.rendering.containers[0].parentElement,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
updatePosition(time, autoCenter = false) {
|
|
190
|
+
const precisionSeconds = 0.3;
|
|
191
|
+
const isPaused = !this.isPlaying();
|
|
192
|
+
if (time !== this.currentTime) {
|
|
193
|
+
this.currentTime = time;
|
|
194
|
+
this.rendering.updateCursor(time / this.maxDuration, autoCenter);
|
|
195
|
+
}
|
|
196
|
+
// Update the current time of each audio
|
|
197
|
+
this.tracks.forEach((track, index) => {
|
|
198
|
+
const audio = this.audios[index];
|
|
199
|
+
const duration = this.durations[index];
|
|
200
|
+
const newTime = time - track.startPosition;
|
|
201
|
+
if (Math.abs(audio.currentTime - newTime) > precisionSeconds) {
|
|
202
|
+
audio.currentTime = newTime;
|
|
203
|
+
}
|
|
204
|
+
// If the position is out of the track bounds, pause it
|
|
205
|
+
if (isPaused || newTime < 0 || newTime > duration) {
|
|
206
|
+
!audio.paused && audio.pause();
|
|
207
|
+
}
|
|
208
|
+
else if (!isPaused) {
|
|
209
|
+
// If the position is in the track bounds, play it
|
|
210
|
+
audio.paused && audio.play();
|
|
211
|
+
}
|
|
212
|
+
// Unmute if cue is reached
|
|
213
|
+
const newVolume = newTime >= (track.startCue || 0) && newTime < (track.endCue || Infinity) ? 1 : 0;
|
|
214
|
+
if (newVolume !== audio.volume)
|
|
215
|
+
audio.volume = newVolume;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
setIsDragging() {
|
|
219
|
+
// Prevent click events when dragging
|
|
220
|
+
this.isDragging = true;
|
|
221
|
+
if (this.timer)
|
|
222
|
+
clearTimeout(this.timer);
|
|
223
|
+
this.timer = setTimeout(() => (this.isDragging = false), 300);
|
|
224
|
+
}
|
|
225
|
+
onDrag(index, delta) {
|
|
226
|
+
this.setIsDragging();
|
|
227
|
+
const track = this.tracks[index];
|
|
228
|
+
if (!track.draggable)
|
|
229
|
+
return;
|
|
230
|
+
const newStartPosition = track.startPosition + delta * this.maxDuration;
|
|
231
|
+
const mainIndex = this.tracks.findIndex((item) => item.url && !item.draggable);
|
|
232
|
+
const mainTrack = this.tracks[mainIndex];
|
|
233
|
+
const minStart = (mainTrack ? mainTrack.startPosition : 0) - this.durations[index];
|
|
234
|
+
const maxStart = mainTrack ? mainTrack.startPosition + this.durations[mainIndex] : this.maxDuration;
|
|
235
|
+
if (newStartPosition >= minStart && newStartPosition <= maxStart) {
|
|
236
|
+
track.startPosition = newStartPosition;
|
|
237
|
+
this.initDurations(this.durations);
|
|
238
|
+
this.rendering.setContainerOffsets();
|
|
239
|
+
this.updatePosition(this.currentTime);
|
|
240
|
+
this.emit('start-position-change', { id: track.id, startPosition: newStartPosition });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
findCurrentTracks() {
|
|
244
|
+
// Find the audios at the current time
|
|
245
|
+
const indexes = [];
|
|
246
|
+
this.tracks.forEach((track, index) => {
|
|
247
|
+
if (track.url &&
|
|
248
|
+
this.currentTime >= track.startPosition &&
|
|
249
|
+
this.currentTime < track.startPosition + this.durations[index]) {
|
|
250
|
+
indexes.push(index);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
if (indexes.length === 0) {
|
|
254
|
+
const minStartTime = Math.min(...this.tracks.filter((t) => t.url).map((track) => track.startPosition));
|
|
255
|
+
indexes.push(this.tracks.findIndex((track) => track.startPosition === minStartTime));
|
|
256
|
+
}
|
|
257
|
+
return indexes;
|
|
258
|
+
}
|
|
259
|
+
startSync() {
|
|
260
|
+
const onFrame = () => {
|
|
261
|
+
const position = this.audios.reduce((pos, audio, index) => {
|
|
262
|
+
if (!audio.paused) {
|
|
263
|
+
pos = Math.max(pos, audio.currentTime + this.tracks[index].startPosition);
|
|
264
|
+
}
|
|
265
|
+
return pos;
|
|
266
|
+
}, this.currentTime);
|
|
267
|
+
if (position > this.currentTime) {
|
|
268
|
+
this.updatePosition(position, true);
|
|
269
|
+
}
|
|
270
|
+
this.frameRequest = requestAnimationFrame(onFrame);
|
|
271
|
+
};
|
|
272
|
+
onFrame();
|
|
273
|
+
}
|
|
274
|
+
play() {
|
|
275
|
+
this.startSync();
|
|
276
|
+
const indexes = this.findCurrentTracks();
|
|
277
|
+
indexes.forEach((index) => {
|
|
278
|
+
this.audios[index]?.play();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
pause() {
|
|
282
|
+
this.audios.forEach((audio) => audio.pause());
|
|
283
|
+
}
|
|
284
|
+
isPlaying() {
|
|
285
|
+
return this.audios.some((audio) => !audio.paused);
|
|
286
|
+
}
|
|
287
|
+
getCurrentTime() {
|
|
288
|
+
return this.currentTime;
|
|
289
|
+
}
|
|
290
|
+
/** Position percentage from 0 to 1 */
|
|
291
|
+
seekTo(position) {
|
|
292
|
+
const wasPlaying = this.isPlaying();
|
|
293
|
+
this.updatePosition(position * this.maxDuration);
|
|
294
|
+
if (wasPlaying)
|
|
295
|
+
this.play();
|
|
296
|
+
}
|
|
297
|
+
/** Set time in seconds */
|
|
298
|
+
setTime(time) {
|
|
299
|
+
const wasPlaying = this.isPlaying();
|
|
300
|
+
this.updatePosition(time);
|
|
301
|
+
if (wasPlaying)
|
|
302
|
+
this.play();
|
|
303
|
+
}
|
|
304
|
+
zoom(pxPerSec) {
|
|
305
|
+
this.options.minPxPerSec = pxPerSec;
|
|
306
|
+
this.wavesurfers.forEach((ws, index) => this.tracks[index].url && ws.zoom(pxPerSec));
|
|
307
|
+
this.rendering.setMainWidth(this.durations, this.maxDuration);
|
|
308
|
+
this.rendering.setContainerOffsets();
|
|
309
|
+
}
|
|
310
|
+
addTrack(track) {
|
|
311
|
+
const index = this.tracks.findIndex((t) => t.id === track.id);
|
|
312
|
+
if (index !== -1) {
|
|
313
|
+
this.tracks[index] = track;
|
|
314
|
+
this.initAudio(track).then((audio) => {
|
|
315
|
+
this.audios[index] = audio;
|
|
316
|
+
this.durations[index] = audio.duration;
|
|
317
|
+
this.initDurations(this.durations);
|
|
318
|
+
const container = this.rendering.containers[index];
|
|
319
|
+
container.innerHTML = '';
|
|
320
|
+
this.wavesurfers[index].destroy();
|
|
321
|
+
this.wavesurfers[index] = this.initWavesurfer(track, index);
|
|
322
|
+
const drag = initDragging(container, (delta) => this.onDrag(index, delta), this.options.rightButtonDrag);
|
|
323
|
+
this.wavesurfers[index].once('destroy', () => drag?.destroy());
|
|
324
|
+
this.initTimeline();
|
|
325
|
+
this.emit('canplay');
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
destroy() {
|
|
330
|
+
if (this.frameRequest)
|
|
331
|
+
cancelAnimationFrame(this.frameRequest);
|
|
332
|
+
this.rendering.destroy();
|
|
333
|
+
this.audios.forEach((audio) => {
|
|
334
|
+
audio.pause();
|
|
335
|
+
audio.src = '';
|
|
336
|
+
});
|
|
337
|
+
this.wavesurfers.forEach((ws) => {
|
|
338
|
+
ws.destroy();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId
|
|
342
|
+
setSinkId(sinkId) {
|
|
343
|
+
return Promise.all(this.wavesurfers.map((ws) => ws.setSinkId(sinkId)));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function initRendering(tracks, options) {
|
|
347
|
+
let pxPerSec = 0;
|
|
348
|
+
let durations = [];
|
|
349
|
+
let mainWidth = 0;
|
|
350
|
+
// Create a common container for all tracks
|
|
351
|
+
const scroll = document.createElement('div');
|
|
352
|
+
scroll.setAttribute('style', 'width: 100%; overflow-x: scroll; overflow-y: hidden; user-select: none;');
|
|
353
|
+
const wrapper = document.createElement('div');
|
|
354
|
+
wrapper.style.position = 'relative';
|
|
355
|
+
scroll.appendChild(wrapper);
|
|
356
|
+
options.container.appendChild(scroll);
|
|
357
|
+
// Create a common cursor
|
|
358
|
+
const cursor = document.createElement('div');
|
|
359
|
+
cursor.setAttribute('style', 'height: 100%; position: absolute; z-index: 10; top: 0; left: 0');
|
|
360
|
+
cursor.style.backgroundColor = options.cursorColor || '#000';
|
|
361
|
+
cursor.style.width = `${options.cursorWidth ?? 1}px`;
|
|
362
|
+
wrapper.appendChild(cursor);
|
|
363
|
+
const { clientWidth } = wrapper;
|
|
364
|
+
// Create containers for each track
|
|
365
|
+
const containers = tracks.map((track, index) => {
|
|
366
|
+
const container = document.createElement('div');
|
|
367
|
+
container.style.position = 'relative';
|
|
368
|
+
if (options.trackBorderColor && index > 0) {
|
|
369
|
+
const borderDiv = document.createElement('div');
|
|
370
|
+
borderDiv.setAttribute('style', `width: 100%; height: 2px; background-color: ${options.trackBorderColor}`);
|
|
371
|
+
wrapper.appendChild(borderDiv);
|
|
372
|
+
}
|
|
373
|
+
if (options.trackBackground && track.url) {
|
|
374
|
+
container.style.background = options.trackBackground;
|
|
375
|
+
}
|
|
376
|
+
// No audio on this track, so make it droppable
|
|
377
|
+
if (!track.url) {
|
|
378
|
+
const dropArea = document.createElement('div');
|
|
379
|
+
dropArea.setAttribute('style', `position: absolute; z-index: 10; left: 10px; top: 10px; right: 10px; bottom: 10px; border: 2px dashed ${options.trackBorderColor};`);
|
|
380
|
+
dropArea.addEventListener('dragover', (e) => {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
dropArea.style.background = options.trackBackground || '';
|
|
383
|
+
});
|
|
384
|
+
dropArea.addEventListener('dragleave', (e) => {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
dropArea.style.background = '';
|
|
387
|
+
});
|
|
388
|
+
dropArea.addEventListener('drop', (e) => {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
dropArea.style.background = '';
|
|
391
|
+
});
|
|
392
|
+
container.appendChild(dropArea);
|
|
393
|
+
}
|
|
394
|
+
wrapper.appendChild(container);
|
|
395
|
+
return container;
|
|
396
|
+
});
|
|
397
|
+
// Set the positions of each container
|
|
398
|
+
const setContainerOffsets = () => {
|
|
399
|
+
containers.forEach((container, i) => {
|
|
400
|
+
const offset = tracks[i].startPosition * pxPerSec;
|
|
401
|
+
if (durations[i]) {
|
|
402
|
+
container.style.width = `${durations[i] * pxPerSec}px`;
|
|
403
|
+
}
|
|
404
|
+
container.style.transform = `translateX(${offset}px)`;
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
return {
|
|
408
|
+
containers,
|
|
409
|
+
// Set the start offset
|
|
410
|
+
setContainerOffsets,
|
|
411
|
+
// Set the container width
|
|
412
|
+
setMainWidth: (trackDurations, maxDuration) => {
|
|
413
|
+
durations = trackDurations;
|
|
414
|
+
pxPerSec = Math.max(options.minPxPerSec || 0, clientWidth / maxDuration);
|
|
415
|
+
mainWidth = pxPerSec * maxDuration;
|
|
416
|
+
wrapper.style.width = `${mainWidth}px`;
|
|
417
|
+
setContainerOffsets();
|
|
418
|
+
},
|
|
419
|
+
// Update cursor position
|
|
420
|
+
updateCursor: (position, autoCenter) => {
|
|
421
|
+
cursor.style.left = `${Math.min(100, position * 100)}%`;
|
|
422
|
+
// Update scroll
|
|
423
|
+
const { clientWidth, scrollLeft } = scroll;
|
|
424
|
+
const center = clientWidth / 2;
|
|
425
|
+
const minScroll = autoCenter ? center : clientWidth;
|
|
426
|
+
const pos = position * mainWidth;
|
|
427
|
+
if (pos > scrollLeft + minScroll || pos < scrollLeft) {
|
|
428
|
+
scroll.scrollLeft = pos - center;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
// Click to seek
|
|
432
|
+
addClickHandler: (onClick) => {
|
|
433
|
+
wrapper.addEventListener('click', (e) => {
|
|
434
|
+
const rect = wrapper.getBoundingClientRect();
|
|
435
|
+
const x = e.clientX - rect.left;
|
|
436
|
+
const position = x / wrapper.offsetWidth;
|
|
437
|
+
onClick(position);
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
// Destroy the container
|
|
441
|
+
destroy: () => {
|
|
442
|
+
scroll.remove();
|
|
443
|
+
},
|
|
444
|
+
// Do something on drop
|
|
445
|
+
addDropHandler: (onDrop) => {
|
|
446
|
+
tracks.forEach((track, index) => {
|
|
447
|
+
if (!track.url) {
|
|
448
|
+
const droppable = containers[index].querySelector('div');
|
|
449
|
+
droppable?.addEventListener('drop', (e) => {
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
onDrop(track.id);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function initDragging(container, onDrag, rightButtonDrag = false) {
|
|
459
|
+
const wrapper = container.parentElement;
|
|
460
|
+
if (!wrapper)
|
|
461
|
+
return;
|
|
462
|
+
// Dragging tracks to set position
|
|
463
|
+
let dragStart = null;
|
|
464
|
+
container.addEventListener('contextmenu', (e) => {
|
|
465
|
+
rightButtonDrag && e.preventDefault();
|
|
466
|
+
});
|
|
467
|
+
// Drag start
|
|
468
|
+
container.addEventListener('mousedown', (e) => {
|
|
469
|
+
if (rightButtonDrag && e.button !== 2)
|
|
470
|
+
return;
|
|
471
|
+
const rect = wrapper.getBoundingClientRect();
|
|
472
|
+
dragStart = e.clientX - rect.left;
|
|
473
|
+
container.style.cursor = 'grabbing';
|
|
474
|
+
});
|
|
475
|
+
// Drag end
|
|
476
|
+
const onMouseUp = (e) => {
|
|
477
|
+
if (dragStart != null) {
|
|
478
|
+
e.stopPropagation();
|
|
479
|
+
dragStart = null;
|
|
480
|
+
container.style.cursor = '';
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
// Drag move
|
|
484
|
+
const onMouseMove = (e) => {
|
|
485
|
+
if (dragStart == null)
|
|
486
|
+
return;
|
|
487
|
+
const rect = wrapper.getBoundingClientRect();
|
|
488
|
+
const x = e.clientX - rect.left;
|
|
489
|
+
const diff = x - dragStart;
|
|
490
|
+
if (diff > 1 || diff < -1) {
|
|
491
|
+
dragStart = x;
|
|
492
|
+
onDrag(diff / wrapper.offsetWidth);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
document.body.addEventListener('mouseup', onMouseUp);
|
|
496
|
+
document.body.addEventListener('mousemove', onMouseMove);
|
|
497
|
+
return {
|
|
498
|
+
destroy: () => {
|
|
499
|
+
document.body.removeEventListener('mouseup', onMouseUp);
|
|
500
|
+
document.body.removeEventListener('mousemove', onMouseMove);
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
export default MultiTrack;
|