Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Created February 16, 2026 23:32
Show Gist options
  • Select an option

  • Save anon987654321/81f018e537fec64f40649dc91c91c1c5 to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/81f018e537fec64f40649dc91c91c1c5 to your computer and use it in GitHub Desktop.
rg69.html: <!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="description" content="RG-69 browser beat machine — boom-bap, dub, industrial techno, ethio-jazz, afrobeat, trap, bossa nova. Layered synthesis, micro-timing, lo-fi processing.">
<meta property="og:title" content="RG-69 Beat Machine">
<meta property="og:description" content="Browser beat machine with layered synthesis, micro-timing, and lo-fi processing. 13 genres, 30+ drum patterns.">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>RG-69 Beat Machine</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#141414;--sf:#1c1c1c;--sf2:#222;--br:#2e2e2e;
--fg:#ccc;--dim:#777;--mid:#999;
--ac:#c17f3e;--ac2:#e8a84c;--active:#a06828;
--red:#b44;--mono:'IBM Plex Mono',monospace;
--hit:#3a3028;--on:#c17f3e;--play:#3a362e;
}
[data-theme="light"]{
--bg:#edebe6;--sf:#f7f5f0;--sf2:#fff;--br:#d0ccc4;
--fg:#1a1a1a;--dim:#8a8580;--mid:#6b6560;
--ac:#9e6b2e;--ac2:#b8812e;--active:#7a5220;
--red:#a33;
--hit:#d8cfc0;--on:#9e6b2e;--play:#d4cbb8;
}
html{font-size:13px}
body{font-family:var(--mono);background:var(--bg);color:var(--fg);overflow-x:hidden;-webkit-user-select:none;user-select:none}
/* ── FOCUS ── */
:focus-visible{outline:2px solid var(--ac);outline-offset:1px}
button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--ac);outline-offset:1px}
/* ── WELCOME ── */
.welcome{position:fixed;inset:0;z-index:300;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;transition:opacity .4s}
.welcome.gone{opacity:0;pointer-events:none}
.welcome h1{font-size:24px;font-weight:300;letter-spacing:.3em;color:var(--fg)}
.welcome p{color:var(--mid);font-size:11px;max-width:280px;text-align:center;line-height:1.6}
.welcome-start{padding:16px 44px;font-size:14px;font-weight:600;letter-spacing:.18em;background:var(--ac);color:var(--bg);border:none;cursor:pointer;font-family:var(--mono);min-height:48px}
.welcome-sub{font-size:9px;color:var(--dim)}
/* ── SHELL ── */
.app{max-width:880px;margin:0 auto;padding:10px 14px 100px;opacity:0;transition:opacity .5s}
.app.visible{opacity:1}
/* ── HEADER ── */
header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--br);padding-bottom:8px;margin-bottom:10px}
.logo{font-weight:300;font-size:14px;letter-spacing:.2em}
.status{font-size:9px;color:var(--dim);display:flex;align-items:center;gap:5px}
.dot{width:5px;height:5px;border-radius:50%;background:var(--dim)}
.dot.live{background:var(--ac)}
header nav{display:flex;gap:4px}
header nav button{background:none;border:1px solid var(--br);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 7px;cursor:pointer;text-transform:uppercase;min-height:28px}
header nav button:hover{color:var(--fg);border-color:var(--mid)}
/* ── VIZ ── */
#viz{width:100%;height:40px;display:block;background:var(--sf);border:1px solid var(--br);margin-bottom:8px}
/* ── TRANSPORT ── */
.transport{display:flex;gap:4px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
.btn{
min-height:36px;min-width:36px;padding:6px 10px;border:1px solid var(--br);background:transparent;
color:var(--fg);font-family:var(--mono);font-size:10px;font-weight:500;
letter-spacing:.05em;text-transform:uppercase;cursor:pointer;display:flex;align-items:center;justify-content:center;
}
.btn:hover{background:var(--sf2);border-color:var(--mid)}
.btn:active{background:var(--br)}
.btn-random{background:var(--ac);color:var(--bg);border-color:var(--ac);font-size:11px;font-weight:600;letter-spacing:.1em;padding:8px 16px}
.btn-random:hover{background:var(--ac2);border-color:var(--ac2)}
.btn-play.on{background:var(--active);color:var(--bg);border-color:var(--active)}
.btn-rec.on{background:var(--red);color:#fff;border-color:var(--red)}
.sep{width:1px;height:22px;background:var(--br);margin:0 1px}
.ctrl{display:flex;align-items:center;gap:3px}
.ctrl span{font-size:9px;color:var(--dim);text-transform:uppercase}
.ctrl .val{color:var(--fg);min-width:20px;font-weight:500;font-size:10px}
.ctrl input[type=range]{width:48px}
.ctrl select{width:auto;padding:2px 4px;background:var(--sf);color:var(--fg);border:1px solid var(--br);font-family:var(--mono);font-size:10px}
/* ── GRID ── */
main{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:10px}
/* ── SECTIONS ── */
section{margin-bottom:12px}
.sec-title{
font-size:10px;font-weight:500;color:var(--dim);letter-spacing:.08em;text-transform:uppercase;
border-bottom:1px solid var(--br);padding-bottom:3px;margin-bottom:8px;cursor:pointer;
display:flex;justify-content:space-between;align-items:center;
}
.sec-title::after{content:'−';font-size:10px;color:var(--dim)}
section.closed .sec-title::after{content:'+'}
section.closed .sec-body{display:none}
/* ── MIXER ── */
.track-row{display:flex;align-items:center;gap:4px;margin-bottom:4px;padding:3px 0}
.track-row .name{font-size:9px;color:var(--dim);text-transform:uppercase;width:40px;flex-shrink:0;letter-spacing:.04em}
.track-row input[type=range]{flex:1;min-width:0}
.track-row .vval{font-size:9px;color:var(--mid);width:22px;text-align:right;flex-shrink:0}
.track-btn{
width:24px;height:24px;border:1px solid var(--br);background:transparent;
font-family:var(--mono);font-size:7px;color:var(--dim);cursor:pointer;
display:flex;align-items:center;justify-content:center;text-transform:uppercase;flex-shrink:0;
}
.track-btn:hover{border-color:var(--mid);color:var(--fg)}
.track-btn.muted{background:var(--red);color:#fff;border-color:var(--red)}
.track-btn.soloed{background:var(--ac);color:var(--bg);border-color:var(--ac)}
/* ── SEQUENCER ── */
.seq-track{margin-bottom:5px}
.seq-head{display:flex;align-items:center;gap:4px;margin-bottom:2px}
.seq-head .name{font-size:8px;color:var(--dim);text-transform:uppercase;width:32px;letter-spacing:.04em}
.beat-markers{display:grid;grid-template-columns:repeat(16,1fr);gap:0;margin-bottom:2px;padding-left:36px}
.beat-markers span{font-size:8px;text-align:center;color:var(--dim)}
.beat-markers span.bar{color:var(--mid);font-weight:500}
.seq-steps{display:grid;grid-template-columns:repeat(16,1fr);gap:0}
.seq-step{
height:20px;background:var(--sf);border:1px solid var(--br);cursor:pointer;
border-left:none;position:relative;
}
.seq-step:first-child{border-left:1px solid var(--br)}
.seq-step:nth-child(4n+1){border-left:2px solid var(--mid)}
/* Velocity-mapped hits: opacity set inline via JS */
.seq-step.hit{background:var(--hit)}
/* Custom hits: amber + inner triangle marker for colorblind distinction */
.seq-step.on{background:var(--on)}
.seq-step.on::after{content:'';position:absolute;top:2px;right:2px;width:0;height:0;border-left:4px solid transparent;border-top:4px solid var(--bg)}
.seq-step.playing{background:var(--play)}
/* ── SELECTS + SLIDERS ── */
.sel-row{margin-bottom:5px}
.sel-row label{display:block;font-size:9px;color:var(--dim);margin-bottom:2px;text-transform:uppercase;letter-spacing:.04em}
select{width:100%;padding:5px;background:var(--sf);color:var(--fg);border:1px solid var(--br);font-family:var(--mono);font-size:10px;cursor:pointer;min-height:28px}
.slider-row{margin-bottom:5px}
.slider-label{display:flex;justify-content:space-between;font-size:9px;color:var(--dim);margin-bottom:2px}
.slider-label span:last-child{color:var(--mid);font-weight:500}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:2px;background:var(--br);border:none;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--ac);border-radius:0;cursor:pointer}
input[type=range]::-moz-range-thumb{width:14px;height:14px;background:var(--ac);border:none;border-radius:0;cursor:pointer}
@media(pointer:coarse){
input[type=range]::-webkit-slider-thumb{width:24px;height:24px}
input[type=range]::-moz-range-thumb{width:24px;height:24px}
.seq-step{height:28px}
.track-btn{width:32px;height:32px;font-size:9px}
}
/* ── STYLE GRID ── */
.mood-row{display:flex;gap:3px;flex-wrap:wrap}
.mood-btn{
padding:5px 9px;background:transparent;border:1px solid var(--br);
font-family:var(--mono);font-size:9px;cursor:pointer;text-transform:uppercase;
letter-spacing:.04em;color:var(--dim);min-height:28px;
}
.mood-btn:hover{border-color:var(--mid);color:var(--fg)}
.mood-btn.active{background:var(--ac);color:var(--bg);border-color:var(--ac)}
/* ── EXPORT BAR ── */
.export-bar{display:none;position:fixed;left:0;right:0;z-index:150;background:var(--sf2);border-top:2px solid var(--ac);padding:10px 16px;align-items:center;gap:8px;flex-wrap:wrap;bottom:0}
.export-bar.show{display:flex}
.export-bar .msg{font-size:10px;color:var(--mid);flex:1}
.export-bar .prog{width:80px;height:3px;background:var(--br);overflow:hidden;flex-shrink:0}
.export-bar .prog-fill{height:100%;background:var(--ac);width:0;transition:width .3s}
/* ── LOG ── */
#log{font-size:8px;color:var(--dim);max-height:28px;overflow-y:auto;margin-top:6px;line-height:1.4}
/* ── MODAL ── */
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200}
.modal-bg.show{display:flex;align-items:center;justify-content:center}
.modal{background:var(--sf2);border:1px solid var(--br);padding:20px;max-width:420px;width:90%;max-height:80vh;overflow-y:auto;font-size:11px;line-height:1.7}
.modal h2{font-size:12px;color:var(--fg);margin-bottom:12px;letter-spacing:.08em;font-weight:500}
.modal p{margin-bottom:10px;color:var(--mid)}
.modal kbd{background:var(--br);padding:1px 5px;font-size:10px}
/* ── RESPONSIVE ── */
@media(max-width:640px){
main{grid-template-columns:1fr}
.transport{position:fixed;bottom:0;left:0;right:0;z-index:100;background:var(--bg);border-top:1px solid var(--br);padding:8px 10px;padding-bottom:calc(8px + env(safe-area-inset-bottom));flex-wrap:nowrap;overflow-x:auto;gap:3px}
.app{padding-bottom:110px}
.sep{display:none}
.export-bar{bottom:60px;bottom:calc(60px + env(safe-area-inset-bottom))}
}
</style>
</head>
<body>
<div class="welcome" id="welcome" role="dialog" aria-label="Welcome">
<h1>RG-69</h1>
<p>Beat machine. Boom-bap, dub, industrial, ethio-jazz, afrobeat, trap, bossa, ambient.</p>
<button class="welcome-start" id="welcomeStart" aria-label="Start">START</button>
<span class="welcome-sub">Headphones recommended</span>
</div>
<div class="app" id="app" role="application">
<header>
<span class="logo">RG-69</span>
<div class="status"><span class="dot" id="dot"></span><span id="statusTxt">Ready</span></div>
<nav aria-label="Settings">
<button id="themeBtn" aria-label="Toggle theme">Theme</button>
<button id="helpBtn" aria-label="Help">?</button>
</nav>
</header>
<canvas id="viz" aria-label="Waveform" role="img"></canvas>
<div class="transport" role="toolbar" aria-label="Transport">
<button class="btn btn-random" id="randomBtn" aria-label="Random beat">Random</button>
<button class="btn btn-play" id="playBtn" aria-label="Play">▶ Play</button>
<button class="btn" id="stopBtn" aria-label="Stop">■</button>
<div class="sep"></div>
<div class="ctrl"><span>BPM</span><input type="range" id="bpm" min="50" max="180" value="88" aria-label="Tempo"><span class="val" id="bpmV">88</span></div>
<div class="ctrl"><span>Swing</span><input type="range" id="swing" min="0" max="100" value="30" aria-label="Swing"><span class="val" id="swingV">30</span></div>
<div class="ctrl"><span>Key</span><select id="rootSel" aria-label="Key"><option value="0">C</option><option value="1">C♯</option><option value="2">D</option><option value="3">D♯</option><option value="4">E</option><option value="5">F</option><option value="6">F♯</option><option value="7">G</option><option value="8">G♯</option><option value="9">A</option><option value="10">A♯</option><option value="11">B</option></select></div>
<div class="sep"></div>
<button class="btn" id="tapBtn" aria-label="Tap tempo">Tap</button>
<button class="btn" id="saveBtn" aria-label="Save">Save</button>
<button class="btn" id="undoBtn" aria-label="Undo">↶</button>
<button class="btn" id="redoBtn" aria-label="Redo">↷</button>
</div>
<main>
<div>
<section aria-label="Style presets">
<div class="sec-title">Style — click to play</div>
<div class="sec-body"><div class="mood-row" id="moodRow"></div></div>
</section>
<section aria-label="Patterns">
<div class="sec-title">Patterns</div>
<div class="sec-body">
<div class="sel-row"><label for="sel-drums">Drums</label><select id="sel-drums"></select></div>
<div class="sel-row"><label for="sel-bass">Bass</label><select id="sel-bass"></select></div>
<div class="sel-row"><label for="sel-keys">Keys</label><select id="sel-keys"></select></div>
<div class="sel-row"><label for="sel-pads">Pads</label><select id="sel-pads"></select></div>
</div>
</section>
<section aria-label="Step sequencer">
<div class="sec-title">Step Editor</div>
<div class="sec-body" id="seqBody"></div>
</section>
</div>
<div>
<section aria-label="Mixer">
<div class="sec-title">Mixer</div>
<div class="sec-body" id="mixerBody"></div>
</section>
<section class="closed" aria-label="Sound shaping">
<div class="sec-title">Sound Shaping ··· 10 controls inside</div>
<div class="sec-body">
<div class="slider-row"><div class="slider-label"><span>Groove · Kick Feel</span><span id="vKL">40ms</span></div><input type="range" id="sKL" min="0" max="80" value="40" aria-label="How late the kick hits — adds weight"></div>
<div class="slider-row"><div class="slider-label"><span>Groove · Snare Push</span><span id="vSR">25ms</span></div><input type="range" id="sSR" min="0" max="60" value="25" aria-label="How early the snare hits — adds urgency"></div>
<div class="slider-row"><div class="slider-label"><span>Groove · Hat Swing</span><span id="vHD">20ms</span></div><input type="range" id="sHD" min="0" max="50" value="20" aria-label="How much hi-hats drift off-grid"></div>
<div class="slider-row"><div class="slider-label"><span>Dirt</span><span id="vDR">20</span></div><input type="range" id="sDR" min="0" max="100" value="20" aria-label="Drum distortion/saturation"></div>
<div class="slider-row"><div class="slider-label"><span>Room</span><span id="vRV">25</span></div><input type="range" id="sRV" min="0" max="100" value="25" aria-label="Reverb size"></div>
<div class="slider-row"><div class="slider-label"><span>Crush</span><span id="vCR">0</span></div><input type="range" id="sCR" min="0" max="100" value="0" aria-label="Bit reduction — lo-fi digital"></div>
<div class="slider-row"><div class="slider-label"><span>Bass Glide</span><span id="vGL">50ms</span></div><input type="range" id="sGL" min="0" max="200" value="50" aria-label="How bass slides between notes"></div>
<div class="slider-row"><div class="slider-label"><span>Vinyl Noise</span><span id="vVN">20</span></div><input type="range" id="sVN" min="0" max="100" value="20" aria-label="Background record hiss"></div>
<div class="slider-row"><div class="slider-label"><span>Crackle</span><span id="vCK">18</span></div><input type="range" id="sCK" min="0" max="100" value="18" aria-label="Record surface pops"></div>
<div class="slider-row"><div class="slider-label"><span>Tape Wobble</span><span id="vFL">20</span></div><input type="range" id="sFL" min="0" max="100" value="20" aria-label="Tape wow and flutter"></div>
</div>
</section>
<div id="log" aria-live="polite"></div>
</div>
</main>
</div>
<div class="export-bar" id="exportBar" role="status">
<span class="msg" id="exportMsg">Choose format:</span>
<div class="prog"><div class="prog-fill" id="exportFill"></div></div>
<button class="btn" id="expWav">WAV</button>
<button class="btn btn-rec" id="expRec">⏺ Record</button>
<button class="btn" id="expClose">✕</button>
</div>
<div class="modal-bg" id="helpModal" role="dialog" aria-label="Help">
<div class="modal">
<h2>RG-69</h2>
<p><b>Start:</b> Click a <b>Style</b> — music plays. Click <b>Random</b> for surprises.</p>
<p><b>Mixer:</b> Volume fader + <b>M</b> (mute) + <b>S</b> (solo) per track.</p>
<p><b>Step Editor:</b> Click to add hits. Amber with corner mark = your additions. Dim = pattern hits. Brightness = velocity.</p>
<p><b>Sound Shaping:</b> Groove controls (per-instrument micro-timing), Dirt (saturation), Room (reverb), Crush (bit reduction), Bass Glide (portamento), Vinyl/Crackle/Tape (lo-fi character).</p>
<p><b>Save:</b> WAV renders 4 bars. ⏺ captures live.</p>
<p><b>Keys:</b> <kbd>Space</kbd> Play · <kbd>R</kbd> Random · <kbd>T</kbd> Tap · <kbd>L</kbd> Theme · <kbd>Ctrl+Z</kbd> Undo</p>
<button class="btn" onclick="document.getElementById('helpModal').classList.remove('show')">Close</button>
</div>
</div>
<script>
// =========================================================================
// RG-69 v5 — All crit2 fixes applied
// Polyphony: releaseAll + maxPoly:16
// Schedule: gap-free rebuild (build new → store → clear old)
// Velocity: step opacity mapped to hit velocity
// A11y: focus-visible, keyboard seq toggle, touch targets
// Naming: Dirt/Room/Crush/Tape Wobble
// =========================================================================
const $=id=>document.getElementById(id);
const log=m=>{const e=$('log');if(e){e.innerHTML+='<div>'+m+'</div>';e.scrollTop=e.scrollHeight;}};
let playing=false,audioReady=false,currentStep=-1;
let kickLag=.04,snareRush=-.025,hatDrift=.02,rootOffset=0;
let hist=[],histIdx=-1,tapTimes=[];
const customSeq={kick:Array(16).fill(0),snare:Array(16).fill(0),clap:Array(16).fill(0),hat:Array(16).fill(0),open:Array(16).fill(0)};
const trackState={drums:{mute:false,solo:false},bass:{mute:false,solo:false},keys:{mute:false,solo:false},pads:{mute:false,solo:false}};
function tp(n,s){if(!n||s===0)return n;try{return Tone.Frequency(n).transpose(s).toNote();}catch(e){return n;}}
function tpPat(p,s){if(!p||s===0)return p;return p.map(x=>!x?null:Array.isArray(x)?x.map(n=>tp(n,s)):tp(x,s));}
function isActive(inst){const any=Object.values(trackState).some(t=>t.solo);if(any)return trackState[inst].solo;return !trackState[inst].mute;}
// ══════════════════════════════════════════════════════════════════
// PATTERNS
// ══════════════════════════════════════════════════════════════════
const DRUMS={
'bb-wonky':{bpm:[82,95],style:'boombap',k:[.9,0,0,0,0,0,.7,0,0,0,.3,0,.8,0,0,.5],s:[0,0,0,0,.9,0,0,.2,0,0,0,0,.9,0,0,.3],c:[0,0,0,0,.6,0,0,0,0,0,0,0,.6,0,0,0],h:[.7,.4,.6,.3,.7,.3,.5,.4,.7,.4,.6,.3,.7,.3,.5,.6],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.4]},
'bb-dusty':{bpm:[85,92],style:'boombap',k:[.9,0,.3,0,0,0,0,0,.8,0,0,.5,0,0,.6,0],s:[0,0,0,0,.9,0,0,0,0,0,.2,0,.9,0,0,.3],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.6,.5,.6,.4,.6,.5,.6,.4,.6,.5,.6,.4,.6,.5,.6,.5],o:[0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0,0]},
'bb-laid':{bpm:[88,93],style:'boombap',k:[.9,0,0,0,0,0,0,0,.8,0,0,0,.7,0,0,0],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.8,0,0,0],c:[0,0,0,0,.4,0,0,0,0,0,0,0,.4,0,0,0],h:[.7,.4,.6,.4,.7,.4,.6,.4,.7,.4,.6,.4,.7,.4,.6,.4],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.3]},
'bb-ghost':{bpm:[84,90],style:'boombap',k:[.9,0,0,0,0,.4,0,0,.8,0,0,0,0,0,.6,0],s:[0,0,.2,0,.9,0,.2,.3,0,0,.2,0,.9,0,.3,0],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.6,.3,.5,.3,.6,.3,.5,.3,.6,.3,.5,.3,.6,.3,.5,.4],o:[0,0,0,0,0,0,0,.4,0,0,0,0,0,0,0,0]},
'bb-erratic':{bpm:[80,88],style:'boombap',k:[.9,0,.4,.6,0,.3,0,.5,.7,0,.3,0,.6,.4,0,.3],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,0,0,.6,0,0,0,0,0,0,0,.5,0,0,0],h:[.5,.4,.5,.3,.5,.4,.5,.3,.5,.4,.5,.3,.5,.4,.5,.4],o:[0,0,0,0,0,0,0,0,0,0,0,.3,0,0,0,0]},
'bb-shuffle':{bpm:[86,96],style:'boombap',k:[.9,0,0,.4,0,0,.7,0,0,.3,0,0,.8,0,0,.3],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.7,0,.5,0,.7,0,.5,0,.7,0,.5,0,.7,0,.5,0],o:[0,0,0,.3,0,0,0,.3,0,0,0,.3,0,0,0,.3]},
'bb-halftime':{bpm:[70,82],style:'boombap',k:[.9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],s:[0,0,0,0,0,0,0,0,.9,0,0,0,0,0,0,0],c:[0,0,0,0,0,0,0,0,.5,0,0,0,0,0,0,0],h:[.6,0,.4,0,.6,0,.4,0,.6,0,.4,0,.6,0,.4,0],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.3]},
'bb-sparse':{bpm:[78,88],style:'boombap',k:[.9,0,0,0,0,0,0,0,0,0,0,0,.7,0,0,0],s:[0,0,0,0,.8,0,0,0,0,0,0,0,0,0,0,0],c:[0,0,0,0,.4,0,0,0,0,0,0,0,0,0,0,0],h:[.5,0,0,0,.5,0,0,0,.5,0,0,0,.5,0,0,0],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
'bb-detroit':{bpm:[84,92],style:'boombap',k:[.9,0,0,.3,0,0,0,0,.8,0,0,0,.6,0,.4,0],s:[0,0,0,0,.9,0,0,.2,0,0,0,0,.9,0,0,.2],c:[0,0,0,0,.6,0,0,0,0,0,0,0,.6,0,0,0],h:[.7,.3,.6,.3,.7,.3,.6,.4,.7,.3,.6,.3,.7,.3,.6,.4],o:[0,0,0,0,0,0,0,.4,0,0,0,0,0,0,0,0]},
'ind-4floor':{bpm:[132,140],style:'industrial',k:[.95,0,0,0,.95,0,0,0,.95,0,0,0,.95,0,0,0],s:[0,0,0,0,.95,0,0,0,0,0,0,0,.95,0,0,0],c:[0,0,0,0,.8,0,0,0,0,0,0,0,.8,0,0,0],h:[.8,.5,.8,.5,.8,.5,.8,.5,.8,.5,.8,.5,.8,.5,.8,.5],o:[0,0,0,0,0,0,0,.6,0,0,0,0,0,0,0,.6]},
'ind-offbeat':{bpm:[128,138],style:'industrial',k:[.95,0,0,0,.95,0,0,0,.95,0,0,0,.95,0,0,0],s:[0,0,.7,0,0,0,.7,0,0,0,.7,0,0,0,.7,0],c:[0,0,0,0,.7,0,0,0,0,0,0,0,.7,0,0,0],h:[0,.7,0,.7,0,.7,0,.7,0,.7,0,.7,0,.7,0,.7],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.5]},
'ind-breakbeat':{bpm:[130,142],style:'industrial',k:[.95,0,0,.7,0,0,.8,0,0,0,.95,0,0,.6,0,0],s:[0,0,0,0,.95,0,0,0,0,.5,0,0,.95,0,0,0],c:[0,0,0,0,.7,0,0,0,0,0,0,0,.7,0,0,0],h:[.8,.4,.6,.4,.8,.4,.6,.4,.8,.4,.6,.4,.8,.4,.6,.4],o:[0,0,0,0,0,0,0,.5,0,0,0,0,0,0,0,.5]},
'ind-relentless':{bpm:[140,155],style:'industrial',k:[.95,0,.6,0,.95,0,.6,0,.95,0,.6,0,.95,0,.6,0],s:[0,0,0,0,.95,0,0,.4,0,0,0,0,.95,0,0,.4],c:[0,0,0,0,.8,0,0,0,0,0,0,0,.8,0,0,0],h:[.9,.6,.8,.6,.9,.6,.8,.6,.9,.6,.8,.6,.9,.6,.8,.6],o:[0,0,0,0,0,0,0,.7,0,0,0,0,0,0,0,.7]},
'ind-stomp':{bpm:[135,148],style:'industrial',k:[.95,.5,0,.5,.95,.5,0,.5,.95,.5,0,.5,.95,.5,0,.5],s:[0,0,0,0,.95,0,0,0,0,0,0,0,.95,0,0,0],c:[0,0,.6,0,0,0,.6,0,0,0,.6,0,0,0,.6,0],h:[.8,.3,.8,.3,.8,.3,.8,.3,.8,.3,.8,.3,.8,.3,.8,.3],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.6]},
'dub-onedrop':{bpm:[68,78],style:'dub',k:[0,0,0,0,0,0,0,0,.9,0,0,0,0,0,0,0],s:[0,0,0,0,0,0,0,0,.9,0,0,0,0,0,0,0],c:[0,0,0,0,0,0,0,0,.5,0,0,0,0,0,0,0],h:[.6,0,.5,0,.6,0,.5,0,.6,0,.5,0,.6,0,.5,0],o:[0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0,.3]},
'dub-steppers':{bpm:[70,80],style:'dub',k:[.9,0,0,0,.9,0,0,0,.9,0,0,0,.9,0,0,0],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.6,0,.5,0,.6,0,.5,0,.6,0,.5,0,.6,0,.5,0],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.4]},
'dub-digital':{bpm:[78,88],style:'dub',k:[.9,0,0,.4,0,0,0,0,.8,0,0,0,.5,0,0,0],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,.3],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.7,.3,.5,.3,.7,.3,.5,.3,.7,.3,.5,.3,.7,.3,.5,.3],o:[0,0,0,0,0,0,0,.4,0,0,0,0,0,0,0,0]},
'eth-6-8':{bpm:[90,110],style:'ethio',k:[.9,0,0,.6,0,0,.8,0,0,.5,0,0,.7,0,0,0],s:[0,0,0,0,0,0,0,0,0,0,0,.9,0,0,0,0],c:[0,0,0,0,0,0,0,0,0,0,0,.5,0,0,0,0],h:[.6,.4,.5,.6,.4,.5,.6,.4,.5,.6,.4,.5,.5,.3,.4,.3],o:[0,0,0,0,0,.3,0,0,0,0,0,0,0,0,0,0]},
'eth-addis':{bpm:[92,105],style:'ethio',k:[.9,0,.4,0,0,0,.7,0,0,.4,0,0,.8,0,0,0],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3],o:[0,0,0,0,0,0,0,0,0,0,0,.3,0,0,0,0]},
'eth-tizita':{bpm:[88,100],style:'ethio',k:[.9,0,0,0,.5,0,.8,0,0,.3,0,0,.7,0,.4,0],s:[0,0,0,0,.9,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,0,0,.4,0,0,0,0,0,0,0,.4,0,0,0],h:[.6,.3,.5,.3,.6,.3,.5,.3,.6,.3,.5,.3,.6,.3,.5,.3],o:[0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0,.3]},
'afro-tony':{bpm:[105,125],style:'afro',k:[1,0,0,.5,0,0,1,0,0,.7,0,0,1,0,.4,0],s:[0,0,0,0,0,0,0,0,0,0,0,0,.9,0,0,0],c:[0,0,.4,0,0,0,0,0,.4,0,0,0,0,0,.4,0],h:[.7,.4,.6,.4,.7,.4,.6,.4,.7,.4,.6,.4,.7,.4,.6,.4],o:[0,0,0,0,0,0,0,.5,0,0,0,0,0,0,0,.5]},
'afro-highlife':{bpm:[100,120],style:'afro',k:[1,0,0,.3,0,0,.6,0,0,1,0,0,.4,0,.5,0],s:[0,0,0,0,.7,0,0,0,0,0,0,0,.8,0,0,.3],c:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],h:[.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3],o:[0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0,.4]},
'bossa-clave':{bpm:[120,140],style:'bossa',k:[1,0,0,.4,0,0,.6,0,0,0,1,0,.4,0,0,0],s:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],c:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],h:[.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3,.3],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
'trap-basic':{bpm:[130,150],style:'trap',k:[1,0,0,0,0,0,0,0,1,0,.5,.5,0,0,0,0],s:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],c:[0,0,0,0,.6,0,0,0,0,0,0,0,.6,0,0,0],h:[1,.5,1,.5,1,.5,1,.5,1,.5,1,.5,1,.5,1,.5],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
'trap-bounce':{bpm:[130,145],style:'trap',k:[1,0,0,.6,0,0,1,0,0,.4,0,0,1,0,.5,0],s:[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0],c:[0,0,0,0,.5,0,0,0,0,0,0,0,.5,0,0,0],h:[1,.7,1,.5,1,.7,1,.5,1,.7,1,.5,1,.7,1,.5],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
'broken-classic':{bpm:[115,135],style:'broken',k:[1,0,0,0,0,0,.7,0,0,0,1,0,0,.5,0,0],s:[0,0,0,0,1,0,0,0,0,0,0,0,.8,0,0,.4],c:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],h:[.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3,.5,.3],o:[0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0,.4]},
'ambient-pulse':{bpm:[60,80],style:'ambient',k:[.4,0,0,0,0,0,0,0,.3,0,0,0,0,0,0,0],s:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],c:[.2,0,0,0,0,0,0,0,0,0,0,0,0,0,.15,0],h:[0,0,0,0,0,0,0,.15,0,0,0,0,0,0,0,.1],o:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
};
const BASS={root5:['C2',null,null,null,'G2',null,null,null,'C2',null,null,null,'Eb2',null,'G2',null],walking:['C2',null,'Eb2',null,'F2',null,'G2',null,'Ab2',null,'G2',null,'F2',null,'Eb2',null],slide:['C2',null,null,'C2',null,null,'Eb2',null,'F2',null,null,'F2',null,null,'Eb2',null],octave:['C2',null,'C3',null,'C2',null,'C3',null,'Eb2',null,'Eb3',null,'F2',null,'G2',null],synco:[null,'C2',null,null,'Eb2',null,null,'F2',null,'G2',null,null,'Eb2',null,null,null],pedal:['C2',null,'C2',null,'C2',null,'C2',null,'C2',null,'C2',null,'C2',null,'C2',null],chromatic:['C2',null,null,'B1','C2',null,null,'Db2','Eb2',null,null,'D2','Eb2',null,'F2',null],gospel:['C2','Eb2','F2','G2','Ab2','G2','F2','Eb2','F2','Ab2','Bb2','C3','Bb2','Ab2','G2','F2'],acid:['C2',null,'C2','C3',null,'C2',null,'Eb2','C2',null,'C2','C3',null,'G2',null,'C2'],drone:['C1',null,null,null,null,null,null,null,'C1',null,null,null,null,null,null,null],pulse:['C2','C2',null,'C2','C2',null,'C2','C2',null,'C2','C2',null,'C2',null,'C2','C2'],dubheavy:['C2',null,null,null,null,null,null,null,'Eb2',null,null,null,null,'G2',null,null],dubroots:['C2',null,null,'G2',null,null,'Eb2',null,null,'F2',null,null,'C2',null,null,null],ethio:['C2',null,'F2',null,'G2',null,null,'C2',null,'Db2',null,'F2','G2',null,'C2',null],callresp:['C2','C2',null,null,'G2','G2',null,null,'Ab2',null,'G2',null,'F2',null,null,null],dorian:['C2',null,'D2',null,'Eb2',null,'F2',null,'G2',null,'F2',null,'Eb2',null,'D2',null],afro:['C2',null,'G2',null,'Eb2',null,'F2',null,'C2',null,'G2',null,'Eb2',null,'F2',null],bossa:['C2',null,'G2',null,'E2',null,'A2',null,'D2',null,'G2',null,'C2',null,null,null],trap:['C1',null,null,null,null,null,null,null,'C1',null,'C1','C1',null,null,null,null],ambient:['C1',null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],broken:['C2',null,'G2',null,null,null,'Eb2',null,'F2',null,null,null,'C2',null,'G2',null]};
const KEYS={min11:[['C3','Eb3','G3','Bb3','D4'],null,['F3','Ab3','C4','Eb4'],null,['Ab3','C4','Eb4','G4'],null,['G3','Bb3','D4','F4'],null],'9ths':[['C3','Eb3','Bb3','D4'],['F3','Ab3','Eb4','G4'],['Eb3','G3','Bb3','F4'],['Ab3','C4','G4','Bb4']],sus4:[['C3','F3','G3','Bb3'],null,['F3','Bb3','C4','Eb4'],null,['Eb3','Ab3','Bb3','D4'],null,['G3','C4','D4','F4'],null],blues:[['C3','Eb3','Gb3','Bb3'],null,['F3','Ab3','B3','Eb4'],null,['G3','Bb3','Db4','F4'],null,['C3','Eb3','G3','Bb3'],null],cluster:[['C3','D3','Eb3','G3','Bb3'],null,['F3','G3','Ab3','C4'],null,['Eb3','F3','G3','Bb3'],null,['Ab3','Bb3','C4','Eb4'],null],quartal:[['C3','F3','Bb3','Eb4'],null,['G3','C4','F4','Bb4'],null,['D3','G3','C4','F4'],null,['A3','D4','G4','C5'],null],drop2:[['C3','G3','Bb3','Eb4'],null,['F3','C4','Eb4','Ab4'],null,['Eb3','Bb3','D4','G4'],null,['Ab3','Eb4','G4','C5'],null],v7b9:[['C3','Eb3','G3','Bb3'],null,['F3','Ab3','C4','Eb4'],null,['G3','B3','D4','F4','Ab4'],null,['C3','Eb3','G3','Bb3'],null],min9walk:[['C3','Eb3','G3','Bb3','D4'],null,['Db3','F3','Ab3','C4','Eb4'],null,['D3','F3','A3','C4','E4'],null,['Eb3','G3','Bb3','D4','F4'],null],canon:[['C3','E3','G3'],null,['G3','B3','D4'],null,['A3','C4','E4'],null,['E3','G3','B3'],null],descseq:[['C4','Eb4','G4'],['Bb3','D4','F4'],['Ab3','C4','Eb4'],['G3','Bb3','D4'],['F3','Ab3','C4'],['Eb3','G3','Bb3'],['D3','F3','Ab3'],['C3','Eb3','G3']],pedaltone:[['C3','G3','C4','Eb4'],null,['C3','Ab3','C4','F4'],null,['C3','G3','Bb3','Eb4'],null,['C3','F3','Ab3','D4'],null],tizita:[['C3','D3','E3','G3','A3'],null,['G3','A3','C4','D4'],null,['E3','G3','A3','C4'],null,['D3','E3','G3','A3'],null],bati:[['C3','Db3','F3','G3','Ab3'],null,['F3','G3','Ab3','C4'],null,['G3','Ab3','C4','Db4'],null,['C3','Db3','F3','G3'],null],ambassel:[['C3','Db3','F3','G3','Ab3'],null,['Db3','F3','Ab3'],null,['F3','Ab3','C4'],null,['G3','C4','Db4'],null],stab:[['C3','Eb3','G3'],null,null,null,['C3','Eb3','G3'],null,null,null,['Ab2','C3','Eb3'],null,null,null,['G2','Bb2','D3'],null,null,null],arp:[['C3'],['Eb3'],['G3'],['C4'],['Eb3'],['G3'],['C4'],['Eb4'],['G3'],['C4'],['Eb4'],['G4'],['Eb4'],['C4'],['G3'],['Eb3']],skank:[null,['C3','Eb3','G3'],null,['C3','Eb3','G3'],null,['Ab2','C3','Eb3'],null,['Ab2','C3','Eb3'],null,['Bb2','D3','F3'],null,['Bb2','D3','F3'],null,['G2','Bb2','D3'],null,['G2','Bb2','D3']],melodica:[['C4'],null,['Eb4'],['D4'],['C4'],null,['G3'],null,['Ab3'],null,['Bb3'],['C4'],['Bb3'],null,['G3'],null],afrokeys:[['C4','Eb4','G4'],null,['Bb3','D4','F4'],null,['Ab3','C4','Eb4'],null,['Bb3','D4','F4'],null,['C4','Eb4','G4'],null,['Bb3','D4','F4'],null,['Ab3','C4','Eb4'],null,null,null],bossachord:[['C4','E4','G4','B4'],null,null,null,['A3','C4','E4','G4'],null,null,null,['D4','F4','A4','C5'],null,null,null,['G3','B3','D4','F4'],null,null,null],trapkeys:[['C4','Eb4','Gb4'],null,null,null,null,null,null,null,['Bb3','Db4','F4'],null,null,null,null,null,null,null],ambientchord:[['C4','G4','D5','A5'],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],brokenkeys:[['C4','Eb4','G4','Bb4'],null,null,['Ab3','C4','Eb4'],null,null,null,['Bb3','D4','F4','A4'],null,null,null,null,['G3','Bb3','D4','F4'],null,null,null]};
const PADS={wash:[['C3','Eb3','G3','Bb3'],['F3','Ab3','C4','F4'],['Ab3','Eb4','Ab4'],['Eb3','Bb3','Eb4']],detune:[['C3','Eb3','G3','C4','Eb4'],['F3','Ab3','C4','F4'],['Ab3','C4','Eb4','Ab4']],rise:[['C3','Eb3','G3'],['D3','F3','A3'],['Eb3','G3','Bb3'],['F3','Ab3','C4']],fall:[['F3','Ab3','C4'],['Eb3','G3','Bb3'],['D3','F3','A3'],['C3','Eb3','G3']],drone:[['C2','G2','C3'],null,null,['C2','G2','C3'],null,null,null,null],dubwash:[['C3','Eb3','Bb3'],null,null,['Eb3','G3','Bb3'],null,null,['F3','Ab3','C4'],null],ethiowash:[['C3','F3','G3','C4'],['Db3','G3','Ab3'],['F3','Ab3','C4','F4']],fifths:[['C3','G3','D4'],['F3','C4','G4'],['Eb3','Bb3','F4'],['G3','D4','A4']],glass:[['C5','G5','C6'],null,null,['Eb5','Bb5'],null,null,['G5','D6'],null],choir:[['C3','D3','Eb3','G3'],['F3','G3','Ab3','C4'],['Eb3','F3','G3','Bb3'],['G3','Ab3','Bb3','D4']],afrodrone:[['C3','G3','Bb3','Eb4'],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],bossawash:[['C3','E3','G3','B3'],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],ambientwash:[['C3','G3','D4','A4','E5'],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],industrialdrone:[['C2','Gb2','C3'],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]};
const MOODS={dusty:{drums:'bb-dusty',bass:'root5',keys:'min11',pads:'wash',bpm:88,drive:20,swing:30},wonky:{drums:'bb-erratic',bass:'synco',keys:'cluster',pads:'detune',bpm:84,drive:15,swing:42},tense:{drums:'bb-ghost',bass:'chromatic',keys:'v7b9',pads:'rise',bpm:90,drive:25,swing:25},ethio:{drums:'eth-6-8',bass:'ethio',keys:'tizita',pads:'ethiowash',bpm:96,drive:10,swing:15},baroque:{drums:'bb-laid',bass:'walking',keys:'descseq',pads:'fifths',bpm:78,drive:8,swing:20},dub:{drums:'dub-onedrop',bass:'dubheavy',keys:'skank',pads:'dubwash',bpm:72,drive:10,swing:10},industrial:{drums:'ind-offbeat',bass:'acid',keys:'stab',pads:'drone',bpm:136,drive:65,swing:0},hardtechno:{drums:'ind-relentless',bass:'pulse',keys:'arp',pads:'industrialdrone',bpm:148,drive:80,swing:0},afrobeat:{drums:'afro-tony',bass:'afro',keys:'afrokeys',pads:'afrodrone',bpm:112,drive:15,swing:18},bossa:{drums:'bossa-clave',bass:'bossa',keys:'bossachord',pads:'bossawash',bpm:130,drive:5,swing:10},trap:{drums:'trap-basic',bass:'trap',keys:'trapkeys',pads:'industrialdrone',bpm:140,drive:30,swing:0},broken:{drums:'broken-classic',bass:'broken',keys:'brokenkeys',pads:'dubwash',bpm:125,drive:18,swing:25},ambient:{drums:'ambient-pulse',bass:'ambient',keys:'ambientchord',pads:'ambientwash',bpm:68,drive:0,swing:5}};
// ══════════════════════════════════════════════════════════════════
// AUDIO ENGINE — maxPolyphony:16 + releaseAll before chords
// ══════════════════════════════════════════════════════════════════
let kickSynth,kickClick,snareBod,snareRing,clapSynth,hatC,hatO;
let drumGain,drumDrive,bassGain,keysGain,padsGain,masterGain,reverbFX,analyser;
let bassSynth,keysSynth,padsSynth;
let vinylNoise,vinylGn,lofi={};
let keysTrem,bitcrush;
function sN(sy,n,d,t,v){try{sy.triggerAttackRelease(n,d,Math.max(t,Tone.now()+.002),v);}catch(e){}}
function sNs(sy,d,t,v){try{if(v!==undefined)sy.volume.value=Tone.gainToDb(Math.max(.01,v));sy.triggerAttackRelease(d,Math.max(t,Tone.now()+.002));}catch(e){}}
// FIXED: releaseAll before chord to prevent polyphony overflow
function sC(sy,n,d,t,v){
try{sy.releaseAll(Math.max(t,Tone.now()+.001));sy.triggerAttackRelease(n,d,Math.max(t,Tone.now()+.002),v);}catch(e){}
}
async function initAudio(){
if(audioReady)return;
masterGain=new Tone.Gain(.75);analyser=new Tone.Analyser('waveform',256);
reverbFX=new Tone.Reverb({decay:2.5,wet:.25});await reverbFX.ready;
masterGain.chain(reverbFX,Tone.Destination);masterGain.connect(analyser);
drumDrive=new Tone.Distortion({distortion:.2,wet:.2,oversample:'2x'});
drumGain=new Tone.Gain(.8).connect(drumDrive);drumDrive.connect(masterGain);
kickSynth=new Tone.MembraneSynth({pitchDecay:.07,octaves:4,oscillator:{type:'fmsine',modulationType:'sine',modulationIndex:.6},envelope:{attack:.003,decay:.5,sustain:.05,release:.3,attackCurve:'exponential'},volume:-4}).connect(drumGain);
const kBP=new Tone.Filter(3000,'bandpass',-12).connect(drumGain);
kickClick=new Tone.NoiseSynth({noise:{type:'white'},envelope:{attack:.001,decay:.02,sustain:0,release:.01},volume:-12}).connect(kBP);
const sBP=new Tone.Filter(2500,'bandpass',-12).connect(drumGain);
snareBod=new Tone.NoiseSynth({noise:{type:'white'},envelope:{attack:.001,decay:.15,sustain:0,release:.08},volume:-8}).connect(sBP);
snareRing=new Tone.Synth({oscillator:{type:'triangle'},envelope:{attack:.001,decay:.12,sustain:0,release:.06},volume:-18}).connect(drumGain);
const cBP=new Tone.Filter(1800,'bandpass',-12).connect(drumGain);
clapSynth=new Tone.NoiseSynth({noise:{type:'white'},envelope:{attack:.001,decay:.08,sustain:0,release:.04},volume:-14}).connect(cBP);
hatC=new Tone.MetalSynth({frequency:400,envelope:{attack:.001,decay:.06,release:.01},harmonicity:5.1,modulationIndex:32,resonance:4000,octaves:1.5,volume:-16}).connect(drumGain);
hatO=new Tone.MetalSynth({frequency:400,envelope:{attack:.001,decay:.25,release:.1},harmonicity:5.1,modulationIndex:32,resonance:4000,octaves:1.5,volume:-18}).connect(drumGain);
bassGain=new Tone.Gain(.7).connect(masterGain);
bassSynth=new Tone.MonoSynth({oscillator:{type:'fatsawtooth',count:2,spread:10},envelope:{attack:.01,decay:.3,sustain:.5,release:.4},filterEnvelope:{attack:.01,decay:.25,sustain:.3,release:.3,baseFrequency:60,octaves:2.2},portamento:.05,volume:-6}).connect(bassGain);
keysGain=new Tone.Gain(.55);
keysTrem=new Tone.Tremolo({frequency:3.2,depth:.25,wet:.3,spread:90}).start();
bitcrush=new Tone.BitCrusher({bits:16,wet:0});
keysGain.chain(keysTrem,bitcrush,masterGain);
// FIXED: maxPolyphony 16 (was 6 — caused drops with 5-note chords + release overlap)
keysSynth=new Tone.PolySynth({voice:Tone.FMSynth,maxPolyphony:16,options:{modulationIndex:2.5,harmonicity:7,oscillator:{type:'sine'},modulation:{type:'triangle'},envelope:{attack:.005,decay:.6,sustain:.35,release:1.2},modulationEnvelope:{attack:.001,decay:.25,sustain:.02,release:.6}}}).connect(keysGain);
keysSynth.volume.value=-10;
const pLP=new Tone.Filter(900,'lowpass',-12).connect(masterGain);
padsGain=new Tone.Gain(.45).connect(pLP);
// FIXED: maxPolyphony 16 (was 8 — 3s release + per-bar triggers = unbounded accumulation)
padsSynth=new Tone.PolySynth({voice:Tone.Synth,maxPolyphony:16,options:{oscillator:{type:'fatsine',count:3,spread:30},envelope:{attack:.8,decay:2,sustain:.6,release:2.5}}}).connect(padsGain);
padsSynth.volume.value=-14;
const vLP=new Tone.Filter(2000,'lowpass',-12).connect(masterGain);
vinylGn=new Tone.Gain(.02).connect(vLP);
vinylNoise=new Tone.Noise('brown').connect(vinylGn);vinylNoise.start();
lofi.crG=new Tone.Gain(.018);const crLP=new Tone.Filter(380,'lowpass',-24);
lofi.cr=new Tone.Noise('brown').chain(crLP,lofi.crG,masterGain);lofi.cr.start();
lofi.hsG=new Tone.Gain(.024);const hsBP=new Tone.Filter({type:'bandpass',frequency:6200,Q:.85});
lofi.hs=new Tone.Noise('pink').chain(hsBP,lofi.hsG,masterGain);lofi.hs.start();
lofi.fl=new Tone.LFO({frequency:.22,min:-2.2,max:2.2}).start();
Tone.Transport.bpm.value=+$('bpm').value;Tone.Transport.swing=+$('swing').value/100;
audioReady=true;log('Engine ready');
}
// ══════════════════════════════════════════════════════════════════
// SCHEDULER — gap-free rebuild
// ══════════════════════════════════════════════════════════════════
let schedId=null;
function buildSchedule(){
const dp=DRUMS[$('sel-drums').value]||DRUMS['bb-wonky'];
const bp=tpPat(BASS[$('sel-bass').value]||BASS.root5,rootOffset);
const kp=tpPat(KEYS[$('sel-keys').value]||KEYS.min11,rootOffset);
const pp=tpPat(PADS[$('sel-pads').value]||PADS.wash,rootOffset);
const isInd=dp.style==='industrial'||dp.style==='trap';
const kDiv=kp.length<=4?2:kp.length<=8?1:0;
const pLen=pp.length||4;
let step=0;
// FIXED: build new schedule FIRST, then clear old — no gap
const newId=Tone.Transport.scheduleRepeat(time=>{
const s=step%16;
const ki=kDiv===2?Math.floor(step/4)%kp.length:kDiv===1?Math.floor(step/2)%kp.length:step%kp.length;
const pi=Math.floor(step/16)%pLen;
if(isActive('drums')){
const kV=Math.max(dp.k[s]||0,customSeq.kick[s]?.8:0);
const sV=Math.max(dp.s[s]||0,customSeq.snare[s]?.8:0);
const cV=Math.max(dp.c[s]||0,customSeq.clap[s]?.6:0);
const hV=Math.max(dp.h[s]||0,customSeq.hat[s]?.6:0);
const oV=Math.max(dp.o[s]||0,customSeq.open[s]?.5:0);
const up=s%2===1;
if(kV>0){const o=isInd?0:kickLag*(.6+Math.random()*.8);sN(kickSynth,'C1','8n',time+o,kV*(.85+Math.random()*.15));sNs(kickClick,'32n',time+o,kV*.4);}
if(sV>0){const o=isInd?0:Math.max(snareRush*(.5+Math.random()),.001);sNs(snareBod,'16n',time+o,sV*(.8+Math.random()*.2));sN(snareRing,'E4','16n',time+o,sV*.5);}
if(cV>0)sNs(clapSynth,'32n',time+(sV>0?.003:.008),cV);
if(hV>0){const o=up?hatDrift*(.5+Math.random()):Math.random()*.003;sN(hatC,'C6','32n',time+o,hV*(.7+Math.random()*.3));}
if(oV>0)sN(hatO,'C6','8n',time+(up?hatDrift*.8:0),oV*(.7+Math.random()*.3));
}
if(isActive('bass')){const bn=bp[s];if(bn)sN(bassSynth,bn,'8n',time+.005,.7+Math.random()*.25);}
if(isActive('keys')){
const fire=kDiv===2?(s%4===0):kDiv===1?(s%2===0):true;
if(fire){const kn=kp[ki];if(kn){try{keysSynth.set({detune:(Math.random()-.5)*16});}catch(e){}sC(keysSynth,Array.isArray(kn)?kn:[kn],'2n',time+.005,.45+Math.random()*.35);}}
}
if(isActive('pads')&&s===0){const pn=pp[pi];if(pn)sC(padsSynth,Array.isArray(pn)?pn:[pn],'1m',time+.005,.4+Math.random()*.2);}
currentStep=s;
Tone.Draw.schedule(()=>updateViz(s),time);
step++;
},'16n');
// Now clear old
if(schedId!==null)Tone.Transport.clear(schedId);
schedId=newId;
}
// ══════════════════════════════════════════════════════════════════
// TRANSPORT
// ══════════════════════════════════════════════════════════════════
async function play(){
if(Tone.context.state!=='running')await Tone.start();
if(!audioReady)await initAudio();
buildSchedule();
Tone.Transport.start('+0.05');
playing=true;
$('playBtn').textContent='⏸ Pause';$('playBtn').classList.add('on');
$('dot').classList.add('live');$('statusTxt').textContent='Playing';
drawViz();log('▶');
}
function stop(){
if(schedId!==null){Tone.Transport.clear(schedId);schedId=null;}
Tone.Transport.stop();Tone.Transport.position=0;
playing=false;currentStep=-1;
// Release all lingering voices
try{keysSynth.releaseAll();padsSynth.releaseAll();}catch(e){}
$('playBtn').textContent='▶ Play';$('playBtn').classList.remove('on');
$('dot').classList.remove('live');$('statusTxt').textContent='Ready';
document.querySelectorAll('.seq-step').forEach(s=>s.classList.remove('playing'));
}
function rebuild(){if(!playing)return;buildSchedule();}
function updateViz(s){document.querySelectorAll('.seq-step').forEach(el=>{el.classList.toggle('playing',+el.dataset.step===s);});}
function drawViz(){
requestAnimationFrame(drawViz);
const c=$('viz'),ctx=c.getContext('2d');c.width=c.offsetWidth;c.height=c.offsetHeight;
const cs=getComputedStyle(document.documentElement);
ctx.fillStyle=cs.getPropertyValue('--sf').trim();ctx.fillRect(0,0,c.width,c.height);
if(!analyser)return;const data=analyser.getValue();
ctx.beginPath();ctx.strokeStyle=playing?cs.getPropertyValue('--ac').trim():cs.getPropertyValue('--br').trim();ctx.lineWidth=1;
for(let i=0;i<data.length;i++){const x=(i/data.length)*c.width,y=(1-(data[i]+1)/2)*c.height;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
ctx.stroke();
}
// ══════════════════════════════════════════════════════════════════
// EXPORT
// ══════════════════════════════════════════════════════════════════
function showExport(){$('exportBar').classList.add('show');$('exportFill').style.width='0%';$('exportMsg').textContent='Choose format:';}
async function doWAV(){
$('exportMsg').textContent='Rendering 4 bars...';$('exportFill').style.width='15%';
const bars=4,bv=Tone.Transport.bpm.value||88,dur=bars*4*(60/bv),sr=44100;
try{
$('exportFill').style.width='30%';
const buf=await Tone.Offline(async({transport})=>{
transport.bpm.value=bv;
const rev=new Tone.Reverb({decay:2,wet:.2});await rev.ready;
const oK=new Tone.MembraneSynth({pitchDecay:.07,octaves:4,volume:-4}).connect(rev).toDestination();
const oS=new Tone.NoiseSynth({envelope:{attack:.001,decay:.15,sustain:0},volume:-8}).connect(rev).toDestination();
const oH=new Tone.MetalSynth({frequency:400,envelope:{attack:.001,decay:.06,release:.01},volume:-16}).connect(rev).toDestination();
const oB=new Tone.MonoSynth({oscillator:{type:'sawtooth'},envelope:{attack:.01,decay:.3,sustain:.5,release:.4},volume:-6}).connect(rev).toDestination();
const oK2=new Tone.PolySynth({voice:Tone.FMSynth,maxPolyphony:16,options:{modulationIndex:2.5,harmonicity:7,oscillator:{type:'sine'},envelope:{attack:.005,decay:.6,sustain:.35,release:1.2}}}).connect(rev).toDestination();
oK2.volume.value=-10;
const oP=new Tone.PolySynth({voice:Tone.Synth,maxPolyphony:16,options:{oscillator:{type:'fatsine',count:3,spread:30},envelope:{attack:.8,decay:2,sustain:.6,release:2.5}}}).connect(rev).toDestination();
oP.volume.value=-14;
const dp=DRUMS[$('sel-drums').value]||DRUMS['bb-wonky'];
const bp=tpPat(BASS[$('sel-bass').value]||BASS.root5,rootOffset);
const kp=tpPat(KEYS[$('sel-keys').value]||KEYS.min11,rootOffset);
const pp=tpPat(PADS[$('sel-pads').value]||PADS.wash,rootOffset);
const st=60/bv/4;
for(let bar=0;bar<bars;bar++){
for(let s=0;s<16;s++){const t=bar*16*st+s*st;
if(isActive('drums')){if((dp.k[s]||0)>0)oK.triggerAttackRelease('C1','.1',t,dp.k[s]);if((dp.s[s]||0)>0)oS.triggerAttackRelease('.1',t);if((dp.h[s]||0)>0)oH.triggerAttackRelease('32n',t,dp.h[s]);}
if(isActive('bass')&&bp[s])oB.triggerAttackRelease(bp[s],'8n',t);
}
const kDiv=kp.length<=4?4:kp.length<=8?2:1;
for(let ki=0;ki<kp.length;ki++){const t=bar*16*st+ki*kDiv*st;
if(isActive('keys')&&kp[ki]){oK2.releaseAll(t);const ch=Array.isArray(kp[ki])?kp[ki]:[kp[ki]];oK2.triggerAttackRelease(ch,'2n',t,.5);}}
const pi=bar%pp.length;
if(isActive('pads')&&pp[pi]&&Array.isArray(pp[pi])){oP.releaseAll(bar*16*st);oP.triggerAttackRelease(pp[pi],'1m',bar*16*st,.4);}
}
},dur,2,sr);
$('exportFill').style.width='75%';
const nc=buf.numberOfChannels,len=buf.length,ab=new ArrayBuffer(44+len*nc*2),v=new DataView(ab);
const ws=(o,s)=>{for(let i=0;i<s.length;i++)v.setUint8(o+i,s.charCodeAt(i));};
ws(0,'RIFF');v.setUint32(4,36+len*nc*2,true);ws(8,'WAVE');ws(12,'fmt ');v.setUint32(16,16,true);v.setUint16(20,1,true);v.setUint16(22,nc,true);v.setUint32(24,sr,true);v.setUint32(28,sr*nc*2,true);v.setUint16(32,nc*2,true);v.setUint16(34,16,true);ws(36,'data');v.setUint32(40,len*nc*2,true);
const chs=[];for(let i=0;i<nc;i++)chs.push(buf.getChannelData(i));let off=44;
for(let i=0;i<len;i++)for(let c=0;c<nc;c++){const s=Math.max(-1,Math.min(1,chs[c][i]));v.setInt16(off,s<0?s*0x8000:s*0x7FFF,true);off+=2;}
const blob=new Blob([ab],{type:'audio/wav'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`rg69-${Date.now()}.wav`;a.click();
$('exportFill').style.width='100%';$('exportMsg').textContent='Downloaded';
log('WAV saved');setTimeout(()=>$('exportBar').classList.remove('show'),2000);
}catch(e){$('exportMsg').textContent='Error: '+e.message;log('Export error');}
}
let recording=false,rec,chunks=[];
function toggleRec(){
if(!recording){
if(!audioReady){$('exportMsg').textContent='Play first, then record';return;}
try{
const dest=Tone.context.createMediaStreamDestination();masterGain.connect(dest);
rec=new MediaRecorder(dest.stream);chunks=[];
rec.ondataavailable=e=>chunks.push(e.data);
rec.onstop=()=>{const blob=new Blob(chunks,{type:'audio/webm'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`rg69-live-${Date.now()}.webm`;a.click();$('exportMsg').textContent='Downloaded';log('Rec saved');setTimeout(()=>$('exportBar').classList.remove('show'),2000);};
rec.start();recording=true;$('expRec').classList.add('on');$('exportMsg').textContent='Recording... click ⏺ to stop';
}catch(e){log('Rec error');}
}else{rec.stop();recording=false;$('expRec').classList.remove('on');}
}
// ══════════════════════════════════════════════════════════════════
// UTILITIES
// ══════════════════════════════════════════════════════════════════
function tapTempo(){const now=Date.now();tapTimes.push(now);if(tapTimes.length>4)tapTimes.shift();if(tapTimes.length>1){const d=[];for(let i=1;i<tapTimes.length;i++)d.push(tapTimes[i]-tapTimes[i-1]);const avg=d.reduce((a,b)=>a+b)/d.length;const b=Math.round(60000/avg);$('bpm').value=b;$('bpmV').textContent=b;if(audioReady)Tone.Transport.bpm.value=b;log('Tap: '+b);}setTimeout(()=>{tapTimes=[];},2000);}
function pushState(){const s={d:$('sel-drums').value,b:$('sel-bass').value,k:$('sel-keys').value,p:$('sel-pads').value,seq:JSON.parse(JSON.stringify(customSeq))};hist.splice(histIdx+1);hist.push(JSON.stringify(s));histIdx=hist.length-1;if(hist.length>50){hist.shift();histIdx--;}}
function undo(){if(histIdx>0){histIdx--;applyHist(JSON.parse(hist[histIdx]));log('Undo');}}
function redo(){if(histIdx<hist.length-1){histIdx++;applyHist(JSON.parse(hist[histIdx]));log('Redo');}}
function applyHist(s){$('sel-drums').value=s.d;$('sel-bass').value=s.b;$('sel-keys').value=s.k;$('sel-pads').value=s.p;Object.keys(s.seq).forEach(k=>{for(let i=0;i<16;i++)customSeq[k][i]=s.seq[k][i];});renderSeq();rebuild();}
function toggleTheme(){document.documentElement.setAttribute('data-theme',document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark');}
// ══════════════════════════════════════════════════════════════════
// UI BUILD
// ══════════════════════════════════════════════════════════════════
function buildUI(){
const ds=$('sel-drums'),grp={};
Object.keys(DRUMS).forEach(k=>{const s=DRUMS[k].style||'other';if(!grp[s])grp[s]=[];grp[s].push(k);});
Object.entries(grp).forEach(([st,ks])=>{const og=document.createElement('optgroup');og.label=st[0].toUpperCase()+st.slice(1);ks.forEach(k=>{const o=document.createElement('option');o.value=k;o.textContent=k;og.appendChild(o);});ds.appendChild(og);});
const addO=(sel,obj)=>{Object.keys(obj).forEach(k=>{const o=document.createElement('option');o.value=k;o.textContent=k;sel.appendChild(o);});};
addO($('sel-bass'),BASS);addO($('sel-keys'),KEYS);addO($('sel-pads'),PADS);
Object.keys(MOODS).forEach(k=>{const b=document.createElement('button');b.className='mood-btn';b.textContent=k;b.dataset.mood=k;$('moodRow').appendChild(b);});
const mx=$('mixerBody');
[{id:'drums',name:'Drums',vol:80},{id:'bass',name:'Bass',vol:70},{id:'keys',name:'Keys',vol:55},{id:'pads',name:'Pads',vol:45}].forEach(t=>{
const row=document.createElement('div');row.className='track-row';
row.innerHTML=`<span class="name">${t.name}</span><input type="range" min="0" max="100" value="${t.vol}" data-vol="${t.id}" aria-label="${t.name} volume"><span class="vval" id="vv-${t.id}">${t.vol}</span><button class="track-btn" data-mute="${t.id}" aria-label="Mute ${t.name}" title="Mute">M</button><button class="track-btn" data-solo="${t.id}" aria-label="Solo ${t.name}" title="Solo">S</button>`;
mx.appendChild(row);
});
renderSeq();attachEvents();pushState();drawViz();
}
function renderSeq(){
const body=$('seqBody');body.innerHTML='';
const dp=DRUMS[$('sel-drums').value];
const tm={kick:'k',snare:'s',clap:'c',hat:'h',open:'o'};
// Beat markers
const beats=document.createElement('div');beats.className='beat-markers';
for(let i=0;i<16;i++){const sp=document.createElement('span');sp.textContent=i%4===0?(i/4+1):'·';if(i%4===0)sp.classList.add('bar');beats.appendChild(sp);}
body.appendChild(beats);
['kick','snare','clap','hat','open'].forEach(track=>{
const row=document.createElement('div');row.className='seq-track';
const head=document.createElement('div');head.className='seq-head';
const name=document.createElement('span');name.className='name';name.textContent=track;
head.appendChild(name);row.appendChild(head);
const steps=document.createElement('div');steps.className='seq-steps';steps.setAttribute('role','group');steps.setAttribute('aria-label',track+' steps');
for(let i=0;i<16;i++){
const s=document.createElement('div');s.className='seq-step';s.dataset.track=track;s.dataset.step=i;
s.setAttribute('role','checkbox');s.setAttribute('aria-label',track+' step '+(i+1));
s.setAttribute('tabindex','0');
const vel=dp&&dp[tm[track]]?(dp[tm[track]][i]||0):0;
if(customSeq[track][i]){
s.classList.add('on');s.setAttribute('aria-checked','true');
} else if(vel>0){
s.classList.add('hit');
// Velocity-mapped opacity
s.style.opacity=(.25+vel*.75).toFixed(2);
}
const toggle=()=>{customSeq[track][i]=customSeq[track][i]?0:1;s.classList.toggle('on',!!customSeq[track][i]);s.setAttribute('aria-checked',String(!!customSeq[track][i]));s.classList.remove('hit');s.style.opacity='';pushState();};
s.onclick=toggle;
// Keyboard: Enter/Space to toggle
s.onkeydown=e=>{if(e.key===' '||e.key==='Enter'){e.preventDefault();toggle();}};
steps.appendChild(s);
}
row.appendChild(steps);body.appendChild(row);
});
}
function attachEvents(){
$('playBtn').onclick=()=>playing?stop():play();
$('stopBtn').onclick=stop;
$('randomBtn').onclick=randomize;
$('tapBtn').onclick=tapTempo;
$('saveBtn').onclick=showExport;
$('expWav').onclick=doWAV;
$('expRec').onclick=toggleRec;
$('expClose').onclick=()=>$('exportBar').classList.remove('show');
$('undoBtn').onclick=undo;$('redoBtn').onclick=redo;
$('themeBtn').onclick=toggleTheme;
$('helpBtn').onclick=()=>$('helpModal').classList.toggle('show');
$('bpm').oninput=function(){$('bpmV').textContent=this.value;if(audioReady)Tone.Transport.bpm.value=+this.value;};
$('swing').oninput=function(){$('swingV').textContent=this.value;if(audioReady)Tone.Transport.swing=+this.value/100;};
$('rootSel').onchange=function(){rootOffset=+this.value;rebuild();};
['sel-drums','sel-bass','sel-keys','sel-pads'].forEach(id=>{$(id).onchange=()=>{if(id==='sel-drums')renderSeq();rebuild();pushState();};});
document.querySelectorAll('[data-vol]').forEach(sl=>{const id=sl.dataset.vol;sl.oninput=function(){$('vv-'+id).textContent=this.value;const g=id==='drums'?drumGain:id==='bass'?bassGain:id==='keys'?keysGain:padsGain;if(g)g.gain.value=+this.value/100;};});
document.querySelectorAll('[data-mute]').forEach(b=>{b.onclick=()=>{const id=b.dataset.mute;trackState[id].mute=!trackState[id].mute;b.classList.toggle('muted',trackState[id].mute);};});
document.querySelectorAll('[data-solo]').forEach(b=>{b.onclick=()=>{const id=b.dataset.solo;trackState[id].solo=!trackState[id].solo;b.classList.toggle('soloed',trackState[id].solo);};});
$('sKL').oninput=function(){kickLag=+this.value/1000;$('vKL').textContent=this.value+'ms';};
$('sSR').oninput=function(){snareRush=-(+this.value/1000);$('vSR').textContent=this.value+'ms';};
$('sHD').oninput=function(){hatDrift=+this.value/1000;$('vHD').textContent=this.value+'ms';};
$('sDR').oninput=function(){$('vDR').textContent=this.value;if(drumDrive){drumDrive.distortion=+this.value/100;drumDrive.wet.value=Math.min(+this.value/100,.85);}};
$('sRV').oninput=function(){$('vRV').textContent=this.value;if(reverbFX)reverbFX.wet.value=+this.value/100;};
$('sCR').oninput=function(){$('vCR').textContent=this.value;if(bitcrush){const v=+this.value;bitcrush.wet.value=v>0?1:0;bitcrush.bits.value=v>0?Math.max(4,16-Math.floor(v/8)):16;}};
$('sGL').oninput=function(){$('vGL').textContent=this.value+'ms';if(bassSynth)bassSynth.portamento=+this.value/1000;};
$('sVN').oninput=function(){$('vVN').textContent=this.value;if(vinylGn)vinylGn.gain.value=+this.value/100*.08;};
$('sCK').oninput=function(){$('vCK').textContent=this.value;if(lofi.crG)lofi.crG.gain.value=+this.value/100*.06;if(lofi.hsG)lofi.hsG.gain.value=+this.value/100*.05;};
$('sFL').oninput=function(){$('vFL').textContent=this.value;if(lofi.fl){const v=+this.value/100;lofi.fl.min=-3*v;lofi.fl.max=3*v;}};
document.querySelectorAll('.mood-btn').forEach(b=>{b.onclick=()=>{document.querySelectorAll('.mood-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');applyMood(b.dataset.mood);};});
document.querySelectorAll('.sec-title').forEach(t=>{t.onclick=()=>t.parentElement.classList.toggle('closed');});
document.addEventListener('keydown',e=>{
if(e.code==='Space'&&(e.target===document.body||e.target.tagName==='BUTTON')){e.preventDefault();playing?stop():play();}
else if(!e.ctrlKey&&!e.metaKey&&(e.key==='r'||e.key==='R')&&e.target===document.body)randomize();
else if(e.key==='t'||e.key==='T')tapTempo();
else if(e.key==='l'||e.key==='L')toggleTheme();
else if((e.ctrlKey||e.metaKey)&&!e.shiftKey&&e.key==='z'){e.preventDefault();undo();}
else if((e.ctrlKey||e.metaKey)&&e.shiftKey&&(e.key==='Z'||e.key==='z')){e.preventDefault();redo();}
});
}
function applyMood(name){
const m=MOODS[name];if(!m)return;
$('sel-drums').value=m.drums;$('sel-bass').value=m.bass;$('sel-keys').value=m.keys;$('sel-pads').value=m.pads;
$('bpm').value=m.bpm;$('bpmV').textContent=m.bpm;
$('sDR').value=m.drive;$('vDR').textContent=m.drive;
$('swing').value=m.swing;$('swingV').textContent=m.swing;
Object.keys(trackState).forEach(k=>{trackState[k].mute=false;trackState[k].solo=false;});
document.querySelectorAll('.track-btn').forEach(b=>{b.classList.remove('muted','soloed');});
if(audioReady){Tone.Transport.bpm.value=m.bpm;Tone.Transport.swing=m.swing/100;drumDrive.distortion=m.drive/100;drumDrive.wet.value=Math.min(m.drive/100,.85);}
renderSeq();
if(!playing)play();else rebuild();
log('Style: '+name);
}
async function randomize(){
const pick=o=>{const k=Object.keys(o);return k[Math.floor(Math.random()*k.length)];};
const dp=pick(DRUMS);$('sel-drums').value=dp;$('sel-bass').value=pick(BASS);$('sel-keys').value=pick(KEYS);$('sel-pads').value=pick(PADS);
const pat=DRUMS[dp],bpm=pat.bpm[0]+Math.floor(Math.random()*(pat.bpm[1]-pat.bpm[0]));
$('bpm').value=bpm;$('bpmV').textContent=bpm;
const drive=pat.style==='industrial'?50+Math.floor(Math.random()*40):pat.style==='trap'?20+Math.floor(Math.random()*30):pat.style==='ethio'?5+Math.floor(Math.random()*15):10+Math.floor(Math.random()*25);
$('sDR').value=drive;$('vDR').textContent=drive;
const sw=(pat.style==='industrial'||pat.style==='trap')?Math.floor(Math.random()*10):pat.style==='ethio'?10+Math.floor(Math.random()*20):15+Math.floor(Math.random()*45);
$('swing').value=sw;$('swingV').textContent=sw;
Object.keys(trackState).forEach(k=>{trackState[k].mute=false;trackState[k].solo=false;});
document.querySelectorAll('.track-btn').forEach(b=>{b.classList.remove('muted','soloed');});
document.querySelectorAll('.mood-btn').forEach(b=>b.classList.remove('active'));
if(!playing)await play();else{Tone.Transport.bpm.value=bpm;Tone.Transport.swing=sw/100;if(drumDrive){drumDrive.distortion=drive/100;drumDrive.wet.value=Math.min(drive/100,.85);}rebuild();}
renderSeq();pushState();log('Random: '+dp+' @ '+bpm);
}
// ── INIT ──
$('welcomeStart').onclick=async()=>{
await Tone.start();$('welcome').classList.add('gone');$('app').classList.add('visible');
setTimeout(()=>{$('welcome').style.display='none';},500);
applyMood('dusty');
};
buildUI();log('RG-69 v5');
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment