/* global React, ReactDOM, Portal, Gallery, Themes, Journal, Prints, About, Feedback, Atmosphere */ const { useState, useEffect, useRef } = React; const TWEAKS = /*EDITMODE-BEGIN*/{ "grainOpacity": 0.22, "whispersOn": true, "soundOn": false, "soundVolume": 0.35, "showAiThought": true, "palette": "violet-night" } /*EDITMODE-END*/; // Audio engine — Indian tanpura-like plucked drone + breath + occasional bowl // Plus a softer "namaste" bell exposed on window.__darshanBell, triggered on chamber enter. function useAmbientSound(on, volume) { const ctxRef = useRef(null); const masterRef = useRef(null); const reverbRef = useRef(null); const stopRef = useRef(() => {}); // ── set up / tear down on `on` flip ───────────────────────────── useEffect(() => { if (!on) { const ctx = ctxRef.current; const m = masterRef.current; if (ctx && m) { m.gain.cancelScheduledValues(ctx.currentTime); m.gain.linearRampToValueAtTime(0.0001, ctx.currentTime + 1.4); // suspend after fade so plucks stop scheduling setTimeout(() => { try { ctx.suspend(); } catch (e) {} }, 1600); } return; } // First-time setup if (!ctxRef.current) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; const ctx = new AC(); ctxRef.current = ctx; // ── master ── const master = ctx.createGain(); master.gain.value = 0; master.connect(ctx.destination); masterRef.current = master; // ── simple feedback-delay "reverb" send ── const reverbIn = ctx.createGain(); reverbIn.gain.value = 1; const delays = [0.087, 0.119, 0.151, 0.181].map((t, i) => { const d = ctx.createDelay(4); d.delayTime.value = t; const fb = ctx.createGain(); fb.gain.value = 0.58 - i * 0.03; const lp = ctx.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 2200; d.connect(lp).connect(fb).connect(d); reverbIn.connect(d); d.connect(master); return d; }); const reverbWet = ctx.createGain(); reverbWet.gain.value = 0.65; delays.forEach((d) => d.connect(reverbWet)); reverbWet.connect(master); reverbRef.current = reverbIn; // ── tanpura pluck (additive sine with harmonics, soft attack, long decay) ── // Traditional tanpura cycle: Pa - Sa - Sa - Sa (5th, then root x3) // Tuned to C (Sa = C2/C3, Pa = G2) const SA_LOW = 65.41; // C2 const PA = 98.00; // G2 const SA_MID = 130.81; // C3 const cycle = [PA, SA_MID, SA_MID, SA_LOW]; // pa, sa, sa, sa-low let cIdx = 0; const pluck = (freq, when, accent = 1.0) => { const harmonics = [ { mult: 1, gain: 0.50 }, { mult: 2, gain: 0.22 }, { mult: 3, gain: 0.12 }, { mult: 4, gain: 0.06 }, { mult: 5.02, gain: 0.04 }, // slight detune for sympathetic shimmer { mult: 6, gain: 0.025 } ]; // Envelope — soft attack, long exponential decay const env = ctx.createGain(); env.gain.setValueAtTime(0.0001, when); env.gain.linearRampToValueAtTime(0.28 * accent, when + 0.06); env.gain.exponentialRampToValueAtTime(0.0001, when + 7.0); env.connect(master); env.connect(reverbIn); // Soft body filter — keeps it warm const body = ctx.createBiquadFilter(); body.type = "lowpass"; body.frequency.setValueAtTime(2400, when); body.frequency.exponentialRampToValueAtTime(900, when + 6.5); body.Q.value = 0.4; body.connect(env); harmonics.forEach((h, i) => { const o = ctx.createOscillator(); o.type = "sine"; o.frequency.value = freq * h.mult; const g = ctx.createGain(); g.gain.value = h.gain * (0.92 + Math.random() * 0.16); // gentle pitch wobble (tanpura "jhanjhar" character) const lfo = ctx.createOscillator(); lfo.frequency.value = 0.18 + Math.random() * 0.22; const lfoG = ctx.createGain(); lfoG.gain.value = 0.4 + i * 0.15; lfo.connect(lfoG).connect(o.frequency); o.connect(g).connect(body); o.start(when); o.stop(when + 7.3); lfo.start(when); lfo.stop(when + 7.3); }); }; // ── breath / distant wind ── const bufLen = 3 * ctx.sampleRate; const buf = ctx.createBuffer(1, bufLen, ctx.sampleRate); const ch = buf.getChannelData(0); for (let i = 0; i < bufLen; i++) ch[i] = (Math.random() * 2 - 1) * 0.6; const noise = ctx.createBufferSource(); noise.buffer = buf; noise.loop = true; const noiseLP = ctx.createBiquadFilter(); noiseLP.type = "lowpass"; noiseLP.frequency.value = 340; const noiseHP = ctx.createBiquadFilter(); noiseHP.type = "highpass"; noiseHP.frequency.value = 80; const noiseGain = ctx.createGain(); noiseGain.gain.value = 0.03; const windLfo = ctx.createOscillator(); windLfo.frequency.value = 0.06; const windLfoG = ctx.createGain(); windLfoG.gain.value = 140; windLfo.connect(windLfoG).connect(noiseLP.frequency); // slow breath on amplitude const breathLfo = ctx.createOscillator(); breathLfo.frequency.value = 0.09; const breathLfoG = ctx.createGain(); breathLfoG.gain.value = 0.015; breathLfo.connect(breathLfoG).connect(noiseGain.gain); noise.connect(noiseHP).connect(noiseLP).connect(noiseGain).connect(master); noise.start(); windLfo.start(); breathLfo.start(); // ── tanpura pluck scheduler ── const PLUCK_INTERVAL = 2.6; // seconds between plucks let nextTime = ctx.currentTime + 0.6; const scheduler = setInterval(() => { if (!ctxRef.current) return; const lookahead = ctx.currentTime + 2.0; while (nextTime < lookahead) { const freq = cycle[cIdx % cycle.length]; // every 4th pluck is the deep Sa — slightly louder const accent = (cIdx % cycle.length === 3) ? 1.15 : 1.0; pluck(freq, nextTime, accent); cIdx++; // tiny rubato — slight humanisation nextTime += PLUCK_INTERVAL + (Math.random() - 0.5) * 0.18; } }, 250); // ── singing bowl (rare, deep, long-decay) ── const bowl = (when) => { const root = 220 + Math.random() * 40; // wandering A3-ish // Inharmonic bell ratios (Risset bell) const partials = [ { mult: 1.00, gain: 0.55, decay: 9 }, { mult: 2.00, gain: 0.30, decay: 7 }, { mult: 2.76, gain: 0.20, decay: 6 }, { mult: 4.07, gain: 0.10, decay: 5 }, { mult: 5.42, gain: 0.05, decay: 4.5 } ]; const mainEnv = ctx.createGain(); mainEnv.gain.value = 1; mainEnv.connect(master); mainEnv.connect(reverbIn); partials.forEach((p) => { const o = ctx.createOscillator(); o.type = "sine"; o.frequency.value = root * p.mult; const g = ctx.createGain(); g.gain.setValueAtTime(0.0001, when); g.gain.linearRampToValueAtTime(p.gain * 0.16, when + 0.005); g.gain.exponentialRampToValueAtTime(0.0001, when + p.decay); o.connect(g).connect(mainEnv); o.start(when); o.stop(when + p.decay + 0.2); }); }; const scheduleBowl = () => { if (!ctxRef.current) return; bowl(ctx.currentTime + 0.2); }; // first bowl ~30s after on, then every 60–110s const bowlT1 = setTimeout(function loop() { scheduleBowl(); const next = 60000 + Math.random() * 50000; bowlIv = setTimeout(loop, next); }, 30000); let bowlIv = bowlT1; // ── expose namaste bell for chamber-enter ── window.__darshanBell = () => { const c = ctxRef.current; const m = masterRef.current; if (!c || !m) return; if (c.state === "suspended") return; // only when sound is on const t = c.currentTime + 0.02; // brighter, soft bell — fundamental ~ E5 const root = 659.25; const partials = [ { mult: 1.00, gain: 0.40, decay: 4.5 }, { mult: 2.00, gain: 0.18, decay: 3.2 }, { mult: 2.76, gain: 0.12, decay: 2.6 }, { mult: 4.10, gain: 0.06, decay: 2.0 } ]; const send = c.createGain(); send.gain.value = 1; send.connect(m); if (reverbRef.current) send.connect(reverbRef.current); partials.forEach((p) => { const o = c.createOscillator(); o.type = "sine"; o.frequency.value = root * p.mult; const g = c.createGain(); g.gain.setValueAtTime(0.0001, t); g.gain.linearRampToValueAtTime(p.gain * 0.10, t + 0.004); g.gain.exponentialRampToValueAtTime(0.0001, t + p.decay); o.connect(g).connect(send); o.start(t); o.stop(t + p.decay + 0.2); }); }; stopRef.current = () => { clearInterval(scheduler); clearTimeout(bowlIv); }; ctx.resume(). then(() => { master.gain.cancelScheduledValues(ctx.currentTime); master.gain.linearRampToValueAtTime(volume, ctx.currentTime + 2.4); }). catch(() => {}); } else { // Already initialised — just resume + fade in const ctx = ctxRef.current; const m = masterRef.current; ctx.resume(). then(() => { m.gain.cancelScheduledValues(ctx.currentTime); m.gain.linearRampToValueAtTime(volume, ctx.currentTime + 1.6); }). catch(() => {}); } // NOTE: no teardown returned — we keep the engine alive across toggles. }, [on]); // ── separate effect for volume changes (no rebuild, no flicker) ── useEffect(() => { if (!on) return; const ctx = ctxRef.current; const m = masterRef.current; if (!ctx || !m) return; m.gain.cancelScheduledValues(ctx.currentTime); m.gain.linearRampToValueAtTime(volume, ctx.currentTime + 0.35); }, [volume, on]); // ── tear down on unmount only ── useEffect(() => { return () => { stopRef.current(); try { ctxRef.current && ctxRef.current.close(); } catch (e) {} try { delete window.__darshanBell; } catch (e) {} }; }, []); } function Veil() { const ref = useRef(null); const [done, setDone] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; let raf; const HOLD = 1600,FADE = 1600; const start = performance.now(); const tick = (now) => { const t = now - start; if (t < HOLD) { el.style.opacity = "1"; } else { const p = Math.min(1, (t - HOLD) / FADE); el.style.opacity = String(1 - p); if (p >= 1) {setDone(true);return;} } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); if (done) return null; return (