'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 { DelayParameters, ReverbParameters, ChorusParameters, FlangerParameters, PhaserParameters, } from '@/lib/audio/effects/time-based'; export type TimeBasedType = 'delay' | 'reverb' | 'chorus' | 'flanger' | 'phaser'; export type TimeBasedParameters = | (DelayParameters & { type: 'delay' }) | (ReverbParameters & { type: 'reverb' }) | (ChorusParameters & { type: 'chorus' }) | (FlangerParameters & { type: 'flanger' }) | (PhaserParameters & { type: 'phaser' }); export interface EffectPreset { name: string; parameters: Partial; } export interface TimeBasedParameterDialogProps { open: boolean; onClose: () => void; effectType: TimeBasedType; onApply: (params: TimeBasedParameters) => void; } const EFFECT_LABELS: Record = { delay: 'Delay/Echo', reverb: 'Reverb', chorus: 'Chorus', flanger: 'Flanger', phaser: 'Phaser', }; const EFFECT_DESCRIPTIONS: Record = { delay: 'Creates echo effects by repeating the audio signal', reverb: 'Simulates acoustic space and ambience', chorus: 'Thickens sound by adding modulated copies', flanger: 'Creates sweeping comb-filter effect', phaser: 'Creates a phase-shifting swoosh effect', }; const PRESETS: Record = { delay: [ { name: 'Short Slap', parameters: { time: 80, feedback: 0.2, mix: 0.3 } }, { name: 'Medium Echo', parameters: { time: 250, feedback: 0.4, mix: 0.4 } }, { name: 'Long Echo', parameters: { time: 500, feedback: 0.5, mix: 0.5 } }, { name: 'Ping Pong', parameters: { time: 375, feedback: 0.6, mix: 0.4 } }, ], reverb: [ { name: 'Small Room', parameters: { roomSize: 0.3, damping: 0.5, mix: 0.2 } }, { name: 'Medium Hall', parameters: { roomSize: 0.6, damping: 0.3, mix: 0.3 } }, { name: 'Large Hall', parameters: { roomSize: 0.8, damping: 0.2, mix: 0.4 } }, { name: 'Cathedral', parameters: { roomSize: 1.0, damping: 0.1, mix: 0.5 } }, ], chorus: [ { name: 'Subtle', parameters: { rate: 0.5, depth: 0.2, delay: 20, mix: 0.3 } }, { name: 'Classic', parameters: { rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 } }, { name: 'Deep', parameters: { rate: 1.5, depth: 0.7, delay: 30, mix: 0.6 } }, { name: 'Lush', parameters: { rate: 0.8, depth: 0.6, delay: 35, mix: 0.7 } }, ], flanger: [ { name: 'Subtle', parameters: { rate: 0.3, depth: 0.3, feedback: 0.2, delay: 2, mix: 0.4 } }, { name: 'Classic', parameters: { rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 } }, { name: 'Jet', parameters: { rate: 0.2, depth: 0.7, feedback: 0.6, delay: 1.5, mix: 0.6 } }, { name: 'Extreme', parameters: { rate: 1.0, depth: 0.8, feedback: 0.7, delay: 2.5, mix: 0.7 } }, ], phaser: [ { name: 'Gentle', parameters: { rate: 0.4, depth: 0.3, feedback: 0.2, stages: 4, mix: 0.4 } }, { name: 'Classic', parameters: { rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 } }, { name: 'Deep', parameters: { rate: 0.3, depth: 0.7, feedback: 0.5, stages: 8, mix: 0.6 } }, { name: 'Vintage', parameters: { rate: 0.5, depth: 0.6, feedback: 0.6, stages: 4, mix: 0.7 } }, ], }; export function TimeBasedParameterDialog({ open, onClose, effectType, onApply, }: TimeBasedParameterDialogProps) { const [parameters, setParameters] = React.useState(() => { if (effectType === 'delay') { return { type: 'delay', time: 250, feedback: 0.4, mix: 0.4 }; } else if (effectType === 'reverb') { return { type: 'reverb', roomSize: 0.6, damping: 0.3, mix: 0.3 }; } else if (effectType === 'chorus') { return { type: 'chorus', rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 }; } else if (effectType === 'flanger') { return { type: 'flanger', rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 }; } else { return { type: 'phaser', rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 }; } }); const canvasRef = React.useRef(null); // Get appropriate presets for this effect type const presets = PRESETS[effectType] || []; // Update parameters when effect type changes React.useEffect(() => { if (effectType === 'delay') { setParameters({ type: 'delay', time: 250, feedback: 0.4, mix: 0.4 }); } else if (effectType === 'reverb') { setParameters({ type: 'reverb', roomSize: 0.6, damping: 0.3, mix: 0.3 }); } else if (effectType === 'chorus') { setParameters({ type: 'chorus', rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 }); } else if (effectType === 'flanger') { setParameters({ type: 'flanger', rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 }); } else { setParameters({ type: 'phaser', rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 }); } }, [effectType]); // Draw visualization 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 canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; // Normalize coordinate system ctx.scale(dpr, dpr); // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); const width = rect.width; const height = rect.height; // Clear with background ctx.fillStyle = getComputedStyle(canvas).getPropertyValue('background-color') || '#1a1a1a'; ctx.fillRect(0, 0, width, height); if (effectType === 'delay') { // Draw delay echoes const delayParams = parameters as DelayParameters & { type: 'delay' }; const maxTime = 2000; // ms const echoCount = 5; ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)'; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); for (let i = 0; i <= 4; i++) { const x = (i / 4) * width; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } ctx.setLineDash([]); let gain = 1.0; for (let i = 0; i < echoCount; i++) { const x = (i * delayParams.time / maxTime) * width; const barHeight = height * gain * 0.8; const y = (height - barHeight) / 2; ctx.fillStyle = `rgba(59, 130, 246, ${gain})`; ctx.fillRect(x - 3, y, 6, barHeight); gain *= delayParams.feedback; if (gain < 0.01) break; } } else if (effectType === 'reverb') { // Draw reverb decay const reverbParams = parameters as ReverbParameters & { type: 'reverb' }; const decayTime = reverbParams.roomSize * 3000; // ms ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2; ctx.beginPath(); for (let x = 0; x < width; x++) { const time = (x / width) * 3000; const decay = Math.exp(-time / (decayTime * (1 - reverbParams.damping * 0.5))); const y = height / 2 + (height / 2 - 20) * (1 - decay); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); // Draw reference line ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)'; ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); ctx.setLineDash([]); } else { // Draw LFO waveform for chorus, flanger, phaser let rate = 1.0; let depth = 0.5; if (effectType === 'chorus') { const chorusParams = parameters as ChorusParameters & { type: 'chorus' }; rate = chorusParams.rate; depth = chorusParams.depth; } else if (effectType === 'flanger') { const flangerParams = parameters as FlangerParameters & { type: 'flanger' }; rate = flangerParams.rate; depth = flangerParams.depth; } else if (effectType === 'phaser') { const phaserParams = parameters as PhaserParameters & { type: 'phaser' }; rate = phaserParams.rate; depth = phaserParams.depth; } ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2; ctx.beginPath(); const cycles = rate * 2; // Show 2 seconds worth for (let x = 0; x < width; x++) { const phase = (x / width) * cycles * 2 * Math.PI; const lfo = Math.sin(phase); const y = height / 2 - (lfo * depth * height * 0.4); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); // Draw center line ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)'; ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); ctx.setLineDash([]); } }, [parameters, effectType]); const handleApply = () => { onApply(parameters); onClose(); }; const handlePresetClick = (preset: EffectPreset) => { setParameters((prev) => ({ ...prev, ...preset.parameters, })); }; return ( } >
{/* Visualization */}
{/* Presets */} {presets.length > 0 && (
{presets.map((preset) => ( ))}
)} {/* Effect-specific parameters */} {effectType === 'delay' && ( <> {/* Delay Time */}
setParameters((prev) => ({ ...prev, time: value })) } min={10} max={2000} step={10} className="w-full" />
{/* Feedback */}
setParameters((prev) => ({ ...prev, feedback: value })) } min={0} max={0.95} step={0.01} className="w-full" />
)} {effectType === 'reverb' && ( <> {/* Room Size */}
setParameters((prev) => ({ ...prev, roomSize: value })) } min={0.1} max={1} step={0.01} className="w-full" />
{/* Damping */}
setParameters((prev) => ({ ...prev, damping: value })) } min={0} max={1} step={0.01} className="w-full" />
)} {effectType === 'chorus' && ( <> {/* Rate */}
setParameters((prev) => ({ ...prev, rate: value })) } min={0.1} max={5} step={0.1} className="w-full" />
{/* Depth */}
setParameters((prev) => ({ ...prev, depth: value })) } min={0} max={1} step={0.01} className="w-full" />
{/* Base Delay */}
setParameters((prev) => ({ ...prev, delay: value })) } min={5} max={50} step={0.5} className="w-full" />
)} {effectType === 'flanger' && ( <> {/* Rate */}
setParameters((prev) => ({ ...prev, rate: value })) } min={0.1} max={5} step={0.1} className="w-full" />
{/* Depth */}
setParameters((prev) => ({ ...prev, depth: value })) } min={0} max={1} step={0.01} className="w-full" />
{/* Feedback */}
setParameters((prev) => ({ ...prev, feedback: value })) } min={0} max={0.95} step={0.01} className="w-full" />
{/* Base Delay */}
setParameters((prev) => ({ ...prev, delay: value })) } min={0.5} max={10} step={0.1} className="w-full" />
)} {effectType === 'phaser' && ( <> {/* Rate */}
setParameters((prev) => ({ ...prev, rate: value })) } min={0.1} max={5} step={0.1} className="w-full" />
{/* Depth */}
setParameters((prev) => ({ ...prev, depth: value })) } min={0} max={1} step={0.01} className="w-full" />
{/* Feedback */}
setParameters((prev) => ({ ...prev, feedback: value })) } min={0} max={0.95} step={0.01} className="w-full" />
{/* Stages */}
setParameters((prev) => ({ ...prev, stages: Math.floor(value) })) } min={2} max={12} step={1} className="w-full" />
)} {/* Mix (common to all) */}
setParameters((prev) => ({ ...prev, mix: value })) } min={0} max={1} step={0.01} className="w-full" />
0% (Dry) 100% (Wet)
); }