import { useState, useCallback, useEffect, useRef } from 'react'; import { AudioUploader } from './AudioUploader'; import { OscilloscopeControls, LiveDisplaySettings } from './OscilloscopeControls'; import { OscilloscopeDisplay } from './OscilloscopeDisplay'; import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer'; import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer'; import { useVideoExporter } from '@/hooks/useVideoExporter'; import { useSettings } from '@/contexts/SettingsContext'; import { Pause, Play } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Slider } from '@/components/ui/slider'; // Format time in mm:ss format const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; export function Oscilloscope() { const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined'); const [liveSettings, setLiveSettings] = useState({ lineThickness: 2, showGrid: true, glowIntensity: 1, displayMode: 'combined', visualizationMode: 'waveform', }); const [isMicActive, setIsMicActive] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [isLooping, setIsLooping] = useState(false); const [seekPosition, setSeekPosition] = useState(0); const [exportResolution, setExportResolution] = useState<'640x480' | '1280x720' | '1920x1080'>('1920x1080'); const [exportFps, setExportFps] = useState<24 | 30 | 60>(60); const [exportQuality, setExportQuality] = useState<'low' | 'medium' | 'high'>('medium'); const [showMicCalibration, setShowMicCalibration] = useState(false); const [micLevel, setMicLevel] = useState(0); const [micGain, setMicGain] = useState(1); const [micGainNode, setMicGainNode] = useState(null); const isMicActiveRef = useRef(false); // Audio playback refs const audioRef = useRef(null); const audioUrlRef = useRef(null); const { audioData, isLoading, fileName, originalFile, loadAudioFile, reset: resetAudio } = useAudioAnalyzer(); const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter(); const { playSound } = useSettings(); // Update mic gain when it changes useEffect(() => { if (micGainNode) { micGainNode.gain.value = micGain; } }, [micGain, micGainNode]); // Real-time microphone input const [micStream, setMicStream] = useState(null); const [micAnalyzer, setMicAnalyzer] = useState(null); // Create audio element when file is loaded useEffect(() => { if (originalFile) { // Clean up previous audio URL if (audioUrlRef.current) { URL.revokeObjectURL(audioUrlRef.current); } // Create new audio element const url = URL.createObjectURL(originalFile); audioUrlRef.current = url; const audio = new Audio(url); audio.loop = isLooping; audio.playbackRate = playbackSpeed; audio.addEventListener('timeupdate', () => { setCurrentTime(audio.currentTime); if (audioData) { const position = audio.currentTime / audioData.duration; setSeekPosition(position); } }); audio.addEventListener('ended', () => { if (!isLooping) { setIsPlaying(false); setCurrentTime(0); setSeekPosition(0); } }); audioRef.current = audio; return () => { audio.pause(); audio.src = ''; if (audioUrlRef.current) { URL.revokeObjectURL(audioUrlRef.current); audioUrlRef.current = null; } }; } }, [originalFile, audioData]); // Update audio playback state useEffect(() => { if (audioRef.current) { audioRef.current.loop = isLooping; audioRef.current.playbackRate = playbackSpeed; } }, [isLooping, playbackSpeed]); // Handle play/pause useEffect(() => { if (audioRef.current) { if (isPlaying) { audioRef.current.play().catch(console.error); } else { audioRef.current.pause(); } } }, [isPlaying]); const handleFileSelect = useCallback((file: File) => { loadAudioFile(file); if (isMicActive) { setIsMicActive(false); } }, [loadAudioFile, isMicActive]); const toggleMic = useCallback(async () => { playSound('click'); if (isMicActive) { // Stop microphone isMicActiveRef.current = false; if (micStream) { micStream.getTracks().forEach(track => track.stop()); setMicStream(null); } setMicAnalyzer(null); setMicGainNode(null); setIsMicActive(false); setMicLevel(0); setShowMicCalibration(false); resetAudio(); } else { // Start microphone try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); setMicStream(stream); const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; analyser.smoothingTimeConstant = 0.8; const gainNode = audioContext.createGain(); gainNode.gain.value = micGain; setMicGainNode(gainNode); const source = audioContext.createMediaStreamSource(stream); source.connect(gainNode); gainNode.connect(analyser); // Start monitoring mic levels isMicActiveRef.current = true; const dataArray = new Uint8Array(analyser.frequencyBinCount); const monitorLevels = () => { if (isMicActiveRef.current && analyser) { analyser.getByteFrequencyData(dataArray); const average = dataArray.reduce((a, b) => a + b) / dataArray.length; setMicLevel(average / 255); // Normalize to 0-1 requestAnimationFrame(monitorLevels); } }; monitorLevels(); setMicAnalyzer(analyser); setIsMicActive(true); resetAudio(); // Clear any loaded file } catch (error) { console.error('Error accessing microphone:', error); alert('Could not access microphone. Please check permissions.'); } } }, [isMicActive, micStream, playSound, resetAudio, micGain]); const handlePreview = useCallback(() => { playSound('click'); setIsPlaying(!isPlaying); }, [isPlaying, playSound]); const handleSeek = useCallback((position: number) => { if (audioData && audioRef.current && position >= 0) { const newTime = position * audioData.duration; audioRef.current.currentTime = newTime; setCurrentTime(newTime); setSeekPosition(position); } }, [audioData]); const handlePlaybackSpeedChange = useCallback((speed: number) => { setPlaybackSpeed(speed); }, []); const handleLoopingChange = useCallback((looping: boolean) => { setIsLooping(looping); }, []); const handleExportResolutionChange = useCallback((resolution: string) => { setExportResolution(resolution as '640x480' | '1280x720' | '1920x1080'); }, []); const handleExportFpsChange = useCallback((fps: number) => { setExportFps(fps as 24 | 30 | 60); }, []); const handleExportQualityChange = useCallback((quality: string) => { setExportQuality(quality as 'low' | 'medium' | 'high'); }, []); const handleGenerate = useCallback(async () => { if (!audioData || !originalFile) return; const [width, height] = exportResolution.split('x').map(Number); await exportVideo(audioData, originalFile, { width, height, fps: exportFps, mode, audioFile: originalFile, quality: exportQuality, }); }, [audioData, originalFile, exportVideo, mode, exportResolution, exportFps, exportQuality]); const handleReset = useCallback(() => { playSound('click'); setIsPlaying(false); resetAudio(); resetExport(); }, [playSound, resetAudio, resetExport]); const canPreview = (audioData !== null || isMicActive) && !isExporting; const canGenerate = audioData !== null && originalFile !== null && !isExporting && !isLoading; return (
{/* Header */}

