// visualizer embed for else-if.org/yr_xtals. (function () { const HOST = 'https://files.else-if.org/f/YrXtals/'; const VIS_MODULE = HOST + 'yr_crystals_web.js'; const ALBUM_FOLDER = HOST + 'Knives_For_Cutting_Corners/'; const ICON = { play: HOST + 'assets/Play.svg', pause: HOST + 'assets/Pause.svg', bskip: HOST + 'assets/BSkip.svg', fskip: HOST + 'assets/FSkip.svg', mute: HOST + 'assets/Mute.svg', unmute: HOST + 'assets/Unmute.svg', }; const ANCHOR_SELECTOR = 'yrxtals'; const TRACKS_HOST_SELECTOR = 'yrxtals-tracks'; const ASPECT_W = 16; const ASPECT_H = 9; const NUM_BINS = 26; const DEFAULT_FFT = 16384; const DEFAULT_HOP = 4096; const TRACK_PARAMS = { 'bouncy castle': { fft: 16384, hop: 2048 }, 'curled': { fft: 8192, hop: 2048 }, 'eeger': { fft: 16384, hop: 2048 }, 'fickle': { fft: 8192, hop: 2048 }, 'fire sale': { fft: 16384, hop: 2048 }, 'friik': { fft: 16384, hop: 2048 }, 'gourded': { fft: 16384, hop: 2048 }, 'moron': { fft: 16384, hop: 2048 }, 'never give an angel a front': { fft: 8192, hop: 4096 }, 'now youre speaking my language': { fft: 16384, hop: 2048 }, 'ornery': { fft: 16384, hop: 2048 }, 'quicksand': { fft: 16384, hop: 2048 }, 'stolen art': { fft: 8192, hop: 2048 }, 'them bunch': { fft: 8192, hop: 2048 }, 'twig': { fft: 16384, hop: 2048 }, 'we that borrowed': { fft: 16384, hop: 2048 }, }; const LOG_MIN = Math.log10(40); const LOG_MAX = Math.log10(11000); const CTRL_FADE_DELAY_MS = 1500; const CTRL_FADE_DURATION_MS = 400; const RESTART_THRESHOLD_S = 3; function isMobileOrTablet() { if (typeof navigator === 'undefined') return false; const ua = navigator.userAgent || ''; if (/Mobi|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|webOS/i.test(ua)) return true; if (/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1) return true; return false; } // builds an img element for one of the asset URLs. function iconImg(src) { const img = document.createElement('img'); img.src = src; img.draggable = false; return img; } function start() { const anchor = document.querySelector(ANCHOR_SELECTOR); if (!anchor) return; swap(anchor); } function swap(anchor) { const mobile = isMobileOrTablet(); const wrap = document.createElement('div'); wrap.className = 'viz-wrap ' + (mobile ? 'viz-mobile' : 'viz-desktop'); const canvasWrap = document.createElement('div'); canvasWrap.className = 'viz-canvas-wrap'; const canvas = document.createElement('canvas'); let externalTracksHost = document.querySelector(TRACKS_HOST_SELECTOR); if (externalTracksHost && !mobile) { externalTracksHost.remove(); externalTracksHost = null; } const tracksEl = externalTracksHost || document.createElement('div'); if (!externalTracksHost) { tracksEl.className = 'viz-tracks'; } const volBtn = document.createElement('button'); volBtn.className = 'viz-ctrl viz-vol initial'; volBtn.setAttribute('aria-label', 'unmute'); volBtn.appendChild(iconImg(ICON.mute)); const promptText = document.createElement('span'); promptText.className = 'viz-prompt'; promptText.textContent = 'Unmute the audio to begin the visualizer to my music!'; volBtn.appendChild(promptText); const transportEl = document.createElement('div'); transportEl.className = 'viz-transport'; const backBtn = document.createElement('button'); backBtn.className = 'viz-ctrl'; backBtn.setAttribute('aria-label', 'previous track'); backBtn.appendChild(iconImg(ICON.bskip)); const playBtn = document.createElement('button'); playBtn.className = 'viz-ctrl viz-ctrl-play'; playBtn.setAttribute('aria-label', 'play'); playBtn.appendChild(iconImg(ICON.play)); const fwdBtn = document.createElement('button'); fwdBtn.className = 'viz-ctrl'; fwdBtn.setAttribute('aria-label', 'next track'); fwdBtn.appendChild(iconImg(ICON.fskip)); transportEl.appendChild(backBtn); transportEl.appendChild(playBtn); transportEl.appendChild(fwdBtn); canvasWrap.appendChild(canvas); canvasWrap.appendChild(volBtn); canvasWrap.appendChild(transportEl); wrap.appendChild(canvasWrap); if (!externalTracksHost) { wrap.appendChild(tracksEl); } anchor.replaceWith(wrap); let vizRef = null; const fit = () => { const r = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const w = Math.max(1, Math.floor(r.width * dpr)); const h = Math.max(1, Math.floor(r.height * dpr)); canvas.width = w; canvas.height = h; if (vizRef && vizRef.resize) vizRef.resize(w, h); }; fit(); new ResizeObserver(fit).observe(canvas); window.addEventListener('resize', fit); window.addEventListener('orientationchange', fit); import(VIS_MODULE) .then(mod => mod.mount(canvas)) .then(viz => { vizRef = viz; fit(); bootAudio({ tracksEl, volBtn, transportEl, backBtn, playBtn, fwdBtn }, viz); }) .catch(err => console.error('[yrxtls] mount failed:', err)); } async function bootAudio(ui, viz) { let html; try { html = await (await fetch(ALBUM_FOLDER)).text(); } catch (e) { console.error('[yrxtls] folder fetch failed:', e); return; } const doc = new DOMParser().parseFromString(html, 'text/html'); const cards = [...doc.querySelectorAll('.folder-card-wrap[data-url$=".mp3"]')]; if (cards.length === 0) return; const tracks = cards .map(c => { const url = c.dataset.url; const stem = decodeURIComponent(url.split('/').pop().replace(/\.mp3$/i, '')); return { url, name: stem.replace(/[_-]+/g, ' ') }; }) .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); const audio = new Audio(); audio.crossOrigin = 'anonymous'; audio.preload = 'auto'; audio.muted = true; // log-spaced band edges in Hz. const binEdges = new Float32Array(NUM_BINS + 1); for (let i = 0; i <= NUM_BINS; i++) { const t = i / NUM_BINS; binEdges[i] = Math.pow(10, LOG_MIN + (LOG_MAX - LOG_MIN) * t); } const outBins = new Float32Array(NUM_BINS); let audioCtx = null; let srcNode = null; let analyser = null; let gainNode = null; let fftBuf = null; let isMuted = true; let lastSampleTime = -1; let currentIndex = -1; let currentHop = DEFAULT_HOP; // idempotently builds the analyser plus gain graph against the media element. function ensureGraph() { if (audioCtx) return; const Ctor = window.AudioContext || window.webkitAudioContext; if (!Ctor) return; audioCtx = new Ctor(); srcNode = audioCtx.createMediaElementSource(audio); analyser = audioCtx.createAnalyser(); analyser.fftSize = DEFAULT_FFT; analyser.smoothingTimeConstant = 0.2; gainNode = audioCtx.createGain(); gainNode.gain.value = isMuted ? 0 : 1; srcNode.connect(analyser); analyser.connect(gainNode); gainNode.connect(audioCtx.destination); audio.muted = false; fftBuf = new Float32Array(analyser.frequencyBinCount); if (audioCtx.state === 'suspended') audioCtx.resume(); requestAnimationFrame(pumpFrames); } // rAF-driven FFT pump gated to one read per currentHop interval. function pumpFrames() { if (!analyser) return; const now = audioCtx.currentTime; const hopSec = currentHop / audioCtx.sampleRate; if (lastSampleTime < 0 || now - lastSampleTime >= hopSec) { lastSampleTime = now; analyser.getFloatFrequencyData(fftBuf); const binHz = audioCtx.sampleRate / analyser.fftSize; for (let b = 0; b < NUM_BINS; b++) { const loIdx = Math.max(0, Math.floor(binEdges[b] / binHz)); const hiIdx = Math.min(fftBuf.length - 1, Math.ceil(binEdges[b + 1] / binHz)); let peak = -200; for (let k = loIdx; k <= hiIdx; k++) { if (fftBuf[k] > peak) peak = fftBuf[k]; } outBins[b] = Math.max(-80, Math.min(0, peak)); } if (viz && viz.pushBins) viz.pushBins(outBins); } requestAnimationFrame(pumpFrames); } const pills = []; tracks.forEach((track, idx) => { const btn = document.createElement('button'); btn.className = 'viz-track'; btn.textContent = track.name; btn.addEventListener('click', () => { if (currentIndex === idx && !audio.paused) { audio.pause(); return; } loadAndPlay(idx); }); pills.push(btn); ui.tracksEl.appendChild(btn); }); // applies the per-track FFT and hop override, falling back to defaults on no match. function normalizeTrackName(s) { return (s || '').toLowerCase().replace(/['"]/g, '').replace(/[_\-\s]+/g, ' ').trim(); } const TRACK_PARAMS_NORM = {}; for (const k of Object.keys(TRACK_PARAMS)) { TRACK_PARAMS_NORM[normalizeTrackName(k)] = TRACK_PARAMS[k]; } function applyTrackParams(name) { const key = normalizeTrackName(name); const hit = TRACK_PARAMS_NORM[key]; const params = hit || { fft: DEFAULT_FFT, hop: DEFAULT_HOP }; console.log('[yrxtls] track', JSON.stringify(name), 'key', JSON.stringify(key), 'params', params, hit ? '(override)' : '(default)'); currentHop = params.hop; if (analyser && srcNode && gainNode && analyser.fftSize !== params.fft) { const smoothing = analyser.smoothingTimeConstant; try { srcNode.disconnect(analyser); } catch (e) {} try { analyser.disconnect(gainNode); } catch (e) {} analyser = audioCtx.createAnalyser(); analyser.fftSize = params.fft; analyser.smoothingTimeConstant = smoothing; srcNode.connect(analyser); analyser.connect(gainNode); fftBuf = new Float32Array(analyser.frequencyBinCount); console.log('[yrxtls] analyser recreated at fftSize', analyser.fftSize, 'bins', analyser.frequencyBinCount); } } // starts playback of the indexed track with wrap-around on out-of-range index. function loadAndPlay(idx) { if (tracks.length === 0) return; const wrapped = ((idx % tracks.length) + tracks.length) % tracks.length; currentIndex = wrapped; pills.forEach((p, i) => p.classList.toggle('active', i === wrapped)); const active = pills[wrapped]; if (active && active.scrollIntoView) { active.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } audio.src = tracks[wrapped].url; ensureGraph(); applyTrackParams(tracks[wrapped].name); audio.play().catch(err => console.error('[yrxtls] play failed:', err)); } function updatePlayIcon() { const playing = !!audio.src && !audio.paused; ui.playBtn.firstChild.src = playing ? ICON.pause : ICON.play; ui.playBtn.setAttribute('aria-label', playing ? 'pause' : 'play'); } ui.backBtn.addEventListener('click', () => { if (currentIndex < 0) { loadAndPlay(0); return; } if (!audio.paused && audio.currentTime > RESTART_THRESHOLD_S) { audio.currentTime = 0; } else { loadAndPlay(currentIndex - 1); } }); ui.fwdBtn.addEventListener('click', () => { loadAndPlay(currentIndex < 0 ? 0 : currentIndex + 1); }); ui.playBtn.addEventListener('click', () => { if (currentIndex < 0) { loadAndPlay(0); return; } if (audio.paused) { ensureGraph(); audio.play().catch(err => console.error('[yrxtls] play failed:', err)); } else { audio.pause(); } }); audio.addEventListener('play', updatePlayIcon); audio.addEventListener('pause', updatePlayIcon); audio.addEventListener('ended', () => { if (currentIndex >= 0 && currentIndex < tracks.length - 1) { loadAndPlay(currentIndex + 1); } else { pills.forEach(p => p.classList.remove('active')); currentIndex = -1; updatePlayIcon(); } }); let fadeTimer = null; // reveals overlay controls and arms the idle-fade timer. function showControls() { ui.volBtn.classList.remove('fade'); ui.transportEl.classList.remove('fade'); if (fadeTimer) { clearTimeout(fadeTimer); fadeTimer = null; } fadeTimer = setTimeout(() => { ui.transportEl.classList.add('fade'); if (!isMuted) ui.volBtn.classList.add('fade'); }, CTRL_FADE_DELAY_MS); } function setMutedState(muted) { isMuted = muted; const iconEl = ui.volBtn.querySelector('img'); if (iconEl) iconEl.src = muted ? ICON.mute : ICON.unmute; ui.volBtn.setAttribute('aria-label', muted ? 'unmute' : 'mute'); if (gainNode) gainNode.gain.value = muted ? 0 : 1; showControls(); } // resolves the seed track index by name match, falling back to alphabetical index zero. function seedTrackIndex() { const target = 'it caves in'; const idx = tracks.findIndex(t => t.name.trim().toLowerCase() === target); return idx >= 0 ? idx : 0; } ui.volBtn.addEventListener('click', () => { const wasInitial = ui.volBtn.classList.contains('initial'); if (wasInitial) { ui.volBtn.classList.remove('initial'); loadAndPlay(seedTrackIndex()); ensureGraph(); setMutedState(false); return; } if (!audio.src && tracks.length > 0) { loadAndPlay(0); } ensureGraph(); setMutedState(!isMuted); }); document.addEventListener('mousemove', showControls); document.addEventListener('touchstart', showControls, { passive: true }); showControls(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } })();