chordlab V2.1
This commit is contained in:
+69
-14
@@ -246,7 +246,7 @@
|
||||
</div>
|
||||
<div class="lcd-right">
|
||||
<div>VOICING <b id="lcdVoicing">0</b> · <b id="lcdDrop">CLOSE</b></div>
|
||||
<div>KEY <b id="lcdKey">OFF</b> · PERF <b id="lcdPerf">BLOCK</b></div>
|
||||
<div>KEY <b id="lcdKey">OFF</b> · PERF <b id="lcdPerf">BLOCK</b> · B <b id="lcdBassPerf">TENUE</b></div>
|
||||
<div>MIDI <b id="lcdMidi">—</b> · VL <b id="lcdVl">OFF</b></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,6 +333,20 @@
|
||||
<option value="-12" selected>−1 oct</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>rythme B</label>
|
||||
<select id="bassPerfSel">
|
||||
<option value="held">tenue</option>
|
||||
<option value="quarter">noires</option>
|
||||
<option value="eighth">croches</option>
|
||||
<option value="offbeat">contretemps</option>
|
||||
<option value="octave">octaves</option>
|
||||
<option value="root5">fond.–quinte</option>
|
||||
<option value="walk">boogie</option>
|
||||
<option value="tresillo">3–3–2</option>
|
||||
<option value="pump">pompe</option>
|
||||
<option value="rnd">aléatoire</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toggle-line">
|
||||
<button class="hwbtn" id="keyBtn"><span class="led"></span>KEY</button>
|
||||
<select id="keyRoot"></select>
|
||||
@@ -435,7 +449,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>_chordlab v2.0 · logique d'accords inspirée de l'Orchid ORC-1 (implémentation indépendante étendue)</footer>
|
||||
<footer>_chordlab v2.1 · logique d'accords inspirée de l'Orchid ORC-1 (implémentation indépendante étendue)</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -465,6 +479,7 @@ const state = {
|
||||
playing:false, loop:false, playIdx:-1, playTimer:null,
|
||||
vlAuto:false,
|
||||
perf:"block", rate:0.25,
|
||||
bassPerf:"held",
|
||||
humanize:false,
|
||||
};
|
||||
|
||||
@@ -662,6 +677,35 @@ function suggestNext(){
|
||||
RENDU D'ÉVÉNEMENTS — partagé audio / MIDI live / export
|
||||
events: [{n, at (beats), dur (beats), vel, isBass}]
|
||||
========================================================= */
|
||||
function renderBassEvents(bassMidi, beats, hV, hT){
|
||||
const p = state.bassPerf, ev = [];
|
||||
const push = (n, at, dur, vel=110)=>{
|
||||
at = Math.max(0, at + hT());
|
||||
dur = Math.min(dur, Math.max(0.1, beats - at - 0.02));
|
||||
ev.push({n, at, dur, vel:vel+hV(), isBass:true});
|
||||
};
|
||||
if (p==="held"){ push(bassMidi, 0, beats*0.97); }
|
||||
else if (p==="quarter"){ for(let t=0;t<beats;t+=1) push(bassMidi,t,0.9); }
|
||||
else if (p==="eighth"){ for(let t=0;t<beats;t+=0.5) push(bassMidi,t,0.42); }
|
||||
else if (p==="offbeat"){ for(let t=0.5;t<beats;t+=1) push(bassMidi,t,0.42); }
|
||||
else if (p==="octave"){ let i=0; for(let t=0;t<beats;t+=0.5) push(i++%2?bassMidi+12:bassMidi,t,0.42); }
|
||||
else if (p==="root5"){ let i=0; for(let t=0;t<beats;t+=1) push(i++%2?bassMidi+7:bassMidi,t,0.9); }
|
||||
else if (p==="walk"){ const seq=[0,7,12,7]; let i=0; for(let t=0;t<beats;t+=1) push(bassMidi+seq[i++%4],t,0.9); }
|
||||
else if (p==="tresillo"){
|
||||
for(let c=0;c<beats;c+=4)
|
||||
[0,1.5,3].forEach(o=>{ if(c+o<beats) push(bassMidi,c+o,1.2,114); });
|
||||
}
|
||||
else if (p==="pump"){ for(let t=0;t<beats;t+=1) push(bassMidi,t,0.38,114); }
|
||||
else if (p==="rnd"){
|
||||
const opts=[0,0,7,12];
|
||||
for(let t=0;t<beats;t+=0.5)
|
||||
if (Math.random()<0.65) push(bassMidi+opts[Math.floor(Math.random()*opts.length)],t,0.42);
|
||||
if (!ev.length) push(bassMidi,0,0.9); // jamais de silence total
|
||||
}
|
||||
else { push(bassMidi, 0, beats*0.97); }
|
||||
return ev;
|
||||
}
|
||||
|
||||
function renderEvents(chord, beats, perfOverride){
|
||||
const perf = perfOverride || state.perf;
|
||||
const rate = state.rate;
|
||||
@@ -670,7 +714,7 @@ function renderEvents(chord, beats, perfOverride){
|
||||
const hV = ()=> hum ? Math.round((Math.random()-0.5)*20) : 0;
|
||||
const hT = ()=> hum ? (Math.random()-0.5)*0.03 : 0;
|
||||
if (chord.bass !== null && chord.bass !== undefined)
|
||||
ev.push({n:chord.bass, at:0, dur:beats*0.97, vel:110+hV(), isBass:true});
|
||||
ev.push(...renderBassEvents(chord.bass, beats, hV, hT));
|
||||
const notes = chord.notes;
|
||||
if (perf==="block"){
|
||||
notes.forEach(n=>ev.push({n, at:Math.max(0,hT()), dur:beats*0.95, vel:100+hV(), isBass:false}));
|
||||
@@ -869,6 +913,7 @@ function saveSession(){
|
||||
drop:state.drop, slash:state.slash, bass:state.bass, bassOct:state.bassOct,
|
||||
keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale,
|
||||
perf:state.perf, rate:state.rate, humanize:state.humanize,
|
||||
bassPerf:state.bassPerf,
|
||||
bpm:getBpm(), vlAuto:state.vlAuto,
|
||||
},
|
||||
progression:state.progression,
|
||||
@@ -898,9 +943,11 @@ function loadSession(file){
|
||||
state.keyScale=s.keyScale||"major";
|
||||
state.perf=s.perf||"block";
|
||||
state.rate=s.rate||0.25;
|
||||
state.bassPerf=s.bassPerf||"held";
|
||||
state.humanize=!!s.humanize;
|
||||
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)); });
|
||||
if (s.bpm) $("bpm").value=s.bpm;
|
||||
syncUiFromState();
|
||||
renderProgression();
|
||||
@@ -919,6 +966,8 @@ function syncUiFromState(){
|
||||
$("keyRoot").value=state.keyRoot;
|
||||
$("keyScaleSel").value=state.keyScale;
|
||||
$("perfSel").value=state.perf; $("rateSel").value=state.rate;
|
||||
$("bassPerfSel").value=state.bassPerf;
|
||||
$("lcdBassPerf").textContent=$("bassPerfSel").selectedOptions[0].textContent.toUpperCase();
|
||||
$("humanBtn").classList.toggle("on", state.humanize);
|
||||
$("vlBtn").classList.toggle("on", state.vlAuto);
|
||||
$("lcdDrop").textContent=state.drop.toUpperCase();
|
||||
@@ -1085,6 +1134,10 @@ $("perfSel").addEventListener("change",function(){
|
||||
$("lcdPerf").textContent=this.selectedOptions[0].textContent.toUpperCase();
|
||||
});
|
||||
$("rateSel").addEventListener("change",function(){state.rate=parseFloat(this.value);});
|
||||
$("bassPerfSel").addEventListener("change",function(){
|
||||
state.bassPerf=this.value;
|
||||
$("lcdBassPerf").textContent=this.selectedOptions[0].textContent.toUpperCase();
|
||||
});
|
||||
$("humanBtn").addEventListener("click",function(){
|
||||
state.humanize=!state.humanize;
|
||||
this.classList.toggle("on",state.humanize);
|
||||
@@ -1121,7 +1174,6 @@ $("optimizeBtn").addEventListener("click",()=>{
|
||||
$("sessionStatus").textContent="Voice-leading appliqué.";
|
||||
});
|
||||
|
||||
const BEAT_CYCLE=[4,8,2,1];
|
||||
function renderProgression(){
|
||||
const el=$("progSlots");
|
||||
el.innerHTML="";
|
||||
@@ -1139,28 +1191,31 @@ function renderProgression(){
|
||||
<div class="sname">${c.label}</div>
|
||||
<div class="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div>
|
||||
<div class="srow">
|
||||
<button class="mv" title="déplacer à gauche">‹</button>
|
||||
<span class="badge" title="durée en temps (clic pour changer)">${c.beats}♩</span>
|
||||
<button class="mv" title="déplacer à droite">›</button>
|
||||
<button class="mv sl" title="déplacer à gauche">‹</button>
|
||||
<button class="mv bminus" title="−1 temps">−</button>
|
||||
<span class="badge" title="durée en temps (1 à 16)">${c.beats}♩</span>
|
||||
<button class="mv bplus" title="+1 temps">+</button>
|
||||
<button class="mv sr" title="déplacer à droite">›</button>
|
||||
</div>`;
|
||||
s.querySelector(".del").addEventListener("click",ev=>{
|
||||
ev.stopPropagation();
|
||||
state.progression.splice(i,1);renderProgression();
|
||||
});
|
||||
const [left,right]=s.querySelectorAll(".mv");
|
||||
left.addEventListener("click",ev=>{
|
||||
s.querySelector(".sl").addEventListener("click",ev=>{
|
||||
ev.stopPropagation();
|
||||
if (i>0){[state.progression[i-1],state.progression[i]]=[state.progression[i],state.progression[i-1]];renderProgression();}
|
||||
});
|
||||
right.addEventListener("click",ev=>{
|
||||
s.querySelector(".sr").addEventListener("click",ev=>{
|
||||
ev.stopPropagation();
|
||||
if (i<state.progression.length-1){[state.progression[i+1],state.progression[i]]=[state.progression[i],state.progression[i+1]];renderProgression();}
|
||||
});
|
||||
s.querySelector(".badge").addEventListener("click",ev=>{
|
||||
s.querySelector(".bminus").addEventListener("click",ev=>{
|
||||
ev.stopPropagation();
|
||||
const idx=BEAT_CYCLE.indexOf(c.beats);
|
||||
c.beats=BEAT_CYCLE[(idx+1)%BEAT_CYCLE.length];
|
||||
renderProgression();
|
||||
c.beats=Math.max(1,c.beats-1);renderProgression();
|
||||
});
|
||||
s.querySelector(".bplus").addEventListener("click",ev=>{
|
||||
ev.stopPropagation();
|
||||
c.beats=Math.min(16,c.beats+1);renderProgression();
|
||||
});
|
||||
s.addEventListener("click",()=>{
|
||||
const ev=renderEvents(c, Math.min(c.beats,2));
|
||||
|
||||
Reference in New Issue
Block a user