MAJ: edition des accords dans la progression
This commit is contained in:
@@ -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
|
## [2.5] — interface bilingue
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -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 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.
|
- 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.
|
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 :
|
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.
|
- **− / +** 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
|
- **‹ ›** — déplacer le slot dans la séquence
|
||||||
- **×** — supprimer
|
- **×** — supprimer
|
||||||
- **clic sur le slot** — l'auditionner seul
|
- **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.
|
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** :
|
**Destinations typiques** :
|
||||||
- **DAW** — glisser-déposer sur une piste instrument, c'est tout.
|
- **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.
|
- **Polyend Tracker Mini** — copier le `.mid` sur la carte SD et l'importer ; chaque piste MIDI devient une piste du pattern.
|
||||||
|
|||||||
+292
-46
@@ -187,6 +187,16 @@
|
|||||||
font-size:.72rem;
|
font-size:.72rem;
|
||||||
}
|
}
|
||||||
.slot.playing{border-color:var(--led-on);box-shadow:0 0 10px rgba(255,174,51,.35)}
|
.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 .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 .snotes{opacity:.55;font-size:.56rem;margin-top:.15rem}
|
||||||
.slot .srow{display:flex;gap:.3rem;margin-top:.3rem;align-items:center}
|
.slot .srow{display:flex;gap:.3rem;margin-top:.3rem;align-items:center}
|
||||||
@@ -256,7 +266,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="unit">
|
<div class="unit">
|
||||||
<header>
|
<header>
|
||||||
<h1><span>_chordlab·v2</span></h1>
|
<h1>_chordlab <span>ORC·v2</span></h1>
|
||||||
<div class="hdr-right">
|
<div class="hdr-right">
|
||||||
<span class="sub" id="tagline">chord ideas machine · underscorelab / leradiologue</span>
|
<span class="sub" id="tagline">chord ideas machine · underscorelab / leradiologue</span>
|
||||||
<span class="seg" role="group" aria-label="Language">
|
<span class="seg" role="group" aria-label="Language">
|
||||||
@@ -409,6 +419,8 @@
|
|||||||
<div class="prog-slots" id="progSlots"></div>
|
<div class="prog-slots" id="progSlots"></div>
|
||||||
<div class="transport">
|
<div class="transport">
|
||||||
<button class="hwbtn" id="addBtn"><span class="led"></span>ADD (␣)</button>
|
<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="playBtn"><span class="led"></span>PLAY</button>
|
||||||
<button class="hwbtn" id="loopBtn"><span class="led"></span>LOOP</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>
|
<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 class="status" id="midiStatus">Web MIDI : en attente…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mod">
|
<div class="mod">
|
||||||
<h2>Export MIDI</h2>
|
<h2>Export</h2>
|
||||||
<div class="io-line"><label>pistes</label><select id="exportSplit">
|
<div class="io-line"><label>pistes</label><select id="exportSplit">
|
||||||
<option value="split">accords + basse (2 pistes)</option>
|
<option value="split">accords + basse (2 pistes)</option>
|
||||||
<option value="chords">accords seuls</option>
|
<option value="chords">accords seuls</option>
|
||||||
@@ -456,6 +468,12 @@
|
|||||||
<option value="block">non (accords plaqués)</option>
|
<option value="block">non (accords plaqués)</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
<button class="export-btn" id="exportBtn">↓ exporter .mid</button>
|
<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 class="status" id="exportStatus"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mod">
|
<div class="mod">
|
||||||
@@ -472,7 +490,7 @@
|
|||||||
<span class="docsep">·</span>
|
<span class="docsep">·</span>
|
||||||
<a href="tuto_en.html" class="doclink" id="doclinkEn">Manual (EN)</a>
|
<a href="tuto_en.html" class="doclink" id="doclinkEn">Manual (EN)</a>
|
||||||
<span class="docsep">·</span>
|
<span class="docsep">·</span>
|
||||||
_chordlab v2.5 · underscorelab / leradiologue
|
_chordlab v2.8 · underscorelab / leradiologue
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -496,6 +514,8 @@ const state = {
|
|||||||
bass:false,
|
bass:false,
|
||||||
bassOct:-12,
|
bassOct:-12,
|
||||||
keyMode:false,
|
keyMode:false,
|
||||||
|
keyTypeOverride:null,
|
||||||
|
editIndex:null,
|
||||||
keyRoot:0,
|
keyRoot:0,
|
||||||
keyScale:"major",
|
keyScale:"major",
|
||||||
lastRoot:null,
|
lastRoot:null,
|
||||||
@@ -506,6 +526,7 @@ const state = {
|
|||||||
humanize:false,
|
humanize:false,
|
||||||
layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty",
|
layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty",
|
||||||
lang:(navigator.language||"").toLowerCase().startsWith("fr") ? "fr" : "en",
|
lang:(navigator.language||"").toLowerCase().startsWith("fr") ? "fr" : "en",
|
||||||
|
trackerRes:2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- Moteur d'accords ---------- */
|
/* ---------- Moteur d'accords ---------- */
|
||||||
@@ -606,38 +627,63 @@ function diatonicChord(notePc, keyRoot, scaleName, exts){
|
|||||||
const deg = scale.indexOf(rel);
|
const deg = scale.indexOf(rel);
|
||||||
if (deg === -1) return null;
|
if (deg === -1) return null;
|
||||||
const pick = j => scale[(deg+j)%7] + 12*Math.floor((deg+j)/7);
|
const pick = j => scale[(deg+j)%7] + 12*Math.floor((deg+j)/7);
|
||||||
const semis = [pick(0), pick(2), pick(4)];
|
const degSemis = [pick(0), pick(2), pick(4)]; // triade diatonique
|
||||||
if (exts.has("6")) semis.push(pick(5));
|
if (exts.has("6")) degSemis.push(pick(5));
|
||||||
if (exts.has("m7") || exts.has("M7")) semis.push(pick(6));
|
if (exts.has("m7") || exts.has("M7")) degSemis.push(pick(6));
|
||||||
if (exts.has("9")) semis.push(pick(8));
|
if (exts.has("9")) degSemis.push(pick(8));
|
||||||
if (exts.has("11")) semis.push(pick(10));
|
if (exts.has("11")) degSemis.push(pick(10));
|
||||||
if (exts.has("13")) semis.push(pick(12));
|
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 root = BASE + notePc;
|
||||||
const notes = semis.map(s => BASE + keyRoot + s);
|
const final = iv.map(i => root + i).sort((a,b)=>a-b);
|
||||||
const shift = root - notes[0];
|
// qualité de la triade (éventuellement altérée)
|
||||||
let final = notes.map(n => n + shift);
|
const has = x => iv.includes(x);
|
||||||
if (exts.has("no3")) final = final.filter((n,i)=>i!==1);
|
let q="";
|
||||||
final.sort((a,b)=>a-b);
|
if (has(4)&&has(8)) q="aug"; else if (has(4)&&has(7)) q="";
|
||||||
const i1 = final[1]-final[0], i2 = final.length>2 ? final[2]-final[0] : 7;
|
else if (has(3)&&has(7)) q="m"; else if (has(3)&&has(6)) q="dim";
|
||||||
let q = "";
|
else if (has(4)) q=""; else if (has(3)) q="m";
|
||||||
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";
|
|
||||||
let name = NOTE_NAMES[notePc] + q;
|
let name = NOTE_NAMES[notePc] + q;
|
||||||
if (exts.has("m7")||exts.has("M7")) name += "7*";
|
if (exts.has("m7")||exts.has("M7")) name += "7";
|
||||||
if (exts.has("6")) name += "6*";
|
if (exts.has("6")) name += "6";
|
||||||
if (exts.has("9")) name += "(9)*";
|
const t=[];
|
||||||
if (exts.has("11")) name += "(11)*";
|
if (exts.has("9")) t.push("9");
|
||||||
if (exts.has("13")) name += "(13)*";
|
if (exts.has("b9")) t.push("♭9");
|
||||||
return {notes: final, name, deg};
|
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 ---------- */
|
/* ---------- Accord courant ---------- */
|
||||||
function currentChord(rootPc){
|
function currentChord(rootPc){
|
||||||
let raw, name;
|
let raw, name;
|
||||||
if (state.keyMode){
|
if (state.keyMode){
|
||||||
const d = diatonicChord(rootPc, state.keyRoot, state.keyScale, state.exts);
|
const rel = (rootPc - state.keyRoot + 12) % 12;
|
||||||
if (!d) return null;
|
if (!SCALES[state.keyScale].includes(rel)) return null; // fondamentale hors tonalité
|
||||||
raw = d.notes; name = d.name;
|
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 {
|
} else {
|
||||||
raw = buildChord(rootPc, state.type, state.exts);
|
raw = buildChord(rootPc, state.type, state.exts);
|
||||||
name = chordName(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];
|
if (b.iv !== 0) label += "/" + NOTE_NAMES[(pc + b.iv) % 12];
|
||||||
}
|
}
|
||||||
state.progression.push({
|
state.progression.push({
|
||||||
label, notes, bass, rootPc: pc, beats: 4,
|
label, notes, bass, rootPc: pc, beats: 4, cfg:diatonicCfg(pc),
|
||||||
});
|
});
|
||||||
renderProgression();
|
renderProgression();
|
||||||
}
|
}
|
||||||
@@ -746,16 +792,20 @@ function renderEvents(chord, beats, perfOverride){
|
|||||||
/* =========================================================
|
/* =========================================================
|
||||||
AUDIO — poly Web Audio
|
AUDIO — poly Web Audio
|
||||||
========================================================= */
|
========================================================= */
|
||||||
let AC=null, master=null;
|
let AC=null, master=null, audioBroken=false;
|
||||||
function ensureAudio(){
|
function ensureAudio(){
|
||||||
if (AC) return;
|
if (AC || audioBroken) return;
|
||||||
AC = new (window.AudioContext||window.webkitAudioContext)();
|
try{
|
||||||
master = AC.createGain(); master.gain.value = 0.5;
|
AC = new (window.AudioContext||window.webkitAudioContext)();
|
||||||
const comp = AC.createDynamicsCompressor();
|
master = AC.createGain(); master.gain.value = 0.5;
|
||||||
master.connect(comp); comp.connect(AC.destination);
|
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){
|
function playNote(midi, dur, vel=0.8, isBass=false, delayS=0){
|
||||||
ensureAudio();
|
ensureAudio();
|
||||||
|
if (!AC) return;
|
||||||
|
try{
|
||||||
const t = AC.currentTime + delayS;
|
const t = AC.currentTime + delayS;
|
||||||
const f = 440 * Math.pow(2,(midi-69)/12);
|
const f = 440 * Math.pow(2,(midi-69)/12);
|
||||||
const g = AC.createGain();
|
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.linearRampToValueAtTime(peak, t+0.015);
|
||||||
g.gain.setTargetAtTime(peak*0.7, t+0.05, 0.25);
|
g.gain.setTargetAtTime(peak*0.7, t+0.05, 0.25);
|
||||||
g.gain.setTargetAtTime(0, t+dur, 0.12);
|
g.gain.setTargetAtTime(0, t+dur, 0.12);
|
||||||
|
}catch(e){ /* aperçu audio non bloquant */ }
|
||||||
}
|
}
|
||||||
function playEventsAudio(events, bpm){
|
function playEventsAudio(events, bpm){
|
||||||
const spb = 60/bpm;
|
const spb = 60/bpm;
|
||||||
@@ -904,6 +955,105 @@ function exportMidi(){
|
|||||||
$("exportStatus").textContent=t("Exporté : ")+a.download;
|
$("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)
|
SESSION save/load (fichier JSON — pas de localStorage)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
@@ -913,9 +1063,9 @@ function saveSession(){
|
|||||||
settings:{
|
settings:{
|
||||||
type:state.type, exts:[...state.exts], voicing:state.voicing,
|
type:state.type, exts:[...state.exts], voicing:state.voicing,
|
||||||
drop:state.drop, slash:state.slash, bass:state.bass, bassOct:state.bassOct,
|
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,
|
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,
|
bpm:getBpm(), vlAuto:state.vlAuto,
|
||||||
},
|
},
|
||||||
progression:state.progression,
|
progression:state.progression,
|
||||||
@@ -941,6 +1091,7 @@ function loadSession(file){
|
|||||||
state.bass=!!s.bass;
|
state.bass=!!s.bass;
|
||||||
state.bassOct=s.bassOct||-12;
|
state.bassOct=s.bassOct||-12;
|
||||||
state.keyMode=!!s.keyMode;
|
state.keyMode=!!s.keyMode;
|
||||||
|
state.keyTypeOverride=(s.keyTypeOverride in TYPE_INTERVALS)?s.keyTypeOverride:null;
|
||||||
state.keyRoot=s.keyRoot||0;
|
state.keyRoot=s.keyRoot||0;
|
||||||
state.keyScale=s.keyScale||"major";
|
state.keyScale=s.keyScale||"major";
|
||||||
state.perf=s.perf||"block";
|
state.perf=s.perf||"block";
|
||||||
@@ -948,6 +1099,7 @@ function loadSession(file){
|
|||||||
state.humanize=!!s.humanize;
|
state.humanize=!!s.humanize;
|
||||||
state.layout=(s.layout==="qwerty")?"qwerty":(s.layout==="azerty"?"azerty":state.layout);
|
state.layout=(s.layout==="qwerty")?"qwerty":(s.layout==="azerty"?"azerty":state.layout);
|
||||||
state.lang=(s.lang==="en")?"en":(s.lang==="fr"?"fr":state.lang);
|
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.vlAuto=!!s.vlAuto;
|
||||||
state.progression=Array.isArray(d.progression)?d.progression:[];
|
state.progression=Array.isArray(d.progression)?d.progression:[];
|
||||||
state.progression.forEach(c=>{ c.beats=Math.max(1,Math.min(16,c.beats||4)); });
|
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);
|
r.readAsText(file);
|
||||||
}
|
}
|
||||||
function syncUiFromState(){
|
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)));
|
document.querySelectorAll("[data-ext]").forEach(b=>b.classList.toggle("on", state.exts.has(b.dataset.ext)));
|
||||||
$("dropSel").value=state.drop; $("slashSel").value=state.slash;
|
$("dropSel").value=state.drop; $("slashSel").value=state.slash;
|
||||||
$("bassBtn").classList.toggle("on", state.bass);
|
$("bassBtn").classList.toggle("on", state.bass);
|
||||||
@@ -969,6 +1121,7 @@ function syncUiFromState(){
|
|||||||
$("keyRoot").value=state.keyRoot;
|
$("keyRoot").value=state.keyRoot;
|
||||||
$("keyScaleSel").value=state.keyScale;
|
$("keyScaleSel").value=state.keyScale;
|
||||||
$("perfSel").value=state.perf; $("rateSel").value=state.rate;
|
$("perfSel").value=state.perf; $("rateSel").value=state.rate;
|
||||||
|
$("trackerRes").value=state.trackerRes;
|
||||||
$("humanBtn").classList.toggle("on", state.humanize);
|
$("humanBtn").classList.toggle("on", state.humanize);
|
||||||
setLayout(state.layout);
|
setLayout(state.layout);
|
||||||
$("vlBtn").classList.toggle("on", state.vlAuto);
|
$("vlBtn").classList.toggle("on", state.vlAuto);
|
||||||
@@ -1022,11 +1175,21 @@ function triggerChord(rootPc){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* type radio */
|
/* 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=>{
|
document.querySelectorAll("#typeBtns .hwbtn").forEach(btn=>{
|
||||||
btn.addEventListener("click",()=>{
|
btn.addEventListener("click",()=>{
|
||||||
state.type=btn.dataset.type;
|
if (state.keyMode){
|
||||||
document.querySelectorAll("#typeBtns .hwbtn").forEach(b=>
|
state.keyTypeOverride = (state.keyTypeOverride===btn.dataset.type) ? null : btn.dataset.type;
|
||||||
b.classList.toggle("on", b.dataset.type===state.type));
|
} else {
|
||||||
|
state.type = btn.dataset.type;
|
||||||
|
}
|
||||||
|
updateTypeLeds();
|
||||||
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1078,7 +1241,7 @@ window.addEventListener("keydown",ev=>{
|
|||||||
if (k in map){
|
if (k in map){
|
||||||
keyEls[map[k]].classList.add("pressed");
|
keyEls[map[k]].classList.add("pressed");
|
||||||
triggerChord(map[k]);
|
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==="ArrowRight"){ setVoicing(state.voicing+1); }
|
||||||
else if (ev.key==="ArrowLeft"){ 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(){
|
$("keyBtn").addEventListener("click",function(){
|
||||||
state.keyMode=!state.keyMode;
|
state.keyMode=!state.keyMode;
|
||||||
this.classList.toggle("on",state.keyMode);
|
this.classList.toggle("on",state.keyMode);
|
||||||
|
updateTypeLeds();
|
||||||
updateKeyLcd();refreshKeyDimming();
|
updateKeyLcd();refreshKeyDimming();
|
||||||
});
|
});
|
||||||
$("keyRoot").addEventListener("change",function(){state.keyRoot=parseInt(this.value);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();
|
$("lcdPerf").textContent=this.selectedOptions[0].textContent.toUpperCase();
|
||||||
});
|
});
|
||||||
$("rateSel").addEventListener("change",function(){state.rate=parseFloat(this.value);});
|
$("rateSel").addEventListener("change",function(){state.rate=parseFloat(this.value);});
|
||||||
|
$("trackerRes").addEventListener("change",function(){state.trackerRes=parseInt(this.value);});
|
||||||
$("humanBtn").addEventListener("click",function(){
|
$("humanBtn").addEventListener("click",function(){
|
||||||
state.humanize=!state.humanize;
|
state.humanize=!state.humanize;
|
||||||
this.classList.toggle("on",state.humanize);
|
this.classList.toggle("on",state.humanize);
|
||||||
@@ -1178,14 +1343,16 @@ function addToProgression(){
|
|||||||
let notes=[...chord.notes];
|
let notes=[...chord.notes];
|
||||||
if (state.vlAuto && state.progression.length)
|
if (state.vlAuto && state.progression.length)
|
||||||
notes=bestVoicingAgainst(notes, state.progression[state.progression.length-1].notes);
|
notes=bestVoicingAgainst(notes, state.progression[state.progression.length-1].notes);
|
||||||
|
exitEdit();
|
||||||
state.progression.push({
|
state.progression.push({
|
||||||
label:chord.name, notes,
|
label:chord.name, notes,
|
||||||
bass:chord.bass, rootPc:chord.rootPc, beats:4,
|
bass:chord.bass, rootPc:chord.rootPc, beats:4,
|
||||||
|
cfg:slotCfg(chord.rootPc),
|
||||||
});
|
});
|
||||||
renderProgression();
|
renderProgression();
|
||||||
}
|
}
|
||||||
$("addBtn").addEventListener("click",addToProgression);
|
$("addBtn").addEventListener("click",addToProgression);
|
||||||
$("progClear").addEventListener("click",()=>{stopPlayback();state.progression=[];renderProgression();});
|
$("progClear").addEventListener("click",()=>{stopPlayback();exitEdit();state.progression=[];renderProgression();});
|
||||||
$("suggestBtn").addEventListener("click",suggestNext);
|
$("suggestBtn").addEventListener("click",suggestNext);
|
||||||
$("optimizeBtn").addEventListener("click",()=>{
|
$("optimizeBtn").addEventListener("click",()=>{
|
||||||
for (let i=1;i<state.progression.length;i++){
|
for (let i=1;i<state.progression.length;i++){
|
||||||
@@ -1196,6 +1363,75 @@ $("optimizeBtn").addEventListener("click",()=>{
|
|||||||
$("sessionStatus").textContent=t("Voice-leading appliqué.");
|
$("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(){
|
function renderProgression(){
|
||||||
const el=$("progSlots");
|
const el=$("progSlots");
|
||||||
el.innerHTML="";
|
el.innerHTML="";
|
||||||
@@ -1208,19 +1444,25 @@ function renderProgression(){
|
|||||||
}
|
}
|
||||||
state.progression.forEach((c,i)=>{
|
state.progression.forEach((c,i)=>{
|
||||||
const s=document.createElement("div");
|
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>
|
s.innerHTML=`<button class="del" title="${t('supprimer')}">×</button>
|
||||||
<div class="sname">${c.label}</div>
|
<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="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div>
|
||||||
<div class="srow">
|
<div class="srow">
|
||||||
|
<button class="mv edit" title="${t('éditer')}">✎</button>
|
||||||
<button class="mv sl" title="${t('déplacer à gauche')}">‹</button>
|
<button class="mv sl" title="${t('déplacer à gauche')}">‹</button>
|
||||||
<button class="mv bminus" title="${t('−1 temps')}">−</button>
|
<button class="mv bminus" title="${t('−1 temps')}">−</button>
|
||||||
<span class="badge" title="${t('durée en temps (1 à 16)')}">${c.beats}♩</span>
|
<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 bplus" title="${t('+1 temps')}">+</button>
|
||||||
<button class="mv sr" title="${t('déplacer à droite')}">›</button>
|
<button class="mv sr" title="${t('déplacer à droite')}">›</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
s.querySelector(".edit").addEventListener("click",ev=>{
|
||||||
|
ev.stopPropagation();
|
||||||
|
state.editIndex===i ? cancelEdit() : enterEdit(i);
|
||||||
|
});
|
||||||
s.querySelector(".del").addEventListener("click",ev=>{
|
s.querySelector(".del").addEventListener("click",ev=>{
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
exitEdit();
|
||||||
state.progression.splice(i,1);renderProgression();
|
state.progression.splice(i,1);renderProgression();
|
||||||
});
|
});
|
||||||
s.querySelector(".sl").addEventListener("click",ev=>{
|
s.querySelector(".sl").addEventListener("click",ev=>{
|
||||||
@@ -1270,7 +1512,7 @@ $("presetLoad").addEventListener("click",()=>{
|
|||||||
bass=b.midi;
|
bass=b.midi;
|
||||||
if (b.iv!==0) label+="/"+NOTE_NAMES[(pc+b.iv)%12];
|
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();
|
renderProgression();
|
||||||
});
|
});
|
||||||
@@ -1329,6 +1571,7 @@ $("midiDev").addEventListener("change",selectMidiDevice);
|
|||||||
|
|
||||||
/* export & session */
|
/* export & session */
|
||||||
$("exportBtn").addEventListener("click",exportMidi);
|
$("exportBtn").addEventListener("click",exportMidi);
|
||||||
|
$("exportTxtBtn").addEventListener("click",exportTracker);
|
||||||
$("saveJson").addEventListener("click",saveSession);
|
$("saveJson").addEventListener("click",saveSession);
|
||||||
$("loadJson").addEventListener("click",()=>$("fileInput").click());
|
$("loadJson").addEventListener("click",()=>$("fileInput").click());
|
||||||
$("fileInput").addEventListener("change",function(){
|
$("fileInput").addEventListener("change",function(){
|
||||||
@@ -1352,13 +1595,14 @@ const EN = {
|
|||||||
"#exportBtn":"↓ export .mid",
|
"#exportBtn":"↓ export .mid",
|
||||||
"#saveJson":"↓ save session (.json)", "#loadJson":"↑ load a session",
|
"#saveJson":"↓ save session (.json)", "#loadJson":"↑ load a session",
|
||||||
"#doclinkFr":"Manual (FR)", "#doclinkEn":"Manual (EN)",
|
"#doclinkFr":"Manual (FR)", "#doclinkEn":"Manual (EN)",
|
||||||
|
"#updateBtn":"✓ update", "#cancelEditBtn":"✕ cancel",
|
||||||
},
|
},
|
||||||
h2:{ "Chord type":"Chord type", "Root":"Root", "Voicing":"Voicing",
|
h2:{ "Chord type":"Chord type", "Root":"Root", "Voicing":"Voicing",
|
||||||
"Progression":"Progression", "MIDI out (live)":"MIDI out (live)",
|
"Progression":"Progression", "MIDI out (live)":"MIDI out (live)",
|
||||||
"Export MIDI":"MIDI export", "Session":"Session" },
|
"Export":"Export", "Session":"Session" },
|
||||||
rowlabel:{ "Tensions":"Tensions", "Altérations":"Alterations",
|
rowlabel:{ "Tensions":"Tensions", "Altérations":"Alterations",
|
||||||
"combos : dim+maj=aug · min+sus=sus2":"" },
|
"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",
|
"périphérique":"device", "canal accords":"chord channel",
|
||||||
"canal basse":"bass channel", "pistes":"tracks", "perf rendue":"render perf" },
|
"canal basse":"bass channel", "pistes":"tracks", "perf rendue":"render perf" },
|
||||||
option:{ "fondamentale":"root", "tierce":"third", "quinte":"fifth", "septième":"seventh",
|
option:{ "fondamentale":"root", "tierce":"third", "quinte":"fifth", "septième":"seventh",
|
||||||
@@ -1398,6 +1642,8 @@ const EN = {
|
|||||||
"+1 temps":"+1 beat",
|
"+1 temps":"+1 beat",
|
||||||
"durée en temps (1 à 16)":"length in beats (1 to 16)",
|
"durée en temps (1 à 16)":"length in beats (1 to 16)",
|
||||||
"Exporté : ":"Exported: ",
|
"Exporté : ":"Exported: ",
|
||||||
|
"Exporté (tracker) : ":"Exported (tracker): ",
|
||||||
|
"éditer":"edit",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const KEYHINT_EN = {
|
const KEYHINT_EN = {
|
||||||
|
|||||||
@@ -149,8 +149,8 @@
|
|||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
|
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<div class="kicker">manuel d'utilisation · rev. 2.5 · <a href="tuto_en.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">English version</a></div>
|
<div class="kicker">manuel d'utilisation · rev. 2.8 · <a href="tuto_en.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">English version</a></div>
|
||||||
<h1><em>_chordlab</em></h1>
|
<h1>_chordlab <em>ORC</em></h1>
|
||||||
<p>Machine à accords et à progressions. Le principe tient en une phrase : <strong>vous ne jouez pas les notes d'un accord, vous le décrivez</strong> — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.</p>
|
<p>Machine à accords et à progressions. Le principe tient en une phrase : <strong>vous ne jouez pas les notes d'un accord, vous le décrivez</strong> — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -486,6 +486,7 @@
|
|||||||
|
|
||||||
<p>Les tensions <code>6</code> <code>m7</code> <code>M7</code> <code>9</code> <code>11</code> <code>13</code> restent actives et sont harmonisées <strong>dans la gamme</strong> : la 7e de G en Do majeur donne un vrai G7 (avec F naturel). L'astérisque dans le nom (<code>G7*</code>) signale cette harmonisation diatonique.</p>
|
<p>Les tensions <code>6</code> <code>m7</code> <code>M7</code> <code>9</code> <code>11</code> <code>13</code> restent actives et sont harmonisées <strong>dans la gamme</strong> : la 7e de G en Do majeur donne un vrai G7 (avec F naturel). L'astérisque dans le nom (<code>G7*</code>) signale cette harmonisation diatonique.</p>
|
||||||
<div class="tip">Le <strong>mineur harmonique</strong> vaut le détour : son degré V devient un véritable accord de dominante avec sensible — la cadence mineure classique, à un doigt.</div>
|
<div class="tip">Le <strong>mineur harmonique</strong> vaut le détour : son degré V devient un véritable accord de dominante avec sensible — la cadence mineure classique, à un doigt.</div>
|
||||||
|
<p><strong>Type et tensions.</strong> Par défaut la qualité vient du degré (mode auto). Tu peux <strong>épingler</strong> 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.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ 06 PROGRESSION ============ -->
|
<!-- ============ 06 PROGRESSION ============ -->
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
<li><span class="dot">3</span><span class="t"><strong>Clic sur le slot</strong> — l'auditionner seul.</span></li>
|
<li><span class="dot">3</span><span class="t"><strong>Clic sur le slot</strong> — l'auditionner seul.</span></li>
|
||||||
<li><span class="dot">4</span><span class="t"><strong>− / +</strong> — durée de <strong>1 à 16 temps</strong>, au pas de 1. Un accord de 3, 5 ou 7 temps tient dans un seul slot.</span></li>
|
<li><span class="dot">4</span><span class="t"><strong>− / +</strong> — durée de <strong>1 à 16 temps</strong>, au pas de 1. Un accord de 3, 5 ou 7 temps tient dans un seul slot.</span></li>
|
||||||
<li><span class="dot">5</span><span class="t"><strong>‹ ›</strong> — déplacer le slot dans la séquence.</span></li>
|
<li><span class="dot">5</span><span class="t"><strong>‹ ›</strong> — déplacer le slot dans la séquence.</span></li>
|
||||||
|
<li><span class="dot" style="background:var(--led-green);box-shadow:none;color:#26231D">✎</span><span class="t"><strong>Éditer l'accord</strong> — recharge sa configuration dans les commandes ; <strong>✓ mettre à jour</strong> réécrit le slot, <strong>✕ annuler</strong> le laisse intact.</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<figcaption>fig. 7 — anatomie d'un slot de progression</figcaption>
|
<figcaption>fig. 7 — anatomie d'un slot de progression</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -690,6 +692,8 @@
|
|||||||
<tr><td><b>perf rendue</b></td><td><b>oui</b> : les strums/arps sont écrits note par note dans le fichier · <b>non</b> : accords plaqués (préférable si vous arpégez ensuite dans le DAW ou le Tracker)</td></tr>
|
<tr><td><b>perf rendue</b></td><td><b>oui</b> : les strums/arps sont écrits note par note dans le fichier · <b>non</b> : accords plaqués (préférable si vous arpégez ensuite dans le DAW ou le Tracker)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:1rem">À côté du <code>.mid</code>, le bouton <strong>↓ tracker (.txt)</strong> 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 (<code>T1..Tn</code> grave→aigu) plus une colonne basse, <code>OFF</code> pour couper et <code>...</code> pour maintenir, suivie d'une section de référence donnant les notes et leur numéro MIDI. Le sélecteur <strong>résol. tracker</strong> (1/16, 1/8, 1/4) doit correspondre à la résolution de pattern de ta machine.</p>
|
||||||
|
|
||||||
<h3>Sessions</h3>
|
<h3>Sessions</h3>
|
||||||
<p><strong>↓ sauver la session</strong> génère un <code>.json</code> avec tous les réglages et la progression complète ; <strong>↑ charger</strong> 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.</p>
|
<p><strong>↓ sauver la session</strong> génère un <code>.json</code> avec tous les réglages et la progression complète ; <strong>↑ charger</strong> 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.</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -730,7 +734,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>_chordlab · manuel rev. 2.5 · underscorelab / leradiologue</footer>
|
<footer>_chordlab · manuel rev. 2.8 · underscorelab / leradiologue</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+7
-3
@@ -149,8 +149,8 @@
|
|||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
|
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<div class="kicker">user manual · rev. 2.5 · <a href="tuto.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">Version française</a></div>
|
<div class="kicker">user manual · rev. 2.8 · <a href="tuto.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">Version française</a></div>
|
||||||
<h1><em>_chordlab</em></h1>
|
<h1>_chordlab <em>ORC</em></h1>
|
||||||
<p>A chord and progression machine. The whole idea fits in one sentence: <strong>you don't play the notes of a chord, you describe it</strong> — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.</p>
|
<p>A chord and progression machine. The whole idea fits in one sentence: <strong>you don't play the notes of a chord, you describe it</strong> — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -486,6 +486,7 @@
|
|||||||
|
|
||||||
<p>The <code>6</code> <code>m7</code> <code>M7</code> <code>9</code> <code>11</code> <code>13</code> tensions stay active and are harmonised <strong>within the scale</strong>: the 7th on G in C major gives a real G7 (with a natural F). The asterisk in the name (<code>G7*</code>) marks that diatonic harmonisation.</p>
|
<p>The <code>6</code> <code>m7</code> <code>M7</code> <code>9</code> <code>11</code> <code>13</code> tensions stay active and are harmonised <strong>within the scale</strong>: the 7th on G in C major gives a real G7 (with a natural F). The asterisk in the name (<code>G7*</code>) marks that diatonic harmonisation.</p>
|
||||||
<div class="tip"><strong>Harmonic minor</strong> is worth a try: its V degree becomes a genuine dominant chord with a leading tone — the classic minor cadence, with one finger.</div>
|
<div class="tip"><strong>Harmonic minor</strong> is worth a try: its V degree becomes a genuine dominant chord with a leading tone — the classic minor cadence, with one finger.</div>
|
||||||
|
<p><strong>Type and tensions.</strong> By default the quality comes from the degree (auto mode). You can <strong>pin</strong> 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.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ 06 PROGRESSION ============ -->
|
<!-- ============ 06 PROGRESSION ============ -->
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
<li><span class="dot">3</span><span class="t"><strong>Click the slot</strong> — audition it on its own.</span></li>
|
<li><span class="dot">3</span><span class="t"><strong>Click the slot</strong> — audition it on its own.</span></li>
|
||||||
<li><span class="dot">4</span><span class="t"><strong>− / +</strong> — length from <strong>1 to 16 beats</strong>, one beat at a time. A 3-, 5- or 7-beat chord fits in a single slot.</span></li>
|
<li><span class="dot">4</span><span class="t"><strong>− / +</strong> — length from <strong>1 to 16 beats</strong>, one beat at a time. A 3-, 5- or 7-beat chord fits in a single slot.</span></li>
|
||||||
<li><span class="dot">5</span><span class="t"><strong>‹ ›</strong> — move the slot along the sequence.</span></li>
|
<li><span class="dot">5</span><span class="t"><strong>‹ ›</strong> — move the slot along the sequence.</span></li>
|
||||||
|
<li><span class="dot" style="background:var(--led-green);box-shadow:none;color:#26231D">✎</span><span class="t"><strong>Edit the chord</strong> — reloads its configuration into the controls; <strong>✓ update</strong> rewrites the slot, <strong>✕ cancel</strong> leaves it untouched.</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
<figcaption>fig. 7 — anatomy of a progression slot</figcaption>
|
<figcaption>fig. 7 — anatomy of a progression slot</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -690,6 +692,8 @@
|
|||||||
<tr><td><b>render perf</b></td><td><b>yes</b>: strums and arpeggios are written note by note into the file · <b>no</b>: block chords (better if you plan to arpeggiate later in the DAW or tracker)</td></tr>
|
<tr><td><b>render perf</b></td><td><b>yes</b>: strums and arpeggios are written note by note into the file · <b>no</b>: block chords (better if you plan to arpeggiate later in the DAW or tracker)</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:1rem">Alongside the <code>.mid</code>, the <strong>↓ tracker (.txt)</strong> 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 (<code>T1..Tn</code> low→high) plus a bass column, with <code>OFF</code> to stop and <code>...</code> to hold, followed by a reference section giving the notes and their MIDI numbers. The <strong>tracker resol.</strong> selector (1/16, 1/8, 1/4) should match your machine's pattern resolution.</p>
|
||||||
|
|
||||||
<h3>Sessions</h3>
|
<h3>Sessions</h3>
|
||||||
<p><strong>↓ save session</strong> writes a <code>.json</code> holding every setting and the full progression; <strong>↑ load</strong> 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.</p>
|
<p><strong>↓ save session</strong> writes a <code>.json</code> holding every setting and the full progression; <strong>↑ load</strong> 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.</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -730,7 +734,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>_chordlab · manual rev. 2.5 · underscorelab / leradiologue</footer>
|
<footer>_chordlab · manual rev. 2.8 · underscorelab / leradiologue</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user