From 17903b63a9cfb2a4f84e90e7c9b2ff3f4c369cad Mon Sep 17 00:00:00 2001 From: lrdlg Date: Tue, 28 Jul 2026 11:23:43 +0200 Subject: [PATCH] MAJ: edition des accords dans la progression --- .DS_Store | Bin 8196 -> 6148 bytes CHANGELOG.md | 48 ++++++ TUTORIEL_UTILISATION.md | 12 ++ index.html | 338 ++++++++++++++++++++++++++++++++++------ tuto.html | 10 +- tuto_en.html | 10 +- 6 files changed, 366 insertions(+), 52 deletions(-) diff --git a/.DS_Store b/.DS_Store index 85ce18a3483f67ed8be61d990da043ac87d096ef..95e51541ba2af933315ec000e44c3d45a96fc6d3 100644 GIT binary patch literal 6148 zcmeHK!Ab-%3{7-h7Cg8|kNt(+FDT1;@azxB>?#NYb6C9h&-J|~>J+QsMMM%vUz;Xr z=fSj%i0JP5xDi>4$O=kSb~-|Hck0ZYmq4vKPPgN97{+Pnr1_gB?)^d@e z$rbg*rS&|X4&yLQBdoIgNFbXT1-%rF?^j8-KS;8jC*f{Ff`MQl7zhS}fo~bWolRL? z8b%uo1OvgqI|Do)5|o%7i(x%F(9{wDxPZ9|`h4{=COKxuVu%@tS}4#$)k_SuaM+Xk zWyfM@;i6uAsIUB1yl7n=`BQZl&4$qi1HnMcz_|_Ay#H_T%XEwUHY7&DKrryn7|_N3 zZok7v`Q7^I^LW=L=slD|zrqd#{pJyX0nd@^I%)HXefVX^V#qAC*KlAw1S%oX1Ovao Fz$-_WM=1aR literal 8196 zcmeHMYitx%6u#fIz>FPWs09i#*{zGv;9^@W+JfNr!An4pwsc#Hb$4fI2c|Q1XLidY zH6_NkiN;rA{6T#FBx*Eb42u3T@fA%&B8kMPiT*P2@dqR7xpQZmQmFdN5S*Lbd+vGM zbN8J6&fLATj4`wpj0VQ48DlbaPPGaeF44H2_lueoN@frRt!LR0Z#ZkaS>jJzWE}~j z3`7}-G7x1T%0QHX%OL}FX7eUbbM6amG)5VSGVs650DnHjsdJhN>6oCwql1Rv3P7}+ z#6hFE#wSGMAx(vJOi6oBmIYW5)K)5r)8w!Hm$$vaB zXGjSejZp@o3|yQ6es?ctL(F0ZvnJo)v%|LK`StauA(fTSm^n+HC0EFMI|kj64nN}; zyw*&959c~v*Upyane1-M9O>7p+FZxaSdN(w7FdQ!CVkzOW4I%&Zox6UU~Pv`$cn7= zYt=`NG&eUS<11G+k0#?snj0IF@usHw(NRU7pQu~AtN(yK)q*|UbQc6-1%XNCvzIo6uIl4fV?)BWf z-xfIQ*R+CX?lQ@jRW#elBe|;NyX_d}ZoJ)Q-Xoew)r**w<8v0qme#IZb@MH$4O?#C zH9l9HSE*L13%ZMzXJu`(Ys51A!JdqlGabWn26`#oj&JR=%)BxWNu|@t44LDV)r%Hi zvqaaYT9>?}3t8*#g5@9560$7zsL>OO_NLrt(1+aa8r-81X!II=xh(JD^0vH5qY$er z^rWI?I`8Wh3e>P40MR(-pyeIfUH zgPFX!V}yWOlHVcA!S_va?rDQz`R;_Wiz_wDDOywm^j(NjkP{^kCQ`LxnmFZ-1+yT> zipGyOoh@TcbdYwkeavM?*`w?jdxM>1@3K$WX?B)<$9`nz*l+AS`vYZAQHdp3ij}BC zJsPkEYq1XNu@#-@!fs@705%-B@G**eFovVJACKX2Jb@?iEMCBicp0zYINrhuyp0d= z5kAJJ_zY+9J$}N^_yxb?JpPoJR3TMK^Q8q+t<)f`l5UhXN}HseQje6C2Bbj|kW?zT zy@D@|(tesvuC4F|p!D3OYb&;C^OoDx)t7y3eL06$)74e;7c7ilx1z4GXr>j0~7Ozx!oLkpitH!h$R3n02ww&0RR3C!9 zJ{gN?5|yQ3>lD zj_o=FBr8GuLh~z^D-)qr^ZWmX>EHifxe16Oi!u;pfT6s-qrH_9G2H`@-?ih^@1o8d yw;K~QSV99g;qu@*PITfALz>4!T`Htwf|7>XfBqui-~G0Z&j0BA5Ab=V4u1hyOK*+< diff --git a/CHANGELOG.md b/CHANGELOG.md index 49c6618..7c65d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,54 @@ Conventions : --- +## [2.8] — édition des accords dans la progression + +### Ajouté +- **Édition d'un slot existant.** Chaque slot porte un bouton ✎ : il **recharge la configuration de l'accord** (type, tensions, altérations, voicing, drop, basse/slash, tonalité) dans les commandes principales. On retouche librement — y compris rejouer une autre fondamentale — puis **✓ mettre à jour** réécrit l'accord dans le même slot en conservant sa durée, ou **✕ annuler** laisse le slot intact. Re-cliquer ✎ sort du mode édition. +- Chaque slot mémorise désormais sa **configuration source**, ce qui le rend ré-éditable. Les slots générés par preset et par ✦ suggérer sont éditables de la même façon. +- La configuration voyage dans les sessions `.json`. + +### Corrigé +- **Robustesse audio** : une éventuelle défaillance de l'aperçu Web Audio n'interrompt plus l'interface ni la sortie MIDI (l'aperçu échoue silencieusement, le reste continue). + +### Notes +- L'édition regénère l'accord à partir des commandes (sans voice-leading automatique), pour un résultat prévisible. +- Les slots issus d'anciennes sessions (sans configuration mémorisée) restent supprimables/déplaçables ; l'édition fine nécessite qu'ils aient été créés en 2.8+. + +--- + +## [2.7] — Key Mode : contrôle complet du type et des tensions + +### Corrigé +- **En Key Mode, les tensions altérées ♭9, ♯9, ♯11, ♭5, ♯5 n'avaient aucun effet.** Le constructeur diatonique ne câblait que 6, m7, M7, 9, 11, 13 (et no3). Les cinq altérations manquantes sont désormais appliquées comme colorations chromatiques sur l'accord diatonique. +- **En Key Mode, les boutons de type paraissaient morts.** C'était en partie voulu (la qualité vient du degré de la gamme), mais sans aucune échappatoire. + +### Ajouté +- **Type override en Key Mode.** Les boutons de type deviennent des interrupteurs : par défaut aucun n'est allumé (mode « auto », qualité diatonique du degré) ; cliquer un type l'**épingle** (LED allumée) et force cette qualité sur la fondamentale jouée, avec accès complet aux tensions et altérations ; re-cliquer le même type revient en auto. La fondamentale **reste dans la tonalité** dans tous les cas (les notes hors gamme restent muettes). +- L'état d'épinglage est enregistré dans les sessions `.json`. + +### Modifié +- Hors Key Mode, les boutons de type conservent leur comportement de sélection unique (radio) inchangé. +- Nommage diatonique unifié : l'astérisque `*` (harmonisation dans la tonalité) est conservé et suffixe le nom complet, tensions comprises (`Dm7(♭9)*`). + +--- + +## [2.6] — export tracker (.txt) + +### Ajouté +- **Export d'un fichier texte pour trackers** (Polyend Tracker Mini et similaires), en plus du `.mid`. Le fichier contient une grille de pattern façon tracker : chaque note de l'accord occupe une piste (`T1..Tn`, de la plus grave à la plus aiguë), la basse a sa propre colonne (`B`), les accords sont plaqués et tenus. `OFF` coupe une voix, `...` maintient. +- **Sélecteur de résolution** : 1 step = 1/16, 1/8 (défaut) ou 1/4 (un pas par temps). Il détermine l'espacement des accords sur la grille et doit correspondre à la résolution de pattern choisie sur le tracker. +- **Section de référence** sous la grille : pour chaque accord, ses notes en notation tracker **et leur numéro MIDI**, avec la plage de steps occupée. +- Gestion correcte des changements de nombre de voix : quand un accord a moins de notes que le précédent, un `OFF` est écrit sur les pistes libérées. Une ligne `OFF` finale ferme le pattern. +- En-tête localisé (FR/EN) reprenant tonalité, BPM, résolution et conventions de notation. + +### Notes d'implémentation +- Convention d'octave : MIDI 60 = `C-4`. Le numéro MIDI est indiqué à côté de chaque note dans la section de référence, ce qui lève toute ambiguïté si l'affichage d'octave du tracker diffère. +- Le fichier texte utilise les accords plaqués (voicing courant), indépendamment du mode de performance choisi pour le `.mid`. +- La résolution retenue est enregistrée dans les sessions `.json`. + +--- + ## [2.5] — interface bilingue ### Ajouté diff --git a/TUTORIEL_UTILISATION.md b/TUTORIEL_UTILISATION.md index 546ccd5..9c7cd18 100644 --- a/TUTORIEL_UTILISATION.md +++ b/TUTORIEL_UTILISATION.md @@ -126,6 +126,8 @@ Activez **KEY**, choisissez une tonique et une gamme parmi les huit disponibles. - Les touches hors tonalité sont **grisées** et n'émettent rien (le LCD affiche `hors tonalité`). - Les tensions 6/7/9/11/13 restent actives et sont harmonisées **dans la gamme** (la 7e de G en Do majeur sera F naturel → G7, pas Gmaj7). Un astérisque dans le nom (`G7*`) signale un accord harmonisé diatoniquement. +**Type et tensions en Key Mode.** Par défaut, la qualité de l'accord vient du degré (mode auto, aucun bouton de type allumé). Tu peux forcer une qualité en **épinglant** un bouton de type : il s'allume et s'applique à la fondamentale jouée, avec accès à toutes les tensions et altérations ; re-clique-le pour revenir en auto. Dans tous les cas la fondamentale reste dans la tonalité. Les douze tensions/altérations (dont ♭9, ♯9, ♯11, ♭5, ♯5) sont actives aussi bien en auto qu'en override. + Le mineur harmonique vaut le détour : le degré V y devient un vrai accord de dominante avec sensible — la cadence mineure classique à un doigt. --- @@ -137,6 +139,7 @@ Jouez un accord, puis **ADD** (ou barre espace). Il apparaît dans un slot avec Sur chaque slot : - **− / +** autour du badge durée (`3♩`) — règle la durée de **1 à 16 temps**, au pas de 1. Un accord de 3, 5 ou 7 temps est désormais un seul slot, plus besoin de le découper. +- **✎** — **éditer l'accord** : recharge sa configuration (type, tensions, voicing, basse, tonalité) dans les commandes ; retouche puis **✓ mettre à jour** pour réécrire le slot, ou **✕ annuler**. Re-clique ✎ pour sortir. - **‹ ›** — déplacer le slot dans la séquence - **×** — supprimer - **clic sur le slot** — l'auditionner seul @@ -196,6 +199,15 @@ Panneau **Export MIDI** : Le fichier généré (`chordlab_Cm7-Fm7-Ab-Bb_112bpm.mid`, format 1, PPQ 480, tempo inclus) respecte la durée individuelle de chaque slot. +### Export tracker (.txt) + +À côté du `.mid`, le bouton **↓ tracker (.txt)** génère un fichier texte pensé pour la transcription manuelle sur un tracker (Polyend Tracker Mini et similaires). Il contient : + +- une **grille de pattern** : chaque note de l'accord occupe une piste (`T1`, `T2`… de la plus grave à la plus aiguë), la basse a sa colonne `B`. `OFF` coupe une voix, `...` la maintient ; +- une **section de référence** listant, pour chaque accord, ses notes en notation tracker (`C-4`, `D#5`) **et leur numéro MIDI**, avec la plage de steps occupée. + +Le sélecteur **résol. tracker** (1/16, 1/8, 1/4) fixe l'espacement des accords sur la grille : règle-le sur la résolution de pattern que tu utilises sur ta machine. Convention d'octave : MIDI 60 = `C-4` ; le numéro MIDI en référence lève toute ambiguïté si l'affichage de ton tracker diffère. + **Destinations typiques** : - **DAW** — glisser-déposer sur une piste instrument, c'est tout. - **Polyend Tracker Mini** — copier le `.mid` sur la carte SD et l'importer ; chaque piste MIDI devient une piste du pattern. diff --git a/index.html b/index.html index 1dcfe96..a07b2f6 100644 --- a/index.html +++ b/index.html @@ -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 @@
-

_chordlab·v2

+

_chordlab ORC·v2

chord ideas machine · underscorelab / leradiologue @@ -409,6 +419,8 @@
+ + @@ -446,7 +458,7 @@
Web MIDI : en attente…
-

Export MIDI

+

Export

+
+
@@ -472,7 +490,7 @@ · Manual (EN) · - _chordlab v2.5 · underscorelab / leradiologue + _chordlab v2.8 · underscorelab / leradiologue
@@ -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;tMath.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{ $("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=`
${c.label}
${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}
+ ${c.beats}♩
`; + 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 = { diff --git a/tuto.html b/tuto.html index 9ae26d4..5e55da7 100644 --- a/tuto.html +++ b/tuto.html @@ -149,8 +149,8 @@
-
manuel d'utilisation · rev. 2.5  ·  English version
-

_chordlab

+
manuel d'utilisation · rev. 2.8  ·  English version
+

_chordlab ORC

Machine à accords et à progressions. Le principe tient en une phrase : vous ne jouez pas les notes d'un accord, vous le décrivez — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.

@@ -486,6 +486,7 @@

Les tensions 6 m7 M7 9 11 13 restent actives et sont harmonisées dans la gamme : la 7e de G en Do majeur donne un vrai G7 (avec F naturel). L'astérisque dans le nom (G7*) signale cette harmonisation diatonique.

Le mineur harmonique vaut le détour : son degré V devient un véritable accord de dominante avec sensible — la cadence mineure classique, à un doigt.
+

Type et tensions. Par défaut la qualité vient du degré (mode auto). Tu peux épingler un bouton de type pour forcer une qualité sur la fondamentale jouée, puis re-cliquer pour revenir en auto ; la fondamentale reste toujours dans la tonalité. Les douze tensions et altérations (♭9, ♯9, ♯11, ♭5, ♯5 comprises) sont actives dans les deux cas.

@@ -519,6 +520,7 @@
  • 3Clic sur le slot — l'auditionner seul.
  • 4− / + — durée de 1 à 16 temps, au pas de 1. Un accord de 3, 5 ou 7 temps tient dans un seul slot.
  • 5‹ › — déplacer le slot dans la séquence.
  • +
  • Éditer l'accord — recharge sa configuration dans les commandes ; ✓ mettre à jour réécrit le slot, ✕ annuler le laisse intact.
  • fig. 7 — anatomie d'un slot de progression
    @@ -690,6 +692,8 @@ perf rendueoui : les strums/arps sont écrits note par note dans le fichier · non : accords plaqués (préférable si vous arpégez ensuite dans le DAW ou le Tracker) +

    À côté du .mid, le bouton ↓ tracker (.txt) produit un fichier texte pour la transcription manuelle sur un tracker (Polyend Tracker Mini et similaires) : une grille de pattern où chaque note occupe une piste (T1..Tn grave→aigu) plus une colonne basse, OFF pour couper et ... pour maintenir, suivie d'une section de référence donnant les notes et leur numéro MIDI. Le sélecteur résol. tracker (1/16, 1/8, 1/4) doit correspondre à la résolution de pattern de ta machine.

    +

    Sessions

    ↓ sauver la session génère un .json avec tous les réglages et la progression complète ; ↑ charger restaure tout. Rien n'est stocké dans le navigateur : le fichier est l'unique persistance — versionnez vos sessions dans Gitea comme n'importe quel fichier de projet.

    @@ -730,7 +734,7 @@ -
    _chordlab · manuel rev. 2.5 · underscorelab / leradiologue
    +
    _chordlab · manuel rev. 2.8 · underscorelab / leradiologue
    diff --git a/tuto_en.html b/tuto_en.html index 0790c9f..d9993ec 100644 --- a/tuto_en.html +++ b/tuto_en.html @@ -149,8 +149,8 @@
    -
    user manual · rev. 2.5  ·  Version française
    -

    _chordlab

    +
    user manual · rev. 2.8  ·  Version française
    +

    _chordlab ORC

    A chord and progression machine. The whole idea fits in one sentence: you don't play the notes of a chord, you describe it — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.

    @@ -486,6 +486,7 @@

    The 6 m7 M7 9 11 13 tensions stay active and are harmonised within the scale: the 7th on G in C major gives a real G7 (with a natural F). The asterisk in the name (G7*) marks that diatonic harmonisation.

    Harmonic minor is worth a try: its V degree becomes a genuine dominant chord with a leading tone — the classic minor cadence, with one finger.
    +

    Type and tensions. By default the quality comes from the degree (auto mode). You can pin a type button to force a quality on the played root, then click again to return to auto; the root always stays in key. All twelve tensions and alterations (including ♭9, ♯9, ♯11, ♭5, ♯5) work in both cases.

    @@ -519,6 +520,7 @@
  • 3Click the slot — audition it on its own.
  • 4− / + — length from 1 to 16 beats, one beat at a time. A 3-, 5- or 7-beat chord fits in a single slot.
  • 5‹ › — move the slot along the sequence.
  • +
  • Edit the chord — reloads its configuration into the controls; ✓ update rewrites the slot, ✕ cancel leaves it untouched.
  • fig. 7 — anatomy of a progression slot
    @@ -690,6 +692,8 @@ render perfyes: strums and arpeggios are written note by note into the file · no: block chords (better if you plan to arpeggiate later in the DAW or tracker) +

    Alongside the .mid, the ↓ tracker (.txt) button produces a text file for hand transcription onto a tracker (Polyend Tracker Mini and similar): a pattern grid where each note takes a track (T1..Tn low→high) plus a bass column, with OFF to stop and ... to hold, followed by a reference section giving the notes and their MIDI numbers. The tracker resol. selector (1/16, 1/8, 1/4) should match your machine's pattern resolution.

    +

    Sessions

    ↓ save session writes a .json holding every setting and the full progression; ↑ load restores all of it. Nothing is kept in the browser: the file is the only persistence — version your sessions in Git like any other project file.

    @@ -730,7 +734,7 @@ -
    _chordlab · manual rev. 2.5 · underscorelab / leradiologue
    +