diff --git a/viz/index.html b/viz/index.html index 13770d9..c5e3992 100644 --- a/viz/index.html +++ b/viz/index.html @@ -378,6 +378,10 @@ #godspeakrec.on { display: flex; animation: gspk-pulse 1.1s ease-in-out infinite; } @keyframes gspk-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } } + /* πŸ“Ό TESTAMENT β€” the record button; red + gently pulsing while it bears witness */ + #testament.on { color: #ff8a8a !important; border-color: rgba(240,110,110,0.6) !important; + background: rgba(230,90,90,0.2) !important; animation: gspk-pulse 1.5s ease-in-out infinite; font-variant-numeric: tabular-nums; } + /* right-click context menu β€” every setting, on everything */ #ctxmenu { position: fixed; z-index: 30; display: none; width: 240px; max-height: 72vh; @@ -7797,6 +7801,8 @@ eventTicker.unshift({ text: ok ? "πŸŽ› GODSONIQ β€” sampling the world for 3s… then every pluck replays it, pitched across the keys (🎸 string voice to exit)" : "πŸŽ› GODSONIQ needs the audio worklet β€” press β™ͺ first, then try again", tAdded: now(), src: "sys" }); }); ctxItem("🌾 AMPLER β€” there is ample (sample the world into the kit)", () => openAmpler()); + ctxItem((Synth.testamentState && Synth.testamentState().on) ? "πŸ“Ό TESTAMENT β€” stop & save the take" : "πŸ“Ό TESTAMENT β€” record what god does", + () => { if (Synth.testament) Synth.testament(); }); ctxItem("⚑ GODSPEED β€” hold a chord, the machine runs", () => openGodspeed()); ctxItem("🌐 " + (Synth.feeding() ? "tab feed: live β€” click to stop" : "feed a tab (YouTube, a stream…) into the world"), async () => { const r = await Synth.feedTab(); @@ -9202,7 +9208,7 @@ g.connect(pan); pan.connect(drumBus); drums[dv] = { g, pan }; } - return { out, squash, squashMakeup, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp, + return { out, squash, squashMakeup, lim, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp, bassOsc, bassAmp, padOscs, padFilt, padAmp, droneOscs, droneAmp, echoDelay, echoFilt, echoFb, echoWet, echoFlutAmt, hissGain, chorusWet, drumBus, drums, noiseBuf, duckG }; @@ -9626,7 +9632,69 @@ } }); + // ---- πŸ“Ό TESTAMENT β€” record what god does ("it is written") -------------- + // A MediaRecorder tapped off N.lim: the TRUE final mix, post-squash and + // post-limiter β€” exactly what you hear. Lazy (destination + recorder built + // on first roll), timesliced so a crash loses ≀1s. The GODSONIQ worklet + // Recorder is a different machine for a different purpose β€” untouched. + let testDest = null, testRec = null, testChunks = [], testT0 = 0, testUiTimer = null; + function testamentStart() { + if (!ctx) start(); // wake the graph β€” a recording can begin the session + if (testRec) return false; // already rolling + if (!testDest) { testDest = ctx.createMediaStreamDestination(); N.lim.connect(testDest); } + const mime = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4"] + .find(m => window.MediaRecorder && MediaRecorder.isTypeSupported(m)); + if (!mime) return false; // no MediaRecorder / no supported container (old Safari) + testChunks = []; testRec = new MediaRecorder(testDest.stream, { mimeType: mime, audioBitsPerSecond: 256000 }); + testRec.ondataavailable = (e) => { if (e.data && e.data.size) testChunks.push(e.data); }; + testRec.start(1000); // 1s timeslices + testT0 = ctx.currentTime; return true; + } + function testamentStop() { + return new Promise((resolve) => { + if (!testRec) { resolve(null); return; } + const rec = testRec, secs = ctx.currentTime - testT0; + const ext = rec.mimeType.indexOf("mp4") >= 0 ? "m4a" : "webm"; + rec.onstop = () => { const blob = new Blob(testChunks, { type: rec.mimeType }); testChunks = []; resolve({ blob, secs, ext }); }; + testRec = null; rec.stop(); + }); + } + function testamentState() { return { on: !!testRec, secs: testRec ? ctx.currentTime - testT0 : 0 }; } + + const testBtn = document.createElement("div"); + testBtn.id = "testament"; testBtn.textContent = "⏺"; testBtn.title = "TESTAMENT β€” record the master mix to a file"; + Object.assign(testBtn.style, { position: "fixed", top: "12px", right: "132px", zIndex: 10, + height: "30px", minWidth: "30px", padding: "0 7px", borderRadius: "8px", display: "flex", + alignItems: "center", justifyContent: "center", background: "rgba(20,28,44,0.7)", color: "#cba6a6", + cursor: "pointer", fontSize: "13px", border: "1px solid rgba(120,150,200,0.25)", + backdropFilter: "blur(6px)", WebkitBackdropFilter: "blur(6px)" }); + if (!AC || !window.MediaRecorder) { testBtn.style.opacity = "0.3"; testBtn.title = "this browser cannot record"; } + document.body.appendChild(testBtn); + const testFmt = (s) => { s = Math.max(0, Math.floor(s)); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); }; + function testPaint() { const st = testamentState(); testBtn.classList.toggle("on", st.on); testBtn.textContent = st.on ? "⏺ " + testFmt(st.secs) : "⏺"; } + function testamentToggle() { + if (testamentState().on) { + testamentStop().then((r) => { + if (testUiTimer) { clearInterval(testUiTimer); testUiTimer = null; } + testPaint(); + if (!r || !r.blob || !r.blob.size) return; + const d = new Date(), pad = (n) => String(n).padStart(2, "0"); + const stamp = "" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + "-" + pad(d.getHours()) + pad(d.getMinutes()); + const a = document.createElement("a"); a.href = URL.createObjectURL(r.blob); + a.download = "godstrument-testament-" + stamp + "." + r.ext; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(a.href), 4000); + eventTicker.unshift({ text: "πŸ“Ό testament: " + testFmt(r.secs) + " β€” it is written", tAdded: now(), src: "sys" }); + }); + } else { + if (!testamentStart()) { eventTicker.unshift({ text: "πŸ“Ό this browser cannot bear witness", tAdded: now(), src: "sys" }); return; } + testPaint(); testUiTimer = setInterval(testPaint, 1000); + } + } + testBtn.addEventListener("click", testamentToggle); + return { update(dests) { if (dests) Object.assign(D, dests); }, pluck, + testamentStart, testamentStop, testamentState, testament: testamentToggle, toggle() { on ? stop() : start(); paint(); return on; }, playing() { return on; }, stringVoice(v) { if (v === undefined) return ksMode; if (!ctx) start(); ksMode = !!v; sampleMode = false; return ksMode; }, godsoniq(secs) { return godsoniq(secs); }, sampling() { return sampleMode && !!sampleBuf; },