'use client'; import * as React from 'react'; import { FileUpload } from './FileUpload'; import { AudioInfo } from './AudioInfo'; import { Waveform } from './Waveform'; import { PlaybackControls } from './PlaybackControls'; import { ZoomControls } from './ZoomControls'; import { EditControls } from './EditControls'; import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer'; import { useToast } from '@/components/ui/Toast'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; import { Slider } from '@/components/ui/Slider'; import { Loader2 } from 'lucide-react'; import type { Selection, ClipboardData } from '@/types/selection'; import { extractBufferSegment, deleteBufferSegment, insertBufferSegment, trimBuffer, } from '@/lib/audio/buffer-utils'; export function AudioEditor() { // Zoom and scroll state const [zoom, setZoom] = React.useState(1); const [scrollOffset, setScrollOffset] = React.useState(0); const [amplitudeScale, setAmplitudeScale] = React.useState(1); // Selection state const [selection, setSelection] = React.useState(null); const [clipboard, setClipboard] = React.useState(null); const { loadFile, loadBuffer, clearFile, play, pause, stop, seek, setVolume, isPlaying, isPaused, currentTime, duration, volume, audioBuffer, fileName, isLoading, error, currentTimeFormatted, durationFormatted, } = useAudioPlayer(); const { addToast } = useToast(); const handleFileSelect = async (file: File) => { try { await loadFile(file); addToast({ title: 'File loaded', description: `Successfully loaded ${file.name}`, variant: 'success', duration: 3000, }); } catch (err) { addToast({ title: 'Error loading file', description: err instanceof Error ? err.message : 'Unknown error', variant: 'error', duration: 5000, }); } }; const handleClear = () => { clearFile(); setZoom(1); setScrollOffset(0); setAmplitudeScale(1); setSelection(null); setClipboard(null); addToast({ title: 'Audio cleared', description: 'Audio file has been removed', variant: 'info', duration: 2000, }); }; // Edit operations const handleCut = () => { if (!selection || !audioBuffer) return; try { // Copy to clipboard const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end); setClipboard({ buffer: clipData, start: selection.start, end: selection.end, duration: selection.end - selection.start, }); // Delete from buffer const newBuffer = deleteBufferSegment(audioBuffer, selection.start, selection.end); loadBuffer(newBuffer); setSelection(null); addToast({ title: 'Cut', description: 'Selection cut to clipboard', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to cut selection', variant: 'error', duration: 3000, }); } }; const handleCopy = () => { if (!selection || !audioBuffer) return; try { const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end); setClipboard({ buffer: clipData, start: selection.start, end: selection.end, duration: selection.end - selection.start, }); addToast({ title: 'Copied', description: 'Selection copied to clipboard', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to copy selection', variant: 'error', duration: 3000, }); } }; const handlePaste = () => { if (!clipboard || !audioBuffer) return; try { const insertTime = currentTime; const newBuffer = insertBufferSegment(audioBuffer, clipboard.buffer, insertTime); loadBuffer(newBuffer); addToast({ title: 'Pasted', description: 'Clipboard pasted at current position', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to paste clipboard', variant: 'error', duration: 3000, }); } }; const handleDelete = () => { if (!selection || !audioBuffer) return; try { const newBuffer = deleteBufferSegment(audioBuffer, selection.start, selection.end); loadBuffer(newBuffer); setSelection(null); addToast({ title: 'Deleted', description: 'Selection deleted', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to delete selection', variant: 'error', duration: 3000, }); } }; const handleTrim = () => { if (!selection || !audioBuffer) return; try { const newBuffer = trimBuffer(audioBuffer, selection.start, selection.end); loadBuffer(newBuffer); setSelection(null); addToast({ title: 'Trimmed', description: 'Audio trimmed to selection', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to trim audio', variant: 'error', duration: 3000, }); } }; const handleSelectAll = () => { if (!audioBuffer) return; setSelection({ start: 0, end: duration }); }; const handleClearSelection = () => { setSelection(null); }; // Zoom controls const handleZoomIn = () => { setZoom((prev) => Math.min(20, prev + 1)); }; const handleZoomOut = () => { setZoom((prev) => Math.max(1, prev - 1)); }; const handleFitToView = () => { setZoom(1); setScrollOffset(0); }; // Auto-adjust scroll when zoom changes React.useEffect(() => { if (!audioBuffer) return; // Reset scroll if zoomed out completely if (zoom === 1) { setScrollOffset(0); } }, [zoom, audioBuffer]); // Keyboard shortcuts React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Prevent shortcuts if typing in an input if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } // Ctrl+A: Select all if (e.ctrlKey && e.key === 'a') { e.preventDefault(); handleSelectAll(); } // Ctrl+X: Cut if (e.ctrlKey && e.key === 'x') { e.preventDefault(); handleCut(); } // Ctrl+C: Copy if (e.ctrlKey && e.key === 'c') { e.preventDefault(); handleCopy(); } // Ctrl+V: Paste if (e.ctrlKey && e.key === 'v') { e.preventDefault(); handlePaste(); } // Delete: Delete selection if (e.key === 'Delete') { e.preventDefault(); handleDelete(); } // Escape: Clear selection if (e.key === 'Escape') { e.preventDefault(); handleClearSelection(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selection, clipboard, audioBuffer, currentTime]); // Show error toast React.useEffect(() => { if (error) { addToast({ title: 'Error', description: error, variant: 'error', duration: 5000, }); } }, [error, addToast]); return (
{/* File Upload or Audio Info */} {!audioBuffer ? ( ) : ( )} {/* Loading State */} {isLoading && (

Loading audio file...

)} {/* Waveform and Controls */} {audioBuffer && !isLoading && ( <> {/* Waveform */} Waveform {/* Horizontal scroll for zoomed waveform */} {zoom > 1 && (
)}
{/* Edit Controls */} Edit {/* Zoom Controls */} Zoom & View {/* Playback Controls */} Playback )}
); }