Audio Oscilloscope

Visualize audio waveforms in real-time

{/* Main Content: Display + Controls Side by Side */}
{/* Oscilloscope Display */}
{ setIsPlaying(false); setCurrentTime(0); setSeekPosition(0); }} onSeek={handleSeek} liveSettings={liveSettings} /> {/* Audio Uploader - below controls on desktop */}
{/* Audio Playback Controls */} {audioData && originalFile && (
{/* Play/Pause and Time Display */}
{formatTime(currentTime)} / {formatTime(audioData.duration)} {/* Progress Bar */}
handleSeek(value[0] / 100)} max={100} step={0.1} className="cursor-pointer" />
)}
{/* Control Panel */}
{/* Microphone Calibration */} {showMicCalibration && isMicActive && (

Microphone Calibration

{/* Level Indicator */}
Input Level {Math.round(micLevel * 100)}%
Quiet 0.7 ? 'text-red-400' : micLevel > 0.5 ? 'text-yellow-400' : 'text-green-400'}> {micLevel > 0.7 ? 'Too Loud' : micLevel > 0.5 ? 'Good' : 'Too Quiet'} Loud
{/* Gain Control */}
Gain {micGain.toFixed(1)}x
setMicGain(Number(e.target.value))} className="w-full" />

Speak into your microphone and adjust gain until the level shows "Good" (green). The bar should peak around 50-70% when speaking normally.

)} {showMicCalibration && isMicActive && (

Microphone Calibration

{/* Level Indicator */}
Input Level {Math.round(micLevel * 100)}%
Quiet 0.7 ? 'text-red-400' : micLevel > 0.5 ? 'text-yellow-400' : 'text-green-400'}> {micLevel > 0.7 ? 'Too Loud' : micLevel > 0.5 ? 'Good' : 'Too Quiet'} Loud
{/* Gain Control */}
Gain {micGain.toFixed(1)}x
setMicGain(Number(e.target.value))} className="w-full" />

Speak into your microphone and adjust gain until the level shows "Good" (green). The bar should peak around 50-70% when speaking normally.

)} {/* Status Info */} {(isLoading || isExporting) && (
{isLoading && "Loading audio..."} {isExporting && `Exporting video... ${progress}%`}
)}
); }