/* Lazy classic-script bridge for the optional JamesDSP ES module and worklet. */ (function (root) { 'use strict'; let irToolsPromise; const irTools = () => irToolsPromise ||= Promise.all([ import(root.Lazy.url('/vendor/jamesdsp/ir-utils.mjs')), import(root.Lazy.url('/vendor/jamesdsp/ir-builtins.mjs')), import(root.Lazy.url('/vendor/jamesdsp/ir-store.mjs')), ]).then(([utils,builtins,store])=>({utils,builtins,store})); 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, bypassed = 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, loadedIRRef = null; 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, bypassed, route: active && !bypassed ? 'jamesdsp' : 'standard', liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null, irRef: loadedIRRef }; } 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 */ } loadedIRRef = null; if (entry.live) { entry.live = false; liveHeaps = Math.max(0, liveHeaps - 1); nodesDestroyed++; } } } function fail(error) { desired = false; active = false; bypassed = 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 || {}) }; if (Number(params['spatial.enabled']) && Number(params['crossfeed.enabled'])) params['crossfeed.enabled'] = 0; 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); const ref = nextSettings?.irRef; const needsIR = ref && (loadedIRRef?.name !== ref.name || loadedIRRef?.hash !== ref.hash); await instance.applyPreset(needsIR ? { params, irRef: ref } : params); if (needsIR) loadedIRRef = { name: ref.name, hash: ref.hash }; await instance.setParam('gain', params.gain); if (desired) await instance.setBypass(bypassed); 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'), 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 (!Number.isFinite(context?.sampleRate) || context.sampleRate < 8000 || context.sampleRate > 96000) throw new RangeError(`JamesDSP supports 8–96 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, bypassed ? standardInput : instance.node); active = true; return true; }).catch(error => { fail(error); throw error; }); } function disable() { desired = false; bypassed = 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 changeBypass(value) { if (!nodeEntry) return Promise.resolve(false); return serial(async () => { const next = Boolean(value); if (next === bypassed) return bypassed; // A/B bypass is an exact route around the worklet. This preserves the // Standard graph sample-for-sample instead of relying on DSP bypass math. if (!next) await nodeEntry.node.setBypass(false); if (active) { rampGain(0); await new Promise(resolve => setTimeout(resolve, 20)); graph.routeAll(elements, next ? standardInput : nodeEntry.node); rampGain(next ? 0 : 1); } if (next) await nodeEntry.node.setBypass(true); bypassed = next; return bypassed; }).catch(error => { fail(error); throw error; }); } function setBypass(value) { return changeBypass(value); } function toggleBypass() { return changeBypass(!bypassed); } function loadIR(buffer, reference = null) { if (!active || !nodeEntry) return Promise.reject(new Error('Enable JamesDSP before loading an impulse response')); return serial(async () => { if (!active || !nodeEntry) throw new Error('JamesDSP is no longer active'); rampGain(0); try { await nodeEntry.node.loadIR(buffer, reference); loadedIRRef = reference ? { name: reference.name, hash: reference.hash } : null; await nodeEntry.node.setParam('convolver.enabled', 1); } finally { if (active && desired) rampGain(1); } return true; }).catch(error => { fail(error); throw error; }); } 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, toggleBypass, loadIR, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot }; } async function generateIR(name, sampleRate, channels = 2, maxFrames = 65536) { const {builtins}=await irTools(); return builtins.generateIR(name,sampleRate,channels,{maxFrames}); } async function decodeIR(context, bytes) { const {utils}=await irTools();return utils.decodeIR(context,bytes); } async function prepareIR(context, buffer, maxFrames = 65536) { const {utils}=await irTools(); const rate=context.sampleRate===44100?44100:48000; const prepared=utils.resampleIR(utils.prepareIR(context,buffer,{normalize:false}),rate); if (![1,2,4].includes(prepared.numberOfChannels) || prepared.length<1 || prepared.length>Math.min(65536,maxFrames)) throw new RangeError(`Impulse response exceeds the ${Math.min(65536,maxFrames)}-frame internal limit`); return prepared; } async function saveIR(name, buffer, metadata) { const {store}=await irTools();return store.saveIR(name,buffer,metadata); } async function getIR(name) { const {store}=await irTools();return store.getIR(name); } async function listIRs() { const {store}=await irTools();return store.listIRs(); } async function deleteIR(name) { const {store}=await irTools();return store.deleteIR(name); } const api = { PRESETS, createController, generateIR, decodeIR, prepareIR, saveIR, getIR, listIRs, deleteIR }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else { root.YT = root.YT || {}; root.YT.JamesDSPFeature = api; } })(typeof window !== 'undefined' ? window : globalThis);