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:
148
components/editor/AudioEditor.tsx
Normal file
148
components/editor/AudioEditor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user