import { useState, useRef, useEffect } from 'react'; import * as Tone from 'tone'; // Tailwind CSS classes for styling (to avoid needing a separate CSS file) const styles = { container: "min-h-screen bg-gray-100 flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8", main: "max-w-xl w-full space-y-8 p-10 bg-white shadow-lg rounded-xl", title: "text-center text-3xl font-extrabold text-gray-900", uploadBox: "mb-6", label: "block text-sm font-medium text-gray-700 mb-2", input: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer", button: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white", buttonEnabled: "bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", buttonDisabled: "bg-gray-400 cursor-not-allowed", results: "mt-8 border-t pt-6", resultsTitle: "text-lg font-medium text-gray-900 mb-4", buttonGroup: "flex space-x-4", playButton: "flex-1 py-2 px-4 bg-green-600 text-white rounded-md hover:bg-green-700", downloadLink: "flex-1 py-2 px-4 bg-gray-800 text-white text-center rounded-md hover:bg-gray-900", errorBox: "mt-4 p-4 bg-red-100 text-red-700 rounded-md", }; export default function Home() { const [isProcessing, setIsProcessing] = useState(false); const [originalFile, setOriginalFile] = useState(null); const [processedFile, setProcessedFile] = useState(null); const [error, setError] = useState(null); // 用 useRef 来持久化 Tone.js 的实例,避免重复创建 const synthRef = useRef(null); const midiRef = useRef(null); // 在组件加载时初始化合成器 useEffect(() => { if (!synthRef.current) { synthRef.current = new Tone.PolySynth(Tone.Synth).toDestination(); } }, []); const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { setOriginalFile(file); setProcessedFile(null); setError(null); } }; const handleUpload = async () => { if (!originalFile) return; setIsProcessing(true); setError(null); try { const formData = new FormData(); formData.append('file', originalFile); // [修正] 使用相对路径调用API const response = await fetch('/process-midi', { method: 'POST', body: formData }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || `Server error: ${response.status}`); } const midiBlob = await response.blob(); setProcessedFile(midiBlob); } catch (err) { console.error("Processing failed:", err); setError(err.message); } finally { setIsProcessing(false); } }; const playMidi = async () => { if (!processedFile || !synthRef.current) return; try { // [修正] 使用 Tone.Midi 来播放 if (Tone.Transport.state === 'started') { Tone.Transport.stop(); Tone.Transport.cancel(); } const objectURL = URL.createObjectURL(processedFile); // 加载MIDI文件 const midi = await Tone.Midi.fromUrl(objectURL); midiRef.current = midi; // 保存引用以便停止 // 将MIDI文件的每个音符事件连接到合成器 midi.tracks.forEach(track => { Tone.Transport.schedule(time => { track.notes.forEach(note => { synthRef.current.triggerAttackRelease(note.name, note.duration, note.time + time, note.velocity); }); }, "0"); }); // 启动播放 await Tone.start(); Tone.Transport.start(); } catch (err) { console.error("Playback error:", err); setError("Failed to parse or play MIDI file."); } }; // 用于添加 ); return ( <>

MIDI-A Lightweight Processor

{originalFile && (

Selected: {originalFile.name}

)}
{processedFile && (

3. Play or Download

Download
)} {error && (

Error: {error}

)}
); }