Extend JamesDSP with convolution and spatial audio
This commit is contained in:
70
frontend/vendor/jamesdsp/jamesdsp-feature.js
vendored
70
frontend/vendor/jamesdsp/jamesdsp-feature.js
vendored
@@ -2,6 +2,13 @@
|
||||
(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 } },
|
||||
@@ -18,7 +25,7 @@
|
||||
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;
|
||||
let tail = Promise.resolve(), maxLiveHeaps = 0, liveHeaps = 0, nodesCreated = 0, nodesDestroyed = 0, loadedIRRef = null;
|
||||
let faultListener = null, notifying = false, dspAnalyser = null;
|
||||
|
||||
const serial = fn => {
|
||||
@@ -27,7 +34,7 @@
|
||||
return result;
|
||||
};
|
||||
|
||||
function snapshot() { return { active, desired, bypassed, liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null }; }
|
||||
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;
|
||||
@@ -51,6 +58,7 @@
|
||||
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++; }
|
||||
}
|
||||
}
|
||||
@@ -84,6 +92,7 @@
|
||||
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') {
|
||||
@@ -91,7 +100,10 @@
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
await instance.setBypass(true);
|
||||
await instance.applyPreset(params);
|
||||
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);
|
||||
@@ -117,7 +129,7 @@
|
||||
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 (!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) {
|
||||
@@ -136,7 +148,7 @@
|
||||
await configure(instance, settings, levelDb);
|
||||
if (!desired) { releaseNode(); return false; }
|
||||
if (context.state === 'suspended') await context.resume().catch(() => {});
|
||||
graph.routeAll(elements, instance.node);
|
||||
graph.routeAll(elements, bypassed ? standardInput : instance.node);
|
||||
active = true;
|
||||
return true;
|
||||
}).catch(error => { fail(error); throw error; });
|
||||
@@ -158,22 +170,35 @@
|
||||
return enable(settings, levelDb);
|
||||
}
|
||||
|
||||
function setBypass(value) {
|
||||
function changeBypass(value) {
|
||||
if (!nodeEntry) return Promise.resolve(false);
|
||||
return serial(async () => { bypassed = Boolean(value); await nodeEntry.node.setBypass(bypassed); return bypassed; }).catch(error => { fail(error); throw error; });
|
||||
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 toggleBypass() {
|
||||
if (!nodeEntry) return Promise.resolve(false);
|
||||
return serial(async () => { bypassed = !bypassed; await nodeEntry.node.setBypass(bypassed); return bypassed; }).catch(error => { fail(error); throw error; });
|
||||
}
|
||||
function setBypass(value) { return changeBypass(value); }
|
||||
function toggleBypass() { return changeBypass(!bypassed); }
|
||||
|
||||
function loadIR(buffer) {
|
||||
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); await nodeEntry.node.setParam('convolver.enabled', 1); }
|
||||
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; });
|
||||
@@ -195,7 +220,24 @@
|
||||
return { enable, disable, setLevelDb, setBypass, toggleBypass, loadIR, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot };
|
||||
}
|
||||
|
||||
const api = { PRESETS, createController };
|
||||
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 || {};
|
||||
|
||||
Reference in New Issue
Block a user