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>
2025-11-17 15:32:00 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import * as React from 'react';
|
2025-11-17 20:03:40 +01:00
|
|
|
import { Music, Loader2 } from 'lucide-react';
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
import { Waveform } from './Waveform';
|
|
|
|
|
import { PlaybackControls } from './PlaybackControls';
|
2025-11-17 20:03:40 +01:00
|
|
|
import { SidePanel } from '@/components/layout/SidePanel';
|
|
|
|
|
import { ThemeToggle } from '@/components/layout/ThemeToggle';
|
|
|
|
|
import { CommandPalette } from '@/components/ui/CommandPalette';
|
|
|
|
|
import type { CommandAction } from '@/components/ui/CommandPalette';
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
|
2025-11-17 17:08:31 +01:00
|
|
|
import { useHistory } from '@/lib/hooks/useHistory';
|
2025-11-17 20:27:08 +01:00
|
|
|
import { useEffectChain } from '@/lib/hooks/useEffectChain';
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
import { useToast } from '@/components/ui/Toast';
|
2025-11-17 15:44:29 +01:00
|
|
|
import { Slider } from '@/components/ui/Slider';
|
2025-11-17 20:03:40 +01:00
|
|
|
import { cn } from '@/lib/utils/cn';
|
2025-11-17 15:50:42 +01:00
|
|
|
import type { Selection, ClipboardData } from '@/types/selection';
|
|
|
|
|
import {
|
|
|
|
|
extractBufferSegment,
|
|
|
|
|
deleteBufferSegment,
|
|
|
|
|
insertBufferSegment,
|
|
|
|
|
trimBuffer,
|
|
|
|
|
} from '@/lib/audio/buffer-utils';
|
2025-11-17 17:08:31 +01:00
|
|
|
import {
|
|
|
|
|
createCutCommand,
|
|
|
|
|
createDeleteCommand,
|
|
|
|
|
createPasteCommand,
|
|
|
|
|
createTrimCommand,
|
|
|
|
|
} from '@/lib/history/commands/edit-command';
|
2025-11-17 20:03:40 +01:00
|
|
|
import {
|
|
|
|
|
createGainCommand,
|
|
|
|
|
createNormalizePeakCommand,
|
|
|
|
|
createNormalizeRMSCommand,
|
|
|
|
|
createFadeInCommand,
|
|
|
|
|
createFadeOutCommand,
|
|
|
|
|
createReverseCommand,
|
|
|
|
|
createLowPassFilterCommand,
|
|
|
|
|
createHighPassFilterCommand,
|
|
|
|
|
createBandPassFilterCommand,
|
|
|
|
|
EffectCommand,
|
|
|
|
|
} from '@/lib/history/commands/effect-command';
|
|
|
|
|
import { applyEffectToSelection, applyAsyncEffectToSelection } from '@/lib/audio/effects/selection';
|
|
|
|
|
import { normalizePeak } from '@/lib/audio/effects/normalize';
|
|
|
|
|
import { applyFadeIn, applyFadeOut } from '@/lib/audio/effects/fade';
|
|
|
|
|
import { reverseAudio } from '@/lib/audio/effects/reverse';
|
|
|
|
|
import { applyLowPassFilter, applyHighPassFilter, applyBandPassFilter, applyFilter } from '@/lib/audio/effects/filters';
|
|
|
|
|
import type { FilterType } from '@/lib/audio/effects/filters';
|
|
|
|
|
import { applyCompressor, applyLimiter, applyGate } from '@/lib/audio/effects/dynamics';
|
|
|
|
|
import { applyDelay, applyReverb, applyChorus, applyFlanger, applyPhaser } from '@/lib/audio/effects/time-based';
|
|
|
|
|
import { applyPitchShift, applyTimeStretch, applyDistortion, applyBitcrusher } from '@/lib/audio/effects/advanced';
|
|
|
|
|
import { EffectParameterDialog, type FilterParameters } from '@/components/effects/EffectParameterDialog';
|
|
|
|
|
import { DynamicsParameterDialog, type DynamicsParameters, type DynamicsType } from '@/components/effects/DynamicsParameterDialog';
|
|
|
|
|
import { TimeBasedParameterDialog, type TimeBasedParameters, type TimeBasedType } from '@/components/effects/TimeBasedParameterDialog';
|
|
|
|
|
import { AdvancedParameterDialog, type AdvancedParameters, type AdvancedType } from '@/components/effects/AdvancedParameterDialog';
|
|
|
|
|
|
|
|
|
|
const EFFECT_LABELS: Record<string, string> = {
|
|
|
|
|
lowpass: 'Low-Pass Filter',
|
|
|
|
|
highpass: 'High-Pass Filter',
|
|
|
|
|
bandpass: 'Band-Pass Filter',
|
|
|
|
|
notch: 'Notch Filter',
|
|
|
|
|
lowshelf: 'Low Shelf Filter',
|
|
|
|
|
highshelf: 'High Shelf Filter',
|
|
|
|
|
peaking: 'Peaking EQ',
|
|
|
|
|
compressor: 'Compressor',
|
|
|
|
|
limiter: 'Limiter',
|
|
|
|
|
gate: 'Gate/Expander',
|
|
|
|
|
delay: 'Delay/Echo',
|
|
|
|
|
reverb: 'Reverb',
|
|
|
|
|
chorus: 'Chorus',
|
|
|
|
|
flanger: 'Flanger',
|
|
|
|
|
phaser: 'Phaser',
|
|
|
|
|
pitch: 'Pitch Shifter',
|
|
|
|
|
timestretch: 'Time Stretch',
|
|
|
|
|
distortion: 'Distortion',
|
|
|
|
|
bitcrusher: 'Bitcrusher',
|
|
|
|
|
};
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
|
|
|
|
|
export function AudioEditor() {
|
2025-11-17 15:44:29 +01:00
|
|
|
// Zoom and scroll state
|
|
|
|
|
const [zoom, setZoom] = React.useState(1);
|
|
|
|
|
const [scrollOffset, setScrollOffset] = React.useState(0);
|
|
|
|
|
const [amplitudeScale, setAmplitudeScale] = React.useState(1);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
// Effect dialog state
|
|
|
|
|
const [effectDialogOpen, setEffectDialogOpen] = React.useState(false);
|
|
|
|
|
const [effectDialogType, setEffectDialogType] = React.useState<'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'lowshelf' | 'highshelf' | 'peaking'>('lowpass');
|
|
|
|
|
|
|
|
|
|
// Dynamics dialog state
|
|
|
|
|
const [dynamicsDialogOpen, setDynamicsDialogOpen] = React.useState(false);
|
|
|
|
|
const [dynamicsDialogType, setDynamicsDialogType] = React.useState<DynamicsType>('compressor');
|
|
|
|
|
|
|
|
|
|
// Time-based dialog state
|
|
|
|
|
const [timeBasedDialogOpen, setTimeBasedDialogOpen] = React.useState(false);
|
|
|
|
|
const [timeBasedDialogType, setTimeBasedDialogType] = React.useState<TimeBasedType>('delay');
|
|
|
|
|
|
|
|
|
|
// Advanced dialog state
|
|
|
|
|
const [advancedDialogOpen, setAdvancedDialogOpen] = React.useState(false);
|
|
|
|
|
const [advancedDialogType, setAdvancedDialogType] = React.useState<AdvancedType>('pitch');
|
|
|
|
|
|
|
|
|
|
// Drag and drop state
|
|
|
|
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
|
|
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
|
2025-11-17 15:50:42 +01:00
|
|
|
// Selection state
|
|
|
|
|
const [selection, setSelection] = React.useState<Selection | null>(null);
|
|
|
|
|
const [clipboard, setClipboard] = React.useState<ClipboardData | null>(null);
|
2025-11-17 17:08:31 +01:00
|
|
|
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
const {
|
|
|
|
|
loadFile,
|
2025-11-17 15:50:42 +01:00
|
|
|
loadBuffer,
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
clearFile,
|
|
|
|
|
play,
|
|
|
|
|
pause,
|
|
|
|
|
stop,
|
|
|
|
|
seek,
|
|
|
|
|
setVolume,
|
|
|
|
|
isPlaying,
|
|
|
|
|
isPaused,
|
|
|
|
|
currentTime,
|
|
|
|
|
duration,
|
|
|
|
|
volume,
|
|
|
|
|
audioBuffer,
|
|
|
|
|
fileName,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
currentTimeFormatted,
|
|
|
|
|
durationFormatted,
|
|
|
|
|
} = useAudioPlayer();
|
|
|
|
|
|
2025-11-17 17:08:31 +01:00
|
|
|
const { execute, undo, redo, clear: clearHistory, state: historyState } = useHistory(50);
|
2025-11-17 20:27:08 +01:00
|
|
|
const {
|
|
|
|
|
chain: effectChain,
|
|
|
|
|
presets: effectPresets,
|
|
|
|
|
toggleEffectEnabled,
|
|
|
|
|
removeEffect,
|
|
|
|
|
reorder: reorderEffects,
|
|
|
|
|
clearChain,
|
|
|
|
|
savePreset,
|
|
|
|
|
loadPresetToChain,
|
|
|
|
|
deletePreset,
|
|
|
|
|
} = useEffectChain();
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
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();
|
2025-11-17 15:44:29 +01:00
|
|
|
setZoom(1);
|
|
|
|
|
setScrollOffset(0);
|
|
|
|
|
setAmplitudeScale(1);
|
2025-11-17 15:50:42 +01:00
|
|
|
setSelection(null);
|
|
|
|
|
setClipboard(null);
|
2025-11-17 17:08:31 +01:00
|
|
|
clearHistory();
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
addToast({
|
|
|
|
|
title: 'Audio cleared',
|
|
|
|
|
description: 'Audio file has been removed',
|
|
|
|
|
variant: 'info',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
// Drag and drop handlers
|
|
|
|
|
const handleDragEnter = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setIsDragging(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragLeave = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
// Only set to false if we're leaving the drop zone entirely
|
|
|
|
|
if (e.currentTarget === e.target) {
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDrop = async (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
|
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
|
|
|
const audioFile = files.find(file => file.type.startsWith('audio/'));
|
|
|
|
|
|
|
|
|
|
if (audioFile) {
|
|
|
|
|
await handleFileSelect(audioFile);
|
|
|
|
|
} else {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Invalid file',
|
|
|
|
|
description: 'Please drop an audio file',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDropZoneClick = () => {
|
|
|
|
|
fileInputRef.current?.click();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
handleFileSelect(file);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 15:50:42 +01:00
|
|
|
// Edit operations
|
|
|
|
|
const handleCut = () => {
|
|
|
|
|
if (!selection || !audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Copy to clipboard
|
|
|
|
|
const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end);
|
|
|
|
|
setClipboard({
|
|
|
|
|
buffer: clipData,
|
|
|
|
|
start: selection.start,
|
|
|
|
|
end: selection.end,
|
|
|
|
|
duration: selection.end - selection.start,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-17 17:08:31 +01:00
|
|
|
// Create and execute cut command
|
|
|
|
|
const command = createCutCommand(audioBuffer, selection, (buffer) => {
|
|
|
|
|
loadBuffer(buffer);
|
|
|
|
|
});
|
|
|
|
|
execute(command);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
|
|
|
|
setSelection(null);
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Cut',
|
|
|
|
|
description: 'Selection cut to clipboard',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to cut selection',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCopy = () => {
|
|
|
|
|
if (!selection || !audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const clipData = extractBufferSegment(audioBuffer, selection.start, selection.end);
|
|
|
|
|
setClipboard({
|
|
|
|
|
buffer: clipData,
|
|
|
|
|
start: selection.start,
|
|
|
|
|
end: selection.end,
|
|
|
|
|
duration: selection.end - selection.start,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Copied',
|
|
|
|
|
description: 'Selection copied to clipboard',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to copy selection',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePaste = () => {
|
|
|
|
|
if (!clipboard || !audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const insertTime = currentTime;
|
2025-11-17 17:08:31 +01:00
|
|
|
|
|
|
|
|
// Create and execute paste command
|
|
|
|
|
const command = createPasteCommand(audioBuffer, clipboard.buffer, insertTime, (buffer) => {
|
|
|
|
|
loadBuffer(buffer);
|
|
|
|
|
});
|
|
|
|
|
execute(command);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Pasted',
|
|
|
|
|
description: 'Clipboard pasted at current position',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to paste clipboard',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = () => {
|
|
|
|
|
if (!selection || !audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-17 17:08:31 +01:00
|
|
|
// Create and execute delete command
|
|
|
|
|
const command = createDeleteCommand(audioBuffer, selection, (buffer) => {
|
|
|
|
|
loadBuffer(buffer);
|
|
|
|
|
});
|
|
|
|
|
execute(command);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
|
|
|
|
setSelection(null);
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Deleted',
|
|
|
|
|
description: 'Selection deleted',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to delete selection',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTrim = () => {
|
|
|
|
|
if (!selection || !audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-17 17:08:31 +01:00
|
|
|
// Create and execute trim command
|
|
|
|
|
const command = createTrimCommand(audioBuffer, selection, (buffer) => {
|
|
|
|
|
loadBuffer(buffer);
|
|
|
|
|
});
|
|
|
|
|
execute(command);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
|
|
|
|
setSelection(null);
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Trimmed',
|
|
|
|
|
description: 'Audio trimmed to selection',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to trim audio',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectAll = () => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
setSelection({ start: 0, end: duration });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClearSelection = () => {
|
|
|
|
|
setSelection(null);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
// Effect operations
|
|
|
|
|
const handleNormalize = () => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Apply to selection or entire buffer
|
|
|
|
|
const modifiedBuffer = applyEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => normalizePeak(buf, 1.0)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection ? 'Normalize Selection' : 'Normalize'
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Normalized',
|
|
|
|
|
description: selection ? 'Selection normalized to peak' : 'Audio normalized to peak',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to normalize audio',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFadeIn = () => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
if (!selection) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'No Selection',
|
|
|
|
|
description: 'Please select a region to apply fade in',
|
|
|
|
|
variant: 'info',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const fadeDuration = selection.end - selection.start;
|
|
|
|
|
const modifiedBuffer = applyEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyFadeIn(buf, buf.duration)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
`Fade In (${fadeDuration.toFixed(2)}s)`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Fade In',
|
|
|
|
|
description: `Applied fade in (${fadeDuration.toFixed(2)}s)`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply fade in',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFadeOut = () => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
if (!selection) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'No Selection',
|
|
|
|
|
description: 'Please select a region to apply fade out',
|
|
|
|
|
variant: 'info',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const fadeDuration = selection.end - selection.start;
|
|
|
|
|
const modifiedBuffer = applyEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyFadeOut(buf, buf.duration)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
`Fade Out (${fadeDuration.toFixed(2)}s)`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Fade Out',
|
|
|
|
|
description: `Applied fade out (${fadeDuration.toFixed(2)}s)`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply fade out',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleReverse = () => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const modifiedBuffer = applyEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => reverseAudio(buf)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection ? 'Reverse Selection' : 'Reverse'
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Reversed',
|
|
|
|
|
description: selection ? 'Selection reversed' : 'Audio reversed',
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to reverse audio',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLowPassFilter = () => {
|
|
|
|
|
setEffectDialogType('lowpass');
|
|
|
|
|
setEffectDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleHighPassFilter = () => {
|
|
|
|
|
setEffectDialogType('highpass');
|
|
|
|
|
setEffectDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBandPassFilter = () => {
|
|
|
|
|
setEffectDialogType('bandpass');
|
|
|
|
|
setEffectDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCompressor = () => {
|
|
|
|
|
setDynamicsDialogType('compressor');
|
|
|
|
|
setDynamicsDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLimiter = () => {
|
|
|
|
|
setDynamicsDialogType('limiter');
|
|
|
|
|
setDynamicsDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGate = () => {
|
|
|
|
|
setDynamicsDialogType('gate');
|
|
|
|
|
setDynamicsDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelay = () => {
|
|
|
|
|
setTimeBasedDialogType('delay');
|
|
|
|
|
setTimeBasedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleReverb = () => {
|
|
|
|
|
setTimeBasedDialogType('reverb');
|
|
|
|
|
setTimeBasedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleChorus = () => {
|
|
|
|
|
setTimeBasedDialogType('chorus');
|
|
|
|
|
setTimeBasedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFlanger = () => {
|
|
|
|
|
setTimeBasedDialogType('flanger');
|
|
|
|
|
setTimeBasedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePhaser = () => {
|
|
|
|
|
setTimeBasedDialogType('phaser');
|
|
|
|
|
setTimeBasedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePitchShift = () => {
|
|
|
|
|
setAdvancedDialogType('pitch');
|
|
|
|
|
setAdvancedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTimeStretch = () => {
|
|
|
|
|
setAdvancedDialogType('timestretch');
|
|
|
|
|
setAdvancedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDistortion = () => {
|
|
|
|
|
setAdvancedDialogType('distortion');
|
|
|
|
|
setAdvancedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBitcrusher = () => {
|
|
|
|
|
setAdvancedDialogType('bitcrusher');
|
|
|
|
|
setAdvancedDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle effect apply from parameter dialog
|
|
|
|
|
const handleEffectApply = async (params: FilterParameters) => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyFilter(buf, {
|
|
|
|
|
type: params.type,
|
|
|
|
|
frequency: params.frequency,
|
|
|
|
|
Q: params.Q,
|
|
|
|
|
gain: params.gain,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const effectName = EFFECT_LABELS[params.type] || 'Filter';
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection
|
|
|
|
|
? `${effectName} Selection (${params.frequency.toFixed(0)}Hz)`
|
|
|
|
|
: `${effectName} (${params.frequency.toFixed(0)}Hz)`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: effectName,
|
|
|
|
|
description: selection
|
|
|
|
|
? `Applied ${effectName.toLowerCase()} to selection (${params.frequency.toFixed(0)}Hz)`
|
|
|
|
|
: `Applied ${effectName.toLowerCase()} (${params.frequency.toFixed(0)}Hz)`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply effect',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle dynamics apply from parameter dialog
|
|
|
|
|
const handleDynamicsApply = async (params: DynamicsParameters) => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let modifiedBuffer: AudioBuffer;
|
|
|
|
|
let effectName: string;
|
|
|
|
|
|
2025-11-17 20:10:47 +01:00
|
|
|
switch (params.type) {
|
|
|
|
|
case 'compressor':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyCompressor(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Compressor';
|
|
|
|
|
break;
|
|
|
|
|
case 'limiter':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyLimiter(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Limiter';
|
|
|
|
|
break;
|
|
|
|
|
case 'gate':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyGate(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Gate';
|
|
|
|
|
break;
|
2025-11-17 20:03:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection
|
|
|
|
|
? `${effectName} Selection (${params.threshold.toFixed(1)}dB)`
|
|
|
|
|
: `${effectName} (${params.threshold.toFixed(1)}dB)`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: effectName,
|
|
|
|
|
description: selection
|
|
|
|
|
? `Applied ${effectName.toLowerCase()} to selection`
|
|
|
|
|
: `Applied ${effectName.toLowerCase()}`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply dynamics effect',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle time-based apply from parameter dialog
|
|
|
|
|
const handleTimeBasedApply = async (params: TimeBasedParameters) => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let modifiedBuffer: AudioBuffer;
|
|
|
|
|
let effectName: string;
|
|
|
|
|
|
2025-11-17 20:10:47 +01:00
|
|
|
switch (params.type) {
|
|
|
|
|
case 'delay':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyDelay(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Delay';
|
|
|
|
|
break;
|
|
|
|
|
case 'reverb':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyReverb(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Reverb';
|
|
|
|
|
break;
|
|
|
|
|
case 'chorus':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyChorus(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Chorus';
|
|
|
|
|
break;
|
|
|
|
|
case 'flanger':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyFlanger(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Flanger';
|
|
|
|
|
break;
|
|
|
|
|
case 'phaser':
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
|
|
|
|
(buf) => applyPhaser(buf, params)
|
|
|
|
|
);
|
|
|
|
|
effectName = 'Phaser';
|
|
|
|
|
break;
|
2025-11-17 20:03:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection
|
|
|
|
|
? `${effectName} Selection`
|
|
|
|
|
: `${effectName}`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: effectName,
|
|
|
|
|
description: selection
|
|
|
|
|
? `Applied ${effectName.toLowerCase()} to selection`
|
|
|
|
|
: `Applied ${effectName.toLowerCase()}`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply time-based effect',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAdvancedApply = async (params: AdvancedParameters) => {
|
|
|
|
|
if (!audioBuffer) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let modifiedBuffer: AudioBuffer;
|
|
|
|
|
let effectName: string;
|
|
|
|
|
|
2025-11-17 20:10:47 +01:00
|
|
|
const effectType = params.type;
|
|
|
|
|
|
|
|
|
|
if (effectType === 'pitch') {
|
2025-11-17 20:03:40 +01:00
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
2025-11-17 20:10:47 +01:00
|
|
|
(buf) => applyPitchShift(buf, params as any)
|
2025-11-17 20:03:40 +01:00
|
|
|
);
|
|
|
|
|
effectName = 'Pitch Shift';
|
2025-11-17 20:10:47 +01:00
|
|
|
} else if (effectType === 'timestretch') {
|
2025-11-17 20:03:40 +01:00
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
2025-11-17 20:10:47 +01:00
|
|
|
(buf) => applyTimeStretch(buf, params as any)
|
2025-11-17 20:03:40 +01:00
|
|
|
);
|
|
|
|
|
effectName = 'Time Stretch';
|
2025-11-17 20:10:47 +01:00
|
|
|
} else if (effectType === 'bitcrusher') {
|
2025-11-17 20:03:40 +01:00
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
2025-11-17 20:10:47 +01:00
|
|
|
(buf) => applyBitcrusher(buf, params as any)
|
2025-11-17 20:03:40 +01:00
|
|
|
);
|
2025-11-17 20:10:47 +01:00
|
|
|
effectName = 'Bitcrusher';
|
2025-11-17 20:03:40 +01:00
|
|
|
} else {
|
|
|
|
|
modifiedBuffer = await applyAsyncEffectToSelection(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
selection,
|
2025-11-17 20:10:47 +01:00
|
|
|
(buf) => applyDistortion(buf, params as any)
|
2025-11-17 20:03:40 +01:00
|
|
|
);
|
2025-11-17 20:10:47 +01:00
|
|
|
effectName = 'Distortion';
|
2025-11-17 20:03:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const command = new EffectCommand(
|
|
|
|
|
audioBuffer,
|
|
|
|
|
modifiedBuffer,
|
|
|
|
|
(buffer) => loadBuffer(buffer),
|
|
|
|
|
selection
|
|
|
|
|
? `${effectName} Selection`
|
|
|
|
|
: `${effectName}`
|
|
|
|
|
);
|
|
|
|
|
execute(command);
|
|
|
|
|
|
|
|
|
|
addToast({
|
|
|
|
|
title: effectName,
|
|
|
|
|
description: selection
|
|
|
|
|
? `Applied ${effectName.toLowerCase()} to selection`
|
|
|
|
|
: `Applied ${effectName.toLowerCase()}`,
|
|
|
|
|
variant: 'success',
|
|
|
|
|
duration: 2000,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: 'Failed to apply advanced effect',
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 15:44:29 +01:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-11-17 15:50:42 +01:00
|
|
|
// Keyboard shortcuts
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
// Prevent shortcuts if typing in an input
|
2025-11-17 20:03:40 +01:00
|
|
|
const isTyping = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
|
|
|
|
|
|
|
|
|
|
// Spacebar: Play/Pause (always, unless typing in an input)
|
|
|
|
|
if (e.code === 'Space' && !isTyping) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
pause();
|
|
|
|
|
} else {
|
|
|
|
|
play();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prevent other shortcuts if typing in an input
|
|
|
|
|
if (isTyping) {
|
2025-11-17 15:50:42 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 17:08:31 +01:00
|
|
|
// Ctrl+Z: Undo
|
|
|
|
|
if (e.ctrlKey && !e.shiftKey && e.key === 'z') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (undo()) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Undo',
|
|
|
|
|
description: 'Last action undone',
|
|
|
|
|
variant: 'info',
|
|
|
|
|
duration: 1500,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl+Y or Ctrl+Shift+Z: Redo
|
|
|
|
|
if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (redo()) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Redo',
|
|
|
|
|
description: 'Last action redone',
|
|
|
|
|
variant: 'info',
|
|
|
|
|
duration: 1500,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 15:50:42 +01:00
|
|
|
// Ctrl+A: Select all
|
|
|
|
|
if (e.ctrlKey && e.key === 'a') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSelectAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl+X: Cut
|
|
|
|
|
if (e.ctrlKey && e.key === 'x') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleCut();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl+C: Copy
|
|
|
|
|
if (e.ctrlKey && e.key === 'c') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleCopy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl+V: Paste
|
|
|
|
|
if (e.ctrlKey && e.key === 'v') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handlePaste();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete: Delete selection
|
|
|
|
|
if (e.key === 'Delete') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleDelete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape: Clear selection
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleClearSelection();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
2025-11-17 20:03:40 +01:00
|
|
|
}, [selection, clipboard, audioBuffer, currentTime, undo, redo, addToast, isPlaying, play, pause]);
|
2025-11-17 15:50:42 +01:00
|
|
|
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
// Show error toast
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (error) {
|
|
|
|
|
addToast({
|
|
|
|
|
title: 'Error',
|
|
|
|
|
description: error,
|
|
|
|
|
variant: 'error',
|
|
|
|
|
duration: 5000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [error, addToast]);
|
|
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
// Command palette actions
|
|
|
|
|
const commandActions: CommandAction[] = React.useMemo(() => {
|
|
|
|
|
const actions: CommandAction[] = [
|
|
|
|
|
// Playback
|
|
|
|
|
{
|
|
|
|
|
id: 'play',
|
|
|
|
|
label: 'Play',
|
|
|
|
|
description: 'Start playback',
|
|
|
|
|
shortcut: 'Space',
|
|
|
|
|
category: 'playback',
|
|
|
|
|
action: play,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pause',
|
|
|
|
|
label: 'Pause',
|
|
|
|
|
description: 'Pause playback',
|
|
|
|
|
shortcut: 'Space',
|
|
|
|
|
category: 'playback',
|
|
|
|
|
action: pause,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'stop',
|
|
|
|
|
label: 'Stop',
|
|
|
|
|
description: 'Stop playback',
|
|
|
|
|
category: 'playback',
|
|
|
|
|
action: stop,
|
|
|
|
|
},
|
|
|
|
|
// Edit
|
|
|
|
|
{
|
|
|
|
|
id: 'cut',
|
|
|
|
|
label: 'Cut',
|
|
|
|
|
description: 'Cut selection to clipboard',
|
|
|
|
|
shortcut: 'Ctrl+X',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleCut,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'copy',
|
|
|
|
|
label: 'Copy',
|
|
|
|
|
description: 'Copy selection to clipboard',
|
|
|
|
|
shortcut: 'Ctrl+C',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleCopy,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'paste',
|
|
|
|
|
label: 'Paste',
|
|
|
|
|
description: 'Paste clipboard at current position',
|
|
|
|
|
shortcut: 'Ctrl+V',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handlePaste,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'delete',
|
|
|
|
|
label: 'Delete',
|
|
|
|
|
description: 'Delete selection',
|
|
|
|
|
shortcut: 'Del',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleDelete,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'trim',
|
|
|
|
|
label: 'Trim to Selection',
|
|
|
|
|
description: 'Trim audio to selected region',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleTrim,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'select-all',
|
|
|
|
|
label: 'Select All',
|
|
|
|
|
description: 'Select entire audio',
|
|
|
|
|
shortcut: 'Ctrl+A',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleSelectAll,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'clear-selection',
|
|
|
|
|
label: 'Clear Selection',
|
|
|
|
|
description: 'Clear current selection',
|
|
|
|
|
shortcut: 'Esc',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: handleClearSelection,
|
|
|
|
|
},
|
|
|
|
|
// View
|
|
|
|
|
{
|
|
|
|
|
id: 'zoom-in',
|
|
|
|
|
label: 'Zoom In',
|
|
|
|
|
description: 'Zoom in on waveform',
|
|
|
|
|
category: 'view',
|
|
|
|
|
action: handleZoomIn,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'zoom-out',
|
|
|
|
|
label: 'Zoom Out',
|
|
|
|
|
description: 'Zoom out on waveform',
|
|
|
|
|
category: 'view',
|
|
|
|
|
action: handleZoomOut,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'fit-to-view',
|
|
|
|
|
label: 'Fit to View',
|
|
|
|
|
description: 'Reset zoom to fit entire waveform',
|
|
|
|
|
category: 'view',
|
|
|
|
|
action: handleFitToView,
|
|
|
|
|
},
|
|
|
|
|
// File
|
|
|
|
|
{
|
|
|
|
|
id: 'clear',
|
|
|
|
|
label: 'Clear Audio',
|
|
|
|
|
description: 'Remove loaded audio file',
|
|
|
|
|
category: 'file',
|
|
|
|
|
action: handleClear,
|
|
|
|
|
},
|
|
|
|
|
// History
|
|
|
|
|
{
|
|
|
|
|
id: 'undo',
|
|
|
|
|
label: 'Undo',
|
|
|
|
|
description: 'Undo last action',
|
|
|
|
|
shortcut: 'Ctrl+Z',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: undo,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'redo',
|
|
|
|
|
label: 'Redo',
|
|
|
|
|
description: 'Redo last undone action',
|
|
|
|
|
shortcut: 'Ctrl+Y',
|
|
|
|
|
category: 'edit',
|
|
|
|
|
action: redo,
|
|
|
|
|
},
|
|
|
|
|
// Effects
|
|
|
|
|
{
|
|
|
|
|
id: 'normalize',
|
|
|
|
|
label: 'Normalize',
|
|
|
|
|
description: 'Normalize audio to peak amplitude',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleNormalize,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'fade-in',
|
|
|
|
|
label: 'Fade In',
|
|
|
|
|
description: 'Apply fade in to selection',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleFadeIn,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'fade-out',
|
|
|
|
|
label: 'Fade Out',
|
|
|
|
|
description: 'Apply fade out to selection',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleFadeOut,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'reverse',
|
|
|
|
|
label: 'Reverse',
|
|
|
|
|
description: 'Reverse entire audio',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleReverse,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'lowpass-filter',
|
|
|
|
|
label: 'Low-Pass Filter',
|
|
|
|
|
description: 'Remove high frequencies (1000Hz cutoff)',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleLowPassFilter,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'highpass-filter',
|
|
|
|
|
label: 'High-Pass Filter',
|
|
|
|
|
description: 'Remove low frequencies (100Hz cutoff)',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleHighPassFilter,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'bandpass-filter',
|
|
|
|
|
label: 'Band-Pass Filter',
|
|
|
|
|
description: 'Isolate frequency range (1000Hz center)',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleBandPassFilter,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'compressor',
|
|
|
|
|
label: 'Compressor',
|
|
|
|
|
description: 'Reduce dynamic range',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleCompressor,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'limiter',
|
|
|
|
|
label: 'Limiter',
|
|
|
|
|
description: 'Prevent audio from exceeding threshold',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleLimiter,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'gate',
|
|
|
|
|
label: 'Gate/Expander',
|
|
|
|
|
description: 'Reduce volume of quiet sounds',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleGate,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'delay',
|
|
|
|
|
label: 'Delay/Echo',
|
|
|
|
|
description: 'Add echo effects with feedback',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleDelay,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'reverb',
|
|
|
|
|
label: 'Reverb',
|
|
|
|
|
description: 'Add acoustic space and ambience',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleReverb,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'chorus',
|
|
|
|
|
label: 'Chorus',
|
|
|
|
|
description: 'Thicken sound with modulation',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleChorus,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'flanger',
|
|
|
|
|
label: 'Flanger',
|
|
|
|
|
description: 'Create sweeping comb-filter effect',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleFlanger,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'phaser',
|
|
|
|
|
label: 'Phaser',
|
|
|
|
|
description: 'Phase-shifting swoosh effect',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handlePhaser,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pitch',
|
|
|
|
|
label: 'Pitch Shifter',
|
|
|
|
|
description: 'Change pitch without affecting duration',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handlePitchShift,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'timestretch',
|
|
|
|
|
label: 'Time Stretch',
|
|
|
|
|
description: 'Change duration without affecting pitch',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleTimeStretch,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'distortion',
|
|
|
|
|
label: 'Distortion',
|
|
|
|
|
description: 'Add overdrive and distortion',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleDistortion,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'bitcrusher',
|
|
|
|
|
label: 'Bitcrusher',
|
|
|
|
|
description: 'Lo-fi bit depth and sample rate reduction',
|
|
|
|
|
category: 'effects',
|
|
|
|
|
action: handleBitcrusher,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
return actions;
|
|
|
|
|
}, [play, pause, stop, handleCut, handleCopy, handlePaste, handleDelete, handleTrim, handleSelectAll, handleClearSelection, handleZoomIn, handleZoomOut, handleFitToView, handleClear, undo, redo, handleNormalize, handleFadeIn, handleFadeOut, handleReverse, handleLowPassFilter, handleHighPassFilter, handleBandPassFilter, handleCompressor, handleLimiter, handleGate, handleDelay, handleReverb, handleChorus, handleFlanger, handlePhaser, handlePitchShift, handleTimeStretch, handleDistortion, handleBitcrusher]);
|
|
|
|
|
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
return (
|
2025-11-17 20:03:40 +01:00
|
|
|
<>
|
|
|
|
|
{/* Compact Header */}
|
|
|
|
|
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0 gap-4">
|
|
|
|
|
{/* Left: Logo */}
|
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
|
|
|
<Music className="h-5 w-5 text-primary" />
|
|
|
|
|
<h1 className="text-lg font-bold text-foreground">Audio UI</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right: Command Palette + Theme Toggle */}
|
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
|
|
|
<CommandPalette actions={commandActions} />
|
|
|
|
|
<ThemeToggle />
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{/* Main content area */}
|
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
|
|
|
{/* Side Panel */}
|
|
|
|
|
<SidePanel
|
|
|
|
|
fileName={fileName}
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
audioBuffer={audioBuffer}
|
2025-11-17 20:03:40 +01:00
|
|
|
onFileSelect={handleFileSelect}
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
onClear={handleClear}
|
2025-11-17 20:03:40 +01:00
|
|
|
selection={selection}
|
|
|
|
|
historyState={historyState}
|
2025-11-17 20:27:08 +01:00
|
|
|
effectChain={effectChain}
|
|
|
|
|
effectPresets={effectPresets}
|
|
|
|
|
onToggleEffect={toggleEffectEnabled}
|
|
|
|
|
onRemoveEffect={removeEffect}
|
|
|
|
|
onReorderEffects={reorderEffects}
|
|
|
|
|
onSavePreset={savePreset}
|
|
|
|
|
onLoadPreset={loadPresetToChain}
|
|
|
|
|
onDeletePreset={deletePreset}
|
|
|
|
|
onClearChain={clearChain}
|
2025-11-17 20:03:40 +01:00
|
|
|
onNormalize={handleNormalize}
|
|
|
|
|
onFadeIn={handleFadeIn}
|
|
|
|
|
onFadeOut={handleFadeOut}
|
|
|
|
|
onReverse={handleReverse}
|
|
|
|
|
onLowPassFilter={handleLowPassFilter}
|
|
|
|
|
onHighPassFilter={handleHighPassFilter}
|
|
|
|
|
onBandPassFilter={handleBandPassFilter}
|
|
|
|
|
onCompressor={handleCompressor}
|
|
|
|
|
onLimiter={handleLimiter}
|
|
|
|
|
onGate={handleGate}
|
|
|
|
|
onDelay={handleDelay}
|
|
|
|
|
onReverb={handleReverb}
|
|
|
|
|
onChorus={handleChorus}
|
|
|
|
|
onFlanger={handleFlanger}
|
|
|
|
|
onPhaser={handlePhaser}
|
|
|
|
|
onPitchShift={handlePitchShift}
|
|
|
|
|
onTimeStretch={handleTimeStretch}
|
|
|
|
|
onDistortion={handleDistortion}
|
|
|
|
|
onBitcrusher={handleBitcrusher}
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
/>
|
2025-11-17 20:03:40 +01:00
|
|
|
|
|
|
|
|
{/* Main canvas area */}
|
|
|
|
|
<main className="flex-1 flex flex-col overflow-hidden bg-background">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex-1 flex items-center justify-center">
|
|
|
|
|
<div className="flex flex-col items-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>
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
</div>
|
2025-11-17 20:03:40 +01:00
|
|
|
) : audioBuffer ? (
|
|
|
|
|
<>
|
|
|
|
|
{/* Waveform - takes maximum space */}
|
|
|
|
|
<div className="flex-1 flex flex-col p-4 overflow-hidden">
|
|
|
|
|
<Waveform
|
|
|
|
|
audioBuffer={audioBuffer}
|
|
|
|
|
currentTime={currentTime}
|
|
|
|
|
duration={duration}
|
|
|
|
|
onSeek={seek}
|
|
|
|
|
height={200}
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
scrollOffset={scrollOffset}
|
|
|
|
|
amplitudeScale={amplitudeScale}
|
|
|
|
|
selection={selection}
|
|
|
|
|
onSelectionChange={setSelection}
|
|
|
|
|
/>
|
2025-11-17 15:44:29 +01:00
|
|
|
|
2025-11-17 20:03:40 +01:00
|
|
|
{/* Horizontal scroll for zoomed waveform */}
|
|
|
|
|
{zoom > 1 && (
|
|
|
|
|
<div className="mt-4 space-y-2">
|
|
|
|
|
<label className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
Scroll Position
|
|
|
|
|
</label>
|
|
|
|
|
<Slider
|
|
|
|
|
value={scrollOffset}
|
|
|
|
|
onChange={setScrollOffset}
|
|
|
|
|
min={0}
|
|
|
|
|
max={Math.max(0, (800 * zoom) - 800)}
|
|
|
|
|
step={1}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Playback Controls - fixed at bottom */}
|
|
|
|
|
<div className="border-t border-border bg-card p-3">
|
|
|
|
|
<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}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex-1 flex items-center justify-center p-8 transition-colors cursor-pointer",
|
|
|
|
|
isDragging && "bg-primary/5 border-2 border-dashed border-primary"
|
2025-11-17 15:44:29 +01:00
|
|
|
)}
|
2025-11-17 20:03:40 +01:00
|
|
|
onDragEnter={handleDragEnter}
|
|
|
|
|
onDragLeave={handleDragLeave}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
onClick={handleDropZoneClick}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="audio/*"
|
|
|
|
|
onChange={handleFileInputChange}
|
|
|
|
|
className="hidden"
|
2025-11-17 17:08:31 +01:00
|
|
|
/>
|
2025-11-17 20:03:40 +01:00
|
|
|
<div className="max-w-md text-center space-y-4">
|
|
|
|
|
<p className="text-lg font-medium text-foreground">
|
|
|
|
|
{isDragging ? 'Drop audio file here' : 'No audio file loaded'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{isDragging
|
|
|
|
|
? 'Release to load the file'
|
|
|
|
|
: 'Click here or use the side panel to load an audio file, or drag and drop a file onto this area.'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Effect Parameter Dialog */}
|
|
|
|
|
<EffectParameterDialog
|
|
|
|
|
open={effectDialogOpen}
|
|
|
|
|
onClose={() => setEffectDialogOpen(false)}
|
|
|
|
|
effectType={effectDialogType}
|
|
|
|
|
onApply={handleEffectApply}
|
|
|
|
|
sampleRate={audioBuffer?.sampleRate}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Dynamics Parameter Dialog */}
|
|
|
|
|
<DynamicsParameterDialog
|
|
|
|
|
open={dynamicsDialogOpen}
|
|
|
|
|
onClose={() => setDynamicsDialogOpen(false)}
|
|
|
|
|
effectType={dynamicsDialogType}
|
|
|
|
|
onApply={handleDynamicsApply}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Time-Based Parameter Dialog */}
|
|
|
|
|
<TimeBasedParameterDialog
|
|
|
|
|
open={timeBasedDialogOpen}
|
|
|
|
|
onClose={() => setTimeBasedDialogOpen(false)}
|
|
|
|
|
effectType={timeBasedDialogType}
|
|
|
|
|
onApply={handleTimeBasedApply}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Advanced Parameter Dialog */}
|
|
|
|
|
<AdvancedParameterDialog
|
|
|
|
|
open={advancedDialogOpen}
|
|
|
|
|
onClose={() => setAdvancedDialogOpen(false)}
|
|
|
|
|
effectType={advancedDialogType}
|
|
|
|
|
onApply={handleAdvancedApply}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
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>
2025-11-17 15:32:00 +01:00
|
|
|
);
|
|
|
|
|
}
|