diff --git a/viz/index.html b/viz/index.html index c67a6e0..33ad3bb 100644 --- a/viz/index.html +++ b/viz/index.html @@ -898,7 +898,7 @@
  • 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. The panel earns its keep like one, too: a 🎛 tracks button sits bottom-right always; drag the panel's top edge to resize (remembered); the top bar tabs between clips and grooves — the grooves tab is a full-size pattern editor (pick a track, pick a letter, big piano roll or 16 steps; double-clicking any clip jumps you straight there); and right-click any track name for that voice's full routing menu — every cable, every level, patch and unpatch without leaving the panel.
  • 📽 The stage — send the visual to any screen. Right-click the sky → the stage and a clean black mirror window opens, fed live from the canvas. Drag it to a projector, an AirPlay display, a second monitor — macOS treats it like any window — and double-click for fullscreen. The stage has a camera: the whole view by default (whatever you're in — a skin, the wheel, mind mode's POV), or right-click any node → project this node and the camera glides after that one sphere through its orbits, or project its station to frame a whole wheel and its satellites. Change cameras live from the menus while the projector runs; the audience sees the camera breathe from subject to subject.
  • -
  • 🕸 The commune — play together. Right-click the sky → the commune, agree on a room word with your friends (any word — "coven" will do), and join. No pairing, no Bluetooth, no setup: everyone on the site is already connected to the same hub, and the hub simply carries your hands to each other — same living room or opposite sides of the earth. Then claim your sections: right-click any voice → claim this voice, and from that moment your levels, mutes, grooves and arranger lanes for it follow your hands on everyone's screen. One of you takes the bass and the fires, another the pads and the moon, and someone claims the godtime itself and conducts. Claims are visible to all, take-overs are announced, and the world — the Source — stays identical for everyone, because it always was. Three people, one planet, one instrument.
  • +
  • 🕸 The commune — play together. Right-click the sky → the commune, agree on a room word with your friends (any word — though the word is the key, so pick an uncommon one), and join. No pairing, no Bluetooth, no setup: everyone on the site is already connected to the same hub, and the hub simply carries your hands to each other — same living room or opposite sides of the earth. Then claim your sections: right-click any voice → claim this voice, and from that moment your levels, mutes, grooves and arranger lanes for it follow your hands on everyone's screen. One of you takes the bass and the fires, another the pads and the moon, and someone claims the godtime itself and conducts. Claims are visible to all, take-overs are announced, and the world — the Source — stays identical for everyone, because it always was. Three people, one planet, one instrument.
  • @@ -916,6 +916,8 @@

    The matrix pours out clean control values whether or not anyone is listening. Choosing how to hear it is choosing which instrument the planet plays.

    +

    Zero — the moods, the gentle front door. On your very first visit the instrument greets you with 🌅 first light instead of the full flood: a bass line, soft hats, a small melody, modest space — a thing shaped like a song, with the world still playing underneath it. Right-click the sky → 🌗 mood to move between first light, ambient drift (no drums, all atmosphere, the planet as weather), and the full storm — the factory instrument with every one of the world's hands on it at once. Ease in, then open the door.

    +

    One — the built-in browser synth. Click and a full voice rack wakes inside the page: lead (bitcoin's price and your market's average price both quantize to it — minor pentatonic and G dorian), bass (quake depth chooses its notes in a low minor), pad (brightened by Tokyo's warmth, daylight, harmony, the birth rate, longer lives), drone (thickened by aircraft aloft, barometric pressure, Saturn's slow wheel, the weight of the national debt), and perc (BTC volatility and market activity drive the hats). Over them sit the effects the world modulates: reverb (earthquakes open the void, room light sizes it), delay (your hand's Y feeds it, Mercury retrograde smears it), saturation (Delhi's air becomes grit, hard aspects add dissonance, wildfires burn it, hunger grinds beneath), and glitch (every Wikipedia edit on Earth, every light flash, every new wildfire fires a burst). Nothing to install; the planet plays a synth in your tab.

    Two — Web MIDI, out into a DAW or hardware. In Chrome or Edge, open the ⚙ panel's WEB MIDI section, enable MIDI, and pick an out port. Every destination now streams as CC or notes straight to your rig — no Python, no IAC bus. The map is fixed and MIDI-learnable at the far end:

    @@ -1303,6 +1305,10 @@ bytes.push(0, 0xff, 0x2f, 0x00); trks.push([bytes]); } + if (trks.length === 1) { // only the tempo track — nothing to hear yet + eventTicker.unshift({ text: "no note clips in the arrangement yet — place some A/B/C cells on lead or bass first", tAdded: now(), src: "sys" }); + return; + } 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); @@ -1581,8 +1587,8 @@ if (st.tw) { const t2 = tw(c.voice); t2.mute = st.tw.m; t2.gain = st.tw.g; t2.offset = st.tw.o; } if (st.steps && st.steps.length === 16) { const sq = seqFor(c.voice, n.isNote); - sq.steps.length = 0; Array.prototype.push.apply(sq.steps, st.steps); // in place: banks stay wired - sq.on = !!st.on; + sq.steps = st.steps.slice(); // replace the reference — never let a peer + sq.on = !!st.on; // overwrite one of YOUR arranger banks } if (st.lane && st.lane.length === arr.bars) arr.grid[c.voice] = st.lane.slice(); if (arrPaintBar) arrPaintBar(); @@ -1722,6 +1728,56 @@ } catch (e) {} } + // ---- moods — song-shaped presets, so first load greets instead of floods -- + // "first light" is the front door: a bass line, hats, a small melody, soft + // space — the world still plays underneath, just fewer of its hands at once. + const MOODS = { + "first light": { + bpm: 92, lock: true, + mutes: { "glitch": 1, "granular.density": 1, "saturation": 1, "tempo.nudge": 1 }, + gains: { "reverb.size": 0.7, "delay.feedback": 0.5, "drone.voices": 0.5, + "pad.brightness": 0.7, "perc.density": 1.3, "master.space": 0.7 }, + grooves: { + "bass.note": [0,-1,-1,-1, 0,-1,-1,-1, 1,-1,-1,-1, 2,-1, 1,-1], + "lead.note": [0,-1, 2,-1, 3,-1, 2,-1, 4,-1, 3, 2, 1,-1, 2,-1], + "perc.density": [1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1], + }, + }, + "ambient drift": { + bpm: 70, lock: true, clearGrooves: true, + mutes: { "glitch": 1, "granular.density": 1, "saturation": 1, "tempo.nudge": 1, + "perc.density": 1, "lead.note": 1, "bass.note": 1 }, + gains: { "reverb.size": 1.2, "master.space": 1.2, "pad.brightness": 1.0, + "drone.voices": 0.9, "delay.feedback": 0.8 }, + }, + "the full storm": { storm: true }, // the factory instrument, every hand at once + }; + const MOOD_KEYS = ["glitch", "granular.density", "saturation", "tempo.nudge", "wavetable.morph", + "lfo.rate", "reverb.size", "delay.feedback", "drone.voices", "pad.brightness", + "perc.density", "master.space", "lead.note", "bass.note"]; + function applyMood(name) { + const m = MOODS[name]; + if (!m) return; + for (const k of MOOD_KEYS) { const t2 = tw(k); t2.mute = 0; t2.gain = 1; t2.offset = 0; } + if (m.storm) { + for (const k in seqs) seqs[k].on = false; + godtime.locked = false; + } else { + for (const k in (m.mutes || {})) tw(k).mute = m.mutes[k]; + for (const k in (m.gains || {})) tw(k).gain = m.gains[k]; + if (m.clearGrooves) for (const k in seqs) seqs[k].on = false; + for (const k in (m.grooves || {})) { + const n = dests.get(k); + const sq = seqFor(k, n ? n.isNote : /note/.test(k)); + sq.steps = m.grooves[k].slice(); // a fresh array — never overwrite an arranger bank + sq.on = true; + } + if (m.bpm) godtime.bpm = m.bpm; + godtime.locked = !!m.lock; + } + eventTicker.unshift({ text: "🌗 mood: " + name, tAdded: now(), src: "sys" }); + } + // the launcher — one visible button, always there for the casuals const arrBtn = document.createElement("div"); arrBtn.id = "arrbtn"; @@ -2040,6 +2096,15 @@ if (Array.isArray(msg.routes)) { preLayoutRoutes(msg.routes); clientMatrix = true; // from here YOUR patch is computed locally + // first visit: greet gently — a song-shaped mood, not the full flood + let seen = false; try { seen = !!localStorage.getItem("gs_seen"); } catch (e) {} + if (!seen) { + try { localStorage.setItem("gs_seen", "1"); } catch (e) {} + setTimeout(() => { + applyMood("first light"); + eventTicker.unshift({ text: "🌅 first light — a gentle arrangement to start you off. right-click the sky → mood → the full storm when you're ready", tAdded: now(), src: "sys" }); + }, 900); + } } if (Array.isArray(msg.patches)) patchList = msg.patches; if (msg.groups) { groupsInfo = msg.groups; buildPanel(); } @@ -3244,10 +3309,10 @@ }; function wheelyearOf(key) { const k = (key || "").toLowerCase(), p = k.split(".")[0]; + if (p === "weather" || /season|harmony/.test(k)) return "ostara"; // the turning, the balance if (/deaths|quake|debt|poverty/.test(k) || p === "astro") return "samhain"; // the ancestors, the deep, the seer's night if (/light|ambient/.test(k)) return "yule"; // the returning light if (/births|fertile/.test(k) || p === "natal") return "imbolc"; // first stirrings; your birth - if (p === "weather" || /season|harmony/.test(k)) return "ostara"; // the turning, the balance if (p === "fire" || ["hand", "tof", "touch", "body", "syn"].includes(p)) return "beltane"; // the fires, the dance, the union if (p === "sun" || /^sky\./.test(k)) return "litha"; // the sun at its height if (["wiki", "audio", "market", "me", "crypto", "planes", "iss"].includes(p)) return "lughnasadh"; // the great fair @@ -4338,6 +4403,12 @@ () => window.dispatchEvent(new KeyboardEvent("keydown", { key: "p" })), "P"); ctxItem(arr.open ? "close tracks" : "🎛 tracks — arrange clips", () => { arr.open ? closeArranger() : openArranger(); }, "T"); + const moodSub = ctxSub("🌗 mood — how much world"); + for (const mn of Object.keys(MOODS)) { + const it2 = document.createElement("div"); it2.className = "it"; it2.textContent = mn; + it2.onclick = () => { applyMood(mn); closeCtx(); }; + moodSub.appendChild(it2); + } if (mindChakra) ctxItem("step out of " + mindChakra.label, () => exitMind(), "esc"); const fxLabel = fxMode === "auto" ? "auto (" + (fxLevel > 1 ? "full" : "lite") + ")" : fxMode === "ultra" ? "ultra ✦ trails" : fxMode;