/* eslint-disable react-hooks/exhaustive-deps */ import { useState, useRef, useEffect } from "react"; import ModalWrapper from "../ModalWrapper"; import Button from "../ui/Button"; import Select from "../ui/Select"; import RestartIcon from "../icons/RestartIcon"; import useModalStore from "../../store/modalStore"; import SettingsModal from "./SettingsModal"; import clsx from "clsx"; import { isMediaDevicesSupported } from "../../lib/mediaDevices"; interface VoiceCheckModalProps { selectedMicrophone: string; microphones: { deviceId: string; label: string }[]; microphoneVolume: number; onSelectMicrophone: (label: string) => void; } function VoiceCheckModal({ selectedMicrophone, microphones, microphoneVolume, onSelectMicrophone, }: VoiceCheckModalProps) { const { setModal } = useModalStore(); const [audioLevel, setAudioLevel] = useState(0); const [status, setStatus] = useState<"default" | "success" | "error">( "default" ); const [isTestRunning, setIsTestRunning] = useState(false); const [soundDetected, setSoundDetected] = useState(false); const [maxAudioLevel, setMaxAudioLevel] = useState(0); const audioContextRef = useRef(null); const analyserRef = useRef(null); const streamRef = useRef(null); const sourceRef = useRef(null); const gainNodeRef = useRef(null); const animationFrameRef = useRef(null); const testTimeoutRef = useRef | null>(null); const statusRef = useRef<"default" | "success" | "error">("default"); function detectAudioLevel() { if (!analyserRef.current) return; const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount); analyserRef.current.getByteFrequencyData(dataArray); const average = dataArray.reduce((a, b) => a + b) / dataArray.length; setAudioLevel(average); // Обновляем максимальный уровень звука setMaxAudioLevel((prev) => Math.max(prev, average)); // Если звук обнаружен и статус ещё не success, устанавливаем success if (average > 5) { if (statusRef.current !== "success") { statusRef.current = "success"; setStatus("success"); } setSoundDetected(true); } animationFrameRef.current = requestAnimationFrame(detectAudioLevel); } async function startMicrophoneTest() { // Проверяем доступность API if (!isMediaDevicesSupported()) { console.error( "navigator.mediaDevices недоступен. Возможно, требуется HTTPS или поддержка браузера." ); setStatus("error"); setIsTestRunning(false); return; } try { const selectedMic = microphones.find( (mic) => mic.label === selectedMicrophone ); const constraints: MediaStreamConstraints = { audio: selectedMic ? { deviceId: { exact: selectedMic.deviceId } } : true, }; const stream = await navigator.mediaDevices.getUserMedia(constraints); streamRef.current = stream; const audioContext = new AudioContext(); audioContextRef.current = audioContext; const analyser = audioContext.createAnalyser(); analyser.fftSize = 256; analyserRef.current = analyser; const gainNode = audioContext.createGain(); gainNode.gain.value = microphoneVolume / 100; gainNodeRef.current = gainNode; const source = audioContext.createMediaStreamSource(stream); sourceRef.current = source; // Подключаем: source -> gainNode -> analyser -> destination source.connect(gainNode); gainNode.connect(analyser); analyser.connect(audioContext.destination); // Сбрасываем статус при новом тесте statusRef.current = "default"; setStatus("default"); setSoundDetected(false); setAudioLevel(0); setMaxAudioLevel(0); setIsTestRunning(true); // Запускаем проверку звука detectAudioLevel(); // Останавливаем проверку через 3 секунды и устанавливаем результат testTimeoutRef.current = setTimeout(() => { // Останавливаем анимацию if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } // Устанавливаем финальный статус if (statusRef.current === "default") { statusRef.current = "error"; setStatus("error"); } // Если статус уже success, он остаётся success setIsTestRunning(false); // Очищаем ресурсы после завершения теста cleanupAudioResources(); }, 3000); } catch (error) { console.error("Ошибка доступа к микрофону:", error); setStatus("error"); setIsTestRunning(false); } } function cleanupAudioResources() { // Отключаем source if (sourceRef.current) { try { sourceRef.current.disconnect(); } catch { // Ignore if already disconnected } sourceRef.current = null; } // Отключаем gainNode if (gainNodeRef.current) { try { gainNodeRef.current.disconnect(); } catch { // Ignore if already disconnected } gainNodeRef.current = null; } // Отключаем analyser if (analyserRef.current) { try { analyserRef.current.disconnect(); } catch { // Ignore if already disconnected } analyserRef.current = null; } // Останавливаем все треки медиа-потока if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } // Закрываем AudioContext if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } } function stopMicrophoneTest() { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } if (testTimeoutRef.current) { clearTimeout(testTimeoutRef.current); testTimeoutRef.current = null; } cleanupAudioResources(); // НЕ сбрасываем audioLevel и maxAudioLevel - они сохраняются до нового теста setSoundDetected(false); setIsTestRunning(false); } function restartMicrophoneTest() { stopMicrophoneTest(); // Небольшая задержка перед запуском нового теста setTimeout(startMicrophoneTest, 100); } useEffect(() => { startMicrophoneTest(); return stopMicrophoneTest; }, [selectedMicrophone]); // Обновляем громкость микрофона при изменении слайдера useEffect(() => { if (gainNodeRef.current) { gainNodeRef.current.gain.value = microphoneVolume / 100; } }, [microphoneVolume]); // Генерируем высоты для баров на основе уровня звука function generateBarHeights() { const baseHeights = [ 3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6, ]; // Используем maxAudioLevel для сохранения максимальной высоты const levelToUse = isTestRunning ? audioLevel : maxAudioLevel; const multiplier = Math.min(levelToUse / 5); return baseHeights.map((h) => Math.max(3, Math.min(60, h * multiplier))); } const barHeights = generateBarHeights(); console.log(barHeights); return (
{/* Выбор микрофона */}

Микрофон