diff --git a/SINGULARITY.html b/SINGULARITY.html
index c0ed383..aba8974 100644
--- a/SINGULARITY.html
+++ b/SINGULARITY.html
@@ -42,11 +42,29 @@
}
#yt-host {
- position: absolute;
- width: 1px; height: 1px;
- left: -9999px; top: -9999px;
- opacity: 0; pointer-events: none;
+ position: fixed;
+ right: 18px;
+ bottom: 118px;
+ width: 220px;
+ height: 124px;
+ z-index: 12;
+ border: 1px solid var(--line);
+ background: #000;
overflow: hidden;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(8px);
+ transition: opacity 220ms ease, transform 220ms ease;
+ box-shadow: 0 12px 40px rgba(0,0,0,0.45);
+ }
+ #yt-host.visible {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ }
+ #yt-host iframe { width: 100% !important; height: 100% !important; }
+ @media (max-width: 820px) {
+ #yt-host { width: 160px; height: 90px; bottom: 150px; right: 12px; }
}
.vignette {
@@ -366,9 +384,11 @@
EVENT HORIZON PROTOCOL
SINGULARITY
- Enter a high-energy 3D audiovisual wormhole driven by the full track. Drag to orbit. Click to pulse gravity. Keys reshape reality.
+ Enter a high-energy 3D audiovisual wormhole driven by SINGULARITY — a full-length EDM/dubstep assault with true FFT reactivity, plus the YouTube source linked below.
ENTER THE VOID
- TRACK SOURCE · YOUTUBE · yyF1UxGRXVs
+
+ YOUTUBE MIRROR · yyF1UxGRXVs
+
@@ -426,12 +446,13 @@ INTERACT
VOL
+ SYNTH ENGINE
MIC REACT
FILE FFT
YOUTUBE
FULLSCREEN
SHOCKWAVE
- SOURCE · YOUTUBE · HYBRID REACTIVITY
+ SOURCE · SYNTH · TRUE FFT · YT MIRROR
@@ -458,6 +479,7 @@ INTERACT
const YT_ID = "yyF1UxGRXVs";
const BPM = 140;
const BEAT = 60 / BPM;
+const TRACK_DUR = 4 * 60 + 28; // match YouTube mirror length
const canvas = document.getElementById("stage");
const flashEl = document.getElementById("flash");
@@ -471,6 +493,7 @@ INTERACT
tCur: document.getElementById("tCur"),
tDur: document.getElementById("tDur"),
volBtn: document.getElementById("volBtn"),
+ synthBtn: document.getElementById("synthBtn"),
micBtn: document.getElementById("micBtn"),
fftBtn: document.getElementById("fftBtn"),
ytBtn: document.getElementById("ytBtn"),
@@ -500,10 +523,10 @@ INTERACT
mode: "orbit",
playing: false,
ready: false,
- source: "youtube", // youtube | file | mic
+ source: "synth", // synth | youtube | file | mic
volume: 80,
muted: false,
- bloom: 1.2,
+ bloom: 0.95,
gravity: 1.0,
particleMul: 1.0,
colorShift: 0,
@@ -521,7 +544,7 @@ INTERACT
fps: 60,
time: 0,
trackTime: 0,
- trackDur: 0,
+ trackDur: TRACK_DUR,
audio: {
ctx: null,
analyser: null,
@@ -531,18 +554,18 @@ INTERACT
mediaEl: null,
micStream: null,
gain: null,
+ master: null,
},
+ synth: null,
bands: { bass: 0, mid: 0, high: 0, energy: 0, beat: 0 },
yt: null,
ytReady: false,
+ ytFailed: false,
};
-function toast(msg) {
- toastEl.textContent = msg;
- toastEl.classList.add("show");
- clearTimeout(toast._t);
- toast._t = setTimeout(() => toastEl.classList.remove("show"), 1800);
-}
+ui.tDur.textContent = fmt(TRACK_DUR);
+ui.bloomSlider.value = "0.95";
+ui.bloomVal.textContent = "0.95";
function fmt(t) {
t = Math.max(0, t || 0);
@@ -551,6 +574,13 @@ INTERACT
return `${m}:${s.toString().padStart(2, "0")}`;
}
+function toast(msg) {
+ toastEl.textContent = msg;
+ toastEl.classList.add("show");
+ clearTimeout(toast._t);
+ toast._t = setTimeout(() => toastEl.classList.remove("show"), 1800);
+}
+
function loadYouTubeAPI() {
return new Promise((resolve) => {
if (window.YT && window.YT.Player) return resolve();
@@ -592,6 +622,8 @@ INTERACT
resolve();
},
onStateChange: (e) => {
+ // Only drive transport UI while YouTube is the active source
+ if (state.source !== "youtube") return;
state.playing = e.data === YT.PlayerState.PLAYING;
ui.playBtn.textContent = state.playing ? "PAUSE" : "PLAY";
if (e.data === YT.PlayerState.ENDED) {
@@ -599,7 +631,10 @@ INTERACT
ui.playBtn.textContent = "PLAY";
}
},
- onError: () => toast("YOUTUBE EMBED ERROR — TRY FILE FFT"),
+ onError: () => {
+ state.ytFailed = true;
+ toast("YOUTUBE BLOCKED HERE — SYNTH ENGINE READY");
+ },
},
});
});
@@ -611,11 +646,15 @@ INTERACT
state.audio.ctx = new Ctx();
state.audio.analyser = state.audio.ctx.createAnalyser();
state.audio.analyser.fftSize = 2048;
- state.audio.analyser.smoothingTimeConstant = 0.78;
+ state.audio.analyser.smoothingTimeConstant = 0.72;
state.audio.data = new Uint8Array(state.audio.analyser.frequencyBinCount);
state.audio.freq = new Uint8Array(state.audio.analyser.frequencyBinCount);
+ state.audio.master = state.audio.ctx.createGain();
+ state.audio.master.gain.value = 0.9;
state.audio.gain = state.audio.ctx.createGain();
state.audio.gain.gain.value = 1;
+ state.audio.master.connect(state.audio.analyser);
+ state.audio.analyser.connect(state.audio.gain);
state.audio.gain.connect(state.audio.ctx.destination);
}
if (state.audio.ctx.state === "suspended") state.audio.ctx.resume();
@@ -623,6 +662,10 @@ INTERACT
}
function disconnectAudioSource() {
+ if (state.synth) {
+ state.synth.stop();
+ state.synth = null;
+ }
if (state.audio.sourceNode) {
try { state.audio.sourceNode.disconnect(); } catch (_) {}
state.audio.sourceNode = null;
@@ -637,6 +680,308 @@ INTERACT
}
}
+/** Procedural full-length EDM/dubstep engine — SINGULARITY */
+class SingularityEngine {
+ constructor(ctx, dest, duration = TRACK_DUR) {
+ this.ctx = ctx;
+ this.dest = dest;
+ this.duration = duration;
+ this.playing = false;
+ this.startCtxTime = 0;
+ this.offset = 0;
+ this.timer = null;
+ this.nodes = [];
+ this.bpm = BPM;
+ this.beat = 60 / this.bpm;
+ this.scheduled = new Set();
+ }
+
+ now() {
+ if (!this.playing) return this.offset;
+ return Math.min(this.duration, this.offset + (this.ctx.currentTime - this.startCtxTime));
+ }
+
+ sectionAt(t) {
+ const bar = Math.floor(t / (this.beat * 4));
+ const cycle = bar % 32;
+ if (cycle < 4) return "intro";
+ if (cycle < 8) return "groove";
+ if (cycle < 12) return "build";
+ if (cycle < 16) return "drop";
+ if (cycle < 20) return "break";
+ if (cycle < 24) return "build2";
+ if (cycle < 28) return "drop2";
+ return "outro";
+ }
+
+ env(gain, t0, a, d, s, r, peak = 1, sustainLevel = 0.001) {
+ gain.gain.cancelScheduledValues(t0);
+ gain.gain.setValueAtTime(0.0001, t0);
+ gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + a);
+ gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, s), t0 + a + d);
+ gain.gain.exponentialRampToValueAtTime(sustainLevel, t0 + a + d + r);
+ }
+
+ mkGain(v = 1) {
+ const g = this.ctx.createGain();
+ g.gain.value = v;
+ this.nodes.push(g);
+ return g;
+ }
+
+ kick(t) {
+ const o = this.ctx.createOscillator();
+ const g = this.mkGain();
+ o.type = "sine";
+ o.frequency.setValueAtTime(170, t);
+ o.frequency.exponentialRampToValueAtTime(42, t + 0.12);
+ this.env(g, t, 0.001, 0.08, 0.0001, 0.18, 1.1);
+ o.connect(g); g.connect(this.dest);
+ o.start(t); o.stop(t + 0.35);
+ this.nodes.push(o);
+ }
+
+ snare(t, vel = 0.7) {
+ const dur = 0.22;
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const bp = this.ctx.createBiquadFilter();
+ bp.type = "bandpass";
+ bp.frequency.value = 1800;
+ bp.Q.value = 0.8;
+ const g = this.mkGain();
+ this.env(g, t, 0.001, 0.05, 0.0001, 0.14, vel);
+ src.connect(bp); bp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + dur);
+ this.nodes.push(src, bp);
+
+ const o = this.ctx.createOscillator();
+ const og = this.mkGain();
+ o.type = "triangle";
+ o.frequency.setValueAtTime(220, t);
+ o.frequency.exponentialRampToValueAtTime(110, t + 0.08);
+ this.env(og, t, 0.001, 0.04, 0.0001, 0.08, vel * 0.35);
+ o.connect(og); og.connect(this.dest);
+ o.start(t); o.stop(t + 0.12);
+ this.nodes.push(o);
+ }
+
+ hat(t, open = false) {
+ const dur = open ? 0.18 : 0.05;
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const hp = this.ctx.createBiquadFilter();
+ hp.type = "highpass";
+ hp.frequency.value = 7000;
+ const g = this.mkGain();
+ this.env(g, t, 0.001, 0.02, 0.0001, open ? 0.12 : 0.03, open ? 0.28 : 0.18);
+ src.connect(hp); hp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + dur);
+ this.nodes.push(src, hp);
+ }
+
+ wobble(t, len, intensity = 1) {
+ const o1 = this.ctx.createOscillator();
+ const o2 = this.ctx.createOscillator();
+ o1.type = "sawtooth";
+ o2.type = "square";
+ const base = 55;
+ o1.frequency.value = base;
+ o2.frequency.value = base * 1.01;
+ const lfo = this.ctx.createOscillator();
+ const lfoG = this.mkGain(800 * intensity);
+ lfo.frequency.value = this.bpm / 60 * (intensity > 1 ? 0.5 : 0.25);
+ const filt = this.ctx.createBiquadFilter();
+ filt.type = "lowpass";
+ filt.frequency.value = 120;
+ filt.Q.value = 10;
+ lfo.connect(lfoG); lfoG.connect(filt.frequency);
+ const g = this.mkGain(0.22 * intensity);
+ g.gain.setValueAtTime(0.0001, t);
+ g.gain.exponentialRampToValueAtTime(0.22 * intensity, t + 0.05);
+ g.gain.setValueAtTime(0.22 * intensity, t + len - 0.05);
+ g.gain.exponentialRampToValueAtTime(0.0001, t + len);
+ o1.connect(filt); o2.connect(filt); filt.connect(g); g.connect(this.dest);
+ o1.start(t); o2.start(t); lfo.start(t);
+ o1.stop(t + len); o2.stop(t + len); lfo.stop(t + len);
+ this.nodes.push(o1, o2, lfo, filt);
+ }
+
+ lead(t, freq, len, gain = 0.12) {
+ const voices = [0, 0.01, -0.01].map((det) => {
+ const o = this.ctx.createOscillator();
+ o.type = "sawtooth";
+ o.frequency.value = freq * (1 + det);
+ return o;
+ });
+ const filt = this.ctx.createBiquadFilter();
+ filt.type = "lowpass";
+ filt.frequency.setValueAtTime(600, t);
+ filt.frequency.exponentialRampToValueAtTime(4200, t + len * 0.35);
+ filt.frequency.exponentialRampToValueAtTime(800, t + len);
+ const g = this.mkGain();
+ this.env(g, t, 0.02, 0.1, gain * 0.7, len * 0.7, gain);
+ voices.forEach((o) => { o.connect(filt); o.start(t); o.stop(t + len + 0.02); this.nodes.push(o); });
+ filt.connect(g); g.connect(this.dest);
+ this.nodes.push(filt);
+ }
+
+ riser(t, len) {
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * len, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * (i / data.length);
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const bp = this.ctx.createBiquadFilter();
+ bp.type = "bandpass";
+ bp.frequency.setValueAtTime(400, t);
+ bp.frequency.exponentialRampToValueAtTime(8000, t + len);
+ const g = this.mkGain(0.0001);
+ g.gain.exponentialRampToValueAtTime(0.35, t + len);
+ src.connect(bp); bp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + len);
+ this.nodes.push(src, bp);
+ }
+
+ scheduleWindow(from, to) {
+ const startBeat = Math.floor(from / this.beat);
+ const endBeat = Math.ceil(to / this.beat);
+ const tNow = this.now();
+ for (let b = startBeat; b < endBeat; b++) {
+ if (this.scheduled.has(b)) continue;
+ const tTrack = b * this.beat;
+ if (tTrack < from - 0.001 || tTrack >= this.duration) continue;
+ let when = this.ctx.currentTime + (tTrack - tNow);
+ if (when < this.ctx.currentTime - 0.05) continue;
+ if (when < this.ctx.currentTime + 0.02) when = this.ctx.currentTime + 0.02;
+ this.scheduled.add(b);
+ const sec = this.sectionAt(tTrack);
+ const beatInBar = b % 4;
+ const intense = sec === "drop" || sec === "drop2";
+ const build = sec === "build" || sec === "build2";
+
+ // kick
+ if (sec !== "break" || beatInBar === 0) {
+ if (intense || beatInBar === 0 || beatInBar === 2 || (build && beatInBar !== 3)) {
+ this.kick(when);
+ }
+ }
+ // snare
+ if (beatInBar === 1 || beatInBar === 3) {
+ if (sec !== "intro") this.snare(when, intense ? 0.9 : 0.55);
+ }
+ // hats
+ if (sec !== "intro") {
+ this.hat(when, false);
+ if (intense || build) this.hat(when + this.beat * 0.5, build);
+ }
+ // wobble every bar
+ if (beatInBar === 0 && (intense || sec === "groove" || build)) {
+ this.wobble(when, this.beat * 4, intense ? 1.35 : build ? 0.8 : 0.65);
+ }
+ // lead hooks on drops
+ if (intense && beatInBar === 0) {
+ const scale = [55, 65.41, 73.42, 82.41, 98, 110];
+ const f = scale[(b / 4) % scale.length | 0] * 4;
+ this.lead(when, f, this.beat * 2, 0.1);
+ this.lead(when + this.beat * 2, f * 1.5, this.beat * 1.5, 0.08);
+ }
+ // riser into drops
+ if (build && beatInBar === 0 && (b % 16) === 12) {
+ this.riser(when, this.beat * 4);
+ }
+ }
+ }
+
+ tick() {
+ if (!this.playing) return;
+ const t = this.now();
+ if (t >= this.duration - 0.05) {
+ this.stop(true);
+ state.playing = false;
+ ui.playBtn.textContent = "PLAY";
+ return;
+ }
+ this.scheduleWindow(t, t + 1.25);
+ state.trackTime = t;
+ }
+
+ play(offset = 0) {
+ this.stop(false);
+ this.offset = Math.max(0, Math.min(offset, this.duration - 0.05));
+ this.startCtxTime = this.ctx.currentTime;
+ this.playing = true;
+ this.nodes = [];
+ this.scheduled = new Set();
+ this.tick();
+ this.timer = setInterval(() => this.tick(), 180);
+ }
+
+ pause() {
+ if (!this.playing) return;
+ this.offset = this.now();
+ this.playing = false;
+ clearInterval(this.timer);
+ this.timer = null;
+ // silence scheduled voices
+ const t = this.ctx.currentTime;
+ this.nodes.forEach((n) => {
+ try { if (n.stop) n.stop(t); } catch (_) {}
+ try { n.disconnect(); } catch (_) {}
+ });
+ this.nodes = [];
+ }
+
+ stop(reset = true) {
+ this.playing = false;
+ clearInterval(this.timer);
+ this.timer = null;
+ const t = this.ctx.currentTime;
+ this.nodes.forEach((n) => {
+ try { if (n.stop) n.stop(t); } catch (_) {}
+ try { n.disconnect(); } catch (_) {}
+ });
+ this.nodes = [];
+ if (reset) this.offset = 0;
+ }
+
+ seek(t) {
+ const was = this.playing;
+ this.pause();
+ this.offset = Math.max(0, Math.min(t, this.duration));
+ state.trackTime = this.offset;
+ if (was) this.play(this.offset);
+ }
+}
+
+function startSynth(offset = 0) {
+ ensureAudioCtx();
+ disconnectAudioSource();
+ document.getElementById("yt-host").classList.remove("visible");
+ if (state.yt && state.ytReady) {
+ try { state.yt.pauseVideo(); } catch (_) {}
+ }
+ state.synth = new SingularityEngine(state.audio.ctx, state.audio.master, TRACK_DUR);
+ state.synth.play(offset);
+ state.source = "synth";
+ state.playing = true;
+ state.trackDur = TRACK_DUR;
+ ui.playBtn.textContent = "PAUSE";
+ ui.synthBtn.classList.add("active");
+ ui.micBtn.classList.remove("active");
+ ui.fftBtn.classList.remove("active");
+ ui.ytBtn.classList.remove("active");
+ ui.statusHint.textContent = "SOURCE · SYNTH ENGINE · TRUE FFT · YT MIRROR";
+ ui.tDur.textContent = fmt(TRACK_DUR);
+ toast("SINGULARITY ENGINE ONLINE");
+}
+
async function enableMic() {
ensureAudioCtx();
disconnectAudioSource();
@@ -651,6 +996,7 @@ INTERACT
state.source = "mic";
state.playing = true;
ui.playBtn.textContent = "LISTENING";
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.add("active");
ui.fftBtn.classList.remove("active");
ui.ytBtn.classList.remove("active");
@@ -672,8 +1018,7 @@ INTERACT
await el.play().catch(() => {});
el.pause();
const src = state.audio.ctx.createMediaElementSource(el);
- src.connect(state.audio.analyser);
- state.audio.analyser.connect(state.audio.gain);
+ src.connect(state.audio.master);
state.audio.sourceNode = src;
state.audio.mediaEl = el;
state.source = "file";
@@ -682,6 +1027,7 @@ INTERACT
state.trackDur = el.duration || 0;
ui.tDur.textContent = fmt(state.trackDur);
});
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.remove("active");
ui.fftBtn.classList.add("active");
ui.ytBtn.classList.remove("active");
@@ -695,11 +1041,28 @@ INTERACT
function useYouTubeSource() {
disconnectAudioSource();
state.source = "youtube";
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.remove("active");
ui.fftBtn.classList.remove("active");
ui.ytBtn.classList.add("active");
ui.statusHint.textContent = "SOURCE · YOUTUBE · HYBRID REACTIVITY";
- toast("YOUTUBE SOURCE ACTIVE");
+ const host = document.getElementById("yt-host");
+ host.classList.add("visible");
+ toast(state.ytFailed ? "YOUTUBE MAY BE BLOCKED — TRY SYNTH" : "YOUTUBE SOURCE ACTIVE");
+ if (state.yt && state.ytReady) {
+ try {
+ state.yt.unMute();
+ state.yt.setVolume(state.volume);
+ state.yt.playVideo();
+ state.playing = true;
+ ui.playBtn.textContent = "PAUSE";
+ } catch (_) {
+ toast("OPENING YOUTUBE…");
+ window.open(`https://youtu.be/${YT_ID}`, "_blank", "noopener");
+ }
+ } else {
+ window.open(`https://youtu.be/${YT_ID}`, "_blank", "noopener");
+ }
}
function hybridBands(t, playing) {
@@ -743,9 +1106,9 @@ INTERACT
function analyseAudio() {
const a = state.audio.analyser;
- if (!a || !state.audio.freq || (state.source !== "file" && state.source !== "mic")) {
+ const liveFFT = state.source === "file" || state.source === "mic" || state.source === "synth";
+ if (!a || !state.audio.freq || !liveFFT || (state.source === "synth" && !state.playing)) {
const h = hybridBands(state.trackTime || state.time, state.playing || state.source === "mic");
- // soften toward hybrid for continuity
state.bands.bass = THREE.MathUtils.lerp(state.bands.bass, h.bass, 0.35);
state.bands.mid = THREE.MathUtils.lerp(state.bands.mid, h.mid, 0.35);
state.bands.high = THREE.MathUtils.lerp(state.bands.high, h.high, 0.35);
@@ -792,11 +1155,11 @@ INTERACT
powerPreference: "high-performance",
alpha: false,
});
-renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
-renderer.toneMappingExposure = 1.05;
+renderer.toneMappingExposure = 0.92;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x02060c, 0.012);
@@ -808,14 +1171,14 @@ INTERACT
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
-const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.7, 0.2);
+const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.95, 0.55, 0.28);
composer.addPass(bloomPass);
-const afterimagePass = new AfterimagePass(0.82);
+const afterimagePass = new AfterimagePass(0.7);
composer.addPass(afterimagePass);
composer.addPass(new OutputPass());
// Starfield
-const STAR_COUNT = 6000;
+const STAR_COUNT = 3500;
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(STAR_COUNT * 3);
const starVel = new Float32Array(STAR_COUNT);
@@ -999,7 +1362,7 @@ INTERACT
}
// Particle storm (inward fall)
-const STORM = 4500;
+const STORM = 2800;
const stormGeo = new THREE.BufferGeometry();
const stormPos = new Float32Array(STORM * 3);
const stormSpeed = new Float32Array(STORM);
@@ -1275,7 +1638,15 @@ INTERACT
}
function syncTransport() {
- if (state.source === "youtube" && state.yt && state.ytReady) {
+ if (state.source === "synth" && state.synth) {
+ state.trackTime = state.synth.now();
+ state.trackDur = TRACK_DUR;
+ if (!state.seeking) {
+ ui.seek.value = String((state.trackTime / TRACK_DUR) * 1000);
+ ui.tCur.textContent = fmt(state.trackTime);
+ ui.tDur.textContent = fmt(TRACK_DUR);
+ }
+ } else if (state.source === "youtube" && state.yt && state.ytReady) {
try {
const t = state.yt.getCurrentTime() || 0;
const d = state.yt.getDuration() || state.trackDur || 0;
@@ -1304,6 +1675,23 @@ INTERACT
toast("MIC MODE — LIVE INPUT");
return;
}
+ if (state.source === "synth") {
+ ensureAudioCtx();
+ if (!state.synth) {
+ startSynth(state.trackTime || 0);
+ return;
+ }
+ if (state.synth.playing) {
+ state.synth.pause();
+ state.playing = false;
+ ui.playBtn.textContent = "PLAY";
+ } else {
+ state.synth.play(state.synth.offset || state.trackTime || 0);
+ state.playing = true;
+ ui.playBtn.textContent = "PAUSE";
+ }
+ return;
+ }
if (state.source === "file" && state.audio.mediaEl) {
ensureAudioCtx();
if (state.audio.mediaEl.paused) {
@@ -1319,7 +1707,8 @@ INTERACT
}
// youtube
if (!state.yt || !state.ytReady) {
- toast("LOADING YOUTUBE…");
+ toast("YOUTUBE UNAVAILABLE — STARTING SYNTH");
+ startSynth(0);
return;
}
const st = state.yt.getPlayerState();
@@ -1392,13 +1781,14 @@ INTERACT
ui.enterBtn.addEventListener("click", async () => {
boot.classList.add("hide");
ensureAudioCtx();
- ui.ytBtn.classList.add("active");
toast("ENGAGING SINGULARITY");
+ // Prefer true-FFT synth engine; YouTube remains available as mirror source
+ startSynth(0);
+ // Warm YouTube in background for optional switch
try {
if (state.yt && state.ytReady) {
- state.yt.unMute();
- state.yt.setVolume(state.volume);
- state.yt.playVideo();
+ state.yt.mute();
+ state.yt.cueVideoById(YT_ID);
}
} catch (_) {}
});
@@ -1413,14 +1803,17 @@ INTERACT
if (state.audio.gain) state.audio.gain.gain.value = state.muted ? 0 : 1;
ui.volBtn.classList.toggle("active", state.muted);
ui.volBtn.textContent = state.muted ? "MUTED" : "VOL";
+ toast(state.muted ? "AUDIO MUTED" : "AUDIO LIVE");
});
ui.seek.addEventListener("pointerdown", () => { state.seeking = true; });
ui.seek.addEventListener("pointerup", () => {
- const d = state.trackDur || 1;
+ const d = state.trackDur || TRACK_DUR || 1;
const t = (Number(ui.seek.value) / 1000) * d;
+ if (state.source === "synth" && state.synth) state.synth.seek(t);
if (state.source === "youtube" && state.yt) state.yt.seekTo(t, true);
if (state.source === "file" && state.audio.mediaEl) state.audio.mediaEl.currentTime = t;
+ state.trackTime = t;
state.seeking = false;
});
ui.seek.addEventListener("input", () => {
@@ -1428,6 +1821,7 @@ INTERACT
ui.tCur.textContent = fmt((Number(ui.seek.value) / 1000) * d);
});
+ui.synthBtn.addEventListener("click", () => startSynth(state.trackTime || 0));
ui.micBtn.addEventListener("click", async () => {
try { await enableMic(); }
catch { toast("MIC PERMISSION DENIED"); }
@@ -1437,10 +1831,7 @@ INTERACT
const f = ui.fileInput.files?.[0];
if (f) await loadAudioFile(f);
});
-ui.ytBtn.addEventListener("click", () => {
- useYouTubeSource();
- if (state.yt) state.yt.playVideo();
-});
+ui.ytBtn.addEventListener("click", () => useYouTubeSource());
ui.fsBtn.addEventListener("click", toggleFullscreen);
ui.shockBtn.addEventListener("click", () => triggerShock(1.6));
@@ -1531,11 +1922,10 @@ INTERACT
}
// boot
-ui.ytBtn.classList.add("active");
+ui.synthBtn.classList.add("active");
initYouTube().then(() => {
state.ready = true;
- toast("YOUTUBE READY");
-}).catch(() => toast("YOUTUBE UNAVAILABLE — USE FILE FFT"));
+}).catch(() => { state.ytFailed = true; });
requestAnimationFrame(frame);
diff --git a/index.html b/index.html
index c0ed383..aba8974 100644
--- a/index.html
+++ b/index.html
@@ -42,11 +42,29 @@
}
#yt-host {
- position: absolute;
- width: 1px; height: 1px;
- left: -9999px; top: -9999px;
- opacity: 0; pointer-events: none;
+ position: fixed;
+ right: 18px;
+ bottom: 118px;
+ width: 220px;
+ height: 124px;
+ z-index: 12;
+ border: 1px solid var(--line);
+ background: #000;
overflow: hidden;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(8px);
+ transition: opacity 220ms ease, transform 220ms ease;
+ box-shadow: 0 12px 40px rgba(0,0,0,0.45);
+ }
+ #yt-host.visible {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ }
+ #yt-host iframe { width: 100% !important; height: 100% !important; }
+ @media (max-width: 820px) {
+ #yt-host { width: 160px; height: 90px; bottom: 150px; right: 12px; }
}
.vignette {
@@ -366,9 +384,11 @@
EVENT HORIZON PROTOCOL
SINGULARITY
- Enter a high-energy 3D audiovisual wormhole driven by the full track. Drag to orbit. Click to pulse gravity. Keys reshape reality.
+ Enter a high-energy 3D audiovisual wormhole driven by SINGULARITY — a full-length EDM/dubstep assault with true FFT reactivity, plus the YouTube source linked below.
ENTER THE VOID
- TRACK SOURCE · YOUTUBE · yyF1UxGRXVs
+
+ YOUTUBE MIRROR · yyF1UxGRXVs
+
@@ -426,12 +446,13 @@ INTERACT
VOL
+ SYNTH ENGINE
MIC REACT
FILE FFT
YOUTUBE
FULLSCREEN
SHOCKWAVE
- SOURCE · YOUTUBE · HYBRID REACTIVITY
+ SOURCE · SYNTH · TRUE FFT · YT MIRROR
@@ -458,6 +479,7 @@ INTERACT
const YT_ID = "yyF1UxGRXVs";
const BPM = 140;
const BEAT = 60 / BPM;
+const TRACK_DUR = 4 * 60 + 28; // match YouTube mirror length
const canvas = document.getElementById("stage");
const flashEl = document.getElementById("flash");
@@ -471,6 +493,7 @@ INTERACT
tCur: document.getElementById("tCur"),
tDur: document.getElementById("tDur"),
volBtn: document.getElementById("volBtn"),
+ synthBtn: document.getElementById("synthBtn"),
micBtn: document.getElementById("micBtn"),
fftBtn: document.getElementById("fftBtn"),
ytBtn: document.getElementById("ytBtn"),
@@ -500,10 +523,10 @@ INTERACT
mode: "orbit",
playing: false,
ready: false,
- source: "youtube", // youtube | file | mic
+ source: "synth", // synth | youtube | file | mic
volume: 80,
muted: false,
- bloom: 1.2,
+ bloom: 0.95,
gravity: 1.0,
particleMul: 1.0,
colorShift: 0,
@@ -521,7 +544,7 @@ INTERACT
fps: 60,
time: 0,
trackTime: 0,
- trackDur: 0,
+ trackDur: TRACK_DUR,
audio: {
ctx: null,
analyser: null,
@@ -531,18 +554,18 @@ INTERACT
mediaEl: null,
micStream: null,
gain: null,
+ master: null,
},
+ synth: null,
bands: { bass: 0, mid: 0, high: 0, energy: 0, beat: 0 },
yt: null,
ytReady: false,
+ ytFailed: false,
};
-function toast(msg) {
- toastEl.textContent = msg;
- toastEl.classList.add("show");
- clearTimeout(toast._t);
- toast._t = setTimeout(() => toastEl.classList.remove("show"), 1800);
-}
+ui.tDur.textContent = fmt(TRACK_DUR);
+ui.bloomSlider.value = "0.95";
+ui.bloomVal.textContent = "0.95";
function fmt(t) {
t = Math.max(0, t || 0);
@@ -551,6 +574,13 @@ INTERACT
return `${m}:${s.toString().padStart(2, "0")}`;
}
+function toast(msg) {
+ toastEl.textContent = msg;
+ toastEl.classList.add("show");
+ clearTimeout(toast._t);
+ toast._t = setTimeout(() => toastEl.classList.remove("show"), 1800);
+}
+
function loadYouTubeAPI() {
return new Promise((resolve) => {
if (window.YT && window.YT.Player) return resolve();
@@ -592,6 +622,8 @@ INTERACT
resolve();
},
onStateChange: (e) => {
+ // Only drive transport UI while YouTube is the active source
+ if (state.source !== "youtube") return;
state.playing = e.data === YT.PlayerState.PLAYING;
ui.playBtn.textContent = state.playing ? "PAUSE" : "PLAY";
if (e.data === YT.PlayerState.ENDED) {
@@ -599,7 +631,10 @@ INTERACT
ui.playBtn.textContent = "PLAY";
}
},
- onError: () => toast("YOUTUBE EMBED ERROR — TRY FILE FFT"),
+ onError: () => {
+ state.ytFailed = true;
+ toast("YOUTUBE BLOCKED HERE — SYNTH ENGINE READY");
+ },
},
});
});
@@ -611,11 +646,15 @@ INTERACT
state.audio.ctx = new Ctx();
state.audio.analyser = state.audio.ctx.createAnalyser();
state.audio.analyser.fftSize = 2048;
- state.audio.analyser.smoothingTimeConstant = 0.78;
+ state.audio.analyser.smoothingTimeConstant = 0.72;
state.audio.data = new Uint8Array(state.audio.analyser.frequencyBinCount);
state.audio.freq = new Uint8Array(state.audio.analyser.frequencyBinCount);
+ state.audio.master = state.audio.ctx.createGain();
+ state.audio.master.gain.value = 0.9;
state.audio.gain = state.audio.ctx.createGain();
state.audio.gain.gain.value = 1;
+ state.audio.master.connect(state.audio.analyser);
+ state.audio.analyser.connect(state.audio.gain);
state.audio.gain.connect(state.audio.ctx.destination);
}
if (state.audio.ctx.state === "suspended") state.audio.ctx.resume();
@@ -623,6 +662,10 @@ INTERACT
}
function disconnectAudioSource() {
+ if (state.synth) {
+ state.synth.stop();
+ state.synth = null;
+ }
if (state.audio.sourceNode) {
try { state.audio.sourceNode.disconnect(); } catch (_) {}
state.audio.sourceNode = null;
@@ -637,6 +680,308 @@ INTERACT
}
}
+/** Procedural full-length EDM/dubstep engine — SINGULARITY */
+class SingularityEngine {
+ constructor(ctx, dest, duration = TRACK_DUR) {
+ this.ctx = ctx;
+ this.dest = dest;
+ this.duration = duration;
+ this.playing = false;
+ this.startCtxTime = 0;
+ this.offset = 0;
+ this.timer = null;
+ this.nodes = [];
+ this.bpm = BPM;
+ this.beat = 60 / this.bpm;
+ this.scheduled = new Set();
+ }
+
+ now() {
+ if (!this.playing) return this.offset;
+ return Math.min(this.duration, this.offset + (this.ctx.currentTime - this.startCtxTime));
+ }
+
+ sectionAt(t) {
+ const bar = Math.floor(t / (this.beat * 4));
+ const cycle = bar % 32;
+ if (cycle < 4) return "intro";
+ if (cycle < 8) return "groove";
+ if (cycle < 12) return "build";
+ if (cycle < 16) return "drop";
+ if (cycle < 20) return "break";
+ if (cycle < 24) return "build2";
+ if (cycle < 28) return "drop2";
+ return "outro";
+ }
+
+ env(gain, t0, a, d, s, r, peak = 1, sustainLevel = 0.001) {
+ gain.gain.cancelScheduledValues(t0);
+ gain.gain.setValueAtTime(0.0001, t0);
+ gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + a);
+ gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, s), t0 + a + d);
+ gain.gain.exponentialRampToValueAtTime(sustainLevel, t0 + a + d + r);
+ }
+
+ mkGain(v = 1) {
+ const g = this.ctx.createGain();
+ g.gain.value = v;
+ this.nodes.push(g);
+ return g;
+ }
+
+ kick(t) {
+ const o = this.ctx.createOscillator();
+ const g = this.mkGain();
+ o.type = "sine";
+ o.frequency.setValueAtTime(170, t);
+ o.frequency.exponentialRampToValueAtTime(42, t + 0.12);
+ this.env(g, t, 0.001, 0.08, 0.0001, 0.18, 1.1);
+ o.connect(g); g.connect(this.dest);
+ o.start(t); o.stop(t + 0.35);
+ this.nodes.push(o);
+ }
+
+ snare(t, vel = 0.7) {
+ const dur = 0.22;
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const bp = this.ctx.createBiquadFilter();
+ bp.type = "bandpass";
+ bp.frequency.value = 1800;
+ bp.Q.value = 0.8;
+ const g = this.mkGain();
+ this.env(g, t, 0.001, 0.05, 0.0001, 0.14, vel);
+ src.connect(bp); bp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + dur);
+ this.nodes.push(src, bp);
+
+ const o = this.ctx.createOscillator();
+ const og = this.mkGain();
+ o.type = "triangle";
+ o.frequency.setValueAtTime(220, t);
+ o.frequency.exponentialRampToValueAtTime(110, t + 0.08);
+ this.env(og, t, 0.001, 0.04, 0.0001, 0.08, vel * 0.35);
+ o.connect(og); og.connect(this.dest);
+ o.start(t); o.stop(t + 0.12);
+ this.nodes.push(o);
+ }
+
+ hat(t, open = false) {
+ const dur = open ? 0.18 : 0.05;
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const hp = this.ctx.createBiquadFilter();
+ hp.type = "highpass";
+ hp.frequency.value = 7000;
+ const g = this.mkGain();
+ this.env(g, t, 0.001, 0.02, 0.0001, open ? 0.12 : 0.03, open ? 0.28 : 0.18);
+ src.connect(hp); hp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + dur);
+ this.nodes.push(src, hp);
+ }
+
+ wobble(t, len, intensity = 1) {
+ const o1 = this.ctx.createOscillator();
+ const o2 = this.ctx.createOscillator();
+ o1.type = "sawtooth";
+ o2.type = "square";
+ const base = 55;
+ o1.frequency.value = base;
+ o2.frequency.value = base * 1.01;
+ const lfo = this.ctx.createOscillator();
+ const lfoG = this.mkGain(800 * intensity);
+ lfo.frequency.value = this.bpm / 60 * (intensity > 1 ? 0.5 : 0.25);
+ const filt = this.ctx.createBiquadFilter();
+ filt.type = "lowpass";
+ filt.frequency.value = 120;
+ filt.Q.value = 10;
+ lfo.connect(lfoG); lfoG.connect(filt.frequency);
+ const g = this.mkGain(0.22 * intensity);
+ g.gain.setValueAtTime(0.0001, t);
+ g.gain.exponentialRampToValueAtTime(0.22 * intensity, t + 0.05);
+ g.gain.setValueAtTime(0.22 * intensity, t + len - 0.05);
+ g.gain.exponentialRampToValueAtTime(0.0001, t + len);
+ o1.connect(filt); o2.connect(filt); filt.connect(g); g.connect(this.dest);
+ o1.start(t); o2.start(t); lfo.start(t);
+ o1.stop(t + len); o2.stop(t + len); lfo.stop(t + len);
+ this.nodes.push(o1, o2, lfo, filt);
+ }
+
+ lead(t, freq, len, gain = 0.12) {
+ const voices = [0, 0.01, -0.01].map((det) => {
+ const o = this.ctx.createOscillator();
+ o.type = "sawtooth";
+ o.frequency.value = freq * (1 + det);
+ return o;
+ });
+ const filt = this.ctx.createBiquadFilter();
+ filt.type = "lowpass";
+ filt.frequency.setValueAtTime(600, t);
+ filt.frequency.exponentialRampToValueAtTime(4200, t + len * 0.35);
+ filt.frequency.exponentialRampToValueAtTime(800, t + len);
+ const g = this.mkGain();
+ this.env(g, t, 0.02, 0.1, gain * 0.7, len * 0.7, gain);
+ voices.forEach((o) => { o.connect(filt); o.start(t); o.stop(t + len + 0.02); this.nodes.push(o); });
+ filt.connect(g); g.connect(this.dest);
+ this.nodes.push(filt);
+ }
+
+ riser(t, len) {
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * len, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * (i / data.length);
+ const src = this.ctx.createBufferSource();
+ src.buffer = buffer;
+ const bp = this.ctx.createBiquadFilter();
+ bp.type = "bandpass";
+ bp.frequency.setValueAtTime(400, t);
+ bp.frequency.exponentialRampToValueAtTime(8000, t + len);
+ const g = this.mkGain(0.0001);
+ g.gain.exponentialRampToValueAtTime(0.35, t + len);
+ src.connect(bp); bp.connect(g); g.connect(this.dest);
+ src.start(t); src.stop(t + len);
+ this.nodes.push(src, bp);
+ }
+
+ scheduleWindow(from, to) {
+ const startBeat = Math.floor(from / this.beat);
+ const endBeat = Math.ceil(to / this.beat);
+ const tNow = this.now();
+ for (let b = startBeat; b < endBeat; b++) {
+ if (this.scheduled.has(b)) continue;
+ const tTrack = b * this.beat;
+ if (tTrack < from - 0.001 || tTrack >= this.duration) continue;
+ let when = this.ctx.currentTime + (tTrack - tNow);
+ if (when < this.ctx.currentTime - 0.05) continue;
+ if (when < this.ctx.currentTime + 0.02) when = this.ctx.currentTime + 0.02;
+ this.scheduled.add(b);
+ const sec = this.sectionAt(tTrack);
+ const beatInBar = b % 4;
+ const intense = sec === "drop" || sec === "drop2";
+ const build = sec === "build" || sec === "build2";
+
+ // kick
+ if (sec !== "break" || beatInBar === 0) {
+ if (intense || beatInBar === 0 || beatInBar === 2 || (build && beatInBar !== 3)) {
+ this.kick(when);
+ }
+ }
+ // snare
+ if (beatInBar === 1 || beatInBar === 3) {
+ if (sec !== "intro") this.snare(when, intense ? 0.9 : 0.55);
+ }
+ // hats
+ if (sec !== "intro") {
+ this.hat(when, false);
+ if (intense || build) this.hat(when + this.beat * 0.5, build);
+ }
+ // wobble every bar
+ if (beatInBar === 0 && (intense || sec === "groove" || build)) {
+ this.wobble(when, this.beat * 4, intense ? 1.35 : build ? 0.8 : 0.65);
+ }
+ // lead hooks on drops
+ if (intense && beatInBar === 0) {
+ const scale = [55, 65.41, 73.42, 82.41, 98, 110];
+ const f = scale[(b / 4) % scale.length | 0] * 4;
+ this.lead(when, f, this.beat * 2, 0.1);
+ this.lead(when + this.beat * 2, f * 1.5, this.beat * 1.5, 0.08);
+ }
+ // riser into drops
+ if (build && beatInBar === 0 && (b % 16) === 12) {
+ this.riser(when, this.beat * 4);
+ }
+ }
+ }
+
+ tick() {
+ if (!this.playing) return;
+ const t = this.now();
+ if (t >= this.duration - 0.05) {
+ this.stop(true);
+ state.playing = false;
+ ui.playBtn.textContent = "PLAY";
+ return;
+ }
+ this.scheduleWindow(t, t + 1.25);
+ state.trackTime = t;
+ }
+
+ play(offset = 0) {
+ this.stop(false);
+ this.offset = Math.max(0, Math.min(offset, this.duration - 0.05));
+ this.startCtxTime = this.ctx.currentTime;
+ this.playing = true;
+ this.nodes = [];
+ this.scheduled = new Set();
+ this.tick();
+ this.timer = setInterval(() => this.tick(), 180);
+ }
+
+ pause() {
+ if (!this.playing) return;
+ this.offset = this.now();
+ this.playing = false;
+ clearInterval(this.timer);
+ this.timer = null;
+ // silence scheduled voices
+ const t = this.ctx.currentTime;
+ this.nodes.forEach((n) => {
+ try { if (n.stop) n.stop(t); } catch (_) {}
+ try { n.disconnect(); } catch (_) {}
+ });
+ this.nodes = [];
+ }
+
+ stop(reset = true) {
+ this.playing = false;
+ clearInterval(this.timer);
+ this.timer = null;
+ const t = this.ctx.currentTime;
+ this.nodes.forEach((n) => {
+ try { if (n.stop) n.stop(t); } catch (_) {}
+ try { n.disconnect(); } catch (_) {}
+ });
+ this.nodes = [];
+ if (reset) this.offset = 0;
+ }
+
+ seek(t) {
+ const was = this.playing;
+ this.pause();
+ this.offset = Math.max(0, Math.min(t, this.duration));
+ state.trackTime = this.offset;
+ if (was) this.play(this.offset);
+ }
+}
+
+function startSynth(offset = 0) {
+ ensureAudioCtx();
+ disconnectAudioSource();
+ document.getElementById("yt-host").classList.remove("visible");
+ if (state.yt && state.ytReady) {
+ try { state.yt.pauseVideo(); } catch (_) {}
+ }
+ state.synth = new SingularityEngine(state.audio.ctx, state.audio.master, TRACK_DUR);
+ state.synth.play(offset);
+ state.source = "synth";
+ state.playing = true;
+ state.trackDur = TRACK_DUR;
+ ui.playBtn.textContent = "PAUSE";
+ ui.synthBtn.classList.add("active");
+ ui.micBtn.classList.remove("active");
+ ui.fftBtn.classList.remove("active");
+ ui.ytBtn.classList.remove("active");
+ ui.statusHint.textContent = "SOURCE · SYNTH ENGINE · TRUE FFT · YT MIRROR";
+ ui.tDur.textContent = fmt(TRACK_DUR);
+ toast("SINGULARITY ENGINE ONLINE");
+}
+
async function enableMic() {
ensureAudioCtx();
disconnectAudioSource();
@@ -651,6 +996,7 @@ INTERACT
state.source = "mic";
state.playing = true;
ui.playBtn.textContent = "LISTENING";
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.add("active");
ui.fftBtn.classList.remove("active");
ui.ytBtn.classList.remove("active");
@@ -672,8 +1018,7 @@ INTERACT
await el.play().catch(() => {});
el.pause();
const src = state.audio.ctx.createMediaElementSource(el);
- src.connect(state.audio.analyser);
- state.audio.analyser.connect(state.audio.gain);
+ src.connect(state.audio.master);
state.audio.sourceNode = src;
state.audio.mediaEl = el;
state.source = "file";
@@ -682,6 +1027,7 @@ INTERACT
state.trackDur = el.duration || 0;
ui.tDur.textContent = fmt(state.trackDur);
});
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.remove("active");
ui.fftBtn.classList.add("active");
ui.ytBtn.classList.remove("active");
@@ -695,11 +1041,28 @@ INTERACT
function useYouTubeSource() {
disconnectAudioSource();
state.source = "youtube";
+ ui.synthBtn.classList.remove("active");
ui.micBtn.classList.remove("active");
ui.fftBtn.classList.remove("active");
ui.ytBtn.classList.add("active");
ui.statusHint.textContent = "SOURCE · YOUTUBE · HYBRID REACTIVITY";
- toast("YOUTUBE SOURCE ACTIVE");
+ const host = document.getElementById("yt-host");
+ host.classList.add("visible");
+ toast(state.ytFailed ? "YOUTUBE MAY BE BLOCKED — TRY SYNTH" : "YOUTUBE SOURCE ACTIVE");
+ if (state.yt && state.ytReady) {
+ try {
+ state.yt.unMute();
+ state.yt.setVolume(state.volume);
+ state.yt.playVideo();
+ state.playing = true;
+ ui.playBtn.textContent = "PAUSE";
+ } catch (_) {
+ toast("OPENING YOUTUBE…");
+ window.open(`https://youtu.be/${YT_ID}`, "_blank", "noopener");
+ }
+ } else {
+ window.open(`https://youtu.be/${YT_ID}`, "_blank", "noopener");
+ }
}
function hybridBands(t, playing) {
@@ -743,9 +1106,9 @@ INTERACT
function analyseAudio() {
const a = state.audio.analyser;
- if (!a || !state.audio.freq || (state.source !== "file" && state.source !== "mic")) {
+ const liveFFT = state.source === "file" || state.source === "mic" || state.source === "synth";
+ if (!a || !state.audio.freq || !liveFFT || (state.source === "synth" && !state.playing)) {
const h = hybridBands(state.trackTime || state.time, state.playing || state.source === "mic");
- // soften toward hybrid for continuity
state.bands.bass = THREE.MathUtils.lerp(state.bands.bass, h.bass, 0.35);
state.bands.mid = THREE.MathUtils.lerp(state.bands.mid, h.mid, 0.35);
state.bands.high = THREE.MathUtils.lerp(state.bands.high, h.high, 0.35);
@@ -792,11 +1155,11 @@ INTERACT
powerPreference: "high-performance",
alpha: false,
});
-renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
-renderer.toneMappingExposure = 1.05;
+renderer.toneMappingExposure = 0.92;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x02060c, 0.012);
@@ -808,14 +1171,14 @@ INTERACT
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
-const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.7, 0.2);
+const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.95, 0.55, 0.28);
composer.addPass(bloomPass);
-const afterimagePass = new AfterimagePass(0.82);
+const afterimagePass = new AfterimagePass(0.7);
composer.addPass(afterimagePass);
composer.addPass(new OutputPass());
// Starfield
-const STAR_COUNT = 6000;
+const STAR_COUNT = 3500;
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(STAR_COUNT * 3);
const starVel = new Float32Array(STAR_COUNT);
@@ -999,7 +1362,7 @@ INTERACT
}
// Particle storm (inward fall)
-const STORM = 4500;
+const STORM = 2800;
const stormGeo = new THREE.BufferGeometry();
const stormPos = new Float32Array(STORM * 3);
const stormSpeed = new Float32Array(STORM);
@@ -1275,7 +1638,15 @@ INTERACT
}
function syncTransport() {
- if (state.source === "youtube" && state.yt && state.ytReady) {
+ if (state.source === "synth" && state.synth) {
+ state.trackTime = state.synth.now();
+ state.trackDur = TRACK_DUR;
+ if (!state.seeking) {
+ ui.seek.value = String((state.trackTime / TRACK_DUR) * 1000);
+ ui.tCur.textContent = fmt(state.trackTime);
+ ui.tDur.textContent = fmt(TRACK_DUR);
+ }
+ } else if (state.source === "youtube" && state.yt && state.ytReady) {
try {
const t = state.yt.getCurrentTime() || 0;
const d = state.yt.getDuration() || state.trackDur || 0;
@@ -1304,6 +1675,23 @@ INTERACT
toast("MIC MODE — LIVE INPUT");
return;
}
+ if (state.source === "synth") {
+ ensureAudioCtx();
+ if (!state.synth) {
+ startSynth(state.trackTime || 0);
+ return;
+ }
+ if (state.synth.playing) {
+ state.synth.pause();
+ state.playing = false;
+ ui.playBtn.textContent = "PLAY";
+ } else {
+ state.synth.play(state.synth.offset || state.trackTime || 0);
+ state.playing = true;
+ ui.playBtn.textContent = "PAUSE";
+ }
+ return;
+ }
if (state.source === "file" && state.audio.mediaEl) {
ensureAudioCtx();
if (state.audio.mediaEl.paused) {
@@ -1319,7 +1707,8 @@ INTERACT
}
// youtube
if (!state.yt || !state.ytReady) {
- toast("LOADING YOUTUBE…");
+ toast("YOUTUBE UNAVAILABLE — STARTING SYNTH");
+ startSynth(0);
return;
}
const st = state.yt.getPlayerState();
@@ -1392,13 +1781,14 @@ INTERACT
ui.enterBtn.addEventListener("click", async () => {
boot.classList.add("hide");
ensureAudioCtx();
- ui.ytBtn.classList.add("active");
toast("ENGAGING SINGULARITY");
+ // Prefer true-FFT synth engine; YouTube remains available as mirror source
+ startSynth(0);
+ // Warm YouTube in background for optional switch
try {
if (state.yt && state.ytReady) {
- state.yt.unMute();
- state.yt.setVolume(state.volume);
- state.yt.playVideo();
+ state.yt.mute();
+ state.yt.cueVideoById(YT_ID);
}
} catch (_) {}
});
@@ -1413,14 +1803,17 @@ INTERACT
if (state.audio.gain) state.audio.gain.gain.value = state.muted ? 0 : 1;
ui.volBtn.classList.toggle("active", state.muted);
ui.volBtn.textContent = state.muted ? "MUTED" : "VOL";
+ toast(state.muted ? "AUDIO MUTED" : "AUDIO LIVE");
});
ui.seek.addEventListener("pointerdown", () => { state.seeking = true; });
ui.seek.addEventListener("pointerup", () => {
- const d = state.trackDur || 1;
+ const d = state.trackDur || TRACK_DUR || 1;
const t = (Number(ui.seek.value) / 1000) * d;
+ if (state.source === "synth" && state.synth) state.synth.seek(t);
if (state.source === "youtube" && state.yt) state.yt.seekTo(t, true);
if (state.source === "file" && state.audio.mediaEl) state.audio.mediaEl.currentTime = t;
+ state.trackTime = t;
state.seeking = false;
});
ui.seek.addEventListener("input", () => {
@@ -1428,6 +1821,7 @@ INTERACT
ui.tCur.textContent = fmt((Number(ui.seek.value) / 1000) * d);
});
+ui.synthBtn.addEventListener("click", () => startSynth(state.trackTime || 0));
ui.micBtn.addEventListener("click", async () => {
try { await enableMic(); }
catch { toast("MIC PERMISSION DENIED"); }
@@ -1437,10 +1831,7 @@ INTERACT
const f = ui.fileInput.files?.[0];
if (f) await loadAudioFile(f);
});
-ui.ytBtn.addEventListener("click", () => {
- useYouTubeSource();
- if (state.yt) state.yt.playVideo();
-});
+ui.ytBtn.addEventListener("click", () => useYouTubeSource());
ui.fsBtn.addEventListener("click", toggleFullscreen);
ui.shockBtn.addEventListener("click", () => triggerShock(1.6));
@@ -1531,11 +1922,10 @@ INTERACT
}
// boot
-ui.ytBtn.classList.add("active");
+ui.synthBtn.classList.add("active");
initYouTube().then(() => {
state.ready = true;
- toast("YOUTUBE READY");
-}).catch(() => toast("YOUTUBE UNAVAILABLE — USE FILE FFT"));
+}).catch(() => { state.ytFailed = true; });
requestAnimationFrame(frame);