File size: 6,344 Bytes
25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 25d51c9 a0fcd39 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | import React, { useState, useEffect, useMemo, useCallback } 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']
function AnalysisDisplay({
detection,
loading,
onReady,
appliedSemitones = 0,
onProcess,
isProcessing,
hasRegion
}) {
const [targetKeyWithMode, setTargetKeyWithMode] = useState('C major')
const [targetBpm, setTargetBpm] = useState(120)
useEffect(() => {
if (detection && onReady) {
onReady()
}
}, [detection, onReady])
useEffect(() => {
if (detection) {
setTargetKeyWithMode(`${detection.key} ${detection.mode}`)
setTargetBpm(detection.bpm)
}
}, [detection])
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 handleApply = useCallback(() => {
if (!detection || !onProcess) return
const newBpm = Math.abs(targetBpm - detection.bpm) > 0.1 ? targetBpm : null
onProcess(semitones, newBpm)
}, [onProcess, semitones, targetBpm, detection])
const hasChanges = detection && (semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1)
if (loading) {
return (
<div className="card rounded-xl p-5 animate-pulse">
<div className="flex gap-6">
<div className="h-12 bg-white/5 rounded-lg w-40"></div>
<div className="h-12 bg-white/5 rounded-lg w-40"></div>
</div>
</div>
)
}
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 (
<div className="card rounded-xl p-5 animate-fade-in">
<div className="flex items-center gap-6 flex-wrap">
{/* BPM Control: - [editable input] + */}
<div className="flex items-center gap-1.5">
<span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">BPM</span>
<button onClick={() => setTargetBpm(v => Math.max(20, v - 1))} className={btnClass}>-</button>
<input
type="number"
value={Math.round(targetBpm)}
onChange={(e) => {
const val = parseFloat(e.target.value)
if (!isNaN(val) && val >= 20 && val <= 400) setTargetBpm(val)
}}
min={20}
max={400}
className={`${inputClass} w-20 px-2 py-1.5 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`}
/>
<button onClick={() => setTargetBpm(v => Math.min(400, v + 1))} className={btnClass}>+</button>
{Math.abs(targetBpm - detection.bpm) > 0.1 && (
<button
onClick={() => setTargetBpm(detection.bpm)}
className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1"
>
Reset
</button>
)}
</div>
{/* Divider */}
<div className="w-px h-10 bg-white/[0.08]"></div>
{/* Key Control: - [dropdown] + */}
<div className="flex items-center gap-1.5">
<span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">Key</span>
<button onClick={() => handleKeyShift(-1)} className={btnClass}>-</button>
<select
value={targetKeyWithMode}
onChange={(e) => setTargetKeyWithMode(e.target.value)}
className={`${inputClass} w-28 px-2 py-1.5 cursor-pointer`}
>
{ALL_KEYS_WITH_MODES.map(k => (
<option key={k} value={k}>{k}</option>
))}
</select>
<button onClick={() => handleKeyShift(1)} className={btnClass}>+</button>
{semitones !== 0 && (
<span className="text-xs text-gray-500 font-mono ml-1">
{semitones > 0 ? '+' : ''}{semitones}st
</span>
)}
{semitones !== 0 && (
<button
onClick={() => setTargetKeyWithMode(`${detection.key} ${detection.mode}`)}
className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1"
>
Reset
</button>
)}
</div>
{/* Divider */}
<div className="w-px h-10 bg-white/[0.08]"></div>
{/* Apply Button */}
<button
onClick={handleApply}
disabled={!hasChanges || isProcessing}
className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${
!hasChanges || isProcessing
? 'bg-white/[0.04] text-gray-600 cursor-not-allowed'
: 'bg-accent-500 hover:bg-accent-400 text-white'
}`}
>
{isProcessing ? 'Processing...' : hasChanges ? (hasRegion ? 'Apply to Selection' : 'Apply') : 'No Changes'}
</button>
{/* Original values */}
{(semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) && (
<span className="text-[10px] text-gray-600 ml-auto">
Original: {detection.key} {detection.mode} / {detection.bpm.toFixed(1)} BPM
</span>
)}
</div>
</div>
)
}
export default AnalysisDisplay
|