diff --git a/index.html b/index.html index 95f91db..965c42b 100644 --- a/index.html +++ b/index.html @@ -256,14 +256,47 @@ canvas{display:block;} @@ -6057,22 +6090,357 @@ function edDoImport(){ document.getElementById("ed-import-status").textContent="JSON parse error: "+e.message; } } -function edImportMIDI(){document.getElementById("ed-midi-file").click();} function edHandleFile(e){ var file=e.target.files[0];if(!file)return; var reader=new FileReader(); - if(file.name.endsWith(".json")){ - reader.onload=function(ev){ - document.getElementById("ed-import-text").value=ev.target.result; - edDoImport(); - }; - reader.readAsText(file); - }else{ - document.getElementById("ed-import-status").textContent="MIDI parsing not yet supported — convert to JSON first (use AI or online converter)"; - } + reader.onload=function(ev){ + document.getElementById("ed-import-text").value=ev.target.result; + edDoImport(); + }; + reader.readAsText(file); e.target.value=""; } +// Tab switching for import panel +function edShowImportTab(tab){ + ["json","text","musicxml","midi"].forEach(function(t){ + document.getElementById("ed-imp-"+t).style.display=t===tab?"block":"none"; + var btn=document.getElementById("ed-imp-tab-"+t); + btn.style.background=t===tab?"#3182ce":""; + }); + document.getElementById("ed-import-status").textContent=""; +} + +// --- TEXT/SHORTHAND IMPORT --- +// Parses: "C4 quarter, D4 eighth, E4 half" or "C4 q D4 e E4 h" or one-per-line +function edDoShorthandImport(){ + var text=document.getElementById("ed-shorthand-text").value.trim(); + if(!text){document.getElementById("ed-import-status").textContent="Type some notes first";return;} + var status=document.getElementById("ed-import-status"); + var durMap={ + "w":"whole","whole":"whole", + "dh":"dotted-half","dotted-half":"dotted-half","d.h":"dotted-half", + "h":"half","half":"half", + "dq":"dotted-quarter","dotted-quarter":"dotted-quarter","d.q":"dotted-quarter", + "q":"quarter","quarter":"quarter", + "de":"dotted-eighth","dotted-eighth":"dotted-eighth","d.e":"dotted-eighth", + "e":"eighth","eighth":"eighth", + "s":"sixteenth","sixteenth":"sixteenth", + "ts":"32nd","32nd":"32nd" + }; + // Normalize: replace commas and newlines with spaces, collapse whitespace + var tokens=text.replace(/,/g," ").replace(/\n/g," ").replace(/\s+/g," ").trim().split(" "); + var notes=[]; + var t=0; + var q=60/edBpm; // quarter note duration in seconds + var i=0; + var errors=[]; + while(i0){ + // Auto-assign lane based on pitch range + var lane=edPitchToLane(freq); + notes.push({t:parseFloat(t.toFixed(4)),l:lane,f:freq,d:parseFloat(durSec.toFixed(4))}); + t+=durSec; + }else{ + errors.push("Bad note: "+tok); + } + i++; + }else if(tok.toLowerCase()==="r"||tok.toLowerCase()==="rest"){ + // Rest — advance time without adding a note + var restDur=q; + if(i+1offset)offset=end;if(m.t>offset)offset=m.t;}); + notes.forEach(function(n){n.t=parseFloat((n.t+offset).toFixed(4));}); + edMelody=edMelody.concat(notes); + }else{ + edMelody=notes; + } + edMelody.sort(function(a,b){return a.t-b.t;}); + edSelected=-1;edDraw();edUpdateCode(); + var msg="Imported "+notes.length+" notes from shorthand!"; + if(errors.length>0)msg+=" ("+errors.length+" warnings: "+errors.slice(0,3).join(", ")+")"; + status.textContent=msg; +} + +// Auto-assign lane based on pitch frequency +function edPitchToLane(freq){ + // Map pitch ranges to lanes 0-4 + // Lane 0=Red (high), 1=Yellow, 2=Blue, 3=Green (low), 4=Kick (very low) + if(freq>=500)return 0; // C5 and above → Red + if(freq>=370)return 1; // F#4 to B4 → Yellow + if(freq>=280)return 2; // C#4 to F4 → Blue + if(freq>=180)return 3; // F#3 to C4 → Green + return 4; // below F#3 → Kick +} + +// --- MUSICXML IMPORT --- +function edHandleMusicXML(e){ + var file=e.target.files[0];if(!file)return; + var status=document.getElementById("ed-import-status"); + var reader=new FileReader(); + reader.onload=function(ev){ + try{ + var parser=new DOMParser(); + var xml=parser.parseFromString(ev.target.result,"text/xml"); + var parseErr=xml.querySelector("parsererror"); + if(parseErr){status.textContent="XML parse error — is this a valid MusicXML file?";return;} + var notes=edParseMusicXML(xml); + if(notes.length===0){status.textContent="No notes found in MusicXML";return;} + var append=document.getElementById("ed-mxml-append").checked; + if(append){ + var offset=0; + edMelody.forEach(function(m){var end=m.t+(m.d||0);if(end>offset)offset=end;}); + notes.forEach(function(n){n.t=parseFloat((n.t+offset).toFixed(4));}); + edMelody=edMelody.concat(notes); + }else{ + edMelody=notes; + } + edMelody.sort(function(a,b){return a.t-b.t;}); + edSelected=-1;edDraw();edUpdateCode(); + status.textContent="Imported "+notes.length+" notes from MusicXML!"; + }catch(err){ + status.textContent="MusicXML error: "+err.message; + } + }; + reader.readAsText(file); + e.target.value=""; +} + +function edParseMusicXML(xml){ + var notes=[]; + var q=60/edBpm; // quarter note duration + // Find divisions (ticks per quarter note) + var divEl=xml.querySelector("attributes divisions"); + var divisions=divEl?parseInt(divEl.textContent):1; + // Get BPM from tempo marking if present + var tempoEl=xml.querySelector("sound[tempo]"); + if(tempoEl){ + var xmlBpm=parseFloat(tempoEl.getAttribute("tempo")); + if(xmlBpm>0){edBpm=Math.round(xmlBpm);document.getElementById("ed-bpm").value=edBpm;q=60/edBpm;} + } + // Walk through all measures → notes + var t=0; // current time in seconds + var measures=xml.querySelectorAll("measure"); + for(var mi=0;mi0){q=60/newBpm;} + } + var children=meas.children; + for(var ci=0;ci0){ + var lane=edPitchToLane(freq); + notes.push({t:parseFloat(t.toFixed(4)),l:lane,f:freq,d:parseFloat(durSec.toFixed(4))}); + } + if(!isChord)t+=durSec; + } + } + return notes; +} + +// --- MIDI IMPORT --- +function edHandleMIDI(e){ + var file=e.target.files[0];if(!file)return; + var status=document.getElementById("ed-import-status"); + var reader=new FileReader(); + reader.onload=function(ev){ + try{ + var buf=new Uint8Array(ev.target.result); + var notes=edParseMIDI(buf); + if(notes.length===0){status.textContent="No notes found in MIDI file";return;} + var append=document.getElementById("ed-midi-append").checked; + if(append){ + var offset=0; + edMelody.forEach(function(m){var end=m.t+(m.d||0);if(end>offset)offset=end;}); + notes.forEach(function(n){n.t=parseFloat((n.t+offset).toFixed(4));}); + edMelody=edMelody.concat(notes); + }else{ + edMelody=notes; + } + edMelody.sort(function(a,b){return a.t-b.t;}); + edSelected=-1;edDraw();edUpdateCode(); + status.textContent="Imported "+notes.length+" notes from MIDI!"; + }catch(err){ + status.textContent="MIDI error: "+err.message; + } + }; + reader.readAsArrayBuffer(file); + e.target.value=""; +} + +function edParseMIDI(buf){ + // Minimal MIDI parser — handles format 0 and 1 single/multi track + var pos=0; + function readU16(){return(buf[pos++]<<8)|buf[pos++];} + function readU32(){return(buf[pos++]<<24)|(buf[pos++]<<16)|(buf[pos++]<<8)|buf[pos++];} + function readVarLen(){ + var v=0; + for(var i=0;i<4;i++){ + var b=buf[pos++]; + v=(v<<7)|(b&0x7F); + if(!(b&0x80))break; + } + return v; + } + // Header: MThd + if(buf[0]!==0x4D||buf[1]!==0x54||buf[2]!==0x68||buf[3]!==0x64)throw new Error("Not a MIDI file"); + pos=4; + var hdrLen=readU32(); // should be 6 + var format=readU16(); + var nTracks=readU16(); + var ticksPerBeat=readU16(); + pos=8+hdrLen; // skip to first track + + var usPerBeat=500000; // default 120 BPM + var allEvents=[]; + + for(var tr=0;tr=0x80){running=byte1;pos++;} + var cmd=running&0xF0; + if(cmd===0x90){// Note on + var note=buf[pos++];var vel=buf[pos++]; + allEvents.push({type:vel>0?"on":"off",tick:tickTime,note:note,vel:vel,track:tr}); + }else if(cmd===0x80){// Note off + var note2=buf[pos++];pos++;// skip vel + allEvents.push({type:"off",tick:tickTime,note:note2,track:tr}); + }else if(cmd===0xA0||cmd===0xB0||cmd===0xE0){pos+=2;} + else if(cmd===0xC0||cmd===0xD0){pos+=1;} + else if(byte1===0xFF){// Meta event + var metaType=buf[pos++]; + var metaLen=readVarLen(); + if(metaType===0x51&&metaLen===3){// Tempo + usPerBeat=(buf[pos]<<16)|(buf[pos+1]<<8)|buf[pos+2]; + } + pos+=metaLen; + }else if(byte1===0xF0||byte1===0xF7){// SysEx + var sysLen=readVarLen();pos+=sysLen; + }else{ + // Unknown — try to skip gracefully + pos++; + } + } + pos=trkEnd; + } + + // Set BPM from tempo + var bpmFromMidi=Math.round(60000000/usPerBeat); + if(bpmFromMidi>0){edBpm=bpmFromMidi;document.getElementById("ed-bpm").value=edBpm;} + + // Convert tick events to notes with time/duration + allEvents.sort(function(a,b){return a.tick-b.tick||(a.type==="off"?-1:1);}); + var secPerTick=usPerBeat/(ticksPerBeat*1000000); + var openNotes={}; // key: noteNum → {tick, event} + var notes=[]; + for(var i=0;i