sourav-das/stem-separator
3
1var __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};10var __rest = (this && this.__rest) || function (s, e) {11 var t = {};12 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)13 t[p] = s[p];14 if (s != null && typeof Object.getOwnPropertySymbols === "function")15 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {16 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))17 t[p[i]] = s[p[i]];18 }19 return t;20};21import EventEmitter from './event-emitter.js';22import * as utils from './renderer-utils.js';23import { createDragStream } from './reactive/drag-stream.js';24import { createScrollStream } from './reactive/scroll-stream.js';25import { effect } from './reactive/store.js';26class Renderer extends EventEmitter {27 constructor(options, audioElement) {28 super();29 this.timeouts = [];30 this.isScrollable = false;31 this.audioData = null;32 this.resizeObserver = null;33 this.lastContainerWidth = 0;34 this.isDragging = false;35 this.subscriptions = [];36 this.unsubscribeOnScroll = [];37 this.dragStream = null;38 this.scrollStream = null;39 this.subscriptions = [];40 this.options = options;41 const parent = this.parentFromOptionsContainer(options.container);42 this.parent = parent;43 const [div, shadow] = this.initHtml();44 parent.appendChild(div);45 this.container = div;46 this.scrollContainer = shadow.querySelector('.scroll');47 this.wrapper = shadow.querySelector('.wrapper');48 this.canvasWrapper = shadow.querySelector('.canvases');49 this.progressWrapper = shadow.querySelector('.progress');50 this.cursor = shadow.querySelector('.cursor');51 if (audioElement) {52 shadow.appendChild(audioElement);53 }54 this.initEvents();55 }56 parentFromOptionsContainer(container) {57 let parent;58 if (typeof container === 'string') {59 parent = document.querySelector(container);60 }61 else if (container instanceof HTMLElement) {62 parent = container;63 }64 if (!parent) {65 throw new Error('Container not found');66 }67 return parent;68 }69 initEvents() {70 // Add a click listener71 this.wrapper.addEventListener('click', (e) => {72 const rect = this.wrapper.getBoundingClientRect();73 const [x, y] = utils.getRelativePointerPosition(rect, e.clientX, e.clientY);74 this.emit('click', x, y);75 });76 // Add a double click listener77 this.wrapper.addEventListener('dblclick', (e) => {78 const rect = this.wrapper.getBoundingClientRect();79 const [x, y] = utils.getRelativePointerPosition(rect, e.clientX, e.clientY);80 this.emit('dblclick', x, y);81 });82 // Drag83 if (this.options.dragToSeek === true || typeof this.options.dragToSeek === 'object') {84 this.initDrag();85 }86 // Add a scroll listener using reactive stream87 this.scrollStream = createScrollStream(this.scrollContainer);88 const unsubscribeScroll = effect(() => {89 const { startX, endX } = this.scrollStream.percentages.value;90 const { left, right } = this.scrollStream.bounds.value;91 this.emit('scroll', startX, endX, left, right);92 }, [this.scrollStream.percentages, this.scrollStream.bounds]);93 this.subscriptions.push(unsubscribeScroll);94 // Re-render the waveform on container resize95 if (typeof ResizeObserver === 'function') {96 const delay = this.createDelay(100);97 this.resizeObserver = new ResizeObserver(() => {98 delay()99 .then(() => this.onContainerResize())100 .catch(() => undefined);101 });102 this.resizeObserver.observe(this.scrollContainer);103 }104 }105 onContainerResize() {106 const width = this.parent.clientWidth;107 if (width === this.lastContainerWidth && this.options.height !== 'auto')108 return;109 this.lastContainerWidth = width;110 this.reRender();111 this.emit('resize');112 }113 initDrag() {114 // Don't initialize drag if it's already set up115 if (this.dragStream)116 return;117 this.dragStream = createDragStream(this.wrapper);118 const unsubscribeDrag = effect(() => {119 const drag = this.dragStream.signal.value;120 if (!drag)121 return;122 const width = this.wrapper.getBoundingClientRect().width;123 const relX = utils.clampToUnit(drag.x / width);124 if (drag.type === 'start') {125 this.isDragging = true;126 this.emit('dragstart', relX);127 }128 else if (drag.type === 'move') {129 this.emit('drag', relX);130 }131 else if (drag.type === 'end') {132 this.isDragging = false;133 this.emit('dragend', relX);134 }135 }, [this.dragStream.signal]);136 this.subscriptions.push(unsubscribeDrag);137 }138 initHtml() {139 const div = document.createElement('div');140 const shadow = div.attachShadow({ mode: 'open' });141 const cspNonce = this.options.cspNonce && typeof this.options.cspNonce === 'string' ? this.options.cspNonce.replace(/"/g, '') : '';142 shadow.innerHTML = `143 <style${cspNonce ? ` nonce="${cspNonce}"` : ''}>144 :host {145 user-select: none;146 min-width: 1px;147 }148 :host audio {149 display: block;150 width: 100%;151 }152 :host .scroll {153 overflow-x: auto;154 overflow-y: hidden;155 width: 100%;156 position: relative;157 }158 :host .noScrollbar {159 scrollbar-color: transparent;160 scrollbar-width: none;161 }162 :host .noScrollbar::-webkit-scrollbar {163 display: none;164 -webkit-appearance: none;165 }166 :host .wrapper {167 position: relative;168 overflow: visible;169 z-index: 2;170 }171 :host .canvases {172 min-height: ${this.getHeight(this.options.height, this.options.splitChannels)}px;173 pointer-events: none;174 }175 :host .canvases > div {176 position: relative;177 }178 :host canvas {179 display: block;180 position: absolute;181 top: 0;182 image-rendering: pixelated;183 }184 :host .progress {185 pointer-events: none;186 position: absolute;187 z-index: 2;188 top: 0;189 left: 0;190 width: 0;191 height: 100%;192 overflow: hidden;193 }194 :host .progress > div {195 position: relative;196 }197 :host .cursor {198 pointer-events: none;199 position: absolute;200 z-index: 5;201 top: 0;202 left: 0;203 height: 100%;204 border-radius: 2px;205 }206 </style>207 208 <div class="scroll" part="scroll">209 <div class="wrapper" part="wrapper">210 <div class="canvases" part="canvases"></div>211 <div class="progress" part="progress"></div>212 <div class="cursor" part="cursor"></div>213 </div>214 </div>215 `;216 return [div, shadow];217 }218 /** Wavesurfer itself calls this method. Do not call it manually. */219 setOptions(options) {220 var _a;221 if (this.options.container !== options.container) {222 const newParent = this.parentFromOptionsContainer(options.container);223 newParent.appendChild(this.container);224 this.parent = newParent;225 }226 if (options.dragToSeek === true || typeof this.options.dragToSeek === 'object') {227 this.initDrag();228 }229 else {230 (_a = this.dragStream) === null || _a === void 0 ? void 0 : _a.cleanup();231 this.dragStream = null;232 }233 this.options = options;234 // Re-render the waveform235 this.reRender();236 }237 getWrapper() {238 return this.wrapper;239 }240 getWidth() {241 return this.scrollContainer.clientWidth;242 }243 getScroll() {244 return this.scrollContainer.scrollLeft;245 }246 setScroll(pixels) {247 this.scrollContainer.scrollLeft = pixels;248 }249 setScrollPercentage(percent) {250 const { scrollWidth } = this.scrollContainer;251 const scrollStart = scrollWidth * percent;252 this.setScroll(scrollStart);253 }254 destroy() {255 var _a;256 this.subscriptions.forEach((unsubscribe) => unsubscribe());257 this.container.remove();258 if (this.resizeObserver) {259 this.resizeObserver.disconnect();260 this.resizeObserver = null;261 }262 (_a = this.unsubscribeOnScroll) === null || _a === void 0 ? void 0 : _a.forEach((unsubscribe) => unsubscribe());263 this.unsubscribeOnScroll = [];264 if (this.dragStream) {265 this.dragStream.cleanup();266 this.dragStream = null;267 }268 if (this.scrollStream) {269 this.scrollStream.cleanup();270 this.scrollStream = null;271 }272 }273 createDelay(delayMs = 10) {274 let timeout;275 let rejectFn;276 const onClear = () => {277 if (timeout) {278 clearTimeout(timeout);279 timeout = undefined;280 }281 if (rejectFn) {282 rejectFn();283 rejectFn = undefined;284 }285 };286 this.timeouts.push(onClear);287 return () => {288 return new Promise((resolve, reject) => {289 // Clear any pending delay290 onClear();291 // Store reject function for cleanup292 rejectFn = reject;293 // Set new timeout294 timeout = setTimeout(() => {295 timeout = undefined;296 rejectFn = undefined;297 resolve();298 }, delayMs);299 });300 };301 }302 getHeight(optionsHeight, optionsSplitChannel) {303 var _a;304 const numberOfChannels = ((_a = this.audioData) === null || _a === void 0 ? void 0 : _a.numberOfChannels) || 1;305 return utils.resolveChannelHeight({306 optionsHeight,307 optionsSplitChannels: optionsSplitChannel,308 parentHeight: this.parent.clientHeight,309 numberOfChannels,310 defaultHeight: utils.DEFAULT_HEIGHT,311 });312 }313 convertColorValues(color, ctx) {314 return utils.resolveColorValue(color, this.getPixelRatio(), ctx === null || ctx === void 0 ? void 0 : ctx.canvas.height);315 }316 getPixelRatio() {317 return utils.getPixelRatio(window.devicePixelRatio);318 }319 renderBarWaveform(channelData, options, ctx, vScale) {320 const { width, height } = ctx.canvas;321 const { halfHeight, barWidth, barRadius, barIndexScale, barSpacing, barMinHeight } = utils.calculateBarRenderConfig({322 width,323 height,324 length: (channelData[0] || []).length,325 options,326 pixelRatio: this.getPixelRatio(),327 });328 const segments = utils.calculateBarSegments({329 channelData,330 barIndexScale,331 barSpacing,332 barWidth,333 halfHeight,334 vScale,335 canvasHeight: height,336 barAlign: options.barAlign,337 barMinHeight,338 });339 ctx.beginPath();340 for (const segment of segments) {341 if (barRadius && 'roundRect' in ctx) {342 ;343 ctx.roundRect(segment.x, segment.y, segment.width, segment.height, barRadius);344 }345 else {346 ctx.rect(segment.x, segment.y, segment.width, segment.height);347 }348 }349 ctx.fill();350 ctx.closePath();351 }352 renderLineWaveform(channelData, _options, ctx, vScale) {353 const { width, height } = ctx.canvas;354 const paths = utils.calculateLinePaths({ channelData, width, height, vScale });355 ctx.beginPath();356 for (const path of paths) {357 if (!path.length)358 continue;359 ctx.moveTo(path[0].x, path[0].y);360 for (let i = 1; i < path.length; i++) {361 const point = path[i];362 ctx.lineTo(point.x, point.y);363 }364 }365 ctx.fill();366 ctx.closePath();367 }368 renderWaveform(channelData, options, ctx) {369 ctx.fillStyle = this.convertColorValues(options.waveColor, ctx);370 if (options.renderFunction) {371 options.renderFunction(channelData, ctx);372 return;373 }374 const vScale = utils.calculateVerticalScale({375 channelData,376 barHeight: options.barHeight,377 normalize: options.normalize,378 maxPeak: options.maxPeak,379 });380 if (utils.shouldRenderBars(options)) {381 this.renderBarWaveform(channelData, options, ctx, vScale);382 return;383 }384 this.renderLineWaveform(channelData, options, ctx, vScale);385 }386 renderSingleCanvas(data, options, width, height, offset, canvasContainer, progressContainer) {387 const pixelRatio = this.getPixelRatio();388 const canvas = document.createElement('canvas');389 canvas.width = Math.round(width * pixelRatio);390 canvas.height = Math.round(height * pixelRatio);391 canvas.style.width = `${width}px`;392 canvas.style.height = `${height}px`;393 canvas.style.left = `${Math.round(offset)}px`;394 canvasContainer.appendChild(canvas);395 const ctx = canvas.getContext('2d');396 if (options.renderFunction) {397 ctx.fillStyle = this.convertColorValues(options.waveColor, ctx);398 options.renderFunction(data, ctx);399 }400 else {401 this.renderWaveform(data, options, ctx);402 }403 // Draw a progress canvas404 if (canvas.width > 0 && canvas.height > 0) {405 const progressCanvas = canvas.cloneNode();406 const progressCtx = progressCanvas.getContext('2d');407 progressCtx.drawImage(canvas, 0, 0);408 // Set the composition method to draw only where the waveform is drawn409 progressCtx.globalCompositeOperation = 'source-in';410 progressCtx.fillStyle = this.convertColorValues(options.progressColor, progressCtx);411 // This rectangle acts as a mask thanks to the composition method412 progressCtx.fillRect(0, 0, canvas.width, canvas.height);413 progressContainer.appendChild(progressCanvas);414 }415 }416 renderMultiCanvas(channelData, options, width, height, canvasContainer, progressContainer) {417 const pixelRatio = this.getPixelRatio();418 const { clientWidth } = this.scrollContainer;419 const totalWidth = width / pixelRatio;420 const singleCanvasWidth = utils.calculateSingleCanvasWidth({ clientWidth, totalWidth, options });421 let drawnIndexes = {};422 // Nothing to render423 if (singleCanvasWidth === 0)424 return;425 // Draw a single canvas426 const draw = (index) => {427 if (index < 0 || index >= numCanvases)428 return;429 if (drawnIndexes[index])430 return;431 drawnIndexes[index] = true;432 const offset = index * singleCanvasWidth;433 let clampedWidth = Math.min(totalWidth - offset, singleCanvasWidth);434 // Clamp the width to the bar grid to avoid empty canvases at the end435 clampedWidth = utils.clampWidthToBarGrid(clampedWidth, options);436 if (clampedWidth <= 0)437 return;438 const data = utils.sliceChannelData({ channelData, offset, clampedWidth, totalWidth });439 this.renderSingleCanvas(data, options, clampedWidth, height, offset, canvasContainer, progressContainer);440 };441 // Clear canvases to avoid too many DOM nodes442 const clearCanvases = () => {443 if (utils.shouldClearCanvases(Object.keys(drawnIndexes).length)) {444 canvasContainer.innerHTML = '';445 progressContainer.innerHTML = '';446 drawnIndexes = {};447 }448 };449 // Calculate how many canvases to render450 const numCanvases = Math.ceil(totalWidth / singleCanvasWidth);451 // Render all canvases if the waveform doesn't scroll452 if (!this.isScrollable) {453 for (let i = 0; i < numCanvases; i++) {454 draw(i);455 }456 return;457 }458 // Lazy rendering459 const initialRange = utils.getLazyRenderRange({460 scrollLeft: this.scrollContainer.scrollLeft,461 totalWidth,462 numCanvases,463 });464 initialRange.forEach((index) => draw(index));465 // Subscribe to the scroll event to draw additional canvases466 if (numCanvases > 1) {467 const unsubscribe = this.on('scroll', () => {468 const { scrollLeft } = this.scrollContainer;469 clearCanvases();470 utils.getLazyRenderRange({ scrollLeft, totalWidth, numCanvases }).forEach((index) => draw(index));471 });472 this.unsubscribeOnScroll.push(unsubscribe);473 }474 }475 renderChannel(channelData, _a, width, channelIndex) {476 var { overlay } = _a, options = __rest(_a, ["overlay"]);477 // A container for canvases478 const canvasContainer = document.createElement('div');479 const height = this.getHeight(options.height, options.splitChannels);480 canvasContainer.style.height = `${height}px`;481 if (overlay && channelIndex > 0) {482 canvasContainer.style.marginTop = `-${height}px`;483 }484 this.canvasWrapper.style.minHeight = `${height}px`;485 this.canvasWrapper.appendChild(canvasContainer);486 // A container for progress canvases487 const progressContainer = canvasContainer.cloneNode();488 this.progressWrapper.appendChild(progressContainer);489 // Render the waveform490 this.renderMultiCanvas(channelData, options, width, height, canvasContainer, progressContainer);491 }492 render(audioData) {493 return __awaiter(this, void 0, void 0, function* () {494 var _a;495 // Clear previous timeouts496 this.timeouts.forEach((clear) => clear());497 this.timeouts = [];498 // Clear the canvases499 this.canvasWrapper.innerHTML = '';500 this.progressWrapper.innerHTML = '';501 // Width502 if (this.options.width != null) {503 this.scrollContainer.style.width =504 typeof this.options.width === 'number' ? `${this.options.width}px` : this.options.width;505 }506 // Determine the width of the waveform507 const pixelRatio = this.getPixelRatio();508 const parentWidth = this.scrollContainer.clientWidth;509 const { scrollWidth, isScrollable, useParentWidth, width } = utils.calculateWaveformLayout({510 duration: audioData.duration,511 minPxPerSec: this.options.minPxPerSec || 0,512 parentWidth,513 fillParent: this.options.fillParent,514 pixelRatio,515 });516 // Whether the container should scroll517 this.isScrollable = isScrollable;518 // Set the width of the wrapper519 this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;520 // Set additional styles521 this.scrollContainer.style.overflowX = this.isScrollable ? 'auto' : 'hidden';522 this.scrollContainer.classList.toggle('noScrollbar', !!this.options.hideScrollbar);523 this.cursor.style.backgroundColor = `${this.options.cursorColor || this.options.progressColor}`;524 this.cursor.style.width = `${this.options.cursorWidth}px`;525 this.audioData = audioData;526 this.emit('render');527 // Render the waveform528 if (this.options.splitChannels) {529 // Render a waveform for each channel530 for (let i = 0; i < audioData.numberOfChannels; i++) {531 const options = Object.assign(Object.assign({}, this.options), (_a = this.options.splitChannels) === null || _a === void 0 ? void 0 : _a[i]);532 this.renderChannel([audioData.getChannelData(i)], options, width, i);533 }534 }535 else {536 // Render a single waveform for the first two channels (left and right)537 const channels = [audioData.getChannelData(0)];538 if (audioData.numberOfChannels > 1)539 channels.push(audioData.getChannelData(1));540 this.renderChannel(channels, this.options, width, 0);541 }542 // Must be emitted asynchronously for backward compatibility543 Promise.resolve().then(() => this.emit('rendered'));544 });545 }546 reRender() {547 this.unsubscribeOnScroll.forEach((unsubscribe) => unsubscribe());548 this.unsubscribeOnScroll = [];549 // Return if the waveform has not been rendered yet550 if (!this.audioData)551 return;552 // Remember the current cursor position553 const { scrollWidth } = this.scrollContainer;554 const { right: before } = this.progressWrapper.getBoundingClientRect();555 // Re-render the waveform556 this.render(this.audioData);557 // Adjust the scroll position so that the cursor stays in the same place558 if (this.isScrollable && scrollWidth !== this.scrollContainer.scrollWidth) {559 const { right: after } = this.progressWrapper.getBoundingClientRect();560 const delta = utils.roundToHalfAwayFromZero(after - before);561 this.scrollContainer.scrollLeft += delta;562 }563 }564 zoom(minPxPerSec) {565 this.options.minPxPerSec = minPxPerSec;566 this.reRender();567 }568 scrollIntoView(progress, isPlaying = false) {569 const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;570 const progressWidth = progress * scrollWidth;571 const startEdge = scrollLeft;572 const endEdge = scrollLeft + clientWidth;573 const middle = clientWidth / 2;574 if (this.isDragging) {575 // Scroll when dragging close to the edge of the viewport576 const minGap = 30;577 if (progressWidth + minGap > endEdge) {578 this.scrollContainer.scrollLeft += minGap;579 }580 else if (progressWidth - minGap < startEdge) {581 this.scrollContainer.scrollLeft -= minGap;582 }583 }584 else {585 if (progressWidth < startEdge || progressWidth > endEdge) {586 this.scrollContainer.scrollLeft = progressWidth - (this.options.autoCenter ? middle : 0);587 }588 // Keep the cursor centered when playing589 const center = progressWidth - scrollLeft - middle;590 if (isPlaying && this.options.autoCenter && center > 0) {591 this.scrollContainer.scrollLeft += center;592 }593 }594 }595 renderProgress(progress, isPlaying) {596 if (isNaN(progress))597 return;598 const percents = progress * 100;599 this.canvasWrapper.style.clipPath = `polygon(${percents}% 0%, 100% 0%, 100% 100%, ${percents}% 100%)`;600 this.progressWrapper.style.width = `${percents}%`;601 this.cursor.style.left = `${percents}%`;602 this.cursor.style.transform = this.options.cursorWidth603 ? `translateX(-${progress * this.options.cursorWidth}px)`604 : '';605 // Only scroll if we have valid audio data to prevent race conditions during loading606 if (this.isScrollable && this.options.autoScroll && this.audioData && this.audioData.duration > 0) {607 this.scrollIntoView(progress, isPlaying);608 }609 }610 exportImage(format, quality, type) {611 return __awaiter(this, void 0, void 0, function* () {612 const canvases = this.canvasWrapper.querySelectorAll('canvas');613 if (!canvases.length) {614 throw new Error('No waveform data');615 }616 // Data URLs617 if (type === 'dataURL') {618 const images = Array.from(canvases).map((canvas) => canvas.toDataURL(format, quality));619 return Promise.resolve(images);620 }621 // Blobs622 return Promise.all(Array.from(canvases).map((canvas) => {623 return new Promise((resolve, reject) => {624 canvas.toBlob((blob) => {625 if (blob) {626 resolve(blob);627 }628 else {629 reject(new Error('Could not export image'));630 }631 }, format, quality);632 });633 }));634 });635 }636}637export default Renderer;638 