viz: groove grids per voice + gentler default sound

- Groove grids: every voice tab gets a 16-step sequencer on the godtime clock.
  Note voices (lead/bass): 8x16 mini piano roll (minor pentatonic), your
  melody replaces the data's while the groove is on. CC voices: 808-style
  gate row that rhythmically chops the world's signal (defaults all-on).
  Grooves serialize with patches (My Patches / spells).
- Default sound tamed: tighter reverb impulse (1.8s), wet 0.9->0.55, delay
  send/wet down, feedback clamp 0.82->0.55, pad/drone gains down, pad detune
  narrowed, perc volumes down — and lead/bass retriggers rate-limited to the
  godtime grid (data jitter glides pitch instead of machine-gunning hits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-05 20:01:36 +10:00
parent 6079ff266a
commit 9c51097258

View File

@ -75,6 +75,17 @@
}
#inspect .midibtn:disabled { opacity: 0.4; cursor: default; }
/* the groove grid — piano roll / 808 steps inside a voice's tab */
.seqgrid { display: grid; grid-template-columns: repeat(16, 12px); gap: 2px; margin-top: 6px; }
.seqcell {
width: 12px; height: 12px; border-radius: 2px; cursor: pointer;
background: rgba(120,150,200,0.14);
}
.seqcell.bar { background: rgba(120,150,200,0.24); }
.seqcell:hover { background: rgba(140,170,220,0.45); }
.seqcell.on { background: #78a0ff; box-shadow: 0 0 6px rgba(120,160,255,0.6); }
.seqcell.cur { outline: 1px solid rgba(255,238,150,0.85); }
/* right-click context menu — every setting, on everything */
#ctxmenu {
position: fixed; z-index: 30; display: none; width: 240px; max-height: 72vh;
@ -791,7 +802,7 @@
<li><b>Click a wheel — go inside it (mind mode).</b> In chakra view, click any chakra's core and the camera steps <i>into</i> the wheel: its mandala of petals turns around your whole field of view, and that chakra's feeds circle <i>you</i> — drag to look around the full 360°. A <b>vector console appears under your hands</b>: 12 keys of that wheel's own scale (C minor pentatonic at the root, rising key by key to B hirajoshi at the crown), played by clicking, by the <kbd>Q</kbd><kbd>]</kbd> row, or by any connected <b>MIDI keyboard</b> — every note runs through the world's own reverb and grit, so you are jamming <i>inside</i> the instrument, in that chakra's mode, while the planet plays around you. <kbd></kbd> <kbd></kbd> travel the spine wheel to wheel; <kbd>esc</kbd> steps back out. And for powerful machines, right-click the sky and set graphics to <b>ultra ✦</b>: every orbiting feed grows a glowing comet trail, and the orbits breathe so the trails weave slow spirals.</li>
<li><b>Skins — the same spine, another cosmology.</b> Right-click the sky and cycle <b>skin</b>. Beyond the seven <i>chakras</i> there is <b>the planes</b>: the Seven Planes &amp; forty-nine sub-planes of the Cosmic Physical Plane from the Alice Bailey / D.K. teachings — <b>I Divine</b> (white light) down through <b>Monadic</b> (violet), <b>Atmic</b>, <b>Buddhic</b>, <b>Mental</b> (where the egoic lotus sits), <b>Astral</b>, to <b>VII Physical</b> (red). The spine becomes the 49-sub-plane spectrum ladder; the side-brackets mark the <b>Monad</b>, the <b>Spiritual Triad</b> and the <b>Personality</b>; and every feed climbs to the plane that matches its rarefaction — the sun's own wind at the Divine, orbits and the sky's geometry at the Monadic, the great cyclic wheels at the Atmic, omen and aspect at the Buddhic, abstract thought and money-as-idea at the Mental, desire and fire and felt sensation at the Astral, and the dense body of the world — weather, quakes, the mortal rates, your own hands — at the Physical. Go inside any plane (mind mode) and its keys play a scale pitched to its altitude: airy whole-tone up at the Divine, grounded minor-pentatonic down at the Physical.</li>
<li><b>One Source, many Spirits, one Matter.</b> That is the whole architecture: the <b>Source</b> is the world's data, the same for everyone; the <b>Spirit</b> is the skin — the belief system through which you read it; the <b>Matter</b> is the sound. Choose your spirit, and the same planet plays through it. The shelf so far, each rendered from its own published lore, literally and without frills: the <b>Tree of Life</b> (Hermetic Qabalah, Golden Dawn attributions — ten sephirot on three pillars, all twenty-two canonical paths, Mercury's data at Hod, the Moon's at Yesod, the body of the world at Malkut); <b>the Lodge</b> (from published Masonic sources only — Worshipful Master in the East, Wardens West and South, the North dark, Boaz and Jachin at the porch where <i>your own hands</i> stand as the candidate, the Blazing Star, the Volume of the Sacred Law as humanity's open book, and music given to the South, for Beauty holds the arts); the <b>Hermetic ladder</b> of the seven planetary gates, lead to silver; <b>Yggdrasil</b> and its nine worlds, the dwarves keeping the markets in Nidavellir; the Sufi <b>lata'if</b>, where the wind data literally breathes at Ruh, spirit-as-breath; and the <b>Wu Xing</b>, five phases in their generating ring and controlling star, the metal birds of the air routed to Metal; and <b>the Spread</b> — the Celtic Cross of the tarot (Waite's published layout), dealt live and never finished: the heavens in <i>what crowns you</i>, the deep debts and quake-roots in <i>what is beneath you</i>, the retrogrades in <i>what is behind you</i>, births and seasons in <i>what is before you</i>, money and fire in <i>your hopes and fears</i> — and <b>your own hands in the Querent's place</b>, because you are the one asking. Right-click the sky → <b>skin</b> to turn the wheel of traditions. Each map stands as its true figure — the Tree as a tree, the Lodge as a lodge — and every station opens into mind mode with its own scale under your hands.</li>
</ul>
<li><b>The groove grids — sequence the voices.</b> Open any voice's tab (shift-click it) and there's a <b>16-step grid on the godtime clock</b>. The note voices (<i>lead, bass</i>) get a <b>mini piano roll</b> — eight rows of minor pentatonic, click to place a note, click again to clear — and while your groove is on, <i>your melody replaces the data's</i>: bitcoin lets go of the lead and you take it. Every other voice gets an <b>808-style gate row</b> that rhythmically chops the world's signal — the reverb, the grit, the percussion breathing in your pattern while the planet still decides how hard. Grooves ride the godtime tempo (lock the dial, or let your heart drive it), and they <b>save with your patch</b>.</li></ul>
<div class="hr"></div>
@ -1079,6 +1090,33 @@
const tweaks = {}; // key or "@group" -> {mute,gain,offset}
const tw = (key) => tweaks[key] || (tweaks[key] = { mute: 0, gain: 1, offset: 0 });
let soloKey = null; // ⌥-click a source to hear it alone
// ---- the groove grids: per-voice 16-step sequencers on godtime -----------
// Note voices get a mini piano roll (8 rows of minor pentatonic); every other
// voice gets an 808-style gate row that rhythmically chops the world's data.
// "Quantize in time" — the rhythmic sibling of the matrix's quantize-to-scale.
const seqs = {}; // destKey -> {on, steps[16]} (notes: -1|row, CC: 0|1)
let seqPhase = 0, seqCurStep = 0, seqGridRefresh = null;
const SEQ_ROWS = [0, 3, 5, 7, 10, 12, 15, 17]; // minor pentatonic, two octaves, low->high
const seqRootFor = (key) => /bass/.test(key) ? 36 : 60;
function seqFor(key, isNote) {
return seqs[key] || (seqs[key] = {
on: false,
steps: isNote ? new Array(16).fill(-1) : new Array(16).fill(1),
});
}
function seqTick() {
for (const k in seqs) {
const sq = seqs[k];
if (!sq.on) continue;
const n = dests.get(k);
if (n && n.isNote) {
const deg = sq.steps[seqCurStep];
if (deg >= 0) Synth.pluck(seqRootFor(k) + SEQ_ROWS[deg], /bass/.test(k) ? 0.9 : 0.7);
}
}
if (seqGridRefresh) seqGridRefresh();
}
function groupsOf(key) {
const out = [];
for (const g in groupsInfo) {
@ -1118,7 +1156,9 @@
const shaped = mxCurve(raw, r.curve || "lin");
const amt = (typeof r.amount === "number") ? r.amount : 0.5;
if (r.quantize) {
notes[r.dest] = mxQuantize(shaped, r.quantize.scale, r.quantize.root, r.quantize.octaves);
// a sequenced note-voice takes its melody from YOUR grid, not the data
if (!(seqs[r.dest] && seqs[r.dest].on))
notes[r.dest] = mxQuantize(shaped, r.quantize.scale, r.quantize.root, r.quantize.octaves);
r.active = Math.abs(amt) * shaped; continue;
}
destVals[r.dest] = (destVals[r.dest] || 0) + amt * shaped;
@ -1133,6 +1173,8 @@
const n = ensureDest(k);
if (tk && !muted && !n.isNote) // per-voice output level (⇧-drag / menu)
v = clamp(v * tk.gain + tk.offset, 0, 1);
const sq = seqs[k]; // groove grid: gate the data in time
if (sq && sq.on && !n.isNote && !sq.steps[seqCurStep]) v = 0;
destVals[k] = v;
n.raw = v; n.muted = muted;
n.norm = n.isNote ? clamp((v - 24) / (96 - 24), 0, 1) : clamp(v, 0, 1);
@ -1166,6 +1208,7 @@
routes: routesArr.map(r => ({ source: r.source, dest: r.dest, amount: r.amount,
curve: r.curve || "lin", gate: r.gate, quantize: r.quantize, label: r.label })),
tweaks: JSON.parse(JSON.stringify(tweaks)),
seqs: JSON.parse(JSON.stringify(seqs)), // your grooves travel with the patch
bpm: godtime.bpm,
};
}
@ -1177,6 +1220,10 @@
}
for (const k in tweaks) delete tweaks[k];
if (p.tweaks) for (const k in p.tweaks) tweaks[k] = Object.assign({ mute: 0, gain: 1, offset: 0 }, p.tweaks[k]);
for (const k in seqs) delete seqs[k];
if (p.seqs) for (const k in p.seqs)
if (p.seqs[k] && Array.isArray(p.seqs[k].steps) && p.seqs[k].steps.length === 16)
seqs[k] = { on: !!p.seqs[k].on, steps: p.seqs[k].steps.slice() };
if (typeof p.bpm === "number") godtime.bpm = clamp(p.bpm, 40, 180);
layoutDirty = true;
}
@ -1561,6 +1608,11 @@
if (godtime.beat >= 1) { godtime.beat -= 1; godtime.beatFlash = 1; }
godtime.beatFlash = Math.max(0, godtime.beatFlash - dt * 3);
// the groove clock — 16th notes on the godtime transport
seqPhase += dt * godtime.bpm / 60 * 4;
const st16 = Math.floor(seqPhase) % 16;
if (st16 !== seqCurStep) { seqCurStep = st16; seqTick(); }
if (layoutDirty) doLayout();
computeDests(); // YOUR patch -> dests (synth + viz + midi)
@ -3378,6 +3430,62 @@
inspectEl.appendChild(row);
}
// ---- the groove grid (voices only): piano roll for notes, 808 row for CC
if (!isSource) {
const sq = seqFor(node.key, node.isNote);
const wrap = document.createElement("div"); wrap.style.marginTop = "10px";
const head = document.createElement("div"); head.className = "sub";
const tog = document.createElement("button"); tog.className = "midibtn";
const paintTog = () => {
tog.textContent = sq.on ? "■ groove on" : "▶ groove off";
tog.classList.toggle("on", sq.on);
};
paintTog();
tog.onclick = () => { sq.on = !sq.on; paintTog(); };
head.appendChild(tog);
const lbl = document.createElement("span");
lbl.style.marginLeft = "8px";
lbl.textContent = node.isNote
? "16 steps · " + midiToName(seqRootFor(node.key)) + " minor pent"
: "16-step gate on the data";
head.appendChild(lbl);
wrap.appendChild(head);
const grid = document.createElement("div");
grid.className = "seqgrid";
const rows = node.isNote ? SEQ_ROWS.length : 1;
const cells = [];
for (let rw = rows - 1; rw >= 0; rw--) { // high notes on top
for (let stp = 0; stp < 16; stp++) {
const cell = document.createElement("div");
cell.className = "seqcell";
if (stp % 4 === 0) cell.classList.add("bar");
if (node.isNote) {
cell.title = midiToName(seqRootFor(node.key) + SEQ_ROWS[rw]) + " · step " + (stp + 1);
cell.onclick = () => {
sq.steps[stp] = sq.steps[stp] === rw ? -1 : rw; // click sets, click again clears
paint();
};
} else {
cell.title = "step " + (stp + 1);
cell.onclick = () => { sq.steps[stp] = sq.steps[stp] ? 0 : 1; paint(); };
}
cells.push({ el: cell, rw, stp });
grid.appendChild(cell);
}
}
const paint = () => {
for (const c of cells) {
c.el.classList.toggle("on", node.isNote ? sq.steps[c.stp] === c.rw : !!sq.steps[c.stp]);
c.el.classList.toggle("cur", c.stp === seqCurStep && sq.on);
}
};
paint();
seqGridRefresh = paint; // the playhead sweeps while open
wrap.appendChild(grid);
inspectEl.appendChild(wrap);
}
const mb = document.createElement("button"); mb.className = "midibtn"; mb.style.marginTop = "8px";
mb.textContent = node.muted ? "unmute" : "mute";
mb.onclick = () => { toggleMuteLocal(node.key); node.muted = isMutedLocal(node.key); mb.textContent = node.muted ? "unmute" : "mute"; };
@ -3687,6 +3795,7 @@
let ctx = null, on = false, N = null, sched = null, lfo = null;
const D = {}; // latest destination values
let leadNote = 0, bassNote = 0, step = 0, nextAt = 0;
let lastLeadT = 0, lastBassT = 0; // retrigger rate-limiters
const mtof = (m) => 440 * Math.pow(2, (m - 69) / 12);
const c01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
@ -3710,15 +3819,15 @@
lim.threshold.value = -8; lim.ratio.value = 12; lim.attack.value = 0.003; lim.release.value = 0.25;
out.connect(lim); lim.connect(ctx.destination);
const conv = ctx.createConvolver(); conv.buffer = impulse(2.4, 2.4);
const conv = ctx.createConvolver(); conv.buffer = impulse(1.8, 3.2); // tighter room, faster decay
const revSend = ctx.createGain(); revSend.gain.value = 0;
const revWet = ctx.createGain(); revWet.gain.value = 0.9;
const revWet = ctx.createGain(); revWet.gain.value = 0.55;
revSend.connect(conv); conv.connect(revWet); revWet.connect(out);
const delay = ctx.createDelay(1.0); delay.delayTime.value = 0.38;
const fb = ctx.createGain(); fb.gain.value = 0.3; delay.connect(fb); fb.connect(delay);
const delSend = ctx.createGain(); delSend.gain.value = 0.16;
const delWet = ctx.createGain(); delWet.gain.value = 0.5;
const delSend = ctx.createGain(); delSend.gain.value = 0.12;
const delWet = ctx.createGain(); delWet.gain.value = 0.35;
delSend.connect(delay); delay.connect(delWet); delWet.connect(out);
const preSat = ctx.createGain(); // all voices sum here
@ -3750,7 +3859,7 @@
const padAmp = ctx.createGain(); padAmp.gain.value = 0;
padFilt.connect(padAmp); padAmp.connect(preSat); lfoAmt.connect(padFilt.frequency);
const padOscs = [0, 7, 16].map((iv, i) => {
const o = ctx.createOscillator(); o.type = "sawtooth"; o.detune.value = (i - 1) * 6;
const o = ctx.createOscillator(); o.type = "sawtooth"; o.detune.value = (i - 1) * 4;
o.frequency.value = mtof(50 + iv); o.connect(padFilt); o.start(); return o;
});
@ -3788,18 +3897,25 @@
S(N.leadFilt.frequency, 250 + d("filter.cutoff", 0.3) * 5000, 0.05);
N.leadFilt.Q.setTargetAtTime(1 + d("wavetable.morph", 0) * 8, t, 0.1);
S(N.padFilt.frequency, 220 + d("pad.brightness", 0) * 4200);
S(N.padAmp.gain, d("pad.brightness", 0) * 0.15, 0.3);
S(N.droneAmp.gain, d("drone.voices", 0) * 0.12, 0.3);
S(N.revSend.gain, (d("reverb.size", 0) * 0.5 + d("master.space", 0) * 0.5) * 0.45, 0.3);
S(N.fb.gain, Math.min(0.82, d("delay.feedback", 0) * 0.85), 0.2);
S(N.padAmp.gain, d("pad.brightness", 0) * 0.10, 0.3);
S(N.droneAmp.gain, d("drone.voices", 0) * 0.08, 0.3);
S(N.revSend.gain, (d("reverb.size", 0) * 0.5 + d("master.space", 0) * 0.5) * 0.30, 0.3);
S(N.fb.gain, Math.min(0.55, d("delay.feedback", 0) * 0.6), 0.2);
const sat = d("saturation", 0);
S(N.satWet.gain, sat * 0.9, 0.2); S(N.satDry.gain, 1 - sat * 0.4, 0.2);
lfo.frequency.setTargetAtTime(0.1 + d("lfo.rate", 0) * 6, t, 0.3);
// retriggers are rate-limited to the godtime grid — data jitter glides
// the pitch but only strikes on musical time, not machine-gun time
const minGap = 30 / Math.max(40, godtime.bpm); // one 8th note
const ln = Math.round(d("lead.note", 0));
if (ln >= 24 && ln !== leadNote) { leadNote = ln; N.leadOsc.frequency.setTargetAtTime(mtof(ln), t, 0.02); hit(N.leadAmp, 0.15, 0.35); }
if (ln >= 24 && ln !== leadNote) {
leadNote = ln; N.leadOsc.frequency.setTargetAtTime(mtof(ln), t, 0.02);
if (t - lastLeadT > minGap) { hit(N.leadAmp, 0.13, 0.35); lastLeadT = t; }
}
const bn = Math.round(d("bass.note", 0));
if (bn >= 20 && bn !== bassNote) {
bassNote = bn; N.bassOsc.frequency.setTargetAtTime(mtof(bn), t, 0.03); hit(N.bassAmp, 0.24, 0.6);
if (bn >= 20 && bn !== bassNote && t - lastBassT > minGap * 2) {
lastBassT = t;
bassNote = bn; N.bassOsc.frequency.setTargetAtTime(mtof(bn), t, 0.03); hit(N.bassAmp, 0.20, 0.6);
N.padOscs.forEach((o, i) => o.frequency.setTargetAtTime(mtof(bn + 12 + [0, 7, 16][i]), t, 0.5));
N.droneOscs.forEach((o, i) => o.frequency.setTargetAtTime(mtof(bn - 12 + [0, 7][i]), t, 0.5));
}
@ -3812,8 +3928,8 @@
const bpm = Math.max(30, godtime.bpm + nudge), six = 60 / bpm / 4;
while (nextAt < now + 0.1) {
const t = Math.max(nextAt, now), dens = d("perc.density", 0);
if (Math.random() < dens * 0.9) noiseHit(7000, 0.05, 0.09 + dens * 0.09);
if (step % 4 === 0 && Math.random() < 0.4 + d("granular.density", 0) * 0.4) noiseHit(2800, 0.03, 0.05);
if (Math.random() < dens * 0.9) noiseHit(7000, 0.05, 0.06 + dens * 0.07);
if (step % 4 === 0 && Math.random() < 0.3 + d("granular.density", 0) * 0.4) noiseHit(2800, 0.03, 0.04);
if (Math.random() < d("glitch", 0) * 0.18) {
const g = N.glitch.gain; g.setValueAtTime(1, t); g.setValueAtTime(0, t + 0.02); g.setValueAtTime(1, t + 0.055);
}