Created
February 16, 2026 23:32
-
-
Save anon987654321/81f018e537fec64f40649dc91c91c1c5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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