Add JamesDSP playback controls and verification harness
This commit is contained in:
29
frontend/vendor/jamesdsp/jamesdsp-feature.js
vendored
29
frontend/vendor/jamesdsp/jamesdsp-feature.js
vendored
@@ -16,7 +16,7 @@
|
||||
|
||||
function createController(options) {
|
||||
const graph = options.graph;
|
||||
let desired = false, active = false, nodeEntry = null, gainNode = null, context = null;
|
||||
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 faultListener = null, notifying = false, dspAnalyser = null;
|
||||
@@ -27,7 +27,7 @@
|
||||
return result;
|
||||
};
|
||||
|
||||
function snapshot() { return { active, desired, liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null }; }
|
||||
function snapshot() { return { active, desired, bypassed, liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null }; }
|
||||
|
||||
function routeStandard() {
|
||||
if (!elements.length) return;
|
||||
@@ -58,6 +58,7 @@
|
||||
function fail(error) {
|
||||
desired = false;
|
||||
active = false;
|
||||
bypassed = false;
|
||||
try { routeStandard(); } catch { /* graph route has already tried a direct audible fallback */ }
|
||||
releaseNode();
|
||||
if (!notifying) {
|
||||
@@ -92,7 +93,7 @@
|
||||
await instance.setBypass(true);
|
||||
await instance.applyPreset(params);
|
||||
await instance.setParam('gain', params.gain);
|
||||
if (desired) await instance.setBypass(false);
|
||||
if (desired) await instance.setBypass(bypassed);
|
||||
if (desired) rampGain(1);
|
||||
}
|
||||
|
||||
@@ -101,7 +102,6 @@
|
||||
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'),
|
||||
});
|
||||
@@ -144,6 +144,7 @@
|
||||
|
||||
function disable() {
|
||||
desired = false;
|
||||
bypassed = false;
|
||||
if (active) {
|
||||
active = false;
|
||||
try { routeStandard(); } catch { /* routeStandard tried its direct fallback */ }
|
||||
@@ -159,7 +160,23 @@
|
||||
|
||||
function setBypass(value) {
|
||||
if (!nodeEntry) return Promise.resolve(false);
|
||||
return nodeEntry.node.setBypass(Boolean(value));
|
||||
return serial(async () => { bypassed = Boolean(value); await nodeEntry.node.setBypass(bypassed); 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 loadIR(buffer) {
|
||||
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); }
|
||||
finally { if (active && desired) rampGain(1); }
|
||||
return true;
|
||||
}).catch(error => { fail(error); throw error; });
|
||||
}
|
||||
|
||||
function analyser() {
|
||||
@@ -175,7 +192,7 @@
|
||||
return dspAnalyser;
|
||||
}
|
||||
|
||||
return { enable, disable, setLevelDb, setBypass, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot };
|
||||
return { enable, disable, setLevelDb, setBypass, toggleBypass, loadIR, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot };
|
||||
}
|
||||
|
||||
const api = { PRESETS, createController };
|
||||
|
||||
11
frontend/vendor/jamesdsp/jamesdsp-node.mjs
vendored
11
frontend/vendor/jamesdsp/jamesdsp-node.mjs
vendored
@@ -7,17 +7,12 @@ export class JamesDSPNode extends EventTarget {
|
||||
static async create(context, options={}) {
|
||||
if (![44100,48000].includes(context.sampleRate)) throw new RangeError('Use a 44.1 or 48 kHz AudioContext');
|
||||
const wasmURL=options.wasmURL || assetURL('/vendor/jamesdsp/dsp/jamesdsp.wasm','./dsp/jamesdsp.wasm');
|
||||
const engineURL=options.engineURL || assetURL('/vendor/jamesdsp/dsp/jamesdsp.mjs','./dsp/jamesdsp.mjs');
|
||||
const paramsURL=options.paramsURL || assetURL('/vendor/jamesdsp/params.mjs','./params.mjs');
|
||||
const baseWorkletURL=options.workletURL || assetURL('/vendor/jamesdsp/worklet.mjs','./worklet.mjs');
|
||||
const paramApi=await loadParams(paramsURL);
|
||||
const workletURL=new URL(baseWorkletURL,import.meta.url);
|
||||
// Fragments are not sent over HTTP, so SW CacheStorage can keep the exact
|
||||
// manifest URL while import.meta.url still carries the build-local engine URL.
|
||||
workletURL.hash=new URLSearchParams({engine:String(engineURL)}).toString();
|
||||
if (!moduleLoads.has(context)) moduleLoads.set(context,new Map());
|
||||
const cache=moduleLoads.get(context), key=String(workletURL);
|
||||
if (!cache.has(key)) cache.set(key,context.audioWorklet.addModule(workletURL).catch(e=>{cache.delete(key);throw e;}));
|
||||
const cache=moduleLoads.get(context), key=String(baseWorkletURL);
|
||||
if (!cache.has(key)) cache.set(key,context.audioWorklet.addModule(baseWorkletURL).catch(e=>{cache.delete(key);throw e;}));
|
||||
const response=await fetch(wasmURL);
|
||||
if (!response.ok) throw new Error(`WASM fetch failed: ${response.status}`);
|
||||
const wasmBytes=await response.arrayBuffer();
|
||||
@@ -26,7 +21,7 @@ export class JamesDSPNode extends EventTarget {
|
||||
try { await wrapper.ready; return wrapper; } catch(e) { wrapper.destroy(); throw e; }
|
||||
}
|
||||
constructor(context,wasmBytes,paramApi) {
|
||||
super(); this.context=context; this.defaultParams=paramApi.DEFAULT_PARAMS; this.validateParam=paramApi.validateParam; this.params={...this.defaultParams}; this.pending=new Map(); this.serial=0; this.destroyed=false;
|
||||
super(); this.context=context; this.defaultParams=Object.fromEntries(Object.entries(paramApi.DEFAULT_PARAMS).filter(([name])=>!/^(?:liveprog|ddc|arbitrary)\./.test(name))); this.validateParam=paramApi.validateParam; this.params={...this.defaultParams}; this.pending=new Map(); this.serial=0; this.destroyed=false;
|
||||
this.node=new AudioWorkletNode(context,'jamesdsp',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2],channelCount:2,channelCountMode:'explicit',processorOptions:{wasmBytes}});
|
||||
this.ready=new Promise((resolve,reject)=> {
|
||||
const timer=setTimeout(()=>reject(new Error('DSP initialization timed out')),15000);
|
||||
|
||||
6
frontend/vendor/jamesdsp/worklet.mjs
vendored
6
frontend/vendor/jamesdsp/worklet.mjs
vendored
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
const engineURL=new URLSearchParams(new URL(import.meta.url).hash.slice(1)).get('engine');
|
||||
if(!engineURL) throw new Error('JamesDSP engine asset URL is missing');
|
||||
const {default:createJamesDSP}=await import(engineURL);
|
||||
// AudioWorkletGlobalScope does not support dynamic import; this immutable
|
||||
// vendor path is versioned with the upstream pin and remapped by the SW cache.
|
||||
import createJamesDSP from './dsp/jamesdsp-77be0b2.mjs';
|
||||
class JamesDSPProcessor extends AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(); this.closed=false;this.failed=false;
|
||||
|
||||
Reference in New Issue
Block a user