Spaces:
No application file
No application file
| import React from "react"; | |
| import { Download, FileDown, Activity, Info, TrendingUp, Key } from "lucide-react"; | |
| import { NoteEvent } from "../utils/midiWriter"; | |
| interface StatsPanelProps { | |
| notes: NoteEvent[]; | |
| bpm: number; | |
| duration: number; | |
| selectedPreset: string; | |
| onDownloadMidi: () => void; | |
| isDownloading: boolean; | |
| } | |
| export default function StatsPanel({ | |
| notes, | |
| bpm, | |
| duration, | |
| selectedPreset, | |
| onDownloadMidi, | |
| isDownloading | |
| }: StatsPanelProps) { | |
| if (notes.length === 0) return null; | |
| // Calculate stats | |
| const noteCount = notes.length; | |
| const density = duration > 0 ? (noteCount / duration).toFixed(1) : "0.0"; | |
| const pitches = notes.map(n => n.midi_note); | |
| const minPitch = Math.min(...pitches); | |
| const maxPitch = Math.max(...pitches); | |
| const getNoteName = (midiNum: number) => { | |
| const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; | |
| const octave = Math.floor(midiNum / 12) - 1; | |
| const noteName = names[midiNum % 12]; | |
| return `${noteName}${octave}`; | |
| }; | |
| // Estimate a simple musical Key based on Pitch frequency tally | |
| const estimateKey = () => { | |
| const pitchCounts = new Array(12).fill(0); | |
| pitches.forEach(p => { | |
| pitchCounts[p % 12]++; | |
| }); | |
| const keyNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; | |
| let maxIdx = 0; | |
| let maxCount = 0; | |
| for (let i = 0; i < 12; i++) { | |
| if (pitchCounts[i] > maxCount) { | |
| maxCount = pitchCounts[i]; | |
| maxIdx = i; | |
| } | |
| } | |
| // Return estimated tonic + dynamic modality heuristic | |
| // If notes contain G and E relative to C, say Major, otherwise Minor | |
| const tonic = keyNames[maxIdx]; | |
| const isMajor = pitchCounts[(maxIdx + 4) % 12] >= pitchCounts[(maxIdx + 3) % 12]; | |
| return `${tonic} ${isMajor ? "Major" : "Minor"}`; | |
| }; | |
| return ( | |
| <div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-xl space-y-6"> | |
| {/* Title */} | |
| <div className="flex items-center gap-2 pb-3 border-b border-slate-800"> | |
| <TrendingUp className="w-5 h-5 text-emerald-400" /> | |
| <h3 className="text-sm font-semibold tracking-wider text-slate-200 uppercase font-sans"> | |
| TRANSCRIPTION METRICS | |
| </h3> | |
| </div> | |
| {/* Grid of stats */} | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| {/* Total notes */} | |
| <div className="bg-slate-950 border border-slate-800/60 rounded-xl p-4 text-center"> | |
| <span className="text-[10px] text-zinc-500 font-mono block uppercase">Total Notes</span> | |
| <span className="text-2xl font-bold font-mono text-emerald-400 mt-1 block"> | |
| {noteCount} | |
| </span> | |
| </div> | |
| {/* Note Density */} | |
| <div className="bg-slate-950 border border-slate-800/60 rounded-xl p-4 text-center"> | |
| <span className="text-[10px] text-zinc-500 font-mono block uppercase">Density</span> | |
| <span className="text-2xl font-bold font-mono text-emerald-400 mt-1 block"> | |
| {density} <span className="text-xs text-slate-500 font-normal">n/s</span> | |
| </span> | |
| </div> | |
| {/* Est. Key Signature */} | |
| <div className="bg-slate-950 border border-slate-800/60 rounded-xl p-4 text-center"> | |
| <span className="text-[10px] text-zinc-500 font-mono block uppercase">Est. Key TONALITY</span> | |
| <span className="text-lg font-bold font-mono text-emerald-400 mt-2 block flex items-center justify-center gap-1"> | |
| <Key className="w-3.5 h-3.5 text-amber-500 inline" /> | |
| {estimateKey()} | |
| </span> | |
| </div> | |
| {/* Pitch Range */} | |
| <div className="bg-slate-950 border border-slate-800/60 rounded-xl p-4 text-center"> | |
| <span className="text-[10px] text-zinc-500 font-mono block uppercase">Pitch Range</span> | |
| <span className="text-sm font-semibold font-mono text-emerald-400 mt-2.5 block truncate"> | |
| {getNoteName(minPitch)} ~ {getNoteName(maxPitch)} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Helper notice */} | |
| <div className="bg-slate-950/60 border border-slate-800 text-slate-400 p-4 rounded-xl flex gap-3 text-xs leading-relaxed"> | |
| <Info className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" /> | |
| <div> | |
| <strong className="text-zinc-300 font-sans">DAW Ready MIDI Format</strong> | |
| <p className="font-sans text-slate-400 mt-0.5 select-none"> | |
| Our exported file includes absolute note events, velocity mappings, and sustain meta events. It is saved in Format 0 standard MIDI structure compatibility, immediately readable in Ableton Live, Logic Pro, GarageBand, Pro Tools, or FL Studio templates. | |
| </p> | |
| </div> | |
| </div> | |
| {/* Downloads Action Panel */} | |
| <div className="pt-2"> | |
| <button | |
| onClick={onDownloadMidi} | |
| disabled={isDownloading} | |
| className="w-full flex items-center justify-center gap-2 bg-emerald-500 hover:bg-emerald-600 font-sans text-slate-950 hover:text-slate-900 font-bold py-3.5 px-4 rounded-xl shadow-lg shadow-emerald-500/10 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200" | |
| > | |
| {isDownloading ? ( | |
| <> | |
| <div className="w-4 h-4 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" /> | |
| <span>COMPILING MIDI TRACK...</span> | |
| </> | |
| ) : ( | |
| <> | |
| <FileDown className="w-5 h-5" /> | |
| <span>EXPORT MIDI FILE (.mid)</span> | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |