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 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;