Files
seedbox/sim/index.html
Gee 346c820b10
Some checks failed
check / check (push) Has been cancelled
Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
- genome(seed, salts) now has three parts with their own RNG streams:
  shared (steps, form, swing, arc, layback, fill odds, phase offset),
  kit (density, frame, accent cycle, free snare) and keys (scale/root,
  progression, motif, hocket, bass tone, note lengths, range). ROLL KEYS
  now renews the harmony too; ROLL KIT leaves the keys alone.
- Keys generators share one signature and can fix note lengths:
  MOTIF (the old line), 303 (acid bass, answers in the rests, drone or
  silent pad), TURING (8/16-bit shift register, DRIFT flips bits, 16-bar
  window), ARP (triad/7th arpeggio, roots on kicks).
- Pages: KIT GEN, KEYS GEN; ROLL shows KIT / KEYS; slots keep both gens.
- Flash record v3 (old settings ignored). Bring-up golden checksums updated.
- C port matches; host test and ASan/UBSan pass.

Intended music change: every seed sounds different (genome split, new
generators). Vectors regenerated with npm run vectors; the case grid now
crosses all kit and keys generators, with TURING runs past its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:50:41 +03:00

238 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>Seedbox</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=Hanken+Grotesk:wght@400;500;600&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
/* Layout: left = the box as it will be built; right = phrase view, voice switches, bench settings. Stacks on phones. */
:root{
--bg:#E3E7EB; --surface:#F1F3F5; --panel-a:#D8DDE2; --panel-b:#BFC7CF; --ink:#19202A; --muted:#566271;
--line:#BAC2CB; --grid:#D3D9DF; --accent:#1E4C9E; --warn:#B4441F;
--l-kit:#13737A; --l-mono:#6243AB;
--oled-bg:#03060A; --cap:#262C33; --cap-hi:#3A424B; --cap-ink:#E6EBEF; --bezel:#11161C;
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
--f-mono:'Martian Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
--bg:#12161B; --surface:#1A1F25; --panel-a:#2A3038; --panel-b:#1E242A; --ink:#E2E7EC; --muted:#8C98A5;
--line:#333B44; --grid:#262D35; --accent:#8EB0F2; --warn:#F07B52;
--l-kit:#4CC4C9; --l-mono:#A88EF2;
--cap:#0E1216; --cap-hi:#272E36; --cap-ink:#D9E0E6; --bezel:#07090C; color-scheme:dark } }
:root[data-theme="dark"]{
--bg:#12161B; --surface:#1A1F25; --panel-a:#2A3038; --panel-b:#1E242A; --ink:#E2E7EC; --muted:#8C98A5;
--line:#333B44; --grid:#262D35; --accent:#8EB0F2; --warn:#F07B52;
--l-kit:#4CC4C9; --l-mono:#A88EF2;
--cap:#0E1216; --cap-hi:#272E36; --cap-ink:#D9E0E6; --bezel:#07090C; color-scheme:dark }
*{box-sizing:border-box}
body{background:var(--bg);color:var(--ink);font:15px/1.5 var(--f-ui);padding:20px 16px 40px}
.wrap{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1fr);gap:28px;align-items:start}
h1{font:700 22px/1.1 var(--f-mono);letter-spacing:-.01em;margin:0}
.sub{color:var(--muted);font-size:14px;margin:6px 0 0;max-width:62ch}
.label{font:500 10px/1.2 var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
/* the box */
.device{position:relative;border-radius:14px;padding:26px 22px 22px;background:linear-gradient(165deg,var(--panel-a),var(--panel-b));
box-shadow:inset 0 1px 0 rgba(255,255,255,.35),inset 0 -2px 0 rgba(0,0,0,.12),0 10px 30px -12px rgba(0,0,0,.35);display:grid;gap:18px}
.screw{position:absolute;width:9px;height:9px;border-radius:50%;background:radial-gradient(circle at 35% 35%,var(--cap-hi),var(--cap))}
.screw.tl{top:8px;left:8px}.screw.tr{top:8px;right:8px}.screw.bl{bottom:8px;left:8px}.screw.br{bottom:8px;right:8px}
.dev-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
.dev-name{font:700 13px var(--f-mono);letter-spacing:.24em;color:var(--accent)}
.oledrow{display:flex;align-items:flex-end;gap:10px}
.device{max-width:820px}
.backwall{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 10px;border-radius:6px;background:rgba(0,0,0,.10);box-shadow:inset 0 1px 2px rgba(0,0,0,.2)}
.backwall .label{margin-right:8px}
.bw-spacer{flex:1}
.usb{width:22px;height:10px;border-radius:3px;background:var(--bezel)}
.zones{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:22px;align-items:center}
@media (max-width:700px){.zones{grid-template-columns:minmax(0,1fr)}}
.zone{display:grid;gap:14px;justify-items:center;min-width:0}
.zl{justify-items:start}
.zlrow{display:flex;gap:22px;align-items:flex-end}
.zm{gap:12px}
.zm .big{width:72px;height:72px}
.oled{background:var(--bezel);border-radius:3px;padding:2mm;line-height:0}
.oled.lcd{background:#3A4440;padding:2.5mm}
.oled canvas{display:block;image-rendering:pixelated;max-width:100%}
.zoomwrap{background:var(--bezel);border-radius:6px;padding:10px;line-height:0}
#oledZoom{display:block;width:100%;max-width:520px;image-rendering:pixelated}
.trk.noled .tled{visibility:hidden}
.runcell{display:grid;place-items:center}
.row{display:flex;gap:16px;align-items:flex-end;justify-content:space-between;flex-wrap:wrap}
.ctl{display:grid;justify-items:center;gap:6px}
.knob{width:62px;height:62px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none;
background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 70%);box-shadow:0 3px 6px rgba(0,0,0,.3),0 0 0 4px rgba(0,0,0,.08)}
.knob::after{content:"";position:absolute;left:50%;top:6px;width:3px;height:18px;margin-left:-1.5px;border-radius:2px;background:var(--cap-ink);transform-origin:50% 25px;transform:rotate(var(--rot,0deg))}
.knob:focus-visible,.enc:focus-visible,.cap:focus-visible,.big:focus-visible,.toggle:focus-visible,.vs:focus-visible,.btn:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.val{font:500 11px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}
.enc{width:52px;height:52px;border-radius:50%;cursor:ew-resize;touch-action:none;position:relative;background:repeating-conic-gradient(var(--cap) 0 6deg,var(--cap-hi) 6deg 12deg);box-shadow:0 3px 6px rgba(0,0,0,.3)}
.enc::after{content:"";position:absolute;inset:10px;border-radius:50%;background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 75%)}
.enc i{position:absolute;z-index:1;left:50%;top:14px;width:4px;height:4px;margin-left:-2px;border-radius:50%;background:var(--cap-ink);transform-origin:2px 12px;transform:rotate(var(--rot,0deg))}
.big{width:96px;height:96px;border-radius:50%;border:0;cursor:pointer;font:700 11px var(--f-mono);letter-spacing:.14em;color:var(--cap-ink);touch-action:none;
background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 72%);box-shadow:0 5px 0 rgba(0,0,0,.35),0 0 0 6px rgba(0,0,0,.1)}
.big.down{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.35),0 0 0 6px rgba(0,0,0,.1)}
.caps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;flex:1;min-width:0}
.capw{display:grid;justify-items:center;gap:6px}
.led{width:8px;height:8px;border-radius:50%;background:rgba(0,0,0,.35);box-shadow:inset 0 1px 1px rgba(0,0,0,.4)}
.led.on{background:var(--c);box-shadow:0 0 8px var(--c)}
.cap{width:100%;max-width:70px;height:38px;border:0;border-radius:6px;cursor:pointer;color:var(--cap-ink);font:600 10px var(--f-mono);letter-spacing:.08em;touch-action:none;
background:linear-gradient(var(--cap-hi),var(--cap));box-shadow:0 3px 0 rgba(0,0,0,.35)}
.cap.down{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.35)}
.cap.off{opacity:.45}
.toggle{display:grid;justify-items:center;gap:6px;background:none;border:0;cursor:pointer;padding:0;color:inherit}
.tg{width:22px;height:40px;border-radius:11px;background:var(--bezel);position:relative;box-shadow:inset 0 2px 3px rgba(0,0,0,.6)}
.tg::after{content:"";position:absolute;left:4px;width:14px;height:18px;border-radius:7px;background:var(--cap-ink);top:18px;transition:top .12s}
.toggle[aria-pressed="true"] .tg::after{top:4px}
.hint{font:400 11px/1.6 var(--f-mono);color:var(--muted);margin:12px 0 0}
.hint kbd{font:inherit;border:1px solid var(--line);border-radius:3px;padding:0 4px}
/* track buttons under a WS2812 strip */
.trk{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px 10px;padding:12px 10px 8px;border-radius:8px;background:rgba(0,0,0,.06);box-shadow:inset 0 1px 2px rgba(0,0,0,.15)}
.tcell{display:grid;justify-items:center;gap:6px;min-width:0}
.tled{width:11px;height:11px;border-radius:50%;background:var(--bezel);box-shadow:inset 0 1px 1px rgba(0,0,0,.6)}
.tbtn{width:34px;height:34px;border-radius:50%;border:0;cursor:pointer;touch-action:none;background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 70%);box-shadow:0 3px 0 rgba(0,0,0,.35),0 0 0 3px rgba(0,0,0,.12)}
.tbtn.down{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.35),0 0 0 3px rgba(0,0,0,.12)}
.tbtn:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.tname{font:600 9px/1.1 var(--f-mono);letter-spacing:.06em;color:var(--accent);text-align:center;white-space:nowrap}
.jacks{display:flex;align-items:center;gap:6px}
.jack{width:18px;height:18px;border-radius:50%;background:radial-gradient(circle,var(--bezel) 40%,var(--cap-hi) 44%,var(--cap) 70%)}
.jacks .label{margin-right:8px}
/* right column */
.main{display:grid;gap:18px;min-width:0}
.statusbar{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;justify-content:space-between}
.pill{font:500 11px var(--f-mono);padding:4px 10px;border-radius:999px;border:1px solid var(--line);color:var(--muted);background:var(--surface)}
.pill.ok{color:var(--l-kit);border-color:currentColor}
.pill.bad{color:var(--warn);border-color:currentColor}
.view{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px;display:grid;gap:12px;min-width:0}
.facts{display:flex;flex-wrap:wrap;gap:4px 18px;font:500 12px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}
.facts b{color:var(--ink);font-weight:500}
#grid{display:block;width:100%;height:340px}
.voices{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.tracks{display:grid;gap:8px;flex:1;min-width:0}
.trow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.trow .label{width:100%}
.vs{display:inline-flex;align-items:center;gap:8px;font:600 12px var(--f-mono);padding:7px 12px;border-radius:6px;cursor:pointer;
border:1px solid var(--c);background:var(--c);color:var(--surface)}
.vs .st{font-weight:500;opacity:.8;font-size:10px;text-transform:uppercase;letter-spacing:.06em}
.vs[aria-pressed="false"]{background:transparent;color:var(--muted);border-color:var(--line)}
.vs.pending{outline:2px dashed var(--accent);outline-offset:2px}
.check{display:flex;gap:8px;align-items:center;font:500 13px var(--f-ui)}
.voices .check{font-size:12px;color:var(--muted)}
.voices{align-items:flex-end}
details{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
summary{cursor:pointer;font:600 14px var(--f-ui)}
.form{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px 16px;margin-top:12px}
.field{display:grid;gap:4px;min-width:0}
.field label{font:500 11px var(--f-mono);color:var(--muted);letter-spacing:.04em}
select,input[type=number],input[type=text]{font:500 13px var(--f-mono);color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:6px 8px;width:100%;min-width:0}
.btn{font:600 12px var(--f-mono);letter-spacing:.04em;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:6px;padding:7px 12px;cursor:pointer}
.btn:hover{border-color:var(--accent)}
textarea{width:100%;min-height:140px;font:400 11px/1.4 var(--f-mono);background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:8px;resize:vertical}
.note{color:var(--muted);font-size:13px;margin:10px 0 0;max-width:70ch}
.padgrid .lbl{font:500 11px var(--f-mono);color:var(--muted)}
.padgrid .val.miss{color:var(--warn)}
.padgrid{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px 12px}
.sect{font:600 12px var(--f-mono);color:var(--accent);letter-spacing:.06em;text-transform:uppercase;grid-column:1/-1;margin-top:6px}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
<div class="wrap">
<section aria-label="Device">
<div class="device">
<span class="screw tl"></span><span class="screw tr"></span><span class="screw bl"></span><span class="screw br"></span>
<div class="backwall" aria-label="Back wall">
<span class="label">back wall</span>
<span class="usb"></span><span class="label">USB</span>
<span class="bw-spacer"></span>
<span class="jack"></span><span class="label">MIDI in</span><span class="jack"></span><span class="label">Kit</span><span class="jack"></span><span class="label">Synth</span>
</div>
<div class="zones">
<div class="zone zl">
<div class="dev-head"><span class="dev-name">SEEDBOX</span></div>
<div class="oledrow"><div class="oled"><canvas id="oled" width="84" height="48" aria-label="Screen, approximately true size"></canvas></div></div>
<div class="zlrow">
<div class="ctl"><div class="enc" id="enc" tabindex="0" role="slider" aria-label="Encoder: turn to change page or value, press to edit"><i></i></div><span class="label">Enc · push</span></div>
</div>
</div>
<div class="zone zm">
<div class="ctl"><div class="knob" id="kEnergy" tabindex="0" role="slider" aria-label="Energy" aria-valuemin="0" aria-valuemax="127"></div><span class="label">Energy <span class="val" id="vEnergy"></span></span></div>
<div class="ctl"><div class="knob" id="kDrift" tabindex="0" role="slider" aria-label="Drift" aria-valuemin="0" aria-valuemax="127"></div><span class="label">Drift <span class="val" id="vDrift"></span></span></div>
<div class="ctl"><button class="big" id="big" aria-label="New seed">SEED</button></div>
</div>
<div class="zone zr"><div class="trk" id="trk" aria-label="Track buttons"></div></div>
</div>
</div>
<p class="note" style="margin:8px 0 0">Landscape, laid out like the panel drawing: you sit at the bottom edge, cables leave from the back. The screen is drawn at roughly real size.</p>
<p class="hint"><b>Track buttons:</b> tap = on ↔ off. Hold = flip it while held (drop a track out, or punch one in).<br>
<b>Screen map / LEDs:</b> filled = on, hollow center = hit, empty = off, blinking = change waiting for the bar line. Map rows match the button rows.<br>
<b>Encoder pages:</b> SEED history · KIT GEN · KEYS GEN (MOTIF, 303, TURING, ARP) · ROOT · SCALE (AUTO = the seed picks) · BPM · SLOT (push loads, hold push saves) · ROLL (re-roll kit or keys) · LEARN (push, press a kit track, hit the pad).<br>
Keys: <kbd>Space</kbd> seed · <kbd>1</kbd>…<kbd>0</kbd> <kbd>-</kbd> tracks · <kbd>←</kbd><kbd>→</kbd> encoder · <kbd>Enter</kbd> push · <kbd>R</kbd> run</p>
</section>
<section class="main">
<div>
<h1>Seedbox</h1>
<p class="sub">A generative rhythm and melody machine built on tracking: one 32-bit seed makes eleven looped tracks for a sampler or drum machine and a synth, and you shape the piece by switching tracks on and off. Each seed carries a chord progression, a melodic motif, a swing amount and a phrase arc, shared by every track. Energy sets how dense the patterns are; drift sets how often a phrase brings a variation.</p>
</div>
<div class="statusbar">
<span class="pill" id="midiStatus">MIDI: not connected</span>
<span class="facts" id="pos"></span>
</div>
<details open>
<summary>Hardware</summary>
<div class="form">
<div class="field"><label for="hwDisplay">Screen</label><select id="hwDisplay">
<option value="5110">Nokia 3310/5110 · 84×48 LCD (≈33×19 mm)</option>
<option value="091">0.91" OLED · 128×32 (22×6 mm)</option>
<option value="096">0.96" OLED · 128×64 (22×11 mm)</option>
<option value="130">1.3" OLED · 128×64 (29×15 mm)</option>
<option value="242">2.42" OLED · 128×64 (55×27 mm)</option></select></div>
<div class="field"><label class="check"><input type="checkbox" id="hwLeds"> LED above each button</label></div>
</div>
<p class="note" id="hwNote"></p>
<div class="zoomwrap"><canvas id="oledZoom" width="84" height="48" aria-label="Screen, magnified"></canvas></div>
<p class="note">Magnified, pixel for pixel. The panel on the left shows it at roughly real size (CSS millimetres, so it depends on your monitor).</p>
</details>
<div class="view">
<div class="facts" id="facts"></div>
<canvas id="grid" aria-label="Current phrase, four bars"></canvas>
<label class="check" style="font-size:12px;color:var(--muted)"><input type="checkbox" id="muteQ"> Track changes wait for the next bar line</label>
</div>
<details open>
<summary>Routing</summary>
<div class="form">
<div class="field"><label for="portDrums">Kit output (sampler / drum machine)</label><select id="portDrums"></select></div>
<div class="field"><label for="portMono">Synth output (line)</label><select id="portMono"></select></div>
<div class="field"><label for="portIn">Kit input (for pad learn)</label><select id="portIn"></select></div>
<div class="field"><label class="check"><input type="checkbox" id="sendClock"> Send MIDI clock + start/stop</label></div>
<div class="sect">Learned pads <button class="btn" id="clearLearn" type="button" style="margin-left:8px;padding:3px 8px;font-size:10px">Clear learned</button></div>
<div id="padFields" class="padgrid"></div>
<div class="sect">Synth line</div>
<div class="field"><label for="monoCh">Channel</label><input type="number" id="monoCh" min="1" max="16"></div>
<div class="field"><label for="monoOct">Octave shift</label><input type="number" id="monoOct" min="-3" max="3"></div>
</div>
<p class="note">Bass, melody and pad all go to the synth on one channel, so put it in Poly mode. They sit in separate octaves (bass below C3, pad C3–B3, melody from C4) so their notes never collide.</p>
<p class="note">Kit tracks play only after they are learned: encoder to LEARN, push, press a track button, hit the pad on the sampler. Unlearned tracks show a dot on the screen.</p>
</details>
<details>
<summary>Port check (golden vectors)</summary>
<p class="note">Exports the exact event list for the current seed and settings. The C port on the Pico must reproduce the same list and checksum.</p>
<div class="form" style="align-items:end">
<div class="field"><label for="vecBars">Bars</label><input type="number" id="vecBars" min="1" max="64" value="8"></div>
<div class="field"><button class="btn" id="vecGo">Generate</button></div>
<div class="field"><button class="btn" id="vecCopy">Copy</button></div>
</div>
<textarea id="vecOut" readonly aria-label="Golden vectors JSON"></textarea>
</details>
</section>
</div>
<script src="engine.js"></script>
<script src="app.js"></script>