UNPKG

mosfez-synth

Version:

A microtonal-aware synth engine library for web.

241 lines (231 loc) 6.33 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function isConstant(paramDef) { return typeof paramDef === "number"; } function isVariable(paramDef) { return typeof paramDef === "string"; } function resolveParam(params, paramDef) { if (isConstant(paramDef)) { return paramDef; } else if (isVariable(paramDef)) { return params[paramDef]; } return void 0; } class DspNode { type; constructor(config) { this.type = config.type; } static isFaustDspNode(DspNode2) { return DspNode2.type === "faust"; } static isPolyDspNode(DspNode2) { return DspNode2.type === "poly"; } static isFaustDspNodeSerialized(serialized) { return serialized.type === "faust"; } static isPolyDspNodeSerialized(serialized) { return serialized.type === "poly"; } serialize() { throw new Error(".serialize() can only be called on subclasses"); } } function lines(lines2) { return lines2.join("\n"); } function series(arr, joiner, callback) { return arr.map(callback).join(joiner); } function env(name, dsp) { return `${name} = environment { ${dsp.replace(/\n/g, "\n ")} }; `; } async function constructNodeFaust(audioContext, dspNode, constructNode) { const { inputs = [], paramDefs, dependencies } = dspNode; const inputNodes = await Promise.all(inputs.map((input) => constructNode(audioContext, input))); const dspToCompile = lines([ 'import("stdfaust.lib");', constructFaustDsp(dspNode) ]); const faustNode = await dependencies.compile(audioContext, dspToCompile); const faustNodeDestroy = faustNode.destroy.bind(faustNode); const node = faustNode; node.destroy = () => { faustNodeDestroy(); inputNodes.forEach((node2) => node2?.destroy()); }; const paramsUsed = faustNode.getParams(); node.set = (params) => { paramsUsed.forEach((name) => { const paramKey = name.replace(/^\/FaustDSP\//g, ""); const paramDef = paramDefs[paramKey]; if (paramDef !== void 0) { const value = resolveParam(params, paramDef); if (typeof value === "number") { faustNode.setParamValue(name, value); } } }); inputNodes.forEach((node2) => node2?.set(params)); }; for (let i = 0; i < inputNodes.length; i++) { inputNodes[i].connect(node, 0, i); } return node; } function constructFaustDsp(dspNode) { const { paramDefs, dsp } = dspNode; const paramsDsp = env("params", lines([ series(Object.entries(paramDefs), "\n", ([name, value]) => { if (typeof value === "number") { return `${name} = ${value}; `; } return `${name} = hslider("${name}",0.0,-9999999.0,9999999.0,0.0000001);`; }) ])); return lines([paramsDsp, dsp]); } async function constructNodePoly(audioContext, dspNode, constructNode) { const { input, polyphony, paramCacheSize = 1e4, release, gate, dependencies } = dspNode; const releaseIsVariable = isVariable(release); const { VoiceController } = dependencies; const controller = new VoiceController({ polyphony, resolveGate: (params) => resolveParam(params, gate), paramCacheSize }); const setRelease = (r) => controller.setRelease(r * 1e3); if (!releaseIsVariable) { setRelease(release); } const voiceNodes = await Promise.all(Array(polyphony).fill(0).map(() => constructNode(audioContext, input))); const gainNode = new GainNode(audioContext); voiceNodes.forEach((node) => node.connect(gainNode)); gainNode.destroy = () => { voiceNodes.forEach((node) => node?.destroy()); }; gainNode.set = (params) => { if (releaseIsVariable) { const value = params[release]; if (typeof value === "number") { setRelease(value); } } const paramsToSet = controller.set(params); paramsToSet.forEach(({ index, params: params2 }) => { voiceNodes[index].set(params2); }); }; return gainNode; } async function constructNode(audioContext, dspNode) { if (DspNode.isFaustDspNode(dspNode)) { return await constructNodeFaust(audioContext, dspNode, constructNode); } if (DspNode.isPolyDspNode(dspNode)) { return await constructNodePoly(audioContext, dspNode, constructNode); } throw new Error(`dspNode has invalid type "${dspNode.type}"`); } function isInputSetEvent(e) { return e.type === "set"; } function isInputStopEvent(e) { return e.type === "stop"; } class InputEventRecorder { recording = false; recordingStartTime = Date.now(); events = []; record() { this.recording = true; this.recordingStartTime = Date.now(); } stop() { const time = (Date.now() - this.recordingStartTime) * 1e-3; this.events.push({ type: "stop", time }); const result = this.events; this.events = []; this.recording = false; return result; } addSetEvent(params) { const time = (Date.now() - this.recordingStartTime) * 1e-3; this.events.push({ type: "set", time, params }); } } class Synth { audioContext; initialParams; node; connection; constructor(config) { this.audioContext = config.audioContext; this.initialParams = config.params; } async build(dspNode) { const newNode = await constructNode(this.audioContext, dspNode); this.node?.disconnect(); this.node?.destroy(); this.node = newNode; this.tryConnectNode(); } connect(audio, output, input) { this.connection = [audio, output, input]; this.tryConnectNode(); return audio; } tryConnectNode() { if (this.node && this.connection) { this.node.disconnect(); this.node.connect(...this.connection); if (this.initialParams) { this.set(this.initialParams); } } } disconnect(outputOrDestinationNode, output, input) { if (this.node) { this.node.disconnect(outputOrDestinationNode, output, input); } } set(params) { if (this.inputEvents.recording) { this.inputEvents.addSetEvent(params); } if (this.node) { this.node.set(params); } } destroy() { this.node?.destroy(); this.node = void 0; } inputEvents = new InputEventRecorder(); } exports.Synth = Synth; exports.isInputSetEvent = isInputSetEvent; exports.isInputStopEvent = isInputStopEvent; //# sourceMappingURL=synth.js.map