/* Quantum UI sound design — hover hat on every menu word.
Web Audio for low-latency retrigger. Volume already -30% baked in the file. */
(function () {
var BASE = (location.pathname.indexOf('/it/') > -1 ? '../assets/' : 'assets/');
var ctx = null;
var sounds = {
menu: { src: BASE + 'hover-hat.mp3?a=2', buffer: null, loading: false },
section: { src: BASE + 'hover-hat2.mp3?a=2', buffer: null, loading: false },
cta: { src: BASE + 'hover-clap.mp3?a=2', buffer: null, loading: false },
wood: { src: BASE + 'hover-wood.mp3?a=2', buffer: null, loading: false },
xylo: { src: BASE + 'hover-xylo.mp3?a=2', buffer: null, loading: false },
tunnel: { src: BASE + 'hover-tunnel.mp3?a=2', buffer: null, loading: false },
wavepad: { src: BASE + 'wave-pad.mp3?a=4', buffer: null, loading: false },
cartadd: { src: BASE + 'cart-add.mp3', buffer: null, loading: false },
cartopen:{ src: BASE + 'cart-open.mp3', buffer: null, loading: false },
healthpad:{ src: BASE + 'health-pad.mp3', buffer: null, loading: false },
oraclepad:{ src: BASE + 'oracle-pad.mp3', buffer: null, loading: false },
infopad: { src: BASE + 'info-pad.mp3', buffer: null, loading: false },
homepad: { src: BASE + 'home-pad.mp3', buffer: null, loading: false },
apppad: { src: BASE + 'app-pad.mp3', buffer: null, loading: false },
pluginspad:{ src: BASE + 'plugins-pad.mp3', buffer: null, loading: false },
threedpad:{ src: BASE + 'threed-pad.mp3?a=3', buffer: null, loading: false },
contactspad:{ src: BASE + 'contacts-pad.mp3', buffer: null, loading: false },
shoppad:{ src: BASE + 'shop-pad.mp3', buffer: null, loading: false },
blackrosepad:{ src: BASE + 'blackrose-pad.mp3', buffer: null, loading: false },
justthispad:{ src: BASE + 'justthis-amb.mp3?b=2', buffer: null, loading: false }
};
var master = null;
var userPaused = false;
function getVol() {
var v = parseFloat(localStorage.getItem('qvolume'));
return (isNaN(v) || v < 0 || v > 1) ? 0.8 : v;
}
function ensureCtx() {
if (!ctx) {
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return null;
ctx = new AC();
master = ctx.createGain();
master.gain.value = getVol(); // persisted volume (default 0.8 = global -20%)
master.connect(ctx.destination);
ctx.onstatechange = function () {
if (ctx.state === 'running') {
Object.keys(sounds).forEach(function (k) {
if (sounds[k].pending) { sounds[k].pending = false; play(k); }
});
}
};
}
if (ctx.state === 'suspended' && !userPaused) ctx.resume();
return ctx;
}
// Floating audio control (bottom-right, every page): volume slider + play/pause
function createAudioToggle() {
if (document.getElementById('q-audio-ctrl')) return;
var wrap = document.createElement('div');
wrap.id = 'q-audio-ctrl';
wrap.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9998;display:flex;align-items:center;gap:11px';
var vol = document.createElement('input');
vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.01'; vol.value = String(getVol());
vol.id = 'q-audio-vol'; vol.setAttribute('aria-label', 'Volume');
vol.style.cssText = 'width:96px;height:5px;accent-color:#a78bfa;cursor:pointer;opacity:.55;transition:opacity .25s ease;filter:drop-shadow(0 6px 14px rgba(0,0,0,.5))';
vol.addEventListener('mouseenter', function () { vol.style.opacity = '1'; });
vol.addEventListener('mouseleave', function () { vol.style.opacity = '.55'; });
vol.addEventListener('input', function () {
var v = parseFloat(vol.value);
localStorage.setItem('qvolume', v); // persist across pages
ensureCtx(); if (master) master.gain.value = v;
});
var btn = document.createElement('button');
btn.id = 'q-audio-toggle';
btn.setAttribute('aria-label', 'Play / pause sound');
btn.style.cssText = 'width:48px;height:48px;border-radius:50%;border:1px solid rgba(167,139,250,.4);background:rgba(10,15,44,.5);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#e7f3ff;opacity:.55;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:opacity .25s ease,transform .25s ease;box-shadow:0 10px 30px -12px rgba(0,0,0,.6)';
btn.addEventListener('mouseenter', function () { btn.style.opacity = '1'; btn.style.transform = 'scale(1.06)'; });
btn.addEventListener('mouseleave', function () { btn.style.opacity = '.55'; btn.style.transform = 'scale(1)'; });
var PAUSE = '';
var PLAY = '';
function icon() { btn.innerHTML = userPaused ? PLAY : PAUSE; }
icon();
btn.addEventListener('click', function () {
ensureCtx();
if (userPaused) { userPaused = false; if (ctx) ctx.resume(); }
else { userPaused = true; if (ctx) ctx.suspend(); }
icon();
});
wrap.appendChild(vol); wrap.appendChild(btn);
document.body.appendChild(wrap);
}
function load(key) {
var snd = sounds[key];
if (snd.buffer || snd.loading) return;
var c = ensureCtx(); if (!c) return;
snd.loading = true;
fetch(snd.src).then(function (r) { return r.arrayBuffer(); })
.then(function (ab) { return c.decodeAudioData(ab); })
.then(function (buf) {
snd.buffer = buf;
if (snd.pending && c.state === 'running') { snd.pending = false; play(key); }
})
.catch(function () { snd.loading = false; });
}
function play(key, vol) {
var c = ensureCtx(); var snd = sounds[key];
if (!c) return;
if (!snd.buffer) { snd.pending = true; load(key); return; }
if (c.state !== 'running') { snd.pending = true; c.resume(); return; }
var s = c.createBufferSource();
s.buffer = snd.buffer;
if (key === 'tunnel') {
// Sync to the tunnel: if audio unlocks late, enter at the elapsed offset
// so the stab never plays past the tunnel, and fade out on its tail.
var g = c.createGain();
s.connect(g); g.connect(master);
var dur = snd.buffer.duration;
var elapsed = window.__introT0 ? Math.max(0, (performance.now() - window.__introT0) / 1000) : 0;
var offset = Math.min(elapsed, Math.max(0, dur - 0.15));
var remaining = dur - offset;
var fade = Math.min(1.2, remaining * 0.35);
var now = c.currentTime;
g.gain.setValueAtTime(1, now);
g.gain.setValueAtTime(1, now + remaining - fade);
g.gain.linearRampToValueAtTime(0.0001, now + remaining);
s.start(0, offset);
return;
}
if (key === 'wavepad' || key === 'healthpad' || key === 'oraclepad' || key === 'infopad' || key === 'homepad' || key === 'apppad' || key === 'pluginspad' || key === 'threedpad' || key === 'contactspad' || key === 'shoppad' || key === 'blackrosepad' || key === 'justthispad') { s.loop = true; }
if (key === 'wavepad') {
// long smooth fade-in for the Quantum Wave DAW background
var fg = c.createGain(); var now = c.currentTime;
fg.gain.setValueAtTime(0.0001, now);
fg.gain.exponentialRampToValueAtTime(1, now + 5);
s.connect(fg); fg.connect(master); s.start(0); return;
}
var v = (vol != null) ? vol : (key === 'menu' ? 0.64 : 1);
if (v !== 1) {
var mg = c.createGain(); mg.gain.value = v;
s.connect(mg); mg.connect(master);
} else {
s.connect(master);
}
s.start(0);
}
// Intro tunnel: eager-decode immediately and fire the instant audio is allowed,
// so there is no decode latency when the tunnel starts.
var tunnelFired = false;
function fireTunnel() {
if (tunnelFired) return;
var introEl = document.getElementById('intro');
if (!introEl || getComputedStyle(introEl).display === 'none' || introEl.classList.contains('hidden')) return;
tunnelFired = true;
play('tunnel'); // pending -> plays the moment the AudioContext is running
}
// Quantum Wave DAW page: pad starts immediately on open (pre-decoded, no latency).
var IS_WAVE = location.pathname.indexOf('wave.html') > -1;
var wavePadFired = false;
function fireWavePad() {
if (wavePadFired || !IS_WAVE) return;
wavePadFired = true;
play('wavepad');
}
// Cart page (reached by clicking the menu cart icon) -> cash sound on open
var IS_CART = location.pathname.indexOf('cart.html') > -1;
var cartOpenFired = false;
function fireCartOpen() {
if (cartOpenFired || !IS_CART) return;
cartOpenFired = true;
play('cartopen');
}
// Health page: ambient track starts on open (looped)
var IS_HEALTH = location.pathname.indexOf('health.html') > -1;
var healthFired = false;
function fireHealth() {
if (healthFired || !IS_HEALTH) return;
healthFired = true;
play('healthpad');
}
// Oracle page: ambient track starts on open (looped)
var IS_ORACLE = location.pathname.indexOf('oracle.html') > -1;
var oracleFired = false;
function fireOracle() {
if (oracleFired || !IS_ORACLE) return;
oracleFired = true;
play('oraclepad');
}
// App page: track starts on open (looped)
var IS_APP = location.pathname.indexOf('app.html') > -1;
var appFired = false;
function fireApp() {
if (appFired || !IS_APP) return;
appFired = true;
play('apppad');
}
// Plugins page: track starts on open (looped)
var IS_PLUGINS = location.pathname.indexOf('plugins.html') > -1;
var pluginsFired = false;
function firePlugins() {
if (pluginsFired || !IS_PLUGINS) return;
pluginsFired = true;
play('pluginspad');
}
// 3D page: track starts on open (looped)
var IS_3D = location.pathname.indexOf('3d.html') > -1;
var threedFired = false;
function fire3D() {
if (threedFired || !IS_3D) return;
threedFired = true;
play('threedpad');
}
// Shop page: track starts on open (looped)
var IS_SHOP = location.pathname.indexOf('shop.html') > -1;
var shopFired = false;
function fireShop() {
if (shopFired || !IS_SHOP) return;
shopFired = true;
play('shoppad');
}
// Just This product page: ambient forest loop on open
var IS_JUSTTHIS = location.pathname.indexOf('product.html') > -1 &&
(new URLSearchParams(location.search).get('id') === 'just-this');
var justthisFired = false;
function fireJustThis() {
if (justthisFired || !IS_JUSTTHIS) return;
justthisFired = true;
play('justthispad');
}
// Black Rose product page: track starts on open (looped)
var IS_BLACKROSE = location.pathname.indexOf('black-rose.html') > -1;
var blackroseFired = false;
function fireBlackRose() {
if (blackroseFired || !IS_BLACKROSE) return;
blackroseFired = true;
play('blackrosepad');
}
// Contacts page: track starts on open (looped)
var IS_CONTACTS = location.pathname.indexOf('contacts.html') > -1;
var contactsFired = false;
function fireContacts() {
if (contactsFired || !IS_CONTACTS) return;
contactsFired = true;
play('contactspad');
}
// Info page: ambient track starts on open (looped)
var IS_INFO = location.pathname.indexOf('info.html') > -1;
var infoFired = false;
function fireInfo() {
if (infoFired || !IS_INFO) return;
infoFired = true;
play('infopad');
}
// Home page: background track (looped) — starts right after the tunnel intro ends.
var pn = location.pathname;
var IS_HOME = pn === '/' || pn === '/it/' || pn.indexOf('index.html') > -1;
var homeFired = false;
function introGone() {
var i = document.getElementById('intro');
return !i || getComputedStyle(i).display === 'none' || i.classList.contains('hidden');
}
function fireHome() {
if (homeFired || !IS_HOME) return;
homeFired = true;
play('homepad');
}
function setupHome() {
if (!IS_HOME) return;
load('homepad');
if (introGone()) { fireHome(); return; }
var iv = setInterval(function () {
if (introGone()) { clearInterval(iv); fireHome(); }
}, 200);
setTimeout(function () { clearInterval(iv); fireHome(); }, 9000); // safety
}
// iOS/Safari hard-unlock: play a 1-sample silent buffer INSIDE the gesture so the
// AudioContext fully unlocks and page loops can play on touch (no mouse needed).
function silentUnlock() {
var c = ensureCtx(); if (!c || userPaused) return;
try { var b = c.createBuffer(1, 1, 22050); var src = c.createBufferSource(); src.buffer = b; src.connect(c.destination); src.start(0); } catch (e) {}
}
// Unlock audio as early/broadly as possible (browsers gate audio behind a user gesture)
function unlock() { ensureCtx(); silentUnlock(); load('menu'); load('section'); load('cta'); load('wood'); load('xylo'); load('tunnel'); fireTunnel(); load('wavepad'); fireWavePad(); load('cartopen'); fireCartOpen(); load('healthpad'); fireHealth(); load('oraclepad'); fireOracle(); load('infopad'); fireInfo(); load('homepad'); fireHome(); load('apppad'); fireApp(); load('pluginspad'); firePlugins(); load('threedpad'); fire3D(); load('contactspad'); fireContacts(); load('shoppad'); fireShop(); load('blackrosepad'); fireBlackRose(); load('justthispad'); fireJustThis(); }
['pointerdown', 'pointerup', 'mousedown', 'touchstart', 'touchend', 'click', 'keydown', 'wheel', 'scroll', 'mousemove'].forEach(function (ev) {
window.addEventListener(ev, unlock, { once: true, passive: true });
});
// keep re-arming the unlock on touch until audio is actually running (mobile resilience)
document.addEventListener('touchend', function rearm() {
if (ctx && ctx.state === 'running') { document.removeEventListener('touchend', rearm); return; }
unlock();
}, { passive: true });
function attach() {
var seen = new Set();
createAudioToggle();
// Intro tunnel sound (buffer already eager-decoded at init -> no latency)
fireTunnel();
// Pre-warm the HTTP cache for page-open sounds when hovering their menu link,
// so the audio is already downloaded before the click -> no latency on arrival.
var prefetched = {};
function prefetchFor(href) {
var file = href.indexOf('health.html') > -1 ? 'health-pad.mp3'
: href.indexOf('oracle.html') > -1 ? 'oracle-pad.mp3'
: href.indexOf('info.html') > -1 ? 'info-pad.mp3'
: href.indexOf('app.html') > -1 ? 'app-pad.mp3'
: href.indexOf('plugins.html') > -1 ? 'plugins-pad.mp3'
: href.indexOf('3d.html') > -1 ? 'threed-pad.mp3?a=3'
: href.indexOf('contacts.html') > -1 ? 'contacts-pad.mp3'
: href.indexOf('shop.html') > -1 ? 'shop-pad.mp3'
: href.indexOf('wave.html') > -1 ? 'wave-pad.mp3?a=4'
: href.indexOf('cart.html') > -1 ? 'cart-open.mp3' : null;
if (!file || prefetched[file]) return;
prefetched[file] = 1;
try { fetch(BASE + file, { cache: 'force-cache' }); } catch (e) {}
}
// every menu word: nav links (skip language flags)
document.querySelectorAll('nav a, .nav-main a, header nav a').forEach(function (a) {
if (seen.has(a) || a.classList.contains('flag')) return;
seen.add(a);
a.addEventListener('mouseenter', function () {
load('menu'); play('menu');
prefetchFor(a.getAttribute('href') || '');
});
});
// every single item inside Practice, Selected Work, How we work, Quantum Editions
document.querySelectorAll('#oracle .service, #work .work-card, #process .step, #editions .teaser').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('mouseenter', function () { load('section'); play('section'); });
});
// App page cards (phone mockups) -> hi-hat (RK_DM6_Hihat_04 = section sound)
document.querySelectorAll('.sos-frame').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('mouseenter', function () { load('section'); play('section'); });
});
// Cart page opened (via the menu cart icon) -> cash sound
fireCartOpen();
// Health page -> ambient loop
fireHealth();
// Oracle page -> ambient loop
fireOracle();
// Info page -> ambient loop
fireInfo();
// Home page -> background loop after the tunnel
setupHome();
// App page -> looped track
fireApp();
// Plugins page -> looped track
firePlugins();
// 3D page -> looped track
fire3D();
// Contacts page -> looped track
fireContacts();
// Shop page -> looped track
fireShop();
// Black Rose page -> looped track
fireBlackRose();
// Just This product page -> ambient loop
fireJustThis();
// Shop: hover over product cards -> same sound as Plugins cards (xylophone)
document.querySelectorAll('.shop-card').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('mouseenter', function () { load('xylo'); play('xylo', 0.64); });
});
// Shop: click on "Add to cart" / "Subscribe" buttons
document.querySelectorAll('.shop-card-cta').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('click', function () { load('cartadd'); play('cartadd'); });
});
// contact CTA: "Have a project that needs thinking?" + the email below it
document.querySelectorAll('#contact h3, #contact .mail').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('mouseenter', function () { load('cta'); play('cta'); });
});
// Info page: xylophone (same as Plugins cards) on every section + every review.
if (location.pathname.indexOf('info.html') > -1) {
document.querySelectorAll('.m-box, .rv-form, .partners').forEach(function (el) {
if (seen.has(el)) return;
seen.add(el);
el.addEventListener('mouseenter', function () { load('xylo'); play('xylo', 0.6); });
});
// reviews (static + user-submitted, added dynamically) -> delegate
var lastRv = null;
document.addEventListener('mouseover', function (e) {
var card = e.target.closest ? e.target.closest('.rv-card') : null;
if (card && card !== lastRv) { lastRv = card; load('xylo'); play('xylo', 0.6); }
else if (!card) { lastRv = null; }
});
}
// Dynamic .vcard galleries: woodblock on 3D, xylophone on Plugins (videos only).
// Cards are built by JS after load -> use event delegation.
var dynKey = location.pathname.indexOf('3d.html') > -1 ? 'wood'
: location.pathname.indexOf('plugins.html') > -1 ? 'xylo' : null;
if (dynKey) {
var lastCard = null;
var videoOnly = (dynKey === 'wood'); // 3D: videos only — Plugins: every card (videos + photos)
document.addEventListener('mouseover', function (e) {
var card = e.target.closest ? e.target.closest('.vcard') : null;
if (card && card !== lastCard) {
lastCard = card;
if (!videoOnly || card.querySelector('video')) { load(dynKey); play(dynKey, dynKey === 'xylo' ? 0.64 : 0.8); }
} else if (!card) { lastCard = null; }
});
}
}
// Mark the tunnel start time (used to keep the stab synced if audio unlocks late).
(function () {
var i = document.getElementById('intro');
if (i && getComputedStyle(i).display !== 'none' && !i.classList.contains('hidden') && !window.__introT0) {
window.__introT0 = performance.now();
}
})();
// Eager: create context + decode the intro stab right now so there is zero
// latency when the tunnel starts; fire immediately (plays the instant audio is allowed).
ensureCtx();
load('tunnel');
fireTunnel();
if (IS_WAVE) { load('wavepad'); fireWavePad(); }
if (IS_CART) { load('cartopen'); fireCartOpen(); }
if (IS_HEALTH) { load('healthpad'); fireHealth(); }
if (IS_ORACLE) { load('oraclepad'); fireOracle(); }
if (IS_INFO) { load('infopad'); fireInfo(); }
if (IS_APP) { load('apppad'); fireApp(); load('pluginspad'); firePlugins(); load('threedpad'); fire3D(); load('contactspad'); fireContacts(); load('shoppad'); fireShop(); load('blackrosepad'); fireBlackRose(); load('justthispad'); fireJustThis(); }
if (IS_HOME) { setupHome(); }
if (document.readyState !== 'loading') attach();
else document.addEventListener('DOMContentLoaded', attach);
})();