1/** 2 * Optional micro UI sounds â SPA exit/enter whoosh samples + procedural UI tones. 3 */ 4(function (global) { 5 'use strict'; 6 7 var WHOOSH_EXIT_URL = '/music/internal/SPAWhoosh.mp3'; 8 var WHOOSH_ENTER_URL = '/music/internal/SPAWhoosh2.mp3'; 9 var WHOOSH_FALLBACK_DELAY_MS = 120; 10 var WHOOSH_ENTER_FALLBACK_DELAY_MS = 90; 11 12 var ctx = null; 13 var bound = false; 14 var navWhooshExitTimer = null; 15 var navWhooshEnterTimer = null; 16 var navWhooshGen = 0; 17 var whooshCache = Object.create(null); 18 var reverbNode = null; 19 var reverbCtx = null; 20 var hoverSoundLastAt = 0; 21 var HOVER_THRUM_MIN_GAP_MS = 55; 22 var HOVER_SOUND_SELECTOR = 23 'a[href], button, input[type="submit"], input[type="button"], input[type="reset"], ' + 24 '[role="button"], [role="link"], [role="tab"], .tool-button, .info-link, .bookings-contact-link'; 25 26 function prefersQuiet() { 27 try { 28 if (global.matchMedia && global.matchMedia('(prefers-reduced-motion: reduce)').matches) { 29 return true; 30 } 31 } catch (e) { /* ignore */ } 32 return false; 33 } 34 35 function readUiSoundsSetting() { 36 if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') { 37 return !!global.SettingsMenu.getSetting('uiSounds'); 38 } 39 try { 40 var raw = localStorage.getItem('puppywifey_settings'); 41 if (raw) { 42 var parsed = JSON.parse(raw); 43 return !!parsed.uiSounds; 44 } 45 } catch (e) { /* ignore */ } 46 return false; 47 } 48 49 function isEnabled() { 50 if (prefersQuiet()) return false; 51 if (global.PERFORMANCE_MODE_ENABLED) return false; 52 return readUiSoundsSetting(); 53 } 54 55 function whooshAssetUrl(url) { 56 if (global.resolveSiteAssetUrl && typeof global.resolveSiteAssetUrl === 'function') { 57 return global.resolveSiteAssetUrl(url); 58 } 59 return url; 60 } 61 62 function getWhooshSlot(url) { 63 if (!whooshCache[url]) { 64 whooshCache[url] = { 65 arrayBuffer: null, 66 decodePromise: null, 67 buffer: null, 68 bufferCtx: null 69 }; 70 } 71 return whooshCache[url]; 72 } 73 74 function ensureContext() { 75 if (ctx) return ctx; 76 var Ctx = global.AudioContext || global.webkitAudioContext; 77 if (!Ctx) return null; 78 ctx = new Ctx(); 79 return ctx; 80 } 81 82 function resumeContext() { 83 var audioCtx = ensureContext(); 84 if (!audioCtx) return Promise.resolve(false); 85 if (audioCtx.state === 'running') return Promise.resolve(true); 86 if (audioCtx.state === 'closed') { 87 ctx = null; 88 whooshCache = Object.create(null); 89 reverbNode = null; 90 reverbCtx = null; 91 audioCtx = ensureContext(); 92 if (!audioCtx) return Promise.resolve(false); 93 } 94 return audioCtx.resume().then(function () { 95 return audioCtx.state === 'running'; 96 }).catch(function () { 97 return false; 98 }); 99 } 100 101 function unlockAudio() { 102 return resumeContext(); 103 } 104 105 function fetchWhooshArrayBuffer(url) { 106 var slot = getWhooshSlot(url); 107 if (slot.arrayBuffer) return Promise.resolve(slot.arrayBuffer); 108 return fetch(whooshAssetUrl(url), { credentials: 'same-origin' }) 109 .then(function (res) { 110 if (!res.ok) throw new Error('HTTP ' + res.status); 111 return res.arrayBuffer(); 112 }) 113 .then(function (buf) { 114 slot.arrayBuffer = buf; 115 return buf; 116 }); 117 } 118 119 function decodeWhooshBuffer(audioCtx, url) { 120 var slot = getWhooshSlot(url); 121 if (slot.buffer && slot.bufferCtx === audioCtx) { 122 return Promise.resolve(slot.buffer); 123 } 124 if (!slot.decodePromise || slot.bufferCtx !== audioCtx) { 125 slot.decodePromise = fetchWhooshArrayBuffer(url) 126 .then(function (buf) { 127 return audioCtx.decodeAudioData(buf.slice(0)); 128 }) 129 .then(function (decoded) { 130 slot.buffer = decoded; 131 slot.bufferCtx = audioCtx; 132 return decoded; 133 }) 134 .catch(function (err) { 135 slot.decodePromise = null; 136 console.warn('[siteAudio] SPA whoosh decode failed', url, err); 137 return null; 138 }); 139 } 140 return slot.decodePromise; 141 } 142 143 function preloadWhoosh() { 144 return resumeContext().then(function (ok) { 145 if (!ok) return null; 146 var audioCtx = ensureContext(); 147 if (!audioCtx) return null; 148 return Promise.all([ 149 decodeWhooshBuffer(audioCtx, WHOOSH_EXIT_URL), 150 decodeWhooshBuffer(audioCtx, WHOOSH_ENTER_URL) 151 ]); 152 }).catch(function () { 153 return null; 154 }); 155 } 156 157 function makeReverbImpulse(audioCtx, seconds, decay) { 158 var rate = audioCtx.sampleRate; 159 var length = Math.floor(rate * seconds); 160 var impulse = audioCtx.createBuffer(2, length, rate); 161 var ch; 162 for (ch = 0; ch < 2; ch++) { 163 var channelData = impulse.getChannelData(ch); 164 var i; 165 for (i = 0; i < length; i++) { 166 channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); 167 } 168 } 169 return impulse; 170 } 171 172 function ensureReverb(audioCtx) { 173 if (reverbNode && reverbCtx === audioCtx) return reverbNode; 174 reverbNode = audioCtx.createConvolver(); 175 reverbNode.buffer = makeReverbImpulse(audioCtx, 1.6, 3.1); 176 reverbCtx = audioCtx; 177 return reverbNode; 178 } 179 180 function tone(freq, duration, type, gainValue, when) { 181 var audioCtx = ensureContext(); 182 if (!audioCtx) return; 183 184 var start = when != null ? when : audioCtx.currentTime; 185 var osc = audioCtx.createOscillator(); 186 var gain = audioCtx.createGain(); 187 osc.type = type || 'sine'; 188 osc.frequency.setValueAtTime(freq, start); 189 gain.gain.setValueAtTime(0.0001, start); 190 gain.gain.exponentialRampToValueAtTime(Math.max(gainValue || 0.12, 0.0002), start + 0.012); 191 gain.gain.exponentialRampToValueAtTime(0.0001, start + duration); 192 osc.connect(gain); 193 gain.connect(audioCtx.destination); 194 osc.start(start); 195 osc.stop(start + duration + 0.03); 196 } 197 198 function playWhenReady(playFn) { 199 if (!isEnabled()) return Promise.resolve(); 200 return resumeContext().then(function (ok) { 201 if (!ok) return; 202 playFn(); 203 }); 204 } 205 206 function playProceduralWhooshFallback(audioCtx, t) { 207 var dur = 0.32; 208 var sampleCount = Math.floor(audioCtx.sampleRate * dur); 209 var buffer = audioCtx.createBuffer(1, sampleCount, audioCtx.sampleRate); 210 var data = buffer.getChannelData(0); 211 var i; 212 for (i = 0;
212 i < sampleCount; i++) { 213 var fade = 1 - (i / sampleCount); 214 data[i] = (Math.random() * 2 - 1) * fade * fade; 215 } 216 217 var noise = audioCtx.createBufferSource(); 218 noise.buffer = buffer; 219 220 var filter = audioCtx.createBiquadFilter(); 221 filter.type = 'bandpass'; 222 filter.Q.value = 0.65; 223 filter.frequency.setValueAtTime(1400, t); 224 filter.frequency.exponentialRampToValueAtTime(280, t + dur); 225 226 var gain = audioCtx.createGain(); 227 gain.gain.setValueAtTime(0.0001, t); 228 gain.gain.exponentialRampToValueAtTime(0.12, t + 0.018); 229 gain.gain.exponentialRampToValueAtTime(0.0001, t + dur); 230 231 noise.connect(filter); 232 filter.connect(gain); 233 gain.connect(audioCtx.destination); 234 noise.start(t); 235 noise.stop(t + dur + 0.02); 236 } 237 238 function playWhooshSample(audioCtx, buffer, t) { 239 var source = audioCtx.createBufferSource(); 240 source.buffer = buffer; 241 source.playbackRate.value = 0.96; 242 243 var warmth = audioCtx.createBiquadFilter(); 244 warmth.type = 'lowpass'; 245 warmth.frequency.setValueAtTime(4800, t); 246 warmth.Q.value = 0.55; 247 248 var air = audioCtx.createBiquadFilter(); 249 air.type = 'highshelf'; 250 air.frequency.value = 2800; 251 air.gain.value = -4.5; 252 253 var reverb = ensureReverb(audioCtx); 254 255 var dryGain = audioCtx.createGain(); 256 dryGain.gain.value = 0.5; 257 258 var wetGain = audioCtx.createGain(); 259 wetGain.gain.value = 0.42; 260 261 var masterGain = audioCtx.createGain(); 262 var tail = Math.min(buffer.duration + 0.35, 1.4); 263 masterGain.gain.setValueAtTime(0.0001, t); 264 masterGain.gain.exponentialRampToValueAtTime(0.2, t + 0.028); 265 masterGain.gain.exponentialRampToValueAtTime(0.07, t + tail * 0.72); 266 masterGain.gain.exponentialRampToValueAtTime(0.0001, t + tail); 267 268 source.connect(warmth); 269 warmth.connect(air); 270 air.connect(dryGain); 271 air.connect(reverb); 272 reverb.connect(wetGain); 273 dryGain.connect(masterGain); 274 wetGain.connect(masterGain); 275 masterGain.connect(audioCtx.destination); 276 277 source.start(t); 278 source.stop(t + tail); 279 } 280 281 function playNavClick() { 282 return playWhenReady(function () { 283 tone(640, 0.07, 'triangle', 0.1); 284 }); 285 } 286 287 function whooshExitDelayMs() { 288 if (global.SpaTransitions && global.SpaTransitions.EXIT_DURATION_MS) { 289 return Math.round(global.SpaTransitions.EXIT_DURATION_MS * 0.38); 290 } 291 return WHOOSH_FALLBACK_DELAY_MS; 292 } 293 294 function whooshEnterDelayMs() { 295 if (global.SpaTransitions && global.SpaTransitions.ENTER_PAUSE_MS) { 296 return global.SpaTransitions.ENTER_PAUSE_MS; 297 } 298 return WHOOSH_ENTER_FALLBACK_DELAY_MS; 299 } 300 301 function clearNavWhooshTimer(timerRef) { 302 if (timerRef === 'exit' && navWhooshExitTimer) { 303 clearTimeout(navWhooshExitTimer); 304 navWhooshExitTimer = null; 305 return; 306 } 307 if (timerRef === 'enter' && navWhooshEnterTimer) { 308 clearTimeout(navWhooshEnterTimer); 309 navWhooshEnterTimer = null; 310 } 311 } 312 313 function cancelNavWhoosh() { 314 navWhooshGen += 1; 315 clearNavWhooshTimer('exit'); 316 clearNavWhooshTimer('enter'); 317 } 318 319 function scheduleNavWhooshExit() { 320 if (!isEnabled()) return; 321 navWhooshGen += 1; 322 var gen = navWhooshGen; 323 clearNavWhooshTimer('exit'); 324 navWhooshExitTimer = setTimeout(function () { 325 navWhooshExitTimer = null; 326 if (gen !== navWhooshGen) return; 327 playNavWhooshExit(); 328 }, whooshExitDelayMs()); 329 } 330 331 function scheduleNavWhooshEnter() { 332 if (!isEnabled()) return; 333 navWhooshGen += 1; 334 var gen = navWhooshGen; 335 clearNavWhooshTimer('enter'); 336 navWhooshEnterTimer = setTimeout(function () { 337 navWhooshEnterTimer = null; 338 if (gen !== navWhooshGen) return; 339 playNavWhooshEnter(); 340 }, whooshEnterDelayMs()); 341 } 342 343 function scheduleNavWhoosh() { 344 scheduleNavWhooshExit(); 345 } 346 347 function playWhooshByUrl(url, useFallback) { 348 return playWhenReady(function () { 349 var audioCtx = ensureContext(); 350 if (!audioCtx) return; 351 352 var t = audioCtx.currentTime; 353 decodeWhooshBuffer(audioCtx, url).then(function (buffer) { 354 if (!isEnabled()) return; 355 if (!buffer) { 356 if (useFallback) playProceduralWhooshFallback(audioCtx, t); 357 return; 358 } 359 playWhooshSample(audioCtx, buffer, audioCtx.currentTime); 360 }); 361 }); 362 } 363 364 function playNavWhooshExit() { 365 return playWhooshByUrl(WHOOSH_EXIT_URL, true); 366 } 367 368 function playNavWhooshEnter() { 369 return playWhooshByUrl(WHOOSH_ENTER_URL, false); 370 } 371 372 function playNavWhoosh() { 373 return playNavWhooshExit(); 374 } 375 376 function playToggle(checked) { 377 return playWhenReady(function () { 378 if (checked) { 379 tone(680, 0.055, 'sine', 0.08); 380 } else { 381 tone(440, 0.055, 'sine', 0.07); 382 } 383 }); 384 } 385 386 function playMenuOpen() { 387 return playWhenReady(function () { 388 var audioCtx = ensureContext(); 389 if (!audioCtx) return; 390 var start = audioCtx.currentTime; 391 tone(340, 0.065, 'sine', 0.065, start); 392 tone(440, 0.075, 'sine', 0.072, start + 0.042); 393 tone(560, 0.085, 'sine', 0.068, start + 0.088); 394 }); 395 } 396 397 function playMenuClose() { 398 return playWhenReady(function () { 399 var audioCtx = ensureContext(); 400 if (!audioCtx) return; 401 var start = audioCtx.currentTime; 402 tone(560, 0.06, 'sine', 0.068, start); 403 tone(440, 0.072, 'sine', 0.07, start + 0.038); 404 tone(320, 0.085, 'sine', 0.062, start + 0.082); 405 }); 406 } 407 408 function playSoftClick() { 409 return playWhenReady(function () { 410 tone(620, 0.045, 'triangle', 0.075); 411 }); 412 } 413 414 function playHoverThrum() { 415 return playWhenReady(function () { 416 var audioCtx = ensureContext(); 417 if (!audioCtx) return; 418 var start = audioCtx.currentTime; 419 var dur = 0.068; 420 421 var osc = audioCtx.createOscillator(); 422 var gain = audioCtx.createGain(); 423 osc.type = 'triangle'; 424 osc.frequency.setValueAtTime(420, start); 425 osc.frequency.exponentialRampToValueAtTime(260, start + dur * 0.82); 426 gain.gain.setValueAtTime(0.0001, start); 427 gain.gain.exponentialRampToValueAtTime(0.072, start + 0.011); 428 gain.gain.exponentialRampToValueAtTime(0.0001, start + dur); 429 osc.connect(gain); 430 gain.connect(audioCtx.destination); 431 osc.start(start); 432 osc.stop(start + dur + 0.03); 433 434 tone(520, 0.042, 'sine', 0.034, start + 0.006); 435 }); 436 } 437 438 function playSelectChange() { 439 return playWhenReady(function () { 440 var audioCtx = ensureContext(); 441 if (!audioCtx) return; 442 var start = audioCtx.currentTime; 443 tone(540, 0.05, 'triangle', 0.07, start); 444 tone(720, 0.04, 'triangle', 0.055, start + 0.03); 445 }); 446 } 447 448 function playPanelClose() { 449 return playWhenReady(function () { 450 var audioCtx = ensureContext(); 451 if (!audioCtx) return; 452 var start = audioCtx.currentTime; 453 tone(560, 0.05, 'sine', 0.07, start); 454 tone(380, 0.07, 'sine', 0.06, start + 0.04); 455 }); 456 } 457 458 function shouldSkipSoundTarget(el) { 459 if (!el || !el.closest) return true; 460 if (el.closest('[data-ui-sound="none"]')) return true; 461 if (document.body && document.body.classList.contains('page-chatroom')) return true; 462 if (el.closest('#chatroom-root, .chatroom-shell, [data-chatroom-root]')) return true; 463 if (el.closest('#music-player-overlay, #music-player-fab, .music-player-fab-popout')) return true; 464 return false;
465 } 466 467 function canUseHoverSound() { 468 try { 469 if (global.matchMedia && !global.matchMedia('(hover: hover) and (pointer: fine)').matches) { 470 return false; 471 } 472 } catch (e) { /* ignore */ } 473 return true; 474 } 475 476 function findHoverSoundTarget(el) { 477 if (!el || !el.closest) return null; 478 var control = el.closest(HOVER_SOUND_SELECTOR); 479 if (!control) return null; 480 if (control.disabled || control.getAttribute('aria-disabled') === 'true') return null; 481 if (control.matches('a[href]') && !control.getAttribute('href')) return null; 482 return control; 483 } 484 485 function shouldSkipHoverSoundControl(control) { 486 if (!control) return true; 487 if (control.closest('#site-theme-toggle, .site-theme-toggle, .settingsbtn')) return true; 488 if (control.closest('#hamburger-menu-btn, #sidebar-overlay, #sidebar-menu')) return true; 489 if (control.closest('label.settings-switch')) return true; 490 if (control.closest('#settings-panel button')) return true; 491 return false; 492 } 493 494 function bindHoverFeedback() { 495 document.addEventListener('pointerover', function (e) { 496 if (!isEnabled() || !canUseHoverSound()) return; 497 var target = e.target; 498 if (!target || shouldSkipSoundTarget(target)) return; 499 500 var control = findHoverSoundTarget(target); 501 if (!control || shouldSkipHoverSoundControl(control)) return; 502 503 var related = e.relatedTarget; 504 if (related && control.contains(related)) return; 505 506 var now = Date.now(); 507 if (now - hoverSoundLastAt < HOVER_THRUM_MIN_GAP_MS) return; 508 hoverSoundLastAt = now; 509 510 playHoverThrum(); 511 }, true); 512 } 513 514 function bindUiFeedback() { 515 document.addEventListener('change', function (e) { 516 if (!isEnabled()) return; 517 var t = e.target; 518 if (!t || shouldSkipSoundTarget(t)) return; 519 520 if (t.matches && t.matches('input.settings-switch__input[type="checkbox"]')) { 521 if (t.id === 'setting-darkMode') return; 522 if (t.id === 'setting-uiSounds') { 523 if (!t.checked) playToggle(false); 524 return; 525 } 526 playToggle(!!t.checked); 527 return; 528 } 529 530 if (t.matches && t.matches('#settings-panel select, .settings-select')) { 531 playSelectChange(); 532 return; 533 } 534 535 if (t.matches && t.matches('input[name="follower"]')) { 536 playSoftClick(); 537 } 538 }, true); 539 540 document.addEventListener('click', function (e) { 541 if (!isEnabled()) return; 542 var target = e.target; 543 if (!target || shouldSkipSoundTarget(target)) return; 544 545 if (target.closest('#settings-panel button')) { 546 var panelBtn = target.closest('#settings-panel button'); 547 if (panelBtn.id === 'settings-close' || panelBtn.id === 'settings-reset') return; 548 playSoftClick(); 549 return; 550 } 551 552 if (target.closest('#site-theme-toggle, .site-theme-toggle, .settingsbtn')) return; 553 if (target.closest('#hamburger-menu-btn, #sidebar-overlay, #sidebar-menu')) return; 554 if (target.closest('label.settings-switch')) return; 555 556 if (!document.body || !document.body.classList.contains('dashboard')) return; 557 558 var control = target.closest( 559 'button, input[type="submit"], input[type="button"], a.tool-button, .tool-button, .info-link, .bookings-contact-link' 560 ); 561 if (!control || control.disabled || control.getAttribute('aria-disabled') === 'true') return; 562 if (control.closest('#settings-panel, #sidebar-menu, #hamburger-menu-btn')) return; 563 if (control.closest('#site-theme-toggle, .site-theme-toggle, .settingsbtn')) return; 564 565 playSoftClick(); 566 }, false); 567 } 568 569 function playSettingsOpen() { 570 return playWhenReady(function () { 571 var audioCtx = ensureContext(); 572 if (!audioCtx) return; 573 var start = audioCtx.currentTime; 574 tone(440, 0.06, 'sine', 0.09, start); 575 tone(660, 0.08, 'sine', 0.08, start + 0.045); 576 }); 577 } 578 579 function playThemeToggle() { 580 return playWhenReady(function () { 581 var audioCtx = ensureContext(); 582 if (!audioCtx) return; 583 var start = audioCtx.currentTime; 584 tone(520, 0.09, 'sine', 0.1, start); 585 tone(780, 0.11, 'sine', 0.085, start + 0.055); 586 }); 587 } 588 589 function playToast() { 590 return playWhenReady(function () { 591 tone(880, 0.06, 'sine', 0.08); 592 }); 593 } 594 595 function bindUnlock() { 596 var unlock = function () { 597 resumeContext().then(function (ok) { 598 if (ok) preloadWhoosh(); 599 }); 600 }; 601 document.addEventListener('pointerdown', unlock, true); 602 document.addEventListener('keydown', unlock, true); 603 } 604 605 function bind() { 606 if (bound) return; 607 bound = true; 608 bindUnlock(); 609 preloadWhoosh(); 610 611 document.addEventListener('pupwife-site-theme', function (e) { 612 if (!e.detail || e.detail.source === 'boot' || e.detail.source === 'home-landing') return; 613 playThemeToggle(); 614 }); 615 616 document.addEventListener('pupwife:settings-opened', function () { 617 playSettingsOpen(); 618 }); 619 620 document.addEventListener('pupwife:settings-closed', function () { 621 playPanelClose(); 622 }); 623 624 document.addEventListener('pupwife:sidebar-opened', function () { 625 playMenuOpen(); 626 }); 627 628 document.addEventListener('pupwife:sidebar-closed', function (e) { 629 if (e.detail && e.detail.silent) return; 630 playMenuClose(); 631 }); 632 633 document.addEventListener('pupwife:setting-changed', function (e) {
634 if (!e.detail || e.detail.key !== 'uiSounds' || !e.detail.value) return; 635 playNavWhooshEnter(); 636 }); 637 638 bindUiFeedback(); 639 bindHoverFeedback(); 640 } 641 642 global.SiteAudio = { 643 playNavClick: playNavClick, 644 playNavWhoosh: playNavWhoosh, 645 playNavWhooshExit: playNavWhooshExit, 646 playNavWhooshEnter: playNavWhooshEnter, 647 scheduleNavWhoosh: scheduleNavWhoosh, 648 scheduleNavWhooshExit: scheduleNavWhooshExit, 649 scheduleNavWhooshEnter: scheduleNavWhooshEnter, 650 cancelNavWhoosh: cancelNavWhoosh, 651 playSettingsOpen: playSettingsOpen, 652 playThemeToggle: playThemeToggle, 653 playToast: playToast, 654 playToggle: playToggle, 655 playMenuOpen: playMenuOpen, 656 playMenuClose: playMenuClose, 657 playSoftClick: playSoftClick, 658 playHoverThrum: playHoverThrum, 659 playSelectChange: playSelectChange, 660 playPanelClose: playPanelClose, 661 isEnabled: isEnabled, 662 unlock: unlockAudio, 663 preloadWhoosh: preloadWhoosh, 664 ensureLoaded: bind 665 }; 666 667 bind(); 668})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.