'use client'; import * as React from 'react'; import { Plus, Upload, ChevronDown, ChevronRight, X } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Track } from './Track'; import { TrackExtensions } from './TrackExtensions'; import { ImportTrackDialog } from './ImportTrackDialog'; import type { Track as TrackType } from '@/types/track'; import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain'; import { AutomationLane } from '@/components/automation/AutomationLane'; import type { AutomationPoint as AutomationPointType } from '@/types/automation'; import { createAutomationPoint } from '@/lib/audio/automation/utils'; import { EffectDevice } from '@/components/effects/EffectDevice'; export interface TrackListProps { tracks: TrackType[]; zoom: number; currentTime: number; duration: number; selectedTrackId?: string | null; onSelectTrack?: (trackId: string | null) => void; onAddTrack: () => void; onImportTrack?: (buffer: AudioBuffer, name: string) => void; onRemoveTrack: (trackId: string) => void; onUpdateTrack: (trackId: string, updates: Partial) => void; onSeek?: (time: number) => void; onSelectionChange?: (trackId: string, selection: { start: number; end: number } | null) => void; onToggleRecordEnable?: (trackId: string) => void; recordingTrackId?: string | null; recordingLevel?: number; trackLevels?: Record; onParameterTouched?: (trackId: string, laneId: string, touched: boolean) => void; isPlaying?: boolean; } export function TrackList({ tracks, zoom, currentTime, duration, selectedTrackId, onSelectTrack, onAddTrack, onImportTrack, onRemoveTrack, onUpdateTrack, onSeek, onSelectionChange, onToggleRecordEnable, recordingTrackId, recordingLevel = 0, trackLevels = {}, onParameterTouched, isPlaying = false, }: TrackListProps) { const [importDialogOpen, setImportDialogOpen] = React.useState(false); const waveformScrollRef = React.useRef(null); const controlsScrollRef = React.useRef(null); // Synchronize vertical scroll between controls and waveforms const handleWaveformScroll = React.useCallback(() => { if (waveformScrollRef.current && controlsScrollRef.current) { controlsScrollRef.current.scrollTop = waveformScrollRef.current.scrollTop; } }, []); const handleImportTrack = (buffer: AudioBuffer, name: string) => { if (onImportTrack) { onImportTrack(buffer, name); } }; if (tracks.length === 0) { return ( <>

No tracks yet. Add a track to get started.

{onImportTrack && ( )}
{onImportTrack && ( setImportDialogOpen(false)} onImportTrack={handleImportTrack} /> )} ); } return (
{/* Track List - Two Column Layout */}
{/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */}
{tracks.map((track) => ( {/* Track Controls */} onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute }) } onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo }) } onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed }) } onVolumeChange={(volume) => onUpdateTrack(track.id, { volume }) } onPanChange={(pan) => onUpdateTrack(track.id, { pan }) } onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name }) } onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer }) } onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect( effectType, EFFECT_NAMES[effectType] ); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderControlsOnly={true} /> ))}
{/* Right Column: Waveforms (Flexible Width, Shared Horizontal & Vertical Scroll) */}
{tracks.map((track) => ( {/* Track Waveform Row with Overlays */}
onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute }) } onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo }) } onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed }) } onVolumeChange={(volume) => onUpdateTrack(track.id, { volume }) } onPanChange={(pan) => onUpdateTrack(track.id, { pan }) } onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name }) } onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer }) } onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect( effectType, EFFECT_NAMES[effectType] ); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderWaveformOnly={true} /> {/* Effects Bar - Always visible at bottom */} {!track.collapsed && (
{/* Effects Header - Collapsible */}
{ onUpdateTrack(track.id, { effectsExpanded: !track.effectsExpanded }); }} > {track.effectsExpanded ? ( ) : ( )} Effects ({track.effectChain.effects.length})
{/* Effects Content - Collapsible, no inner container */} {track.effectsExpanded && (
{track.effectChain.effects.length === 0 ? (
No effects. Click + to add an effect.
) : ( track.effectChain.effects.map((effect) => ( { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemove={() => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effect.id), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateParameters={(params) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, parameters: params } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onToggleExpanded={() => { const updatedEffects = track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, expanded: !e.expanded } : e ); onUpdateTrack(track.id, { effectChain: { ...track.effectChain, effects: updatedEffects }, }); }} /> )) )}
)}
)} {/* Automation Bar - Collapsible, above effects bar at bottom */} {!track.collapsed && (
{/* Automation Header - Clickable to toggle */}
{ const currentLane = track.automation.lanes.find( l => l.parameterId === track.automation.selectedParameterId ); if (currentLane) { const updatedLanes = track.automation.lanes.map((l) => l.id === currentLane.id ? { ...l, visible: !l.visible } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); } }} > {track.automation.lanes.find(l => l.parameterId === track.automation.selectedParameterId)?.visible ? ( ) : ( )} Automation {track.automation.selectedParameterId || 'Volume'}
{/* Automation Lane Content - Collapsible */} {track.automation.lanes .filter((lane) => lane.parameterId === track.automation.selectedParameterId && lane.visible) .map((lane) => (
{ const newPoint = createAutomationPoint({ time, value, curve: 'linear' }); const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdatePoint={(pointId, updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.map((p) => p.id === pointId ? { ...p, ...updates } : p ), } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onRemovePoint={(pointId) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.filter((p) => p.id !== pointId) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdateLane={(updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, ...updates } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} />
))}
)}
))}
{/* Import Dialog */} {onImportTrack && ( setImportDialogOpen(false)} onImportTrack={handleImportTrack} /> )}
); }