Custom Code Experiment •
101
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>SH-101</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600;700&family=Barlow:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
box-sizing:border-box;
padding-top:env(safe-area-inset-top,0px);
padding-bottom:env(safe-area-inset-bottom,0px);
--page:#e6e4df;
--page-2:#d4d2cc;
--page-ink:#26262a;
--page-dim:#6a6a70;
/* grey model */
--case:#b4b2ab;
--case-hi:#cbc9c3;
--case-lo:#86847e;
--case-ink:#2a2a2c;
--plate:#2c2c2e;
--plate-hi:#3a3a3d;
--plate-lo:#1c1c1e;
--ink:#f1efe9;
--ink-dim:#9d9b96;
--rule:#565659;
--cap:#d9d7d1;
--cap-lo:#a5a39d;
--cap-line:#1a1a1c;
--slot:#141416;
--led:#ff3b1f;
--led-dim:#4a1a12;
--key-w:#f4f2ec;
--key-w-lo:#cfcdc6;
--key-b:#232326;
}
html[data-model="blue"]{ --case:#2f5f9c; --case-hi:#4a7cba; --case-lo:#1f4370; --case-ink:#eef3f9; }
html[data-model="red"]{ --case:#b0372c; --case-hi:#cb5748; --case-lo:#7d241c; --case-ink:#fdeeeb; }
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
--page:#171718;
--page-2:#0e0e0f;
--page-ink:#e9e7e2;
--page-dim:#8c8a86;
}
}
:root[data-theme="dark"]{ --page:#171718; --page-2:#0e0e0f; --page-ink:#e9e7e2; --page-dim:#8c8a86; }
*,*::before,*::after{box-sizing:inherit}
html{scroll-padding-top:env(safe-area-inset-top,0px)}
body{
margin:0;
background:
radial-gradient(120% 90% at 50% 0%, var(--page) 0%, var(--page-2) 100%);
color:var(--page-ink);
font-family:'Barlow','Helvetica Neue',Arial,sans-serif;
-webkit-text-size-adjust:100%;
padding:18px 14px 40px;
min-height:100%;
overscroll-behavior-y:none;
}
@media (prefers-reduced-motion: reduce){ *{animation:none!important; transition:none!important} }
.wrap{max-width:1204px;margin:0 auto}
.help,.synth{scrollbar-width:thin}
/* ---------- case ---------- */
.synth{
width:fit-content;max-width:100%;margin:0 auto;
background:linear-gradient(180deg,var(--case-hi) 0%,var(--case) 12%,var(--case) 88%,var(--case-lo) 100%);
border-radius:10px 10px 14px 14px;
padding:12px 12px 14px;
box-shadow:
0 1px 0 rgba(255,255,255,.5) inset,
0 -2px 0 rgba(0,0,0,.25) inset,
0 18px 40px rgba(0,0,0,.35),
0 2px 6px rgba(0,0,0,.25);
color:var(--case-ink);
position:relative;
}
.synth::after{
content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
background-image:repeating-linear-gradient(0deg,rgba(0,0,0,.035) 0 1px,transparent 1px 3px);
opacity:.55;mix-blend-mode:multiply;
}
/* ---------- top bar ---------- */
.topbar{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:12px}
.brand{display:flex;align-items:baseline;gap:10px;margin-right:auto}
.brand .model{
font-family:'Barlow Condensed',sans-serif;font-weight:700;font-size:34px;letter-spacing:.02em;
line-height:.9;color:var(--case-ink);
}
.brand .model span{color:var(--led);}
.brand .tag{
font-family:'Barlow Condensed',sans-serif;font-size:13px;letter-spacing:.14em;
text-transform:uppercase;opacity:.65;line-height:1
}
.topgrp{display:flex;align-items:center;gap:8px}
.topgrp .cap-lbl{
font-family:'Barlow Condensed',sans-serif;font-size:11px;letter-spacing:.12em;
text-transform:uppercase;opacity:.6
}
select.sel{
font-family:'Barlow Condensed',sans-serif;font-size:14px;letter-spacing:.04em;
background:var(--plate);color:var(--ink);border:1px solid var(--plate-lo);
border-radius:3px;padding:5px 8px;cursor:pointer;max-width:190px
}
select.sel:focus-visible{outline:2px solid var(--led);outline-offset:2px}
.swatches{display:flex;gap:5px}
.swatch{width:20px;height:20px;border-radius:50%;border:2px solid rgba(0,0,0,.35);cursor:pointer;padding:0}
.swatch[aria-pressed="true"]{box-shadow:0 0 0 2px var(--case-ink)}
.swatch.grey{background:#b4b2ab}.swatch.blue{background:#2f5f9c}.swatch.red{background:#b0372c}
.pwr{
font-family:'Barlow Condensed',sans-serif;font-size:14px;letter-spacing:.12em;text-transform:uppercase;
background:var(--plate);color:var(--ink);border:1px solid var(--plate-lo);border-radius:3px;
padding:6px 12px;cursor:pointer;display:flex;align-items:center;gap:8px
}
.pwr .dot{width:8px;height:8px;border-radius:50%;background:var(--led-dim)}
.pwr[data-on="1"] .dot{background:var(--led);box-shadow:0 0 8px var(--led)}
.pwr:focus-visible{outline:2px solid var(--led);outline-offset:2px}
/* ---------- panel ---------- */
.panel{display:flex;flex-wrap:wrap;gap:6px;align-items:stretch;align-content:flex-start;justify-content:flex-start}
.sec{
background:linear-gradient(180deg,var(--plate-hi),var(--plate) 22%,var(--plate) 78%,var(--plate-lo));
border-radius:5px;
border:1px solid rgba(0,0,0,.4);
box-shadow:0 1px 0 rgba(255,255,255,.12) inset, 0 2px 5px rgba(0,0,0,.3);
padding:5px 5px 8px;
display:flex;flex-direction:column;gap:6px;flex:0 1 auto
}
.sec > h2{
font-family:'Barlow Condensed',sans-serif;font-weight:600;font-size:12px;letter-spacing:.2em;
text-transform:uppercase;color:var(--ink);margin:0;padding-bottom:3px;
border-bottom:1px solid var(--rule)
}
.row{display:flex;gap:5px;align-items:flex-end}
/* ---------- slider ---------- */
.ctrl{display:flex;flex-direction:column;align-items:center;gap:4px;width:28px}
.fader{
position:relative;width:28px;height:142px;touch-action:none;cursor:ns-resize;
display:flex;justify-content:center;border-radius:3px
}
.fader:focus-visible{outline:2px solid var(--led);outline-offset:2px}
.fader .slot{
position:absolute;top:0;bottom:0;left:50%;transform:translateX(-50%);
width:6px;border-radius:3px;background:var(--slot);
box-shadow:0 1px 1px rgba(255,255,255,.14), inset 0 0 3px rgba(0,0,0,.9)
}
.fader .ticks{position:absolute;top:10px;bottom:10px;left:2px;width:4px}
.fader .ticks i{position:absolute;left:0;width:4px;height:1px;background:var(--ink-dim);opacity:.5}
.fader .ticks i.maj{width:7px;left:-2px;opacity:.85}
.fader .cap{
position:absolute;left:50%;width:24px;height:19px;
transform:translate(-50%,0);border-radius:3px;
background:linear-gradient(180deg,var(--cap) 0%,var(--cap) 42%,var(--cap-lo) 46%,var(--cap) 50%,var(--cap) 100%);
box-shadow:0 2px 3px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.7) inset;
border:1px solid rgba(0,0,0,.45);pointer-events:none
}
.fader .cap::after{
content:"";position:absolute;left:3px;right:3px;top:50%;height:2px;margin-top:-1px;
background:var(--cap-line);border-radius:1px;opacity:.9
}
.lbl{
font-family:'Barlow Condensed',sans-serif;font-size:10px;font-weight:500;letter-spacing:.09em;
text-transform:uppercase;color:var(--ink);text-align:center;line-height:1.05;min-height:22px
}
.lbl b{display:block;font-weight:500}
.lbl .num{display:block;color:var(--ink-dim);font-size:9px;letter-spacing:.04em;font-variant-numeric:tabular-nums}
/* ---------- switch ---------- */
.swc{display:flex;flex-direction:column;align-items:center;gap:4px;min-width:42px}
.sw{display:flex;gap:4px;align-items:stretch;padding:2px 0}
.sw .rail{
position:relative;width:13px;border-radius:3px;background:var(--slot);
box-shadow:inset 0 0 3px rgba(0,0,0,.9),0 1px 0 rgba(255,255,255,.12)
}
.sw .lever{
position:absolute;left:1px;width:11px;height:13px;border-radius:2px;
background:linear-gradient(180deg,#f0eee8,#b9b7b1);
box-shadow:0 1px 2px rgba(0,0,0,.6);transition:top .08s ease
}
.sw .opts{display:flex;flex-direction:column;gap:6px}
.sw .opt{
font-family:'Barlow Condensed',sans-serif;font-size:10px;letter-spacing:.03em;text-transform:uppercase;
background:none;border:0;padding:0;color:var(--ink-dim);cursor:pointer;text-align:left;
line-height:13px;white-space:nowrap
}
.sw .opt[aria-checked="true"]{color:var(--ink);font-weight:600}
.sw:focus-visible{outline:2px solid var(--led);outline-offset:3px;border-radius:3px}
/* ---------- led ---------- */
.led{width:9px;height:9px;border-radius:50%;background:var(--led-dim);
box-shadow:inset 0 1px 2px rgba(0,0,0,.7);flex:0 0 auto}
.led[data-on="1"]{background:var(--led);box-shadow:0 0 7px var(--led),inset 0 0 2px rgba(255,255,255,.6)}
.ledrow{display:flex;flex-direction:column;align-items:center;gap:4px;justify-content:flex-end;padding-bottom:24px;width:10px}
/* ---------- transport ---------- */
.transport{
display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:10px;
background:linear-gradient(180deg,var(--plate-hi),var(--plate));
border:1px solid rgba(0,0,0,.4);border-radius:5px;padding:8px 10px;
box-shadow:0 1px 0 rgba(255,255,255,.12) inset
}
.tgrp{display:flex;gap:6px;align-items:center}
.tgrp > .name{
font-family:'Barlow Condensed',sans-serif;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
color:var(--ink-dim);margin-right:2px
}
.btn{
font-family:'Barlow Condensed',sans-serif;font-size:12px;letter-spacing:.09em;text-transform:uppercase;
background:linear-gradient(180deg,#4b4b4f,#343437);color:var(--ink);
border:1px solid rgba(0,0,0,.55);border-radius:3px;padding:5px 9px 5px 7px;cursor:pointer;
display:flex;align-items:center;gap:6px;box-shadow:0 2px 3px rgba(0,0,0,.35),0 1px 0 rgba(255,255,255,.14) inset
}
.btn:active{transform:translateY(1px)}
.btn:focus-visible{outline:2px solid var(--led);outline-offset:2px}
.readout{
font-family:'Barlow Condensed',sans-serif;font-size:12px;letter-spacing:.08em;color:var(--ink-dim);
font-variant-numeric:tabular-nums;margin-left:auto;text-align:right
}
.readout b{color:var(--ink);font-weight:600}
/* ---------- pattern bank ---------- */
.bank{
display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:6px;
background:linear-gradient(180deg,var(--plate-hi),var(--plate));
border:1px solid rgba(0,0,0,.4);border-radius:5px;padding:8px 10px;
box-shadow:0 1px 0 rgba(255,255,255,.12) inset
}
.bgrp{display:flex;gap:6px;align-items:center}
.bgrp > .name{
font-family:'Barlow Condensed',sans-serif;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
color:var(--ink-dim);margin-right:2px
}
.slots{display:flex;gap:4px}
.slot{
width:34px;height:32px;border-radius:3px;cursor:pointer;
background:linear-gradient(180deg,#3d3d41,#2a2a2d);border:1px solid rgba(0,0,0,.55);
color:var(--ink-dim);font-family:'Barlow Condensed',sans-serif;display:flex;
flex-direction:column;align-items:center;justify-content:center;line-height:1;padding:0;
box-shadow:0 1px 0 rgba(255,255,255,.1) inset
}
.slot .ltr{font-size:14px;font-weight:600}
.slot .ct{font-size:9px;letter-spacing:.04em;opacity:.75}
.slot[data-full="1"]{color:var(--ink);background:linear-gradient(180deg,#55555a,#3a3a3e)}
.slot[data-sel="1"]{box-shadow:0 0 0 2px var(--led),0 1px 0 rgba(255,255,255,.1) inset}
.chainwrap{flex:1 1 100%;display:flex;align-items:center;gap:8px;min-width:0}
.chainstrip{
flex:1;min-width:0;display:flex;gap:3px;overflow-x:auto;align-items:center;
background:var(--plate-lo);border:1px solid rgba(0,0,0,.5);border-radius:3px;
padding:4px;min-height:28px
}
.chaincell{
flex:0 0 auto;min-width:24px;text-align:center;font-family:'Barlow Condensed',sans-serif;
font-size:12px;font-weight:600;color:var(--ink-dim);background:#26262a;border-radius:2px;
padding:2px 5px;cursor:pointer
}
.chaincell[data-cur="1"]{background:var(--led);color:#fff}
/* ---------- lower / keyboard ---------- */
.lower{display:flex;gap:12px;margin-top:10px;align-items:stretch}
.grip{
flex:0 0 96px;background:linear-gradient(180deg,var(--plate-hi),var(--plate-lo));
border:1px solid rgba(0,0,0,.45);border-radius:5px;padding:8px;
display:flex;flex-direction:column;gap:6px
}
.grip .name{
font-family:'Barlow Condensed',sans-serif;font-size:10px;letter-spacing:.14em;text-transform:uppercase;
color:var(--ink-dim);text-align:center
}
.pad{
flex:1;position:relative;border-radius:4px;background:var(--slot);touch-action:none;cursor:grab;
box-shadow:inset 0 0 8px rgba(0,0,0,.9);min-height:86px;overflow:hidden
}
.pad:active{cursor:grabbing}
.pad .cross{position:absolute;inset:0;pointer-events:none;opacity:.35}
.pad .cross i{position:absolute;background:var(--ink-dim)}
.pad .cross i.h{left:8%;right:8%;top:50%;height:1px}
.pad .cross i.v{top:8%;bottom:8%;left:50%;width:1px}
.pad .lever{
position:absolute;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;
background:radial-gradient(circle at 35% 30%,#6f6d68,#242426 70%);
border:1px solid rgba(0,0,0,.6);box-shadow:0 2px 6px rgba(0,0,0,.6);
left:50%;top:100%
}
.pad:focus-visible{outline:2px solid var(--led);outline-offset:2px}
.kbwrap{flex:1;min-width:0;background:var(--plate-lo);border-radius:5px;padding:6px;
border:1px solid rgba(0,0,0,.5);box-shadow:inset 0 3px 8px rgba(0,0,0,.55)}
.kbscroll{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}
.kb{position:relative;height:120px;min-width:660px;display:flex;touch-action:none;user-select:none}
.wkey{
position:relative;flex:1 0 0;border-radius:0 0 3px 3px;
background:linear-gradient(180deg,var(--key-w) 76%,var(--key-w-lo));
box-shadow:0 2px 2px rgba(0,0,0,.4),inset -1px 0 0 rgba(0,0,0,.22),inset 1px 0 0 rgba(255,255,255,.5);cursor:pointer
}
.wkey[data-down="1"]{background:linear-gradient(180deg,#cfcdc6 60%,#b6b4ad);box-shadow:inset 0 2px 4px rgba(0,0,0,.35)}
.bkey{
position:absolute;top:0;width:calc(var(--kw) * .62);height:63%;border-radius:0 0 3px 3px;
background:linear-gradient(180deg,#3a3a3e,var(--key-b) 70%);
box-shadow:0 3px 3px rgba(0,0,0,.5);cursor:pointer;z-index:2
}
.bkey[data-down="1"]{background:linear-gradient(180deg,#151517,#0c0c0e);box-shadow:0 1px 1px rgba(0,0,0,.6)}
.wkey .kn{
position:absolute;bottom:4px;left:0;right:0;text-align:center;font-family:'Barlow Condensed',sans-serif;
font-size:9px;color:#96948e;letter-spacing:.04em;pointer-events:none
}
/* ---------- help ---------- */
.help{max-width:1204px;margin:22px auto 0;color:var(--page-ink)}
.help h3{font-family:'Barlow Condensed',sans-serif;font-size:15px;letter-spacing:.14em;
text-transform:uppercase;margin:0 0 8px;font-weight:600}
.help p{margin:0 0 10px;max-width:68ch;line-height:1.55;font-size:14.5px;color:var(--page-dim)}
.help kbd{
font-family:'Barlow Condensed',sans-serif;background:var(--page-2);border:1px solid var(--page-dim);
border-bottom-width:2px;border-radius:3px;padding:0 5px;font-size:13px;color:var(--page-ink)
}
.help .cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px}
.help small{display:block;margin-top:14px;font-size:12.5px;color:var(--page-dim);opacity:.8}
/* ---------- value chip ---------- */
.fader{--caph:19px}
.fader .chip{
position:absolute;left:50%;top:2px;transform:translateX(-50%);
background:#0d0d0f;color:var(--ink);border:1px solid var(--rule);border-radius:3px;
padding:1px 5px;font-family:'Barlow Condensed',sans-serif;font-size:11px;letter-spacing:.05em;
white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .09s;z-index:8;
font-variant-numeric:tabular-nums;box-shadow:0 2px 6px rgba(0,0,0,.6)
}
.fader[data-chip="low"] .chip{top:auto;bottom:2px}
.fader[data-act="1"] .chip,.fader:focus-visible .chip{opacity:1}
.lbl .num{display:none}
/* ---------- key states ---------- */
.wkey[data-latched="1"]{background:linear-gradient(180deg,#ffd6c8 74%,#edb19e)}
.bkey[data-latched="1"]{background:linear-gradient(180deg,#714034,#48261e 70%)}
.wkey[data-play="1"]{background:linear-gradient(180deg,#ff8f70 70%,#e26b4d)}
.bkey[data-play="1"]{background:linear-gradient(180deg,#b84e34,#7f3120 70%)}
/* ---------- sequencer strip ---------- */
.seqwrap{flex:1 1 100%;display:flex;align-items:center;gap:8px;min-width:0}
.seqstrip{
flex:1;min-width:0;display:flex;gap:2px;overflow-x:auto;overflow-y:hidden;
background:var(--plate-lo);border:1px solid rgba(0,0,0,.5);border-radius:3px;
padding:4px;min-height:28px;align-items:center;scrollbar-width:thin
}
.seqcell{
flex:0 0 auto;min-width:23px;text-align:center;font-family:'Barlow Condensed',sans-serif;
font-size:11px;letter-spacing:.02em;color:var(--ink-dim);background:#26262a;
border-radius:2px;padding:2px 3px;cursor:pointer;font-variant-numeric:tabular-nums
}
.seqcell[data-cur="1"]{background:var(--led);color:#fff}
.seqempty{font-family:'Barlow Condensed',sans-serif;font-size:11px;letter-spacing:.06em;
color:var(--ink-dim);padding:2px 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.octval{font-family:'Barlow Condensed',sans-serif;font-size:13px;color:var(--ink);
min-width:18px;text-align:center;font-variant-numeric:tabular-nums}
/* ---------- display ---------- */
.scope{
width:184px;height:46px;border-radius:3px;background:#0b0c0d;
border:1px solid rgba(0,0,0,.55);box-shadow:inset 0 0 10px rgba(0,0,0,.9);display:block
}
.btn[disabled]{opacity:.38;cursor:default}
.btn[disabled]:active{transform:none}
@media (max-width:900px){
body{padding:10px 8px 32px}
.synth{padding:10px 10px 12px}
.fader{height:110px;width:27px}
.ctrl{width:27px}
.fader .cap{width:23px}
.brand .model{font-size:27px}
.lower{flex-direction:column}
.grip{flex:0 0 auto;flex-direction:row;align-items:center}
.grip .name{writing-mode:vertical-rl;transform:rotate(180deg)}
.pad{min-height:64px}
.readout{margin-left:0;text-align:left;width:100%}
}
@media (max-width:700px){
.panel{flex-wrap:nowrap;overflow-x:auto;justify-content:flex-start;
scroll-snap-type:x proximity;padding-bottom:6px;-webkit-overflow-scrolling:touch}
.sec{flex:0 0 auto;scroll-snap-align:center;min-width:calc(100vw - 64px)}
.row{justify-content:center;gap:14px;padding-top:4px}
.fader{height:156px;width:40px;--caph:22px}
.ctrl{width:40px}
.fader .cap{width:34px;height:22px}
.fader .chip{font-size:12px}
.lbl{font-size:11px}
.swc{min-width:62px}
.sw .opt{font-size:11px}
.kb{min-width:900px;height:130px}
.scope{width:100%;max-width:240px}
.brand{margin-right:0;width:100%}
.topbar{gap:10px}
}
</style>
</head>
<body>
<div class="wrap">
<div class="synth" id="synth">
<div class="topbar">
<div class="brand">
<div class="model">SH<span>-</span>101</div>
<div class="tag">monophonic synthesizer</div>
</div>
<div class="topgrp">
<canvas id="scope" class="scope" width="368" height="92" aria-hidden="true"></canvas>
</div>
<div class="topgrp">
<span class="cap-lbl">Patch</span>
<select class="sel" id="presets" aria-label="Patch"></select>
<button class="btn" id="patchSave">Save</button>
<button class="btn" id="patchUndo" disabled>Undo</button>
<button class="btn" id="patchDel" disabled>Delete</button>
</div>
<div class="topgrp swatches" role="group" aria-label="Case colour">
<button class="swatch grey" data-model="grey" aria-label="Grey" aria-pressed="true"></button>
<button class="swatch blue" data-model="blue" aria-label="Blue" aria-pressed="false"></button>
<button class="swatch red" data-model="red" aria-label="Red" aria-pressed="false"></button>
</div>
<button class="pwr" id="power" data-on="0"><span class="dot"></span>Power</button>
</div>
<div class="panel">
<section class="sec">
<h2>Modulator</h2>
<div class="row">
<div class="swc"><div class="sw" data-p="lfoWave" data-opts="TRI|SQR|RND|NSE" tabindex="0" role="radiogroup" aria-label="LFO waveform"></div><div class="lbl"><b>Wave</b></div></div>
<div class="ctrl"><div class="fader" data-p="lfoRate" tabindex="0" role="slider" aria-label="LFO rate"></div><div class="lbl"><b>Rate</b><span class="num"></span></div></div>
<div class="ledrow"><span class="led" id="lfoLed" aria-hidden="true"></span></div>
</div>
</section>
<section class="sec">
<h2>VCO</h2>
<div class="row">
<div class="swc"><div class="sw" data-p="vcoRange" data-opts="16'|8'|4'|2'" tabindex="0" role="radiogroup" aria-label="VCO range"></div><div class="lbl"><b>Range</b></div></div>
<div class="ctrl"><div class="fader" data-p="tune" tabindex="0" role="slider" aria-label="Tune"></div><div class="lbl"><b>Tune</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="vcoMod" tabindex="0" role="slider" aria-label="LFO pitch modulation"></div><div class="lbl"><b>Mod</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="pw" tabindex="0" role="slider" aria-label="Pulse width"></div><div class="lbl"><b>PW/PWM</b><span class="num"></span></div></div>
<div class="swc"><div class="sw" data-p="pwmMode" data-opts="LFO|MAN|ENV" tabindex="0" role="radiogroup" aria-label="PWM source"></div><div class="lbl"><b>PWM</b></div></div>
</div>
</section>
<section class="sec">
<h2>Source mixer</h2>
<div class="row">
<div class="ctrl"><div class="fader" data-p="mixPulse" tabindex="0" role="slider" aria-label="Pulse level"></div><div class="lbl"><b>Pulse</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="mixSaw" tabindex="0" role="slider" aria-label="Sawtooth level"></div><div class="lbl"><b>Saw</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="mixSub" tabindex="0" role="slider" aria-label="Sub oscillator level"></div><div class="lbl"><b>Sub</b><span class="num"></span></div></div>
<div class="swc"><div class="sw" data-p="subType" data-opts="1 OCT|2 OCT|2 PLS" tabindex="0" role="radiogroup" aria-label="Sub oscillator waveform"></div><div class="lbl"><b>Sub wave</b></div></div>
<div class="ctrl"><div class="fader" data-p="mixNoise" tabindex="0" role="slider" aria-label="Noise level"></div><div class="lbl"><b>Noise</b><span class="num"></span></div></div>
</div>
</section>
<section class="sec">
<h2>VCF</h2>
<div class="row">
<div class="ctrl"><div class="fader" data-p="vcfFreq" tabindex="0" role="slider" aria-label="Cutoff frequency"></div><div class="lbl"><b>Freq</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="vcfRes" tabindex="0" role="slider" aria-label="Resonance"></div><div class="lbl"><b>Res</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="vcfEnv" tabindex="0" role="slider" aria-label="Filter envelope amount"></div><div class="lbl"><b>Env</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="vcfMod" tabindex="0" role="slider" aria-label="Filter LFO modulation"></div><div class="lbl"><b>Mod</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="vcfKbd" tabindex="0" role="slider" aria-label="Keyboard follow"></div><div class="lbl"><b>Kybd</b><span class="num"></span></div></div>
</div>
</section>
<section class="sec">
<h2>Env</h2>
<div class="row">
<div class="ctrl"><div class="fader" data-p="envA" tabindex="0" role="slider" aria-label="Attack"></div><div class="lbl"><b>A</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="envD" tabindex="0" role="slider" aria-label="Decay"></div><div class="lbl"><b>D</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="envS" tabindex="0" role="slider" aria-label="Sustain"></div><div class="lbl"><b>S</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="envR" tabindex="0" role="slider" aria-label="Release"></div><div class="lbl"><b>R</b><span class="num"></span></div></div>
<div class="swc"><div class="sw" data-p="envTrig" data-opts="G+T|GATE|LFO" tabindex="0" role="radiogroup" aria-label="Envelope trigger"></div><div class="lbl"><b>Trig</b></div></div>
</div>
</section>
<section class="sec">
<h2>VCA</h2>
<div class="row">
<div class="swc"><div class="sw" data-p="vcaMode" data-opts="ENV|GATE" tabindex="0" role="radiogroup" aria-label="VCA mode"></div><div class="lbl"><b>Mode</b></div></div>
<div class="ctrl"><div class="fader" data-p="volume" tabindex="0" role="slider" aria-label="Volume"></div><div class="lbl"><b>Volume</b><span class="num"></span></div></div>
</div>
</section>
<section class="sec">
<h2>Controller</h2>
<div class="row">
<div class="ctrl"><div class="fader" data-p="porta" tabindex="0" role="slider" aria-label="Portamento time"></div><div class="lbl"><b>Porta</b><span class="num"></span></div></div>
<div class="swc"><div class="sw" data-p="portaMode" data-opts="AUTO|OFF|ON" tabindex="0" role="radiogroup" aria-label="Portamento mode"></div><div class="lbl"><b>Mode</b></div></div>
<div class="ctrl"><div class="fader" data-p="bendVco" tabindex="0" role="slider" aria-label="Bender pitch sensitivity"></div><div class="lbl"><b>Bend VCO</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="bendVcf" tabindex="0" role="slider" aria-label="Bender filter sensitivity"></div><div class="lbl"><b>Bend VCF</b><span class="num"></span></div></div>
<div class="ctrl"><div class="fader" data-p="gripMod" tabindex="0" role="slider" aria-label="Grip LFO modulation depth"></div><div class="lbl"><b>LFO mod</b><span class="num"></span></div></div>
</div>
</section>
</div>
<div class="transport">
<div class="tgrp">
<span class="name">Sequencer</span>
<button class="btn" id="seqRec"><span class="led"></span>Rec</button>
<button class="btn" id="seqPlay"><span class="led"></span>Play</button>
<button class="btn" id="seqRest">Rest</button>
<button class="btn" id="seqUndo">Undo step</button>
<button class="btn" id="seqClear">Clear</button>
</div>
<div class="tgrp">
<span class="name">Arpeggio</span>
<button class="btn" id="arpUp"><span class="led"></span>Up</button>
<button class="btn" id="arpUD"><span class="led"></span>U&D</button>
<button class="btn" id="arpDown"><span class="led"></span>Down</button>
</div>
<div class="tgrp">
<button class="btn" id="hold"><span class="led"></span>Hold</button>
</div>
<div class="tgrp">
<span class="name">Transpose</span>
<button class="btn" id="octDown" aria-label="Octave down">−</button>
<span class="octval" id="octVal">0</span>
<button class="btn" id="octUp" aria-label="Octave up">+</button>
</div>
<div class="readout" id="readout">Press <b>Power</b> to start</div>
<div class="seqwrap"><div class="seqstrip" id="seqstrip"></div></div>
</div>
<div class="bank">
<div class="bgrp">
<span class="name">Patterns</span>
<div class="slots" id="slots"></div>
</div>
<div class="bgrp">
<button class="btn" id="patStore">Store</button>
<button class="btn" id="patLoad">Load</button>
<button class="btn" id="patAdd">Add</button>
<button class="btn" id="patClearSlot">Erase</button>
</div>
<div class="bgrp">
<span class="name">Chain</span>
<button class="btn" id="chainPlay"><span class="led"></span>Play</button>
<button class="btn" id="chainClear">Clear</button>
<button class="btn" id="exportWav">Export audio</button>
</div>
<div class="chainwrap"><div class="chainstrip" id="chainstrip"></div></div>
</div>
<div class="lower">
<div class="grip">
<div class="name">Bender</div>
<div class="pad" id="pad" tabindex="0" role="application" aria-label="Bender: drag left and right to bend, push up for LFO modulation">
<div class="cross"><i class="h"></i><i class="v"></i></div>
<div class="lever" id="lever"></div>
</div>
</div>
<div class="kbwrap">
<div class="kbscroll"><div class="kb" id="kb"></div></div>
</div>
</div>
</div>
</div>
<div class="help">
<div class="cols">
<div>
<h3>Patterns and arrangements</h3>
<p>A pattern is a sequence plus the sound that plays it. Record a bass line, pick a slot and press <b>Store</b>; change the patch, record another, store that in the next slot. <b>Add</b> puts the selected slot on the end of the chain, so A A B A plays your first line twice, then the second, then the first again. <b>Load</b> brings a slot back for editing, and clicking a chain entry removes it.</p>
<p><b>Export audio</b> renders the whole chain offline, faster than real time, and hands you a 16-bit 44.1 kHz mono WAV. Each pattern is rendered with its own patch and tempo, and the tail of one is allowed to ring into the next.</p>
</div>
<div>
<h3>Playing</h3>
<p>Hit <b>Power</b>, then play the on-screen keys or use your computer keyboard: the <kbd>z</kbd> row is the lower octave, the <kbd>q</kbd> row the upper, with <kbd>s</kbd> <kbd>d</kbd> <kbd>f</kbd> and <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> as the black notes. <b>Transpose</b> shifts everything by an octave. Drag the bender left and right to bend pitch and filter; push it up for vibrato. A connected MIDI keyboard is picked up automatically.</p>
<p>Every fader shows its value while you drag it, and double-clicking one returns it to its default. <b>Save</b> keeps the current patch in the list; <b>Undo</b> steps back to whatever you had before you last loaded one, and then flips to Redo so you can compare the two.</p>
</div>
<div>
<h3>Arpeggio and sequencer</h3>
<p>Both run from the LFO rate slider, exactly as they do on the original — move <b>Rate</b> to change tempo. Press <b>Rec</b>, play a line one note at a time (use <b>Rest</b> for gaps), then <b>Play</b>. Steps appear in the strip as you record: <b>Undo step</b> removes the last one, and while recording you can click any step to delete it. The playhead lights up as the line runs, and pressing a key transposes it. <b>Hold</b> latches whatever you are holding down, and latched keys stay lit so you can see what the arpeggiator is working from.</p>
</div>
<div>
<h3>Where the sound comes from</h3>
<p>One oscillator with saw, pulse and a sub an octave or two below, plus noise, all mixed before a 24 dB low-pass filter. The display shows the spectrum with a red line marking where the cutoff currently sits, so you can watch the envelope sweep it. Push <b>Res</b> past about eight-tenths and the filter starts to whistle on its own — with a fast decay and plenty of <b>Env</b> that is the classic 101 kick drum.</p>
</div>
</div>
<small>An affectionate reconstruction of the 1982 Roland SH-101 in Web Audio. Not affiliated with or endorsed by Roland Corporation.</small>
</div>
<script>
(function(){
"use strict";
/* ============================ parameters ============================ */
var SW = {lfoWave:4, vcoRange:4, pwmMode:3, subType:3, envTrig:3, vcaMode:2, portaMode:3};
var DEF = {
lfoWave:0, lfoRate:0.36,
vcoRange:1, tune:0.5, vcoMod:0, pw:0, pwmMode:1,
mixPulse:0, mixSaw:0.85, mixSub:0.45, subType:0, mixNoise:0,
vcfFreq:0.46, vcfRes:0.28, vcfEnv:0.5, vcfMod:0, vcfKbd:0.5,
envA:0.02, envD:0.42, envS:0.35, envR:0.22, envTrig:0,
vcaMode:0, volume:0.78,
porta:0, portaMode:1, bendVco:0.25, bendVcf:0, gripMod:0.3
};
var P = {};
for (var k in DEF) P[k] = DEF[k];
function expMap(v,a,b){ return a*Math.pow(b/a,v); }
function clamp(v,a,b){ return v<a?a:(v>b?b:v); }
var S = {
lfoRate: function(){ return expMap(P.lfoRate,0.1,30); },
rangeMul: function(){ return [0.5,1,2,4][P.vcoRange]; },
tuneC: function(){ return (P.tune-0.5)*100; },
vcoModC: function(){ return P.vcoMod*P.vcoMod*1200; },
pwBase: function(){ return P.pwmMode===0 ? 0.5-P.pw*0.23 : 0.5-P.pw*0.46; },
pwDepth: function(){ return P.pwmMode===0 ? P.pw*0.23 : (P.pwmMode===2 ? P.pw*0.46 : 0); },
cutoff: function(){ return expMap(P.vcfFreq,18,18000); },
resQ: function(){ return P.vcfRes; },
vcfEnvC: function(){ return P.vcfEnv*7600; },
vcfModC: function(){ return P.vcfMod*P.vcfMod*4800; },
atk: function(){ return expMap(P.envA,0.0015,4); },
dec: function(){ return expMap(P.envD,0.003,10); },
rel: function(){ return expMap(P.envR,0.003,10); },
porta: function(){ return P.porta*P.porta*5; },
bendVcoC: function(){ return P.bendVco*1200; },
bendVcfC: function(){ return P.bendVcf*4800; },
vol: function(){ return P.volume*P.volume; }
};
function fmt(p){
var v=P[p];
switch(p){
case 'lfoRate': var r=S.lfoRate(); return (r<10? r.toFixed(2): r.toFixed(1))+'Hz';
case 'vcfFreq': var f=S.cutoff(); return f>=1000 ? (f/1000).toFixed(1)+'k' : Math.round(f)+'';
case 'tune': var c=S.tuneC(); return (c>0?'+':'')+c.toFixed(0)+'\u00A2';
case 'envA': return tstr(S.atk());
case 'envD': return tstr(S.dec());
case 'envR': return tstr(S.rel());
case 'porta': return S.porta()<0.005?'off':tstr(S.porta());
case 'pw': return P.pwmMode===1 ? Math.round((0.5-P.pw*0.46)*100)+'%' : Math.round(P.pw*100)+'';
default: return Math.round(v*100)+'';
}
}
function tstr(t){ return t<1 ? Math.round(t*1000)+'ms' : t.toFixed(t<10?2:1)+'s'; }
/* ============================ audio engine ============================ */
var ctx=null, A={}, powered=false, currentFreq=261.63, lastNote=60;
function gain(v){ var g=ctx.createGain(); g.gain.value=v; return g; }
function makeNoise(){
var len=Math.floor(ctx.sampleRate*2), b=ctx.createBuffer(1,len,ctx.sampleRate), d=b.getChannelData(0);
for(var i=0;i<len;i++) d[i]=Math.random()*2-1;
var s=ctx.createBufferSource(); s.buffer=b; s.loop=true; return s;
}
function pulseWave(duty){
var n=48, re=new Float32Array(n+1), im=new Float32Array(n+1);
for(var i=1;i<=n;i++) re[i]=(2/(i*Math.PI))*Math.sin(i*Math.PI*duty);
return ctx.createPeriodicWave(re,im,{disableNormalization:false});
}
function driveCurve(a){
var n=2048, c=new Float32Array(n), d=Math.tanh(a);
for(var i=0;i<n;i++){ var x=i*2/n-1; c[i]=Math.tanh(x*a)/d; }
return c;
}
function hold(par,t){
if(par.cancelAndHoldAtTime) par.cancelAndHoldAtTime(t);
else { par.cancelScheduledValues(t); par.setValueAtTime(par.value,t); }
}
function boot(){
if(ctx) return;
ctx = new (window.AudioContext||window.webkitAudioContext)();
buildGraph();
setPitch(noteFreq(60),ctx.currentTime,0);
applyAll();
nextTick = ctx.currentTime + 0.05;
if(!clockTimer) clockTimer = setInterval(scheduler,25);
}
var instantParams=false, rendering=false;
function buildGraph(){
A.master=gain(0);
A.comp=ctx.createDynamicsCompressor();
A.comp.threshold.value=-9; A.comp.knee.value=16; A.comp.ratio.value=6;
A.comp.attack.value=0.004; A.comp.release.value=0.22;
A.master.connect(A.comp); A.comp.connect(ctx.destination);
A.an=ctx.createAnalyser(); A.an.fftSize=2048; A.an.smoothingTimeConstant=0.62;
A.comp.connect(A.an);
A.vca=gain(0); A.vca.connect(A.master);
A.f1=ctx.createBiquadFilter(); A.f1.type='lowpass';
A.f2=ctx.createBiquadFilter(); A.f2.type='lowpass';
A.drive=ctx.createWaveShaper(); A.drive.curve=driveCurve(1.7); A.drive.oversample='2x';
A.dc=ctx.createBiquadFilter(); A.dc.type='highpass'; A.dc.frequency.value=14; A.dc.Q.value=0.5;
A.mix=gain(0.5);
A.mix.connect(A.dc); A.dc.connect(A.drive); A.drive.connect(A.f1);
A.f1.connect(A.f2); A.f2.connect(A.vca);
/* oscillator section */
A.saw=ctx.createOscillator(); A.saw.type='sawtooth';
A.psaw=ctx.createOscillator(); A.psaw.type='sawtooth';
A.sub=ctx.createOscillator(); A.sub.type='square';
A.noise=makeNoise();
A.subPulse=pulseWave(0.25);
A.sawG=gain(0); A.saw.connect(A.sawG); A.sawG.connect(A.mix);
A.pwDelay=ctx.createDelay(0.15);
A.pinv=gain(-1);
A.pulseSum=gain(0.5);
A.psaw.connect(A.pwDelay); A.pwDelay.connect(A.pinv);
A.psaw.connect(A.pulseSum); A.pinv.connect(A.pulseSum);
A.pulseG=gain(0); A.pulseSum.connect(A.pulseG); A.pulseG.connect(A.mix);
A.subG=gain(0); A.sub.connect(A.subG); A.subG.connect(A.mix);
A.noiseG=gain(0); A.noise.connect(A.noiseG); A.noiseG.connect(A.mix);
/* filter self-oscillation */
A.sine=ctx.createOscillator(); A.sine.type='sine';
A.sineG=gain(0); A.sine.connect(A.sineG); A.sineG.connect(A.vca);
/* envelope + gate */
A.env=ctx.createConstantSource(); A.env.offset.value=0;
A.gate=ctx.createConstantSource(); A.gate.offset.value=0;
A.envVca=gain(1); A.env.connect(A.envVca); A.envVca.connect(A.vca.gain);
A.gateVca=gain(0); A.gate.connect(A.gateVca); A.gateVca.connect(A.vca.gain);
/* LFO */
A.lfoOsc=ctx.createOscillator(); A.lfoOsc.type='triangle'; A.lfoOsc.frequency.value=S.lfoRate();
A.lfoRand=ctx.createConstantSource(); A.lfoRand.offset.value=0;
A.lfoNz=makeNoise();
A.lfoNzLP=ctx.createBiquadFilter(); A.lfoNzLP.type='lowpass'; A.lfoNzLP.frequency.value=28; A.lfoNzLP.Q.value=0.6;
A.lfoNz.connect(A.lfoNzLP);
A.lfoBus=gain(1);
A.gOsc=gain(1); A.gRand=gain(0); A.gNz=gain(0);
A.lfoOsc.connect(A.gOsc); A.gOsc.connect(A.lfoBus);
A.lfoRand.connect(A.gRand); A.gRand.connect(A.lfoBus);
A.lfoNzLP.connect(A.gNz); A.gNz.connect(A.lfoBus);
/* bender */
A.bend=ctx.createConstantSource(); A.bend.offset.value=0;
A.bendP=gain(0); A.bend.connect(A.bendP);
A.bendF=gain(0); A.bend.connect(A.bendF);
/* modulation routing */
A.lfoPitch=gain(0); A.lfoBus.connect(A.lfoPitch);
A.lfoVcf=gain(0); A.lfoBus.connect(A.lfoVcf);
A.lfoPwm=gain(0); A.lfoBus.connect(A.lfoPwm);
A.envVcf=gain(0); A.env.connect(A.envVcf);
A.envPwm=gain(0); A.env.connect(A.envPwm);
[A.lfoPitch,A.bendP].forEach(function(n){
n.connect(A.saw.detune); n.connect(A.psaw.detune); n.connect(A.sub.detune);
});
[A.lfoVcf,A.envVcf,A.bendF].forEach(function(n){
n.connect(A.f1.detune); n.connect(A.f2.detune); n.connect(A.sine.detune);
});
A.lfoPwm.connect(A.pwDelay.delayTime);
A.envPwm.connect(A.pwDelay.delayTime);
[A.saw,A.psaw,A.sub,A.noise,A.sine,A.lfoOsc,A.lfoRand,A.lfoNz,A.env,A.gate,A.bend].forEach(function(n){ n.start(0); });
}
var kbOct=0;
function noteFreq(n){ return 440*Math.pow(2,(n+kbOct*12-69)/12)*S.rangeMul(); }
function setPitch(f,t,glide){
currentFreq=f;
var tc = glide>0 ? glide/3 : 0;
var sf = f*[0.5,0.25,0.25][P.subType];
function go(par,v){
if(tc>0) par.setTargetAtTime(v,t,tc); else par.setValueAtTime(v,t);
}
go(A.saw.frequency,f); go(A.psaw.frequency,f); go(A.sub.frequency,sf);
var base=S.pwBase()/f;
if(tc>0) A.pwDelay.delayTime.setTargetAtTime(base,t,tc);
else A.pwDelay.delayTime.setValueAtTime(base,t);
A.lfoPwm.gain.value = (P.pwmMode===0 ? S.pwDepth()/f : 0);
A.envPwm.gain.value = (P.pwmMode===2 ? S.pwDepth()/f : 0);
}
function filterBase(){
var f=S.cutoff()*Math.pow(2,P.vcfKbd*(lastNote-60)/12);
return clamp(f,10,ctx.sampleRate*0.45);
}
function applyAll(){
if(!ctx) return;
var t=ctx.currentTime, tc=0.012;
function st(par,v,c){ if(instantParams) par.setValueAtTime(v,t); else par.setTargetAtTime(v,t,c||tc); }
/* LFO */
A.lfoOsc.type = P.lfoWave===1 ? 'square' : 'triangle';
st(A.lfoOsc.frequency,S.lfoRate(),0.02);
A.gOsc.gain.value = (P.lfoWave===0||P.lfoWave===1) ? 1 : 0;
A.gRand.gain.value = P.lfoWave===2 ? 1 : 0;
A.gNz.gain.value = P.lfoWave===3 ? 30 : 0;
/* VCO */
var tn=S.tuneC();
A.saw.detune.value=tn; A.psaw.detune.value=tn; A.sub.detune.value=tn;
st(A.lfoPitch.gain, S.vcoModC() + padY*P.gripMod*1200, 0.03);
A.sub.frequency.setTargetAtTime(currentFreq*[0.5,0.25,0.25][P.subType],t,0.01);
if(A._sub!==P.subType){
A._sub=P.subType;
if(P.subType===2) A.sub.setPeriodicWave(A.subPulse); else A.sub.type='square';
}
var base=S.pwBase()/currentFreq;
st(A.pwDelay.delayTime,base,0.01);
A.lfoPwm.gain.value = (P.pwmMode===0 ? S.pwDepth()/currentFreq : 0);
A.envPwm.gain.value = (P.pwmMode===2 ? S.pwDepth()/currentFreq : 0);
/* mixer */
st(A.pulseG.gain,P.mixPulse*0.9);
st(A.sawG.gain, P.mixSaw*0.9);
st(A.subG.gain, P.mixSub*0.9);
st(A.noiseG.gain,P.mixNoise*0.55);
/* filter */
var r=P.vcfRes;
st(A.f1.frequency,filterBase(),0.01);
st(A.f2.frequency,filterBase(),0.01);
st(A.sine.frequency,filterBase(),0.01);
A.f1.Q.value=-3+r*9;
A.f2.Q.value=-3+r*26;
var so=Math.max(0,(r-0.78)/0.22);
st(A.sineG.gain,so*so*0.45);
st(A.mix.gain,0.5*(1-0.28*r));
st(A.envVcf.gain,S.vcfEnvC(),0.02);
st(A.lfoVcf.gain,S.vcfModC(),0.02);
st(A.bendP.gain,S.bendVcoC(),0.02);
st(A.bendF.gain,S.bendVcfC(),0.02);
/* VCA */
A.envVca.gain.value = P.vcaMode===0?1:0;
A.gateVca.gain.value = P.vcaMode===1?1:0;
st(A.master.gain, powered ? S.vol() : 0, 0.03);
}
/* ---- envelope ---- */
function envAttack(t){
var p=A.env.offset, a=S.atk(), d=S.dec(), s=P.envS;
hold(p,t);
p.linearRampToValueAtTime(1,t+a);
p.setTargetAtTime(s,t+a,Math.max(d,0.002)/3.2);
}
function envRelease(t){
var p=A.env.offset;
hold(p,t);
p.setTargetAtTime(0,t,Math.max(S.rel(),0.002)/3.2);
}
function gateOn(t){ var p=A.gate.offset; hold(p,t); p.linearRampToValueAtTime(1,t+0.003); }
function gateOff(t){ var p=A.gate.offset; hold(p,t); p.linearRampToValueAtTime(0,t+0.004); }
/* ============================ voice / clock ============================ */
var held=[], latched=[], holdOn=false, sounding=false;
var arpMode=0, arpPos=0;
var seqMode='idle', seq=[], seqPos=0, seqTrans=0;
var padX=0, padY=0;
var nextTick=0, clockTimer=null;
function activeSet(){ return holdOn ? latched : held; }
function arpRunning(){ return arpMode>0; }
function priorityNote(list){
if(!list.length) return null;
if(P.envTrig===1) return Math.min.apply(null,list);
return list[list.length-1];
}
function updFilterFreq(t){
var f=filterBase();
A.f1.frequency.setValueAtTime(f,t);
A.f2.frequency.setValueAtTime(f,t);
A.sine.frequency.setValueAtTime(f,t);
}
function updateVoice(){
var list=activeSet(), t=ctx.currentTime;
if(!list.length){
if(sounding){ envRelease(t); gateOff(t); sounding=false; }
return;
}
var n=priorityNote(list), was=sounding;
var glide=(P.portaMode===2||(P.portaMode===0&&was))?S.porta():0;
lastNote=n;
setPitch(noteFreq(n),t,glide);
updFilterFreq(t);
gateOn(t);
if(P.envTrig===0 || !was) { if(P.envTrig!==2 || !was) envAttack(t); }
sounding=true;
}
function repaintKeys(){
var all=document.querySelectorAll('[data-midi]');
for(var i=0;i<all.length;i++){
var m=+all[i].dataset.midi;
all[i].dataset.down=held.indexOf(m)>=0?'1':'0';
all[i].dataset.latched=(holdOn&&latched.indexOf(m)>=0)?'1':'0';
}
}
function flash(n,delay,dur){
if(rendering) return;
var e=document.querySelector('[data-midi="'+n+'"]'); if(!e) return;
setTimeout(function(){
e.dataset.play='1';
setTimeout(function(){ e.dataset.play='0'; }, dur||120);
}, Math.max(delay||0,0));
}
function playStep(n,t,step){
lastNote=n;
flash(n,(t-ctx.currentTime)*1000,Math.min(Math.max(step*420,70),200));
setPitch(noteFreq(n),t,0);
updFilterFreq(t);
envAttack(t); gateOn(t);
var off=t+Math.max(step*0.5,0.02);
envRelease(off); gateOff(off);
}
function noteOn(n){
if(!ctx) return;
if(seqMode==='rec'){
if(seq.length<100){ seq.push({n:n}); blip(n); renderSeq(); save(); }
flash(n,0,150); updButtons(); readout(); return;
}
if(seqMode==='play'){
var root=seqRoot(); if(root!==null) seqTrans=n-root;
flash(n,0,150); readout(); return;
}
if(holdOn && held.length===0) latched=[];
if(held.indexOf(n)<0) held.push(n);
if(holdOn && latched.indexOf(n)<0) latched.push(n);
repaintKeys();
if(!arpRunning()) updateVoice();
readout();
}
function noteOff(n){
if(!ctx) return;
if(seqMode==='rec'||seqMode==='play') return;
var i=held.indexOf(n); if(i>=0) held.splice(i,1);
repaintKeys();
if(!arpRunning() && !holdOn) updateVoice();
if(!arpRunning() && holdOn && latched.length===0) updateVoice();
readout();
}
function blip(n){
var t=ctx.currentTime;
lastNote=n; setPitch(noteFreq(n),t,0); updFilterFreq(t);
envAttack(t); gateOn(t);
envRelease(t+0.14); gateOff(t+0.14);
}
function seqRoot(){
for(var i=0;i<seq.length;i++) if(!seq[i].rest) return seq[i].n;
return null;
}
function panic(){
held=[]; latched=[];
var all=document.querySelectorAll('[data-midi]');
for(var i=0;i<all.length;i++){ all[i].dataset.down='0'; all[i].dataset.latched='0'; all[i].dataset.play='0'; }
if(ctx){ var t=ctx.currentTime; envRelease(t); gateOff(t); }
sounding=false;
}
function releaseVoice(){
if(ctx){ var t=ctx.currentTime; envRelease(t); gateOff(t); }
sounding=false;
}
function scheduler(){
if(!ctx||!powered) return;
var guard=0;
if(nextTick < ctx.currentTime) nextTick=ctx.currentTime;
var ahead = seqMode==='chain' ? 0.06 : 0.12;
while(nextTick < ctx.currentTime+ahead && guard++<48){
doTick(nextTick);
nextTick += 1/Math.max(S.lfoRate(),0.08);
}
}
function doTick(t){
if(seqMode==='chain' && chain.length){ chainTick(t); return; }
if(P.lfoWave===2) A.lfoRand.offset.setValueAtTime(Math.random()*2-1,t);
var step=1/Math.max(S.lfoRate(),0.08);
if(seqMode==='play' && seq.length){
if(seqPos>=seq.length) seqPos=0;
var idx=seqPos, s=seq[seqPos]; seqPos++;
markStepAt(idx,t);
if(s.rest){ envRelease(t); gateOff(t); }
else playStep(s.n+seqTrans,t,step);
return;
}
var list=activeSet();
if(arpMode>0 && list.length){
var order=arpOrder(list);
if(arpPos>=order.length) arpPos=0;
playStep(order[arpPos],t,step);
arpPos=(arpPos+1)%order.length;
return;
}
if(P.envTrig===2 && sounding) envAttack(t);
}
function chainTick(t){
var guard=0;
while(guard++<chain.length+1){
var pat=PAT[chain[chainPos]];
if(pat && pat.seq.length) break;
chainPos=(chainPos+1)%chain.length; chainStep=0;
}
var pat2=PAT[chain[chainPos]];
if(!pat2 || !pat2.seq.length) return;
if(chainStep===0){ applyPatchLive(pat2.p); markChain(chainPos,t); }
if(P.lfoWave===2) A.lfoRand.offset.setValueAtTime(Math.random()*2-1,t);
var step=1/Math.max(S.lfoRate(),0.08);
var s=pat2.seq[chainStep];
if(s.rest){ envRelease(t); gateOff(t); } else playStep(s.n,t,step);
chainStep++;
if(chainStep>=pat2.seq.length){ chainStep=0; chainPos=(chainPos+1)%chain.length; }
}
function arpOrder(list){
var up=list.slice().sort(function(a,b){return a-b;});
if(arpMode===1) return up;
if(arpMode===3) return up.slice().reverse();
if(up.length<3) return up.concat(up.slice().reverse().slice(1,-1));
return up.concat(up.slice().reverse().slice(1,-1));
}
/* ============================ UI ============================ */
var $=function(s){return document.querySelector(s);};
var $$=function(s){return Array.prototype.slice.call(document.querySelectorAll(s));};
var userOff=false;
function ensure(){
if(userOff){ if(ctx && ctx.state==='suspended') ctx.resume(); return; }
if(!powered) setPower(true);
else if(ctx && ctx.state==='suspended') ctx.resume();
}
function setPower(on){
powered=on; userOff=!on;
if(on){ boot(); if(ctx.state==='suspended') ctx.resume(); } else { panic(); }
$('#power').dataset.on=on?'1':'0';
applyAll(); readout();
}
/* ---- faders ---- */
function faderUI(p){
var el=document.querySelector('.fader[data-p="'+p+'"]'); if(!el) return;
var v=P[p], cap=el.querySelector('.cap');
cap.style.top='calc('+((1-v)*100).toFixed(2)+'% - '+(1-v).toFixed(3)+' * var(--caph))';
el.dataset.chip = v>0.5 ? 'low' : 'high';
el.setAttribute('aria-valuenow',Math.round(v*100));
el.setAttribute('aria-valuetext',fmt(p));
var chip=el.querySelector('.chip'); if(chip) chip.textContent=fmt(p);
}
function setParam(p,v){ P[p]=v; faderUI(p); applyAll(); save(); }
$$('.fader').forEach(function(el){
var p=el.dataset.p;
el.innerHTML='<div class="slot"></div><div class="ticks"></div><div class="cap"></div><div class="chip"></div>';
var tk=el.querySelector('.ticks');
for(var i=0;i<=10;i++){ var s=document.createElement('i'); s.style.top=(i*10)+'%'; if(i%5===0) s.className='maj'; tk.appendChild(s); }
el.setAttribute('aria-valuemin','0'); el.setAttribute('aria-valuemax','100');
var drag=false, sy=0, sv=0;
el.addEventListener('pointerdown',function(e){
ensure(); drag=true; sy=e.clientY; sv=P[p]; el.dataset.act='1';
try{el.setPointerCapture(e.pointerId);}catch(_){}
e.preventDefault();
});
el.addEventListener('pointermove',function(e){
if(!drag) return;
var cap=el.querySelector('.cap');
var usable=Math.max(el.clientHeight-(cap&&cap.offsetHeight||19),1);
setParam(p,clamp(sv+(sy-e.clientY)/usable,0,1));
});
function end(e){ if(drag){ drag=false; el.dataset.act='0'; try{el.releasePointerCapture(e.pointerId);}catch(_){} } }
el.addEventListener('pointerup',end); el.addEventListener('pointercancel',end);
el.addEventListener('dblclick',function(){ setParam(p,DEF[p]); });
el.addEventListener('keydown',function(e){
var d=e.shiftKey?0.005:0.02, v=P[p];
if(e.key==='ArrowUp'||e.key==='ArrowRight') v+=d;
else if(e.key==='ArrowDown'||e.key==='ArrowLeft') v-=d;
else if(e.key==='Home') v=1; else if(e.key==='End') v=0;
else return;
e.preventDefault(); ensure(); setParam(p,clamp(v,0,1));
});
});
/* ---- switches ---- */
function switchUI(p){
var el=document.querySelector('.sw[data-p="'+p+'"]'); if(!el) return;
var i=P[p];
el.querySelector('.lever').style.top=(i*19)+'px';
el.querySelectorAll('.opt').forEach(function(b,j){ b.setAttribute('aria-checked',j===i?'true':'false'); });
}
function setSwitch(p,i){
P[p]=i; switchUI(p);
if(ctx && (p==='vcoRange'||p==='subType')) setPitch(noteFreq(lastNote),ctx.currentTime,0);
applyAll(); save(); readout();
['pw','lfoRate','vcfFreq'].forEach(faderUI);
}
$$('.sw').forEach(function(el){
var p=el.dataset.p, opts=el.dataset.opts.split('|');
var rail=document.createElement('div'); rail.className='rail';
var lev=document.createElement('div'); lev.className='lever'; rail.appendChild(lev);
var box=document.createElement('div'); box.className='opts';
opts.forEach(function(o,i){
var b=document.createElement('button'); b.type='button'; b.className='opt';
b.textContent=o; b.setAttribute('role','radio'); b.tabIndex=-1;
b.addEventListener('click',function(){ ensure(); setSwitch(p,i); });
box.appendChild(b);
});
el.appendChild(rail); el.appendChild(box);
el.addEventListener('keydown',function(e){
var i=P[p];
if(e.key==='ArrowDown'||e.key==='ArrowRight') i=Math.min(i+1,opts.length-1);
else if(e.key==='ArrowUp'||e.key==='ArrowLeft') i=Math.max(i-1,0);
else return;
e.preventDefault(); ensure(); setSwitch(p,i);
});
});
/* ---- keyboard ---- */
var KB_LO=53, KB_HI=84, WHITE=[0,2,4,5,7,9,11];
var KEYMAP={}, KEYNAME={};
(function(){
var lo=[['z',0],['s',1],['x',2],['d',3],['c',4],['f',5],['v',6],['b',7],['h',8],['n',9],['j',10],['m',11],[',',12],['l',13],['.',14],[';',15],['/',16]];
var up=[['q',0],['2',1],['w',2],['3',3],['e',4],['4',5],['r',6],['t',7],['6',8],['y',9],['7',10],['u',11],['i',12],['9',13],['o',14],['0',15],['p',16]];
lo.forEach(function(a){ KEYMAP[a[0]]=53+a[1]; if(!KEYNAME[53+a[1]]) KEYNAME[53+a[1]]=a[0]; });
up.forEach(function(a){ KEYMAP[a[0]]=65+a[1]; if(!KEYNAME[65+a[1]]) KEYNAME[65+a[1]]=a[0]; });
})();
(function buildKeys(){
var kb=$('#kb'), whites=[], n;
for(n=KB_LO;n<=KB_HI;n++) if(WHITE.indexOf(n%12)>=0) whites.push(n);
kb.style.setProperty('--kw',(100/whites.length)+'%');
var wi=-1, blacks=[];
for(n=KB_LO;n<=KB_HI;n++){
if(WHITE.indexOf(n%12)>=0){
wi++;
var d=document.createElement('div');
d.className='wkey'; d.dataset.midi=n; d.dataset.down='0';
if(KEYNAME[n]){ var s=document.createElement('span'); s.className='kn'; s.textContent=KEYNAME[n]; d.appendChild(s); }
kb.appendChild(d);
} else {
var b=document.createElement('div');
b.className='bkey'; b.dataset.midi=n; b.dataset.down='0';
b.style.left='calc('+(wi+1)+' * var(--kw) - var(--kw) * 0.31)';
b.style.width='calc(var(--kw) * 0.62)';
blacks.push(b);
}
}
blacks.forEach(function(b){ kb.appendChild(b); });
})();
function paintKey(n,down){
var e=document.querySelector('[data-midi="'+n+'"]');
if(e) e.dataset.down=down?'1':'0';
}
function midiFrom(el){
if(!el) return null;
var k=el.closest ? el.closest('[data-midi]') : null;
return k ? parseInt(k.dataset.midi,10) : null;
}
(function(){
var kb=$('#kb'), down=false, cur=null;
kb.addEventListener('pointerdown',function(e){
ensure(); var m=midiFrom(e.target); if(m===null) return;
down=true; cur=m; noteOn(m);
try{kb.setPointerCapture(e.pointerId);}catch(_){}
e.preventDefault();
});
kb.addEventListener('pointermove',function(e){
if(!down) return;
var m=midiFrom(document.elementFromPoint(e.clientX,e.clientY));
if(m!==null && m!==cur){ if(cur!==null) noteOff(cur); cur=m; noteOn(m); }
});
function up(){ if(down){ if(cur!==null) noteOff(cur); cur=null; down=false; } }
window.addEventListener('pointerup',up);
window.addEventListener('pointercancel',up);
})();
var pressed={};
window.addEventListener('keydown',function(e){
var t=e.target;
if(t && (t.tagName==='SELECT'||t.tagName==='INPUT'||t.tagName==='TEXTAREA')) return;
if(e.metaKey||e.ctrlKey||e.altKey) return;
var k=e.key.toLowerCase();
if(!(k in KEYMAP) || pressed[k]) return;
pressed[k]=true; ensure(); noteOn(KEYMAP[k]); e.preventDefault();
});
window.addEventListener('keyup',function(e){
var k=e.key.toLowerCase();
if(pressed[k]){ delete pressed[k]; noteOff(KEYMAP[k]); }
});
window.addEventListener('blur',function(){ pressed={}; panic(); });
/* ---- bender ---- */
(function(){
var pad=$('#pad'), lever=$('#lever'), dragging=false;
function place(){
lever.style.left=((padX*0.5+0.5)*100)+'%';
lever.style.top=((1-padY)*100)+'%';
}
function from(e){
var r=pad.getBoundingClientRect();
padX=clamp(((e.clientX-r.left)/r.width)*2-1,-1,1);
padY=clamp(1-(e.clientY-r.top)/r.height,0,1);
padApply(); place();
}
pad.addEventListener('pointerdown',function(e){ ensure(); dragging=true; try{pad.setPointerCapture(e.pointerId);}catch(_){} from(e); e.preventDefault(); });
pad.addEventListener('pointermove',function(e){ if(dragging) from(e); });
function end(){ dragging=false; }
pad.addEventListener('pointerup',end); pad.addEventListener('pointercancel',end);
pad.addEventListener('keydown',function(e){
if(e.key==='ArrowLeft') padX=clamp(padX-0.2,-1,1);
else if(e.key==='ArrowRight') padX=clamp(padX+0.2,-1,1);
else if(e.key==='ArrowUp') padY=clamp(padY+0.2,0,1);
else if(e.key==='ArrowDown') padY=clamp(padY-0.2,0,1);
else return;
e.preventDefault(); ensure(); padApply(); place();
});
window.__padSpring=function(){
if(dragging) return;
if(Math.abs(padX)>0.0005||padY>0.0005){
padX*=0.8; padY*=0.8;
if(Math.abs(padX)<0.0005) padX=0;
if(padY<0.0005) padY=0;
padApply(); place();
}
};
place();
})();
function padApply(){
if(!ctx) return;
var t=ctx.currentTime;
A.bend.offset.setTargetAtTime(padX,t,0.01);
A.lfoPitch.gain.setTargetAtTime(S.vcoModC()+padY*P.gripMod*1200,t,0.02);
}
/* ---- pattern bank ---- */
var PAT=[null,null,null,null,null,null,null,null];
var patSel=0, chain=[], chainPos=0, chainStep=0, chainSaved=null;
var exporting=false, exportMsg='';
function slotLetter(i){ return String.fromCharCode(65+i); }
function clonePat(a){ return a.map(function(x){ return x.rest?{rest:true}:{n:x.n}; }); }
function renderSlots(){
var el=$('#slots'); if(!el) return;
if(!el.children.length){
for(var i=0;i<8;i++){
var b=document.createElement('button');
b.type='button'; b.className='slot'; b.dataset.i=i;
b.innerHTML='<span class="ltr">'+slotLetter(i)+'</span><span class="ct"></span>';
b.addEventListener('click',(function(k){ return function(){ patSel=k; renderSlots(); updButtons(); readout(); }; })(i));
el.appendChild(b);
}
}
for(var j=0;j<8;j++){
var c=el.children[j], pt=PAT[j];
c.dataset.full=pt?'1':'0';
c.dataset.sel=(j===patSel)?'1':'0';
c.querySelector('.ct').textContent=pt?pt.seq.length:'\u2013';
c.setAttribute('aria-label','Pattern '+slotLetter(j)+(pt?', '+pt.seq.length+' steps':', empty'));
}
}
function renderChain(){
var el=$('#chainstrip'); if(!el) return;
if(!chain.length){
el.innerHTML='<span class="seqempty">Chain is empty. Select a pattern and press Add to build an arrangement.</span>';
return;
}
var h='';
for(var i=0;i<chain.length;i++) h+='<span class="chaincell" data-i="'+i+'">'+slotLetter(chain[i])+'</span>';
el.innerHTML=h;
}
function markChain(pos,t){
var d=(t-ctx.currentTime)*1000;
setTimeout(function(){
if(seqMode!=='chain') return;
var el=$('#chainstrip'); if(!el) return;
for(var j=0;j<el.children.length;j++) el.children[j].dataset.cur=(j===pos?'1':'0');
var c=el.children[pos];
if(c && el.scrollWidth>el.clientWidth) el.scrollLeft=Math.max(0,c.offsetLeft-el.clientWidth/2);
readout();
}, Math.max(d,0));
}
function clearChainMarks(){
var el=$('#chainstrip'); if(!el) return;
for(var j=0;j<el.children.length;j++) el.children[j].dataset.cur='0';
}
function applyPatchLive(pp){
for(var k in DEF) if(typeof pp[k]==='number') P[k]=pp[k];
for(var k2 in P){ if(SW[k2]!==undefined) switchUI(k2); else faderUI(k2); }
applyAll();
}
function stopChain(){
if(seqMode==='chain'){
seqMode='idle';
if(chainSaved){ applySnapshot(chainSaved); chainSaved=null; }
clearChainMarks();
}
}
/* ---- sequencer strip ---- */
var NOTE_NAMES=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
function noteName(n){ return NOTE_NAMES[((n%12)+12)%12]+(Math.floor(n/12)-1); }
function renderSeq(){
var el=$('#seqstrip'); if(!el) return;
if(!seq.length){
el.innerHTML='<span class="seqempty">Empty. Press Rec, then play a line one note at a time \u2014 Rest adds a gap.</span>';
return;
}
var h='';
for(var i=0;i<seq.length;i++) h+='<span class="seqcell" data-i="'+i+'">'+(seq[i].rest?'\u00B7':noteName(seq[i].n))+'</span>';
el.innerHTML=h;
}
function markStep(i){
var el=$('#seqstrip'); if(!el||!el.children.length) return;
for(var j=0;j<el.children.length;j++) el.children[j].dataset.cur = (j===i?'1':'0');
var c=el.children[i];
if(c && el.scrollWidth>el.clientWidth) el.scrollLeft=Math.max(0,c.offsetLeft-el.clientWidth/2);
}
function clearMarks(){
var el=$('#seqstrip'); if(!el) return;
for(var j=0;j<el.children.length;j++) el.children[j].dataset.cur='0';
}
function markStepAt(i,t){
var d=(t-ctx.currentTime)*1000;
setTimeout(function(){ if(seqMode==='play') markStep(i); }, Math.max(d,0));
}
/* ---- transport ---- */
function updButtons(){
function led(id,on){ var b=$(id); if(b){ var l=b.querySelector('.led'); if(l) l.dataset.on=on?'1':'0'; } }
led('#seqRec',seqMode==='rec'); led('#seqPlay',seqMode==='play');
led('#arpUp',arpMode===1); led('#arpUD',arpMode===2); led('#arpDown',arpMode===3);
led('#hold',holdOn);
$('#seqRest').disabled = seqMode!=='rec';
$('#seqUndo').disabled = !(seqMode==='rec' && seq.length>0);
$('#seqPlay').disabled = !seq.length && seqMode!=='play';
$('#seqClear').disabled = !seq.length;
led('#chainPlay',seqMode==='chain');
var b;
if((b=$('#patStore'))) b.disabled = !seq.length;
if((b=$('#patLoad'))) b.disabled = !PAT[patSel];
if((b=$('#patAdd'))) b.disabled = !PAT[patSel];
if((b=$('#patClearSlot'))) b.disabled = !PAT[patSel];
if((b=$('#chainPlay'))) b.disabled = !chain.length;
if((b=$('#chainClear'))) b.disabled = !chain.length;
if((b=$('#exportWav'))) b.disabled = exporting || !(chain.length || PAT[patSel]);
}
function octUI(){ var e=$('#octVal'); if(e) e.textContent=(kbOct>0?'+':'')+kbOct; }
function setOct(v){
kbOct=clamp(v,-2,2); octUI(); save();
if(ctx && sounding){ var t=ctx.currentTime; setPitch(noteFreq(lastNote),t,0); updFilterFreq(t); }
}
$('#seqRec').addEventListener('click',function(){
ensure(); stopChain();
if(seqMode==='rec'){ seqMode='idle'; }
else { seqMode='rec'; seq=[]; seqPos=0; seqTrans=0; panic(); }
clearMarks(); renderSeq(); updButtons(); readout(); save();
});
$('#seqPlay').addEventListener('click',function(){
ensure(); stopChain();
if(seqMode==='play'){ seqMode='idle'; panic(); clearMarks(); }
else if(seq.length){ seqMode='play'; seqPos=0; seqTrans=0; arpMode=0; panic(); nextTick=ctx.currentTime+0.02; }
updButtons(); readout();
});
$('#seqRest').addEventListener('click',function(){
if(seqMode==='rec' && seq.length<100){ seq.push({rest:true}); renderSeq(); updButtons(); readout(); save(); }
});
$('#seqUndo').addEventListener('click',function(){
if(seqMode==='rec' && seq.length){ seq.pop(); renderSeq(); updButtons(); readout(); save(); }
});
$('#seqClear').addEventListener('click',function(){
seq=[]; seqMode='idle'; seqTrans=0; panic(); renderSeq(); updButtons(); readout(); save();
});
$('#seqstrip').addEventListener('click',function(e){
var c=e.target.closest ? e.target.closest('.seqcell') : null;
if(!c || seqMode!=='rec') return;
seq.splice(+c.dataset.i,1); renderSeq(); updButtons(); readout(); save();
});
$('#octDown').addEventListener('click',function(){ ensure(); setOct(kbOct-1); });
$('#octUp').addEventListener('click',function(){ ensure(); setOct(kbOct+1); });
[['#arpUp',1],['#arpUD',2],['#arpDown',3]].forEach(function(a){
$(a[0]).addEventListener('click',function(){
ensure(); stopChain();
arpMode = arpMode===a[1] ? 0 : a[1];
arpPos=0;
if(seqMode==='play'){ seqMode='idle'; clearMarks(); }
releaseVoice();
if(!arpRunning()) updateVoice();
repaintKeys(); updButtons(); readout();
});
});
$('#hold').addEventListener('click',function(){
ensure();
holdOn=!holdOn;
if(holdOn){ latched=held.slice(); }
else { latched=[]; if(!arpRunning()) updateVoice(); }
repaintKeys(); updButtons(); readout();
});
$('#patStore').addEventListener('click',function(){
if(!seq.length) return;
PAT[patSel]={name:slotLetter(patSel),seq:clonePat(seq),p:snapshot()};
renderSlots(); updButtons(); readout(); save();
});
$('#patLoad').addEventListener('click',function(){
var pt=PAT[patSel]; if(!pt) return;
ensure(); stopChain();
seq=clonePat(pt.seq); seqPos=0; seqTrans=0; seqMode='idle';
applySnapshot(pt.p);
});
$('#patAdd').addEventListener('click',function(){
if(!PAT[patSel] || chain.length>=64) return;
chain.push(patSel); renderChain(); updButtons(); readout(); save();
});
$('#patClearSlot').addEventListener('click',function(){
if(!PAT[patSel]) return;
PAT[patSel]=null;
chain=chain.filter(function(i){ return PAT[i]; });
if(seqMode==='chain' && !chain.length) stopChain();
renderSlots(); renderChain(); updButtons(); readout(); save();
});
$('#chainstrip').addEventListener('click',function(e){
var c=e.target.closest ? e.target.closest('.chaincell') : null;
if(!c) return;
chain.splice(+c.dataset.i,1);
if(chainPos>=chain.length) chainPos=0;
if(!chain.length) stopChain();
renderChain(); updButtons(); readout(); save();
});
$('#chainPlay').addEventListener('click',function(){
ensure();
if(seqMode==='chain'){ stopChain(); panic(); }
else if(chain.length){
chainSaved=snapshot();
seqMode='chain'; chainPos=0; chainStep=0; arpMode=0;
panic(); clearMarks(); nextTick=ctx.currentTime+0.02;
}
updButtons(); readout();
});
$('#chainClear').addEventListener('click',function(){
stopChain(); chain=[]; chainPos=0; chainStep=0;
renderChain(); updButtons(); readout(); save();
});
$('#exportWav').addEventListener('click',function(){ exportAudio(); });
$('#power').addEventListener('click',function(){ setPower(!powered); });
function readout(){
var r=$('#readout'), txt;
if(exportMsg) txt=exportMsg;
else if(!powered) txt='Press <b>Power</b> to start';
else if(seqMode==='chain') txt='<b>Chain</b> \u00b7 '+chain.length+' patterns \u00b7 playing '+slotLetter(chain[chainPos]||0)+' ('+(chainPos+1)+'/'+chain.length+')';
else if(seqMode==='rec') txt='<b>Recording</b> · '+seq.length+' steps · play notes, add rests';
else if(seqMode==='play') txt='<b>Sequence</b> · '+seq.length+' steps'+(seqTrans?' · transposed '+(seqTrans>0?'+':'')+seqTrans:'')+' · '+S.lfoRate().toFixed(2)+' Hz';
else if(arpMode) txt='<b>'+['','Up','Up & down','Down'][arpMode]+'</b> arpeggio · '+S.lfoRate().toFixed(2)+' Hz';
else txt='<b>Ready</b> · clock '+S.lfoRate().toFixed(2)+' Hz'+(seq.length?' · '+seq.length+' steps stored':'');
r.innerHTML=txt;
}
/* ---- patches ---- */
var PRESETS=[
{name:'Init',p:{}},
{name:'Acid bass',p:{mixSaw:1,mixSub:0.3,vcoRange:0,vcfFreq:0.24,vcfRes:0.72,vcfEnv:0.55,vcfKbd:0.3,envA:0,envD:0.3,envS:0,envR:0.18,porta:0.32,portaMode:0}},
{name:'Sub bass',p:{mixSaw:0,mixPulse:0.5,mixSub:1,subType:0,vcoRange:0,vcfFreq:0.3,vcfRes:0.12,vcfEnv:0.26,envA:0,envD:0.5,envS:0.25,envR:0.22}},
{name:'Rubber pluck',p:{mixSaw:0.7,mixSub:0.6,subType:1,vcfFreq:0.2,vcfRes:0.56,vcfEnv:0.62,envA:0,envD:0.28,envS:0,envR:0.2}},
{name:'PWM pad',p:{mixPulse:1,mixSaw:0,mixSub:0.25,pw:0.6,pwmMode:0,lfoRate:0.16,vcfFreq:0.52,vcfRes:0.2,vcfEnv:0.3,envA:0.45,envD:0.6,envS:0.85,envR:0.55}},
{name:'Lead',p:{mixSaw:0.9,mixPulse:0.5,pw:0.35,pwmMode:1,vcoRange:2,vcoMod:0.25,vcfFreq:0.42,vcfRes:0.45,vcfEnv:0.4,vcfKbd:0.6,envA:0.08,envD:0.5,envS:0.6,envR:0.3,porta:0.3,portaMode:0}},
{name:'Arp stab',p:{mixPulse:0.8,mixSaw:0.4,mixSub:0.4,pw:0.4,pwmMode:1,vcfFreq:0.3,vcfRes:0.6,vcfEnv:0.55,envA:0,envD:0.22,envS:0,envR:0.1,lfoRate:0.62}},
{name:'Self-osc kick',p:{mixSaw:0,mixSub:0,mixPulse:0,mixNoise:0,vcfFreq:0.05,vcfRes:1,vcfEnv:0.34,vcfKbd:0,envA:0,envD:0.17,envS:0,envR:0.12}},
{name:'Noise sweep',p:{mixSaw:0,mixSub:0,mixNoise:1,vcfFreq:0.35,vcfRes:0.62,vcfEnv:0.55,vcfMod:0.5,lfoRate:0.1,envA:0.5,envD:0.6,envS:0.35,envR:0.5}},
{name:'Sample and hold',p:{mixSaw:0.8,mixPulse:0.4,lfoWave:2,lfoRate:0.55,vcfFreq:0.3,vcfRes:0.5,vcfMod:0.6,vcfEnv:0.2,envA:0,envD:0.5,envS:0.6,envR:0.2}}
];
var userPatches=[], undoState=null;
function snapshot(){ var o={}; for(var k in DEF) o[k]=P[k]; return o; }
function applySnapshot(o){
for(var k in DEF) if(typeof o[k]==='number') P[k]=o[k];
if(ctx) setPitch(noteFreq(lastNote),ctx.currentTime,0);
refreshAll(); save();
}
function allPatches(){ return PRESETS.concat(userPatches); }
function refreshAll(){
for(var k in P){ if(SW[k]!==undefined) switchUI(k); else faderUI(k); }
applyAll(); renderSeq(); renderSlots(); renderChain(); octUI(); updButtons(); readout();
}
function loadPreset(i){
var pr=allPatches()[i]; if(!pr) return;
for(var k in DEF) P[k]=DEF[k];
for(var k2 in pr.p) P[k2]=pr.p[k2];
if(ctx) setPitch(noteFreq(lastNote),ctx.currentTime,0);
refreshAll(); save();
}
function rebuildPatchList(select){
var sel=$('#presets'); if(!sel) return;
sel.innerHTML='';
allPatches().forEach(function(pr,i){
var o=document.createElement('option');
o.value=i; o.textContent=(i>=PRESETS.length?'\u2605 ':'')+pr.name;
sel.appendChild(o);
});
if(select!==undefined && select!==null) sel.value=String(select);
$('#patchDel').disabled = !(sel.value!=='' && +sel.value>=PRESETS.length);
}
function setUndo(state,label){
undoState=state;
var b=$('#patchUndo');
b.disabled=!state; b.textContent=label||'Undo';
}
(function(){
var sel=$('#presets');
rebuildPatchList(0);
sel.addEventListener('change',function(){
ensure(); setUndo(snapshot(),'Undo');
loadPreset(parseInt(sel.value,10));
$('#patchDel').disabled = +sel.value < PRESETS.length;
});
$('#patchSave').addEventListener('click',function(){
var fallback='My patch '+(userPatches.length+1), name=fallback, cancelled=false;
try{
var r=window.prompt('Name this patch',fallback);
if(r===null) cancelled=true; else if(r.trim()) name=r.trim();
}catch(e){}
if(cancelled) return;
if(userPatches.length>=24) userPatches.shift();
userPatches.push({name:name,p:snapshot()});
rebuildPatchList(PRESETS.length+userPatches.length-1);
save();
});
$('#patchUndo').addEventListener('click',function(){
if(!undoState) return;
var cur=snapshot();
applySnapshot(undoState);
setUndo(cur, $('#patchUndo').textContent==='Undo' ? 'Redo' : 'Undo');
});
$('#patchDel').addEventListener('click',function(){
var sel2=$('#presets'), i=+sel2.value;
if(i<PRESETS.length) return;
userPatches.splice(i-PRESETS.length,1);
rebuildPatchList(0); save();
});
})();
/* ---- case colour ---- */
$$('.swatch').forEach(function(b){
b.addEventListener('click',function(){
document.documentElement.dataset.model=b.dataset.model;
$$('.swatch').forEach(function(o){ o.setAttribute('aria-pressed',o===b?'true':'false'); });
readColours(); save();
});
});
/* ---- persistence ---- */
var saveT=null;
function save(){
clearTimeout(saveT);
saveT=setTimeout(function(){
try{ localStorage.setItem('sh101.state',JSON.stringify({
p:P, model:document.documentElement.dataset.model||'grey',
seq:seq, oct:kbOct, user:userPatches, pat:PAT, chain:chain
})); }catch(e){}
},250);
}
(function restore(){
try{
var raw=localStorage.getItem('sh101.state'); if(!raw) return;
var st=JSON.parse(raw);
if(st && st.p) for(var k in DEF) if(typeof st.p[k]==='number') P[k]=st.p[k];
if(st && typeof st.oct==='number') kbOct=clamp(st.oct,-2,2);
if(st && Object.prototype.toString.call(st.seq)==='[object Array]'){
seq=st.seq.filter(function(x){ return x && (x.rest===true || typeof x.n==='number'); }).slice(0,100);
}
if(st && Object.prototype.toString.call(st.pat)==='[object Array]'){
for(var pi=0;pi<8;pi++){
var pt=st.pat[pi];
PAT[pi]=(pt && pt.p && Object.prototype.toString.call(pt.seq)==='[object Array]')
? {name:slotLetter(pi),seq:clonePat(pt.seq),p:pt.p} : null;
}
}
if(st && Object.prototype.toString.call(st.chain)==='[object Array]'){
chain=st.chain.filter(function(i){ return typeof i==='number' && i>=0 && i<8 && PAT[i]; }).slice(0,64);
}
if(st && Object.prototype.toString.call(st.user)==='[object Array]'){
userPatches=st.user.filter(function(x){ return x && typeof x.name==='string' && x.p; }).slice(0,24);
rebuildPatchList();
}
if(st && st.model){
document.documentElement.dataset.model=st.model;
$$('.swatch').forEach(function(o){ o.setAttribute('aria-pressed',o.dataset.model===st.model?'true':'false'); });
}
}catch(e){}
})();
/* ---- MIDI ---- */
(function(){
if(!navigator.requestMIDIAccess) return;
try{
navigator.requestMIDIAccess().then(function(acc){
acc.inputs.forEach(function(inp){
inp.onmidimessage=function(m){
var d=m.data, cmd=d[0]&0xf0;
if(cmd===0x90 && d[2]>0){ ensure(); noteOn(d[1]); }
else if(cmd===0x80 || (cmd===0x90&&d[2]===0)) noteOff(d[1]);
else if(cmd===0xe0){ padX=clamp(((d[2]*128+d[1])-8192)/8192,-1,1); padApply(); }
};
});
},function(){});
}catch(e){}
})();
/* ---- offline render and audio export ---- */
function rateOf(pp){ return Math.max(expMap(pp.lfoRate,0.1,30),0.08); }
function relOf(pp){ return expMap(pp.envR,0.003,10); }
function copyPatch(pp){ var o={}; for(var k in DEF) o[k]=(typeof pp[k]==='number')?pp[k]:DEF[k]; return o; }
function renderPattern(pat,sr){
var pp=copyPatch(pat.p), steps=pat.seq.length;
var rate=rateOf(pp), body=steps/rate;
var tail=Math.min(Math.max(relOf(pp),0.35),4)+0.3;
var len=Math.max(1,Math.ceil((body+tail)*sr));
var OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext;
var oac=new OAC(1,len,sr);
var sCtx=ctx,sA=A,sP=P,sNote=lastNote,sSound=sounding,sPow=powered,sInst=instantParams,sRend=rendering,sPadY=padY;
try{
ctx=oac; A={}; P=pp; lastNote=60; sounding=false; powered=true;
instantParams=true; rendering=true; padY=0;
buildGraph();
setPitch(noteFreq(60),0,0);
applyAll();
var t=0.004, step=1/rate;
for(var i=0;i<steps;i++){
if(P.lfoWave===2) A.lfoRand.offset.setValueAtTime(Math.random()*2-1,t);
var st2=pat.seq[i];
if(st2.rest){ envRelease(t); gateOff(t); } else playStep(st2.n,t,step);
t+=step;
}
envRelease(t); gateOff(t);
} finally {
ctx=sCtx; A=sA; P=sP; lastNote=sNote; sounding=sSound; powered=sPow;
instantParams=sInst; rendering=sRend; padY=sPadY;
}
return oac.startRendering().then(function(b){ return {data:b.getChannelData(0), body:body}; });
}
function renderArrangement(sr){
var items = chain.length ? chain.slice() : (PAT[patSel] ? [patSel] : []);
items = items.filter(function(i){ return PAT[i] && PAT[i].seq.length; });
if(!items.length) return Promise.resolve(null);
var uniq=[], seen={};
items.forEach(function(i){ if(!seen[i]){ seen[i]=1; uniq.push(i); } });
var cache={};
return uniq.reduce(function(chainP,i){
return chainP.then(function(){
return renderPattern(PAT[i],sr).then(function(r){ cache[i]=r; });
});
},Promise.resolve()).then(function(){
var offs=[], total=0;
items.forEach(function(i){ offs.push(total); total+=cache[i].body; });
var lastLen=cache[items[items.length-1]].data.length/sr;
var totalLen=Math.ceil((offs[offs.length-1]+lastLen)*sr);
var out=new Float32Array(totalLen);
items.forEach(function(idx,k){
var d=cache[idx].data, off=Math.round(offs[k]*sr);
for(var j=0;j<d.length;j++){ var w=off+j; if(w<totalLen) out[w]+=d[j]; }
});
var peak=0;
for(var m=0;m<totalLen;m++){ var a=out[m]<0?-out[m]:out[m]; if(a>peak) peak=a; }
if(peak>0.98){ var g=0.98/peak; for(var q=0;q<totalLen;q++) out[q]*=g; }
var fade=Math.min(Math.round(sr*0.01),totalLen);
for(var f=0;f<fade;f++) out[totalLen-1-f]*=f/fade;
return {data:out, sr:sr, seconds:totalLen/sr};
});
}
function encodeWav(samples,sr){
var nn=samples.length, buf=new ArrayBuffer(44+nn*2), v=new DataView(buf);
function str(o,t){ for(var i=0;i<t.length;i++) v.setUint8(o+i,t.charCodeAt(i)); }
str(0,'RIFF'); v.setUint32(4,36+nn*2,true); str(8,'WAVE');
str(12,'fmt '); v.setUint32(16,16,true); v.setUint16(20,1,true); v.setUint16(22,1,true);
v.setUint32(24,sr,true); v.setUint32(28,sr*2,true); v.setUint16(32,2,true); v.setUint16(34,16,true);
str(36,'data'); v.setUint32(40,nn*2,true);
var o=44;
for(var i=0;i<nn;i++){
var x=samples[i]; x=x<-1?-1:(x>1?1:x);
v.setInt16(o,x<0?x*0x8000:x*0x7FFF,true); o+=2;
}
return new Uint8Array(buf);
}
var CRC_T=null;
function crc32(u8){
if(!CRC_T){
CRC_T=new Int32Array(256);
for(var i=0;i<256;i++){ var c=i; for(var k=0;k<8;k++) c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1); CRC_T[i]=c; }
}
var crc=-1;
for(var j=0;j<u8.length;j++) crc=(crc>>>8)^CRC_T[(crc^u8[j])&0xFF];
return (crc^-1)>>>0;
}
function zipStore(name,data){
var enc=[]; for(var i=0;i<name.length;i++) enc.push(name.charCodeAt(i)&0xFF);
var nl=enc.length, crc=crc32(data), sz=data.length;
var total=30+nl+sz+46+nl+22;
var out=new Uint8Array(total), v=new DataView(out.buffer), p=0;
function u32(x){ v.setUint32(p,x,true); p+=4; }
function u16(x){ v.setUint16(p,x,true); p+=2; }
u32(0x04034b50); u16(20); u16(0); u16(0); u16(0); u16(0x21);
u32(crc); u32(sz); u32(sz); u16(nl); u16(0);
out.set(enc,p); p+=nl;
out.set(data,p); p+=sz;
var cd=p;
u32(0x02014b50); u16(20); u16(20); u16(0); u16(0); u16(0); u16(0x21);
u32(crc); u32(sz); u32(sz); u16(nl); u16(0); u16(0); u16(0); u16(0); u32(0); u32(0);
out.set(enc,p); p+=nl;
var cdSize=p-cd;
u32(0x06054b50); u16(0); u16(0); u16(1); u16(1); u32(cdSize); u32(cd); u16(0);
return out;
}
function exportStatus(msg){ exportMsg=msg; readout(); }
function exportAudio(){
if(exporting) return;
if(!(chain.length || PAT[patSel])){ exportStatus('Store a pattern first, then export.'); return; }
exporting=true; updButtons(); exportStatus('<b>Rendering</b> \u2026');
var sr=44100, base='sh101-'+(chain.length?'chain':'pattern-'+slotLetter(patSel));
renderArrangement(sr).then(function(res){
if(!res){ throw {code:'empty'}; }
var wav=encodeWav(res.data,sr);
var secs=res.seconds.toFixed(1);
var inShell=!!(window.claude && window.claude.use);
var use=inShell ? window.claude.use('downloads') : Promise.resolve(null);
exportStatus('<b>Preparing</b> \u2026 '+secs+'s rendered');
return use.catch(function(){ return null; }).then(function(dl){
if(dl && dl.save){
var zip=zipStore(base+'.wav',wav);
return dl.save({filename:base+'.zip', data:new Blob([zip])}).then(function(){
exportStatus('<b>Saved</b> \u00b7 '+secs+'s WAV inside '+base+'.zip');
});
}
if(inShell){ exportStatus('Saving is not available in this view.'); return; }
var blob=new Blob([wav],{type:'audio/wav'});
var url=URL.createObjectURL(blob);
var a=document.createElement('a');
a.href=url; a.download=base+'.wav';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function(){ URL.revokeObjectURL(url); },8000);
exportStatus('<b>Exported</b> \u00b7 '+secs+'s WAV');
});
}).catch(function(err){
var code=err&&err.code;
if(code==='declined') exportStatus('Export cancelled.');
else if(code==='empty') exportStatus('Nothing to export yet.');
else if(code==='too_large') exportStatus('That arrangement is too large to save.');
else exportStatus('Export failed \u2014 try a shorter chain.');
}).then(function(){
exporting=false; updButtons();
setTimeout(function(){ exportMsg=''; readout(); },6000);
});
}
/* ---- display ---- */
var scopeCv=$('#scope'), scopeC=null, freqData=null, ledCol='#ff3b1f';
try{ if(scopeCv && scopeCv.getContext) scopeC=scopeCv.getContext('2d'); }catch(e){}
function readColours(){
try{
var cs=getComputedStyle(document.documentElement);
ledCol=(cs.getPropertyValue('--led')||'#ff3b1f').trim()||'#ff3b1f';
}catch(e){}
}
readColours();
function drawScope(){
if(!scopeC) return;
var W=scopeCv.width, H=scopeCv.height;
scopeC.fillStyle='#0b0c0d'; scopeC.fillRect(0,0,W,H);
scopeC.strokeStyle='rgba(255,255,255,.07)'; scopeC.lineWidth=1;
for(var gx=1;gx<4;gx++){
scopeC.beginPath(); scopeC.moveTo(W*gx/4,0); scopeC.lineTo(W*gx/4,H); scopeC.stroke();
}
if(!ctx||!powered||!A.an){
scopeC.strokeStyle='rgba(255,255,255,.18)'; scopeC.lineWidth=2;
scopeC.beginPath(); scopeC.moveTo(0,H-3); scopeC.lineTo(W,H-3); scopeC.stroke();
return;
}
if(!freqData) freqData=new Uint8Array(A.an.frequencyBinCount);
A.an.getByteFrequencyData(freqData);
var minF=40, maxF=16000, nyq=ctx.sampleRate/2, bins=freqData.length, lr=Math.log(maxF/minF);
scopeC.beginPath(); scopeC.moveTo(0,H);
for(var x=0;x<=W;x+=2){
var f=minF*Math.exp(lr*x/W);
var b=Math.min(bins-1,Math.round(f/nyq*bins));
scopeC.lineTo(x,H-(freqData[b]/255)*(H-3));
}
scopeC.lineTo(W,H); scopeC.closePath();
var g=scopeC.createLinearGradient(0,0,0,H);
g.addColorStop(0,'rgba(238,236,230,.6)'); g.addColorStop(1,'rgba(238,236,230,.05)');
scopeC.fillStyle=g; scopeC.fill();
var ev=A.env.offset.value||0;
var cf=clamp(filterBase()*Math.pow(2,(S.vcfEnvC()*ev)/1200),minF,maxF);
var mx=W*Math.log(cf/minF)/lr;
scopeC.strokeStyle=ledCol; scopeC.lineWidth=2;
scopeC.beginPath(); scopeC.moveTo(mx,0); scopeC.lineTo(mx,H); scopeC.stroke();
}
/* ---- animation ---- */
var frameN=0;
(function frame(){
requestAnimationFrame(frame);
if(window.__padSpring) window.__padSpring();
var led=$('#lfoLed');
if(ctx && powered){
var ph=(ctx.currentTime*S.lfoRate())%1;
led.dataset.on = ph<0.5 ? '1':'0';
} else led.dataset.on='0';
if((frameN++ & 1)===0) drawScope();
})();
refreshAll();
})();
</script>
</body>
</html>