Files
audio-ui/components/editor/AudioEditor.tsx
Sebastian Krüger 5cf9a69056 feat: complete Phase 3 - Advanced waveform visualization and zoom controls
Phase 3 Complete Features:
 Drag-to-scrub audio functionality
 Horizontal zoom (1x-20x) with smooth scaling
 Vertical amplitude zoom (0.5x-5x)
 Horizontal scrolling for zoomed waveform
 Grid lines every second for time reference
 Viewport culling for better performance
 Zoom controls UI component

Components Added:
- ZoomControls: Complete zoom control panel with:
  - Horizontal zoom slider and buttons
  - Amplitude zoom slider
  - Zoom in/out buttons
  - Fit to view button
  - Real-time zoom level display

Waveform Enhancements:
- Drag-to-scrub: Click and drag to scrub through audio
- Zoom support: View waveform at different zoom levels
- Scroll support: Navigate through zoomed waveform
- Grid lines: Visual time markers every second
- Viewport culling: Only render visible portions
- Cursor feedback: Grabbing cursor when dragging

AudioEditor Updates:
- Integrated zoom and scroll state management
- Auto-reset zoom on file clear
- Scroll slider appears when zoomed
- Smooth zoom transitions

Technical Improvements:
- Viewport culling: Only render visible waveform portions
- Grid rendering: Time-aligned vertical grid lines
- Smart scroll clamping: Prevent scrolling beyond bounds
- Zoom-aware seeking: Accurate time calculation with zoom
- Performance optimized rendering

Features Working:
 Drag waveform to scrub audio
 Zoom in up to 20x for detailed editing
 Adjust amplitude for better visualization
 Scroll through zoomed waveform
 Grid lines show time markers
 Smooth cursor interactions

Phase 3 Status: 95% complete
- Completed: All major features
- Optional: Measure/beat markers, OffscreenCanvas, Web Workers

Build verified and working ✓

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 15:44:29 +01:00

219 lines
5.7 KiB
TypeScript

'use client';
import * as React from 'react';
import { FileUpload } from './FileUpload';
import { AudioInfo } from './AudioInfo';
import { Waveform } from './Waveform';
import { PlaybackControls } from './PlaybackControls';
import { ZoomControls } from './ZoomControls';
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
import { useToast } from '@/components/ui/Toast';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Slider } from '@/components/ui/Slider';
import { Loader2 } from 'lucide-react';
export function AudioEditor() {
// Zoom and scroll state
const [zoom, setZoom] = React.useState(1);
const [scrollOffset, setScrollOffset] = React.useState(0);
const [amplitudeScale, setAmplitudeScale] = React.useState(1);
const {
loadFile,
clearFile,
play,
pause,
stop,
seek,
setVolume,
isPlaying,
isPaused,
currentTime,
duration,
volume,
audioBuffer,
fileName,
isLoading,
error,
currentTimeFormatted,
durationFormatted,
} = useAudioPlayer();
const { addToast } = useToast();
const handleFileSelect = async (file: File) => {
try {
await loadFile(file);
addToast({
title: 'File loaded',
description: `Successfully loaded ${file.name}`,
variant: 'success',
duration: 3000,
});
} catch (err) {
addToast({
title: 'Error loading file',
description: err instanceof Error ? err.message : 'Unknown error',
variant: 'error',
duration: 5000,
});
}
};
const handleClear = () => {
clearFile();
setZoom(1);
setScrollOffset(0);
setAmplitudeScale(1);
addToast({
title: 'Audio cleared',
description: 'Audio file has been removed',
variant: 'info',
duration: 2000,
});
};
// Zoom controls
const handleZoomIn = () => {
setZoom((prev) => Math.min(20, prev + 1));
};
const handleZoomOut = () => {
setZoom((prev) => Math.max(1, prev - 1));
};
const handleFitToView = () => {
setZoom(1);
setScrollOffset(0);
};
// Auto-adjust scroll when zoom changes
React.useEffect(() => {
if (!audioBuffer) return;
// Reset scroll if zoomed out completely
if (zoom === 1) {
setScrollOffset(0);
}
}, [zoom, audioBuffer]);
// Show error toast
React.useEffect(() => {
if (error) {
addToast({
title: 'Error',
description: error,
variant: 'error',
duration: 5000,
});
}
}, [error, addToast]);
return (
<div className="space-y-6">
{/* File Upload or Audio Info */}
{!audioBuffer ? (
<FileUpload onFileSelect={handleFileSelect} />
) : (
<AudioInfo
fileName={fileName || 'Unknown'}
audioBuffer={audioBuffer}
onClear={handleClear}
/>
)}
{/* Loading State */}
{isLoading && (
<Card>
<CardContent className="p-8">
<div className="flex flex-col items-center justify-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading audio file...</p>
</div>
</CardContent>
</Card>
)}
{/* Waveform and Controls */}
{audioBuffer && !isLoading && (
<>
{/* Waveform */}
<Card>
<CardHeader>
<CardTitle>Waveform</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Waveform
audioBuffer={audioBuffer}
currentTime={currentTime}
duration={duration}
onSeek={seek}
height={150}
zoom={zoom}
scrollOffset={scrollOffset}
amplitudeScale={amplitudeScale}
/>
{/* Horizontal scroll for zoomed waveform */}
{zoom > 1 && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Scroll Position
</label>
<Slider
value={scrollOffset}
onChange={setScrollOffset}
min={0}
max={Math.max(0, (800 * zoom) - 800)}
step={1}
/>
</div>
)}
</CardContent>
</Card>
{/* Zoom Controls */}
<Card>
<CardHeader>
<CardTitle>Zoom & View</CardTitle>
</CardHeader>
<CardContent>
<ZoomControls
zoom={zoom}
onZoomChange={setZoom}
amplitudeScale={amplitudeScale}
onAmplitudeScaleChange={setAmplitudeScale}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onFitToView={handleFitToView}
/>
</CardContent>
</Card>
{/* Playback Controls */}
<Card>
<CardHeader>
<CardTitle>Playback</CardTitle>
</CardHeader>
<CardContent>
<PlaybackControls
isPlaying={isPlaying}
isPaused={isPaused}
currentTime={currentTime}
duration={duration}
volume={volume}
onPlay={play}
onPause={pause}
onStop={stop}
onSeek={seek}
onVolumeChange={setVolume}
currentTimeFormatted={currentTimeFormatted}
durationFormatted={durationFormatted}
/>
</CardContent>
</Card>
</>
)}
</div>
);
}