MAJ: edition des accords dans la progression

This commit is contained in:
lrdlg
2026-07-28 11:23:43 +02:00
parent a680173138
commit 17903b63a9
6 changed files with 366 additions and 52 deletions
+292 -46
View File
@@ -187,6 +187,16 @@
font-size:.72rem;
}
.slot.playing{border-color:var(--led-on);box-shadow:0 0 10px rgba(255,174,51,.35)}
.slot.editing{border-color:var(--led-green);box-shadow:0 0 10px rgba(139,195,74,.45)}
.slot .edit{color:var(--led-green);opacity:.75}
.slot .edit:hover{opacity:1}
.edit-action{
background:var(--module-2);border:1px solid #4A4436;border-radius:7px;
color:var(--silk-light);font-family:var(--mono);font-size:.7rem;font-weight:600;
padding:.45rem .7rem;cursor:pointer;
}
.edit-action:active{transform:translateY(1px)}
.edit-action.upd{border-color:var(--led-green);color:var(--led-green)}
.slot .sname{font-weight:700;color:var(--lcd-amber);font-size:.85rem;padding-right:.9rem}
.slot .snotes{opacity:.55;font-size:.56rem;margin-top:.15rem}
.slot .srow{display:flex;gap:.3rem;margin-top:.3rem;align-items:center}
@@ -256,7 +266,7 @@
<body>
<div class="unit">
<header>
<h1><span>_chordlab·v2</span></h1>
<h1>_chordlab <span>ORC·v2</span></h1>
<div class="hdr-right">
<span class="sub" id="tagline">chord ideas machine · underscorelab / leradiologue</span>
<span class="seg" role="group" aria-label="Language">
@@ -409,6 +419,8 @@
<div class="prog-slots" id="progSlots"></div>
<div class="transport">
<button class="hwbtn" id="addBtn"><span class="led"></span>ADD (␣)</button>
<button class="edit-action upd" id="updateBtn" style="display:none">✓ mettre à jour</button>
<button class="edit-action" id="cancelEditBtn" style="display:none">✕ annuler</button>
<button class="hwbtn" id="playBtn"><span class="led"></span>PLAY</button>
<button class="hwbtn" id="loopBtn"><span class="led"></span>LOOP</button>
<button class="hwbtn" id="vlBtn" title="applique le voice-leading automatiquement à chaque ADD"><span class="led"></span>VL AUTO</button>
@@ -446,7 +458,7 @@
<div class="status" id="midiStatus">Web MIDI : en attente…</div>
</div>
<div class="mod">
<h2>Export MIDI</h2>
<h2>Export</h2>
<div class="io-line"><label>pistes</label><select id="exportSplit">
<option value="split">accords + basse (2 pistes)</option>
<option value="chords">accords seuls</option>
@@ -456,6 +468,12 @@
<option value="block">non (accords plaqués)</option>
</select></div>
<button class="export-btn" id="exportBtn">↓ exporter .mid</button>
<div class="io-line" style="margin-top:.6rem"><label>résol. tracker</label><select id="trackerRes">
<option value="4">1 step = 1/16</option>
<option value="2" selected>1 step = 1/8</option>
<option value="1">1 step = 1/4</option>
</select></div>
<button class="ghost-btn" id="exportTxtBtn">↓ tracker (.txt)</button>
<div class="status" id="exportStatus"></div>
</div>
<div class="mod">
@@ -472,7 +490,7 @@
<span class="docsep">·</span>
<a href="tuto_en.html" class="doclink" id="doclinkEn">Manual (EN)</a>
<span class="docsep">·</span>
_chordlab v2.5 · underscorelab / leradiologue
_chordlab v2.8 · underscorelab / leradiologue
</footer>
</div>
@@ -496,6 +514,8 @@ const state = {
bass:false,
bassOct:-12,
keyMode:false,
keyTypeOverride:null,
editIndex:null,
keyRoot:0,
keyScale:"major",
lastRoot:null,
@@ -506,6 +526,7 @@ const state = {
humanize:false,
layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty",
lang:(navigator.language||"").toLowerCase().startsWith("fr") ? "fr" : "en",
trackerRes:2,
};
/* ---------- Moteur d'accords ---------- */
@@ -606,38 +627,63 @@ function diatonicChord(notePc, keyRoot, scaleName, exts){
const deg = scale.indexOf(rel);
if (deg === -1) return null;
const pick = j => scale[(deg+j)%7] + 12*Math.floor((deg+j)/7);
const semis = [pick(0), pick(2), pick(4)];
if (exts.has("6")) semis.push(pick(5));
if (exts.has("m7") || exts.has("M7")) semis.push(pick(6));
if (exts.has("9")) semis.push(pick(8));
if (exts.has("11")) semis.push(pick(10));
if (exts.has("13")) semis.push(pick(12));
const degSemis = [pick(0), pick(2), pick(4)]; // triade diatonique
if (exts.has("6")) degSemis.push(pick(5));
if (exts.has("m7") || exts.has("M7")) degSemis.push(pick(6));
if (exts.has("9")) degSemis.push(pick(8));
if (exts.has("11")) degSemis.push(pick(10));
if (exts.has("13")) degSemis.push(pick(12));
const r0 = degSemis[0];
let iv = degSemis.map(s => s - r0); // intervalles depuis la fondamentale
// altérations chromatiques
if (exts.has("b5") || exts.has("s5")){
iv = iv.filter(i => !(i===6 || i===7 || i===8));
iv.push(exts.has("b5") ? 6 : 8);
}
if (exts.has("no3")) iv = iv.filter(i => !(i===3 || i===4));
if (exts.has("b9")) iv.push(13);
if (exts.has("s9")) iv.push(15);
if (exts.has("s11")) iv.push(18);
iv = [...new Set(iv)].sort((a,b)=>a-b);
const root = BASE + notePc;
const notes = semis.map(s => BASE + keyRoot + s);
const shift = root - notes[0];
let final = notes.map(n => n + shift);
if (exts.has("no3")) final = final.filter((n,i)=>i!==1);
final.sort((a,b)=>a-b);
const i1 = final[1]-final[0], i2 = final.length>2 ? final[2]-final[0] : 7;
let q = "";
if (i1===4 && i2===7) q=""; else if (i1===3 && i2===7) q="m";
else if (i1===3 && i2===6) q="dim"; else if (i1===4 && i2===8) q="aug";
const final = iv.map(i => root + i).sort((a,b)=>a-b);
// qualité de la triade (éventuellement altérée)
const has = x => iv.includes(x);
let q="";
if (has(4)&&has(8)) q="aug"; else if (has(4)&&has(7)) q="";
else if (has(3)&&has(7)) q="m"; else if (has(3)&&has(6)) q="dim";
else if (has(4)) q=""; else if (has(3)) q="m";
let name = NOTE_NAMES[notePc] + q;
if (exts.has("m7")||exts.has("M7")) name += "7*";
if (exts.has("6")) name += "6*";
if (exts.has("9")) name += "(9)*";
if (exts.has("11")) name += "(11)*";
if (exts.has("13")) name += "(13)*";
return {notes: final, name, deg};
if (exts.has("m7")||exts.has("M7")) name += "7";
if (exts.has("6")) name += "6";
const t=[];
if (exts.has("9")) t.push("9");
if (exts.has("b9")) t.push("♭9");
if (exts.has("s9")) t.push("♯9");
if (exts.has("11")) t.push("11");
if (exts.has("s11"))t.push("♯11");
if (exts.has("13")) t.push("13");
if (exts.has("b5")) t.push("♭5");
if (exts.has("s5")) t.push("♯5");
if (exts.has("no3"))t.push("no3");
if (t.length) name += "("+t.join(",")+")";
return {notes: final, name: name+"*", deg}; // * = harmonisé dans la tonalité
}
/* ---------- Accord courant ---------- */
function currentChord(rootPc){
let raw, name;
if (state.keyMode){
const d = diatonicChord(rootPc, state.keyRoot, state.keyScale, state.exts);
if (!d) return null;
raw = d.notes; name = d.name;
const rel = (rootPc - state.keyRoot + 12) % 12;
if (!SCALES[state.keyScale].includes(rel)) return null; // fondamentale hors tonalité
if (state.keyTypeOverride){ // type épinglé → construction libre
raw = buildChord(rootPc, state.keyTypeOverride, state.exts);
name = chordName(rootPc, state.keyTypeOverride, state.exts);
} else { // défaut : accord diatonique
const d = diatonicChord(rootPc, state.keyRoot, state.keyScale, state.exts);
if (!d) return null;
raw = d.notes; name = d.name;
}
} else {
raw = buildChord(rootPc, state.type, state.exts);
name = chordName(rootPc, state.type, state.exts);
@@ -699,7 +745,7 @@ function suggestNext(){
if (b.iv !== 0) label += "/" + NOTE_NAMES[(pc + b.iv) % 12];
}
state.progression.push({
label, notes, bass, rootPc: pc, beats: 4,
label, notes, bass, rootPc: pc, beats: 4, cfg:diatonicCfg(pc),
});
renderProgression();
}
@@ -746,16 +792,20 @@ function renderEvents(chord, beats, perfOverride){
/* =========================================================
AUDIO — poly Web Audio
========================================================= */
let AC=null, master=null;
let AC=null, master=null, audioBroken=false;
function ensureAudio(){
if (AC) return;
AC = new (window.AudioContext||window.webkitAudioContext)();
master = AC.createGain(); master.gain.value = 0.5;
const comp = AC.createDynamicsCompressor();
master.connect(comp); comp.connect(AC.destination);
if (AC || audioBroken) return;
try{
AC = new (window.AudioContext||window.webkitAudioContext)();
master = AC.createGain(); master.gain.value = 0.5;
const comp = AC.createDynamicsCompressor();
master.connect(comp); comp.connect(AC.destination);
}catch(e){ audioBroken=true; AC=null; } // aperçu audio indisponible : on continue sans casser l'UI/MIDI
}
function playNote(midi, dur, vel=0.8, isBass=false, delayS=0){
ensureAudio();
if (!AC) return;
try{
const t = AC.currentTime + delayS;
const f = 440 * Math.pow(2,(midi-69)/12);
const g = AC.createGain();
@@ -775,6 +825,7 @@ function playNote(midi, dur, vel=0.8, isBass=false, delayS=0){
g.gain.linearRampToValueAtTime(peak, t+0.015);
g.gain.setTargetAtTime(peak*0.7, t+0.05, 0.25);
g.gain.setTargetAtTime(0, t+dur, 0.12);
}catch(e){ /* aperçu audio non bloquant */ }
}
function playEventsAudio(events, bpm){
const spb = 60/bpm;
@@ -904,6 +955,105 @@ function exportMidi(){
$("exportStatus").textContent=t("Exporté : ")+a.download;
}
/* =========================================================
EXPORT TRACKER (.txt) — grille façon Polyend Tracker
Chaque note d'accord = une piste (grave→aigu), basse à part.
Accords plaqués et tenus ; "OFF" coupe, "..." maintient.
========================================================= */
function trackerNoteName(midi){
const pc=((midi%12)+12)%12;
const oct=Math.floor(midi/12)-1; // MIDI 60 = C-4
const b=NOTE_NAMES[pc];
return (b.length===2?b:b+"-")+oct;
}
function scaleLabel(s, lang){
const fr={major:"majeur",minor:"mineur",harmMinor:"mineur harm.",dorian:"dorien",phrygian:"phrygien",lydian:"lydien",mixolydian:"mixolydien",locrian:"locrien"};
const en={major:"major",minor:"minor",harmMinor:"harm. minor",dorian:"dorian",phrygian:"phrygian",lydian:"lydian",mixolydian:"mixolydian",locrian:"locrian"};
return (lang==="en"?en:fr)[s]||s;
}
function buildTrackerText(spb, lang){
const prog=state.progression;
if(!prog.length) return null;
const fr = lang!=="en";
const maxV=Math.max(...prog.map(c=>c.notes.length));
const hasBass=prog.some(c=>c.bass!=null);
const cols=maxV+(hasBass?1:0);
const NONE="...", OFF="OFF";
const meta=[]; let cursor=0;
prog.forEach((c,ci)=>{ const dur=Math.max(1,Math.round(c.beats*spb)); meta.push({ci,c,start:cursor,dur}); cursor+=dur; });
const totalSteps=cursor;
const rows=[];
for(let i=0;i<=totalSteps;i++) rows.push(Array(cols).fill(NONE)); // +1 : ligne OFF finale
const prev=Array(maxV).fill(null); let prevB=null;
meta.forEach(m=>{
const notes=[...m.c.notes].sort((a,b)=>a-b);
const s=m.start;
for(let t=0;t<maxV;t++){
const nn = t<notes.length ? notes[t] : null;
if(nn!==null){ rows[s][t]=trackerNoteName(nn); prev[t]=nn; }
else if(prev[t]!==null){ rows[s][t]=OFF; prev[t]=null; }
}
if(hasBass){
const bc=maxV;
if(m.c.bass!=null){ rows[s][bc]=trackerNoteName(m.c.bass); prevB=m.c.bass; }
else if(prevB!==null){ rows[s][bc]=OFF; prevB=null; }
}
});
for(let t=0;t<maxV;t++) if(prev[t]!==null) rows[totalSteps][t]=OFF;
if(hasBass && prevB!==null) rows[totalSteps][maxV]=OFF;
const colNames=[]; for(let t=0;t<maxV;t++) colNames.push("T"+(t+1)); if(hasBass) colNames.push("B");
const w=colNames.map((nm,i)=>Math.max(3, nm.length, ...rows.map(r=>r[i].length)));
const pad=(s,n)=>s+" ".repeat(Math.max(0,n-s.length));
const stepW=Math.max(4, String(totalSteps).length);
const keyStr = state.keyMode ? (NOTE_NAMES[state.keyRoot]+" "+scaleLabel(state.keyScale,lang)) : (fr?"libre":"free");
const resLabel = {4:"1/16",2:"1/8",1:"1/4"}[spb]||("1/"+spb);
const L=[];
L.push("_chordlab — "+(fr?"export Polyend Tracker":"Polyend Tracker export"));
L.push("==================================");
L.push((fr?"Tonalité : ":"Key : ")+keyStr);
L.push("BPM : "+getBpm());
L.push((fr?"Résolution : ":"Resolution : ")+"1 step = "+resLabel+" ("+spb+(fr?" step/temps)":" step/beat)"));
L.push(fr?'Notation : note-octave, MIDI 60 = C-4. "OFF" coupe, "..." maintient.'
:'Notation : note-octave, MIDI 60 = C-4. "OFF" stops, "..." holds.');
L.push(fr?"Pistes : T1..Tn = voix (grave→aigu), B = basse. 1 note/piste."
:"Tracks : T1..Tn = voices (low→high), B = bass. one note/track.");
L.push("");
L.push("PATTERN");
L.push(pad("Step",stepW)+" "+colNames.map((nm,i)=>pad(nm,w[i])).join(" "));
L.push("-".repeat(stepW)+" "+w.map(x=>"-".repeat(x)).join(" "));
rows.forEach((r,i)=>{
L.push(String(i).padStart(stepW,"0")+" "+r.map((c,j)=>pad(c,w[j])).join(" "));
});
L.push("");
L.push(fr?"RÉFÉRENCE ACCORDS":"CHORD REFERENCE");
meta.forEach((m,idx)=>{
const notes=[...m.c.notes].sort((a,b)=>a-b);
const end=m.start+m.dur-1;
L.push("["+(idx+1)+"] "+pad(m.c.label,12)+" steps "+String(m.start).padStart(stepW,"0")+"-"+String(end).padStart(stepW,"0")+" ("+m.c.beats+(fr?" temps)":" beats)"));
const parts=notes.map((n,tt)=>"T"+(tt+1)+" "+trackerNoteName(n)+" ("+n+")");
if(m.c.bass!=null) parts.push("B "+trackerNoteName(m.c.bass)+" ("+m.c.bass+")");
L.push(" "+parts.join(" "));
});
return L.join("\n")+"\n";
}
function exportTracker(){
const spb=parseInt($("trackerRes").value)||2;
const txt=buildTrackerText(spb, state.lang);
if(!txt){ $("exportStatus").textContent=t("Progression vide."); return; }
const blob=new Blob([txt],{type:"text/plain;charset=utf-8"});
const a=document.createElement("a");
a.href=URL.createObjectURL(blob);
const labels=state.progression.map(c=>c.label.replace(/[^\w#]/g,"")).slice(0,4).join("-");
a.download="chordlab_"+labels+"_tracker.txt";
a.click(); URL.revokeObjectURL(a.href);
$("exportStatus").textContent=t("Exporté (tracker) : ")+a.download;
}
/* =========================================================
SESSION save/load (fichier JSON — pas de localStorage)
========================================================= */
@@ -913,9 +1063,9 @@ function saveSession(){
settings:{
type:state.type, exts:[...state.exts], voicing:state.voicing,
drop:state.drop, slash:state.slash, bass:state.bass, bassOct:state.bassOct,
keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale,
keyMode:state.keyMode, keyTypeOverride:state.keyTypeOverride, keyRoot:state.keyRoot, keyScale:state.keyScale,
perf:state.perf, rate:state.rate, humanize:state.humanize,
layout:state.layout, lang:state.lang,
layout:state.layout, lang:state.lang, trackerRes:state.trackerRes,
bpm:getBpm(), vlAuto:state.vlAuto,
},
progression:state.progression,
@@ -941,6 +1091,7 @@ function loadSession(file){
state.bass=!!s.bass;
state.bassOct=s.bassOct||-12;
state.keyMode=!!s.keyMode;
state.keyTypeOverride=(s.keyTypeOverride in TYPE_INTERVALS)?s.keyTypeOverride:null;
state.keyRoot=s.keyRoot||0;
state.keyScale=s.keyScale||"major";
state.perf=s.perf||"block";
@@ -948,6 +1099,7 @@ function loadSession(file){
state.humanize=!!s.humanize;
state.layout=(s.layout==="qwerty")?"qwerty":(s.layout==="azerty"?"azerty":state.layout);
state.lang=(s.lang==="en")?"en":(s.lang==="fr"?"fr":state.lang);
if([1,2,4].includes(s.trackerRes)) state.trackerRes=s.trackerRes;
state.vlAuto=!!s.vlAuto;
state.progression=Array.isArray(d.progression)?d.progression:[];
state.progression.forEach(c=>{ c.beats=Math.max(1,Math.min(16,c.beats||4)); });
@@ -960,7 +1112,7 @@ function loadSession(file){
r.readAsText(file);
}
function syncUiFromState(){
document.querySelectorAll("#typeBtns .hwbtn").forEach(b=>b.classList.toggle("on", b.dataset.type===state.type));
updateTypeLeds();
document.querySelectorAll("[data-ext]").forEach(b=>b.classList.toggle("on", state.exts.has(b.dataset.ext)));
$("dropSel").value=state.drop; $("slashSel").value=state.slash;
$("bassBtn").classList.toggle("on", state.bass);
@@ -969,6 +1121,7 @@ function syncUiFromState(){
$("keyRoot").value=state.keyRoot;
$("keyScaleSel").value=state.keyScale;
$("perfSel").value=state.perf; $("rateSel").value=state.rate;
$("trackerRes").value=state.trackerRes;
$("humanBtn").classList.toggle("on", state.humanize);
setLayout(state.layout);
$("vlBtn").classList.toggle("on", state.vlAuto);
@@ -1022,11 +1175,21 @@ function triggerChord(rootPc){
}
/* type radio */
function updateTypeLeds(){
document.querySelectorAll("#typeBtns .hwbtn").forEach(b=>{
const on = state.keyMode ? (state.keyTypeOverride===b.dataset.type)
: (state.type===b.dataset.type);
b.classList.toggle("on", on);
});
}
document.querySelectorAll("#typeBtns .hwbtn").forEach(btn=>{
btn.addEventListener("click",()=>{
state.type=btn.dataset.type;
document.querySelectorAll("#typeBtns .hwbtn").forEach(b=>
b.classList.toggle("on", b.dataset.type===state.type));
if (state.keyMode){
state.keyTypeOverride = (state.keyTypeOverride===btn.dataset.type) ? null : btn.dataset.type;
} else {
state.type = btn.dataset.type;
}
updateTypeLeds();
if (state.lastRoot!==null) triggerChord(state.lastRoot);
});
});
@@ -1078,7 +1241,7 @@ window.addEventListener("keydown",ev=>{
if (k in map){
keyEls[map[k]].classList.add("pressed");
triggerChord(map[k]);
} else if (ev.code==="Space"){ ev.preventDefault(); addToProgression(); }
} else if (ev.code==="Space"){ ev.preventDefault(); state.editIndex!==null ? commitEdit() : addToProgression(); }
else if (ev.key==="ArrowRight"){ setVoicing(state.voicing+1); }
else if (ev.key==="ArrowLeft"){ setVoicing(state.voicing-1); }
});
@@ -1144,6 +1307,7 @@ NOTE_NAMES.forEach((n,i)=>{
$("keyBtn").addEventListener("click",function(){
state.keyMode=!state.keyMode;
this.classList.toggle("on",state.keyMode);
updateTypeLeds();
updateKeyLcd();refreshKeyDimming();
});
$("keyRoot").addEventListener("change",function(){state.keyRoot=parseInt(this.value);updateKeyLcd();refreshKeyDimming();});
@@ -1160,6 +1324,7 @@ $("perfSel").addEventListener("change",function(){
$("lcdPerf").textContent=this.selectedOptions[0].textContent.toUpperCase();
});
$("rateSel").addEventListener("change",function(){state.rate=parseFloat(this.value);});
$("trackerRes").addEventListener("change",function(){state.trackerRes=parseInt(this.value);});
$("humanBtn").addEventListener("click",function(){
state.humanize=!state.humanize;
this.classList.toggle("on",state.humanize);
@@ -1178,14 +1343,16 @@ function addToProgression(){
let notes=[...chord.notes];
if (state.vlAuto && state.progression.length)
notes=bestVoicingAgainst(notes, state.progression[state.progression.length-1].notes);
exitEdit();
state.progression.push({
label:chord.name, notes,
bass:chord.bass, rootPc:chord.rootPc, beats:4,
cfg:slotCfg(chord.rootPc),
});
renderProgression();
}
$("addBtn").addEventListener("click",addToProgression);
$("progClear").addEventListener("click",()=>{stopPlayback();state.progression=[];renderProgression();});
$("progClear").addEventListener("click",()=>{stopPlayback();exitEdit();state.progression=[];renderProgression();});
$("suggestBtn").addEventListener("click",suggestNext);
$("optimizeBtn").addEventListener("click",()=>{
for (let i=1;i<state.progression.length;i++){
@@ -1196,6 +1363,75 @@ $("optimizeBtn").addEventListener("click",()=>{
$("sessionStatus").textContent=t("Voice-leading appliqué.");
});
function slotCfg(rootPc){
return {
rootPc,
type:state.type, keyTypeOverride:state.keyTypeOverride,
exts:[...state.exts], voicing:state.voicing, drop:state.drop,
slash:state.slash, bassOn:state.bass, bassOct:state.bassOct,
keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale,
};
}
function diatonicCfg(pc){
return {
rootPc:pc, type:state.type, keyTypeOverride:null,
exts:[...state.exts], voicing:state.voicing, drop:state.drop,
slash:state.slash, bassOn:state.bass, bassOct:state.bassOct,
keyMode:true, keyRoot:state.keyRoot, keyScale:state.keyScale,
};
}
function applyCfgToState(cfg){
state.type=cfg.type||"maj";
state.keyTypeOverride=(cfg.keyTypeOverride in TYPE_INTERVALS)?cfg.keyTypeOverride:null;
state.exts=new Set(cfg.exts||[]);
state.voicing=cfg.voicing||0;
state.drop=cfg.drop||"close";
state.slash=cfg.slash||"root";
state.bass=!!cfg.bassOn;
state.bassOct=cfg.bassOct||-12;
state.keyMode=!!cfg.keyMode;
state.keyRoot=(cfg.keyRoot!=null)?cfg.keyRoot:0;
state.keyScale=cfg.keyScale||"major";
document.querySelectorAll("[data-ext]").forEach(b=>b.classList.toggle("on", state.exts.has(b.dataset.ext)));
$("dropSel").value=state.drop; $("slashSel").value=state.slash;
$("bassBtn").classList.toggle("on", state.bass); $("bassOct").value=state.bassOct;
$("keyBtn").classList.toggle("on", state.keyMode);
$("keyRoot").value=state.keyRoot; $("keyScaleSel").value=state.keyScale;
updateTypeLeds(); updateKeyLcd(); refreshKeyDimming(); setVoicing(state.voicing, true);
triggerChord(cfg.rootPc);
}
function setEditUI(on){
$("addBtn").style.display = on ? "none" : "";
$("updateBtn").style.display = on ? "" : "none";
$("cancelEditBtn").style.display = on ? "" : "none";
}
function exitEdit(){ state.editIndex=null; setEditUI(false); }
function enterEdit(i){
stopPlayback();
state.editIndex=i;
const slot=state.progression[i];
if (slot.cfg) applyCfgToState(slot.cfg);
else if (slot.rootPc!=null) triggerChord(slot.rootPc); // ancien slot sans config
setEditUI(true);
renderProgression();
}
function commitEdit(){
if (state.editIndex===null) return;
if (state.lastRoot===null){ cancelEdit(); return; }
const chord=currentChord(state.lastRoot);
if (!chord) return; // hors tonalité : on reste en édition
const i=state.editIndex, beats=state.progression[i].beats;
state.progression[i]={
label:chord.name, notes:[...chord.notes],
bass:chord.bass, rootPc:chord.rootPc, beats,
cfg:slotCfg(state.lastRoot),
};
exitEdit(); renderProgression();
}
function cancelEdit(){ exitEdit(); renderProgression(); }
$("updateBtn").addEventListener("click",commitEdit);
$("cancelEditBtn").addEventListener("click",cancelEdit);
function renderProgression(){
const el=$("progSlots");
el.innerHTML="";
@@ -1208,19 +1444,25 @@ function renderProgression(){
}
state.progression.forEach((c,i)=>{
const s=document.createElement("div");
s.className="slot"+(i===state.playIdx?" playing":"");
s.className="slot"+(i===state.playIdx?" playing":"")+(i===state.editIndex?" editing":"");
s.innerHTML=`<button class="del" title="${t('supprimer')}">×</button>
<div class="sname">${c.label}</div>
<div class="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div>
<div class="srow">
<button class="mv edit" title="${t('éditer')}">✎</button>
<button class="mv sl" title="${t('déplacer à gauche')}"></button>
<button class="mv bminus" title="${t('1 temps')}"></button>
<span class="badge" title="${t('durée en temps (1 à 16)')}">${c.beats}♩</span>
<button class="mv bplus" title="${t('+1 temps')}">+</button>
<button class="mv sr" title="${t('déplacer à droite')}"></button>
</div>`;
s.querySelector(".edit").addEventListener("click",ev=>{
ev.stopPropagation();
state.editIndex===i ? cancelEdit() : enterEdit(i);
});
s.querySelector(".del").addEventListener("click",ev=>{
ev.stopPropagation();
exitEdit();
state.progression.splice(i,1);renderProgression();
});
s.querySelector(".sl").addEventListener("click",ev=>{
@@ -1270,7 +1512,7 @@ $("presetLoad").addEventListener("click",()=>{
bass=b.midi;
if (b.iv!==0) label+="/"+NOTE_NAMES[(pc+b.iv)%12];
}
state.progression.push({label,notes,bass,rootPc:pc,beats:4});
state.progression.push({label,notes,bass,rootPc:pc,beats:4,cfg:diatonicCfg(pc)});
});
renderProgression();
});
@@ -1329,6 +1571,7 @@ $("midiDev").addEventListener("change",selectMidiDevice);
/* export & session */
$("exportBtn").addEventListener("click",exportMidi);
$("exportTxtBtn").addEventListener("click",exportTracker);
$("saveJson").addEventListener("click",saveSession);
$("loadJson").addEventListener("click",()=>$("fileInput").click());
$("fileInput").addEventListener("change",function(){
@@ -1352,13 +1595,14 @@ const EN = {
"#exportBtn":"↓ export .mid",
"#saveJson":"↓ save session (.json)", "#loadJson":"↑ load a session",
"#doclinkFr":"Manual (FR)", "#doclinkEn":"Manual (EN)",
"#updateBtn":"✓ update", "#cancelEditBtn":"✕ cancel",
},
h2:{ "Chord type":"Chord type", "Root":"Root", "Voicing":"Voicing",
"Progression":"Progression", "MIDI out (live)":"MIDI out (live)",
"Export MIDI":"MIDI export", "Session":"Session" },
"Export":"Export", "Session":"Session" },
rowlabel:{ "Tensions":"Tensions", "Altérations":"Alterations",
"combos : dim+maj=aug · min+sus=sus2":"" },
label:{ "drop":"drop", "basse /":"bass /", "gamme":"scale",
label:{ "drop":"drop", "basse /":"bass /", "gamme":"scale", "résol. tracker":"tracker resol.",
"périphérique":"device", "canal accords":"chord channel",
"canal basse":"bass channel", "pistes":"tracks", "perf rendue":"render perf" },
option:{ "fondamentale":"root", "tierce":"third", "quinte":"fifth", "septième":"seventh",
@@ -1398,6 +1642,8 @@ const EN = {
"+1 temps":"+1 beat",
"durée en temps (1 à 16)":"length in beats (1 to 16)",
"Exporté : ":"Exported: ",
"Exporté (tracker) : ":"Exported (tracker): ",
"éditer":"edit",
},
};
const KEYHINT_EN = {