feat: implement Phase 2 - Web Audio API engine and waveform visualization

Phase 2 Complete Features:
- Web Audio API context management with browser compatibility
- Audio file upload with drag-and-drop support
- Audio decoding for multiple formats (WAV, MP3, OGG, FLAC, AAC, M4A)
- AudioPlayer class with full playback control
- Waveform visualization using Canvas API
- Real-time waveform rendering with progress indicator
- Playback controls (play, pause, stop, seek)
- Volume control with mute/unmute
- Timeline scrubbing
- Audio file information display

Components:
- AudioEditor: Main editor container
- FileUpload: Drag-and-drop file upload component
- AudioInfo: Display audio file metadata
- Waveform: Canvas-based waveform visualization
- PlaybackControls: Transport controls with volume slider

Audio Engine:
- lib/audio/context.ts: AudioContext management
- lib/audio/decoder.ts: Audio file decoding utilities
- lib/audio/player.ts: AudioPlayer class for playback
- lib/waveform/peaks.ts: Waveform peak generation

Hooks:
- useAudioPlayer: Complete audio player state management

Types:
- types/audio.ts: TypeScript definitions for audio types

Features Working:
✓ Load audio files via drag-and-drop or file picker
✓ Display waveform with real-time progress
✓ Play/pause/stop controls
✓ Seek by clicking on waveform or using timeline slider
✓ Volume control with visual feedback
✓ Audio file metadata display (duration, sample rate, channels)
✓ Toast notifications for user feedback
✓ SSR-safe audio context initialization
✓ Dark/light theme support

Tech Stack:
- Web Audio API for playback
- Canvas API for waveform rendering
- React 19 hooks for state management
- TypeScript for type safety

Build verified and working ✓

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-17 15:32:00 +01:00
parent 45b73e148b
commit ebfb4d3fff
12 changed files with 1313 additions and 103 deletions

View File

@@ -0,0 +1,148 @@
'use client';
import * as React from 'react';
import { FileUpload } from './FileUpload';
import { AudioInfo } from './AudioInfo';
import { Waveform } from './Waveform';
import { PlaybackControls } from './PlaybackControls';
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
import { useToast } from '@/components/ui/Toast';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Loader2 } from 'lucide-react';
export function AudioEditor() {
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();
addToast({
title: 'Audio cleared',
description: 'Audio file has been removed',
variant: 'info',
duration: 2000,
});
};
// 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>
<Waveform
audioBuffer={audioBuffer}
currentTime={currentTime}
duration={duration}
onSeek={seek}
height={150}
/>
</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>
);
}