diff --git a/viz/index.html b/viz/index.html index edd7e00..030c5ce 100644 --- a/viz/index.html +++ b/viz/index.html @@ -105,6 +105,39 @@ border-radius: 6px; padding: 3px 10px; font-size: 11px; cursor: pointer; } + /* the arranger — tracks & clips for the casuals */ + #arranger { + position: fixed; left: 12px; right: 12px; bottom: 54px; z-index: 13; + background: rgba(10,15,26,0.95); border: 1px solid rgba(120,150,200,0.35); + border-radius: 12px; padding: 8px 10px; display: none; color: #c7d4ea; + font-size: 11px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); + max-height: 44vh; + } + #arranger.open { display: block; } + #arranger .atop { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } + #arranger .albl { color: rgba(150,170,200,0.65); font-size: 10px; flex: 1; } + #arranger .aclose { cursor: pointer; color: #8fa4c8; font-size: 16px; padding: 0 4px; } + #arranger .midibtn { + background: rgba(40,52,74,0.85); color: #b9c8e2; border: 1px solid rgba(120,150,200,0.25); + border-radius: 6px; padding: 3px 10px; font-size: 11px; cursor: pointer; + } + #arranger .midibtn.on { background: rgba(120,220,150,0.22); color: #bff0c9; border-color: rgba(130,230,160,0.55); } + #arranger .abody { max-height: calc(44vh - 44px); overflow-y: auto; } + #arranger .arow { display: flex; align-items: center; gap: 3px; margin: 2px 0; } + #arranger .aname { + width: 118px; min-width: 118px; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; color: #aebfda; font-size: 10px; + } + #arranger .acell { + flex: 1; height: 17px; border-radius: 3px; cursor: pointer; text-align: center; + line-height: 17px; font-size: 9px; font-weight: 700; user-select: none; + } + #arranger .acell.alive { background: rgba(100,180,140,0.18); color: rgba(140,220,170,0.7); } + #arranger .acell.aclip { background: rgba(90,130,230,0.4); color: #dbe6ff; } + #arranger .acell.amute { background: rgba(30,36,54,0.7); } + #arranger .acell:hover { outline: 1px solid rgba(150,180,230,0.5); } + #arranger .acell.acur { outline: 1px solid rgba(255,238,150,0.9); } + /* 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 { @@ -832,7 +865,8 @@
  • Click a wheel — go inside it (mind mode). In chakra view, click any chakra's core and the camera steps into the wheel: its mandala of petals turns around your whole field of view, and that chakra's feeds circle you — drag to look around the full 360°. A vector console appears under your hands: 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 Q] row, or by any connected MIDI keyboard — every note runs through the world's own reverb and grit, so you are jamming inside the instrument, in that chakra's mode, while the planet plays around you. travel the spine wheel to wheel; esc steps back out. And for powerful machines, right-click the sky and set graphics to ultra ✦: every orbiting feed grows a glowing comet trail, and the orbits breathe so the trails weave slow spirals.
  • Skins — the same spine, another cosmology. Right-click the sky and cycle skin. Beyond the seven chakras there is the planes: the Seven Planes & forty-nine sub-planes of the Cosmic Physical Plane from the Alice Bailey / D.K. teachings — I Divine (white light) down through Monadic (violet), Atmic, Buddhic, Mental (where the egoic lotus sits), Astral, to VII Physical (red). The spine becomes the 49-sub-plane spectrum ladder; the side-brackets mark the Monad, the Spiritual Triad and the Personality; 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.
  • One Source, many Spirits, one Matter. That is the whole architecture: the Source is the world's data, the same for everyone; the Spirit is the skin — the belief system through which you read it; the Matter 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 Tree of Life (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); the Lodge (from published Masonic sources only — Worshipful Master in the East, Wardens West and South, the North dark, Boaz and Jachin at the porch where your own hands 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 Hermetic ladder of the seven planetary gates, lead to silver; Yggdrasil and its nine worlds, the dwarves keeping the markets in Nidavellir; the Sufi lata'if, where the wind data literally breathes at Ruh, spirit-as-breath; and the Wu Xing, five phases in their generating ring and controlling star, the metal birds of the air routed to Metal; and the Spread — the Celtic Cross of the tarot (Waite's published layout), dealt live and never finished: the heavens in what crowns you, the deep debts and quake-roots in what is beneath you, the retrogrades in what is behind you, births and seasons in what is before you, money and fire in your hopes and fears — and your own hands in the Querent's place, because you are the one asking. Right-click the sky → skin 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. Newest on the shelf: the wheel — the astrologer's own view, twelve signs in a circle with 0° Aries at the ascendant's place, every feed routed to its sign by traditional rulership (wildfires to Aries, your holdings to Taurus, the word to Gemini, the far-travelling planes to Sagittarius…), the live planets riding the rim, your natal chart on the inner ring, and the aspects drawn as chords across the circle — red the squares and oppositions, green the trines and sextiles, gold the conjunctions, dashed where the living sky strikes your natal points. And everywhere else, those same aspects now draw as faint cables between the planets' own spheres in any skin — the sky's geometry made visible in whatever cosmology you're standing in. And every Spirit now speaks its own tongue: the sephirot in Hebrew (כתר, חכמה, בינה…), the chakras in Devanagari (सहस्रार, अनाहत…), the lata'if in Arabic (قلب, روح, سر…), the nine worlds in Old Norse (ásgarðr, miðgarðr, helheimr…), the planetary gates in their glyphs (☽ ☿ ♀ ☉ ♂ ♃ ♄), the five phases in their characters (木 火 土 金 水). The lore, in the letters it was written in. Two more Spirits joined the shelf: the runes — the Elder Futhark, all twenty-four in the Kylver-stone row, ringed as a rune-band, every feed seated by its rune-poem meaning (wealth's strife at ᚠ fehu, Odin's word at ᚨ ansuz, the giant's blow at ᚦ thurisaz for the quakes, need's narrow band at ᚾ nauthiz for hunger and want, the lot-cup ᛈ perthro holding your natal fate, and your raised hands at ᛉ algiz, the warding rune that is itself a figure with arms lifted); and the Wheel of the Year — the eight Celtic festivals at their true solar longitudes (the astronomical reckoning: quarters at the equinoxes and solstices, cross-quarters at fifteen degrees of the fixed signs), deaths and the deep things at Samhain where the veil thins, births at Imbolc, the fires and the dance and the bond at Beltane, the great fair of commerce and chatter at Lughnasadh — with the live sun walking the rim, showing exactly where the year stands today. Honesty note kept from the scholarship: the unified eightfold wheel is a modern synthesis of the attested Irish fire festivals and older solstice customs — true lore, honestly labeled.
  • -
  • The groove grids — sequence the voices. Open any voice's tab (shift-click it) and there's a 16-step grid on the godtime clock. The note voices (lead, bass) get a mini piano roll — eight rows of minor pentatonic, click to place a note, click again to clear — and while your groove is on, your melody replaces the data's: bitcoin lets go of the lead and you take it. Every other voice gets an 808-style gate row 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 save with your patch. And the sources get the grid too, with a different power: sample & hold — while a source's groove is on, the world's number is only sampled on your lit steps and held between them, so a smooth drifting feed becomes stepped, rhythmic, quantized in time. Bitcoin arpeggiating in your pattern. The wind as a sequence. If an out port is chosen in ⚙, godtime also sends MIDI clock (24 ppqn), so a DAW can slave its tempo to the planet — or to your pulse. And the grids play outward too: with an out port chosen, piano-roll notes go out as real MIDI on the lead and bass channels (each note one sixteenth long), and the mind-mode console plays on its own third channel — so your grooves and your chakra-scale jams drive Ableton, Strudel, or hardware exactly as they drive the built-in synth. The three mouths drink at once: ♪ for the browser synth, the out port for MIDI, both or neither — there is no switch, only where you point the sound.
  • +
  • The groove grids — sequence the voices. Open any voice's tab (shift-click it) and there's a 16-step grid on the godtime clock. The note voices (lead, bass) get a mini piano roll — eight rows of minor pentatonic, click to place a note, click again to clear — and while your groove is on, your melody replaces the data's: bitcoin lets go of the lead and you take it. Every other voice gets an 808-style gate row 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 save with your patch. And the sources get the grid too, with a different power: sample & hold — while a source's groove is on, the world's number is only sampled on your lit steps and held between them, so a smooth drifting feed becomes stepped, rhythmic, quantized in time. Bitcoin arpeggiating in your pattern. The wind as a sequence. If an out port is chosen in ⚙, godtime also sends MIDI clock (24 ppqn), so a DAW can slave its tempo to the planet — or to your pulse. And the grids play outward too: with an out port chosen, piano-roll notes go out as real MIDI on the lead and bass channels (each note one sixteenth long), and the mind-mode console plays on its own third channel — so your grooves and your chakra-scale jams drive Ableton, Strudel, or hardware exactly as they drive the built-in synth. The three mouths drink at once: ♪ for the browser synth, the out port for MIDI, both or neither — there is no switch, only where you point the sound.
  • +
  • Press T — tracks, for making actual songs. A conventional tracks-and-clips view for anyone who finds the cathedral overwhelming: every voice is a track, eight bars run across, and each cell cycles ● live (the world plays that lane) → ABC (clip patterns on the groove grids — A and B come pre-seeded so it makes music immediately) → silent. Double-click any clip to edit its groove in the piano roll. Press and the arrangement loops on the godtime clock — the planet still plays through your song, because live lanes stay wired to the world. Arrangements save with your patch, play out over MIDI like everything else, and export .mid hands you a Standard MIDI File of the note lanes to drag into Ableton, Logic, anything. Not a DAW — a sketchpad where the world sits in on your session.
  • @@ -1138,6 +1172,192 @@ steps: isNote ? new Array(16).fill(-1) : new Array(16).fill(1), }); } + // ---- the arranger — tracks & clips for the casuals ----------------------- + // Ableton-shaped, not Ableton-sized: tracks are the voices, a clip is a bar + // of groove (A/B/C patterns on the existing grids), "live" means the world + // plays that lane, dark means silence. Eight bars, looping on godtime. The + // Source stays live; you arrange when the Matter speaks. + const arr = { open: false, playing: false, bars: 8, bar: -1, grid: {}, banks: {}, prevMute: {} }; + let arrPaintBar = null; // the panel's playhead painter, when open + let arrBarAbs = -1; // absolute bar counter off the godtime clock + function arrBank(key, isNote) { + if (!arr.banks[key]) { + const mk2 = (fill) => new Array(16).fill(fill); + const A = mk2(isNote ? -1 : 1), B = mk2(isNote ? -1 : 0), C = mk2(isNote ? -1 : 1); + if (isNote) { [0, 4, 8, 12].forEach(s2 => A[s2] = 0); [0, 3, 6, 8, 11, 14].forEach(s2 => B[s2] = (s2 % 2) ? 3 : 0); } + else { for (let s2 = 0; s2 < 16; s2 += 2) B[s2] = 1; [3, 7, 11, 15].forEach(s2 => C[s2] = 0); } + arr.banks[key] = { A, B, C }; + } + return arr.banks[key]; + } + function arrLane(key) { + return arr.grid[key] || (arr.grid[key] = new Array(arr.bars).fill("live")); + } + function applyArrColumn() { + for (const key in arr.grid) { + const n = dests.get(key); + if (!n) continue; + const st = arr.grid[key][arr.bar] || "live"; + const t2 = tw(key); + if (st === "mute") { + t2.mute = 1; + const sq = seqs[key]; if (sq) sq.on = false; + } else if (st === "live") { + t2.mute = 0; + const sq = seqs[key]; if (sq) sq.on = false; + } else { // a clip letter — play that pattern + t2.mute = 0; + const sq = seqFor(key, n.isNote); + sq.steps = arrBank(key, n.isNote)[st]; // shared reference: grid edits write through + sq.on = true; + } + } + if (arrPaintBar) arrPaintBar(); + } + function arrPlay() { + if (arr.playing) return; + arr.playing = true; + arr.prevMute = {}; + for (const key in arr.grid) arr.prevMute[key] = tw(key).mute; + arr.bar = -1; // next bar boundary starts the song + } + function arrStop() { + if (!arr.playing) return; + arr.playing = false; arr.bar = -1; + for (const key in arr.grid) { // hand the voices back to the world + tw(key).mute = arr.prevMute[key] || 0; + const sq = seqs[key]; if (sq) sq.on = false; + } + if (arrPaintBar) arrPaintBar(); + } + + // ---- export the arrangement as a Standard MIDI File (type 1, 96 ppq) ----- + function midVarlen(n) { + const out = [n & 0x7f]; + while ((n >>= 7)) out.unshift((n & 0x7f) | 0x80); + return out; + } + function exportMid() { + const PPQ = 96, TICKS16 = PPQ / 4; + const trks = []; + // tempo track + const uspq = Math.round(60000000 / clamp(godtime.bpm, 40, 180)); + trks.push([[0, 0xff, 0x51, 0x03, (uspq >> 16) & 255, (uspq >> 8) & 255, uspq & 255], + [0, 0xff, 0x2f, 0x00]]); + // one track per note lane that has any clip in the grid + for (const key in arr.grid) { + const n = dests.get(key); + if (!n || !n.isNote) continue; + const ch = /bass/.test(key) ? 1 : 0, root = seqRootFor(key); + const ev = []; + for (let b = 0; b < arr.bars; b++) { + const st = arr.grid[key][b]; + if (st !== "A" && st !== "B" && st !== "C") continue; + const steps = arrBank(key, true)[st]; + for (let s2 = 0; s2 < 16; s2++) { + if (steps[s2] < 0) continue; + const note = root + SEQ_ROWS[steps[s2]], tick = (b * 16 + s2) * TICKS16; + ev.push([tick, 0x90 | ch, note, 100], [tick + TICKS16 - 2, 0x80 | ch, note, 0]); + } + } + if (!ev.length) continue; + ev.sort((a, b2) => a[0] - b2[0]); + const bytes = []; + let last = 0; + for (const e of ev) { + bytes.push(...midVarlen(e[0] - last), ...e.slice(1)); + last = e[0]; + } + bytes.push(0, 0xff, 0x2f, 0x00); + trks.push([bytes]); + } + const flat = (t2) => t2.length === 1 ? t2[0] : t2.reduce((a, e) => a.concat(e), []); + const chunks = []; + const push32 = (a, v) => a.push((v >> 24) & 255, (v >> 16) & 255, (v >> 8) & 255, v & 255); + const head = [0x4d, 0x54, 0x68, 0x64]; push32(head, 6); + head.push(0, 1, (trks.length >> 8) & 255, trks.length & 255, (PPQ >> 8) & 255, PPQ & 255); + chunks.push(head); + for (const t2 of trks) { + const body = flat(t2); + const h2 = [0x4d, 0x54, 0x72, 0x6b]; push32(h2, body.length); + chunks.push(h2, body); + } + const all = chunks.reduce((a, c) => a.concat(c), []); + const blob = new Blob([new Uint8Array(all)], { type: "audio/midi" }); + const a2 = document.createElement("a"); + a2.href = URL.createObjectURL(blob); + a2.download = "godstrument-track.mid"; + a2.click(); + setTimeout(() => URL.revokeObjectURL(a2.href), 5000); + eventTicker.unshift({ text: "🎛 arrangement exported — drag the .mid into any DAW", tAdded: now(), src: "sys" }); + } + + // the arranger panel — tracks down the left, eight bars across + const arrEl = document.createElement("div"); + arrEl.id = "arranger"; + document.body.appendChild(arrEl); + function closeArranger() { arr.open = false; arrEl.classList.remove("open"); arrPaintBar = null; } + function openArranger() { + arr.open = true; + arrEl.innerHTML = ""; + const bar0 = document.createElement("div"); bar0.className = "atop"; + const play = document.createElement("button"); play.className = "midibtn"; + const paintPlay = () => { play.textContent = arr.playing ? "■ stop" : "▶ play"; play.classList.toggle("on", arr.playing); }; + paintPlay(); + play.onclick = () => { arr.playing ? arrStop() : arrPlay(); paintPlay(); }; + const exp = document.createElement("button"); exp.className = "midibtn"; exp.textContent = "export .mid"; + exp.title = "a Standard MIDI File of the note lanes — drag it into any DAW"; + exp.onclick = exportMid; + const lbl = document.createElement("span"); lbl.className = "albl"; + lbl.textContent = "tracks · click a cell: live → A → B → C → silent · double-click a clip to edit its groove · loops 8 bars on godtime"; + const x2 = document.createElement("span"); x2.className = "aclose"; x2.textContent = "×"; + x2.onclick = closeArranger; + bar0.appendChild(play); bar0.appendChild(exp); bar0.appendChild(lbl); bar0.appendChild(x2); + arrEl.appendChild(bar0); + + const body = document.createElement("div"); body.className = "abody"; + arrEl.appendChild(body); + const CYCLE = ["live", "A", "B", "C", "mute"]; + const cellRefs = []; + const keys2 = Array.from(dests.keys()).sort(); + for (const key of keys2) { + const n = dests.get(key); + const row = document.createElement("div"); row.className = "arow"; + const name = document.createElement("span"); name.className = "aname"; + name.textContent = prettyDest(key); + name.title = key + (n.isNote ? " · note lane (exports to .mid)" : " · texture lane (gates the data)"); + row.appendChild(name); + const lane = arrLane(key); + for (let b = 0; b < arr.bars; b++) { + const cell = document.createElement("div"); cell.className = "acell"; + const paint = () => { + const st = lane[b]; + cell.textContent = st === "live" ? "●" : st === "mute" ? "" : st; + cell.className = "acell " + (st === "live" ? "alive" : st === "mute" ? "amute" : "aclip"); + if (arr.playing && b === arr.bar) cell.classList.add("acur"); + }; + cell.onclick = () => { + lane[b] = CYCLE[(CYCLE.indexOf(lane[b]) + 1) % CYCLE.length]; + if (arr.playing && b === arr.bar) applyArrColumn(); + paint(); + }; + cell.ondblclick = () => { + const st = lane[b]; + if (st !== "A" && st !== "B" && st !== "C") return; + const sq = seqFor(key, n.isNote); // load the clip into the groove grid + sq.steps = arrBank(key, n.isNote)[st]; // shared ref — edits write through + openInspector(n); + }; + paint(); + cellRefs.push(paint); + row.appendChild(cell); + } + body.appendChild(row); + } + arrPaintBar = () => { for (const p2 of cellRefs) p2(); paintPlay(); }; + arrEl.classList.add("open"); + } + function seqTick() { for (const k in seqs) { const sq = seqs[k]; @@ -1252,6 +1472,7 @@ 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 + arr: { grid: JSON.parse(JSON.stringify(arr.grid)), banks: JSON.parse(JSON.stringify(arr.banks)) }, bpm: godtime.bpm, }; } @@ -1267,6 +1488,11 @@ 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 (p.arr && p.arr.grid && p.arr.banks) { // the arrangement travels too + arr.grid = JSON.parse(JSON.stringify(p.arr.grid)); + arr.banks = JSON.parse(JSON.stringify(p.arr.banks)); + if (arr.open) openArranger(); // rebuild the panel on the new song + } if (typeof p.bpm === "number") godtime.bpm = clamp(p.bpm, 40, 180); layoutDirty = true; } @@ -1656,6 +1882,16 @@ const st16 = Math.floor(seqPhase) % 16; if (st16 !== seqCurStep) { seqCurStep = st16; seqTick(); } + // the arrangement advances one column per bar + const barAbs = Math.floor(seqPhase / 16); + if (barAbs !== arrBarAbs) { + arrBarAbs = barAbs; + if (arr.playing) { + arr.bar = (arr.bar + 1) % arr.bars; + applyArrColumn(); + } + } + // MIDI clock out — 24 ppqn (0xF8) whenever an out port is chosen, so a DAW // can slave to godtime. (rAF-timed: fine for lights & loose sync; a // lookahead scheduler is the upgrade path if tight sync ever matters.) @@ -3596,6 +3832,9 @@ if (!chakraMode) { layoutDirty = true; exitMind(); } // sources glide home else clearTrails(); } + if (ev.key === "t" || ev.key === "T") { + arr.open ? closeArranger() : openArranger(); // tracks, for the casuals + } }); // --------------------------------------------------------------------------- @@ -3711,6 +3950,8 @@ () => { setSkin(nextSkinKey()); if (!chakraMode) { chakraMode = true; clearTrails(); } }); ctxItem(performMode ? "leave performance mode" : "performance mode", () => window.dispatchEvent(new KeyboardEvent("keydown", { key: "p" })), "P"); + ctxItem(arr.open ? "close tracks" : "🎛 tracks — arrange clips", + () => { arr.open ? closeArranger() : openArranger(); }, "T"); if (mindChakra) ctxItem("step out of " + mindChakra.label, () => exitMind(), "esc"); const fxLabel = fxMode === "auto" ? "auto (" + (fxLevel > 1 ? "full" : "lite") + ")" : fxMode === "ultra" ? "ultra ✦ trails" : fxMode;