feat: complete Phase 7.4 - real-time track effects system

Implemented comprehensive real-time effect processing for multi-track audio:

Core Features:
- Per-track effect chains with drag-and-drop reordering
- Effect bypass/enable toggle per effect
- Real-time parameter updates (filters, dynamics, time-based, distortion, bitcrusher, pitch, timestretch)
- Add/remove effects during playback without interruption
- Effect chain persistence via localStorage
- Automatic playback stop when tracks are deleted

Technical Implementation:
- Effect processor with dry/wet routing for bypass functionality
- Real-time effect parameter updates using AudioParam setValueAtTime
- Structure change detection for add/remove/reorder operations
- Stale closure fix using refs for latest track state
- ScriptProcessorNode for bitcrusher, pitch shifter, and time stretch
- Dual-tap delay line for pitch shifting
- Overlap-add synthesis for time stretching

UI Components:
- EffectBrowser dialog with categorized effects
- EffectDevice component with parameter controls
- EffectParameters for all 19 real-time effect types
- Device rack with horizontal scrolling (Ableton-style)

Removed offline-only effects (normalize, fadeIn, fadeOut, reverse) as they don't fit the real-time processing model.

Completed all items in Phase 7.4:
- [x] Per-track effect chain
- [x] Effect rack UI
- [x] Effect bypass per track
- [x] Real-time effect processing during playback
- [x] Add/remove effects during playback
- [x] Real-time parameter updates
- [x] Effect chain persistence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 12:08:33 +01:00
parent cbcd38b1ed
commit beb7085c89
11 changed files with 2180 additions and 100 deletions

View File

@@ -25,12 +25,12 @@ export function useMultiTrack() {
return [];
}
// Note: AudioBuffers and EffectChains can't be serialized, so we only restore track metadata
// Note: AudioBuffers can't be serialized, but EffectChains can
return parsed.map((t: any) => ({
...t,
name: String(t.name || 'Untitled Track'), // Ensure name is always a string
audioBuffer: null, // Will need to be reloaded
effectChain: createEffectChain(`${t.name} Effects`), // Recreate effect chain
effectChain: t.effectChain || createEffectChain(`${t.name} Effects`), // Restore effect chain or create new
}));
}
} catch (error) {
@@ -47,7 +47,7 @@ export function useMultiTrack() {
if (typeof window === 'undefined') return;
try {
// Only save serializable fields, excluding audioBuffer, effectChain, and any DOM references
// Only save serializable fields, excluding audioBuffer and any DOM references
const trackData = tracks.map((track) => ({
id: track.id,
name: String(track.name || 'Untitled Track'),
@@ -60,7 +60,7 @@ export function useMultiTrack() {
recordEnabled: track.recordEnabled,
collapsed: track.collapsed,
selected: track.selected,
// Note: effectChain is excluded - will be recreated on load
effectChain: track.effectChain, // Save effect chain
}));
localStorage.setItem(STORAGE_KEY, JSON.stringify(trackData));
} catch (error) {