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:
2025-11-18 23:29:18 +01:00
parent a1f230a6e6
commit c54d5089c5
13 changed files with 1040 additions and 70 deletions

View 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>
);
}