webpd
Version:
WebPd is a compiler for audio programming language Pure Data allowing to run .pd patches on web pages.
502 lines (499 loc) • 21.5 kB
JavaScript
import { resolveRootPatch, resolveNodeType, resolvePatch, resolvePdNodeDollarArgs, resolvePdNode } from './compile-helpers.js';
import instantiateAbstractions from './instantiate-abstractions.js';
import { nodeBuilders } from '../nodes/nodes/subpatch.js';
import { builder } from '../nodes/nodes/_mixer~.js';
import { builder as builder$1 } from '../nodes/nodes/sig~.js';
import { builder as builder$2 } from '../nodes/nodes/_routemsg.js';
import { deleteNode, connect, addNode } from '../../node_modules/@webpd/compiler/dist/src/dsp-graph/mutators.js';
import { nodeDefaults } from '../../node_modules/@webpd/compiler/dist/src/dsp-graph/graph-helpers.js';
import { getNode, getOutlet } from '../../node_modules/@webpd/compiler/dist/src/dsp-graph/getters.js';
/*
* Copyright (c) 2022-2023 Sébastien Piquemal <sebpiq@protonmail.com>, Chris McCormick.
*
* This file is part of WebPd
* (see https://github.com/sebpiq/WebPd).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const IMPLICIT_NODE_TYPES = {
MIXER: '_mixer~',
ROUTE_MSG: '_routemsg',
CONSTANT_SIGNAL: 'sig~',
};
var IdNamespaces;
(function (IdNamespaces) {
IdNamespaces["PD"] = "n";
/** Node added for enabling translation from pd to dp graph */
IdNamespaces["IMPLICIT_NODE"] = "m";
})(IdNamespaces || (IdNamespaces = {}));
const buildGraphNodeId = (patchId, nodeLocalId) => {
return `${IdNamespaces.PD}_${patchId}_${nodeLocalId}`;
};
const buildGraphPortletId = (pdPortletId) => pdPortletId.toString(10);
/** Node id for nodes added while converting from PdJson */
const _buildImplicitGraphNodeId = (sink, nodeType) => {
nodeType = nodeType.replaceAll(/[^a-zA-Z0-9_]/g, '');
return `${IdNamespaces.IMPLICIT_NODE}_${sink.nodeId}_${sink.portletId}_${nodeType}`;
};
// ================================== MAIN ================================== //
var toDspGraph = async (pd, nodeBuilders$1, abstractionLoader = async (nodeType) => ({
status: 1,
unknownNodeType: nodeType,
})) => {
const abstractionsResult = await instantiateAbstractions(pd, nodeBuilders$1, abstractionLoader);
const hasWarnings = Object.keys(abstractionsResult.warnings);
if (abstractionsResult.status === 1) {
return {
status: 1,
abstractionsLoadingErrors: abstractionsResult.errors,
abstractionsLoadingWarnings: hasWarnings
? abstractionsResult.warnings
: undefined,
};
}
const { pd: pdWithResolvedAbstractions } = abstractionsResult;
const compilation = {
pd: pdWithResolvedAbstractions,
nodeBuilders: nodeBuilders$1,
graph: {},
};
const rootPatch = resolveRootPatch(pdWithResolvedAbstractions);
_traversePatches(compilation, [rootPatch], _buildNodes);
_buildConnections(compilation, [rootPatch]);
Object.values(compilation.graph).forEach((node) => {
if (Object.keys(nodeBuilders).includes(node.type)) {
deleteNode(compilation.graph, node.id);
}
});
const arrays = Object.values(compilation.pd.arrays).reduce((arrays, array) => {
arrays[array.args[0]] = array.data
? new Float32Array(array.data)
: new Float32Array(array.args[1]);
return arrays;
}, {});
return {
status: 0,
graph: compilation.graph,
pd: compilation.pd,
arrays,
abstractionsLoadingWarnings: hasWarnings
? abstractionsResult.warnings
: undefined,
};
};
// ================================== DSP GRAPH NODES ================================== //
const _buildNodes = (compilation, patchPath) => {
const patch = _currentPatch(patchPath);
const rootPatch = _rootPatch(patchPath);
Object.values(patch.nodes).forEach((pdNode) => {
const nodeId = buildGraphNodeId(patch.id, pdNode.id);
_buildNode(compilation, rootPatch, patch, pdNode, nodeId);
});
};
const _buildNode = (compilation, rootPatch, patch, pdNode, nodeId) => {
const nodeTypeResolution = resolveNodeType(compilation.nodeBuilders, pdNode.type);
if (nodeTypeResolution === null) {
throw new Error(`unknown node type ${pdNode.type}`);
}
const { nodeBuilder, nodeType } = nodeTypeResolution;
if (nodeBuilder.isNoop === true) {
return null;
}
if (!nodeBuilder.skipDollarArgsResolution) {
pdNode = {
...pdNode,
args: resolvePdNodeDollarArgs(rootPatch, pdNode.args),
};
}
const nodeArgs = nodeBuilder.translateArgs(pdNode, patch, compilation.pd);
const partialNode = nodeBuilder.build(nodeArgs);
return addNode(compilation.graph, {
...nodeDefaults(nodeId, nodeType),
args: nodeArgs,
...partialNode,
});
};
// ================================== DSP GRAPH CONNECTIONS ================================== //
const _buildConnections = (compilation, rootPatchPath) => {
const { graph } = compilation;
let pdConnections = [];
// 1. Get recursively through the patches and collect all pd connections
// in one single array. In the process, we also resolve subpatch's portlets.
_traversePatches(compilation, rootPatchPath, (compilation, patchPath) => {
_resolveSubpatches(compilation, patchPath, pdConnections);
});
// 2. Convert connections from PdJson to DspGraph, and group them by source
const groupedGraphConnections = _groupAndResolveGraphConnections(compilation, pdConnections);
// 3. Finally, we iterate over the grouped sources and build the graph connections.
Object.values(graph).forEach((node) => {
Object.values(node.inlets).forEach((inlet) => {
const graphSink = {
nodeId: node.id,
portletId: inlet.id,
};
const graphSources = (groupedGraphConnections[node.id]
? groupedGraphConnections[node.id][inlet.id]
: undefined) || { signalSources: [], messageSources: [] };
if (inlet.type === 'signal') {
const { nodeBuilder: sinkNodeBuilder } = resolveNodeType(compilation.nodeBuilders, node.type);
const messageToSignalConfig = sinkNodeBuilder.configureMessageToSignalConnection
? sinkNodeBuilder.configureMessageToSignalConnection(graphSink.portletId, node.args)
: undefined;
_buildConnectionToSignalSink(graph, graphSources.signalSources, graphSources.messageSources, graphSink, messageToSignalConfig);
}
else {
if (graphSources.signalSources.length !== 0) {
throw new Error(`Unexpected signal connection to node id ${graphSink.nodeId}, inlet ${graphSink.portletId}`);
}
_buildConnectionToMessageSink(graph, graphSources.messageSources, graphSink);
}
});
});
};
const _buildConnectionToMessageSink = (graph, sources, sink) => sources.forEach((source) => {
connect(graph, source, sink);
});
/**
* Build a graph connection. Add nodes that are implicit in Pd, and that we want explicitely
* declared in our graph.
*
* Implicit Pd behavior made explicit by this compilation :
* - Multiple DSP inputs mixed into one
*
* ```
* [ signal1~ ] [ signal2~ ]
* \ /
* [ _mixer~ ]
* |
* [ someNode~ ]
*
* ```
*
* - When messages to DSP input, automatically turned into a signal
*
* ```
* [ sig~ ]
* |
* [ someNode~ ]
* ```
*
* - Re-route messages from signal inlet to a message inlet
*
* ```
* [ message1 ]
* |
* [ _routemsg ] ( on the left inlet float messages, on the the right inlet, the rest. )
* | \
* [ sig~ ] |
* | |
* [ someNode~ ]
* ```
*
* - Initial value of DSP input
*
*
*/
const _buildConnectionToSignalSink = (graph, signalSources, messageSources, sink, messageToSignalConfig) => {
let implicitSigNode = null;
// 1. SIGNAL SOURCES
// 1.1. if single signal source, we just put a normal connection
if (signalSources.length === 1) {
connect(graph, signalSources[0], sink);
// 1.2. if several signal sources, we put a mixer node in between.
}
else if (signalSources.length > 1) {
const mixerNodeArgs = {
channelCount: signalSources.length,
};
const implicitMixerNode = addNode(graph, {
...nodeDefaults(_buildImplicitGraphNodeId(sink, IMPLICIT_NODE_TYPES.MIXER), IMPLICIT_NODE_TYPES.MIXER),
args: mixerNodeArgs,
...builder.build(mixerNodeArgs),
});
connect(graph, {
nodeId: implicitMixerNode.id,
portletId: '0',
}, sink);
signalSources.forEach((source, i) => {
connect(graph, source, {
nodeId: implicitMixerNode.id,
portletId: buildGraphPortletId(i),
});
});
// 1.3. if no signal source, we need to simulate one by plugging a sig node to the inlet
}
else {
const sigNodeArgs = {
initValue: messageToSignalConfig
? messageToSignalConfig.initialSignalValue
: 0,
};
implicitSigNode = addNode(graph, {
...nodeDefaults(_buildImplicitGraphNodeId(sink, IMPLICIT_NODE_TYPES.CONSTANT_SIGNAL), IMPLICIT_NODE_TYPES.CONSTANT_SIGNAL),
args: sigNodeArgs,
...builder$1.build(sigNodeArgs),
});
connect(graph, {
nodeId: implicitSigNode.id,
portletId: '0',
}, sink);
}
// 2. MESSAGE SOURCES
// If message sources, we split the incoming message flow in 2 using `_routemsg`.
// - outlet 0 : float messages are proxied to the sig~ if present, so they set its value
// - outlet 1 : other messages must be proxied to a different sink (cause here we are dealing
// with a signal sink which can't accept messages).
if (messageSources.length) {
const routeMsgArgs = {};
const implicitRouteMsgNode = addNode(graph, {
...nodeDefaults(_buildImplicitGraphNodeId(sink, IMPLICIT_NODE_TYPES.ROUTE_MSG), IMPLICIT_NODE_TYPES.ROUTE_MSG),
args: routeMsgArgs,
...builder$2.build(routeMsgArgs),
});
let isMsgSortNodeConnected = false;
if (implicitSigNode) {
connect(graph, { nodeId: implicitRouteMsgNode.id, portletId: '0' }, { nodeId: implicitSigNode.id, portletId: '0' });
isMsgSortNodeConnected = true;
}
if (messageToSignalConfig &&
messageToSignalConfig.reroutedMessageInletId !== undefined) {
connect(graph, { nodeId: implicitRouteMsgNode.id, portletId: '1' }, {
nodeId: sink.nodeId,
portletId: messageToSignalConfig.reroutedMessageInletId,
});
isMsgSortNodeConnected = true;
}
if (isMsgSortNodeConnected) {
messageSources.forEach((graphMessageSource) => {
connect(graph, graphMessageSource, {
nodeId: implicitRouteMsgNode.id,
portletId: '0',
});
});
}
}
};
/**
* Take an array of global PdJson connections and :
* - group them by sink
* - convert them to graph connections
* - split them into signal and message connections
*/
const _groupAndResolveGraphConnections = (compilation, pdConnections) => {
const { graph } = compilation;
const groupedGraphConnections = {};
pdConnections.forEach((connection) => {
const [_, pdGlobSink] = connection;
// Resolve the graph sink corresponding with the connection,
// if already handled, we move on.
const [patchPath, pdSink] = pdGlobSink;
const graphNodeId = buildGraphNodeId(_currentPatch(patchPath).id, pdSink.nodeId);
groupedGraphConnections[graphNodeId] =
groupedGraphConnections[graphNodeId] || {};
const graphPortletId = buildGraphPortletId(pdSink.portletId);
if (groupedGraphConnections[graphNodeId][graphPortletId]) {
return;
}
// Collect all sources for `pdGlobSink`
let pdGlobSources = [];
pdConnections.forEach((connection) => {
const [pdGlobSource, otherPdGlobSink] = connection;
if (_arePdGlobEndpointsEqual(pdGlobSink, otherPdGlobSink)) {
pdGlobSources.push(pdGlobSource);
}
});
// For each source, resolve it to a graph source, and split between
// signal and message sources.
const graphSignalSources = [];
const graphMessageSources = [];
pdGlobSources.forEach(([sourcePatchPath, pdSource]) => {
const sourcePatch = _currentPatch(sourcePatchPath);
const graphSource = {
nodeId: buildGraphNodeId(sourcePatch.id, pdSource.nodeId),
portletId: buildGraphPortletId(pdSource.portletId),
};
const sourceNode = getNode(graph, graphSource.nodeId);
const outlet = getOutlet(sourceNode, graphSource.portletId);
if (outlet.type === 'signal') {
graphSignalSources.push(graphSource);
}
else {
graphMessageSources.push(graphSource);
}
});
groupedGraphConnections[graphNodeId][graphPortletId] = {
signalSources: graphSignalSources,
messageSources: graphMessageSources,
};
});
return groupedGraphConnections;
};
/**
* Traverse the graph recursively and collect all connections in a flat list,
* by navigating inside and outside subpatches through their portlets.
*/
const _resolveSubpatches = (compilation, patchPath, pdConnections) => {
const { graph } = compilation;
const patch = _currentPatch(patchPath);
// First we remove connections for pd nodes that have been removed
// from the graph.
const connections = patch.connections.filter(({ source, sink }) => {
const sourceNodeId = buildGraphNodeId(patch.id, source.nodeId);
const sinkNodeId = buildGraphNodeId(patch.id, sink.nodeId);
if (graph[sourceNodeId] && graph[sinkNodeId]) {
return true;
}
return false;
});
connections.forEach(({ source, sink }) => {
const resolvedSources = _resolveSource(compilation, [patchPath, source]);
const resolvedSinks = _resolveSink(compilation, patchPath, sink);
resolvedSources.forEach((pdGSource) => resolvedSinks.forEach((pdGSink) => {
const alreadyExists = pdConnections.some(([otherPdGSource, otherPdGSink]) => {
return (_arePdGlobEndpointsEqual(pdGSource, otherPdGSource) && _arePdGlobEndpointsEqual(pdGSink, otherPdGSink));
});
if (!alreadyExists) {
pdConnections.push([pdGSource, pdGSink]);
}
}));
});
};
const _resolveSource = (compilation, [patchPath, source]) => {
const { pd } = compilation;
const patch = _currentPatch(patchPath);
const pdSourceNode = resolvePdNode(patch, source.nodeId);
// 1. If inlet, we lookup in parent patch for the sources of the
// corresponding inlets, then continue the resolution recursively.
if (pdSourceNode.type === 'inlet' || pdSourceNode.type === 'inlet~') {
const parentPatch = _parentPatch(patchPath);
// When we load an abstraction as main patch, it will have
// inlets / outlets which are not connected
if (!parentPatch) {
return [];
}
const subpatchNode = _resolveSubpatchNode(parentPatch, patch.id);
const subpatchNodePortletId = _resolveSubpatchPortletId(patch.inlets, pdSourceNode.id);
return parentPatch.connections
.filter(({ sink }) => sink.nodeId === subpatchNode.id &&
sink.portletId === subpatchNodePortletId)
.flatMap(({ source }) => _resolveSource(compilation, [
[...patchPath.slice(0, -1)],
source,
]));
// 2. If subpatch, we enter the subpatch and lookup for the
// sources of the corresponding outlet, then continue the
// resolution recursively.
}
else if (pdSourceNode.nodeClass === 'subpatch') {
const subpatch = resolvePatch(pd, pdSourceNode.patchId);
const outletPdNodeId = _resolveSubpatchPortletNode(subpatch.outlets, source.portletId);
return subpatch.connections
.filter(({ sink }) => sink.nodeId === outletPdNodeId && sink.portletId === 0)
.flatMap(({ source }) => _resolveSource(compilation, [[...patchPath, subpatch], source]));
// 3. This is the general case for all other nodes which are not
// subpatch related.
}
else {
return [[patchPath, source]];
}
};
const _resolveSink = (compilation, patchPath, pdSink) => {
const { pd } = compilation;
const patch = _currentPatch(patchPath);
const pdSinkNode = resolvePdNode(patch, pdSink.nodeId);
// 1. If outlet, we lookup in parent patch for the sinks of the
// corresponding outlets, then continue the resolution recursively.
if (pdSinkNode.type === 'outlet' || pdSinkNode.type === 'outlet~') {
const parentPatch = _parentPatch(patchPath);
// When we load an abstraction as main patch, it will have
// inlets / outlets which are not connected
if (!parentPatch) {
return [];
}
const subpatchNode = _resolveSubpatchNode(parentPatch, patch.id);
const subpatchNodePortletId = _resolveSubpatchPortletId(patch.outlets, pdSinkNode.id);
return parentPatch.connections
.filter(({ source }) => source.nodeId === subpatchNode.id &&
source.portletId === subpatchNodePortletId)
.flatMap(({ sink }) => _resolveSink(compilation, [...patchPath.slice(0, -1)], sink));
// 2. If subpatch, we enter the subpatch and lookup for the
// sinks of the corresponding inlet, then continue the
// resolution recursively.
}
else if (pdSinkNode.nodeClass === 'subpatch') {
const subpatch = resolvePatch(pd, pdSinkNode.patchId);
const inletPdNodeId = _resolveSubpatchPortletNode(subpatch.inlets, pdSink.portletId);
return subpatch.connections
.filter(({ source }) => source.nodeId === inletPdNodeId && source.portletId === 0)
.flatMap(({ sink }) => _resolveSink(compilation, [...patchPath, subpatch], sink));
// 3. This is the general case for all other nodes which are not
// subpatch related.
}
else {
return [[patchPath, pdSink]];
}
};
// ================================== HELPERS ================================== //
const _traversePatches = (compilation, patchPath, func) => {
const patch = _currentPatch(patchPath);
func(compilation, patchPath);
Object.values(patch.nodes).forEach((pdNode) => {
if (pdNode.nodeClass === 'subpatch') {
const subpatch = resolvePatch(compilation.pd, pdNode.patchId);
_traversePatches(compilation, [...patchPath, subpatch], func);
}
});
};
const _currentPatch = (patchPath) => {
const patch = patchPath.slice(-1)[0];
if (!patch) {
throw new Error(`patchPath empty !`);
}
return patch;
};
const _parentPatch = (patchPath) => {
if (patchPath.length < 2) {
return null;
}
return patchPath.slice(-2)[0];
};
const _rootPatch = (patchPath) => {
const firstRootPatch = patchPath
.slice(0)
.reverse()
.find((patch) => patch.isRoot);
if (!firstRootPatch) {
throw new Error(`Could not resolve root patch from path`);
}
return firstRootPatch;
};
const _resolveSubpatchPortletNode = (portletNodeIds, portletId) => {
const pdNodeId = portletNodeIds[portletId];
if (pdNodeId === undefined) {
throw new Error(`Portlet ${portletId} is undefined in patch.outlets/patch.inlets`);
}
return pdNodeId;
};
const _resolveSubpatchPortletId = (portletNodeIds, pdNodeId) => portletNodeIds.findIndex((portletId) => portletId === pdNodeId);
const _resolveSubpatchNode = (patch, patchId) => {
const subpatchNode = Object.values(patch.nodes).find((pdNode) => pdNode.nodeClass === 'subpatch' && pdNode.patchId === patchId);
if (subpatchNode === undefined) {
throw new Error(`could not find subpatch node with patchId=${patchId} inside patch ${patch.id}`);
}
return subpatchNode;
};
const _arePdGlobEndpointsEqual = ([pp1, ep1], [pp2, ep2]) => _currentPatch(pp1).id === _currentPatch(pp2).id &&
ep1.nodeId === ep2.nodeId &&
ep1.portletId === ep2.portletId;
export { _buildConnections, _buildNodes, _traversePatches, buildGraphNodeId, buildGraphPortletId, toDspGraph as default };