// SPDX-License-Identifier: GPL-3.0-or-later import {DEFAULT_PARAMS, validateParam} from './params.js'; const moduleLoads = new WeakMap(); export class JamesDSPNode extends EventTarget { static async create(context, {wasmURL=new URL('./dsp/jamesdsp.wasm',import.meta.url), workletURL=new URL('./worklet.js',import.meta.url)}={}) { if (![44100,48000].includes(context.sampleRate)) throw new RangeError('Use a 44.1 or 48 kHz AudioContext'); 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 response=await fetch(wasmURL); if (!response.ok) throw new Error(`WASM fetch failed: ${response.status}`); const wasmBytes=await response.arrayBuffer(); await cache.get(key); const wrapper=new JamesDSPNode(context,wasmBytes); try { await wrapper.ready; return wrapper; } catch(e) { wrapper.destroy(); throw e; } } constructor(context,wasmBytes) { super(); this.context=context; this.params={...DEFAULT_PARAMS}; this.texts={}; this.pending=new Map(); this.serial=0; this.destroyed=false; this.source=null; 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); this.node.onprocessorerror=()=>{const error=new Error('DSP processor stopped');clearTimeout(timer);reject(error);this.fail(error);}; this.node.port.onmessage=({data})=> { if(data.type==='ready') {clearTimeout(timer);resolve();} else if(data.type==='reply') { const p=this.pending.get(data.id); if(p) {this.pending.delete(data.id);clearTimeout(p.timer);data.error?p.reject(new Error(data.error)):p.resolve(data.result);} } else if(data.type==='error') {const error=new Error(data.error); clearTimeout(timer); reject(error); this.fail(error);} else this.dispatchEvent(new CustomEvent(data.type,{detail:data})); }; }); } fail(error) { for(const p of this.pending.values()) {clearTimeout(p.timer);p.reject(error);} this.pending.clear();this.dispatchEvent(new CustomEvent('error',{detail:error})); } request(type,data={}) { if(this.destroyed) return Promise.reject(new Error('DSP has been destroyed')); const id=++this.serial; return new Promise((resolve,reject)=> { const timer=setTimeout(()=>{this.pending.delete(id);reject(new Error(`DSP ${type} timed out`));},15000); this.pending.set(id,{resolve,reject,timer});this.node.port.postMessage({type,id,...data}); }); } // One source → this DSP → destination. For arbitrary graphs use .node directly. connect(source,destination=this.context.destination) { if(this.destroyed) throw new Error('DSP has been destroyed'); if(this.source) this.source.disconnect(this.node); this.node.disconnect();this.source=source;source.connect(this.node);this.node.connect(destination);return destination; } async setParam(name,value) {validateParam(name,value);await this.request('param',{name,value});this.params[name]=value;} setBypass(enabled) {return this.setParam('bypass',Number(Boolean(enabled)));} async setText(effect,text) { if(!['liveprog','ddc','arbitrary'].includes(effect) || typeof text!=='string' || text.length>65536) throw new RangeError('Invalid text effect'); await this.request('text',{effect,text});this.texts[effect]=text;this.params[`${effect}.enabled`]=0; } async loadIR(buffer) { if(buffer.sampleRate!==this.context.sampleRate || ![1,2].includes(buffer.numberOfChannels) || buffer.length>65536 || buffer.length<1) throw new RangeError('IR must be mono/stereo, at context rate, at most 65536 frames'); const pcm=new Float32Array(buffer.length*buffer.numberOfChannels); for(let ch=0;chNumber(a.endsWith('.enabled'))-Number(b.endsWith('.enabled')))) await this.setParam(name,value); } toJSON() {return {version:1,params:{...this.params},texts:{...this.texts}};} destroy() { if(this.destroyed) return; if(this.source) {try {this.source.disconnect(this.node);} catch {}} this.node.disconnect(); this.node.port.postMessage({type:'destroy'});this.destroyed=true;this.fail(new Error('DSP destroyed'));this.node.port.close(); } }