Spaces:
Sleeping
Sleeping
Download frontend/src/components/AnalysisDisplay.jsx from MusoraProductDepartment/jam-tracks: direct link, hf CLI and curl.
- Browser
- Download file 7.34 kB
-
https://huggingface.co/spaces/MusoraProductDepartment/jam-tracks/resolve/main/frontend/src/components/AnalysisDisplay.jsx
- Command line
-
hf download hf://spaces/MusoraProductDepartment/jam-tracks/frontend/src/components/AnalysisDisplay.jsx
-
curl -L -o AnalysisDisplay.jsx https://huggingface.co/spaces/MusoraProductDepartment/jam-tracks/resolve/main/frontend/src/components/AnalysisDisplay.jsx
7.34 kB
| 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 ( | |
| <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={() => handleBpmStep(-1)} className={btnClass}>-</button> | |
| <input | |
| type="number" | |
| value={targetBpmInput} | |
| onChange={handleBpmInputChange} | |
| onBlur={handleBpmInputBlur} | |
| onKeyDown={(event) => { | |
| 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`} | |
| /> | |
| <button onClick={() => handleBpmStep(1)} className={btnClass}>+</button> | |
| {Math.abs(targetBpm - detection.bpm) > 0.1 && ( | |
| <button | |
| onClick={() => setBpmValue(detection.bpm)} | |
| className="text-[10px] text-gray-400 hover:text-gray-200 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-400 font-mono ml-1"> | |
| {semitones > 0 ? '+' : ''}{semitones}st | |
| </span> | |
| )} | |
| {semitones !== 0 && ( | |
| <button | |
| onClick={() => setTargetKeyWithMode(`${detection.key} ${detection.mode}`)} | |
| className="text-[10px] text-gray-400 hover:text-gray-200 transition-colors ml-1" | |
| > | |
| Reset | |
| </button> | |
| )} | |
| </div> | |
| <div className="w-px h-10 bg-white/[0.08]"></div> | |
| <div className="px-3 py-1.5 rounded-lg border border-emerald-400/30 bg-emerald-400/10 text-emerald-300 text-[10px] uppercase tracking-widest font-semibold"> | |
| Live | |
| </div> | |
| {/* Original values */} | |
| {(semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) && ( | |
| <span className="text-[10px] text-gray-400 ml-auto"> | |
| Original: {detection.key} {detection.mode} / {detection.bpm.toFixed(1)} BPM | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default AnalysisDisplay | |