import { Box, Flex, Text, useMantineTheme } from "@mantine/core"; import { useEffect, useRef, useState } from "react"; interface VoiceRecorderProps { duration: number; waves: number[]; } type AnimatedBar = { id: number; value: number; entered: boolean; }; const VISIBLE_BARS = 50; const BAR_WIDTH = 3; const BAR_GAP = 2; const STEP_PX = BAR_WIDTH + BAR_GAP; const COMPONENT_HEIGHT = 45; const MAX_BAR_HEIGHT = 28; const MIN_BAR_HEIGHT = 4; export function VoiceRecorder(props: VoiceRecorderProps) { const theme = useMantineTheme(); const [bars, setBars] = useState([]); const [subShift, setSubShift] = useState(0); const prevLengthRef = useRef(0); const prevWavesRef = useRef([]); const idRef = useRef(0); const enterFrameRef = useRef(null); const scrollFrameRef = useRef(null); const lastAppendAtRef = useRef(null); const appendIntervalRef = useRef(120); const barsLengthRef = useRef(0); const formatDuration = (seconds: number) => { const mins = Math.floor(seconds / 60) .toString() .padStart(2, "0"); const secs = (seconds % 60).toString().padStart(2, "0"); return `${mins}:${secs}`; }; useEffect(() => { barsLengthRef.current = bars.length; }, [bars.length]); useEffect(() => { if (props.waves.length === 0) { setBars([]); setSubShift(0); prevLengthRef.current = 0; prevWavesRef.current = []; lastAppendAtRef.current = null; return; } if (props.waves.length < prevLengthRef.current) { const resetBars = props.waves.slice(-VISIBLE_BARS).map((value) => ({ id: idRef.current++, value, entered: true, })); setBars(resetBars); setSubShift(0); prevLengthRef.current = props.waves.length; prevWavesRef.current = props.waves; lastAppendAtRef.current = performance.now(); return; } const prevWaves = prevWavesRef.current; let appended: number[] = []; // Обычный режим: длина выросла if (props.waves.length > prevLengthRef.current) { appended = props.waves.slice(prevLengthRef.current); } else if (props.waves.length === prevLengthRef.current && props.waves.length > 0) { // Rolling buffer: длина та же, но данные сдвигаются let changed = false; if (prevWaves.length !== props.waves.length) { changed = true; } else { for (let i = 0; i < props.waves.length; i++) { if (props.waves[i] !== prevWaves[i]) { changed = true; break; } } } if (changed) { appended = [props.waves[props.waves.length - 1]]; } } if (appended.length > 0) { const now = performance.now(); if (lastAppendAtRef.current != null) { const dt = now - lastAppendAtRef.current; const perBar = dt / appended.length; appendIntervalRef.current = appendIntervalRef.current * 0.7 + perBar * 0.3; } lastAppendAtRef.current = now; setSubShift(0); const newIds: number[] = []; setBars((prev) => { const next = [...prev]; appended.forEach((value) => { const id = idRef.current++; newIds.push(id); next.push({ id, value, entered: false, }); }); return next.slice(-VISIBLE_BARS); }); if (enterFrameRef.current) { cancelAnimationFrame(enterFrameRef.current); } enterFrameRef.current = requestAnimationFrame(() => { setBars((prev) => { const ids = new Set(newIds); return prev.map((bar) => (ids.has(bar.id) ? { ...bar, entered: true } : bar)); }); }); } prevLengthRef.current = props.waves.length; prevWavesRef.current = props.waves; }, [props.waves]); useEffect(() => { const tick = () => { const startedAt = lastAppendAtRef.current; if (startedAt != null) { const elapsed = performance.now() - startedAt; const interval = Math.max(16, appendIntervalRef.current); const progress = Math.min(1, elapsed / interval); const smoothShift = barsLengthRef.current >= VISIBLE_BARS ? -progress * STEP_PX : 0; setSubShift(smoothShift); } else { setSubShift(0); } scrollFrameRef.current = requestAnimationFrame(tick); }; scrollFrameRef.current = requestAnimationFrame(tick); return () => { if (scrollFrameRef.current) { cancelAnimationFrame(scrollFrameRef.current); } }; }, []); useEffect(() => { return () => { if (enterFrameRef.current) { cancelAnimationFrame(enterFrameRef.current); } if (scrollFrameRef.current) { cancelAnimationFrame(scrollFrameRef.current); } }; }, []); const waveformWidth = VISIBLE_BARS * BAR_WIDTH + (VISIBLE_BARS - 1) * BAR_GAP; return ( {formatDuration(props.duration)} {Array.from({ length: VISIBLE_BARS }).map((_, index) => { const bar = bars[index]; if (!bar) { return ( ); } const normalized = Math.max(0, Math.min(1, bar.value)); const height = Math.max( MIN_BAR_HEIGHT, Math.min(MAX_BAR_HEIGHT, MIN_BAR_HEIGHT + normalized * (MAX_BAR_HEIGHT - MIN_BAR_HEIGHT)) ); const isLast = index === bars.length - 1; const isNearTail = index >= bars.length - 3; return ( ); })} ); }