mosfez-synth
Version:
A microtonal-aware synth engine library for web.
241 lines (231 loc) • 6.33 kB
JavaScript
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
;