'use client'; import * as React from 'react'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { Slider } from '@/components/ui/Slider'; import { cn } from '@/lib/utils/cn'; import type { FilterType } from '@/lib/audio/effects/filters'; export interface FilterParameters { type: FilterType; frequency: number; Q?: number; gain?: number; } export interface EffectPreset { name: string; parameters: Partial; } export interface EffectParameterDialogProps { open: boolean; onClose: () => void; effectType: 'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'lowshelf' | 'highshelf' | 'peaking'; onApply: (params: FilterParameters) => void; sampleRate?: number; } const EFFECT_LABELS: Record = { 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', }; const EFFECT_DESCRIPTIONS: Record = { lowpass: 'Removes high frequencies above the cutoff', highpass: 'Removes low frequencies below the cutoff', bandpass: 'Isolates frequencies around the center frequency', notch: 'Removes frequencies around the center frequency', lowshelf: 'Boosts or cuts low frequencies', highshelf: 'Boosts or cuts high frequencies', peaking: 'Boosts or cuts a specific frequency band', }; const PRESETS: Record = { lowpass: [ { name: 'Telephone', parameters: { frequency: 3000, Q: 0.7 } }, { name: 'Radio', parameters: { frequency: 5000, Q: 1.0 } }, { name: 'Warm', parameters: { frequency: 8000, Q: 0.5 } }, { name: 'Muffled', parameters: { frequency: 1000, Q: 1.5 } }, ], highpass: [ { name: 'Rumble Removal', parameters: { frequency: 80, Q: 0.7 } }, { name: 'Voice Clarity', parameters: { frequency: 150, Q: 1.0 } }, { name: 'Thin', parameters: { frequency: 300, Q: 0.5 } }, ], bandpass: [ { name: 'Telephone', parameters: { frequency: 1000, Q: 2.0 } }, { name: 'Vocal Range', parameters: { frequency: 2000, Q: 1.0 } }, { name: 'Narrow', parameters: { frequency: 1000, Q: 10.0 } }, ], notch: [ { name: '60Hz Hum', parameters: { frequency: 60, Q: 10.0 } }, { name: '50Hz Hum', parameters: { frequency: 50, Q: 10.0 } }, { name: 'Narrow Notch', parameters: { frequency: 1000, Q: 20.0 } }, ], lowshelf: [ { name: 'Bass Boost', parameters: { frequency: 200, gain: 6 } }, { name: 'Bass Cut', parameters: { frequency: 200, gain: -6 } }, { name: 'Warmth', parameters: { frequency: 150, gain: 3 } }, ], highshelf: [ { name: 'Treble Boost', parameters: { frequency: 3000, gain: 6 } }, { name: 'Treble Cut', parameters: { frequency: 3000, gain: -6 } }, { name: 'Brightness', parameters: { frequency: 5000, gain: 3 } }, ], peaking: [ { name: 'Presence Boost', parameters: { frequency: 3000, Q: 1.0, gain: 4 } }, { name: 'Vocal Cut', parameters: { frequency: 2000, Q: 2.0, gain: -3 } }, { name: 'Narrow Boost', parameters: { frequency: 1000, Q: 5.0, gain: 6 } }, ], }; export function EffectParameterDialog({ open, onClose, effectType, onApply, sampleRate = 48000, }: EffectParameterDialogProps) { const [parameters, setParameters] = React.useState(() => ({ type: effectType, frequency: effectType === 'lowpass' ? 1000 : effectType === 'highpass' ? 100 : 1000, Q: 1.0, gain: 0, })); const canvasRef = React.useRef(null); // Get appropriate presets for this effect type const presets = PRESETS[effectType] || []; // Update parameters when effect type changes React.useEffect(() => { setParameters((prev) => ({ ...prev, type: effectType })); }, [effectType]); // Draw frequency response curve React.useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Get actual dimensions const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; // Set actual size in memory (scaled to account for extra pixel density) canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; // Normalize coordinate system to use CSS pixels ctx.scale(dpr, dpr); const width = rect.width; const height = rect.height; const nyquist = sampleRate / 2; // Clear canvas ctx.fillStyle = getComputedStyle(canvas).getPropertyValue('background-color') || '#1a1a1a'; ctx.fillRect(0, 0, width, height); // Draw grid ctx.strokeStyle = 'rgba(128, 128, 128, 0.2)'; ctx.lineWidth = 1; // Horizontal grid lines (dB) for (let db = -24; db <= 24; db += 6) { const y = height / 2 - (db / 24) * (height / 2); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // Vertical grid lines (frequency) const frequencies = [100, 1000, 10000]; frequencies.forEach((freq) => { const x = (Math.log10(freq) - 1) / (Math.log10(nyquist) - 1) * width; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); }); // Draw frequency response curve ctx.strokeStyle = '#3b82f6'; // Primary blue ctx.lineWidth = 2; ctx.beginPath(); for (let x = 0; x < width; x++) { const freq = Math.pow(10, 1 + (x / width) * (Math.log10(nyquist) - 1)); const magnitude = getFilterMagnitude(freq, parameters, sampleRate); const db = 20 * Math.log10(Math.max(magnitude, 0.0001)); // Prevent log(0) const y = height / 2 - (db / 24) * (height / 2); if (x === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.stroke(); // Draw 0dB line ctx.strokeStyle = 'rgba(156, 163, 175, 0.5)'; // Muted foreground ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); ctx.setLineDash([]); }, [parameters, sampleRate]); const handleApply = () => { onApply(parameters); onClose(); }; const handlePresetClick = (preset: EffectPreset) => { setParameters((prev) => ({ ...prev, ...preset.parameters, })); }; const needsQ = ['lowpass', 'highpass', 'bandpass', 'notch', 'peaking'].includes(effectType); const needsGain = ['lowshelf', 'highshelf', 'peaking'].includes(effectType); return ( } >
{/* Frequency Response Visualization */}
100 Hz 1 kHz 10 kHz
{/* Presets */} {presets.length > 0 && (
{presets.map((preset) => ( ))}
)} {/* Frequency Parameter */}
setParameters((prev) => ({ ...prev, frequency: Math.pow(10, value) })) } min={1} max={Math.log10(sampleRate / 2)} step={0.01} className="w-full" />
10 Hz {(sampleRate / 2).toFixed(0)} Hz
{/* Q Parameter */} {needsQ && (
setParameters((prev) => ({ ...prev, Q: value })) } min={0.1} max={20} step={0.1} className="w-full" />
0.1 (Gentle) 20 (Sharp)
)} {/* Gain Parameter */} {needsGain && (
setParameters((prev) => ({ ...prev, gain: value })) } min={-24} max={24} step={0.5} className="w-full" />
-24 dB +24 dB
)}
); } /** * Calculate filter magnitude at a given frequency */ function getFilterMagnitude( freq: number, params: FilterParameters, sampleRate: number ): number { const w = (2 * Math.PI * freq) / sampleRate; const w0 = (2 * Math.PI * params.frequency) / sampleRate; const Q = params.Q || 1.0; const gain = params.gain || 0; const A = Math.pow(10, gain / 40); // Simplified magnitude calculation for different filter types switch (params.type) { case 'lowpass': { const ratio = freq / params.frequency; return 1 / Math.sqrt(1 + Math.pow(ratio * Q, 2 * 2)); } case 'highpass': { const ratio = params.frequency / freq; return 1 / Math.sqrt(1 + Math.pow(ratio * Q, 2 * 2)); } case 'bandpass': { const ratio = Math.abs(freq - params.frequency) / (params.frequency / Q); return 1 / Math.sqrt(1 + Math.pow(ratio, 2)); } case 'notch': { const ratio = Math.abs(freq - params.frequency) / (params.frequency / Q); return Math.abs(ratio) / Math.sqrt(1 + Math.pow(ratio, 2)); } case 'lowshelf': case 'highshelf': case 'peaking': { // Simplified for visualization const dist = Math.abs(Math.log(freq / params.frequency)); const influence = Math.exp(-dist * Q); return 1 + (A - 1) * influence; } default: return 1; } }