import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react' const ALL_KEYS_WITH_MODES = [ 'C major', 'C minor', 'C# major', 'C# minor', 'D major', 'D minor', 'D# major', 'D# minor', 'E major', 'E minor', 'F major', 'F minor', 'F# major', 'F# minor', 'G major', 'G minor', 'G# major', 'G# minor', 'A major', 'A minor', 'A# major', 'A# minor', 'B major', 'B minor' ] const KEY_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] const MIN_BPM = 20 const MAX_BPM = 400 function clampBpm(value) { return Math.max(MIN_BPM, Math.min(MAX_BPM, value)) } function formatBpm(value) { if (!Number.isFinite(value)) return '' return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2))) } function AnalysisDisplay({ detection, loading, onReady, onLiveTransform }) { const [targetKeyWithMode, setTargetKeyWithMode] = useState('C major') const [targetBpm, setTargetBpm] = useState(120) const [targetBpmInput, setTargetBpmInput] = useState('120') const readyDetectionRef = useRef(null) const setBpmValue = useCallback((value) => { const parsed = Number(value) if (!Number.isFinite(parsed)) return const next = clampBpm(parsed) setTargetBpm(next) setTargetBpmInput(formatBpm(next)) }, []) useEffect(() => { if (detection && onReady && readyDetectionRef.current !== detection) { readyDetectionRef.current = detection onReady() } }, [detection, onReady]) useEffect(() => { if (detection) { setTargetKeyWithMode(`${detection.key} ${detection.mode}`) setBpmValue(detection.bpm) } }, [detection, setBpmValue]) const targetKey = targetKeyWithMode.split(' ')[0] const semitones = useMemo(() => { if (!detection || !targetKey) return 0 const fromIdx = KEY_NAMES.indexOf(detection.key) const toIdx = KEY_NAMES.indexOf(targetKey) let diff = toIdx - fromIdx if (diff > 6) diff -= 12 if (diff < -6) diff += 12 return diff }, [detection, targetKey]) const handleKeyShift = useCallback((shift) => { const currentIdx = KEY_NAMES.indexOf(targetKey) const newIdx = (currentIdx + shift + 12) % 12 const mode = targetKeyWithMode.split(' ')[1] setTargetKeyWithMode(`${KEY_NAMES[newIdx]} ${mode}`) }, [targetKey, targetKeyWithMode]) const handleBpmInputChange = useCallback((event) => { const rawValue = event.target.value setTargetBpmInput(rawValue) if (rawValue.trim() === '') return const parsed = Number(rawValue) if (Number.isFinite(parsed) && parsed >= MIN_BPM && parsed <= MAX_BPM) { setTargetBpm(parsed) } }, []) const handleBpmInputBlur = useCallback(() => { const trimmedValue = targetBpmInput.trim() const parsed = trimmedValue === '' ? NaN : Number(trimmedValue) setBpmValue(Number.isFinite(parsed) ? parsed : targetBpm) }, [setBpmValue, targetBpm, targetBpmInput]) const handleBpmStep = useCallback((delta) => { setBpmValue(targetBpm + delta) }, [setBpmValue, targetBpm]) useEffect(() => { if (!detection || !onLiveTransform) return onLiveTransform({ semitones, originalBpm: detection.bpm, targetBpm, highQuality: true }) }, [detection, onLiveTransform, semitones, targetBpm]) const hasChanges = detection && (semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) if (loading) { return (
) } if (!detection) return null const btnClass = 'w-8 h-8 rounded-lg font-bold text-sm transition-all bg-white/[0.06] hover:bg-white/[0.12] text-gray-300 border border-white/[0.08] flex items-center justify-center flex-shrink-0' const inputClass = 'bg-surface-elevated border border-white/[0.08] rounded-lg text-white text-sm text-center focus:outline-none focus:border-accent-500 transition-colors' return (
{/* BPM Control: - [editable input] + */}
BPM { if (event.key === 'Enter') event.currentTarget.blur() }} min={MIN_BPM} max={MAX_BPM} className={`${inputClass} w-20 px-2 py-1.5 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`} /> {Math.abs(targetBpm - detection.bpm) > 0.1 && ( )}
{/* Divider */}
{/* Key Control: - [dropdown] + */}
Key {semitones !== 0 && ( {semitones > 0 ? '+' : ''}{semitones}st )} {semitones !== 0 && ( )}
Live
{/* Original values */} {(semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) && ( Original: {detection.key} {detection.mode} / {detection.bpm.toFixed(1)} BPM )}
) } export default AnalysisDisplay