- Master
+ {/* Master Label with collapse button */}
+
+
+ Master
+
+ {onToggleCollapse && (
+
+
+
+ )}
{/* Pan Control */}
diff --git a/components/dialogs/BrowserCompatDialog.tsx b/components/dialogs/BrowserCompatDialog.tsx
new file mode 100644
index 0000000..c3268d3
--- /dev/null
+++ b/components/dialogs/BrowserCompatDialog.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import * as React from 'react';
+import { AlertTriangle, XCircle, Info, X } from 'lucide-react';
+import { Modal } from '@/components/ui/Modal';
+import { Button } from '@/components/ui/Button';
+import { getBrowserInfo } from '@/lib/utils/browser-compat';
+
+interface BrowserCompatDialogProps {
+ open: boolean;
+ missingFeatures: string[];
+ warnings: string[];
+ onClose: () => void;
+}
+
+export function BrowserCompatDialog({
+ open,
+ missingFeatures,
+ warnings,
+ onClose,
+}: BrowserCompatDialogProps) {
+ const [browserInfo, setBrowserInfo] = React.useState({ name: 'Unknown', version: 'Unknown' });
+ const hasErrors = missingFeatures.length > 0;
+
+ // Get browser info only on client side
+ React.useEffect(() => {
+ setBrowserInfo(getBrowserInfo());
+ }, []);
+
+ if (!open) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+ {hasErrors ? (
+ <>
+
+
Browser Not Supported
+ >
+ ) : (
+ <>
+
+
Browser Warnings
+ >
+ )}
+
+
+
+
+
+
+
+ {hasErrors ? (
+ <>Your browser is missing required features to run this audio editor.>
+ ) : (
+ <>Some features may not work as expected in your browser.>
+ )}
+
+
+
+ {/* Browser Info */}
+
+
+
+ {browserInfo.name} {browserInfo.version}
+
+
+
+ {/* Missing Features */}
+ {missingFeatures.length > 0 && (
+
+
+
+ Missing Required Features:
+
+
+ {missingFeatures.map((feature) => (
+ {feature}
+ ))}
+
+
+ )}
+
+ {/* Warnings */}
+ {warnings.length > 0 && (
+
+
+
+ Warnings:
+
+
+ {warnings.map((warning) => (
+ {warning}
+ ))}
+
+
+ )}
+
+ {/* Recommendations */}
+ {hasErrors && (
+
+
Recommended Browsers:
+
+ • Chrome 90+ or Edge 90+
+ • Firefox 88+
+ • Safari 14+
+
+
+ )}
+
+ {/* Actions */}
+
+ {hasErrors ? (
+
+ Close
+
+ ) : (
+
+ Continue Anyway
+
+ )}
+
+
+
+
+ );
+}
diff --git a/components/dialogs/KeyboardShortcutsDialog.tsx b/components/dialogs/KeyboardShortcutsDialog.tsx
new file mode 100644
index 0000000..9335aa1
--- /dev/null
+++ b/components/dialogs/KeyboardShortcutsDialog.tsx
@@ -0,0 +1,140 @@
+'use client';
+
+import * as React from 'react';
+import { Keyboard, X } from 'lucide-react';
+import { Modal } from '@/components/ui/Modal';
+import { Button } from '@/components/ui/Button';
+import { cn } from '@/lib/utils/cn';
+
+export interface KeyboardShortcutsDialogProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+interface ShortcutCategory {
+ name: string;
+ shortcuts: Array<{
+ keys: string[];
+ description: string;
+ }>;
+}
+
+const SHORTCUTS: ShortcutCategory[] = [
+ {
+ name: 'Playback',
+ shortcuts: [
+ { keys: ['Space'], description: 'Play / Pause' },
+ { keys: ['Home'], description: 'Go to Start' },
+ { keys: ['End'], description: 'Go to End' },
+ { keys: ['←'], description: 'Seek Backward' },
+ { keys: ['→'], description: 'Seek Forward' },
+ { keys: ['Ctrl', '←'], description: 'Seek Backward 5s' },
+ { keys: ['Ctrl', '→'], description: 'Seek Forward 5s' },
+ ],
+ },
+ {
+ name: 'Edit',
+ shortcuts: [
+ { keys: ['Ctrl', 'Z'], description: 'Undo' },
+ { keys: ['Ctrl', 'Shift', 'Z'], description: 'Redo' },
+ { keys: ['Ctrl', 'X'], description: 'Cut' },
+ { keys: ['Ctrl', 'C'], description: 'Copy' },
+ { keys: ['Ctrl', 'V'], description: 'Paste' },
+ { keys: ['Delete'], description: 'Delete Selection' },
+ { keys: ['Ctrl', 'D'], description: 'Duplicate' },
+ { keys: ['Ctrl', 'A'], description: 'Select All' },
+ ],
+ },
+ {
+ name: 'View',
+ shortcuts: [
+ { keys: ['Ctrl', '+'], description: 'Zoom In' },
+ { keys: ['Ctrl', '-'], description: 'Zoom Out' },
+ { keys: ['Ctrl', '0'], description: 'Fit to View' },
+ ],
+ },
+ {
+ name: 'File',
+ shortcuts: [
+ { keys: ['Ctrl', 'S'], description: 'Save Project' },
+ { keys: ['Ctrl', 'K'], description: 'Open Command Palette' },
+ ],
+ },
+];
+
+function KeyboardKey({ keyName }: { keyName: string }) {
+ return (
+
+ {keyName}
+
+ );
+}
+
+export function KeyboardShortcutsDialog({ open, onClose }: KeyboardShortcutsDialogProps) {
+ if (!open) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
Keyboard Shortcuts
+
+
+
+
+
+
+ {/* Shortcuts Grid */}
+
+ {SHORTCUTS.map((category) => (
+
+
+ {category.name}
+
+
+ {category.shortcuts.map((shortcut, index) => (
+
+
+ {shortcut.description}
+
+
+ {shortcut.keys.map((key, keyIndex) => (
+
+ {keyIndex > 0 && (
+ +
+ )}
+
+
+ ))}
+
+
+ ))}
+
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ Press + to open the
+ command palette and search for more actions
+
+
+
+ {/* Close Button */}
+
+
+ Close
+
+
+
+
+ );
+}
diff --git a/components/dialogs/MemoryWarningDialog.tsx b/components/dialogs/MemoryWarningDialog.tsx
new file mode 100644
index 0000000..56eba8d
--- /dev/null
+++ b/components/dialogs/MemoryWarningDialog.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import * as React from 'react';
+import { AlertTriangle, Info, X } from 'lucide-react';
+import { Modal } from '@/components/ui/Modal';
+import { Button } from '@/components/ui/Button';
+import { formatMemorySize } from '@/lib/utils/memory-limits';
+
+interface MemoryWarningDialogProps {
+ open: boolean;
+ estimatedMemoryMB: number;
+ availableMemoryMB?: number;
+ warning: string;
+ fileName?: string;
+ onContinue: () => void;
+ onCancel: () => void;
+}
+
+export function MemoryWarningDialog({
+ open,
+ estimatedMemoryMB,
+ availableMemoryMB,
+ warning,
+ fileName,
+ onContinue,
+ onCancel,
+}: MemoryWarningDialogProps) {
+ if (!open) return null;
+
+ const estimatedBytes = estimatedMemoryMB * 1024 * 1024;
+ const availableBytes = availableMemoryMB ? availableMemoryMB * 1024 * 1024 : undefined;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {warning}
+
+
+
+ {/* File Info */}
+ {fileName && (
+
+
+ File:
+ {fileName}
+
+ )}
+
+ {/* Memory Details */}
+
+
+ Estimated Memory:
+ {formatMemorySize(estimatedBytes)}
+
+ {availableBytes && (
+
+ Available Memory:
+ {formatMemorySize(availableBytes)}
+
+ )}
+
+
+ {/* Warning Message */}
+
+
+ Note: Loading large files may cause performance issues or browser crashes,
+ especially on devices with limited memory. Consider:
+
+
+ Closing other browser tabs
+ Using a shorter audio file
+ Splitting large files into smaller segments
+
+
+
+ {/* Actions */}
+
+
+ Cancel
+
+
+ Continue Anyway
+
+
+
+
+
+ );
+}
diff --git a/components/dialogs/UnsupportedFormatDialog.tsx b/components/dialogs/UnsupportedFormatDialog.tsx
new file mode 100644
index 0000000..8eaaf0f
--- /dev/null
+++ b/components/dialogs/UnsupportedFormatDialog.tsx
@@ -0,0 +1,106 @@
+'use client';
+
+import * as React from 'react';
+import { AlertCircle, FileQuestion, X } from 'lucide-react';
+import { Modal } from '@/components/ui/Modal';
+import { Button } from '@/components/ui/Button';
+
+export interface UnsupportedFormatDialogProps {
+ open: boolean;
+ fileName: string;
+ fileType: string;
+ onClose: () => void;
+}
+
+const SUPPORTED_FORMATS = [
+ { extension: 'WAV', mimeType: 'audio/wav', description: 'Lossless, widely supported' },
+ { extension: 'MP3', mimeType: 'audio/mpeg', description: 'Compressed, universal support' },
+ { extension: 'OGG', mimeType: 'audio/ogg', description: 'Free, open format' },
+ { extension: 'FLAC', mimeType: 'audio/flac', description: 'Lossless compression' },
+ { extension: 'M4A/AAC', mimeType: 'audio/aac', description: 'Apple audio format' },
+ { extension: 'AIFF', mimeType: 'audio/aiff', description: 'Apple lossless format' },
+ { extension: 'WebM', mimeType: 'audio/webm', description: 'Modern web format' },
+];
+
+export function UnsupportedFormatDialog({
+ open,
+ fileName,
+ fileType,
+ onClose,
+}: UnsupportedFormatDialogProps) {
+ if (!open) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
Unsupported File Format
+
+
+
+
+
+
+ {/* Error Message */}
+
+
+
+
+
+ Cannot open this file
+
+
+ {fileName}
+ {fileType && (
+ ({fileType})
+ )}
+
+
+
+
+
+ {/* Supported Formats */}
+
+
Supported Audio Formats:
+
+ {SUPPORTED_FORMATS.map((format) => (
+
+
+
+ {format.extension}
+
+
+ {format.description}
+
+
+
+ ))}
+
+
+
+ {/* Recommendations */}
+
+
How to fix this:
+
+ Convert your audio file to a supported format (WAV or MP3 recommended)
+ Use a free audio converter like Audacity, FFmpeg, or online converters
+ Check that the file isn't corrupted or incomplete
+
+
+
+ {/* Close Button */}
+
+
+ Got it
+
+
+
+
+ );
+}
diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx
index 60dd86d..729b21b 100644
--- a/components/editor/AudioEditor.tsx
+++ b/components/editor/AudioEditor.tsx
@@ -4,16 +4,9 @@ import * as React from 'react';
import { Music, Plus, Upload, Trash2, Settings, Download, FolderOpen } from 'lucide-react';
import { PlaybackControls } from './PlaybackControls';
import { MasterControls } from '@/components/controls/MasterControls';
-import { FrequencyAnalyzer } from '@/components/analysis/FrequencyAnalyzer';
-import { Spectrogram } from '@/components/analysis/Spectrogram';
-import { PhaseCorrelationMeter } from '@/components/analysis/PhaseCorrelationMeter';
-import { LUFSMeter } from '@/components/analysis/LUFSMeter';
-import { AudioStatistics } from '@/components/analysis/AudioStatistics';
import { ThemeToggle } from '@/components/layout/ThemeToggle';
import { CommandPalette } from '@/components/ui/CommandPalette';
-import { GlobalSettingsDialog } from '@/components/settings/GlobalSettingsDialog';
-import { ExportDialog, type ExportSettings } from '@/components/dialogs/ExportDialog';
-import { ProjectsDialog } from '@/components/dialogs/ProjectsDialog';
+import { BrowserCompatDialog } from '@/components/dialogs/BrowserCompatDialog';
import { Button } from '@/components/ui/Button';
import type { CommandAction } from '@/components/ui/CommandPalette';
import { useMultiTrack } from '@/lib/hooks/useMultiTrack';
@@ -21,7 +14,21 @@ import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer';
import { useEffectChain } from '@/lib/hooks/useEffectChain';
import { useToast } from '@/components/ui/Toast';
import { TrackList } from '@/components/tracks/TrackList';
-import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
+import type { ExportSettings } from '@/components/dialogs/ExportDialog';
+
+// Lazy load dialogs for better performance
+const GlobalSettingsDialog = React.lazy(() => import('@/components/settings/GlobalSettingsDialog').then(m => ({ default: m.GlobalSettingsDialog })));
+const ExportDialog = React.lazy(() => import('@/components/dialogs/ExportDialog').then(m => ({ default: m.ExportDialog })));
+const ProjectsDialog = React.lazy(() => import('@/components/dialogs/ProjectsDialog').then(m => ({ default: m.ProjectsDialog })));
+const ImportTrackDialog = React.lazy(() => import('@/components/tracks/ImportTrackDialog').then(m => ({ default: m.ImportTrackDialog })));
+const KeyboardShortcutsDialog = React.lazy(() => import('@/components/dialogs/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })));
+
+// Lazy load analysis components (shown conditionally based on analyzerView)
+const FrequencyAnalyzer = React.lazy(() => import('@/components/analysis/FrequencyAnalyzer').then(m => ({ default: m.FrequencyAnalyzer })));
+const Spectrogram = React.lazy(() => import('@/components/analysis/Spectrogram').then(m => ({ default: m.Spectrogram })));
+const PhaseCorrelationMeter = React.lazy(() => import('@/components/analysis/PhaseCorrelationMeter').then(m => ({ default: m.PhaseCorrelationMeter })));
+const LUFSMeter = React.lazy(() => import('@/components/analysis/LUFSMeter').then(m => ({ default: m.LUFSMeter })));
+const AudioStatistics = React.lazy(() => import('@/components/analysis/AudioStatistics').then(m => ({ default: m.AudioStatistics })));
import { formatDuration } from '@/lib/audio/decoder';
import { useHistory } from '@/lib/hooks/useHistory';
import { useRecording } from '@/lib/hooks/useRecording';
@@ -48,6 +55,7 @@ import {
type ProjectMetadata,
} from '@/lib/storage/projects';
import { getAudioContext } from '@/lib/audio/context';
+import { checkBrowserCompatibility } from '@/lib/utils/browser-compat';
export function AudioEditor() {
// Settings hook
@@ -66,6 +74,7 @@ export function AudioEditor() {
const [masterVolume, setMasterVolume] = React.useState(0.8);
const [masterPan, setMasterPan] = React.useState(0);
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
+ const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
const [clipboard, setClipboard] = React.useState
(null);
const [recordingTrackId, setRecordingTrackId] = React.useState(null);
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
@@ -87,6 +96,12 @@ export function AudioEditor() {
const [projects, setProjects] = React.useState([]);
const [currentProjectId, setCurrentProjectId] = React.useState(null);
const [currentProjectName, setCurrentProjectName] = React.useState('Untitled Project');
+ const [browserCompatDialogOpen, setBrowserCompatDialogOpen] = React.useState(false);
+ const [shortcutsDialogOpen, setShortcutsDialogOpen] = React.useState(false);
+ const [browserCompatInfo, setBrowserCompatInfo] = React.useState<{
+ missingFeatures: string[];
+ warnings: string[];
+ }>({ missingFeatures: [], warnings: [] });
const { addToast } = useToast();
@@ -633,6 +648,9 @@ export function AudioEditor() {
);
setClipboard(extracted);
+ // Ensure the track is selected so paste works
+ setSelectedTrackId(track.id);
+
// Execute cut command
const command = createMultiTrackCutCommand(
track.id,
@@ -664,6 +682,9 @@ export function AudioEditor() {
);
setClipboard(extracted);
+ // Ensure the track is selected so paste works
+ setSelectedTrackId(track.id);
+
// Execute copy command (doesn't modify buffer, just for undo history)
const command = createMultiTrackCopyCommand(
track.id,
@@ -684,7 +705,25 @@ export function AudioEditor() {
}, [tracks, executeCommand, updateTrack, addToast]);
const handlePaste = React.useCallback(() => {
- if (!clipboard || !selectedTrackId) return;
+ if (!clipboard) {
+ addToast({
+ title: 'Nothing to Paste',
+ description: 'Clipboard is empty. Copy or cut a selection first.',
+ variant: 'error',
+ duration: 2000,
+ });
+ return;
+ }
+
+ if (!selectedTrackId) {
+ addToast({
+ title: 'No Track Selected',
+ description: 'Select a track to paste into.',
+ variant: 'error',
+ duration: 2000,
+ });
+ return;
+ }
const track = tracks.find((t) => t.id === selectedTrackId);
if (!track) return;
@@ -1056,6 +1095,16 @@ export function AudioEditor() {
}
}, [loadTracks, addToast]);
+ // Check browser compatibility on mount
+ React.useEffect(() => {
+ const { isSupported, missingFeatures, warnings } = checkBrowserCompatibility();
+
+ if (!isSupported || warnings.length > 0) {
+ setBrowserCompatInfo({ missingFeatures, warnings });
+ setBrowserCompatDialogOpen(true);
+ }
+ }, []);
+
// Auto-load last project on mount
const [hasAutoLoaded, setHasAutoLoaded] = React.useState(false);
React.useEffect(() => {
@@ -1266,6 +1315,80 @@ export function AudioEditor() {
category: 'playback',
action: stop,
},
+ // Edit
+ {
+ 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+Shift+Z',
+ category: 'edit',
+ action: redo,
+ },
+ {
+ 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 content',
+ shortcut: 'Ctrl+V',
+ category: 'edit',
+ action: handlePaste,
+ },
+ {
+ id: 'delete',
+ label: 'Delete',
+ description: 'Delete selection',
+ shortcut: 'Delete',
+ category: 'edit',
+ action: handleDelete,
+ },
+ {
+ id: 'duplicate',
+ label: 'Duplicate',
+ description: 'Duplicate selection',
+ shortcut: 'Ctrl+D',
+ category: 'edit',
+ action: handleDuplicate,
+ },
+ {
+ id: 'select-all',
+ label: 'Select All',
+ description: 'Select all content on current track',
+ shortcut: 'Ctrl+A',
+ category: 'edit',
+ action: () => {
+ if (selectedTrackId) {
+ const track = tracks.find(t => t.id === selectedTrackId);
+ if (track?.audioBuffer) {
+ updateTrack(selectedTrackId, {
+ selection: { start: 0, end: track.audioBuffer.duration }
+ });
+ }
+ }
+ },
+ },
// Project
{
id: 'save-project',
@@ -1287,6 +1410,7 @@ export function AudioEditor() {
id: 'zoom-in',
label: 'Zoom In',
description: 'Zoom in on waveforms',
+ shortcut: 'Ctrl++',
category: 'view',
action: handleZoomIn,
},
@@ -1294,6 +1418,7 @@ export function AudioEditor() {
id: 'zoom-out',
label: 'Zoom Out',
description: 'Zoom out on waveforms',
+ shortcut: 'Ctrl+-',
category: 'view',
action: handleZoomOut,
},
@@ -1301,9 +1426,59 @@ export function AudioEditor() {
id: 'fit-to-view',
label: 'Fit to View',
description: 'Reset zoom to fit all tracks',
+ shortcut: 'Ctrl+0',
category: 'view',
action: handleFitToView,
},
+ // Navigation
+ {
+ id: 'seek-start',
+ label: 'Go to Start',
+ description: 'Seek to beginning',
+ shortcut: 'Home',
+ category: 'playback',
+ action: () => seek(0),
+ },
+ {
+ id: 'seek-end',
+ label: 'Go to End',
+ description: 'Seek to end',
+ shortcut: 'End',
+ category: 'playback',
+ action: () => seek(duration),
+ },
+ {
+ id: 'seek-backward',
+ label: 'Seek Backward',
+ description: 'Seek backward 1 second',
+ shortcut: '←',
+ category: 'playback',
+ action: () => seek(Math.max(0, currentTime - 1)),
+ },
+ {
+ id: 'seek-forward',
+ label: 'Seek Forward',
+ description: 'Seek forward 1 second',
+ shortcut: '→',
+ category: 'playback',
+ action: () => seek(Math.min(duration, currentTime + 1)),
+ },
+ {
+ id: 'seek-backward-5s',
+ label: 'Seek Backward 5s',
+ description: 'Seek backward 5 seconds',
+ shortcut: 'Ctrl+←',
+ category: 'playback',
+ action: () => seek(Math.max(0, currentTime - 5)),
+ },
+ {
+ id: 'seek-forward-5s',
+ label: 'Seek Forward 5s',
+ description: 'Seek forward 5 seconds',
+ shortcut: 'Ctrl+→',
+ category: 'playback',
+ action: () => seek(Math.min(duration, currentTime + 5)),
+ },
// Tracks
{
id: 'add-track',
@@ -1326,9 +1501,18 @@ export function AudioEditor() {
category: 'tracks',
action: handleClearTracks,
},
+ // Help
+ {
+ id: 'keyboard-shortcuts',
+ label: 'Keyboard Shortcuts',
+ description: 'View all keyboard shortcuts',
+ shortcut: '?',
+ category: 'view',
+ action: () => setShortcutsDialogOpen(true),
+ },
];
return actions;
- }, [play, pause, stop, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
+ }, [play, pause, stop, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, setShortcutsDialogOpen]);
// Keyboard shortcuts
React.useEffect(() => {
@@ -1398,6 +1582,27 @@ export function AudioEditor() {
return;
}
+ // Ctrl/Cmd++: Zoom In (also accepts Ctrl+=)
+ if ((e.ctrlKey || e.metaKey) && (e.key === '=' || e.key === '+')) {
+ e.preventDefault();
+ handleZoomIn();
+ return;
+ }
+
+ // Ctrl/Cmd+-: Zoom Out
+ if ((e.ctrlKey || e.metaKey) && e.key === '-') {
+ e.preventDefault();
+ handleZoomOut();
+ return;
+ }
+
+ // Ctrl/Cmd+0: Fit to View
+ if ((e.ctrlKey || e.metaKey) && e.key === '0') {
+ e.preventDefault();
+ handleFitToView();
+ return;
+ }
+
// Delete or Backspace: Delete selection
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
@@ -1457,7 +1662,16 @@ export function AudioEditor() {
// Ctrl+A: Select All (select all content on current track)
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault();
- if (selectedTrackId) {
+ if (!selectedTrackId) {
+ // If no track selected, try to select the first track with audio
+ const trackWithAudio = tracks.find(t => t.audioBuffer);
+ if (trackWithAudio && trackWithAudio.audioBuffer) {
+ setSelectedTrackId(trackWithAudio.id);
+ updateTrack(trackWithAudio.id, {
+ selection: { start: 0, end: trackWithAudio.audioBuffer.duration }
+ });
+ }
+ } else {
const track = tracks.find(t => t.id === selectedTrackId);
if (track?.audioBuffer) {
updateTrack(selectedTrackId, {
@@ -1468,45 +1682,31 @@ export function AudioEditor() {
return;
}
- // Ctrl+Plus/Equals: Zoom in
- if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '=')) {
+ // ?: Open keyboard shortcuts help
+ if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
- handleZoomIn();
- return;
- }
-
- // Ctrl+Minus: Zoom out
- if ((e.ctrlKey || e.metaKey) && e.key === '-') {
- e.preventDefault();
- handleZoomOut();
- return;
- }
-
- // Ctrl+0: Fit to view
- if ((e.ctrlKey || e.metaKey) && e.key === '0') {
- e.preventDefault();
- handleFitToView();
+ setShortcutsDialogOpen(true);
return;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
- }, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView]);
+ }, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView, setSelectedTrackId, setShortcutsDialogOpen]);
return (
<>
{/* Compact Header */}
-
+
{/* Left: Logo */}
-
+
-
Audio UI
+ Audio UI
{/* Project Name */}
-
+
- {/* Track Actions */}
-
-
+ {/* Track Actions - Compact on mobile */}
+
+
Projects
+
+
+
+
addTrack()}>
-
- Add Track
+
+ Add Track
-
- Import
+
+ Import
{tracks.length > 0 && (
<>
-
setExportDialogOpen(true)}>
+ setExportDialogOpen(true)} className="hidden md:flex">
Export
-
+
Clear All
@@ -1591,8 +1795,8 @@ export function AudioEditor() {
- {/* Right Sidebar - Master Controls & Analyzers */}
- }>
+
+
+ )}
+ {analyzerView === 'lufs' && (
+
Loading... }>
+
+
+ )}
+ {analyzerView === 'stats' && (
+
Loading... }>
+
+
+ )}