Files
audio-ui/components/layout/SidePanel.tsx
Sebastian Krüger ee48f9475f feat: add advanced audio effects and improve UI
Phase 6.5 Advanced Effects:
- Add Pitch Shifter with semitones and cents adjustment
- Add Time Stretch with pitch preservation using overlap-add
- Add Distortion with soft/hard/tube types and tone control
- Add Bitcrusher with bit depth and sample rate reduction
- Add AdvancedParameterDialog with real-time waveform visualization
- Add 4 professional presets per effect type

Improvements:
- Fix undefined parameter errors by adding nullish coalescing operators
- Add global custom scrollbar styling with color-mix transparency
- Add custom-scrollbar utility class for side panel
- Improve theme-aware scrollbar appearance in light/dark modes
- Fix parameter initialization when switching effect types

Integration:
- All advanced effects support undo/redo via EffectCommand
- Effects accessible via command palette and side panel
- Selection-based processing support
- Toast notifications for all effects

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 20:03:40 +01:00

516 lines
16 KiB
TypeScript

'use client';
import * as React from 'react';
import {
FileAudio,
History,
Info,
ChevronLeft,
ChevronRight,
Upload,
Download,
X,
Sparkles,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import { formatDuration } from '@/lib/audio/decoder';
import type { Selection } from '@/types/selection';
import type { HistoryState } from '@/lib/history/history-manager';
export interface SidePanelProps {
// File info
fileName: string | null;
audioBuffer: AudioBuffer | null;
onFileSelect: (file: File) => void;
onClear: () => void;
// Selection info
selection: Selection | null;
// History info
historyState: HistoryState;
// Effects handlers
onNormalize: () => void;
onFadeIn: () => void;
onFadeOut: () => void;
onReverse: () => void;
onLowPassFilter: () => void;
onHighPassFilter: () => void;
onBandPassFilter: () => void;
onCompressor: () => void;
onLimiter: () => void;
onGate: () => void;
onDelay: () => void;
onReverb: () => void;
onChorus: () => void;
onFlanger: () => void;
onPhaser: () => void;
onPitchShift: () => void;
onTimeStretch: () => void;
onDistortion: () => void;
onBitcrusher: () => void;
className?: string;
}
export function SidePanel({
fileName,
audioBuffer,
onFileSelect,
onClear,
selection,
historyState,
onNormalize,
onFadeIn,
onFadeOut,
onReverse,
onLowPassFilter,
onHighPassFilter,
onBandPassFilter,
onCompressor,
onLimiter,
onGate,
onDelay,
onReverb,
onChorus,
onFlanger,
onPhaser,
onPitchShift,
onTimeStretch,
onDistortion,
onBitcrusher,
className,
}: SidePanelProps) {
const [isCollapsed, setIsCollapsed] = React.useState(false);
const [activeTab, setActiveTab] = React.useState<'file' | 'history' | 'info' | 'effects'>('file');
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleFileClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onFileSelect(file);
}
};
if (isCollapsed) {
return (
<div
className={cn(
'w-12 bg-card border-r border-border flex flex-col items-center py-2',
className
)}
>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsCollapsed(false)}
title="Expand Side Panel"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
}
return (
<div className={cn('w-64 bg-card border-r border-border flex flex-col', className)}>
{/* Header */}
<div className="flex items-center justify-between p-2 border-b border-border">
<div className="flex items-center gap-1">
<Button
variant={activeTab === 'file' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('file')}
title="File"
>
<FileAudio className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'effects' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('effects')}
title="Effects"
>
<Sparkles className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'history' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('history')}
title="History"
>
<History className="h-4 w-4" />
</Button>
<Button
variant={activeTab === 'info' ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={() => setActiveTab('info')}
title="Info"
>
<Info className="h-4 w-4" />
</Button>
</div>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsCollapsed(true)}
title="Collapse Side Panel"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-3 space-y-3 custom-scrollbar">
{activeTab === 'file' && (
<>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Audio File
</h3>
{audioBuffer ? (
<div className="space-y-2">
<div className="p-2 bg-secondary/30 rounded text-xs">
<div className="font-medium text-foreground truncate" title={fileName || 'Unknown'}>
{fileName || 'Unknown'}
</div>
<div className="text-muted-foreground mt-1">
Duration: {formatDuration(audioBuffer.duration)}
</div>
<div className="text-muted-foreground">
Channels: {audioBuffer.numberOfChannels}
</div>
<div className="text-muted-foreground">
Sample Rate: {audioBuffer.sampleRate} Hz
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={onClear}
className="w-full"
>
<X className="h-3.5 w-3.5 mr-1.5" />
Clear File
</Button>
</div>
) : (
<div className="space-y-2">
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={handleFileChange}
className="hidden"
/>
<Button
variant="outline"
size="sm"
onClick={handleFileClick}
className="w-full"
>
<Upload className="h-3.5 w-3.5 mr-1.5" />
Load Audio File
</Button>
<div className="text-xs text-muted-foreground">
Or drag and drop an audio file onto the waveform area.
</div>
</div>
)}
</div>
</>
)}
{activeTab === 'history' && (
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Edit History
</h3>
{historyState.historySize > 0 ? (
<div className="space-y-1 text-xs">
<div className="p-2 bg-secondary/30 rounded">
<div className="text-foreground">
{historyState.historySize} action{historyState.historySize !== 1 ? 's' : ''}
</div>
{historyState.undoDescription && (
<div className="text-muted-foreground mt-1">
Next undo: {historyState.undoDescription}
</div>
)}
{historyState.redoDescription && (
<div className="text-muted-foreground mt-1">
Next redo: {historyState.redoDescription}
</div>
)}
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
No history available. Edit operations will appear here.
</div>
)}
</div>
)}
{activeTab === 'info' && (
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Selection Info
</h3>
{selection ? (
<div className="p-2 bg-secondary/30 rounded text-xs">
<div className="text-foreground font-medium">Selection Active</div>
<div className="text-muted-foreground mt-1">
Duration: {formatDuration(selection.end - selection.start)}
</div>
<div className="text-muted-foreground">
Start: {formatDuration(selection.start)}
</div>
<div className="text-muted-foreground">
End: {formatDuration(selection.end)}
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
No selection. Drag on the waveform to select a region.
</div>
)}
</div>
)}
{activeTab === 'effects' && (
<div className="space-y-3">
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Basic Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onNormalize}
className="w-full justify-start text-xs"
>
Normalize
</Button>
<Button
variant="outline"
size="sm"
onClick={onFadeIn}
className="w-full justify-start text-xs"
>
Fade In
</Button>
<Button
variant="outline"
size="sm"
onClick={onFadeOut}
className="w-full justify-start text-xs"
>
Fade Out
</Button>
<Button
variant="outline"
size="sm"
onClick={onReverse}
className="w-full justify-start text-xs"
>
Reverse
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply effects.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Filters
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onLowPassFilter}
className="w-full justify-start text-xs"
>
Low-Pass Filter
</Button>
<Button
variant="outline"
size="sm"
onClick={onHighPassFilter}
className="w-full justify-start text-xs"
>
High-Pass Filter
</Button>
<Button
variant="outline"
size="sm"
onClick={onBandPassFilter}
className="w-full justify-start text-xs"
>
Band-Pass Filter
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply filters.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Dynamics Processing
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onCompressor}
className="w-full justify-start text-xs"
>
Compressor
</Button>
<Button
variant="outline"
size="sm"
onClick={onLimiter}
className="w-full justify-start text-xs"
>
Limiter
</Button>
<Button
variant="outline"
size="sm"
onClick={onGate}
className="w-full justify-start text-xs"
>
Gate/Expander
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply dynamics processing.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Time-Based Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onDelay}
className="w-full justify-start text-xs"
>
Delay/Echo
</Button>
<Button
variant="outline"
size="sm"
onClick={onReverb}
className="w-full justify-start text-xs"
>
Reverb
</Button>
<Button
variant="outline"
size="sm"
onClick={onChorus}
className="w-full justify-start text-xs"
>
Chorus
</Button>
<Button
variant="outline"
size="sm"
onClick={onFlanger}
className="w-full justify-start text-xs"
>
Flanger
</Button>
<Button
variant="outline"
size="sm"
onClick={onPhaser}
className="w-full justify-start text-xs"
>
Phaser
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply time-based effects.
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
Advanced Effects
</h3>
{audioBuffer ? (
<div className="space-y-1.5">
<Button
variant="outline"
size="sm"
onClick={onPitchShift}
className="w-full justify-start text-xs"
>
Pitch Shifter
</Button>
<Button
variant="outline"
size="sm"
onClick={onTimeStretch}
className="w-full justify-start text-xs"
>
Time Stretch
</Button>
<Button
variant="outline"
size="sm"
onClick={onDistortion}
className="w-full justify-start text-xs"
>
Distortion
</Button>
<Button
variant="outline"
size="sm"
onClick={onBitcrusher}
className="w-full justify-start text-xs"
>
Bitcrusher
</Button>
</div>
) : (
<div className="text-xs text-muted-foreground">
Load an audio file to apply advanced effects.
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}