import * as React from 'react'; import {WaveSurferOptions} from 'wavesurfer.js'; import clsx from 'clsx'; import {getFormValues} from '@theoryofnekomata/formxtra'; export type SpectrogramCanvasDerivedElement = HTMLDivElement; export interface SpectrogramCanvasProps extends React.HTMLProps, Omit { waveColor?: string; audioRef?: React.Ref; } export const SpectrogramCanvas = React.forwardRef(({ className, children, controls, // TODO organize props for color waveColor, progressColor, cursorColor, cursorWidth, barWidth, barGap, barRadius, barHeight, barAlign, minPxPerSec, peaks, duration, autoPlay, interact, hideScrollbar, audioRate, autoScroll, autoCenter, sampleRate, splitChannels, normalize, audioRef, ...etcProps }, forwardedRef) => { const [isPlaying, setIsPlaying] = React.useState(false); const defaultRef = React.useRef(null); const containerRef = forwardedRef ?? defaultRef; const waveSurferRef = React.useRef(null); const cursorRef = React.useRef(null); const handleAction: React.FormEventHandler = (e) => { e.preventDefault(); const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; const formData = getFormValues( e.currentTarget, { submitter: nativeEvent.submitter, } ); const actionName = formData['action'] as string; switch (actionName) { case 'togglePlayback': setIsPlaying((prev) => !prev); break; default: break; } }; React.useEffect(() => { if (!(typeof audioRef === 'object' && audioRef)) { return; } const { current: media } = audioRef; if (!media) { return; } if (!(typeof containerRef === 'object' && containerRef)) { return; } const { current: container } = containerRef; if (!container) { return; } if (!(typeof cursorRef === 'object' && cursorRef)) { return; } const { current: cursor } = cursorRef; if (!cursor) { return; } const handleTimeUpdate = (e: Event) => { const thisMedia = e.currentTarget as HTMLAudioElement; cursor.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`; }; const load = async (media: HTMLAudioElement, container: HTMLElement) => { const { default: WaveSurfer } = await import('wavesurfer.js'); const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram'); const dummyContainer = window.document.createElement('div'); window.document.body.appendChild(dummyContainer); const waveSurferInstance = WaveSurfer.create({ container: dummyContainer, height: 100, fillParent: true, autoplay: autoPlay, waveColor, progressColor, cursorColor, barWidth, barGap, barRadius, barHeight, barAlign, minPxPerSec, peaks, duration, interact, hideScrollbar, audioRate, autoScroll, autoCenter, sampleRate, splitChannels, normalize, plugins: [], cursorWidth, media, }); let colorMap: Array<[number, number, number, number]> = []; if (waveColor?.toLowerCase().startsWith('rgb(')) { const waveColorParse = waveColor.match(/rgb\((\d+)[, ]\s*(\d+)[, ]\s*(\d+)\)/); const waveColorR = parseInt(waveColorParse?.[1] ?? '0', 10); const waveColorG = parseInt(waveColorParse?.[2] ?? '0', 10); const waveColorB = parseInt(waveColorParse?.[3] ?? '0', 10); for (let i = 0; i < 256; i += 1) { colorMap.push([waveColorR / 256, waveColorG / 256, waveColorB / 256, i / 256]); } } waveSurferInstance.registerPlugin( Spectrogram.create({ container, labels: true, labelsColor: 'rgb(0 0 0/0)', height: container.clientHeight, colorMap, }), ) waveSurferInstance.on('ready', () => { if (!container) { return; } while (container.children.length > 1) { container.removeChild(container.children[0]); } dummyContainer.remove(); }); await waveSurferInstance.load(media.currentSrc); waveSurferInstance.setTime(media.currentTime); media.addEventListener('timeupdate', handleTimeUpdate); return waveSurferInstance; }; const { current: waveSurferCurrent } = waveSurferRef; void load(media, container).then((i) => { waveSurferRef.current = i; }); return () => { if (waveSurferCurrent) { (waveSurferCurrent as unknown as Record).destroy(); } if (container) { container.innerHTML = ''; } if (media) { media.removeEventListener('timeupdate', handleTimeUpdate); } }; }, [ audioRef, autoPlay, waveColor, progressColor, cursorColor, barWidth, barGap, barRadius, barHeight, barAlign, minPxPerSec, peaks, duration, interact, hideScrollbar, audioRate, autoScroll, autoCenter, sampleRate, splitChannels, normalize, cursorWidth, containerRef, ]); return (
{controls && (
)}
); }); SpectrogramCanvas.displayName = 'WavesurferCanvas';