Files
audio-ui/lib/hooks/useMultiTrack.ts
Sebastian Krüger 9b1eedc379 feat: Ableton Live-style effects and complete automation system
Enhanced visual design:
- Improved device rack container with darker background and inner shadow
- Device cards now have rounded corners, shadows, and colored indicators
- Better visual separation between enabled/disabled effects
- Active devices highlighted with accent border

Complete automation infrastructure (Phase 9):
- Created comprehensive type system for automation lanes and points
- Implemented AutomationPoint component with drag-and-drop editing
- Implemented AutomationHeader with mode controls (Read/Write/Touch/Latch)
- Implemented AutomationLane with canvas-based curve rendering
- Integrated automation lanes into Track component below effects
- Created automation playback engine with real-time interpolation
- Added automation data persistence to localStorage

Automation features:
- Add/remove automation points by clicking/double-clicking
- Drag points to change time and value
- Multiple automation modes (Read, Write, Touch, Latch)
- Linear and step curve types (bezier planned)
- Adjustable lane height (60-180px)
- Show/hide automation per lane
- Real-time value display at playhead
- Color-coded lanes by parameter type
- Keyboard delete support (Delete/Backspace)

Track type updates:
- Added automation field to Track interface
- Updated track creation to initialize empty automation
- Updated localStorage save/load to include automation data

Files created:
- components/automation/AutomationPoint.tsx
- components/automation/AutomationHeader.tsx
- components/automation/AutomationLane.tsx
- lib/audio/automation/utils.ts (helper functions)
- lib/audio/automation/playback.ts (playback engine)
- types/automation.ts (complete type system)

Files modified:
- components/effects/EffectDevice.tsx (Ableton-style visual improvements)
- components/tracks/Track.tsx (automation lanes integration)
- types/track.ts (automation field added)
- lib/audio/track-utils.ts (automation initialization)
- lib/hooks/useMultiTrack.ts (automation persistence)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:30:01 +01:00

132 lines
4.2 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import type { Track } from '@/types/track';
import { createTrack, createTrackFromBuffer } from '@/lib/audio/track-utils';
import { createEffectChain } from '@/lib/audio/effects/chain';
import { DEFAULT_TRACK_HEIGHT } from '@/types/track';
const STORAGE_KEY = 'audio-ui-multi-track';
export function useMultiTrack() {
const [tracks, setTracks] = useState<Track[]>(() => {
if (typeof window === 'undefined') return [];
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
// Clear corrupted data immediately if we detect issues
const hasInvalidData = parsed.some((t: any) =>
typeof t.name !== 'string' || t.name === '[object Object]'
);
if (hasInvalidData) {
console.warn('Detected corrupted track data in localStorage, clearing...');
localStorage.removeItem(STORAGE_KEY);
return [];
}
// Note: AudioBuffers can't be serialized, but EffectChains and Automation can
return parsed.map((t: any) => ({
...t,
name: String(t.name || 'Untitled Track'), // Ensure name is always a string
height: t.height && t.height >= DEFAULT_TRACK_HEIGHT ? t.height : DEFAULT_TRACK_HEIGHT, // Migrate old heights
audioBuffer: null, // Will need to be reloaded
effectChain: t.effectChain || createEffectChain(`${t.name} Effects`), // Restore effect chain or create new
automation: t.automation || { lanes: [], showAutomation: false }, // Restore automation or create new
selection: t.selection || null, // Initialize selection
}));
}
} catch (error) {
console.error('Failed to load tracks from localStorage:', error);
// Clear corrupted data
localStorage.removeItem(STORAGE_KEY);
}
return [];
});
// Save tracks to localStorage (without audio buffers)
useEffect(() => {
if (typeof window === 'undefined') return;
try {
// Only save serializable fields, excluding audioBuffer and any DOM references
const trackData = tracks.map((track) => ({
id: track.id,
name: String(track.name || 'Untitled Track'),
color: track.color,
height: track.height,
volume: track.volume,
pan: track.pan,
mute: track.mute,
solo: track.solo,
recordEnabled: track.recordEnabled,
collapsed: track.collapsed,
selected: track.selected,
effectChain: track.effectChain, // Save effect chain
automation: track.automation, // Save automation data
}));
localStorage.setItem(STORAGE_KEY, JSON.stringify(trackData));
} catch (error) {
console.error('Failed to save tracks to localStorage:', error);
}
}, [tracks]);
const addTrack = useCallback((name?: string) => {
const track = createTrack(name);
setTracks((prev) => [...prev, track]);
return track;
}, []);
const addTrackFromBuffer = useCallback((buffer: AudioBuffer, name?: string) => {
const track = createTrackFromBuffer(buffer, name);
setTracks((prev) => [...prev, track]);
return track;
}, []);
const removeTrack = useCallback((trackId: string) => {
setTracks((prev) => prev.filter((t) => t.id !== trackId));
}, []);
const updateTrack = useCallback((trackId: string, updates: Partial<Track>) => {
setTracks((prev) =>
prev.map((track) =>
track.id === trackId ? { ...track, ...updates } : track
)
);
}, []);
const clearTracks = useCallback(() => {
setTracks([]);
}, []);
const reorderTracks = useCallback((fromIndex: number, toIndex: number) => {
setTracks((prev) => {
const newTracks = [...prev];
const [movedTrack] = newTracks.splice(fromIndex, 1);
newTracks.splice(toIndex, 0, movedTrack);
return newTracks;
});
}, []);
const setTrackBuffer = useCallback((trackId: string, buffer: AudioBuffer) => {
setTracks((prev) =>
prev.map((track) =>
track.id === trackId ? { ...track, audioBuffer: buffer } : track
)
);
}, []);
return {
tracks,
addTrack,
addTrackFromBuffer,
removeTrack,
updateTrack,
clearTracks,
reorderTracks,
setTrackBuffer,
};
}