Continuator / app.py
pachet's picture
added the style fix to the js
7e0f543
import gradio as gr
from fastapi import FastAPI, Request
import json
# ✅ Create FastAPI App
app = FastAPI()
# ✅ MIDI Processing Function in Python
@app.post("/midi_input")
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)}
@app.post("/midi_phrase")
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)