Add editor rhythm notation, time signatures, play-from-position, fix Drunken Sailor

Editor improvements:
- Musical notation symbols (♩♪𝅗𝅥𝅝) displayed on notes with duration
- Number keys 1-5 set note values (whole/half/quarter/eighth/sixteenth), 6-8 for dotted
- Time signature selector (4/4, 3/4, 6/8, 9/8, etc.) with measure-aware grid lines
- Double-click header to start playback from any position
- Singalong/non-Classical pieces no longer play background orchestra in editor preview
  (only Classical genre gets conductor mode with bg track)
- Status bar shows note value name when selected

Drunken Sailor fix:
- Corrected rhythm to quarter-eighth-eighth pattern per sheet music
- Melody notes now store duration and frequency for proper editor display

Singalong pieces now tagged with isSingalong flag and store duration on melody notes.

https://claude.ai/code/session_01AqTWV8hPy2XcGP5KY2Q8zv
This commit is contained in:
Claude
2026-04-02 14:31:07 +00:00
parent 2c95b9130f
commit 34f65f4ead
+173 -69
View File
@@ -195,6 +195,7 @@ canvas{display:block;}
<button class="smbtn" onclick="closeEditor()">← Back</button> <button class="smbtn" onclick="closeEditor()">← Back</button>
<span id="ed-title" style="font-weight:bold;color:#ccc;margin-right:8px;">Editing: —</span> <span id="ed-title" style="font-weight:bold;color:#ccc;margin-right:8px;">Editing: —</span>
<label style="color:#aaa;font-size:12px;">BPM: <input type="number" id="ed-bpm" value="120" min="30" max="300" style="width:55px;background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px 4px;border-radius:3px;" onchange="edUpdateBpm()"></label> <label style="color:#aaa;font-size:12px;">BPM: <input type="number" id="ed-bpm" value="120" min="30" max="300" style="width:55px;background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px 4px;border-radius:3px;" onchange="edUpdateBpm()"></label>
<label style="color:#aaa;font-size:12px;">Time: <select id="ed-timesig" style="background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px;border-radius:3px;font-size:12px;" onchange="edUpdateTimeSig()"><option>4/4</option><option>3/4</option><option>2/4</option><option>6/8</option><option>9/8</option><option>5/4</option><option>7/8</option><option>12/8</option></select></label>
<label style="color:#aaa;font-size:12px;">Zoom: <input type="range" id="ed-zoom" min="20" max="200" value="80" style="width:80px;" oninput="edDraw()"></label> <label style="color:#aaa;font-size:12px;">Zoom: <input type="range" id="ed-zoom" min="20" max="200" value="80" style="width:80px;" oninput="edDraw()"></label>
<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>
@@ -209,7 +210,7 @@ canvas{display:block;}
<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 | Click note=select | Drag=move | Drag right edge=resize duration | D=add duration | Shift+D=remove duration | Shift+drag=select range | Ctrl+C/V=copy/paste | Ctrl+A=select all | Esc=deselect | Del=delete | Scroll=pan Click=add | Click note=select | Drag=move | Drag right edge=resize duration | 1-5=set note value (whole/half/quarter/eighth/sixteenth) | D=add duration | Shift+D=remove | Shift+drag=select range | Ctrl+C/V=copy/paste | Ctrl+A=select all | Esc=deselect | Del=delete | Scroll=pan | Dbl-click header=play from here
<span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span> <span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
</div> </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 id="ed-code-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;max-height:200px;overflow:auto;">
@@ -3952,8 +3953,9 @@ const SINGALONG_PIECES=(function(){
var C3=NT.C3,D3=NT.D3,E3=NT.E3,F3=NT.F3,G3=NT.G3,Ab3=NT.Ab3,A3=NT.A3,B3=NT.B3, var C3=NT.C3,D3=NT.D3,E3=NT.E3,F3=NT.F3,G3=NT.G3,Ab3=NT.Ab3,A3=NT.A3,B3=NT.B3,
C4=NT.C4,D4=NT.D4,E4=NT.E4,F4=NT.F4,G4=NT.G4,A4=NT.A4,Bb4=NT.Bb4,B4=NT.B4, C4=NT.C4,D4=NT.D4,E4=NT.E4,F4=NT.F4,G4=NT.G4,A4=NT.A4,Bb4=NT.Bb4,B4=NT.B4,
C5=NT.C5,D5=NT.D5,E5=NT.E5,F5=NT.F5,G5=NT.G5; C5=NT.C5,D5=NT.D5,E5=NT.E5,F5=NT.F5,G5=NT.G5;
function pub(name,origin,bpm,theme,lanes,durations,passes,root){ function pub(name,origin,bpm,theme,lanes,durations,passes,root,opts){
var bg=[],mel=[],t=0,q=30/bpm; var bg=[],mel=[],t=0,q=30/bpm;
var timeSig=(opts&&opts.timeSig)||[4,4];
for(var rep=0;rep<passes;rep++){ for(var rep=0;rep<passes;rep++){
var v=0.22+rep*0.07; var v=0.22+rep*0.07;
var inst=rep<Math.floor(passes*0.4)?'woodwind':rep<Math.floor(passes*0.7)?'strings':'brass'; var inst=rep<Math.floor(passes*0.4)?'woodwind':rep<Math.floor(passes*0.7)?'strings':'brass';
@@ -3962,14 +3964,14 @@ const SINGALONG_PIECES=(function(){
bg.push({t:t,f:f,d:d*0.85,i:inst,v:v}); bg.push({t:t,f:f,d:d*0.85,i:inst,v:v});
if(rep>1)bg.push({t:t,f:f/2,d:d*0.85,i:'bass',v:v*0.35}); if(rep>1)bg.push({t:t,f:f/2,d:d*0.85,i:'bass',v:v*0.35});
if(rep>Math.floor(passes*0.6))bg.push({t:t,f:f,d:d*0.85,i:'brass',v:v*0.4}); if(rep>Math.floor(passes*0.6))bg.push({t:t,f:f,d:d*0.85,i:'brass',v:v*0.4});
mel.push({t:t,l:lanes[i]});t+=d; mel.push({t:t,l:lanes[i],d:d,f:f});t+=d;
}); });
t+=0.4; t+=0.4;
} }
var r=root||theme[0]; var r=root||theme[0];
[r,r*5/4,r*3/2,r*2].forEach(function(f){bg.push({t:t,f:f,d:2.0,i:'brass',v:0.5});bg.push({t:t,f:f,d:2.0,i:'strings',v:0.4});}); [r,r*5/4,r*3/2,r*2].forEach(function(f){bg.push({t:t,f:f,d:2.0,i:'brass',v:0.5});bg.push({t:t,f:f,d:2.0,i:'strings',v:0.4});});
bg.push({t:t,f:r/2,d:1.5,i:'timpani',v:0.55});mel.push({t:t,l:4}); bg.push({t:t,f:r/2,d:1.5,i:'timpani',v:0.55});mel.push({t:t,l:4,d:1.5});
return {n:name,composer:origin,bpm:bpm,duration:Math.ceil(t+2.5),isClassical:true,bg:bg,melody:mel}; return {n:name,composer:origin,bpm:bpm,duration:Math.ceil(t+2.5),isClassical:true,isSingalong:true,timeSig:timeSig,bg:bg,melody:mel};
} }
var drunken=(function(){ var drunken=(function(){
// Accurate "What Shall We Do With A Drunken Sailor" from sheet music // Accurate "What Shall We Do With A Drunken Sailor" from sheet music
@@ -3978,7 +3980,7 @@ const SINGALONG_PIECES=(function(){
var e=q/2; // eighth note var e=q/2; // eighth note
function note(f,dur,lane,inst,v){ function note(f,dur,lane,inst,v){
bg.push({t:t,f:f,d:dur*0.85,i:inst||'strings',v:v||0.35}); bg.push({t:t,f:f,d:dur*0.85,i:inst||'strings',v:v||0.35});
mel.push({t:t,l:lane}); mel.push({t:t,l:lane,d:dur,f:f});
t+=dur; t+=dur;
} }
function chord(fs,dur,inst,v){ function chord(fs,dur,inst,v){
@@ -4002,28 +4004,24 @@ const SINGALONG_PIECES=(function(){
} }
// M1-2: "What shall we do with a drunken sailor?" (Am) // M1-2: "What shall we do with a drunken sailor?" (Am)
// A A A, A A A, A A, D F A // Rhythm: q ee q ee | q q q q
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,q,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v);
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,q,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v);
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(B4,q,3,inst,v); note(C5,q,1,inst,v); note(A4,q,0,inst,v); note(A4,q,0,inst,v);
note(A4,e,0,inst,v);
note(D4,q,1,inst,v); note(F4,q,1,inst,v); note(A4,q*2,0,inst,v);
// M3-4: "What shall we do with a drunken sailor?" (G) // M3-4: "What shall we do with a drunken sailor?" (G)
note(G4,e,3,inst,v); note(G4,e,3,inst,v); note(G4,e,3,inst,v); // Rhythm: q ee q ee | q q q q
note(G4,e,3,inst,v); note(G4,e,3,inst,v); note(G4,q,3,inst,v); note(G4,e,3,inst,v); note(G4,e,3,inst,v);
note(G4,e,3,inst,v); note(G4,e,3,inst,v); note(G4,q,3,inst,v); note(G4,e,3,inst,v); note(G4,e,3,inst,v);
note(G4,e,3,inst,v); note(A4,q,0,inst,v); note(B4,q,3,inst,v); note(G4,q,3,inst,v); note(G4,q,3,inst,v);
note(C4,q,2,inst,v); note(E4,q,2,inst,v); note(G4,q*2,3,inst,v);
// M5-6: "What shall we do with a drunken sailor" (Am) // M5-6: "What shall we do with a drunken sailor" (Am)
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v); // Rhythm: q ee q ee | q q q q
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,q,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v);
note(A4,e,0,inst,v); note(A4,e,0,inst,v); note(A4,q,0,inst,v); note(A4,e,0,inst,v); note(A4,e,0,inst,v);
note(A4,e,0,inst,v); note(B4,q,3,inst,v); note(C5,q,1,inst,v); note(D5,q,1,inst,v); note(D5,q,1,inst,v);
note(B4,q,3,inst,v); note(C5,q,1,inst,v); note(D5,q*2,1,inst,v);
// M7-8: "early in the morning?" (C-A-G-E-D-D) // M7-8: "early in the morning?" (C-A-G-E | D-D)
note(C5,q,0,inst,v); note(A4,q,0,inst,v); note(C5,q,0,inst,v); note(A4,q,0,inst,v);
note(G4,q,3,inst,v); note(E4,q,2,inst,v); note(G4,q,3,inst,v); note(E4,q,2,inst,v);
note(D4,q*2,1,inst,v); note(D4,q*2,1,inst,v); note(D4,q*2,1,inst,v); note(D4,q*2,1,inst,v);
@@ -4056,7 +4054,7 @@ const SINGALONG_PIECES=(function(){
// Finale chord // Finale chord
[A3,C4,E4,A4].forEach(function(f){bg.push({t:t,f:f,d:2.5,i:'brass',v:0.55});bg.push({t:t,f:f,d:2.5,i:'strings',v:0.4});}); [A3,C4,E4,A4].forEach(function(f){bg.push({t:t,f:f,d:2.5,i:'brass',v:0.55});bg.push({t:t,f:f,d:2.5,i:'strings',v:0.4});});
bg.push({t:t,f:A3/2,d:2.0,i:'timpani',v:0.6});mel.push({t:t,l:4}); bg.push({t:t,f:A3/2,d:2.0,i:'timpani',v:0.6});mel.push({t:t,l:4});
return {n:"Drunken Sailor",composer:"Sea Shanty (Trad.)",bpm:140,duration:Math.ceil(t+3),isClassical:true,bg:bg,melody:mel}; return {n:"Drunken Sailor",composer:"Sea Shanty (Trad.)",bpm:140,duration:Math.ceil(t+3),isClassical:true,isSingalong:true,timeSig:[4,4],bg:bg,melody:mel};
})(); })();
var molly=pub("Molly Malone","Irish (1880s)",100, var molly=pub("Molly Malone","Irish (1880s)",100,
[C4,E4,E4,E4,F4,G4,G4,G4,A4,G4,F4,E4,D4,E4,F4,G4,A4,C5,C5,B4,A4,G4,F4,E4,D4,C4,E4,G4,C5,C5,B4,A4,G4,F4,E4,D4,C4], [C4,E4,E4,E4,F4,G4,G4,G4,A4,G4,F4,E4,D4,E4,F4,G4,A4,C5,C5,B4,A4,G4,F4,E4,D4,C4,E4,G4,C5,C5,B4,A4,G4,F4,E4,D4,C4],
@@ -4170,7 +4168,7 @@ const SINGALONG_PIECES=(function(){
// Finale: big Am chord // Finale: big Am chord
[A3,C4,E4,A4].forEach(function(f){bg.push({t:t,f:f,d:2.5,i:'brass',v:0.55});bg.push({t:t,f:f,d:2.5,i:'choir',v:0.4});}); [A3,C4,E4,A4].forEach(function(f){bg.push({t:t,f:f,d:2.5,i:'brass',v:0.55});bg.push({t:t,f:f,d:2.5,i:'choir',v:0.4});});
bg.push({t:t,f:A3/2,d:2.0,i:'timpani',v:0.6});mel.push({t:t,l:4}); bg.push({t:t,f:A3/2,d:2.0,i:'timpani',v:0.6});mel.push({t:t,l:4});
return {n:"Wellerman",composer:"NZ Sea Shanty (1860s)",bpm:125,duration:Math.ceil(t+3),isClassical:true,bg:bg,melody:mel}; return {n:"Wellerman",composer:"NZ Sea Shanty (1860s)",bpm:125,duration:Math.ceil(t+3),isClassical:true,isSingalong:true,timeSig:[4,4],bg:bg,melody:mel};
})(); })();
// ── Leave Her Johnny ── // ── Leave Her Johnny ──
var leavejohnny=pub("Leave Her Johnny","Sea Shanty (Trad.)",105, var leavejohnny=pub("Leave Her Johnny","Sea Shanty (Trad.)",105,
@@ -4875,9 +4873,11 @@ refreshMiniLB();
// ── MELODY EDITOR ───────────────────────────────────── // ── MELODY EDITOR ─────────────────────────────────────
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 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 edIsSingalong=false; // true = no bg orchestra in preview
var edTimeSig=[4,4]; // time signature [beats per measure, beat unit]
var edResizing=false,edResizeNote=-1; // right-edge duration drag var edResizing=false,edResizeNote=-1; // right-edge duration drag
var edSelRange=null,edSelecting=false,edClipboard=[]; // range select + copy/paste var edSelRange=null,edSelecting=false,edClipboard=[]; // range select + copy/paste
var edPlayhead=-1,edPlayheadStart=0,edPlayheadRAF=0; // preview playhead var edPlayhead=-1,edPlayheadStart=0,edPlayheadRAF=0,edPlayheadOffset=0; // preview playhead + start offset
var edShowLabels=false; // toggle note labels on canvas var edShowLabels=false; // toggle note labels on canvas
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=20,ED_NOTE_W=14,ED_NOTE_H=16,ED_MIN_DUR=0.05; var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=20,ED_NOTE_W=14,ED_NOTE_H=16,ED_MIN_DUR=0.05;
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"]; var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
@@ -4913,24 +4913,29 @@ function openEditor(){
edPiece=pieces[curIdx]; edPiece=pieces[curIdx];
edGenre=curGenre; edGenre=curGenre;
edIsClassical=!!edPiece.isClassical; edIsClassical=!!edPiece.isClassical;
edIsSingalong=!!edPiece.isSingalong||(curGenre!=="Classical"&&edIsClassical);
edTimeSig=edPiece.timeSig||[4,4];
// 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){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;}); edMelody=saved.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;if(m.f)n.f=m.f;return n;});
edBpm=saved.bpm||edPiece.bpm; edBpm=saved.bpm||edPiece.bpm;
if(saved.timeSig)edTimeSig=saved.timeSig;
if(!edIsClassical)edBars=saved.bars||edPiece.bars||8; if(!edIsClassical)edBars=saved.bars||edPiece.bars||8;
}else if(edIsClassical){ }else if(edIsClassical){
edMelody=edPiece.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;}); edMelody=edPiece.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;if(m.f)n.f=m.f;return n;});
edBpm=edPiece.bpm; edBpm=edPiece.bpm;
}else{ }else{
edMelody=edPiece.p.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;}); edMelody=edPiece.p.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
edBpm=edPiece.bpm; edBpm=edPiece.bpm;
edBars=edPiece.bars||8; edBars=edPiece.bars||8;
} }
edSelected=-1;edScrollX=0;edSelRange=null;edClipboard=[];edPlayhead=-1; edSelected=-1;edScrollX=0;edSelRange=null;edClipboard=[];edPlayhead=-1;edPlayheadOffset=0;
document.getElementById("ed-title").textContent="Editing: "+edPiece.n+(edPiece.composer?" ("+edPiece.composer+")":""); 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-timesig").value=edTimeSig[0]+"/"+edTimeSig[1];
var modeLabel=edIsClassical?(edIsSingalong?"Singalong (full timeline)":"Classical (full timeline, conductor)"):"Pattern (4-beat cycle, repeats "+edBars+"x)";
document.getElementById("ed-mode-label").textContent=modeLabel;
document.getElementById("ed-code-panel").style.display="none"; document.getElementById("ed-code-panel").style.display="none";
document.getElementById("ed-import-panel").style.display="none"; document.getElementById("ed-import-panel").style.display="none";
show("sed"); show("sed");
@@ -4977,28 +4982,52 @@ function edDraw(){
ctx.fillStyle="#0a0a12"; ctx.fillStyle="#0a0a12";
ctx.fillRect(0,0,W,ED_HEAD); ctx.fillRect(0,0,W,ED_HEAD);
// Grid lines + measure numbers // Grid lines + measure numbers (time-signature aware)
var beatsPerMeasure=edTimeSig[0];
var beatUnit=edTimeSig[1]; // 4=quarter, 8=eighth
var beatSec=edQuarterSec(); // quarter note duration
var subBeatSec=beatSec/2; // eighth note grid
if(beatUnit===8) subBeatSec=beatSec/2; // for x/8, sub-grid is sixteenth
var measSec=edMeasureSec();
if(edIsClassical){ if(edIsClassical){
var beatSec=60/edBpm; // Draw measure and beat lines
var startBeat=Math.floor(edXToTime(0)/beatSec); var startTime=edXToTime(0);
var endBeat=Math.ceil(edXToTime(W)/beatSec); var endTime=edXToTime(W);
for(var b=startBeat;b<=endBeat;b++){ var startMeas=Math.max(0,Math.floor(startTime/measSec));
var bx=edTimeToX(b*beatSec); var endMeas=Math.ceil(endTime/measSec)+1;
if(bx<0||bx>W)continue; for(var mi=startMeas;mi<=endMeas;mi++){
var isMeasure=b%4===0; var measT=mi*measSec;
ctx.strokeStyle=isMeasure?"#444":"#222"; // Draw each beat within measure
ctx.lineWidth=isMeasure?1.5:0.5; var bCount=beatsPerMeasure;
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke(); var oneBeatSec=(beatUnit===8)?(beatSec/2):beatSec;
if(isMeasure){ for(var bi=0;bi<bCount;bi++){
// Measure number in header var bt2=measT+bi*oneBeatSec;
var mNum=Math.floor(b/4)+1; var bx=edTimeToX(bt2);
if(bx<-10||bx>W+10)continue;
var isMeasStart=bi===0;
ctx.strokeStyle=isMeasStart?"#555":"#282838";
ctx.lineWidth=isMeasStart?2:0.5;
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
// Sub-beat (eighth grid for x/4, sixteenth for x/8)
var subX=edTimeToX(bt2+oneBeatSec/2);
if(subX>0&&subX<W){
ctx.strokeStyle="#1a1a2a";ctx.lineWidth=0.3;
ctx.beginPath();ctx.moveTo(subX,ED_HEAD);ctx.lineTo(subX,H);ctx.stroke();
}
}
// Measure number and time sig in header
var mx=edTimeToX(measT);
if(mx>-20&&mx<W+20){
ctx.fillStyle="#888";ctx.font="bold 11px sans-serif"; ctx.fillStyle="#888";ctx.font="bold 11px sans-serif";
ctx.fillText("M"+mNum,bx+3,ED_HEAD-5); ctx.fillText("M"+(mi+1),mx+3,ED_HEAD-5);
// Time at bottom
ctx.fillStyle="#555";ctx.font="10px sans-serif"; ctx.fillStyle="#555";ctx.font="10px sans-serif";
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,H-4); ctx.fillText(measT.toFixed(1)+"s",mx+2,H-4);
} }
} }
// Show time signature in header
ctx.fillStyle="#666";ctx.font="bold 10px sans-serif";
ctx.fillText(edTimeSig[0]+"/"+edTimeSig[1],edTimeToX(0)+1,ED_HEAD-5+40);
}else{ }else{
for(var sub=0;sub<=16;sub++){ for(var sub=0;sub<=16;sub++){
var bt=sub*0.25; var bt=sub*0.25;
@@ -5080,11 +5109,23 @@ function edDraw(){
ctx.strokeStyle="#805ad5";ctx.lineWidth=1.5; ctx.strokeStyle="#805ad5";ctx.lineWidth=1.5;
ctx.strokeRect(nx-1,ny-1,noteW+2,ED_NOTE_H+2); ctx.strokeRect(nx-1,ny-1,noteW+2,ED_NOTE_H+2);
} }
// Musical notation symbol on note (always shown if duration exists)
if(dur>0){
var di=edDurInfo(dur);
ctx.font="bold 13px sans-serif";
ctx.fillStyle=isPlaying?"#000":"rgba(255,255,255,0.9)";
ctx.textAlign="center";
ctx.fillText(di.sym,nx+noteW/2,ny+ED_NOTE_H/2+4);
ctx.textAlign="left";
}
// Note label (when Code/Labels toggled on, or for selected note) // Note label (when Code/Labels toggled on, or for selected note)
if(edShowLabels||idx===edSelected){ if(edShowLabels||idx===edSelected){
ctx.font="9px monospace"; ctx.font="9px monospace";
var label="t:"+m.t.toFixed(edIsClassical?2:2)+",l:"+m.l; var label="t:"+m.t.toFixed(edIsClassical?2:2)+",l:"+m.l;
if(dur>0)label+=",d:"+dur.toFixed(2); if(dur>0){
var di2=edDurInfo(dur);
label+=",d:"+dur.toFixed(2)+"s ("+di2.name+")";
}
// Position label above the note, inside its lane // Position label above the note, inside its lane
var lx=nx; var lx=nx;
var ly=ny-4; var ly=ny-4;
@@ -5130,8 +5171,8 @@ function edDraw(){
}else if(edSelected>=0){ }else if(edSelected>=0){
var sm=edMelody[edSelected]; var sm=edMelody[edSelected];
statusTxt+=" | Selected #"+edSelected+" (t="+sm.t.toFixed(3)+"s, "+ED_NAMES[sm.l]; statusTxt+=" | Selected #"+edSelected+" (t="+sm.t.toFixed(3)+"s, "+ED_NAMES[sm.l];
if(sm.d>0)statusTxt+=", dur="+sm.d.toFixed(3)+"s"; if(sm.d>0){var sdi=edDurInfo(sm.d);statusTxt+=", "+sdi.sym+" "+sdi.name+" ("+sm.d.toFixed(3)+"s)";}
statusTxt+=") | D=add duration, Shift+D=remove"; statusTxt+=") | 1-5=note value, D=+dur, Shift+D=remove";
}else{ }else{
statusTxt+=" | Click=add, Shift+drag=select range"; statusTxt+=" | Click=add, Shift+drag=select range";
} }
@@ -5165,6 +5206,15 @@ function edMouseDown(e){
var cv=getEdCanvas(),rect=cv.getBoundingClientRect(); var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
var mx=e.clientX-rect.left,my=e.clientY-rect.top; var mx=e.clientX-rect.left,my=e.clientY-rect.top;
// Double-click on header = play from this position
if(e.detail===2&&my<ED_HEAD){
var startT=edXToTime(mx);
if(startT<0)startT=0;
edPlayheadOffset=startT;
edPreview(startT);
return;
}
// Shift+click = range select // Shift+click = range select
if(e.shiftKey){ if(e.shiftKey){
var t=edXToTime(mx); var t=edXToTime(mx);
@@ -5370,6 +5420,23 @@ function edKeyDown(e){
edMelody[edSelected].l=Math.min(4,edMelody[edSelected].l+1); edMelody[edSelected].l=Math.min(4,edMelody[edSelected].l+1);
edDraw();e.preventDefault(); edDraw();e.preventDefault();
} }
// Number keys 1-5: set note value on selected note
// 1=whole, 2=half, 3=quarter, 4=eighth, 5=sixteenth
if(e.key>="1"&&e.key<="5"&&edSelected>=0&&!e.ctrlKey&&!e.metaKey){
var q=edQuarterSec();
var durMap={"1":q*4,"2":q*2,"3":q,"4":q/2,"5":q/4};
edMelody[edSelected].d=parseFloat(durMap[e.key].toFixed(4));
edDraw();e.preventDefault();
return;
}
// 6=dotted half, 7=dotted quarter, 8=dotted eighth
if(e.key>="6"&&e.key<="8"&&edSelected>=0&&!e.ctrlKey&&!e.metaKey){
var q2=edQuarterSec();
var durMap2={"6":q2*3,"7":q2*1.5,"8":q2*0.75};
edMelody[edSelected].d=parseFloat(durMap2[e.key].toFixed(4));
edDraw();e.preventDefault();
return;
}
// D key — toggle/set duration on selected note // D key — toggle/set duration on selected note
if(e.key==="d"&&edSelected>=0&&!e.ctrlKey&&!e.metaKey){ if(e.key==="d"&&edSelected>=0&&!e.ctrlKey&&!e.metaKey){
var n=edMelody[edSelected]; var n=edMelody[edSelected];
@@ -5400,6 +5467,37 @@ function edKeyDown(e){
})(); })();
function edUpdateBpm(){edBpm=parseInt(document.getElementById("ed-bpm").value)||120;edDraw();} function edUpdateBpm(){edBpm=parseInt(document.getElementById("ed-bpm").value)||120;edDraw();}
function edUpdateTimeSig(){
var val=document.getElementById("ed-timesig").value;
var parts=val.split("/");
edTimeSig=[parseInt(parts[0])||4,parseInt(parts[1])||4];
edDraw();
}
// Get quarter note duration in seconds at current BPM
function edQuarterSec(){return 60/edBpm;}
// Get measure duration in seconds based on time signature
function edMeasureSec(){
var q=edQuarterSec();
// For x/4 time: beats * quarter note duration
// For x/8 time: beats * eighth note duration
if(edTimeSig[1]===8) return edTimeSig[0]*(q/2);
return edTimeSig[0]*q;
}
// Duration name and symbol for a note duration in seconds
function edDurInfo(durSec){
var q=edQuarterSec();
var ratio=durSec/q;
// Map ratio to note value
if(ratio>=3.5) return {sym:"\uD834\uDD5D",name:"whole",val:4};
if(ratio>=2.5) return {sym:"\uD834\uDD5E.",name:"dotted half",val:3};
if(ratio>=1.8) return {sym:"\uD834\uDD5E",name:"half",val:2};
if(ratio>=1.3) return {sym:"\u2669.",name:"dotted quarter",val:1.5};
if(ratio>=0.8) return {sym:"\u2669",name:"quarter",val:1};
if(ratio>=0.6) return {sym:"\u266A.",name:"dotted eighth",val:0.75};
if(ratio>=0.4) return {sym:"\u266A",name:"eighth",val:0.5};
if(ratio>=0.2) return {sym:"\u266C",name:"sixteenth",val:0.25};
return {sym:"\u266C\u266C",name:"32nd",val:0.125};
}
function edStopPlayhead(){ function edStopPlayhead(){
edPlayhead=-1; edPlayhead=-1;
@@ -5407,9 +5505,10 @@ function edStopPlayhead(){
edPlayheadRAF=0; edPlayheadRAF=0;
edDraw(); edDraw();
} }
function edStartPlayhead(totalDur){ function edStartPlayhead(totalDur,offset){
edPlayheadStart=performance.now(); edPlayheadStart=performance.now();
var dur=totalDur; var dur=totalDur;
var off=offset||0;
function tick(){ function tick(){
if(!edPreviewing){edStopPlayhead();return;} if(!edPreviewing){edStopPlayhead();return;}
var elapsed=(performance.now()-edPlayheadStart)/1000; var elapsed=(performance.now()-edPlayheadStart)/1000;
@@ -5419,7 +5518,7 @@ function edStartPlayhead(totalDur){
edStopPlayhead(); edStopPlayhead();
return; return;
} }
edPlayhead=elapsed; edPlayhead=elapsed+off; // playhead position in absolute piece time
// Auto-scroll to keep playhead visible // Auto-scroll to keep playhead visible
var cv=getEdCanvas(); var cv=getEdCanvas();
var phx=edTimeToX(edPlayhead); var phx=edTimeToX(edPlayhead);
@@ -5431,33 +5530,41 @@ function edStartPlayhead(totalDur){
} }
edPlayheadRAF=requestAnimationFrame(tick); edPlayheadRAF=requestAnimationFrame(tick);
} }
function edPreview(){ function edPreview(startFrom){
if(edPreviewing){stopPreview();edPreviewing=false;edStopPlayhead();document.getElementById("ed-pv-btn").textContent="▶ Preview";return;} if(edPreviewing){stopPreview();edPreviewing=false;edStopPlayhead();document.getElementById("ed-pv-btn").textContent="▶ Preview";return;}
var ac=getAc(true);ac.resume(); var ac=getAc(true);ac.resume();
var piece=edPiece; var piece=edPiece;
edScrollX=0; // reset scroll to start var offset=typeof startFrom==="number"?startFrom:0;
edPlayheadOffset=offset;
if(offset===0)edScrollX=0; // reset scroll only when starting from beginning
if(edIsClassical){ if(edIsClassical){
// Play background orchestration if(!edIsSingalong){
if(piece.bg)for(var i=0;i<piece.bg.length;i++){ // Classical: play background orchestration (conductor mode)
var n=piece.bg[i]; if(piece.bg)for(var i=0;i<piece.bg.length;i++){
playSynth(ac,n.f,ac.currentTime+0.1+n.t,n.d,n.i,n.v*1.2,previewNodes); var n=piece.bg[i];
if(n.t<offset)continue; // skip notes before start position
playSynth(ac,n.f,ac.currentTime+0.1+(n.t-offset),n.d,n.i,n.v*1.2,previewNodes);
}
} }
// Play melody notes as woodwind clicks // Play melody notes (what the player sees/hits)
edMelody.forEach(function(m){ edMelody.forEach(function(m){
var freq=[329,392,440,349,220][m.l]||329; if(m.t<offset)return; // skip notes before start position
playSynth(ac,freq,ac.currentTime+0.1+m.t,0.1,'woodwind',0.5,previewNodes); var freq=m.f||[329,392,440,349,220][m.l]||329;
var dur=m.d||0.1;
var inst=edIsSingalong?'strings':'woodwind';
var vol=edIsSingalong?0.6:0.5;
playSynth(ac,freq,ac.currentTime+0.1+(m.t-offset),Math.min(dur,2),'woodwind',vol,previewNodes);
}); });
var dur=piece.duration||90; var totalDur=(piece.duration||90)-offset;
edPreviewing=true; edPreviewing=true;
document.getElementById("ed-pv-btn").textContent="⏹ Stop"; document.getElementById("ed-pv-btn").textContent="⏹ Stop";
edStartPlayhead(dur); edStartPlayhead(totalDur,offset);
}else{ }else{
// Regular rhythm: play drum sounds for each note in pattern, looped over bars // Regular rhythm: play drum sounds for each note in pattern, looped over bars
var bs=60/edBpm; var bs=60/edBpm;
var bars=edBars||8; var bars=edBars||8;
var drumFreqs=[329,392,440,349,220]; // red,yellow,blue,green,kick var drumFreqs=[329,392,440,349,220]; // red,yellow,blue,green,kick
var drumInst=['strings','woodwind','woodwind','strings','bass']; var drumInst=['strings','woodwind','woodwind','strings','bass'];
// For playhead: we need to convert pattern time to absolute seconds
for(var bar=0;bar<bars;bar++){ for(var bar=0;bar<bars;bar++){
edMelody.forEach(function(m){ edMelody.forEach(function(m){
var t=ac.currentTime+0.1+(bar*4+m.t)*bs; var t=ac.currentTime+0.1+(bar*4+m.t)*bs;
@@ -5466,12 +5573,10 @@ function edPreview(){
}); });
} }
var totalDur=bars*4*bs; var totalDur=bars*4*bs;
// For pattern mode, playhead is in beat units (0-4 repeating)
edPreviewing=true; edPreviewing=true;
document.getElementById("ed-pv-btn").textContent="⏹ Stop"; document.getElementById("ed-pv-btn").textContent="⏹ Stop";
// Playhead in pattern mode: cycles through 0-4 beats over bars
edPlayheadStart=performance.now(); edPlayheadStart=performance.now();
var beatDur=bs; // seconds per beat var beatDur=bs;
function patternTick(){ function patternTick(){
if(!edPreviewing){edStopPlayhead();return;} if(!edPreviewing){edStopPlayhead();return;}
var elapsed=(performance.now()-edPlayheadStart)/1000; var elapsed=(performance.now()-edPlayheadStart)/1000;
@@ -5481,7 +5586,6 @@ function edPreview(){
edStopPlayhead(); edStopPlayhead();
return; return;
} }
// Convert elapsed seconds to beat offset within 4-beat pattern
var beatPos=(elapsed/beatDur)%4; var beatPos=(elapsed/beatDur)%4;
edPlayhead=beatPos; edPlayhead=beatPos;
edDraw(); edDraw();
@@ -5504,7 +5608,7 @@ function edGetAllSaved(){
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();
var entry={melody:edMelody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;}),bpm:edBpm,isClassical:edIsClassical}; var entry={melody:edMelody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;if(m.f)n.f=m.f;return n;}),bpm:edBpm,isClassical:edIsClassical,timeSig:edTimeSig};
if(!edIsClassical)entry.bars=edBars; if(!edIsClassical)entry.bars=edBars;
all[edPiece.n]=entry; 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){}