diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx index 132eb2b..2ce13aa 100644 --- a/components/tracks/Track.tsx +++ b/components/tracks/Track.tsx @@ -816,90 +816,101 @@ export function Track({ // Render only waveform if (renderWaveformOnly) { return ( -
- {/* Inner container with dynamic width */} + <>
1 - ? `${duration * zoom * 100}px` - : "100%", - }} + className={cn( + "relative bg-waveform-bg border-b transition-all duration-200", + isSelected && "bg-primary/5", + )} + style={{ height: trackHeight }} > - {/* Delete Button - Top Right Overlay */} - + {/* Delete Button - Top Right Overlay */} + - {track.audioBuffer ? ( - <> - {/* Waveform Canvas */} - - - ) : ( - !track.collapsed && ( + {track.audioBuffer ? ( <> -
{ - e.stopPropagation(); - handleLoadAudioClick(); - }} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - -

- {isDragging - ? "Drop audio file here" - : "Click to load audio file"} -

-

or drag & drop

-
- - ) - )} -
{" "} - {/* Close inner container with minWidth */} -
+ ) : ( + !track.collapsed && ( + <> +
{ + e.stopPropagation(); + handleLoadAudioClick(); + }} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > + +

+ {isDragging + ? "Drop audio file here" + : "Click to load audio file"} +

+

or drag & drop

+
+ + + ) + )} + + + + {/* Import Dialog */} + + ); } diff --git a/components/tracks/TrackExtensions.tsx b/components/tracks/TrackExtensions.tsx new file mode 100644 index 0000000..e33dfca --- /dev/null +++ b/components/tracks/TrackExtensions.tsx @@ -0,0 +1,128 @@ +'use client'; + +import * as React from 'react'; +import { Plus, ChevronDown, ChevronRight } from 'lucide-react'; +import type { Track as TrackType } from '@/types/track'; +import { Button } from '@/components/ui/Button'; +import { cn } from '@/lib/utils/cn'; +import { EffectDevice } from '@/components/effects/EffectDevice'; +import { EffectBrowser } from '@/components/effects/EffectBrowser'; +import type { EffectType } from '@/lib/audio/effects/chain'; + +export interface TrackExtensionsProps { + track: TrackType; + onUpdateTrack: (trackId: string, updates: Partial) => void; + onToggleEffect?: (effectId: string) => void; + onRemoveEffect?: (effectId: string) => void; + onUpdateEffect?: (effectId: string, parameters: any) => void; + onAddEffect?: (effectType: EffectType) => void; +} + +export function TrackExtensions({ + track, + onUpdateTrack, + onToggleEffect, + onRemoveEffect, + onUpdateEffect, + onAddEffect, +}: TrackExtensionsProps) { + const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false); + + // Don't render if track is collapsed + if (track.collapsed) { + return null; + } + + return ( + <> + {/* Effects Section (Collapsible, Full Width) */} +
+ {/* Effects Header - clickable to toggle */} +
{ + onUpdateTrack(track.id, { + showEffects: !track.showEffects, + }); + }} + > + {track.showEffects ? ( + + ) : ( + + )} + + {/* Show mini effect chain when collapsed */} + {!track.showEffects && track.effectChain.effects.length > 0 ? ( +
+ {track.effectChain.effects.map((effect) => ( +
+ {effect.name} +
+ ))} +
+ ) : ( + + Devices ({track.effectChain.effects.length}) + + )} + + +
+ + {/* Horizontal scrolling device rack - expanded state */} + {track.showEffects && ( +
+
+ {track.effectChain.effects.length === 0 ? ( +
+ No devices. Click + to add an effect. +
+ ) : ( + track.effectChain.effects.map((effect) => ( + onToggleEffect?.(effect.id)} + onRemove={() => onRemoveEffect?.(effect.id)} + onUpdateParameters={(params) => onUpdateEffect?.(effect.id, params)} + /> + )) + )} +
+
+ )} +
+ + {/* Effect Browser Dialog */} + setEffectBrowserOpen(false)} + onSelectEffect={(effectType) => { + if (onAddEffect) { + onAddEffect(effectType); + } + }} + /> + + ); +} diff --git a/components/tracks/TrackList.tsx b/components/tracks/TrackList.tsx index 93d4ce2..77ef944 100644 --- a/components/tracks/TrackList.tsx +++ b/components/tracks/TrackList.tsx @@ -4,9 +4,13 @@ import * as React from 'react'; import { Plus, Upload } 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'; export interface TrackListProps { tracks: TrackType[]; @@ -102,91 +106,103 @@ export function TrackList({ {/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */}
{tracks.map((track) => ( - 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} - /> + + {/* 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} + /> + + {/* Spacer for Automation Lane */} + {!track.collapsed && track.automation?.showAutomation && ( +
+ )} + + {/* Spacer for Effects Section */} + {!track.collapsed && ( +
+ )} + ))}
@@ -196,93 +212,203 @@ export function TrackList({ onScroll={handleWaveformScroll} className="flex-1 overflow-auto custom-scrollbar" > - {tracks.map((track) => ( - 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} - /> - ))} +
+ {tracks.map((track) => ( + + {/* Track Waveform Row */} + 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} + /> + + {/* Automation Lane Section */} + {!track.collapsed && track.automation?.showAutomation && ( +
+ {track.automation.lanes + .filter((lane) => lane.parameterId === track.automation.selectedParameterId) + .map((lane) => ( + { + const newPoint = createAutomationPoint(time, value); + 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 }, + }); + }} + onSeek={onSeek} + /> + ))} +
+ )} + + {/* Effects Section */} + { + 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 }); + }} + /> +
+ ))} +