Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from fastapi import FastAPI, Request | |
| import json | |
| # ✅ Create FastAPI App | |
| app = FastAPI() | |
| # ✅ MIDI Processing Function in Python | |
| async def process_midi(request: Request): | |
| try: | |
| midi_data = await request.json() | |
| note = midi_data["note"] | |
| velocity = midi_data["velocity"] | |
| print(f"🎹 Received MIDI Note: {note}, Velocity: {velocity}") | |
| # 🚀 Process MIDI data (example: Transpose + Generate New Notes) | |
| generated_note = (note + 5) % 128 # Transpose up by 3 semitones | |
| generated_velocity = min(velocity + 10, 127) # Increase velocity slightly | |
| # ✅ Send MIDI Response Back to Client | |
| return { | |
| "status": "success", | |
| "generated_note": generated_note, | |
| "generated_velocity": generated_velocity, | |
| "original_note": note | |
| } | |
| except Exception as e: | |
| print(f"🚨 Error processing MIDI: {str(e)}") | |
| return {"status": "error", "message": str(e)} | |
| async def process_midi_phrase(request: Request): | |
| try: | |
| data = await request.json() | |
| phrase = data["phrase"] # List of MIDI notes | |
| print(f"🎹 Received MIDI Phrase ({len(phrase)} notes)") | |
| # 🚀 Process Phrase: Example - Transpose Each Note Up by 3 Semitones | |
| generated_phrase = [ | |
| { | |
| "note": (note["note"] + 3) % 128, | |
| "velocity": note["velocity"], | |
| "duration": note["duration"], # ✅ Keep original duration | |
| "inter_onset": note["inter_onset"] # ✅ Keep inter-onset time | |
| } | |
| for note in phrase | |
| ] | |
| return {"status": "success", "generated_phrase": generated_phrase} | |
| except Exception as e: | |
| print(f"🚨 Error processing MIDI phrase: {str(e)}") | |
| return {"status": "error", "message": str(e)} | |
| # ✅ JavaScript to Capture and Send MIDI Data | |
| midi_js = """ | |
| <script> | |
| let style = document.createElement("style"); | |
| style.innerHTML = ` | |
| * { | |
| font-family: Arial, sans-serif !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| console.log("✅ Applied fallback font to prevent 404 error."); | |
| let midiAccess = null; | |
| let selectedInput = null; | |
| let selectedOutput = null; | |
| // ✅ Request MIDI Access | |
| navigator.requestMIDIAccess() | |
| .then(access => { | |
| console.log("✅ MIDI Access Granted!"); | |
| midiAccess = access; | |
| updateMIDIDevices(); | |
| midiAccess.onstatechange = updateMIDIDevices; | |
| }) | |
| .catch(err => console.error("🚨 MIDI API Error:", err)); | |
| // ✅ Update MIDI Input & Output Menus | |
| function updateMIDIDevices() { | |
| let inputSelect = document.getElementById("midiInput"); | |
| let outputSelect = document.getElementById("midiOutput"); | |
| if (!inputSelect || !outputSelect) { | |
| console.error("❌ MIDI dropdowns not found!"); | |
| return; | |
| } | |
| // Clear existing options | |
| inputSelect.innerHTML = '<option value="">Select MIDI Input</option>'; | |
| outputSelect.innerHTML = '<option value="">Select MIDI Output</option>'; | |
| // Populate MIDI Input Devices | |
| midiAccess.inputs.forEach((input, key) => { | |
| let option = document.createElement("option"); | |
| option.value = key; | |
| option.textContent = input.name || `MIDI Input ${key}`; | |
| inputSelect.appendChild(option); | |
| }); | |
| // Populate MIDI Output Devices | |
| midiAccess.outputs.forEach((output, key) => { | |
| let option = document.createElement("option"); | |
| option.value = key; | |
| option.textContent = output.name || `MIDI Output ${key}`; | |
| outputSelect.appendChild(option); | |
| }); | |
| console.log("🎛 Updated MIDI Input & Output devices."); | |
| } | |
| // ✅ Handle MIDI Input Selection | |
| function selectMIDIInput() { | |
| let inputSelect = document.getElementById("midiInput"); | |
| let inputId = inputSelect.value; | |
| if (selectedInput) { | |
| selectedInput.onmidimessage = null; | |
| } | |
| if (midiAccess.inputs.has(inputId)) { | |
| selectedInput = midiAccess.inputs.get(inputId); | |
| selectedInput.onmidimessage = handleMIDIMessage; | |
| console.log(`🎤 MIDI Input Selected: ${selectedInput.name}`); | |
| } | |
| } | |
| // ✅ Handle MIDI Output Selection | |
| function selectMIDIOutput() { | |
| let outputSelect = document.getElementById("midiOutput"); | |
| let outputId = outputSelect.value; | |
| if (midiAccess.outputs.has(outputId)) { | |
| selectedOutput = midiAccess.outputs.get(outputId); | |
| console.log(`🎹 MIDI Output Selected: ${selectedOutput.name}`); | |
| } | |
| } | |
| // ✅ Play a MIDI Note Sent Back from Python | |
| function playMIDINote(note, velocity) { | |
| if (!selectedOutput) { | |
| console.warn("⚠️ No MIDI output selected."); | |
| return; | |
| } | |
| let noteOnMessage = [0x90, note, velocity]; // Note On | |
| let noteOffMessage = [0x80, note, 0]; // Note Off | |
| console.log(`🎵 Playing Generated MIDI Note ${note}, Velocity ${velocity}`); | |
| try { | |
| selectedOutput.send(noteOnMessage); | |
| setTimeout(() => { | |
| selectedOutput.send(noteOffMessage); | |
| console.log(`🔇 Note Off ${note}`); | |
| }, 500); | |
| } catch (error) { | |
| console.error("🚨 Error playing MIDI note:", error); | |
| } | |
| } | |
| // ✅ Send MIDI Data to Python | |
| // ✅ Handle Incoming MIDI Messages and Send to Python | |
| let midiPhrase = []; // ✅ Buffer to store incoming notes | |
| let activeNotes = new Map(); // ✅ Track active notes with start times | |
| let phraseTimeout = null; // ✅ Timer to detect phrase end | |
| let lastNoteOnTime = null; // ✅ Track previous note-on time for inter-onset calculation | |
| // ✅ Handle Incoming MIDI Messages (Phrase Buffering) | |
| function handleMIDIMessage(event) { | |
| let command = event.data[0] & 0xf0; // Extract MIDI command | |
| let note = event.data[1]; | |
| let velocity = event.data[2]; | |
| let timestamp = performance.now(); // ✅ Get precise timing | |
| if (command === 0x90 && velocity > 0) { | |
| // ✅ Note On: Store start time and inter-onset interval | |
| let interOnsetTime = lastNoteOnTime ? timestamp - lastNoteOnTime : 0; | |
| lastNoteOnTime = timestamp; | |
| console.log(interOnsetTime); | |
| activeNotes.set(note, timestamp); // ✅ Store note start time | |
| midiPhrase.push({ note, velocity, start_time: timestamp, duration: null, inter_onset: interOnsetTime }); | |
| console.log(`🎤 Note ON: ${note}, Velocity ${velocity}, IOT ${interOnsetTime}ms`); | |
| } | |
| else if (command === 0x80 || (command === 0x90 && velocity === 0)) { | |
| // ✅ Note Off: Calculate duration and update phrase | |
| if (activeNotes.has(note)) { | |
| let startTime = activeNotes.get(note); | |
| let duration = timestamp - startTime; | |
| activeNotes.delete(note); | |
| // ✅ Find the note in the phrase and update duration | |
| let noteIndex = midiPhrase.findIndex(n => n.note === note && n.duration === null); | |
| if (noteIndex !== -1) { | |
| midiPhrase[noteIndex].duration = duration; | |
| console.log(`🎹 Note OFF: ${note}, Duration ${duration}ms`); | |
| } | |
| } | |
| // ✅ If no active notes, start phrase timeout (1s delay before sending) | |
| if (activeNotes.size === 0) { | |
| if (phraseTimeout) clearTimeout(phraseTimeout); | |
| phraseTimeout = setTimeout(sendPhraseToPython, 1000); | |
| } | |
| } | |
| } | |
| // ✅ Send the Phrase to Python After 1 Second of Silence | |
| function sendPhraseToPython() { | |
| if (midiPhrase.length === 0 || activeNotes.size > 0) return; // ✅ Do not send if notes are still active | |
| console.log("📨 Sending MIDI phrase to Python:", midiPhrase); | |
| fetch("/midi_phrase", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ phrase: midiPhrase }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| console.log("📩 Python Response:", data); | |
| if (data.generated_phrase) { | |
| playGeneratedPhrase(data.generated_phrase); | |
| } | |
| }) | |
| .catch(error => console.error("🚨 Error sending MIDI phrase:", error)); | |
| // ✅ Clear the phrase buffer | |
| midiPhrase = []; | |
| lastNoteOnTime = null; | |
| } | |
| // ✅ Play Generated MIDI Phrase from Python (Keeping Timing) | |
| function playGeneratedPhrase(phrase) { | |
| if (!selectedOutput) { | |
| console.warn("⚠️ No MIDI output selected."); | |
| return; | |
| } | |
| console.log("🎵 Playing Generated Phrase:", phrase); | |
| let accumulatedDelay = 0; // ✅ Track total delay from phrase start | |
| phrase.forEach((noteData, index) => { | |
| accumulatedDelay += noteData.inter_onset; // ✅ Accumulate inter-onset time | |
| setTimeout(() => { | |
| let noteOnMessage = [0x90, noteData.note, noteData.velocity]; | |
| let noteOffMessage = [0x80, noteData.note, 0]; | |
| selectedOutput.send(noteOnMessage); | |
| console.log(`🎶 Note ON: ${noteData.note}, Velocity: ${noteData.velocity}, Start Delay: ${accumulatedDelay}ms`); | |
| setTimeout(() => { | |
| selectedOutput.send(noteOffMessage); | |
| console.log(`🔇 Note OFF: ${noteData.note}`); | |
| }, noteData.duration); // ✅ Use stored duration | |
| }, accumulatedDelay); // ✅ Use accumulated delay for precise timing | |
| }); | |
| } | |
| // ✅ Attach Generate Button Event | |
| function attachButtonEvent() { | |
| let generateButton = document.getElementById("generateButton"); | |
| if (generateButton) { | |
| console.log("✅ Generate button found! Attaching event listener..."); | |
| generateButton.addEventListener("click", function () { | |
| console.log("🎹 Generate button clicked."); | |
| if (!selectedOutput) { | |
| alert("⚠️ Please select a MIDI Output first!"); | |
| return; | |
| } | |
| let randomNote = 60 + Math.floor(Math.random() * 12); // Random note from C4 to B4 | |
| console.log(`🎵 Generating MIDI Note: ${randomNote}`); | |
| playMIDINote(randomNote, 100); | |
| }); | |
| } else { | |
| console.log("⏳ Waiting for button to be available..."); | |
| setTimeout(attachButtonEvent, 500); // Try again in 500ms | |
| } | |
| } | |
| // ✅ Ensure the Button and Menus Are Loaded | |
| window.onload = function() { | |
| console.log("✅ Page fully loaded. Checking for elements..."); | |
| updateMIDIDevices(); | |
| attachButtonEvent(); | |
| }; | |
| </script> | |
| <!-- 🎛 MIDI Input & Output Selection --> | |
| <div> | |
| <label for="midiInput">MIDI Input: </label> | |
| <select id="midiInput" onchange="selectMIDIInput()"></select> | |
| <label for="midiOutput">MIDI Output: </label> | |
| <select id="midiOutput" onchange="selectMIDIOutput()"></select> | |
| </div> | |
| <!-- 🎶 "Generate MIDI" Button --> | |
| <button id="generateButton">🎵 Generate MIDI Note</button> | |
| """ | |
| # ✅ Inject JavaScript and HTML | |
| with gr.Blocks() as demo: | |
| gr.HTML(midi_js) | |
| # ✅ Mount FastAPI with Gradio | |
| app = gr.mount_gradio_app(app, demo, path="/") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| print("🚀 Starting FastAPI with Gradio...") | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |