CoolFace
Apppublic

sourav-das/stem-separator

sourceHugging Facemitupdated 6mo agoView on Hugging Face
3likes
renderer.js638 linesDownload Raw Back to dist
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