diff --git a/static/index.html b/static/index.html
index a43c61f..325158f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -238,7 +238,13 @@
.tempo-row { display:flex; align-items:center; gap:10px; margin-top: 8px; }
.tempo-row input[type=range] { flex:1; accent-color: var(--accent); }
.tempo-val { font-variant-numeric: tabular-nums; color: var(--accent-2); min-width: 64px; text-align:right; font-size:13px;}
- .beat-controls { display:flex; gap:8px; margin-top:12px; }
+ .beat-controls { display:flex; gap:8px; margin-top:12px; align-items:center; }
+ .beat-ind { display:flex; gap:6px; margin-left:auto; }
+ .beat-ind span {
+ width:9px; height:9px; border-radius:50%;
+ background: var(--line); transition: background .05s;
+ }
+ .beat-ind span.on { background: var(--accent); }
footer { grid-column: 1 / -1; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #3a342d; border-radius: 6px; border: 2px solid var(--panel); }
@@ -328,6 +334,8 @@ Double-click any word to look it up on the right.">
@@ -593,7 +601,11 @@ function cadenceColors(){
if(!analysis || !analysis.meter) return map;
const byPat = {};
analysis.meter.forEach(m=>{
- if(m.stress && m.stress.length >= 5) (byPat[m.stress] ||= []).push(m.l);
+ if(!m.stress || m.stress.length < 5) return;
+ // flexible monosyllables (x) read as stressed in delivery, so
+ // "x01" and "101" are the same flow
+ const key = m.stress.replace(/x/g, '1');
+ (byPat[key] ||= []).push(m.l);
});
let fid = 0;
Object.keys(byPat).sort().forEach(pat=>{
@@ -1042,13 +1054,24 @@ const tab = (name)=>{
};
document.querySelectorAll('.tab').forEach(t=> t.addEventListener('click', ()=>tab(t.dataset.tab)));
+// bass: [step, semitone offset, length in 16ths]
const PATTERNS = {
- 'Boom Bap': {bpm:90, kick:[0,6,8], snare:[4,12], hat:[0,2,4,6,8,10,12,14], swing:0.12},
- 'Trap': {bpm:140, kick:[0,3,7,8,11], snare:[8], hat:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], swing:0},
- 'Lo-Fi': {bpm:75, kick:[0,8], snare:[4,12], hat:[2,6,10,14], swing:0.18},
- 'Drill': {bpm:142, kick:[0,3,6,10,11], snare:[4,12,13], hat:[0,2,4,6,8,10,12,14], swing:0.05},
- 'Spoken/Jazz':{bpm:96, kick:[0,10], snare:[4,12], hat:[0,3,6,9,12,15], swing:0.2},
- 'West Coast': {bpm:94, kick:[0,6,8,14], snare:[4,12], hat:[0,2,4,6,8,10,12,14], swing:0.08}
+ 'Boom Bap': {bpm:90, kick:[0,6,8], snare:[4,12], hat:[0,2,4,6,8,10,12,14], swing:0.12,
+ bass:[[0,0,2],[6,0,1],[8,0,2],[14,3,1]]},
+ 'Trap': {bpm:140, kick:[0,3,7,8,11], snare:[8], hat:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], swing:0,
+ clap:true, bass:[[0,0,3],[7,-2,1],[8,0,3],[11,3,2]]},
+ 'Lo-Fi': {bpm:75, kick:[0,8], snare:[4,12], hat:[2,6,10,14], swing:0.18,
+ ohat:[10], bass:[[0,0,3],[8,5,3],[12,3,2]]},
+ 'Drill': {bpm:142, kick:[0,3,6,10,11], snare:[4,12,13], hat:[0,2,4,6,8,10,12,14], swing:0.05,
+ bass:[[0,0,2],[3,1,2],[6,-2,2],[10,0,1],[11,3,2]]},
+ 'Spoken/Jazz':{bpm:96, kick:[0,10], snare:[4,12], hat:[0,3,6,9,12,15], swing:0.2,
+ ohat:[14], bass:[[0,0,2],[4,7,2],[8,5,2],[12,3,2]]},
+ 'West Coast': {bpm:94, kick:[0,6,8,14], snare:[4,12], hat:[0,2,4,6,8,10,12,14], swing:0.08,
+ bass:[[0,0,2],[6,0,1],[8,-4,2],[14,0,1]]},
+ 'Halftime': {bpm:74, kick:[0,10], snare:[8], hat:[0,2,4,6,8,10,12,14], swing:0.1,
+ clap:true, ohat:[6], bass:[[0,0,6],[10,-2,4]]},
+ 'Dembow': {bpm:96, kick:[0,4,8,12], snare:[3,6,11,14], hat:[0,2,4,6,8,10,12,14], swing:0,
+ bass:[[0,0,3],[6,0,1],[8,0,3],[14,0,1]]}
};
let actx=null, masterGain=null, playing=false, schedTimer=null, current='Boom Bap', step=0, nextTime=0;
@@ -1108,9 +1131,43 @@ function hat(t){
const d=buf.getChannelData(0); for(let i=0;i{ if(bs === s) bass(t, semi, len, secPer16); });
+ if(s % 4 === 0){
+ const beat = s / 4;
+ setTimeout(()=>flashBeat(beat), Math.max(0, (t - actx.currentTime) * 1000));
+ }
nextTime += secPer16;
step++;
}
schedTimer = setTimeout(scheduler, 25);
}
+const beatInd = document.getElementById('beatInd');
+function flashBeat(n){
+ if(!playing) return;
+ [...beatInd.children].forEach((d, i)=> d.classList.toggle('on', i === n));
+}
+const taps = [];
+document.getElementById('tapBtn').addEventListener('click', ()=>{
+ const now = performance.now();
+ if(taps.length && now - taps[taps.length - 1] > 2000) taps.length = 0;
+ taps.push(now);
+ if(taps.length >= 2){
+ const iv = (taps[taps.length - 1] - taps[0]) / (taps.length - 1);
+ setTempo(Math.max(60, Math.min(180, Math.round(60000 / iv))));
+ }
+});
+
function startBeat(){
ensureCtx(); if(actx.state==='suspended') actx.resume();
if(playing) return;
@@ -1136,6 +1215,7 @@ function startBeat(){
}
function stopBeat(){
playing=false; clearTimeout(schedTimer);
+ [...beatInd.children].forEach(d=>d.classList.remove('on'));
document.querySelectorAll('.beat').forEach(b=>b.classList.remove('playing'));
document.getElementById('playBeat').textContent='Play';
}