diff --git a/src/components/AISongModal.tsx b/src/components/AISongModal.tsx index 8298c9c3..bb97ebac 100644 --- a/src/components/AISongModal.tsx +++ b/src/components/AISongModal.tsx @@ -1154,6 +1154,10 @@ export function AISongModal({ isOpen, onClose, onImport, onShowToast, audioEngin ) : null} +
+ {trackStatisticsRows.length > 0 && (trackStatisticsRows as React.ReactNode)} +
+ ) : null} {/* Automation Visualization */} diff --git a/src/components/SamplerVoicePanel.tsx b/src/components/SamplerVoicePanel.tsx index 9af81958..073f60ec 100644 --- a/src/components/SamplerVoicePanel.tsx +++ b/src/components/SamplerVoicePanel.tsx @@ -40,6 +40,418 @@ const midiToNote = (midi: number) => { }; +// Vertical Knob Component (for pitch envelope) +const VerticalKnob: React.FC<{ + label: string; + value: number; // 0-1 + onChange: (value: number) => void; + colorHex: [number, number, number]; +}> = ({ label, value, onChange, colorHex }) => { + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + const startY = e.clientY; + const startVal = value; + + const handleMouseMove = (e: MouseEvent) => { + const dy = startY - e.clientY; + const newVal = Math.max(0, Math.min(1, startVal + dy * 0.01)); + onChange(newVal); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = 'default'; + }; + + document.body.style.cursor = 'ns-resize'; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }, [value, onChange]); + + const color = `rgba(${colorHex[0] * 255}, ${colorHex[1] * 255}, ${colorHex[2] * 255}, 1)`; + const height = 40; + const fillHeight = value * height; + + return ( +
+ {label} +
+ {/* Bevel highlight */} +
+ {/* Fill with gradient */} +
+ {/* Center marker with LED style */} +
+ {/* Tick marks */} + {[0.25, 0.5, 0.75].map(tick => ( +
+ ))} +
+ {Math.round(value * 100)}% +
+ ); +}; + +// Horizontal Slider Component +const HSlider: React.FC<{ + label: string; + value: number; // -1 to 1 normalized + displayValue: string; + onChange: (value: number) => void; + colorHex: [number, number, number]; +}> = ({ label, value, displayValue, onChange, colorHex }) => { + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + const rect = (e.target as HTMLElement).parentElement?.getBoundingClientRect(); + if (!rect) return; + + const handleMouseMove = (e: MouseEvent) => { + const x = e.clientX - rect.left; + const normalized = Math.max(-1, Math.min(1, (x / rect.width) * 2 - 1)); + onChange(normalized); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = 'default'; + }; + + document.body.style.cursor = 'ew-resize'; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + // Initial set + const x = e.clientX - rect.left; + const normalized = Math.max(-1, Math.min(1, (x / rect.width) * 2 - 1)); + onChange(normalized); + }, [onChange]); + + const color = `rgba(${colorHex[0] * 255}, ${colorHex[1] * 255}, ${colorHex[2] * 255}, 1)`; + const percent = ((value + 1) / 2) * 100; + + return ( +
+
+ {label} + {displayValue} +
+
+ {/* Track background with gradient */} +
+ {/* Center line with LED glow */} +
+ {/* Fill from center with glow */} +
0 ? `${100 - percent}%` : '50%', + background: `linear-gradient(to ${value < 0 ? 'left' : 'right'}, ${color}40 0%, ${color} 100%)`, + boxShadow: `0 0 12px ${color}50, inset 0 1px 0 rgba(255,255,255,0.1)` + }} + /> + {/* Thumb with plastic look */} +
+ {/* Thumb highlight */} +
+
+
+
+ ); +}; + +// Harmonizer Popover Component +const HarmonizerPopover: React.FC<{ + isOpen: boolean; + onClose: () => void; + config: HarmonizerConfig; + isActive: boolean; + onApply: (config: HarmonizerConfig, isActive: boolean) => void; + colorHex: [number, number, number]; +}> = ({ isOpen, onClose, config, isActive, onApply, colorHex }) => { + const [localConfig, setLocalConfig] = useState(config); + const [localActive, setLocalActive] = useState(isActive); + + if (!isOpen) return null; + + const color = `rgba(${colorHex[0] * 255}, ${colorHex[1] * 255}, ${colorHex[2] * 255}, 1)`; + + const handleVoiceCountChange = (count: 2 | 3 | 4) => { + setLocalConfig(prev => ({ ...prev, voiceCount: count })); + }; + + const handleHarmonyTypeChange = (type: HarmonyType) => { + setLocalConfig(prev => ({ ...prev, harmonyType: type })); + }; + + const handleDetuneChange = (value: number) => { + setLocalConfig(prev => ({ ...prev, detuneSpread: Math.round(value * 50) })); + }; + + const handleFormantChange = (value: number) => { + setLocalConfig(prev => ({ ...prev, formantSpread: Math.round(value * 12) })); + }; + + const handleApply = () => { + onApply(localConfig, localActive); + onClose(); + }; + + const harmonyTypes: { value: HarmonyType; label: string }[] = [ + { value: 'octave', label: 'OCTAVE' }, + { value: 'fifth', label: 'FIFTH' }, + { value: 'third', label: 'THIRD' }, + { value: 'cluster', label: 'CLUSTER' }, + { value: 'custom', label: 'CUSTOM' } + ]; + + return ( + <> + {/* Backdrop */} +
+ + {/* Popover with hardware panel aesthetic */} +
+ {/* Screw corners */} +
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Header with holographic gradient */} +
+ {/* Holographic header background */} +
+ + + HARMONIZER + + {/* Toggle switch style ON/OFF button */} + +
+ + {/* Content */} +
+ {/* Voice Count - Toggle switch style */} +
+ Voices +
+ {[2, 3, 4].map(count => ( + + ))} +
+
+ + {/* Harmony Type - 3D button style */} +
+ Harmony Type +
+ {harmonyTypes.map(({ value, label }) => ( + + ))} +
+
+ + {/* Detune Spread - Styled slider */} +
+
+ Detune + + {localConfig.detuneSpread}¢ + +
+
+
+ handleDetuneChange(parseInt(e.target.value) / 50)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + aria-label="Detune Spread" + aria-valuetext={`${localConfig.detuneSpread} cents`} + /> +
+
+ + {/* Formant Spread - Styled slider */} +
+
+ Formant + + {localConfig.formantSpread}st + +
+
+
+ handleFormantChange(parseInt(e.target.value) / 12)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + aria-label="Formant Spread" + aria-valuetext={`${localConfig.formantSpread} semitones`} + /> +
+
+ + {/* Presets - Hardware button style */} +
+ Quick Presets +
+ {[ + { key: 'subtle', label: 'DBL' }, + { key: 'classic', label: '3RD' }, + { key: 'choir', label: 'CHR' }, + { key: 'power', label: '5TH' } + ].map(({ key, label }) => ( + + ))} +
+
+ + {/* Apply Button - Animated hardware style */} + +
+
+ + ); +}; export const SamplerVoicePanel: React.FC = ({ title,