/* Lazy classic-script bridge for the optional JamesDSP ES module and worklet. */ (function (root) { 'use strict'; const PRESETS = Object.freeze({ flat: { name: 'Flat', params: {} }, bass: { name: 'Bass', params: { 'eq.enabled': 1, 'eq.band0': 4, 'eq.band1': 3, 'bass.enabled': 1, 'bass.boost': 5, gain: -3 } }, vocal: { name: 'Vocal', params: { 'eq.enabled': 1, 'eq.band5': 2, 'eq.band7': 3, 'eq.band9': 2, 'compressor.enabled': 1 } }, loudness: { name: 'Loudness', params: { 'limiter.threshold': -3, gain: 2 } }, headphone: { name: 'Headphone', params: { 'crossfeed.enabled': 1, 'crossfeed.mode': 1 } }, bright: { name: 'Bright', params: { 'eq.enabled': 1, 'eq.band9': 2, 'eq.band10': 3, 'eq.band11': 3, 'eq.band12': 2 } }, warm: { name: 'Warm', params: { 'eq.enabled': 1, 'eq.band0': 3, 'eq.band1': 2, 'eq.band7': -1, gain: -2 } }, }); const clampGain = value => Math.max(-24, Math.min(12, Number(value) || 0)); function createController(options) { const graph = options.graph; let desired = false, active = false, nodeEntry = null, gainNode = null, context = null; let elements = [], standardInput = null, levelDb = 0, settings = null; let tail = Promise.resolve(), maxLiveHeaps = 0, liveHeaps = 0, nodesCreated = 0, nodesDestroyed = 0; let faultListener = null, notifying = false, dspAnalyser = null; const serial = fn => { const result = tail.then(fn, fn); tail = result.catch(() => {}); return result; }; function snapshot() { return { active, desired, liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null }; } function routeStandard() { if (!elements.length) return; try { graph.routeAll(elements, standardInput || context?.destination); } catch (error) { try { if (context?.destination) graph.routeAll(elements, context.destination); } catch { /* direct graph route is the last audible fallback */ } throw error; } } function releaseNode() { const entry = nodeEntry; nodeEntry = null; if (faultListener && entry) entry.node.removeEventListener('error', faultListener); faultListener = null; try { entry?.node.node.disconnect(); } catch { /* already disconnected */ } try { gainNode?.disconnect(); } catch { /* already disconnected */ } gainNode = null; try { dspAnalyser?.disconnect(); } catch { /* already disconnected */ } dspAnalyser = null; if (entry) { try { entry.node.destroy(); } catch { /* continue freeing references */ } if (entry.live) { entry.live = false; liveHeaps = Math.max(0, liveHeaps - 1); nodesDestroyed++; } } } function fail(error) { desired = false; active = false; try { routeStandard(); } catch { /* graph route has already tried a direct audible fallback */ } releaseNode(); if (!notifying) { notifying = true; try { options.onError?.(error instanceof Error ? error : new Error(String(error || 'JamesDSP failed'))); } finally { notifying = false; } } } function rampGain(value) { if (!gainNode || !context) return; const param = gainNode.gain; const now = context.currentTime || 0; if (context.state !== 'running' || typeof param.linearRampToValueAtTime !== 'function') { param.value = value; return; } param.cancelScheduledValues?.(now); param.setValueAtTime?.(param.value, now); param.linearRampToValueAtTime(value, now + 0.018); } async function configure(instance, nextSettings, nextLevelDb) { const preset = PRESETS[nextSettings?.preset] || PRESETS.flat; const params = { ...preset.params, ...(nextSettings?.params || {}) }; params.gain = clampGain((Number(params.gain) || 0) + (Number(nextLevelDb) || 0)); params.bypass = 1; if (active && context?.state === 'running') { rampGain(0); await new Promise(resolve => setTimeout(resolve, 20)); } await instance.setBypass(true); await instance.applyPreset(params); await instance.setParam('gain', params.gain); if (desired) await instance.setBypass(false); if (desired) rampGain(1); } async function loadNode(audioContext) { const moduleURL = root.Lazy.url('/vendor/jamesdsp/jamesdsp-node.mjs'); const { JamesDSPNode } = await import(moduleURL); return JamesDSPNode.create(audioContext, { wasmURL: root.Lazy.url('/vendor/jamesdsp/dsp/jamesdsp.wasm'), engineURL: root.Lazy.url('/vendor/jamesdsp/dsp/jamesdsp.mjs'), paramsURL: root.Lazy.url('/vendor/jamesdsp/params.mjs'), workletURL: root.Lazy.url('/vendor/jamesdsp/worklet.mjs'), }); } async function enable(nextSettings, nextLevelDb = levelDb) { desired = true; settings = nextSettings || {}; levelDb = Number(nextLevelDb) || 0; return serial(async () => { if (!desired) return false; const prepared = await options.prepareGraph(); context = prepared.context; elements = prepared.elements; standardInput = prepared.standardInput; if (![44100, 48000].includes(context?.sampleRate)) throw new RangeError(`JamesDSP supports 44.1 or 48 kHz; this device uses ${context?.sampleRate || 'unknown'} Hz`); if (!desired) return false; let instance = nodeEntry?.node; if (!instance) { instance = await (options.loadNode || loadNode)(context); if (!desired) { try { instance.destroy(); } catch {} return false; } nodeEntry = { node: instance, live: true }; liveHeaps++; nodesCreated++; maxLiveHeaps = Math.max(maxLiveHeaps, liveHeaps); faultListener = event => fail(event.detail || new Error('JamesDSP processing failed')); instance.addEventListener('error', faultListener); gainNode = context.createGain(); gainNode.gain.value = 1; instance.node.connect(gainNode); gainNode.connect(context.destination); } if (!desired) { releaseNode(); return false; } await configure(instance, settings, levelDb); if (!desired) { releaseNode(); return false; } if (context.state === 'suspended') await context.resume().catch(() => {}); graph.routeAll(elements, instance.node); active = true; return true; }).catch(error => { fail(error); throw error; }); } function disable() { desired = false; if (active) { active = false; try { routeStandard(); } catch { /* routeStandard tried its direct fallback */ } } return serial(async () => { releaseNode(); return true; }); } function setLevelDb(value) { levelDb = Number(value) || 0; if (!desired) return Promise.resolve(false); return enable(settings, levelDb); } function setBypass(value) { if (!nodeEntry) return Promise.resolve(false); return nodeEntry.node.setBypass(Boolean(value)); } function analyser() { if (!active || !gainNode || !context) return null; if (!dspAnalyser) { dspAnalyser = context.createAnalyser(); dspAnalyser.fftSize = 128; dspAnalyser.smoothingTimeConstant = 0.78; gainNode.disconnect(); gainNode.connect(dspAnalyser); dspAnalyser.connect(context.destination); } return dspAnalyser; } return { enable, disable, setLevelDb, setBypass, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot }; } const api = { PRESETS, createController }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else { root.YT = root.YT || {}; root.YT.JamesDSPFeature = api; } })(typeof window !== 'undefined' ? window : globalThis);