Fixed "Cannot read properties of undefined (reading 'toFixed')" errors in TimeBasedParameterDialog and DynamicsParameterDialog by adding nullish coalescing operators with default values to all parameter accesses. This prevents errors when loading presets that have partial parameter sets. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
674 lines
24 KiB
TypeScript
674 lines
24 KiB
TypeScript
'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<DelayParameters | ReverbParameters | ChorusParameters | FlangerParameters | PhaserParameters>;
|
|
}
|
|
|
|
export interface TimeBasedParameterDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
effectType: TimeBasedType;
|
|
onApply: (params: TimeBasedParameters) => void;
|
|
}
|
|
|
|
const EFFECT_LABELS: Record<TimeBasedType, string> = {
|
|
delay: 'Delay/Echo',
|
|
reverb: 'Reverb',
|
|
chorus: 'Chorus',
|
|
flanger: 'Flanger',
|
|
phaser: 'Phaser',
|
|
};
|
|
|
|
const EFFECT_DESCRIPTIONS: Record<TimeBasedType, string> = {
|
|
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<TimeBasedType, EffectPreset[]> = {
|
|
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<TimeBasedParameters>(() => {
|
|
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<HTMLCanvasElement>(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 (
|
|
<Modal
|
|
open={open}
|
|
onClose={onClose}
|
|
title={EFFECT_LABELS[effectType] || 'Time-Based Effect'}
|
|
description={EFFECT_DESCRIPTIONS[effectType]}
|
|
size="lg"
|
|
footer={
|
|
<>
|
|
<Button variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleApply}>
|
|
Apply Effect
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="space-y-4">
|
|
{/* Visualization */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
{effectType === 'delay' && 'Echo Pattern'}
|
|
{effectType === 'reverb' && 'Reverb Decay'}
|
|
{(effectType === 'chorus' || effectType === 'flanger' || effectType === 'phaser') && 'Modulation (LFO)'}
|
|
</label>
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-32 border border-border rounded bg-background"
|
|
/>
|
|
</div>
|
|
|
|
{/* Presets */}
|
|
{presets.length > 0 && (
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
Presets
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{presets.map((preset) => (
|
|
<Button
|
|
key={preset.name}
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePresetClick(preset)}
|
|
className="justify-start"
|
|
>
|
|
{preset.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Effect-specific parameters */}
|
|
{effectType === 'delay' && (
|
|
<>
|
|
{/* Delay Time */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Delay Time</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as DelayParameters).time ?? 250).toFixed(0)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as DelayParameters).time ?? 250)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, time: value }))
|
|
}
|
|
min={10}
|
|
max={2000}
|
|
step={10}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as DelayParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as DelayParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'reverb' && (
|
|
<>
|
|
{/* Room Size */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Room Size</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ReverbParameters).roomSize ?? 0.6) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ReverbParameters).roomSize ?? 0.6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, roomSize: value }))
|
|
}
|
|
min={0.1}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Damping */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Damping</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ReverbParameters).damping ?? 0.3) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ReverbParameters).damping ?? 0.3)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, damping: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'chorus' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as ChorusParameters).rate ?? 1.0).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).rate ?? 1.0)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ChorusParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Base Delay */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Base Delay</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as ChorusParameters).delay ?? 25).toFixed(1)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).delay ?? 25)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, delay: value }))
|
|
}
|
|
min={5}
|
|
max={50}
|
|
step={0.5}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'flanger' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as FlangerParameters).rate ?? 0.5).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).rate ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as FlangerParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as FlangerParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Base Delay */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Base Delay</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as FlangerParameters).delay ?? 3).toFixed(1)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).delay ?? 3)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, delay: value }))
|
|
}
|
|
min={0.5}
|
|
max={10}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'phaser' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as PhaserParameters).rate ?? 0.6).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).rate ?? 0.6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as PhaserParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as PhaserParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Stages */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Stages</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as PhaserParameters).stages ?? 6).toFixed(0)}
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).stages ?? 6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, stages: Math.floor(value) }))
|
|
}
|
|
min={2}
|
|
max={12}
|
|
step={1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Mix (common to all) */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Mix (Dry/Wet)</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters.mix ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[(parameters.mix ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, mix: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>0% (Dry)</span>
|
|
<span>100% (Wet)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|