feat: implement Phase 7.1-7.2 multi-track infrastructure
Added core multi-track support with track management and controls: **Track Types & Utilities:** - Track interface with audio buffer, controls (volume/pan/solo/mute) - Track utility functions for creation, mixing, and gain calculation - Track color system with 9 preset colors - Configurable track heights (60-300px) **Components:** - TrackHeader: Collapsible track controls with inline name editing - Solo/Mute buttons with visual feedback - Volume slider (0-100%) and Pan control (L-C-R) - Track color indicator and remove button - Track: Waveform display component with canvas rendering - Click-to-seek on waveform - Playhead visualization - Support for collapsed state - TrackList: Container managing multiple tracks - Scrollable track list with custom scrollbar - Add track button - Empty state UI **State Management:** - useMultiTrack hook with localStorage persistence - Add/remove/update/reorder track operations - Track buffer management Features implemented: - ✅ Track creation and removal - ✅ Track naming (editable) - ✅ Track colors - ✅ Solo/Mute per track - ✅ Volume fader per track (0-100%) - ✅ Pan control per track (L-C-R) - ✅ Track collapse/expand - ✅ Track height configuration - ✅ Waveform visualization per track - ✅ Multi-track audio mixing utilities Next: Integrate into AudioEditor and implement multi-track playback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
98
lib/hooks/useMultiTrack.ts
Normal file
98
lib/hooks/useMultiTrack.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import type { Track } from '@/types/track';
|
||||
import { createTrack, createTrackFromBuffer } from '@/lib/audio/track-utils';
|
||||
|
||||
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);
|
||||
// Note: AudioBuffers can't be serialized, so we only restore track metadata
|
||||
return parsed.map((t: any) => ({
|
||||
...t,
|
||||
audioBuffer: null, // Will need to be reloaded
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tracks from localStorage:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
// Save tracks to localStorage (without audio buffers)
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const trackData = tracks.map((track) => ({
|
||||
...track,
|
||||
audioBuffer: null, // Don't serialize audio buffers
|
||||
}));
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user