import React, { useState, useRef, useEffect, useMemo } from 'react'; import { Download, Link as LinkIcon, Play, Pause, X, HelpCircle, Scissors, Wand2, UploadCloud, Sparkles, CheckCircle2, AudioWaveform, RefreshCcw, Check } from 'lucide-react'; import { SidePanel } from './components/SidePanel'; import { Waveform } from './components/Waveform'; import { AudioFileState, ProcessingStats } from './types'; import { formatTime, analyzeSilence, processAudio, createProcessedBuffer, generateUUID } from './utils/audio'; const App: React.FC = () => { const [sidePanelOpen, setSidePanelOpen] = useState(false); const [isDragOver, setIsDragOver] = useState(false); // Audio State const [audioState, setAudioState] = useState({ file: null, url: null, name: '', duration: 0, buffer: null }); // Processed State const [processedBuffer, setProcessedBuffer] = useState(null); const [processedStats, setProcessedStats] = useState(null); const [lastProcessedConfig, setLastProcessedConfig] = useState<{fileId: string, duration: number} | null>(null); // App Logic State const [hasProcessed, setHasProcessed] = useState(false); const [isProcessingUI, setIsProcessingUI] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); const [isCopied, setIsCopied] = useState(false); // Playback State const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [removeSilenceActive, setRemoveSilenceActive] = useState(false); const [isProcessingDownload, setIsProcessingDownload] = useState(false); // Processing Parameters const [maxSilenceDuration, setMaxSilenceDuration] = useState(0.5); // Seconds // Refs const audioContextRef = useRef(null); const sourceNodeRef = useRef(null); const startTimeRef = useRef(0); const pausedTimeRef = useRef(0); const animationFrameRef = useRef(null); const fileInputRef = useRef(null); const resultContainerRef = useRef(null); // Computed Silence Regions (Calculated on fly for visualizer of original) const silenceRegions = useMemo(() => { if (!audioState.buffer) return []; return analyzeSilence(audioState.buffer, maxSilenceDuration); }, [audioState.buffer, maxSilenceDuration]); // Initialize AudioContext useEffect(() => { audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); return () => { if (audioContextRef.current?.state !== 'closed') { audioContextRef.current?.close(); } if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, []); const handleFileUpload = async (file: File) => { if (!file.type.startsWith('audio/')) { alert('Please upload an audio file.'); return; } setIsAnalyzing(true); setHasProcessed(false); setProcessedBuffer(null); setLastProcessedConfig(null); handleResetPlayback(); const arrayBuffer = await file.arrayBuffer(); const audioContext = audioContextRef.current!; try { const decodedBuffer = await audioContext.decodeAudioData(arrayBuffer); const url = URL.createObjectURL(file); setAudioState({ file, url, name: file.name, duration: decodedBuffer.duration, buffer: decodedBuffer }); } catch (e) { console.error(e); alert("Error decoding audio file."); } finally { setIsAnalyzing(false); } }; const onDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { handleFileUpload(e.dataTransfer.files[0]); } }; const getActiveBuffer = () => { return removeSilenceActive && processedBuffer ? processedBuffer : audioState.buffer; }; const getActiveDuration = () => { const buf = getActiveBuffer(); return buf ? buf.duration : 0; }; const playAudio = (startOffset?: number) => { const buffer = getActiveBuffer(); if (!audioContextRef.current || !buffer) return; if (audioContextRef.current.state === 'suspended') { audioContextRef.current.resume(); } // Stop existing source if any if (sourceNodeRef.current) { sourceNodeRef.current.stop(); } const source = audioContextRef.current.createBufferSource(); source.buffer = buffer; source.connect(audioContextRef.current.destination); const duration = buffer.duration; // Use provided offset or fall back to tracked time const offset = startOffset !== undefined ? startOffset : (pausedTimeRef.current % duration); source.start(0, offset); startTimeRef.current = audioContextRef.current.currentTime - offset; sourceNodeRef.current = source; setIsPlaying(true); // Cancel existing loop if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } const animate = () => { const now = audioContextRef.current!.currentTime; let progress = now - startTimeRef.current; if (progress >= duration) { pauseAudio(); setCurrentTime(0); pausedTimeRef.current = 0; } else { setCurrentTime(progress); animationFrameRef.current = requestAnimationFrame(animate); } }; animationFrameRef.current = requestAnimationFrame(animate); }; const pauseAudio = () => { if (sourceNodeRef.current) { sourceNodeRef.current.stop(); sourceNodeRef.current = null; } if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } if (audioContextRef.current) { pausedTimeRef.current = audioContextRef.current.currentTime - startTimeRef.current; } setIsPlaying(false); }; const handleResetPlayback = () => { pauseAudio(); setCurrentTime(0); pausedTimeRef.current = 0; }; const togglePlay = () => { if (isPlaying) pauseAudio(); else playAudio(); }; const handleToggleView = () => { const newState = !removeSilenceActive; // Stop playback, reset time, update state if (isPlaying) { pauseAudio(); } setCurrentTime(0); pausedTimeRef.current = 0; setRemoveSilenceActive(newState); // Optional: Auto-play when switching views? // User requirement was about seeking, but auto-playing here might be jarring. // We'll keep it paused to be safe unless requested otherwise. }; const handleSeek = (time: number) => { // Update time references pausedTimeRef.current = time; setCurrentTime(time); // If it was playing, restart immediately from new time if (isPlaying) { playAudio(time); } }; const handleResetFile = () => { handleResetPlayback(); setAudioState({ file: null, url: null, name: '', duration: 0, buffer: null }); setHasProcessed(false); setProcessedBuffer(null); setRemoveSilenceActive(false); setMaxSilenceDuration(0.5); }; const handleDownload = async () => { if (!audioState.buffer) return; setIsProcessingDownload(true); setTimeout(async () => { try { const wavBlob = processAudio(audioState.buffer!, silenceRegions); const uuid = generateUUID().slice(0, 8); const originalName = audioState.name.replace(/\.[^/.]+$/, ""); const filename = `${originalName}_clean_${uuid}.wav`; const url = URL.createObjectURL(wavBlob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error("Export failed:", error); alert("Failed to process audio."); } finally { setIsProcessingDownload(false); } }, 100); }; const handleCopyUrl = () => { const uuid = generateUUID(); const fakeUrl = `${window.location.origin}/share/${uuid}`; navigator.clipboard.writeText(fakeUrl); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }; const handleProcessClick = async () => { if (!audioState.buffer || !audioContextRef.current) return; if (hasProcessed && lastProcessedConfig?.fileId === audioState.name && lastProcessedConfig?.duration === maxSilenceDuration) { setRemoveSilenceActive(true); return; } setIsProcessingUI(true); setHasProcessed(false); // Reduced delay for snappy feel (600ms) setTimeout(() => { const buffer = audioState.buffer!; const totalSilence = silenceRegions.reduce((acc, region) => acc + (region.end - region.start), 0); const oldDuration = buffer.duration; const newDuration = Math.max(0, oldDuration - totalSilence); const processingTime = oldDuration * 0.15; const pBuffer = createProcessedBuffer(buffer, silenceRegions, audioContextRef.current!); setProcessedStats({ oldDuration, newDuration, timeSaved: totalSilence, processingTime }); setProcessedBuffer(pBuffer); setLastProcessedConfig({ fileId: audioState.name, duration: maxSilenceDuration }); setHasProcessed(true); setIsProcessingUI(false); setRemoveSilenceActive(true); handleResetPlayback(); }, 600); }; const triggerFileInput = () => { fileInputRef.current?.click(); } return (
{/* Background Glow */}
{/* Header */}

Silence Remover

{/* PERMANENT SPLIT GRID LAYOUT */}
{/* LEFT COLUMN (Controls / Upload) - Span 4 */}
{/* 1. UPLOAD BOX OR CONTROLS */} {!audioState.buffer ? ( /* UPLOAD STATE */
{ e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={onDrop} > e.target.files && handleFileUpload(e.target.files[0])} className="hidden" /> {isAnalyzing ? (

Analyzing Audio...

) : (

Upload Audio

Drag & drop your audio file here
or click to browse

WAV MP3 M4A
)}
) : ( /* CONTROLS STATE */
{/* File Info */}
File Loaded

{audioState.name}

{formatTime(audioState.duration)}

{/* Slider */}
{maxSilenceDuration.toFixed(1)}s
{/* Track Background - Explicitly styled for visibility */}
setMaxSilenceDuration(parseFloat(e.target.value))} className="absolute w-full h-full opacity-0 cursor-pointer z-20" />

Silence longer than {maxSilenceDuration.toFixed(1)} seconds will be removed.

{/* Action Buttons */}
)}
{/* RIGHT COLUMN (Results / Animation) - Span 8 */}
{/* CONDITION 1: PROCESSING ANIMATION */} {isProcessingUI && (
{/* Spinning Rings */}
{/* Center Icon */}
{/* Scanning Line */}

Optimizing Audio...

Removing silence patterns

)} {/* CONDITION 2: RESULT VIEW */} {hasProcessed && audioState.buffer ? (
{/* PLAYER CARD */}
{/* Visualizer Area */}
{/* Big Play Button Overlay */}
{/* Player Controls Footer */}
{/* Left: Empty or extra controls */}
{removeSilenceActive ? 'Processed Audio' : 'Original Audio'}
{/* Center: Toggle Switch */}
Original
Processed
{/* Right: Time Display */}
{formatTime(currentTime)} | {formatTime(getActiveDuration())}
{/* STATS ROW */}

Original Duration

{formatTime(processedStats?.oldDuration || 0)}

New Duration

{formatTime(processedStats?.newDuration || 0)}

{processedStats && (
-{processedStats.timeSaved.toFixed(1)}s
)}
{/* DOWNLOAD ACTIONS */}
) : ( /* CONDITION 3: EMPTY / IDLE STATE */
{audioState.buffer ? ( <>

Ready to Process

Your audio is loaded. Configure settings on the left and click Process to generate your clean audio.

) : ( <>

Waiting for audio...

)}
)}
setSidePanelOpen(false)} />
); }; export default App;