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>
132 lines
4.2 KiB
TypeScript
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,
|
|
};
|
|
}
|