'use client'; import * as React from 'react'; import { Music, Loader2 } from 'lucide-react'; import { Waveform } from './Waveform'; import { PlaybackControls } from './PlaybackControls'; import { SidePanel } from '@/components/layout/SidePanel'; import { ThemeToggle } from '@/components/layout/ThemeToggle'; import { CommandPalette } from '@/components/ui/CommandPalette'; import type { CommandAction } from '@/components/ui/CommandPalette'; import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer'; import { useHistory } from '@/lib/hooks/useHistory'; import { useEffectChain } from '@/lib/hooks/useEffectChain'; import { useMultiTrack } from '@/lib/hooks/useMultiTrack'; import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer'; import { useToast } from '@/components/ui/Toast'; import { Slider } from '@/components/ui/Slider'; import { cn } from '@/lib/utils/cn'; import type { Selection, ClipboardData } from '@/types/selection'; import { extractBufferSegment, deleteBufferSegment, insertBufferSegment, trimBuffer, } from '@/lib/audio/buffer-utils'; import { createCutCommand, createDeleteCommand, createPasteCommand, createTrimCommand, } from '@/lib/history/commands/edit-command'; import { createGainCommand, createNormalizePeakCommand, createNormalizeRMSCommand, createFadeInCommand, createFadeOutCommand, createReverseCommand, createLowPassFilterCommand, createHighPassFilterCommand, createBandPassFilterCommand, EffectCommand, } from '@/lib/history/commands/effect-command'; import { applyEffectToSelection, applyAsyncEffectToSelection } from '@/lib/audio/effects/selection'; import { normalizePeak } from '@/lib/audio/effects/normalize'; import { applyFadeIn, applyFadeOut } from '@/lib/audio/effects/fade'; import { reverseAudio } from '@/lib/audio/effects/reverse'; import { applyLowPassFilter, applyHighPassFilter, applyBandPassFilter, applyFilter } from '@/lib/audio/effects/filters'; import type { FilterType } from '@/lib/audio/effects/filters'; import { applyCompressor, applyLimiter, applyGate } from '@/lib/audio/effects/dynamics'; import { applyDelay, applyReverb, applyChorus, applyFlanger, applyPhaser } from '@/lib/audio/effects/time-based'; import { applyPitchShift, applyTimeStretch, applyDistortion, applyBitcrusher } from '@/lib/audio/effects/advanced'; import { EffectParameterDialog, type FilterParameters } from '@/components/effects/EffectParameterDialog'; import { DynamicsParameterDialog, type DynamicsParameters, type DynamicsType } from '@/components/effects/DynamicsParameterDialog'; import { TimeBasedParameterDialog, type TimeBasedParameters, type TimeBasedType } from '@/components/effects/TimeBasedParameterDialog'; import { AdvancedParameterDialog, type AdvancedParameters, type AdvancedType } from '@/components/effects/AdvancedParameterDialog'; import { TrackList } from '@/components/tracks/TrackList'; import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog'; const EFFECT_LABELS: Record = { lowpass: 'Low-Pass Filter', highpass: 'High-Pass Filter', bandpass: 'Band-Pass Filter', notch: 'Notch Filter', lowshelf: 'Low Shelf Filter', highshelf: 'High Shelf Filter', peaking: 'Peaking EQ', compressor: 'Compressor', limiter: 'Limiter', gate: 'Gate/Expander', delay: 'Delay/Echo', reverb: 'Reverb', chorus: 'Chorus', flanger: 'Flanger', phaser: 'Phaser', pitch: 'Pitch Shifter', timestretch: 'Time Stretch', distortion: 'Distortion', bitcrusher: 'Bitcrusher', }; export function AudioEditor() { // View mode state const [viewMode, setViewMode] = React.useState<'waveform' | 'tracks'>('waveform'); const [importDialogOpen, setImportDialogOpen] = React.useState(false); // Zoom and scroll state const [zoom, setZoom] = React.useState(1); const [scrollOffset, setScrollOffset] = React.useState(0); const [amplitudeScale, setAmplitudeScale] = React.useState(1); // Effect dialog state const [effectDialogOpen, setEffectDialogOpen] = React.useState(false); const [effectDialogType, setEffectDialogType] = React.useState<'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'lowshelf' | 'highshelf' | 'peaking'>('lowpass'); // Dynamics dialog state const [dynamicsDialogOpen, setDynamicsDialogOpen] = React.useState(false); const [dynamicsDialogType, setDynamicsDialogType] = React.useState('compressor'); // Time-based dialog state const [timeBasedDialogOpen, setTimeBasedDialogOpen] = React.useState(false); const [timeBasedDialogType, setTimeBasedDialogType] = React.useState('delay'); // Advanced dialog state const [advancedDialogOpen, setAdvancedDialogOpen] = React.useState(false); const [advancedDialogType, setAdvancedDialogType] = React.useState('pitch'); // Drag and drop state const [isDragging, setIsDragging] = React.useState(false); const fileInputRef = React.useRef(null); // 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 { execute, undo, redo, clear: clearHistory, state: historyState } = useHistory(50); const { chain: effectChain, presets: effectPresets, toggleEffectEnabled, removeEffect, reorder: reorderEffects, clearChain, savePreset, loadPresetToChain, deletePreset, } = useEffectChain(); const { addToast } = useToast(); // Multi-track hooks const { tracks, addTrack, addTrackFromBuffer, removeTrack, updateTrack, clearTracks, } = useMultiTrack(); const { isPlaying: isMultiTrackPlaying, currentTime: multiTrackCurrentTime, duration: multiTrackDuration, play: playMultiTrack, pause: pauseMultiTrack, stop: stopMultiTrack, seek: seekMultiTrack, togglePlayPause: toggleMultiTrackPlayPause, } = useMultiTrackPlayer(tracks); 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); clearHistory(); addToast({ title: 'Audio cleared', description: 'Audio file has been removed', variant: 'info', duration: 2000, }); }; // Multi-track handlers const handleConvertToTrack = () => { if (!audioBuffer) return; const trackName = fileName || 'Audio Track'; addTrackFromBuffer(audioBuffer, trackName); setViewMode('tracks'); addToast({ title: 'Converted to Track', description: `"${trackName}" added to tracks`, variant: 'success', duration: 2000, }); }; const handleImportTracks = () => { setImportDialogOpen(true); }; const handleImportTrack = (buffer: AudioBuffer, name: string) => { addTrackFromBuffer(buffer, name); setViewMode('tracks'); }; const handleClearTracks = () => { clearTracks(); setViewMode('waveform'); addToast({ title: 'Tracks Cleared', description: 'All tracks have been removed', variant: 'info', duration: 2000, }); }; // Drag and drop handlers const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Only set to false if we're leaving the drop zone entirely if (e.currentTarget === e.target) { setIsDragging(false); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files); const audioFile = files.find(file => file.type.startsWith('audio/')); if (audioFile) { await handleFileSelect(audioFile); } else { addToast({ title: 'Invalid file', description: 'Please drop an audio file', variant: 'error', duration: 3000, }); } }; const handleDropZoneClick = () => { fileInputRef.current?.click(); }; const handleFileInputChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { handleFileSelect(file); } }; // 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, }); // Create and execute cut command const command = createCutCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); 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; // Create and execute paste command const command = createPasteCommand(audioBuffer, clipboard.buffer, insertTime, (buffer) => { loadBuffer(buffer); }); execute(command); 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 { // Create and execute delete command const command = createDeleteCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); 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 { // Create and execute trim command const command = createTrimCommand(audioBuffer, selection, (buffer) => { loadBuffer(buffer); }); execute(command); 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); }; // Effect operations const handleNormalize = () => { if (!audioBuffer) return; try { // Apply to selection or entire buffer const modifiedBuffer = applyEffectToSelection( audioBuffer, selection, (buf) => normalizePeak(buf, 1.0) ); const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? 'Normalize Selection' : 'Normalize' ); execute(command); addToast({ title: 'Normalized', description: selection ? 'Selection normalized to peak' : 'Audio normalized to peak', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to normalize audio', variant: 'error', duration: 3000, }); } }; const handleFadeIn = () => { if (!audioBuffer) return; if (!selection) { addToast({ title: 'No Selection', description: 'Please select a region to apply fade in', variant: 'info', duration: 2000, }); return; } try { const fadeDuration = selection.end - selection.start; const modifiedBuffer = applyEffectToSelection( audioBuffer, selection, (buf) => applyFadeIn(buf, buf.duration) ); const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), `Fade In (${fadeDuration.toFixed(2)}s)` ); execute(command); addToast({ title: 'Fade In', description: `Applied fade in (${fadeDuration.toFixed(2)}s)`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply fade in', variant: 'error', duration: 3000, }); } }; const handleFadeOut = () => { if (!audioBuffer) return; if (!selection) { addToast({ title: 'No Selection', description: 'Please select a region to apply fade out', variant: 'info', duration: 2000, }); return; } try { const fadeDuration = selection.end - selection.start; const modifiedBuffer = applyEffectToSelection( audioBuffer, selection, (buf) => applyFadeOut(buf, buf.duration) ); const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), `Fade Out (${fadeDuration.toFixed(2)}s)` ); execute(command); addToast({ title: 'Fade Out', description: `Applied fade out (${fadeDuration.toFixed(2)}s)`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply fade out', variant: 'error', duration: 3000, }); } }; const handleReverse = () => { if (!audioBuffer) return; try { const modifiedBuffer = applyEffectToSelection( audioBuffer, selection, (buf) => reverseAudio(buf) ); const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? 'Reverse Selection' : 'Reverse' ); execute(command); addToast({ title: 'Reversed', description: selection ? 'Selection reversed' : 'Audio reversed', variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to reverse audio', variant: 'error', duration: 3000, }); } }; const handleLowPassFilter = () => { setEffectDialogType('lowpass'); setEffectDialogOpen(true); }; const handleHighPassFilter = () => { setEffectDialogType('highpass'); setEffectDialogOpen(true); }; const handleBandPassFilter = () => { setEffectDialogType('bandpass'); setEffectDialogOpen(true); }; const handleCompressor = () => { setDynamicsDialogType('compressor'); setDynamicsDialogOpen(true); }; const handleLimiter = () => { setDynamicsDialogType('limiter'); setDynamicsDialogOpen(true); }; const handleGate = () => { setDynamicsDialogType('gate'); setDynamicsDialogOpen(true); }; const handleDelay = () => { setTimeBasedDialogType('delay'); setTimeBasedDialogOpen(true); }; const handleReverb = () => { setTimeBasedDialogType('reverb'); setTimeBasedDialogOpen(true); }; const handleChorus = () => { setTimeBasedDialogType('chorus'); setTimeBasedDialogOpen(true); }; const handleFlanger = () => { setTimeBasedDialogType('flanger'); setTimeBasedDialogOpen(true); }; const handlePhaser = () => { setTimeBasedDialogType('phaser'); setTimeBasedDialogOpen(true); }; const handlePitchShift = () => { setAdvancedDialogType('pitch'); setAdvancedDialogOpen(true); }; const handleTimeStretch = () => { setAdvancedDialogType('timestretch'); setAdvancedDialogOpen(true); }; const handleDistortion = () => { setAdvancedDialogType('distortion'); setAdvancedDialogOpen(true); }; const handleBitcrusher = () => { setAdvancedDialogType('bitcrusher'); setAdvancedDialogOpen(true); }; // Handle effect apply from parameter dialog const handleEffectApply = async (params: FilterParameters) => { if (!audioBuffer) return; try { const modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyFilter(buf, { type: params.type, frequency: params.frequency, Q: params.Q, gain: params.gain, }) ); const effectName = EFFECT_LABELS[params.type] || 'Filter'; const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? `${effectName} Selection (${params.frequency.toFixed(0)}Hz)` : `${effectName} (${params.frequency.toFixed(0)}Hz)` ); execute(command); addToast({ title: effectName, description: selection ? `Applied ${effectName.toLowerCase()} to selection (${params.frequency.toFixed(0)}Hz)` : `Applied ${effectName.toLowerCase()} (${params.frequency.toFixed(0)}Hz)`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply effect', variant: 'error', duration: 3000, }); } }; // Handle dynamics apply from parameter dialog const handleDynamicsApply = async (params: DynamicsParameters) => { if (!audioBuffer) return; try { let modifiedBuffer: AudioBuffer; let effectName: string; switch (params.type) { case 'compressor': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyCompressor(buf, params) ); effectName = 'Compressor'; break; case 'limiter': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyLimiter(buf, params) ); effectName = 'Limiter'; break; case 'gate': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyGate(buf, params) ); effectName = 'Gate'; break; } const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? `${effectName} Selection (${params.threshold.toFixed(1)}dB)` : `${effectName} (${params.threshold.toFixed(1)}dB)` ); execute(command); addToast({ title: effectName, description: selection ? `Applied ${effectName.toLowerCase()} to selection` : `Applied ${effectName.toLowerCase()}`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply dynamics effect', variant: 'error', duration: 3000, }); } }; // Handle time-based apply from parameter dialog const handleTimeBasedApply = async (params: TimeBasedParameters) => { if (!audioBuffer) return; try { let modifiedBuffer: AudioBuffer; let effectName: string; switch (params.type) { case 'delay': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyDelay(buf, params) ); effectName = 'Delay'; break; case 'reverb': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyReverb(buf, params) ); effectName = 'Reverb'; break; case 'chorus': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyChorus(buf, params) ); effectName = 'Chorus'; break; case 'flanger': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyFlanger(buf, params) ); effectName = 'Flanger'; break; case 'phaser': modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyPhaser(buf, params) ); effectName = 'Phaser'; break; } const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? `${effectName} Selection` : `${effectName}` ); execute(command); addToast({ title: effectName, description: selection ? `Applied ${effectName.toLowerCase()} to selection` : `Applied ${effectName.toLowerCase()}`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply time-based effect', variant: 'error', duration: 3000, }); } }; const handleAdvancedApply = async (params: AdvancedParameters) => { if (!audioBuffer) return; try { let modifiedBuffer: AudioBuffer; let effectName: string; const effectType = params.type; if (effectType === 'pitch') { modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyPitchShift(buf, params as any) ); effectName = 'Pitch Shift'; } else if (effectType === 'timestretch') { modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyTimeStretch(buf, params as any) ); effectName = 'Time Stretch'; } else if (effectType === 'bitcrusher') { modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyBitcrusher(buf, params as any) ); effectName = 'Bitcrusher'; } else { modifiedBuffer = await applyAsyncEffectToSelection( audioBuffer, selection, (buf) => applyDistortion(buf, params as any) ); effectName = 'Distortion'; } const command = new EffectCommand( audioBuffer, modifiedBuffer, (buffer) => loadBuffer(buffer), selection ? `${effectName} Selection` : `${effectName}` ); execute(command); addToast({ title: effectName, description: selection ? `Applied ${effectName.toLowerCase()} to selection` : `Applied ${effectName.toLowerCase()}`, variant: 'success', duration: 2000, }); } catch (error) { addToast({ title: 'Error', description: 'Failed to apply advanced effect', variant: 'error', duration: 3000, }); } }; // 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 const isTyping = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement; // Spacebar: Play/Pause (always, unless typing in an input) if (e.code === 'Space' && !isTyping) { e.preventDefault(); if (isPlaying) { pause(); } else { play(); } return; } // Prevent other shortcuts if typing in an input if (isTyping) { return; } // Ctrl+Z: Undo if (e.ctrlKey && !e.shiftKey && e.key === 'z') { e.preventDefault(); if (undo()) { addToast({ title: 'Undo', description: 'Last action undone', variant: 'info', duration: 1500, }); } } // Ctrl+Y or Ctrl+Shift+Z: Redo if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) { e.preventDefault(); if (redo()) { addToast({ title: 'Redo', description: 'Last action redone', variant: 'info', duration: 1500, }); } } // 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, undo, redo, addToast, isPlaying, play, pause]); // Show error toast React.useEffect(() => { if (error) { addToast({ title: 'Error', description: error, variant: 'error', duration: 5000, }); } }, [error, addToast]); // Command palette actions const commandActions: CommandAction[] = React.useMemo(() => { const actions: CommandAction[] = [ // Playback { id: 'play', label: 'Play', description: 'Start playback', shortcut: 'Space', category: 'playback', action: play, }, { id: 'pause', label: 'Pause', description: 'Pause playback', shortcut: 'Space', category: 'playback', action: pause, }, { id: 'stop', label: 'Stop', description: 'Stop playback', category: 'playback', action: stop, }, // Edit { id: 'cut', label: 'Cut', description: 'Cut selection to clipboard', shortcut: 'Ctrl+X', category: 'edit', action: handleCut, }, { id: 'copy', label: 'Copy', description: 'Copy selection to clipboard', shortcut: 'Ctrl+C', category: 'edit', action: handleCopy, }, { id: 'paste', label: 'Paste', description: 'Paste clipboard at current position', shortcut: 'Ctrl+V', category: 'edit', action: handlePaste, }, { id: 'delete', label: 'Delete', description: 'Delete selection', shortcut: 'Del', category: 'edit', action: handleDelete, }, { id: 'trim', label: 'Trim to Selection', description: 'Trim audio to selected region', category: 'edit', action: handleTrim, }, { id: 'select-all', label: 'Select All', description: 'Select entire audio', shortcut: 'Ctrl+A', category: 'edit', action: handleSelectAll, }, { id: 'clear-selection', label: 'Clear Selection', description: 'Clear current selection', shortcut: 'Esc', category: 'edit', action: handleClearSelection, }, // View { id: 'zoom-in', label: 'Zoom In', description: 'Zoom in on waveform', category: 'view', action: handleZoomIn, }, { id: 'zoom-out', label: 'Zoom Out', description: 'Zoom out on waveform', category: 'view', action: handleZoomOut, }, { id: 'fit-to-view', label: 'Fit to View', description: 'Reset zoom to fit entire waveform', category: 'view', action: handleFitToView, }, // File { id: 'clear', label: 'Clear Audio', description: 'Remove loaded audio file', category: 'file', action: handleClear, }, // History { id: 'undo', label: 'Undo', description: 'Undo last action', shortcut: 'Ctrl+Z', category: 'edit', action: undo, }, { id: 'redo', label: 'Redo', description: 'Redo last undone action', shortcut: 'Ctrl+Y', category: 'edit', action: redo, }, // Effects { id: 'normalize', label: 'Normalize', description: 'Normalize audio to peak amplitude', category: 'effects', action: handleNormalize, }, { id: 'fade-in', label: 'Fade In', description: 'Apply fade in to selection', category: 'effects', action: handleFadeIn, }, { id: 'fade-out', label: 'Fade Out', description: 'Apply fade out to selection', category: 'effects', action: handleFadeOut, }, { id: 'reverse', label: 'Reverse', description: 'Reverse entire audio', category: 'effects', action: handleReverse, }, { id: 'lowpass-filter', label: 'Low-Pass Filter', description: 'Remove high frequencies (1000Hz cutoff)', category: 'effects', action: handleLowPassFilter, }, { id: 'highpass-filter', label: 'High-Pass Filter', description: 'Remove low frequencies (100Hz cutoff)', category: 'effects', action: handleHighPassFilter, }, { id: 'bandpass-filter', label: 'Band-Pass Filter', description: 'Isolate frequency range (1000Hz center)', category: 'effects', action: handleBandPassFilter, }, { id: 'compressor', label: 'Compressor', description: 'Reduce dynamic range', category: 'effects', action: handleCompressor, }, { id: 'limiter', label: 'Limiter', description: 'Prevent audio from exceeding threshold', category: 'effects', action: handleLimiter, }, { id: 'gate', label: 'Gate/Expander', description: 'Reduce volume of quiet sounds', category: 'effects', action: handleGate, }, { id: 'delay', label: 'Delay/Echo', description: 'Add echo effects with feedback', category: 'effects', action: handleDelay, }, { id: 'reverb', label: 'Reverb', description: 'Add acoustic space and ambience', category: 'effects', action: handleReverb, }, { id: 'chorus', label: 'Chorus', description: 'Thicken sound with modulation', category: 'effects', action: handleChorus, }, { id: 'flanger', label: 'Flanger', description: 'Create sweeping comb-filter effect', category: 'effects', action: handleFlanger, }, { id: 'phaser', label: 'Phaser', description: 'Phase-shifting swoosh effect', category: 'effects', action: handlePhaser, }, { id: 'pitch', label: 'Pitch Shifter', description: 'Change pitch without affecting duration', category: 'effects', action: handlePitchShift, }, { id: 'timestretch', label: 'Time Stretch', description: 'Change duration without affecting pitch', category: 'effects', action: handleTimeStretch, }, { id: 'distortion', label: 'Distortion', description: 'Add overdrive and distortion', category: 'effects', action: handleDistortion, }, { id: 'bitcrusher', label: 'Bitcrusher', description: 'Lo-fi bit depth and sample rate reduction', category: 'effects', action: handleBitcrusher, }, ]; return actions; }, [play, pause, stop, handleCut, handleCopy, handlePaste, handleDelete, handleTrim, handleSelectAll, handleClearSelection, handleZoomIn, handleZoomOut, handleFitToView, handleClear, undo, redo, handleNormalize, handleFadeIn, handleFadeOut, handleReverse, handleLowPassFilter, handleHighPassFilter, handleBandPassFilter, handleCompressor, handleLimiter, handleGate, handleDelay, handleReverb, handleChorus, handleFlanger, handlePhaser, handlePitchShift, handleTimeStretch, handleDistortion, handleBitcrusher]); return ( <> {/* Compact Header */}
{/* Left: Logo */}

