feat: complete Phase 9.3 - automation recording with write/touch/latch modes
Implemented comprehensive automation recording system for volume, pan, and effect parameters: - Added automation recording modes: - Write: Records continuously during playback when values change - Touch: Records only while control is being touched/moved - Latch: Records from first touch until playback stops - Implemented value change detection (0.001 threshold) to prevent infinite loops - Fixed React setState-in-render errors by: - Using queueMicrotask() to defer state updates - Moving lane creation logic to useEffect - Properly memoizing touch handlers with useMemo - Added proper value ranges for effect parameters: - Frequency: 20-20000 Hz - Q: 0.1-20 - Gain: -40-40 dB - Enhanced automation lane auto-creation with parameter-specific ranges - Added touch callbacks to all parameter controls (volume, pan, effects) - Implemented throttling (100ms) to avoid excessive automation points Technical improvements: - Used tracksRef and onRecordAutomationRef to ensure latest values in animation loops - Added proper cleanup on playback stop - Optimized recording to only trigger when values actually change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
149
components/dialogs/ExportDialog.tsx
Normal file
149
components/dialogs/ExportDialog.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { X, Download } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
export interface ExportSettings {
|
||||
format: 'wav';
|
||||
bitDepth: 16 | 24 | 32;
|
||||
normalize: boolean;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface ExportDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onExport: (settings: ExportSettings) => void;
|
||||
isExporting?: boolean;
|
||||
}
|
||||
|
||||
export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDialogProps) {
|
||||
const [settings, setSettings] = React.useState<ExportSettings>({
|
||||
format: 'wav',
|
||||
bitDepth: 16,
|
||||
normalize: true,
|
||||
filename: 'mix',
|
||||
});
|
||||
|
||||
const handleExport = () => {
|
||||
onExport(settings);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-foreground">Export Audio</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<div className="space-y-4">
|
||||
{/* Filename */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Filename
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.filename}
|
||||
onChange={(e) => setSettings({ ...settings, filename: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
disabled={isExporting}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">.wav will be added automatically</p>
|
||||
</div>
|
||||
|
||||
{/* Format */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Format
|
||||
</label>
|
||||
<select
|
||||
value={settings.format}
|
||||
onChange={(e) => setSettings({ ...settings, format: e.target.value as 'wav' })}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<option value="wav">WAV (Uncompressed)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Bit Depth */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Bit Depth
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[16, 24, 32].map((depth) => (
|
||||
<button
|
||||
key={depth}
|
||||
onClick={() => setSettings({ ...settings, bitDepth: depth as 16 | 24 | 32 })}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
|
||||
settings.bitDepth === depth
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-background border border-border text-foreground hover:bg-accent'
|
||||
)}
|
||||
disabled={isExporting}
|
||||
>
|
||||
{depth}-bit {depth === 32 && '(Float)'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Normalize */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.normalize}
|
||||
onChange={(e) => setSettings({ ...settings, normalize: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-border text-primary focus:ring-primary"
|
||||
disabled={isExporting}
|
||||
/>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Normalize audio
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-6">
|
||||
Prevents clipping by adjusting peak levels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="flex-1"
|
||||
disabled={isExporting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
className="flex-1"
|
||||
disabled={isExporting || !settings.filename.trim()}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{isExporting ? 'Exporting...' : 'Export'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user