import { useEffect, useState, useRef, useCallback } from "react"; import { renderPitchContour } from "@renderer/lib/utils"; import { extractFrequencies } from "@/utils"; import WaveSurfer from "wavesurfer.js"; import { Button, Skeleton } from "@renderer/components/ui"; import { PlayIcon, PauseIcon } from "lucide-react"; import { useIntersectionObserver } from "@uidotdev/usehooks"; import { secondsToTimestamp } from "@renderer/lib/utils"; export const SpeechPlayer = (props: { speech: Partial; height?: number; }) => { const { speech, height = 100 } = props; const [isPlaying, setIsPlaying] = useState(false); const [wavesurfer, setWavesurfer] = useState(null); const containerRef = useRef(); const [ref, entry] = useIntersectionObserver({ threshold: 1, }); const [duration, setDuration] = useState(0); const [initialized, setInitialized] = useState(false); const onPlayClick = useCallback(() => { wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play(); }, [wavesurfer]); useEffect(() => { // use the intersection observer to only create the wavesurfer instance // when the player is visible if (!entry?.isIntersecting) return; if (!speech?.src) return; if (wavesurfer) return; const ws = WaveSurfer.create({ container: containerRef.current, url: speech.src, height, barWidth: 1, cursorWidth: 0, autoCenter: false, autoScroll: true, hideScrollbar: true, minPxPerSec: 100, waveColor: "#ddd", progressColor: "rgba(0, 0, 0, 0.25)", normalize: true, }); setWavesurfer(ws); }, [speech, entry]); useEffect(() => { if (!wavesurfer) return; const subscriptions = [ wavesurfer.on("play", () => { setIsPlaying(true); }), wavesurfer.on("pause", () => { setIsPlaying(false); }), wavesurfer.on("ready", () => { setDuration(wavesurfer.getDuration()); const peaks = wavesurfer.getDecodedData().getChannelData(0); const sampleRate = wavesurfer.options.sampleRate; const data = extractFrequencies({ peaks, sampleRate }); setTimeout(() => { renderPitchContour({ wrapper: wavesurfer.getWrapper(), canvasId: `pitch-contour-${speech.id}-canvas`, labels: new Array(data.length).fill(""), datasets: [ { data, cubicInterpolationMode: "monotone", pointRadius: 1, }, ], }); }, 1000); setInitialized(true); }), ]; return () => { subscriptions.forEach((unsub) => unsub()); wavesurfer?.destroy(); }; }, [wavesurfer]); return (
{secondsToTimestamp(duration)}
{!initialized && (
)}
); };