Audio UI

{/* Right: Command Palette + Theme Toggle */}
{/* Main content area */}
{/* Side Panel */} {/* Main canvas area */}
{isLoading ? (

Loading audio file...

) : viewMode === 'tracks' ? ( <> {/* Multi-Track View */}
{/* Multi-Track Playback Controls */}
{}} currentTimeFormatted={`${Math.floor(multiTrackCurrentTime / 60)}:${String(Math.floor(multiTrackCurrentTime % 60)).padStart(2, '0')}`} durationFormatted={`${Math.floor(multiTrackDuration / 60)}:${String(Math.floor(multiTrackDuration % 60)).padStart(2, '0')}`} />
) : audioBuffer ? ( <> {/* Waveform - takes maximum space */}
{/* Horizontal scroll for zoomed waveform */} {zoom > 1 && (
)}
{/* Playback Controls - fixed at bottom */}
) : (

{isDragging ? 'Drop audio file here' : 'No audio file loaded'}

{isDragging ? 'Release to load the file' : 'Click here or use the side panel to load an audio file, or drag and drop a file onto this area.'}

)}
{/* Effect Parameter Dialog */} setEffectDialogOpen(false)} effectType={effectDialogType} onApply={handleEffectApply} sampleRate={audioBuffer?.sampleRate} /> {/* Dynamics Parameter Dialog */} setDynamicsDialogOpen(false)} effectType={dynamicsDialogType} onApply={handleDynamicsApply} /> {/* Time-Based Parameter Dialog */} setTimeBasedDialogOpen(false)} effectType={timeBasedDialogType} onApply={handleTimeBasedApply} /> {/* Advanced Parameter Dialog */} setAdvancedDialogOpen(false)} effectType={advancedDialogType} onApply={handleAdvancedApply} /> {/* Import Track Dialog */} setImportDialogOpen(false)} onImportTrack={handleImportTrack} /> ); }