'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 type { PitchShifterParameters, TimeStretchParameters, DistortionParameters, BitcrusherParameters, } from '@/lib/audio/effects/advanced'; export type AdvancedType = 'pitch' | 'timestretch' | 'distortion' | 'bitcrusher'; export type AdvancedParameters = | (PitchShifterParameters & { type: 'pitch' }) | (BitcrusherParameters & { type: 'bitcrusher' }) | (TimeStretchParameters & { type: 'timestretch' }) | (DistortionParameters & { type: 'distortion' }); interface EffectPreset { name: string; parameters: T; } const PRESETS: Record = { pitch: [ { name: 'Octave Up', parameters: { semitones: 12, cents: 0, mix: 1.0 } }, { name: 'Fifth Up', parameters: { semitones: 7, cents: 0, mix: 1.0 } }, { name: 'Octave Down', parameters: { semitones: -12, cents: 0, mix: 1.0 } }, { name: 'Subtle Shift', parameters: { semitones: 2, cents: 0, mix: 0.5 } }, ], timestretch: [ { name: 'Half Speed', parameters: { rate: 0.5, preservePitch: true, mix: 1.0 } }, { name: 'Double Speed', parameters: { rate: 2.0, preservePitch: true, mix: 1.0 } }, { name: 'Slow Motion', parameters: { rate: 0.75, preservePitch: true, mix: 1.0 } }, { name: 'Fast Forward', parameters: { rate: 1.5, preservePitch: true, mix: 1.0 } }, ], distortion: [ { name: 'Light Overdrive', parameters: { drive: 0.3, tone: 0.7, output: 0.8, type: 'soft' as const, mix: 1.0 } }, { name: 'Heavy Distortion', parameters: { drive: 0.8, tone: 0.5, output: 0.6, type: 'hard' as const, mix: 1.0 } }, { name: 'Tube Warmth', parameters: { drive: 0.4, tone: 0.6, output: 0.75, type: 'tube' as const, mix: 0.8 } }, { name: 'Extreme Fuzz', parameters: { drive: 1.0, tone: 0.3, output: 0.5, type: 'hard' as const, mix: 1.0 } }, ], bitcrusher: [ { name: 'Lo-Fi', parameters: { bitDepth: 8, sampleRate: 8000, mix: 1.0 } }, { name: 'Telephone', parameters: { bitDepth: 4, sampleRate: 4000, mix: 1.0 } }, { name: 'Subtle Crunch', parameters: { bitDepth: 12, sampleRate: 22050, mix: 0.6 } }, { name: 'Extreme Crush', parameters: { bitDepth: 2, sampleRate: 2000, mix: 1.0 } }, ], }; const DEFAULT_PARAMS: Record = { pitch: { semitones: 0, cents: 0, mix: 1.0 }, timestretch: { rate: 1.0, preservePitch: true, mix: 1.0 }, distortion: { drive: 0.5, tone: 0.5, output: 0.7, type: 'soft', mix: 1.0 }, bitcrusher: { bitDepth: 8, sampleRate: 8000, mix: 1.0 }, }; const EFFECT_LABELS: Record = { pitch: 'Pitch Shifter', timestretch: 'Time Stretch', distortion: 'Distortion', bitcrusher: 'Bitcrusher', }; export interface AdvancedParameterDialogProps { open: boolean; onClose: () => void; effectType: AdvancedType; onApply: (params: AdvancedParameters) => void; } export function AdvancedParameterDialog({ open, onClose, effectType, onApply, }: AdvancedParameterDialogProps) { const [parameters, setParameters] = React.useState( DEFAULT_PARAMS[effectType] ); const canvasRef = React.useRef(null); // Reset parameters when effect type changes React.useEffect(() => { setParameters(DEFAULT_PARAMS[effectType]); }, [effectType]); // Draw visual feedback React.useEffect(() => { if (!open) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width; const height = canvas.height; // Clear canvas ctx.clearRect(0, 0, width, height); // Draw background ctx.fillStyle = 'rgb(15, 23, 42)'; ctx.fillRect(0, 0, width, height); // Draw visualization based on effect type ctx.strokeStyle = 'rgb(59, 130, 246)'; ctx.lineWidth = 2; if (effectType === 'pitch') { const pitchParams = parameters as PitchShifterParameters; const totalCents = (pitchParams.semitones ?? 0) * 100 + (pitchParams.cents ?? 0); const pitchRatio = Math.pow(2, totalCents / 1200); // Draw waveform with pitch shift ctx.beginPath(); for (let x = 0; x < width; x++) { const t = (x / width) * 4 * Math.PI * pitchRatio; const y = height / 2 + Math.sin(t) * (height / 3); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); // Draw reference waveform ctx.strokeStyle = 'rgba(148, 163, 184, 0.3)'; ctx.beginPath(); for (let x = 0; x < width; x++) { const t = (x / width) * 4 * Math.PI; const y = height / 2 + Math.sin(t) * (height / 3); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } else if (effectType === 'timestretch') { const stretchParams = parameters as TimeStretchParameters; // Draw time-stretched waveform ctx.beginPath(); for (let x = 0; x < width; x++) { const t = (x / width) * 4 * Math.PI / (stretchParams.rate ?? 1.0); const y = height / 2 + Math.sin(t) * (height / 3); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } else if (effectType === 'distortion') { const distParams = parameters as DistortionParameters; // Draw distorted waveform ctx.beginPath(); for (let x = 0; x < width; x++) { const t = (x / width) * 4 * Math.PI; let sample = Math.sin(t); // Apply distortion const drive = 1 + (distParams.drive ?? 0.5) * 10; sample *= drive; const distType = distParams.type ?? 'soft'; if (distType === 'soft') { sample = Math.tanh(sample); } else if (distType === 'hard') { sample = Math.max(-1, Math.min(1, sample)); } else { sample = sample > 0 ? 1 - Math.exp(-sample) : -1 + Math.exp(sample); } const y = height / 2 - sample * (height / 3); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } else if (effectType === 'bitcrusher') { const crushParams = parameters as BitcrusherParameters; const bitLevels = Math.pow(2, crushParams.bitDepth ?? 8); const step = 2 / bitLevels; // Draw bitcrushed waveform ctx.beginPath(); let lastY = height / 2; for (let x = 0; x < width; x++) { const t = (x / width) * 4 * Math.PI; let sample = Math.sin(t); // Quantize sample = Math.floor(sample / step) * step; const y = height / 2 - sample * (height / 3); // Sample and hold effect if (x % Math.max(1, Math.floor(width / ((crushParams.sampleRate ?? 8000) / 1000))) === 0) { lastY = y; } if (x === 0) ctx.moveTo(x, lastY); else ctx.lineTo(x, lastY); } ctx.stroke(); } // Draw center line ctx.strokeStyle = 'rgba(148, 163, 184, 0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); }, [parameters, effectType, open]); const handleApply = () => { onApply({ ...parameters, type: effectType } as AdvancedParameters); onClose(); }; const handlePreset = (preset: EffectPreset) => { setParameters(preset.parameters); }; return (
{/* Visual Feedback */}
{/* Presets */}
{PRESETS[effectType].map((preset) => ( ))}
{/* Effect-specific parameters */} {effectType === 'pitch' && ( <>
setParameters({ ...parameters, semitones: value }) } min={-12} max={12} step={1} />
setParameters({ ...parameters, cents: value }) } min={-100} max={100} step={1} />
)} {effectType === 'timestretch' && ( <>
setParameters({ ...parameters, rate: value }) } min={0.5} max={2.0} step={0.1} />
setParameters({ ...parameters, preservePitch: e.target.checked }) } className="h-4 w-4 rounded border-border" />
)} {effectType === 'distortion' && ( <>
{(['soft', 'hard', 'tube'] as const).map((type) => ( ))}
setParameters({ ...parameters, drive: value }) } min={0} max={1} step={0.01} />
setParameters({ ...parameters, tone: value }) } min={0} max={1} step={0.01} />
setParameters({ ...parameters, output: value }) } min={0} max={1} step={0.01} />
)} {effectType === 'bitcrusher' && ( <>
setParameters({ ...parameters, bitDepth: Math.round(value) }) } min={1} max={16} step={1} />
setParameters({ ...parameters, sampleRate: Math.round(value) }) } min={100} max={48000} step={100} />
)} {/* Mix control */}
setParameters({ ...parameters, mix: value }) } min={0} max={1} step={0.01} />
{/* Actions */}
); }