import React, { useState, useEffect, useRef } from 'react'; import { Upload, Play, Square, Download, Music, Brain, Loader2, Plus, Trash2, Library } from 'lucide-react'; import * as Tone from 'tone'; const App = () => { const [isLibraryLoading, setIsLibraryLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [uploadedFiles, setUploadedFiles] = useState([]); const [generatedMidi, setGeneratedMidi] = useState(null); const [status, setStatus] = useState("Ready to analyze MIDI files."); const [isPlaying, setIsPlaying] = useState(false); const [creativity, setCreativity] = useState(0.5); // 0 = predictable, 1 = random const [length, setLength] = useState(32); // Markov Chain "Brain" const markovModel = useRef({ pitchMap: {}, // key: currentPitch, value: [nextPitches] durationMap: {}, // key: currentDur, value: [nextDurs] starts: [] // potential starting notes }); const synthRef = useRef(null); const partRef = useRef(null); useEffect(() => { // Initialize a simple PolySynth synthRef.current = new Tone.PolySynth(Tone.Synth).toDestination(); // Cleanup return () => { if (partRef.current) partRef.current.dispose(); if (synthRef.current) synthRef.current.dispose(); }; }, []); // Process MIDI files to build the Markov Chain const trainModel = (midiData) => { const model = markovModel.current; midiData.tracks.forEach(track => { const notes = track.notes; if (notes.length < 2) return; // Add the first note to potential starts model.starts.push({ midi: notes[0].midi, duration: notes[0].duration }); for (let i = 0; i < notes.length - 1; i++) { const currentNote = notes[i]; const nextNote = notes[i + 1]; // Map Pitches if (!model.pitchMap[currentNote.midi]) model.pitchMap[currentNote.midi] = []; model.pitchMap[currentNote.midi].push(nextNote.midi); // Map Durations if (!model.durationMap[currentNote.duration]) model.durationMap[currentNote.duration] = []; model.durationMap[currentNote.duration].push(nextNote.duration); } }); }; const handleFileUpload = async (e) => { const files = Array.from(e.target.files); if (files.length === 0) return; // Check if library loaded via CDN if (!window.Midi) { setStatus("MIDI Parser library still loading. Please wait a second."); return; } setIsLibraryLoading(true); setStatus(`Processing ${files.length} file(s)...`); try { const newUploads = []; for (const file of files) { const arrayBuffer = await file.arrayBuffer(); // Accessing global Midi from window object const midi = new window.Midi(arrayBuffer); trainModel(midi); newUploads.push({ id: Math.random().toString(36).substr(2, 9), name: file.name, noteCount: midi.tracks.reduce((acc, t) => acc + t.notes.length, 0) }); } setUploadedFiles(prev => [...prev, ...newUploads]); setStatus(`Training complete. AI learned from ${files.length} new files.`); } catch (err) { console.error(err); setStatus("Error processing MIDI. Ensure they are valid .mid files."); } finally { setIsLibraryLoading(false); } }; const generateMusic = () => { if (uploadedFiles.length === 0) { setStatus("Upload some MIDI files first so the AI has data to learn from!"); return; } setIsGenerating(true); setStatus("AI is calculating probabilities..."); const model = markovModel.current; const sequence = []; // Pick a starting note let current = model.starts.length > 0 ? model.starts[Math.floor(Math.random() * model.starts.length)] : { midi: 60, duration: 0.5 }; // Fallback to middle C let currentTime = 0; for (let i = 0; i < length; i++) { sequence.push({ midi: current.midi, time: currentTime, duration: current.duration }); currentTime += current.duration; // Decide next note const possiblePitches = model.pitchMap[current.midi] || []; const possibleDurs = model.durationMap[current.duration] || []; const useRandom = Math.random() < creativity || possiblePitches.length === 0; if (useRandom) { const allPitches = Object.keys(model.pitchMap); const randomPitch = allPitches.length > 0 ? allPitches[Math.floor(Math.random() * allPitches.length)] : 60; const allDurs = Object.keys(model.durationMap); const randomDur = allDurs.length > 0 ? allDurs[Math.floor(Math.random() * allDurs.length)] : 0.5; current = { midi: parseInt(randomPitch), duration: parseFloat(randomDur) }; } else { current = { midi: possiblePitches[Math.floor(Math.random() * possiblePitches.length)], duration: possibleDurs[Math.floor(Math.random() * possibleDurs.length)] }; } } setGeneratedMidi(sequence); setStatus("New sequence generated!"); setIsGenerating(false); }; const togglePlayback = async () => { if (isPlaying) { Tone.Transport.stop(); Tone.Transport.cancel(); if (partRef.current) partRef.current.stop(); setIsPlaying(false); return; } if (!generatedMidi) return; await Tone.start(); if (partRef.current) partRef.current.dispose(); partRef.current = new Tone.Part((time, note) => { synthRef.current.triggerAttackRelease( Tone.Frequency(note.midi, "midi").toNote(), note.duration, time ); }, generatedMidi.map(n => [n.time, n])).start(0); Tone.Transport.start(); setIsPlaying(true); const totalTime = generatedMidi[generatedMidi.length - 1].time + generatedMidi[generatedMidi.length - 1].duration; setTimeout(() => { if (Tone.Transport.state === 'started') { setIsPlaying(false); } }, totalTime * 1000 + 100); }; const clearLibrary = () => { setUploadedFiles([]); markovModel.current = { pitchMap: {}, durationMap: {}, starts: [] }; setGeneratedMidi(null); setStatus("Library cleared."); }; return (
Upload MIDI to train a local probabilistic model.
{file.name}
{file.noteCount} notes parsed
No data available
Awaiting Generation