Files
audio-ui/components/tracks/Track.tsx
Sebastian Krüger de8a3ff187 feat: refactor to multi-track only editor with sidebar controls
Major refactor to simplify the editor and focus exclusively on multi-track editing:

**AudioEditor Changes:**
- Removed single-file waveform view and useAudioPlayer
- Removed all single-file editing operations (cut, copy, paste, trim)
- Simplified to multi-track only with track selection support
- Added selectedTrackId state for track-specific effect chain
- Reduced from ~1500 lines to ~290 lines

**SidePanel Changes:**
- Complete rewrite with 2 tabs: Tracks and Effect Chain
- Tracks tab shows all tracks with inline controls (volume, pan, solo, mute)
- Click tracks to select/deselect
- Effect Chain tab shows effects for selected track
- Removed old file/history/info/effects tabs
- Sidebar now wider (320px) to accommodate inline track controls

**TrackList/Track Changes:**
- Added track selection support (isSelected/onSelect props)
- Visual feedback with ring border when track is selected
- Click anywhere on track to select it

**Workflow:**
1. Import or add audio tracks
2. Select a track in the sidebar or main view
3. Apply effects to selected track via Effect Chain tab
4. Adjust track controls (volume, pan, solo, mute) in Tracks tab

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:17:09 +01:00

184 lines
4.7 KiB
TypeScript

'use client';
import * as React from 'react';
import type { Track as TrackType } from '@/types/track';
import { TrackHeader } from './TrackHeader';
import { cn } from '@/lib/utils/cn';
export interface TrackProps {
track: TrackType;
zoom: number;
currentTime: number;
duration: number;
isSelected?: boolean;
onSelect?: () => void;
onToggleMute: () => void;
onToggleSolo: () => void;
onToggleCollapse: () => void;
onVolumeChange: (volume: number) => void;
onPanChange: (pan: number) => void;
onRemove: () => void;
onNameChange: (name: string) => void;
onSeek?: (time: number) => void;
}
export function Track({
track,
zoom,
currentTime,
duration,
isSelected,
onSelect,
onToggleMute,
onToggleSolo,
onToggleCollapse,
onVolumeChange,
onPanChange,
onRemove,
onNameChange,
onSeek,
}: TrackProps) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
// Draw waveform
React.useEffect(() => {
if (!track.audioBuffer || !canvasRef.current || track.collapsed) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
// Clear canvas
ctx.fillStyle = 'rgb(15, 23, 42)';
ctx.fillRect(0, 0, width, height);
const buffer = track.audioBuffer;
const channelData = buffer.getChannelData(0);
const samplesPerPixel = Math.floor(buffer.length / (width * zoom));
// Draw waveform
ctx.fillStyle = track.color;
ctx.strokeStyle = track.color;
ctx.lineWidth = 1;
for (let x = 0; x < width; x++) {
const startSample = Math.floor(x * samplesPerPixel);
const endSample = Math.floor((x + 1) * samplesPerPixel);
let min = 1.0;
let max = -1.0;
for (let i = startSample; i < endSample && i < channelData.length; i++) {
const sample = channelData[i];
if (sample < min) min = sample;
if (sample > max) max = sample;
}
const y1 = (height / 2) * (1 - max);
const y2 = (height / 2) * (1 - min);
ctx.beginPath();
ctx.moveTo(x, y1);
ctx.lineTo(x, y2);
ctx.stroke();
}
// Draw center line
ctx.strokeStyle = 'rgba(148, 163, 184, 0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
// Draw playhead
if (duration > 0) {
const playheadX = (currentTime / duration) * width;
ctx.strokeStyle = 'rgba(59, 130, 246, 0.8)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(playheadX, 0);
ctx.lineTo(playheadX, height);
ctx.stroke();
}
}, [track.audioBuffer, track.color, track.collapsed, zoom, currentTime, duration]);
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!onSeek || !duration) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const clickTime = (x / rect.width) * duration;
onSeek(clickTime);
};
if (track.collapsed) {
return (
<div
className={cn(
'border-b border-border cursor-pointer',
isSelected && 'ring-2 ring-primary ring-inset'
)}
onClick={onSelect}
>
<TrackHeader
track={track}
onToggleMute={onToggleMute}
onToggleSolo={onToggleSolo}
onToggleCollapse={onToggleCollapse}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onRemove={onRemove}
onNameChange={onNameChange}
/>
</div>
);
}
return (
<div
ref={containerRef}
className={cn(
'border-b border-border cursor-pointer',
isSelected && 'ring-2 ring-primary ring-inset'
)}
onClick={onSelect}
>
<TrackHeader
track={track}
onToggleMute={onToggleMute}
onToggleSolo={onToggleSolo}
onToggleCollapse={onToggleCollapse}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onRemove={onRemove}
onNameChange={onNameChange}
/>
<div className="relative" style={{ height: track.height }}>
{track.audioBuffer ? (
<canvas
ref={canvasRef}
className="w-full h-full cursor-pointer"
onClick={handleCanvasClick}
/>
) : (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
No audio loaded
</div>
)}
</div>
</div>
);
}