KittenTTS-WebGPU / src /WaveformPlayer.tsx
shreyask's picture
Upload folder using huggingface_hub
5c97b55 verified
import { useRef, useEffect, useState, useCallback } from "react";
interface WaveformPlayerProps {
audioUrl: string;
duration?: number | null;
}
export default function WaveformPlayer({ audioUrl, duration }: WaveformPlayerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
const animRef = useRef<number>(0);
const waveformRef = useRef<number[]>([]);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrTime] = useState(0);
const [totalDuration, setTotalDuration] = useState(0);
const [hovering, setHovering] = useState(false);
const [hoverX, setHoverX] = useState(0);
// Decode audio and compute waveform peaks
useEffect(() => {
if (!audioUrl) return;
const ctx = new AudioContext();
fetch(audioUrl)
.then((r) => r.arrayBuffer())
.then((buf) => ctx.decodeAudioData(buf))
.then((decoded) => {
const raw = decoded.getChannelData(0);
const bars = 100;
const blockSize = Math.floor(raw.length / bars);
const peaks: number[] = [];
for (let i = 0; i < bars; i++) {
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum += Math.abs(raw[i * blockSize + j]);
}
peaks.push(sum / blockSize);
}
// Normalize
const max = Math.max(...peaks, 0.01);
waveformRef.current = peaks.map((p) => p / max);
drawWaveform();
ctx.close();
})
.catch(() => {});
}, [audioUrl]);
const drawWaveform = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const w = rect.width;
const h = rect.height;
const peaks = waveformRef.current;
const bars = peaks.length || 1;
const audio = audioRef.current;
const progress = audio && audio.duration ? audio.currentTime / audio.duration : 0;
ctx.clearRect(0, 0, w, h);
const barWidth = (w / bars) * 0.7;
const gap = (w / bars) * 0.3;
const mid = h / 2;
for (let i = 0; i < bars; i++) {
const x = (i / bars) * w;
const barH = Math.max(2, (peaks[i] || 0) * mid * 0.9);
const iPlayed = i / bars < progress;
ctx.fillStyle = iPlayed ? "#c084fc" : hovering && x < hoverX ? "rgba(192,132,252,0.4)" : "#444";
ctx.beginPath();
ctx.roundRect(x + gap / 2, mid - barH, barWidth, barH * 2, 1.5);
ctx.fill();
}
}, [hovering, hoverX]);
// Animation loop
useEffect(() => {
const tick = () => {
const audio = audioRef.current;
if (audio) setCurrTime(audio.currentTime);
drawWaveform();
animRef.current = requestAnimationFrame(tick);
};
animRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(animRef.current);
}, [drawWaveform]);
const togglePlay = () => {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play();
setPlaying(true);
} else {
audio.pause();
setPlaying(false);
}
};
const seek = (e: React.MouseEvent<HTMLCanvasElement>) => {
const audio = audioRef.current;
const canvas = canvasRef.current;
if (!audio || !canvas || !audio.duration) return;
const rect = canvas.getBoundingClientRect();
const ratio = (e.clientX - rect.left) / rect.width;
audio.currentTime = ratio * audio.duration;
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
setHoverX(e.clientX - rect.left);
};
const fmt = (s: number) => {
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, "0")}`;
};
return (
<div className="waveform-player">
<audio
ref={audioRef}
src={audioUrl}
onLoadedMetadata={() => setTotalDuration(audioRef.current?.duration || 0)}
onEnded={() => setPlaying(false)}
/>
<button className="waveform-play" onClick={togglePlay} aria-label={playing ? "Pause" : "Play"}>
{playing ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="3" y="2" width="4" height="12" rx="1" />
<rect x="9" y="2" width="4" height="12" rx="1" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 2.5v11l9-5.5z" />
</svg>
)}
</button>
<canvas
ref={canvasRef}
className="waveform-canvas"
onClick={seek}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onMouseMove={handleMouseMove}
/>
<span className="waveform-time">
{fmt(currentTime)} / {fmt(totalDuration)}
</span>
{duration !== null && duration !== undefined && (
<span className="waveform-gen-time">
{(duration / 1000).toFixed(1)}s
</span>
)}
</div>
);
}