Spaces:
Runtime error
Runtime error
Download src/components/waveform.tsx from RalphThings/yue2-studio: direct link, hf CLI and curl.
- Browser
- Download file 4.01 kB
-
https://huggingface.co/spaces/RalphThings/yue2-studio/resolve/main/src/components/waveform.tsx
- Command line
-
hf download hf://spaces/RalphThings/yue2-studio/src/components/waveform.tsx
-
curl -L -o waveform.tsx https://huggingface.co/spaces/RalphThings/yue2-studio/resolve/main/src/components/waveform.tsx
4.01 kB
| "use client"; | |
| import { useEffect, useRef, useState } from "react"; | |
| /** Decodes audio once and reduces it to per-column peak pairs for drawing. */ | |
| export function useAudioPeaks(source: string | File | null, columns = 900) { | |
| const [peaks, setPeaks] = useState<Float32Array | null>(null); | |
| const [duration, setDuration] = useState(0); | |
| const [failed, setFailed] = useState(false); | |
| useEffect(() => { | |
| if (!source) { | |
| setPeaks(null); | |
| setDuration(0); | |
| setFailed(false); | |
| return; | |
| } | |
| let cancelled = false; | |
| setFailed(false); | |
| setPeaks(null); | |
| const run = async () => { | |
| try { | |
| const bytes = | |
| typeof source === "string" | |
| ? await (await fetch(source)).arrayBuffer() | |
| : await source.arrayBuffer(); | |
| const AudioCtx = | |
| window.AudioContext ?? | |
| (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; | |
| const context = new AudioCtx(); | |
| const buffer = await context.decodeAudioData(bytes); | |
| await context.close(); | |
| if (cancelled) return; | |
| const channel = buffer.getChannelData(0); | |
| const perColumn = Math.max(1, Math.floor(channel.length / columns)); | |
| const result = new Float32Array(columns); | |
| for (let column = 0; column < columns; column += 1) { | |
| const start = column * perColumn; | |
| let peak = 0; | |
| for (let i = 0; i < perColumn; i += 1) { | |
| const value = Math.abs(channel[start + i] ?? 0); | |
| if (value > peak) peak = value; | |
| } | |
| result[column] = peak; | |
| } | |
| setPeaks(result); | |
| setDuration(buffer.duration); | |
| } catch { | |
| if (!cancelled) setFailed(true); | |
| } | |
| }; | |
| void run(); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [source, columns]); | |
| return { peaks, duration, failed }; | |
| } | |
| export function Waveform({ | |
| peaks, | |
| progress, | |
| onSeek, | |
| height = 56, | |
| idle = false, | |
| }: { | |
| peaks: Float32Array | null; | |
| progress: number; | |
| onSeek?: (ratio: number) => void; | |
| height?: number; | |
| idle?: boolean; | |
| }) { | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| const wrapRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| const wrap = wrapRef.current; | |
| if (!canvas || !wrap) return; | |
| const draw = () => { | |
| const ratio = window.devicePixelRatio || 1; | |
| const width = wrap.clientWidth; | |
| canvas.width = width * ratio; | |
| canvas.height = height * ratio; | |
| canvas.style.width = `${width}px`; | |
| canvas.style.height = `${height}px`; | |
| const context = canvas.getContext("2d"); | |
| if (!context) return; | |
| context.scale(ratio, ratio); | |
| context.clearRect(0, 0, width, height); | |
| const columnWidth = 2; | |
| const gap = 1; | |
| const total = Math.floor(width / (columnWidth + gap)); | |
| const middle = height / 2; | |
| const playedColumns = Math.round(total * progress); | |
| for (let i = 0; i < total; i += 1) { | |
| const value = peaks | |
| ? (peaks[Math.floor((i / total) * peaks.length)] ?? 0) | |
| : idle | |
| ? 0.06 | |
| : 0.06 + 0.04 * Math.sin(i / 3); | |
| const amplitude = Math.max(1.5, value * (height / 2) * 0.94); | |
| context.fillStyle = i <= playedColumns && peaks ? "#c7e84f" : "#33333b"; | |
| context.fillRect(i * (columnWidth + gap), middle - amplitude, columnWidth, amplitude * 2); | |
| } | |
| }; | |
| draw(); | |
| const observer = new ResizeObserver(draw); | |
| observer.observe(wrap); | |
| return () => observer.disconnect(); | |
| }, [peaks, progress, height, idle]); | |
| return ( | |
| <div | |
| ref={wrapRef} | |
| className={onSeek ? "w-full cursor-pointer" : "w-full"} | |
| onClick={(event) => { | |
| if (!onSeek) return; | |
| const rect = event.currentTarget.getBoundingClientRect(); | |
| onSeek(Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width))); | |
| }} | |
| > | |
| <canvas ref={canvasRef} aria-hidden /> | |
| </div> | |
| ); | |
| } | |