peter288's picture
Upload 8 files
178f666 verified
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.");
}
};
// 用于添加<script>标签,因为Next.js静态导出不直接支持
const TailwindScript = () => (
<script src="https://cdn.tailwindcss.com"></script>
);
return (
<>
<TailwindScript />
<div className={styles.container}>
<div className={styles.main}>
<div>
<h1 className={styles.title}>
MIDI-A Lightweight Processor
</h1>
</div>
<div className={styles.uploadBox}>
<label htmlFor="midi-upload" className={styles.label}>
1. Upload a MIDI File (.mid)
</label>
<input
id="midi-upload"
type="file"
accept=".mid,.midi"
onChange={handleFileChange}
className={styles.input}
disabled={isProcessing}
/>
{originalFile && (
<p className="mt-2 text-sm text-gray-600">
Selected: {originalFile.name}
</p>
)}
</div>
<button
onClick={handleUpload}
disabled={!originalFile || isProcessing}
className={`${styles.button} ${!originalFile || isProcessing ? styles.buttonDisabled : styles.buttonEnabled}`}
>
{isProcessing ? 'Processing...' : '2. Process MIDI'}
</button>
{processedFile && (
<div className={styles.results}>
<h2 className={styles.resultsTitle}>3. Play or Download</h2>
<div className={styles.buttonGroup}>
<button
onClick={playMidi}
className={styles.playButton}
>
Play Processed MIDI
</button>
<a
href={URL.createObjectURL(processedFile)}
download={`processed_${originalFile.name}`}
className={styles.downloadLink}
>
Download
</a>
</div>
</div>
)}
{error && (
<div className={styles.errorBox}>
<p><strong>Error:</strong> {error}</p>
</div>
)}
</div>
</div>
</>
);
}