import { useEffect, useState, useRef, useCallback } from "react"; import { PitchContour } from "@renderer/components"; import WaveSurfer from "wavesurfer.js"; import { Button } 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 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("decode", () => { setDuration(wavesurfer.getDuration()); const peaks = wavesurfer.getDecodedData().getChannelData(0); const sampleRate = wavesurfer.options.sampleRate; wavesurfer.renderer.getWrapper().appendChild( PitchContour({ peaks, sampleRate, height, }) ); }), ]; return () => { subscriptions.forEach((unsub) => unsub()); wavesurfer?.destroy(); }; }, [wavesurfer]); return (
{secondsToTimestamp(duration)}
); };