diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index c14c3dd..bf5c273 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -44,17 +44,17 @@ export function AudioEditor() { togglePlayPause, } = useMultiTrackPlayer(tracks, masterVolume); - // Effect chain (for selected track) + // Master effect chain const { - chain: effectChain, - presets: effectPresets, - toggleEffectEnabled, - removeEffect, - reorder: reorderEffects, - clearChain, - savePreset, - loadPresetToChain, - deletePreset, + chain: masterEffectChain, + presets: masterEffectPresets, + toggleEffectEnabled: toggleMasterEffect, + removeEffect: removeMasterEffect, + reorder: reorderMasterEffects, + clearChain: clearMasterChain, + savePreset: saveMasterPreset, + loadPresetToChain: loadMasterPreset, + deletePreset: deleteMasterPreset, } = useEffectChain(); // Multi-track handlers @@ -84,6 +84,48 @@ export function AudioEditor() { } }; + // Per-track effect chain handlers + const handleToggleTrackEffect = (effectId: string) => { + if (!selectedTrack) return; + const updatedChain = { + ...selectedTrack.effectChain, + effects: selectedTrack.effectChain.effects.map((e) => + e.id === effectId ? { ...e, enabled: !e.enabled } : e + ), + }; + updateTrack(selectedTrack.id, { effectChain: updatedChain }); + }; + + const handleRemoveTrackEffect = (effectId: string) => { + if (!selectedTrack) return; + const updatedChain = { + ...selectedTrack.effectChain, + effects: selectedTrack.effectChain.effects.filter((e) => e.id !== effectId), + }; + updateTrack(selectedTrack.id, { effectChain: updatedChain }); + }; + + const handleReorderTrackEffects = (fromIndex: number, toIndex: number) => { + if (!selectedTrack) return; + const effects = [...selectedTrack.effectChain.effects]; + const [removed] = effects.splice(fromIndex, 1); + effects.splice(toIndex, 0, removed); + const updatedChain = { + ...selectedTrack.effectChain, + effects, + }; + updateTrack(selectedTrack.id, { effectChain: updatedChain }); + }; + + const handleClearTrackChain = () => { + if (!selectedTrack) return; + const updatedChain = { + ...selectedTrack.effectChain, + effects: [], + }; + updateTrack(selectedTrack.id, { effectChain: updatedChain }); + }; + // Zoom controls const handleZoomIn = () => { setZoom((prev) => Math.min(20, prev + 1)); @@ -254,15 +296,20 @@ export function AudioEditor() { onUpdateTrack={updateTrack} onRemoveTrack={handleRemoveTrack} onClearTracks={handleClearTracks} - effectChain={effectChain} - effectPresets={effectPresets} - onToggleEffect={toggleEffectEnabled} - onRemoveEffect={removeEffect} - onReorderEffects={reorderEffects} - onSavePreset={savePreset} - onLoadPreset={loadPresetToChain} - onDeletePreset={deletePreset} - onClearChain={clearChain} + trackEffectChain={selectedTrack?.effectChain ?? null} + onToggleTrackEffect={handleToggleTrackEffect} + onRemoveTrackEffect={handleRemoveTrackEffect} + onReorderTrackEffects={handleReorderTrackEffects} + onClearTrackChain={handleClearTrackChain} + masterEffectChain={masterEffectChain} + masterEffectPresets={masterEffectPresets} + onToggleMasterEffect={toggleMasterEffect} + onRemoveMasterEffect={removeMasterEffect} + onReorderMasterEffects={reorderMasterEffects} + onSaveMasterPreset={saveMasterPreset} + onLoadMasterPreset={loadMasterPreset} + onDeleteMasterPreset={deleteMasterPreset} + onClearMasterChain={clearMasterChain} /> {/* Main canvas area */} diff --git a/components/layout/SidePanel.tsx b/components/layout/SidePanel.tsx index 9d3abd5..ec33161 100644 --- a/components/layout/SidePanel.tsx +++ b/components/layout/SidePanel.tsx @@ -28,16 +28,23 @@ export interface SidePanelProps { onRemoveTrack: (trackId: string) => void; onClearTracks: () => void; - // Effect chain - effectChain: EffectChain; - effectPresets: EffectPreset[]; - onToggleEffect: (effectId: string) => void; - onRemoveEffect: (effectId: string) => void; - onReorderEffects: (fromIndex: number, toIndex: number) => void; - onSavePreset: (preset: EffectPreset) => void; - onLoadPreset: (preset: EffectPreset) => void; - onDeletePreset: (presetId: string) => void; - onClearChain: () => void; + // Track effect chain (for selected track) + trackEffectChain: EffectChain | null; + onToggleTrackEffect: (effectId: string) => void; + onRemoveTrackEffect: (effectId: string) => void; + onReorderTrackEffects: (fromIndex: number, toIndex: number) => void; + onClearTrackChain: () => void; + + // Master effect chain + masterEffectChain: EffectChain; + masterEffectPresets: EffectPreset[]; + onToggleMasterEffect: (effectId: string) => void; + onRemoveMasterEffect: (effectId: string) => void; + onReorderMasterEffects: (fromIndex: number, toIndex: number) => void; + onSaveMasterPreset: (preset: EffectPreset) => void; + onLoadMasterPreset: (preset: EffectPreset) => void; + onDeleteMasterPreset: (presetId: string) => void; + onClearMasterChain: () => void; className?: string; } @@ -51,19 +58,24 @@ export function SidePanel({ onUpdateTrack, onRemoveTrack, onClearTracks, - effectChain, - effectPresets, - onToggleEffect, - onRemoveEffect, - onReorderEffects, - onSavePreset, - onLoadPreset, - onDeletePreset, - onClearChain, + trackEffectChain, + onToggleTrackEffect, + onRemoveTrackEffect, + onReorderTrackEffects, + onClearTrackChain, + masterEffectChain, + masterEffectPresets, + onToggleMasterEffect, + onRemoveMasterEffect, + onReorderMasterEffects, + onSaveMasterPreset, + onLoadMasterPreset, + onDeleteMasterPreset, + onClearMasterChain, className, }: SidePanelProps) { const [isCollapsed, setIsCollapsed] = React.useState(false); - const [activeTab, setActiveTab] = React.useState<'tracks' | 'chain'>('tracks'); + const [activeTab, setActiveTab] = React.useState<'tracks' | 'trackFx' | 'masterFx'>('tracks'); const [presetDialogOpen, setPresetDialogOpen] = React.useState(false); const selectedTrack = tracks.find((t) => t.id === selectedTrackId); @@ -102,13 +114,21 @@ export function SidePanel({ + - {effectChain.effects.length > 0 && ( + {trackEffectChain && trackEffectChain.effects.length > 0 && ( + {masterEffectChain.effects.length > 0 && ( + + )} + + + + + setPresetDialogOpen(false)} + currentChain={masterEffectChain} + presets={masterEffectPresets} + onSavePreset={onSaveMasterPreset} + onLoadPreset={onLoadMasterPreset} + onDeletePreset={onDeleteMasterPreset} + onExportPreset={() => {}} + onImportPreset={(preset) => onSaveMasterPreset(preset)} + /> + + )} ); diff --git a/lib/audio/track-utils.ts b/lib/audio/track-utils.ts index 012a54f..3a2af88 100644 --- a/lib/audio/track-utils.ts +++ b/lib/audio/track-utils.ts @@ -4,6 +4,7 @@ import type { Track, TrackColor } from '@/types/track'; import { DEFAULT_TRACK_HEIGHT, TRACK_COLORS } from '@/types/track'; +import { createEffectChain } from '@/lib/audio/effects/chain'; /** * Generate a unique track ID @@ -33,6 +34,7 @@ export function createTrack(name?: string, color?: TrackColor): Track { mute: false, solo: false, recordEnabled: false, + effectChain: createEffectChain(`${trackName} Effects`), collapsed: false, selected: false, }; diff --git a/lib/hooks/useMultiTrack.ts b/lib/hooks/useMultiTrack.ts index 2a19279..cdf6efa 100644 --- a/lib/hooks/useMultiTrack.ts +++ b/lib/hooks/useMultiTrack.ts @@ -1,6 +1,7 @@ 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'; const STORAGE_KEY = 'audio-ui-multi-track'; @@ -24,11 +25,12 @@ export function useMultiTrack() { return []; } - // Note: AudioBuffers can't be serialized, so we only restore track metadata + // Note: AudioBuffers and EffectChains can't be serialized, so we only restore track metadata 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 })); } } catch (error) { @@ -45,7 +47,7 @@ export function useMultiTrack() { if (typeof window === 'undefined') return; try { - // Only save serializable fields, excluding audioBuffer and any DOM references + // Only save serializable fields, excluding audioBuffer, effectChain, and any DOM references const trackData = tracks.map((track) => ({ id: track.id, name: String(track.name || 'Untitled Track'), @@ -58,6 +60,7 @@ export function useMultiTrack() { recordEnabled: track.recordEnabled, collapsed: track.collapsed, selected: track.selected, + // Note: effectChain is excluded - will be recreated on load })); localStorage.setItem(STORAGE_KEY, JSON.stringify(trackData)); } catch (error) { diff --git a/types/track.ts b/types/track.ts index f8f518e..6ee0c1e 100644 --- a/types/track.ts +++ b/types/track.ts @@ -2,6 +2,8 @@ * Multi-track types and interfaces */ +import type { EffectChain } from '@/lib/audio/effects/chain'; + export interface Track { id: string; name: string; @@ -16,6 +18,9 @@ export interface Track { solo: boolean; recordEnabled: boolean; + // Effects + effectChain: EffectChain; + // UI state collapsed: boolean; selected: boolean;