feat: add MP3 and FLAC export formats

Implemented Phase 11.1 export format support:
- Added MP3 export using lamejs library
- Added FLAC export using fflate DEFLATE compression
- Updated ExportDialog with format selector and format-specific options
  - MP3: bitrate selector (128/192/256/320 kbps)
  - FLAC: compression quality slider (0-9)
  - WAV: bit depth selector (16/24/32-bit)
- Updated AudioEditor to route export based on selected format
- Created TypeScript declarations for lamejs
- Fixed AudioStatistics to use audioBuffer instead of buffer property

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 02:14:32 +01:00
parent 1c56e596b5
commit 6577d9f27b
7 changed files with 265 additions and 44 deletions

View File

@@ -6,8 +6,10 @@ import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
export interface ExportSettings {
format: 'wav';
format: 'wav' | 'mp3' | 'flac';
bitDepth: 16 | 24 | 32;
bitrate: number; // For MP3: 128, 192, 256, 320 kbps
quality: number; // For FLAC: 0-9
normalize: boolean;
filename: string;
}
@@ -23,6 +25,8 @@ export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDia
const [settings, setSettings] = React.useState<ExportSettings>({
format: 'wav',
bitDepth: 16,
bitrate: 192, // Default MP3 bitrate
quality: 6, // Default FLAC quality
normalize: true,
filename: 'mix',
});
@@ -62,7 +66,9 @@ export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDia
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isExporting}
/>
<p className="text-xs text-muted-foreground mt-1">.wav will be added automatically</p>
<p className="text-xs text-muted-foreground mt-1">
.{settings.format} will be added automatically
</p>
</div>
{/* Format */}
@@ -72,37 +78,92 @@ export function ExportDialog({ open, onClose, onExport, isExporting }: ExportDia
</label>
<select
value={settings.format}
onChange={(e) => setSettings({ ...settings, format: e.target.value as 'wav' })}
onChange={(e) => setSettings({ ...settings, format: e.target.value as 'wav' | 'mp3' | 'flac' })}
className="w-full px-3 py-2 bg-background border border-border rounded text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isExporting}
>
<option value="wav">WAV (Uncompressed)</option>
<option value="mp3">MP3 (Lossy)</option>
<option value="flac">FLAC (Lossless)</option>
</select>
</div>
{/* Bit Depth */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Bit Depth
</label>
<div className="flex gap-2">
{[16, 24, 32].map((depth) => (
<button
key={depth}
onClick={() => setSettings({ ...settings, bitDepth: depth as 16 | 24 | 32 })}
className={cn(
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
settings.bitDepth === depth
? 'bg-primary text-primary-foreground'
: 'bg-background border border-border text-foreground hover:bg-accent'
)}
disabled={isExporting}
>
{depth}-bit {depth === 32 && '(Float)'}
</button>
))}
{/* Bit Depth (WAV and FLAC only) */}
{(settings.format === 'wav' || settings.format === 'flac') && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Bit Depth
</label>
<div className="flex gap-2">
{[16, 24, 32].map((depth) => (
<button
key={depth}
onClick={() => setSettings({ ...settings, bitDepth: depth as 16 | 24 | 32 })}
className={cn(
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
settings.bitDepth === depth
? 'bg-primary text-primary-foreground'
: 'bg-background border border-border text-foreground hover:bg-accent'
)}
disabled={isExporting}
>
{depth}-bit {depth === 32 && '(Float)'}
</button>
))}
</div>
</div>
</div>
)}
{/* MP3 Bitrate */}
{settings.format === 'mp3' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Bitrate
</label>
<div className="flex gap-2">
{[128, 192, 256, 320].map((rate) => (
<button
key={rate}
onClick={() => setSettings({ ...settings, bitrate: rate })}
className={cn(
'flex-1 px-3 py-2 rounded text-sm font-medium transition-colors',
settings.bitrate === rate
? 'bg-primary text-primary-foreground'
: 'bg-background border border-border text-foreground hover:bg-accent'
)}
disabled={isExporting}
>
{rate} kbps
</button>
))}
</div>
</div>
)}
{/* FLAC Quality */}
{settings.format === 'flac' && (
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Compression Quality
</label>
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">Fast</span>
<input
type="range"
min="0"
max="9"
value={settings.quality}
onChange={(e) => setSettings({ ...settings, quality: parseInt(e.target.value) })}
className="flex-1"
disabled={isExporting}
/>
<span className="text-xs text-muted-foreground">Small</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Level {settings.quality} (Higher = smaller file, slower encoding)
</p>
</div>
)}
{/* Normalize */}
<div>