Spaces:
No application file
No application file
File size: 5,676 Bytes
54f863b | 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 | 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>
);
}
|