Extend melody editor to all genres with code view and import
- Editor now works for regular rhythm patterns (rock, jazz, etc.) not just classical - Added code view toggle showing JS array notation for notes/patterns - Added import panel for pasting JSON note data or loading .json files - Preview mode handles both classical (synth orchestra) and regular (drum sounds) - buildNotes() now checks localStorage for user-saved pattern edits - Fixed edPreview() to play drum patterns with proper bar looping https://claude.ai/code/session_01TFVkxq4PYhkSFGYHEi4uqY
This commit is contained in:
+226
-65
@@ -190,11 +190,31 @@ canvas{display:block;}
|
|||||||
<button class="smbtn" onclick="edPreview()" id="ed-pv-btn">▶ Preview</button>
|
<button class="smbtn" onclick="edPreview()" id="ed-pv-btn">▶ Preview</button>
|
||||||
<button class="smbtn" onclick="edSave()" style="background:#38a169;">💾 Save</button>
|
<button class="smbtn" onclick="edSave()" style="background:#38a169;">💾 Save</button>
|
||||||
<button class="smbtn" onclick="edExport()">📋 Copy JSON</button>
|
<button class="smbtn" onclick="edExport()">📋 Copy JSON</button>
|
||||||
|
<button class="smbtn" onclick="edToggleCode()" id="ed-code-btn">{} Code</button>
|
||||||
|
<button class="smbtn" onclick="edToggleImport()">📥 Import</button>
|
||||||
<button class="smbtn red" onclick="edReset()">↺ Reset</button>
|
<button class="smbtn red" onclick="edReset()">↺ Reset</button>
|
||||||
<span id="ed-status" style="color:#888;font-size:11px;margin-left:auto;"></span>
|
<span id="ed-status" style="color:#888;font-size:11px;margin-left:auto;"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="ed-help" style="padding:4px 12px;background:#0d0d18;color:#666;font-size:11px;border-bottom:1px solid #222;">
|
<div id="ed-help" style="padding:4px 12px;background:#0d0d18;color:#666;font-size:11px;border-bottom:1px solid #222;">
|
||||||
Click = add note | Click note = select | Drag = move | Delete/Backspace = remove selected | Scroll = pan timeline
|
Click = add note | Click note = select | Drag = move | Delete/Backspace = remove selected | Scroll = pan timeline
|
||||||
|
<span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
|
||||||
|
</div>
|
||||||
|
<div id="ed-code-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;max-height:200px;overflow:auto;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
|
||||||
|
<span style="color:#888;font-size:11px;">JavaScript — edit and click "Apply Code" to update the timeline</span>
|
||||||
|
<button class="smbtn" onclick="edApplyCode()" style="font-size:11px;">Apply Code</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="ed-code" style="width:100%;height:140px;background:#111;color:#8f8;font-family:monospace;font-size:11px;border:1px solid #333;padding:4px;resize:vertical;" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="ed-import-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;">
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||||
|
<span style="color:#888;font-size:11px;">Paste JSON array of {t,l} notes or drag a .json file:</span>
|
||||||
|
<textarea id="ed-import-text" style="width:100%;height:60px;background:#111;color:#ff8;font-family:monospace;font-size:11px;border:1px solid #333;padding:4px;resize:vertical;" placeholder='[{"t":0,"l":0},{"t":0.5,"l":1}]'></textarea>
|
||||||
|
<button class="smbtn" onclick="edDoImport()" style="background:#3182ce;">Import</button>
|
||||||
|
<button class="smbtn" onclick="edImportMIDI()">MIDI File</button>
|
||||||
|
<input type="file" id="ed-midi-file" accept=".mid,.midi,.json" style="display:none;" onchange="edHandleFile(event)">
|
||||||
|
<span id="ed-import-status" style="color:#888;font-size:11px;"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="ed-canvas" style="display:block;width:100%;cursor:crosshair;"></canvas>
|
<canvas id="ed-canvas" style="display:block;width:100%;cursor:crosshair;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -3352,16 +3372,16 @@ function renderScores(){
|
|||||||
refreshMiniLB();
|
refreshMiniLB();
|
||||||
|
|
||||||
// ── MELODY EDITOR ─────────────────────────────────────
|
// ── MELODY EDITOR ─────────────────────────────────────
|
||||||
var edPiece=null,edMelody=[],edBpm=120,edSelected=-1,edScrollX=0,edDragging=false,edDragNote=-1,edDragOffX=0,edDragOffY=0,edPreviewing=false;
|
var edPiece=null,edMelody=[],edBpm=120,edBars=8,edSelected=-1,edScrollX=0,edDragging=false,edDragNote=-1,edDragOffX=0,edDragOffY=0,edPreviewing=false,edIsClassical=false,edGenre="";
|
||||||
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=0,ED_NOTE_W=14,ED_NOTE_H=16;
|
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=0,ED_NOTE_W=14,ED_NOTE_H=16;
|
||||||
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
|
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
|
||||||
var ED_NAMES=["Red","Yellow","Blue","Green","Kick"];
|
var ED_NAMES=["Red","Yellow","Blue","Green","Kick"];
|
||||||
|
|
||||||
function getEdCanvas(){return document.getElementById("ed-canvas");}
|
function getEdCanvas(){return document.getElementById("ed-canvas");}
|
||||||
function getEdCtx(){return getEdCanvas().getContext("2d");}
|
function getEdCtx(){return getEdCanvas().getContext("2d");}
|
||||||
function edPxPerSec(){return parseInt(document.getElementById("ed-zoom").value);}
|
function edPxPerUnit(){return parseInt(document.getElementById("ed-zoom").value);}
|
||||||
function edTimeToX(t){return t*edPxPerSec()-edScrollX;}
|
function edTimeToX(t){return t*edPxPerUnit()-edScrollX;}
|
||||||
function edXToTime(x){return(x+edScrollX)/edPxPerSec();}
|
function edXToTime(x){return(x+edScrollX)/edPxPerUnit();}
|
||||||
function edYToLane(y){
|
function edYToLane(y){
|
||||||
if(y<ED_HEAD)return-1;
|
if(y<ED_HEAD)return-1;
|
||||||
var laneY=y-ED_HEAD;
|
var laneY=y-ED_HEAD;
|
||||||
@@ -3375,22 +3395,32 @@ function edLaneToY(l){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditor(){
|
function openEditor(){
|
||||||
var pieces=SYNTH_GENRES[curGenre];
|
var pieces=SYNTH_GENRES[curGenre]||GENRES[curGenre];
|
||||||
if(!pieces||!pieces[curIdx])return;
|
if(!pieces||!pieces[curIdx])return;
|
||||||
edPiece=pieces[curIdx];
|
edPiece=pieces[curIdx];
|
||||||
if(!edPiece.isClassical)return;
|
edGenre=curGenre;
|
||||||
|
edIsClassical=!!edPiece.isClassical;
|
||||||
// Load saved edits or use original
|
// Load saved edits or use original
|
||||||
var saved=edLoadSaved(edPiece.n);
|
var saved=edLoadSaved(edPiece.n);
|
||||||
if(saved){
|
if(saved){
|
||||||
edMelody=saved.melody.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=saved.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
edBpm=saved.bpm||edPiece.bpm;
|
edBpm=saved.bpm||edPiece.bpm;
|
||||||
}else{
|
if(!edIsClassical)edBars=saved.bars||edPiece.bars||8;
|
||||||
|
}else if(edIsClassical){
|
||||||
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
edBpm=edPiece.bpm;
|
edBpm=edPiece.bpm;
|
||||||
|
}else{
|
||||||
|
// Regular rhythm: load the repeating pattern
|
||||||
|
edMelody=edPiece.p.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edBpm=edPiece.bpm;
|
||||||
|
edBars=edPiece.bars||8;
|
||||||
}
|
}
|
||||||
edSelected=-1;edScrollX=0;
|
edSelected=-1;edScrollX=0;
|
||||||
document.getElementById("ed-title").textContent="Editing: "+edPiece.n;
|
document.getElementById("ed-title").textContent="Editing: "+edPiece.n+(edPiece.composer?" ("+edPiece.composer+")":"");
|
||||||
document.getElementById("ed-bpm").value=edBpm;
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
|
document.getElementById("ed-mode-label").textContent=edIsClassical?"Classical (full timeline)":"Pattern (4-beat cycle, repeats "+edBars+"x)";
|
||||||
|
document.getElementById("ed-code-panel").style.display="none";
|
||||||
|
document.getElementById("ed-import-panel").style.display="none";
|
||||||
show("sed");
|
show("sed");
|
||||||
setTimeout(edResize,50);
|
setTimeout(edResize,50);
|
||||||
}
|
}
|
||||||
@@ -3430,29 +3460,40 @@ function edDraw(){
|
|||||||
ctx.fillText("Kick",4,ED_HEAD+4*ED_LANE_H+15);
|
ctx.fillText("Kick",4,ED_HEAD+4*ED_LANE_H+15);
|
||||||
ctx.globalAlpha=1;
|
ctx.globalAlpha=1;
|
||||||
|
|
||||||
// Grid lines (every beat and every second)
|
// Grid lines
|
||||||
var beatSec=60/edBpm;
|
if(edIsClassical){
|
||||||
var startBeat=Math.floor(edXToTime(0)/beatSec);
|
// Classical: grid by beats (seconds / (60/bpm))
|
||||||
var endBeat=Math.ceil(edXToTime(W)/beatSec);
|
var beatSec=60/edBpm;
|
||||||
for(var b=startBeat;b<=endBeat;b++){
|
var startBeat=Math.floor(edXToTime(0)/beatSec);
|
||||||
var bx=edTimeToX(b*beatSec);
|
var endBeat=Math.ceil(edXToTime(W)/beatSec);
|
||||||
if(bx<0||bx>W)continue;
|
for(var b=startBeat;b<=endBeat;b++){
|
||||||
ctx.strokeStyle=b%4===0?"#444":"#222";
|
var bx=edTimeToX(b*beatSec);
|
||||||
ctx.lineWidth=b%4===0?1.5:0.5;
|
if(bx<0||bx>W)continue;
|
||||||
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
ctx.strokeStyle=b%4===0?"#444":"#222";
|
||||||
if(b%4===0){
|
ctx.lineWidth=b%4===0?1.5:0.5;
|
||||||
ctx.fillStyle="#555";ctx.font="10px sans-serif";
|
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
||||||
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,ED_HEAD+H-4);
|
if(b%4===0){
|
||||||
|
ctx.fillStyle="#555";ctx.font="10px sans-serif";
|
||||||
|
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,H-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// Regular rhythm: grid by beat subdivisions (t=0 to 4)
|
||||||
|
// Draw 16th note grid (every 0.25 beats)
|
||||||
|
for(var sub=0;sub<=16;sub++){
|
||||||
|
var bt=sub*0.25;
|
||||||
|
var bx=edTimeToX(bt);
|
||||||
|
if(bx<0||bx>W)continue;
|
||||||
|
var isBeat=sub%4===0;
|
||||||
|
var isHalf=sub%2===0;
|
||||||
|
ctx.strokeStyle=isBeat?"#555":isHalf?"#333":"#1a1a2a";
|
||||||
|
ctx.lineWidth=isBeat?2:0.5;
|
||||||
|
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
||||||
|
if(isBeat){
|
||||||
|
ctx.fillStyle="#888";ctx.font="bold 11px sans-serif";
|
||||||
|
ctx.fillText("Beat "+(sub/4+1),bx+3,H-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Second markers
|
|
||||||
var startSec=Math.floor(edXToTime(0));
|
|
||||||
var endSec=Math.ceil(edXToTime(W));
|
|
||||||
for(var s=startSec;s<=endSec;s++){
|
|
||||||
var sx=edTimeToX(s);
|
|
||||||
if(sx<0||sx>W)continue;
|
|
||||||
ctx.strokeStyle="#333";ctx.lineWidth=0.5;
|
|
||||||
ctx.beginPath();ctx.moveTo(sx,0);ctx.lineTo(sx,ED_HEAD);ctx.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lane separator lines
|
// Lane separator lines
|
||||||
@@ -3477,15 +3518,16 @@ function edDraw(){
|
|||||||
ctx.globalAlpha=1;
|
ctx.globalAlpha=1;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Duration marker
|
// Duration / pattern end marker
|
||||||
if(edPiece){
|
if(edPiece){
|
||||||
var dx=edTimeToX(edPiece.duration);
|
var endTime=edIsClassical?(edPiece.duration||90):4; // 4 beats for pattern
|
||||||
|
var dx=edTimeToX(endTime);
|
||||||
if(dx>0&&dx<W){
|
if(dx>0&&dx<W){
|
||||||
ctx.strokeStyle="#e53e3e";ctx.lineWidth=2;ctx.setLineDash([4,4]);
|
ctx.strokeStyle="#e53e3e";ctx.lineWidth=2;ctx.setLineDash([4,4]);
|
||||||
ctx.beginPath();ctx.moveTo(dx,0);ctx.lineTo(dx,H);ctx.stroke();
|
ctx.beginPath();ctx.moveTo(dx,0);ctx.lineTo(dx,H);ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.fillStyle="#e53e3e";ctx.font="10px sans-serif";
|
ctx.fillStyle="#e53e3e";ctx.font="10px sans-serif";
|
||||||
ctx.fillText("END",dx+3,12);
|
ctx.fillText(edIsClassical?"END":"LOOP (4 beats)",dx+3,12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3520,9 +3562,12 @@ function edMouseDown(e){
|
|||||||
var t=edXToTime(mx);
|
var t=edXToTime(mx);
|
||||||
var l=edYToLane(my);
|
var l=edYToLane(my);
|
||||||
if(l>=0&&l<=4&&t>=0){
|
if(l>=0&&l<=4&&t>=0){
|
||||||
// Snap to nearest beat subdivision (1/4 beat)
|
// Snap to nearest subdivision
|
||||||
var snap=60/edBpm/4;
|
var snap=edIsClassical?(60/edBpm/4):0.25; // 1/4 beat in seconds or 1/16 note in beat units
|
||||||
t=Math.round(t/snap)*snap;
|
t=Math.round(t/snap)*snap;
|
||||||
|
t=parseFloat(t.toFixed(4));
|
||||||
|
// For regular rhythms, clamp to 0-4 range
|
||||||
|
if(!edIsClassical)t=Math.max(0,Math.min(3.99,t));
|
||||||
edMelody.push({t:t,l:l});
|
edMelody.push({t:t,l:l});
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
edSelected=edMelody.findIndex(function(m){return m.t===t&&m.l===l;});
|
edSelected=edMelody.findIndex(function(m){return m.t===t&&m.l===l;});
|
||||||
@@ -3568,14 +3613,15 @@ function edKeyDown(e){
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if(e.key==="ArrowLeft"&&edSelected>=0){
|
if(e.key==="ArrowLeft"&&edSelected>=0){
|
||||||
var snap=60/edBpm/4;
|
var snap=edIsClassical?(60/edBpm/4):0.25;
|
||||||
edMelody[edSelected].t=Math.max(0,edMelody[edSelected].t-snap);
|
edMelody[edSelected].t=Math.max(0,parseFloat((edMelody[edSelected].t-snap).toFixed(4)));
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
edDraw();e.preventDefault();
|
edDraw();e.preventDefault();
|
||||||
}
|
}
|
||||||
if(e.key==="ArrowRight"&&edSelected>=0){
|
if(e.key==="ArrowRight"&&edSelected>=0){
|
||||||
var snap2=60/edBpm/4;
|
var snap2=edIsClassical?(60/edBpm/4):0.25;
|
||||||
edMelody[edSelected].t+=snap2;
|
var maxT=edIsClassical?9999:3.99;
|
||||||
|
edMelody[edSelected].t=Math.min(maxT,parseFloat((edMelody[edSelected].t+snap2).toFixed(4)));
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
edDraw();e.preventDefault();
|
edDraw();e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -3604,23 +3650,39 @@ function edUpdateBpm(){edBpm=parseInt(document.getElementById("ed-bpm").value)||
|
|||||||
|
|
||||||
function edPreview(){
|
function edPreview(){
|
||||||
if(edPreviewing){stopPreview();edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";return;}
|
if(edPreviewing){stopPreview();edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";return;}
|
||||||
// Play the piece with current edits
|
|
||||||
var ac=getAc(true);ac.resume();
|
var ac=getAc(true);ac.resume();
|
||||||
var piece=edPiece;
|
var piece=edPiece;
|
||||||
for(var i=0;i<piece.bg.length;i++){
|
if(edIsClassical){
|
||||||
var n=piece.bg[i];
|
// Play background orchestration
|
||||||
playSynth(ac,n.f,ac.currentTime+0.1+n.t,n.d,n.i,n.v*1.2,previewNodes);
|
if(piece.bg)for(var i=0;i<piece.bg.length;i++){
|
||||||
|
var n=piece.bg[i];
|
||||||
|
playSynth(ac,n.f,ac.currentTime+0.1+n.t,n.d,n.i,n.v*1.2,previewNodes);
|
||||||
|
}
|
||||||
|
// Play melody notes as woodwind clicks
|
||||||
|
edMelody.forEach(function(m){
|
||||||
|
var freq=[329,392,440,349,220][m.l]||329;
|
||||||
|
playSynth(ac,freq,ac.currentTime+0.1+m.t,0.1,'woodwind',0.5,previewNodes);
|
||||||
|
});
|
||||||
|
var dur=piece.duration||90;
|
||||||
|
setTimeout(function(){if(edPreviewing){edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";}},dur*1000+500);
|
||||||
|
}else{
|
||||||
|
// Regular rhythm: play drum sounds for each note in pattern, looped over bars
|
||||||
|
var bs=60/edBpm;
|
||||||
|
var bars=edBars||8;
|
||||||
|
var drumFreqs=[329,392,440,349,220]; // red,yellow,blue,green,kick
|
||||||
|
var drumInst=['strings','woodwind','woodwind','strings','bass'];
|
||||||
|
for(var bar=0;bar<bars;bar++){
|
||||||
|
edMelody.forEach(function(m){
|
||||||
|
var t=ac.currentTime+0.1+(bar*4+m.t)*bs;
|
||||||
|
var freq=drumFreqs[m.l]||329;
|
||||||
|
playSynth(ac,freq,t,0.08,drumInst[m.l]||'woodwind',0.6,previewNodes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var totalDur=bars*4*bs;
|
||||||
|
setTimeout(function(){if(edPreviewing){edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";}},totalDur*1000+500);
|
||||||
}
|
}
|
||||||
// Also play a click/drum sound on each melody note so user hears the hits
|
|
||||||
edMelody.forEach(function(m){
|
|
||||||
var freq=[329,392,440,349,220][m.l]||329;
|
|
||||||
playSynth(ac,freq,ac.currentTime+0.1+m.t,0.1,'woodwind',0.5,previewNodes);
|
|
||||||
});
|
|
||||||
edPreviewing=true;
|
edPreviewing=true;
|
||||||
document.getElementById("ed-pv-btn").textContent="⏹ Stop";
|
document.getElementById("ed-pv-btn").textContent="⏹ Stop";
|
||||||
// Auto-stop after duration
|
|
||||||
var dur=piece.duration||90;
|
|
||||||
setTimeout(function(){if(edPreviewing){edPreviewing=false;document.getElementById("ed-pv-btn").textContent="▶ Preview";}},dur*1000+500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save/load edits to localStorage
|
// Save/load edits to localStorage
|
||||||
@@ -3629,38 +3691,134 @@ function edGetAllSaved(){try{return JSON.parse(localStorage.getItem(ED_STORAGE_K
|
|||||||
function edLoadSaved(name){var all=edGetAllSaved();return all[name]||null;}
|
function edLoadSaved(name){var all=edGetAllSaved();return all[name]||null;}
|
||||||
function edSave(){
|
function edSave(){
|
||||||
var all=edGetAllSaved();
|
var all=edGetAllSaved();
|
||||||
all[edPiece.n]={melody:edMelody.map(function(m){return{t:m.t,l:m.l};}),bpm:edBpm};
|
var entry={melody:edMelody.map(function(m){return{t:m.t,l:m.l};}),bpm:edBpm,isClassical:edIsClassical};
|
||||||
|
if(!edIsClassical)entry.bars=edBars;
|
||||||
|
all[edPiece.n]=entry;
|
||||||
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
document.getElementById("ed-status").textContent="Saved! ("+edMelody.length+" notes)";
|
document.getElementById("ed-status").textContent="Saved! ("+edMelody.length+" notes)";
|
||||||
}
|
}
|
||||||
function edReset(){
|
function edReset(){
|
||||||
if(!confirm("Reset to original melody? This discards your edits."))return;
|
if(!confirm("Reset to original? This discards your edits."))return;
|
||||||
var all=edGetAllSaved();delete all[edPiece.n];
|
var all=edGetAllSaved();delete all[edPiece.n];
|
||||||
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
if(edIsClassical){
|
||||||
|
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
}else{
|
||||||
|
edMelody=edPiece.p.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edBars=edPiece.bars||8;
|
||||||
|
}
|
||||||
edBpm=edPiece.bpm;
|
edBpm=edPiece.bpm;
|
||||||
document.getElementById("ed-bpm").value=edBpm;
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
edSelected=-1;edDraw();
|
edSelected=-1;edDraw();
|
||||||
}
|
}
|
||||||
function edExport(){
|
function edExport(){
|
||||||
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,melody:edMelody},null,2);
|
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,type:edIsClassical?"classical":"pattern",melody:edMelody},null,2);
|
||||||
navigator.clipboard.writeText(data).then(function(){
|
navigator.clipboard.writeText(data).then(function(){
|
||||||
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
// Fallback: show in prompt
|
|
||||||
prompt("Copy this JSON:",data);
|
prompt("Copy this JSON:",data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide edit button based on whether current genre has classical pieces
|
// Code view toggle — shows JS representation of current notes
|
||||||
function updateEditBtn(){
|
function edToggleCode(){
|
||||||
var pieces=SYNTH_GENRES[curGenre];
|
var panel=document.getElementById("ed-code-panel");
|
||||||
var btn=document.getElementById("edit-btn");
|
if(panel.style.display==="none"){
|
||||||
if(pieces&&pieces[curIdx]&&pieces[curIdx].isClassical){
|
panel.style.display="block";
|
||||||
btn.style.display="";
|
edUpdateCode();
|
||||||
}else{
|
}else{
|
||||||
btn.style.display="none";
|
panel.style.display="none";
|
||||||
}
|
}
|
||||||
|
setTimeout(edResize,50);
|
||||||
|
}
|
||||||
|
function edUpdateCode(){
|
||||||
|
var ta=document.getElementById("ed-code");
|
||||||
|
if(edIsClassical){
|
||||||
|
// Show melody array as JS
|
||||||
|
var lines=edMelody.map(function(m){
|
||||||
|
return" {t:"+m.t.toFixed(3)+", l:"+m.l+"}";
|
||||||
|
});
|
||||||
|
ta.value="// "+edPiece.n+" — melody ("+edMelody.length+" notes)\n"+
|
||||||
|
"// BPM: "+edBpm+", Duration: "+(edPiece.duration||"?")+"s\n"+
|
||||||
|
"// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
|
||||||
|
"melody: [\n"+lines.join(",\n")+"\n]";
|
||||||
|
}else{
|
||||||
|
// Show pattern array as JS
|
||||||
|
var lines2=edMelody.map(function(m){
|
||||||
|
return" {t:"+m.t+", l:"+m.l+"}";
|
||||||
|
});
|
||||||
|
ta.value="// "+edPiece.n+" — pattern ("+edMelody.length+" notes, repeats "+edBars+"x)\n"+
|
||||||
|
"// BPM: "+edBpm+", Bars: "+edBars+"\n"+
|
||||||
|
"// t = beat offset (0-4), Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
|
||||||
|
"p: [\n"+lines2.join(",\n")+"\n]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function edApplyCode(){
|
||||||
|
var ta=document.getElementById("ed-code");
|
||||||
|
var code=ta.value;
|
||||||
|
// Extract the array from the code text
|
||||||
|
var match=code.match(/\[\s*([\s\S]*)\s*\]/);
|
||||||
|
if(!match){document.getElementById("ed-status").textContent="Could not find [...] array in code";return;}
|
||||||
|
try{
|
||||||
|
// Parse the array — handle JS object notation {t:1, l:2} as JSON
|
||||||
|
var arrStr="["+match[1].replace(/(\w+)\s*:/g,'"$1":')+"]";
|
||||||
|
var arr=JSON.parse(arrStr);
|
||||||
|
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||||
|
.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
edSelected=-1;edDraw();
|
||||||
|
document.getElementById("ed-status").textContent="Applied! "+edMelody.length+" notes loaded from code.";
|
||||||
|
}catch(e){
|
||||||
|
document.getElementById("ed-status").textContent="Parse error: "+e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import panel
|
||||||
|
function edToggleImport(){
|
||||||
|
var panel=document.getElementById("ed-import-panel");
|
||||||
|
panel.style.display=panel.style.display==="none"?"block":"none";
|
||||||
|
setTimeout(edResize,50);
|
||||||
|
}
|
||||||
|
function edDoImport(){
|
||||||
|
var text=document.getElementById("ed-import-text").value.trim();
|
||||||
|
if(!text){document.getElementById("ed-import-status").textContent="Paste some data first";return;}
|
||||||
|
try{
|
||||||
|
var data=JSON.parse(text);
|
||||||
|
// Accept array of {t,l} or object with melody property
|
||||||
|
var arr=Array.isArray(data)?data:(data.melody||data.p||[]);
|
||||||
|
if(!Array.isArray(arr)||arr.length===0){document.getElementById("ed-import-status").textContent="No notes found";return;}
|
||||||
|
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||||
|
.map(function(m){return{t:m.t,l:m.l};});
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
if(data.bpm)edBpm=data.bpm;
|
||||||
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
|
edSelected=-1;edDraw();edUpdateCode();
|
||||||
|
document.getElementById("ed-import-status").textContent="Imported "+edMelody.length+" notes!";
|
||||||
|
}catch(e){
|
||||||
|
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)";
|
||||||
|
}
|
||||||
|
e.target.value="";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide edit button — works for all genres
|
||||||
|
function updateEditBtn(){
|
||||||
|
var pieces=SYNTH_GENRES[curGenre]||GENRES[curGenre];
|
||||||
|
var btn=document.getElementById("edit-btn");
|
||||||
|
btn.style.display=(pieces&&pieces[curIdx])?"":"none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GAME START ─────────────────────────────────────────
|
// ── GAME START ─────────────────────────────────────────
|
||||||
@@ -3686,9 +3844,12 @@ function stopGame(){playing=false;if(mp3Src){try{mp3Src.stop();}catch(e){}mp3Src
|
|||||||
function resetStats(){cScore=0;cStreak=0;cTotal=0;cGood=0;cMaxStreak=0;gameEndTime=null;}
|
function resetStats(){cScore=0;cStreak=0;cTotal=0;cGood=0;cMaxStreak=0;gameEndTime=null;}
|
||||||
|
|
||||||
function buildNotes(r){
|
function buildNotes(r){
|
||||||
const bs=60/r.bpm/gameSpeed,bars=r.bars||8,res=[];
|
var saved=edLoadSaved(r.n);
|
||||||
|
var pattern=saved?saved.melody:r.p;
|
||||||
|
var bpm=saved?saved.bpm:r.bpm;
|
||||||
|
const bs=60/bpm/gameSpeed,bars=r.bars||8,res=[];
|
||||||
for(let bar=0;bar<bars;bar++)
|
for(let bar=0;bar<bars;bar++)
|
||||||
for(const p of r.p)
|
for(const p of pattern)
|
||||||
if(activePads[p.l])
|
if(activePads[p.l])
|
||||||
res.push({time:(bar*4+p.t)*bs,lane:p.l,hit:false,missed:false});
|
res.push({time:(bar*4+p.t)*bs,lane:p.l,hit:false,missed:false});
|
||||||
return res.sort((a,b)=>a.time-b.time);
|
return res.sort((a,b)=>a.time-b.time);
|
||||||
|
|||||||
Reference in New Issue
Block a user