|
- 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<SpectrogramCanvasDerivedElement>,
- Omit<WaveSurferOptions, 'waveColor' | 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {
- waveColor?: string;
- audioRef?: React.Ref<HTMLAudioElement>;
- }
-
- export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElement, SpectrogramCanvasProps>(({
- 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<SpectrogramCanvasDerivedElement>(null);
- const containerRef = forwardedRef ?? defaultRef;
- const waveSurferRef = React.useRef<any>(null);
- const cursorRef = React.useRef<HTMLDivElement>(null);
-
- const handleAction: React.FormEventHandler<HTMLFormElement> = (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<string, Function>).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 (
- <div
- className={clsx(
- 'flex flex-col',
- className,
- )}
- >
- <div
- className="flex-auto relative aspect-video sm:aspect-auto"
- style={{
- maskImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACEAYAAAAiJtFnAAAFDmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMjdUMTk6MTE6MTQrMDgwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyMy0wNi0yN1QxOToxMToxNCswODAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICB0aWZmOkltYWdlV2lkdGg9IjIiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjIiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLzEiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLzEiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMiAyLjAuNCIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0wNi0yN1QxOToxMjoyMyswODowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+HFYtUQAAAYFpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZHLS0JBFIc/tRdZFNQiqIWEtaowA6lNkBEWSIgZ9Nro1auB2uVeI6Jt0DYoiNr0WtRfUNugdRAURRBta13UpuR2rgpK5BnOnG9+M+cwcwbskbSSMWo8kMnm9HDA75qbX3DVv1JHJ05gIKoY2lgoFKSqfT1gs+Jdv1Wr+rl/zRlPGArYGoRHFU3PCU8KB9dymsW7wu1KKhoXPhfu0+WCwveWHivym8XJIv9YrEfC42BvFXYlKzhWwUpKzwjLy3Fn0qtK6T7WS5oS2dkZid3iXRiECeDHxRQTjONjkBGZffTjZUBWVMn3FPKnWZFcRWaNdXSWSZIiR5+oq1I9IVEVPSEjzbrV/799NdQhb7F6kx9qX0zzowfqdyC/bZrfx6aZPwHHM1xly/krRzD8Kfp2WXMfQssmXFyXtdgeXG5Bx5MW1aMFySFuV1V4P4PmeWi7hcbFYs9K+5w+QmRDvuoG9g+gV863LP0CQBln1EZARokAAAAJcEhZcwAACxMAAAsTAQCanBgAAAASSURBVAiZY2BAASzdqHyG//8BDTECjpzQZHQAAAAASUVORK5CYII=)',
- WebkitMaskImage: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACEAYAAAAiJtFnAAAFDmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMjdUMTk6MTE6MTQrMDgwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDYtMjdUMTk6MTI6MjMrMDg6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyMy0wNi0yN1QxOToxMToxNCswODAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICB0aWZmOkltYWdlV2lkdGg9IjIiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjIiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLzEiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLzEiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMiAyLjAuNCIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMy0wNi0yN1QxOToxMjoyMyswODowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+HFYtUQAAAYFpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZHLS0JBFIc/tRdZFNQiqIWEtaowA6lNkBEWSIgZ9Nro1auB2uVeI6Jt0DYoiNr0WtRfUNugdRAURRBta13UpuR2rgpK5BnOnG9+M+cwcwbskbSSMWo8kMnm9HDA75qbX3DVv1JHJ05gIKoY2lgoFKSqfT1gs+Jdv1Wr+rl/zRlPGArYGoRHFU3PCU8KB9dymsW7wu1KKhoXPhfu0+WCwveWHivym8XJIv9YrEfC42BvFXYlKzhWwUpKzwjLy3Fn0qtK6T7WS5oS2dkZid3iXRiECeDHxRQTjONjkBGZffTjZUBWVMn3FPKnWZFcRWaNdXSWSZIiR5+oq1I9IVEVPSEjzbrV/799NdQhb7F6kx9qX0zzowfqdyC/bZrfx6aZPwHHM1xly/krRzD8Kfp2WXMfQssmXFyXtdgeXG5Bx5MW1aMFySFuV1V4P4PmeWi7hcbFYs9K+5w+QmRDvuoG9g+gV863LP0CQBln1EZARokAAAAJcEhZcwAACxMAAAsTAQCanBgAAAASSURBVAiZY2BAASzdqHyG//8BDTECjpzQZHQAAAAASUVORK5CYII=)',
- }}
- >
- <div
- ref={cursorRef}
- style={{
- position: 'absolute',
- top: 0,
- left: 0,
- height: '100%',
- mixBlendMode: 'plus-lighter',
- backgroundColor: 'rgb(var(--color-primary))',
- zIndex: 5,
- }}
- />
- <div className="absolute top-0 left-0 w-full h-full"
- ref={containerRef}
- />
- </div>
- {controls && (
- <form
- onSubmit={handleAction}
- >
- <button
- type="submit"
- name="action"
- value="togglePlayback"
- >
- {isPlaying ? '⏸' : '▶'}
- </button>
- </form>
- )}
- </div>
- );
- });
-
- SpectrogramCanvas.displayName = 'WavesurferCanvas';
|