wavesurfer-multitracks
Version:
A modification to the multi-track super-plugin for wavesurfer.js that allows each track to have a different container
99 lines (76 loc) • 109 kB
JavaScript
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["Multitracks"] = factory();
else
root["Multitracks"] = factory();
})(this, () => {
return /******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/event-emitter.ts":
/*!******************************!*\
!*** ./src/event-emitter.ts ***!
\******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/** A simple event emitter that can be used to listen to and emit events. */\nclass EventEmitter {\n constructor() {\n this.listeners = {};\n }\n /** Subscribe to an event. Returns an unsubscribe function. */\n on(eventName, listener) {\n if (!this.listeners[eventName]) {\n this.listeners[eventName] = new Set();\n }\n this.listeners[eventName].add(listener);\n return () => this.un(eventName, listener);\n }\n /** Subscribe to an event only once */\n once(eventName, listener) {\n // The actual subscription\n const unsubscribe = this.on(eventName, listener);\n // Another subscription that will unsubscribe the actual subscription and itself after the first event\n const unsubscribeOnce = this.on(eventName, () => {\n unsubscribe();\n unsubscribeOnce();\n });\n return unsubscribe;\n }\n /** Unsubscribe from an event */\n un(eventName, listener) {\n if (this.listeners[eventName]) {\n if (listener) {\n this.listeners[eventName].delete(listener);\n }\n else {\n delete this.listeners[eventName];\n }\n }\n }\n /** Clear all events */\n unAll() {\n this.listeners = {};\n }\n /** Emit an event */\n emit(eventName, ...args) {\n if (this.listeners[eventName]) {\n this.listeners[eventName].forEach((listener) => listener(...args));\n }\n }\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (EventEmitter);\n\n\n//# sourceURL=webpack://Multitracks/./src/event-emitter.ts?");
/***/ }),
/***/ "./src/multitracks.ts":
/*!****************************!*\
!*** ./src/multitracks.ts ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var wavesurfer_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! wavesurfer.js */ \"./node_modules/wavesurfer.js/dist/wavesurfer.js\");\n/* harmony import */ var wavesurfer_js_dist_plugins_regions_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! wavesurfer.js/dist/plugins/regions.js */ \"./node_modules/wavesurfer.js/dist/plugins/regions.js\");\n/* harmony import */ var wavesurfer_js_dist_plugins_timeline_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! wavesurfer.js/dist/plugins/timeline.js */ \"./node_modules/wavesurfer.js/dist/plugins/timeline.js\");\n/* harmony import */ var wavesurfer_js_dist_plugins_envelope_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! wavesurfer.js/dist/plugins/envelope.js */ \"./node_modules/wavesurfer.js/dist/plugins/envelope.js\");\n/* harmony import */ var _event_emitter__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./event-emitter */ \"./src/event-emitter.ts\");\n/**\n * MultiTracks is a super-plugin for creating a MultiTrack audio player.\n * Individual tracks are synced and played together.\n * They can be dragged to set their start position.\n * The top track is meant for dragging'n'dropping an additional track id (not a file).\n */\n\n\n\n\n\nclass MultiTracks extends _event_emitter__WEBPACK_IMPORTED_MODULE_4__[\"default\"] {\n static create(tracks, options) {\n return new MultiTracks(tracks, options);\n }\n constructor(tracks, options) {\n super();\n this.audios = [];\n this.wavesurfers = [];\n this.durations = [];\n this.currentTime = 0;\n this.maxDuration = 0;\n this.isDragging = false;\n this.frameRequest = null;\n this.timer = null;\n this.subscriptions = [];\n this.timeline = null;\n this.tracks = tracks.map((track) => ({\n ...track,\n startPosition: track.startPosition || 0,\n peaks: track.peaks || (track.url ? undefined : [new Float32Array()]),\n }));\n this.options = options;\n this.rendering = initRendering(this.tracks, this.options);\n this.rendering.addDropHandler((trackId) => {\n this.emit(\"drop\", { id: trackId });\n });\n this.initAllAudios().then((durations) => {\n this.initDurations(durations);\n this.initAllWavesurfers();\n this.rendering.containers.forEach(({ container, wrapper }, index) => {\n const drag = initDragging(wrapper, (delta) => this.onDrag(index, delta), options.rightButtonDrag);\n this.wavesurfers[index].once(\"destroy\", () => drag?.destroy());\n // Click to seek\n container.addEventListener(\"click\", (e) => {\n if (this.isDragging)\n return;\n // determine poisition for current track\n const rect = container.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const position = x / container.offsetWidth;\n const time = position * durations[index] + tracks[index].startPosition;\n this.seekTo(time);\n });\n });\n this.emit(\"canplay\");\n });\n }\n initDurations(durations) {\n this.durations = durations;\n this.maxDuration = this.tracks.reduce((max, track, index) => {\n return Math.max(max, track.startPosition + durations[index]);\n }, 0);\n this.rendering.setMainWidth(durations, this.maxDuration);\n }\n initAudio(track) {\n const audio = new Audio(track.url);\n return new Promise((resolve) => {\n if (!audio.src)\n return resolve(audio);\n audio.addEventListener(\"loadedmetadata\", () => resolve(audio), {\n once: true,\n });\n });\n }\n async initAllAudios() {\n this.audios = await Promise.all(this.tracks.map((track) => this.initAudio(track)));\n return this.audios.map((a) => (a.src ? a.duration : 0));\n }\n initWavesurfer(track, index) {\n const { container } = this.rendering.containers[index];\n // Create a wavesurfer instance\n const ws = wavesurfer_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"].create({\n ...track.options,\n container,\n minPxPerSec: 0,\n media: this.audios[index],\n peaks: track.peaks,\n cursorColor: \"transparent\",\n cursorWidth: 0,\n interact: false,\n hideScrollbar: true,\n });\n // Regions and markers\n const wsRegions = wavesurfer_js_dist_plugins_regions_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"].create();\n ws.registerPlugin(wsRegions);\n this.subscriptions.push(ws.once(\"decode\", () => {\n // Start and end cues\n if (track.startCue != null || track.endCue != null) {\n const { startCue = 0, endCue = this.durations[index] } = track;\n const startCueRegion = wsRegions.addRegion({\n start: 0,\n end: startCue,\n color: \"rgba(0, 0, 0, 0.7)\",\n drag: false,\n });\n const endCueRegion = wsRegions.addRegion({\n start: endCue,\n end: endCue + this.durations[index],\n color: \"rgba(0, 0, 0, 0.7)\",\n drag: false,\n });\n // Allow resizing only from one side\n startCueRegion.element.firstElementChild?.remove();\n endCueRegion.element.lastChild?.remove();\n // Prevent clicks when dragging\n // Update the start and end cues on resize\n this.subscriptions.push(startCueRegion.on(\"update-end\", () => {\n track.startCue = startCueRegion.end;\n this.emit(\"start-cue-change\", {\n id: track.id,\n startCue: track.startCue,\n });\n }), endCueRegion.on(\"update-end\", () => {\n track.endCue = endCueRegion.start;\n this.emit(\"end-cue-change\", {\n id: track.id,\n endCue: track.endCue,\n });\n }));\n }\n // Intro\n if (track.intro) {\n const introRegion = wsRegions.addRegion({\n start: 0,\n end: track.intro.endTime,\n content: track.intro.label,\n color: this.options.trackBackground,\n drag: false,\n });\n introRegion.element.querySelector('[data-resize=\"left\"]')?.remove();\n introRegion.element.parentElement.style.mixBlendMode = \"plus-lighter\";\n if (track.intro.color) {\n introRegion.element.querySelector('[data-resize=\"right\"]').style.borderColor = track.intro.color;\n }\n this.subscriptions.push(introRegion.on(\"update-end\", () => {\n this.emit(\"intro-end-change\", {\n id: track.id,\n endTime: introRegion.end,\n });\n }));\n }\n // Render markers\n if (track.markers) {\n track.markers.forEach((marker) => {\n wsRegions.addRegion({\n start: marker.time,\n content: marker.label,\n color: marker.color,\n resize: false,\n });\n });\n }\n }));\n // Envelope\n const envelope = ws.registerPlugin(wavesurfer_js_dist_plugins_envelope_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"].create({\n ...this.options.envelopeOptions,\n fadeInStart: track.startCue,\n fadeInEnd: track.fadeInEnd,\n fadeOutStart: track.fadeOutStart,\n fadeOutEnd: track.endCue,\n volume: track.volume,\n }));\n this.subscriptions.push(envelope.on(\"volume-change\", (volume) => {\n this.setIsDragging();\n this.emit(\"volume-change\", { id: track.id, volume });\n }), envelope.on(\"fade-in-change\", (time) => {\n this.setIsDragging();\n this.emit(\"fade-in-change\", { id: track.id, fadeInEnd: time });\n }), envelope.on(\"fade-out-change\", (time) => {\n this.setIsDragging();\n this.emit(\"fade-out-change\", { id: track.id, fadeOutStart: time });\n }), this.on(\"start-cue-change\", ({ id, startCue }) => {\n if (id === track.id) {\n envelope.setStartTime(startCue);\n }\n }), this.on(\"end-cue-change\", ({ id, endCue }) => {\n if (id === track.id) {\n envelope.setEndTime(endCue);\n }\n }));\n return ws;\n }\n initAllWavesurfers() {\n const wavesurfers = this.tracks.map((track, index) => {\n return this.initWavesurfer(track, index);\n });\n this.wavesurfers = wavesurfers;\n this.initTimeline();\n }\n initTimeline() {\n if (this.timeline)\n this.timeline.destroy();\n this.timeline = this.wavesurfers[0].registerPlugin(wavesurfer_js_dist_plugins_timeline_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"].create({\n duration: this.maxDuration,\n container: this.rendering.containers[0].container.parentElement,\n }));\n }\n updatePosition(time, autoCenter = false) {\n const precisionSeconds = 0.3;\n const isPaused = !this.isPlaying();\n if (time !== this.currentTime) {\n this.currentTime = time;\n this.rendering.containers.forEach((container, i) => {\n this.rendering.updateCursor(container, (time - this.tracks[i].startPosition) / this.durations[i], autoCenter);\n });\n }\n // Update the current time of each audio\n this.tracks.forEach((track, index) => {\n const audio = this.audios[index];\n const duration = this.durations[index];\n const newTime = time - track.startPosition;\n if (Math.abs(audio.currentTime - newTime) > precisionSeconds) {\n audio.currentTime = newTime;\n }\n // If the position is out of the track bounds, pause it\n if (isPaused || newTime < 0 || newTime > duration) {\n !audio.paused && audio.pause();\n }\n else if (!isPaused) {\n // If the position is in the track bounds, play it\n audio.paused && audio.play();\n }\n // Unmute if cue is reached\n const newVolume = newTime >= (track.startCue || 0) && newTime < (track.endCue || Infinity)\n ? 1\n : 0;\n if (newVolume !== audio.volume)\n audio.volume = newVolume;\n });\n }\n setIsDragging() {\n // Prevent click events when dragging\n this.isDragging = true;\n if (this.timer)\n clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.isDragging = false;\n }, 300);\n }\n onDrag(index, delta) {\n this.setIsDragging();\n const track = this.tracks[index];\n if (!track.draggable)\n return;\n const newStartPosition = track.startPosition + delta * this.maxDuration;\n const mainIndex = this.tracks.findIndex((item) => item.url && !item.draggable);\n const mainTrack = this.tracks[mainIndex];\n const minStart = (mainTrack ? mainTrack.startPosition : 0) - this.durations[index];\n const maxStart = mainTrack\n ? mainTrack.startPosition + this.durations[mainIndex]\n : this.maxDuration;\n if (newStartPosition >= minStart && newStartPosition <= maxStart) {\n track.startPosition = newStartPosition;\n this.initDurations(this.durations);\n this.rendering.setContainerOffsets();\n this.updatePosition(this.currentTime);\n this.emit(\"start-position-change\", {\n id: track.id,\n startPosition: newStartPosition,\n });\n }\n }\n findCurrentTracks() {\n // Find the audios at the current time\n const indexes = [];\n this.tracks.forEach((track, index) => {\n if (track.url &&\n this.currentTime >= track.startPosition &&\n this.currentTime < track.startPosition + this.durations[index]) {\n indexes.push(index);\n }\n });\n if (indexes.length === 0) {\n const minStartTime = Math.min(...this.tracks.filter((t) => t.url).map((track) => track.startPosition));\n indexes.push(this.tracks.findIndex((track) => track.startPosition === minStartTime));\n }\n return indexes;\n }\n startSync() {\n const onFrame = () => {\n const syncTime = this.audios.reduce((pos, audio, index) => {\n let position = pos;\n if (!audio.paused) {\n position = Math.max(pos, audio.currentTime + this.tracks[index].startPosition);\n }\n return position;\n }, this.currentTime);\n if (syncTime > this.currentTime) {\n this.updatePosition(syncTime, true);\n }\n this.frameRequest = requestAnimationFrame(onFrame);\n };\n onFrame();\n }\n play() {\n this.startSync();\n const indexes = this.findCurrentTracks();\n indexes.forEach((index) => {\n this.audios[index]?.play();\n });\n }\n pause() {\n this.audios.forEach((audio) => audio.pause());\n }\n isPlaying() {\n return this.audios.some((audio) => !audio.paused);\n }\n getCurrentTime() {\n return this.currentTime;\n }\n // Seek to absolute time for other tracks based on position of clicked track\n seekTo(time) {\n const wasPlaying = this.isPlaying();\n this.wavesurfers.forEach(() => this.updatePosition(time));\n if (wasPlaying)\n this.play();\n }\n /** Set time in seconds */\n setTime(time) {\n const wasPlaying = this.isPlaying();\n this.updatePosition(time);\n if (wasPlaying)\n this.play();\n }\n zoom(pxPerSec) {\n this.options.minPxPerSec = pxPerSec;\n this.wavesurfers.forEach((ws, index) => this.tracks[index].url && ws.zoom(pxPerSec));\n this.rendering.setMainWidth(this.durations, this.maxDuration);\n }\n addTrack(track) {\n const index = this.tracks.findIndex((t) => t.id === track.id);\n if (index !== -1) {\n this.tracks[index] = track;\n this.initAudio(track).then((audio) => {\n this.audios[index] = audio;\n this.durations[index] = audio.duration;\n this.initDurations(this.durations);\n const { container } = this.rendering.containers[index];\n container.innerHTML = \"\";\n this.wavesurfers[index].destroy();\n this.wavesurfers[index] = this.initWavesurfer(track, index);\n const drag = initDragging(container, (delta) => this.onDrag(index, delta), this.options.rightButtonDrag);\n this.wavesurfers[index].once(\"destroy\", () => drag?.destroy());\n this.initTimeline();\n this.emit(\"canplay\");\n });\n }\n }\n destroy() {\n if (this.frameRequest)\n cancelAnimationFrame(this.frameRequest);\n this.rendering.destroy();\n this.audios.forEach((audio) => {\n audio.pause();\n audio.src = \"\";\n });\n this.wavesurfers.forEach((ws) => {\n ws.destroy();\n });\n }\n // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId\n setSinkId(sinkId) {\n return Promise.all(this.wavesurfers.map((ws) => ws.setSinkId(sinkId)));\n }\n}\nfunction initRendering(tracks, options) {\n let pxPerSec = 0;\n let durations = [];\n let mainWidth = 0;\n const multiWrapper = options.container || document.body;\n // Create containers for each track\n const containers = tracks.map((track, index) => {\n const container = track.container || document.createElement(\"div\");\n // Create the scrollbar for each track\n const scroll = document.createElement(\"div\");\n scroll.setAttribute(\"style\", `width: 100%; overflow-x: ${track.hideScrollbar ? \"hidden\" : \"scroll\"}; overflow-y: hidden; user-select: none; position: relative;`);\n const wrapper = document.createElement(\"div\");\n wrapper.style.position = \"relative\";\n scroll.appendChild(wrapper);\n container.appendChild(scroll);\n // Create a cursor for each track\n const cursor = document.createElement(\"div\");\n cursor.setAttribute(\"style\", \"height: 100%; position: absolute; z-index: 10; top: 0; left: 0\");\n cursor.style.backgroundColor = options.cursorColor || \"#000\";\n cursor.style.width = `${options.cursorWidth ?? 1}px`;\n container.appendChild(cursor);\n if (options.trackBorderColor && index > 0) {\n const borderDiv = document.createElement(\"div\");\n borderDiv.setAttribute(\"style\", `width: 100%; height: 2px; background-color: ${options.trackBorderColor}`);\n wrapper.appendChild(borderDiv);\n }\n if (options.trackBackground && track.url) {\n container.style.background = options.trackBackground;\n }\n // No audio on this track, so make it droppable\n if (!track.url) {\n const dropArea = document.createElement(\"div\");\n dropArea.setAttribute(\"style\", `position: absolute; z-index: 10; left: 10px; top: 10px; right: 10px; bottom: 10px; border: 2px dashed ${options.trackBorderColor};`);\n dropArea.addEventListener(\"dragover\", (e) => {\n e.preventDefault();\n dropArea.style.background = options.trackBackground || \"\";\n });\n dropArea.addEventListener(\"dragleave\", (e) => {\n e.preventDefault();\n dropArea.style.background = \"\";\n });\n dropArea.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n dropArea.style.background = \"\";\n });\n container.appendChild(dropArea);\n }\n if (multiWrapper)\n multiWrapper.appendChild(container);\n return { container, scroll, cursor, wrapper };\n });\n // Set the positions of each container\n const setContainerOffsets = () => {\n containers.forEach(({ container }, i) => {\n const offset = tracks[i].startPosition * pxPerSec;\n if (durations[i]) {\n container.style.width = `${durations[i] * pxPerSec}px`;\n }\n container.style.transform = `translateX(${offset}px)`;\n });\n };\n return {\n containers,\n // Set the start offset\n setContainerOffsets,\n // Set the container width\n setMainWidth: (trackDurations, maxDuration) => {\n durations = trackDurations;\n durations.forEach((_, i) => {\n pxPerSec = Math.max(options.minPxPerSec || 0, containers[i].wrapper.clientWidth / maxDuration);\n mainWidth = pxPerSec * maxDuration;\n containers[i].container.style.width = `${mainWidth}px`;\n });\n setContainerOffsets();\n },\n // Update cursor position\n updateCursor: ({ cursor, scroll }, position, autoCenter) => {\n cursor.style.left = `${Math.min(100, position * 100)}%`;\n // Update scroll\n const { clientWidth, scrollLeft } = scroll;\n const center = clientWidth / 2;\n const minScroll = autoCenter ? center : clientWidth;\n const pos = position * mainWidth;\n if (pos > scrollLeft + minScroll || pos < scrollLeft) {\n scroll.scrollLeft = pos - center;\n }\n },\n // Destroy the container\n destroy: () => {\n containers.forEach(({ scroll }) => scroll.remove());\n },\n // Do something on drop\n addDropHandler: (onDrop) => {\n tracks.forEach((track, index) => {\n if (!track.url) {\n const droppable = containers[index].wrapper.querySelector(\"div\");\n droppable?.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n onDrop(track.id);\n });\n }\n });\n },\n };\n}\nfunction initDragging(container, onDrag, rightButtonDrag = false) {\n const wrapper = container.parentElement;\n if (!wrapper)\n return;\n // Dragging tracks to set position\n let dragStart = null;\n container.addEventListener(\"contextmenu\", (e) => {\n rightButtonDrag && e.preventDefault();\n });\n // Drag start\n container.addEventListener(\"mousedown\", (e) => {\n if (rightButtonDrag && e.button !== 2)\n return;\n const rect = wrapper.getBoundingClientRect();\n dragStart = e.clientX - rect.left;\n container.style.cursor = \"grabbing\";\n });\n // Drag end\n const onMouseUp = (e) => {\n if (dragStart != null) {\n e.stopPropagation();\n dragStart = null;\n container.style.cursor = \"\";\n }\n };\n // Drag move\n const onMouseMove = (e) => {\n if (dragStart == null)\n return;\n const rect = wrapper.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const diff = x - dragStart;\n if (diff > 1 || diff < -1) {\n dragStart = x;\n onDrag(diff / wrapper.offsetWidth);\n }\n };\n document.body.addEventListener(\"mouseup\", onMouseUp);\n document.body.addEventListener(\"mousemove\", onMouseMove);\n return {\n destroy: () => {\n document.body.removeEventListener(\"mouseup\", onMouseUp);\n document.body.removeEventListener(\"mousemove\", onMouseMove);\n },\n };\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (MultiTracks);\n\n\n//# sourceURL=webpack://Multitracks/./src/multitracks.ts?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/base-plugin.js":
/*!********************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/base-plugin.js ***!
\********************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ BasePlugin: () => (/* binding */ BasePlugin),\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _event_emitter_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./event-emitter.js */ \"./node_modules/wavesurfer.js/dist/event-emitter.js\");\n\nclass BasePlugin extends _event_emitter_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"] {\n constructor(options) {\n super();\n this.subscriptions = [];\n this.options = options;\n }\n onInit() {\n // Overridden in plugin definition\n return;\n }\n init(wavesurfer) {\n this.wavesurfer = wavesurfer;\n this.onInit();\n }\n destroy() {\n this.subscriptions.forEach((unsubscribe) => unsubscribe());\n }\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (BasePlugin);\n\n\n//# sourceURL=webpack://Multitracks/./node_modules/wavesurfer.js/dist/base-plugin.js?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/decoder.js":
/*!****************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/decoder.js ***!
\****************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/** Decode an array buffer into an audio buffer */\nasync function decode(audioData, sampleRate) {\n const audioCtx = new AudioContext({ sampleRate });\n const decode = audioCtx.decodeAudioData(audioData);\n decode.finally(() => audioCtx.close());\n return decode;\n}\n/** Normalize peaks to -1..1 */\nfunction normalize(channelData) {\n const firstChannel = channelData[0];\n if (firstChannel.some((n) => n > 1 || n < -1)) {\n const length = firstChannel.length;\n let max = 0;\n for (let i = 0; i < length; i++) {\n const absN = Math.abs(firstChannel[i]);\n if (absN > max)\n max = absN;\n }\n for (const channel of channelData) {\n for (let i = 0; i < length; i++) {\n channel[i] /= max;\n }\n }\n }\n return channelData;\n}\n/** Create an audio buffer from pre-decoded audio data */\nfunction createBuffer(channelData, duration) {\n // If a single array of numbers is passed, make it an array of arrays\n if (typeof channelData[0] === 'number')\n channelData = [channelData];\n // Normalize to -1..1\n normalize(channelData);\n return {\n duration,\n length: channelData[0].length,\n sampleRate: channelData[0].length / duration,\n numberOfChannels: channelData.length,\n getChannelData: (i) => channelData?.[i],\n copyFromChannel: AudioBuffer.prototype.copyFromChannel,\n copyToChannel: AudioBuffer.prototype.copyToChannel,\n };\n}\nconst Decoder = {\n decode,\n createBuffer,\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Decoder);\n\n\n//# sourceURL=webpack://Multitracks/./node_modules/wavesurfer.js/dist/decoder.js?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/event-emitter.js":
/*!**********************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/event-emitter.js ***!
\**********************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/** A simple event emitter that can be used to listen to and emit events. */\nclass EventEmitter {\n constructor() {\n this.listeners = {};\n }\n /** Subscribe to an event. Returns an unsubscribe function. */\n on(eventName, listener) {\n if (!this.listeners[eventName]) {\n this.listeners[eventName] = new Set();\n }\n this.listeners[eventName].add(listener);\n return () => this.un(eventName, listener);\n }\n /** Subscribe to an event only once */\n once(eventName, listener) {\n // The actual subscription\n const unsubscribe = this.on(eventName, listener);\n // Another subscription that will unsubscribe the actual subscription and itself after the first event\n const unsubscribeOnce = this.on(eventName, () => {\n unsubscribe();\n unsubscribeOnce();\n });\n return unsubscribe;\n }\n /** Unsubscribe from an event */\n un(eventName, listener) {\n if (this.listeners[eventName]) {\n if (listener) {\n this.listeners[eventName].delete(listener);\n }\n else {\n delete this.listeners[eventName];\n }\n }\n }\n /** Clear all events */\n unAll() {\n this.listeners = {};\n }\n /** Emit an event */\n emit(eventName, ...args) {\n if (this.listeners[eventName]) {\n this.listeners[eventName].forEach((listener) => listener(...args));\n }\n }\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (EventEmitter);\n\n\n//# sourceURL=webpack://Multitracks/./node_modules/wavesurfer.js/dist/event-emitter.js?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/fetcher.js":
/*!****************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/fetcher.js ***!
\****************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nasync function fetchArrayBuffer(url) {\n return fetch(url).then((response) => response.arrayBuffer());\n}\nconst Fetcher = {\n fetchArrayBuffer,\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Fetcher);\n\n\n//# sourceURL=webpack://Multitracks/./node_modules/wavesurfer.js/dist/fetcher.js?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/player.js":
/*!***************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/player.js ***!
\***************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _event_emitter_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./event-emitter.js */ \"./node_modules/wavesurfer.js/dist/event-emitter.js\");\n\nclass Player extends _event_emitter_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"] {\n constructor(options) {\n super();\n this.isExternalMedia = false;\n if (options.media) {\n this.media = options.media;\n this.isExternalMedia = true;\n }\n else {\n this.media = document.createElement('audio');\n }\n // Autoplay\n if (options.autoplay) {\n this.media.autoplay = true;\n }\n // Speed\n if (options.playbackRate != null) {\n this.media.playbackRate = options.playbackRate;\n }\n }\n onMediaEvent(event, callback, options) {\n this.media.addEventListener(event, callback, options);\n return () => this.media.removeEventListener(event, callback);\n }\n onceMediaEvent(event, callback) {\n return this.onMediaEvent(event, callback, { once: true });\n }\n revokeSrc() {\n const src = this.media.currentSrc || this.media.src || '';\n if (src.startsWith('blob:')) {\n URL.revokeObjectURL(this.media.currentSrc);\n }\n }\n setSrc(url, arrayBuffer) {\n const src = this.media.currentSrc || this.media.src || '';\n if (src === url)\n return;\n this.revokeSrc();\n const newSrc = arrayBuffer ? URL.createObjectURL(new Blob([arrayBuffer], { type: 'audio/wav' })) : url;\n this.media.src = newSrc;\n }\n destroy() {\n this.media.pause();\n this.revokeSrc();\n if (!this.isExternalMedia) {\n this.media.remove();\n }\n }\n /** Start playing the audio */\n play() {\n return this.media.play();\n }\n /** Pause the audio */\n pause() {\n this.media.pause();\n }\n /** Check if the audio is playing */\n isPlaying() {\n return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;\n }\n /** Jumpt to a specific time in the audio (in seconds) */\n setTime(time) {\n this.media.currentTime = time;\n }\n /** Get the duration of the audio in seconds */\n getDuration() {\n return this.media.duration;\n }\n /** Get the current audio position in seconds */\n getCurrentTime() {\n return this.media.currentTime;\n }\n /** Get the audio volume */\n getVolume() {\n return this.media.volume;\n }\n /** Set the audio volume */\n setVolume(volume) {\n this.media.volume = volume;\n }\n /** Get the audio muted state */\n getMuted() {\n return this.media.muted;\n }\n /** Mute or unmute the audio */\n setMuted(muted) {\n this.media.muted = muted;\n }\n /** Get the playback speed */\n getPlaybackRate() {\n return this.media.playbackRate;\n }\n /** Set the playback speed, pass an optional false to NOT preserve the pitch */\n setPlaybackRate(rate, preservePitch) {\n // preservePitch is true by default in most browsers\n if (preservePitch != null) {\n this.media.preservesPitch = preservePitch;\n }\n this.media.playbackRate = rate;\n }\n /** Get the HTML media element */\n getMediaElement() {\n return this.media;\n }\n /** Set a sink id to change the audio output device */\n setSinkId(sinkId) {\n // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId\n const media = this.media;\n return media.setSinkId(sinkId);\n }\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Player);\n\n\n//# sourceURL=webpack://Multitracks/./node_modules/wavesurfer.js/dist/player.js?");
/***/ }),
/***/ "./node_modules/wavesurfer.js/dist/plugins/envelope.js":
/*!*************************************************************!*\
!*** ./node_modules/wavesurfer.js/dist/plugins/envelope.js ***!
\*************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _base_plugin_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../base-plugin.js */ \"./node_modules/wavesurfer.js/dist/base-plugin.js\");\n/* harmony import */ var _event_emitter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../event-emitter.js */ \"./node_modules/wavesurfer.js/dist/event-emitter.js\");\n/**\n * Envelope is a visual UI for controlling the audio volume and add fade-in and fade-out effects.\n */\n\n\nconst defaultOptions = {\n fadeInStart: 0,\n fadeOutEnd: 0,\n fadeInEnd: 0,\n fadeOutStart: 0,\n lineWidth: 4,\n lineColor: 'rgba(0, 0, 255, 0.5)',\n dragPointSize: 10,\n dragPointFill: 'rgba(255, 255, 255, 0.8)',\n dragPointStroke: 'rgba(255, 255, 255, 0.8)',\n};\nclass Polyline extends _event_emitter_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"] {\n constructor(options, wrapper) {\n super();\n this.top = 0;\n // An padding to make the envelope fit into the SVG\n this.padding = options.dragPointSize / 2 + 1;\n // SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', '100%');\n svg.setAttribute('height', '100%');\n svg.setAttribute('viewBox', '0 0 0 0');\n svg.setAttribute('preserveAspectRatio', 'none');\n svg.setAttribute('style', 'position: absolute; left: 0; top: 0; z-index: 4; pointer-events: none;');\n svg.setAttribute('part', 'envelope');\n this.svg = svg;\n // A polyline representing the envelope\n const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');\n polyline.setAttribute('points', '0,0 0,0 0,0 0,0');\n polyline.setAttribute('stroke', options.lineColor);\n polyline.setAttribute('stroke-width', options.lineWidth);\n polyline.setAttribute('fill', 'none');\n polyline.setAttribute('style', 'pointer-events: none;');\n polyline.setAttribute('part', 'polyline');\n svg.appendChild(polyline);\n // Draggable top line\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('stroke', 'transparent');\n line.setAttribute('stroke-width', (options.lineWidth * 3).toString());\n line.setAttribute('style', 'cursor: ns-resize; pointer-events: all;');\n line.setAttribute('part', 'line');\n svg.appendChild(line);\n [0, 1].forEach(() => {\n const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('r', (options.dragPointSize / 2).toString());\n circle.setAttribute('fill', options.dragPointFill);\n circle.setAttribute('stroke', options.dragPointStroke || options.dragPointFill);\n circle.setAttribute('stroke-width', '2');\n circle.setAttribute('style', 'cursor: ew-resize; pointer-events: all;');\n circle.setAttribute('part', 'circle');\n svg.appendChild(circle);\n });\n wrapper.appendChild(svg);\n // Init dtagging\n {\n // On top line drag\n const onDragY = (dy) => {\n const newTop = this.top + dy;\n const { height } = svg.viewBox.baseVal;\n if (newTop < -0.5 || newTop > height)\n return;\n const relativeY = Math.min(1, Math.max(0, (height - newTop) / height));\n this.emit('line-move', relativeY);\n };\n // On points drag\n const onDragX = (index, dx) => {\n const point = polyline.points.getItem(index);\n const newX = point.x + dx;\n const { width } = svg.viewBox.baseVal;\n this.emit('point-move', index, newX / width);\n };\n // Draggable top line of the polyline\n this.makeDraggable(line, (_, dy) => onDragY(dy));\n // Make each point draggable\n const draggables = this.svg.querySelectorAll('circle');\n Array.from(draggables).forEach((draggable, index) => {\n this.makeDraggable(draggable, (dx) => onDragX(index + 1, dx));\n });\n }\n }\n makeDraggable(draggable, onDrag) {\n draggable.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n });\n draggable.addEventListener('mousedown', (e) => {\n e.preventDefault();\n e.stopPropagation();\n let x = e.clientX;\n let y = e.clientY;\n const move = (e) => {\n const dx = e.clientX - x;\n const dy = e.clientY - y;\n x = e.clientX;\n y = e.clientY;\n onDrag(dx, dy);\n };\n const up = () => {\n document.removeEventListener('mousemove', move);\n document.removeEventListener('mouseup', up);\n };\n document.addEventListener('mousemove', move);\n document.addEventListener('mouseup', up);\n });\n }\n update({ x1, x2, x3, x4, y }) {\n const width = this.svg.clientWidth;\n const height = this.svg.clientHeight;\n this.top = height - y * height;\n const paddedTop = Math.max(this.padding, Math.min(this.top, height - this.padding));\n this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n const polyline = this.svg.querySelector('polyline');\n const { points } = polyline;\n points.getItem(0).x = x1 * width;\n points.getItem(0).y = height;\n points.getItem(1).x = x2 * width;\n points.getItem(1).y = paddedTop;\n points.getItem(2).x = x3 * width;\n points.getItem(2).y = paddedTop;\n points.getItem(3).x = x4 * width;\n points.getItem(3).y = height;\n const line = this.svg.querySelector('line');\n line.setAttribute('x1', points.getItem(1).x.toString());\n line.setAttribute('x2', points.getItem(2).x.toString());\n line.setAttribute('y1', paddedTop.toString());\n line.setAttribute('y2', paddedTop.toString());\n const circles = this.svg.querySelectorAll('circle');\n Array.from(circles).forEach((circle, i) => {\n const point = points.getItem(i + 1);\n circle.setAttribute('cx', point.x.toString());\n circle.setAttribute('cy', point.y.toString());\n });\n }\n destroy() {\n this.svg.remove();\n }\n}\nclass EnvelopePlugin extends _base_plugin_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"] {\n constructor(options) {\n super(options);\n this.polyline = null;\n this.audioContext = null;\n this.gainNode = null;\n this.volume = 1;\n this.isFadingIn = false;\n this.isFadingOut = false;\n // Adjust the exponent to change the curve of the volume control\n this.naturalVolumeExponent = 1.5;\n this.options = Object.assign({}, defaultOptions, options);\n this.options.lineColor = this.options.lineColor || defaultOptions.lineColor;\n this.options.dragPointFill = this.options.dragPointFill || defaultOptions.dragPointFill;\n this.options.dragPointStroke = this.options.dragPointStroke || defaultOptions.dragPointStroke;\n this.volume = this.options.volume ?? 1;\n }\n static create(options) {\n return new EnvelopePlugin(options);\n }\n destroy() {\n this.polyline?.destroy();\n super.destroy();\n }\n /** Called by wavesurfer, don't call manually */\n onInit() {\n if (!this.wavesurfer) {\n throw Error('WaveSurfer is not initialized');\n }\n this.initWebAudio();\n this.initSvg();\n this.initFadeEffects();\n this.subscriptions.push(this.wavesurfer.on('redraw', () => {\n const duration = this.wavesurfer?.getDuration();\n if (!duration)\n return;\n this.options.fadeInStart = this.options.fadeInStart || 0;\n this.options.fadeOutEnd = this.options.fadeOutEnd || duration;\n this.options.fadeInEnd = this.options.fadeInEnd || this.options.fadeInStart;\n this.options.fadeOutStart = this.options.fadeOutStart || this.options.fadeOutEnd;\n this.renderPolyline();\n }));\n }\n initSvg() {\n if (!this.wavesurfer)\n return;\n const wrapper = this.wavesurfer.getWrapper();\n this.polyline = new Polyline(this.options, wrapper);\n this.subscriptions.push(this.polyline.on('line-move', (relativeY) => {\n this.setVolume(this.naturalVolume(relativeY));\n }), this.polyline.on('point-move', (index, relativeX) => {\n const duration = this.wavesurfer?.getDuration() || 0;\n const newTime = relativeX * duration;\n // Fade-in end point\n if (index === 1) {\n if (newTime < this.options.fadeInStart || newTime > this.options.fadeOutStart)\n return;\n this.options.fadeInEnd = newTime;\n this.emit('fade-in-change', newTime);\n }\n else if (index === 2) {\n // Fade-out start point\n if (newTime > this.options.fadeOutEnd || newTime < this.options.fadeInEnd)\n return;\n this.options.fadeOutStart = newTime;\n this.emit('fade-out-change', newTime);\n }\n this.renderPolyline();\n }));\n }\n renderPolyline() {\n if (!this.polyline || !this.wavesurfer)\n return;\n const duration = this.wavesurfer.getDuration();\n if (!duration)\n return;\n this.polyline.update({\n x1: this.options.fadeInStart / duration,\n x2: this.options.fadeInEnd / duration,\n x3: this.options.fadeOutStart / duration,\n x4: this.options.fadeOutEnd / duration,\n y: this.invertNaturalVolume(this.volume),\n });\n }\n initWebAudio() {\n const audio = this.wavesurfer?.getMediaElement();\n if (!audio)\n return null;\n this.volume = this.options.volume ?? audio.volume;\n // Create an AudioContext\n const audioContext = new window.AudioContext();\n // Create a GainNode for controlling the volume\n this.gainNode = audioContext.createGain();\n this.setGainValue();\n // Create a MediaElementAudioSourceNode using the audio element\n const source = audioContext.createMediaElementSource(audio);\n // Connect the source to the GainNode, and the GainNode to the destination (speakers)\n source.connect(this.gainNode);\n this.gainNode.connect(audioContext.destination);\n this.audioContext = audioContext;\n }\n invertNaturalVolume(value) {\n const minValue = 0.0001;\n const maxValue = 1;\n const interpolatedValue = Math.pow((value - minValue) / (maxValue - minValue), 1 / this.naturalVolumeExponent);\n return interpolatedValue;\n }\n naturalVolume(value) {\n