webpd
Version:
WebPd is a compiler for audio programming language Pure Data allowing to run .pd patches on web pages.
282 lines (261 loc) • 12 kB
JavaScript
import WEBPD_RUNTIME_CODE from './assets/runtime.js.txt.js';
import { traversePdGui } from '../../../pd-gui/index.js';
import { readMetadata } from '../../../../node_modules/@webpd/compiler/dist/src/run/index.js';
import { getNode } 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 WEBPD_RUNTIME_FILENAME = 'webpd-runtime.js';
var buildApp = async (artefacts) => {
if (!artefacts.javascript && !artefacts.wasm) {
throw new Error(`Needs at least javascript or wasm to run`);
}
let target;
let compiledPatchFilename;
let engineMetadata;
let compiledPatchCode;
if (artefacts.javascript) {
target = 'javascript';
compiledPatchFilename = 'patch.js';
engineMetadata = await readMetadata('javascript', artefacts.javascript);
compiledPatchCode = artefacts.javascript;
}
else {
target = 'assemblyscript';
compiledPatchFilename = 'patch.wasm';
engineMetadata = await readMetadata('assemblyscript', artefacts.wasm);
compiledPatchCode = artefacts.wasm;
}
const webPdMetadata = engineMetadata.customMetadata;
if (!webPdMetadata.pdGui || !webPdMetadata.graph || !webPdMetadata.pdNodes) {
throw new Error(`Missing data in WebPd metadata`);
}
const generatedApp = {
[WEBPD_RUNTIME_FILENAME]: WEBPD_RUNTIME_CODE,
[compiledPatchFilename]: compiledPatchCode,
// prettier-ignore
'index.html': `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WebPd boilerplate</title>
<style>
#start {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#loading {
width: 100%;
height: 100%;
position: fixed;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<h1>My Web Page</h1>
<div>For more info about usage (how to interact with the patch), you can open this HTML file in a code editor.</div>
<button id="start"> Start </button>
<div id="loading"> Loading ... </div>
<script src="${WEBPD_RUNTIME_FILENAME}"></script>
<script>
// SUMMARY
// 1. WEB PAGE INITIALIZATION
// 2. SENDING MESSAGES FROM JAVASCRIPT TO THE PATCH
// 3. SENDING MESSAGES FROM THE PATCH TO JAVASCRIPT
// ------------- 1. WEB PAGE INITIALIZATION
const loadingDiv = document.querySelector('#loading')
const startButton = document.querySelector('#start')
const audioContext = new AudioContext()
let patch = null
let stream = null
let webpdNode = null
const initApp = async () => {
// Register the worklet
await WebPdRuntime.initialize(audioContext)
// Fetch the patch code
response = await fetch('${compiledPatchFilename}')
patch = await ${target === 'javascript' ?
'response.text()' : 'response.arrayBuffer()'}
// Comment this if you don't need audio input
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
// Hide loading and show start button
loadingDiv.style.display = 'none'
startButton.style.display = 'block'
}
const startApp = async () => {
// AudioContext needs to be resumed on click to protects users
// from being spammed with autoplay.
// See : https://github.com/WebAudio/web-audio-api/issues/345
if (audioContext.state === 'suspended') {
audioContext.resume()
}
// Setup web audio graph
webpdNode = await WebPdRuntime.run(
audioContext,
patch,
WebPdRuntime.defaultSettingsForRun(
'./${compiledPatchFilename}',
// Comment this if you don't need to receive messages from the patch
receiveMsgFromWebPd,
),
)
webpdNode.connect(audioContext.destination)
// Comment this if you don't need audio input
const sourceNode = audioContext.createMediaStreamSource(stream)
sourceNode.connect(webpdNode)
// Hide the start button
startButton.style.display = 'none'
}
startButton.onclick = startApp
initApp().
then(() => {
console.log('App initialized')
})
// ------------- 2. SENDING MESSAGES FROM JAVASCRIPT TO THE PATCH
// Use the function sendMsgToWebPd to send a message from JavaScript to an object inside your patch.
//
// Parameters :
// - nodeId: the ID of the object you want to send a message to.
// This ID is a string that has been assigned by WebPd at compilation.
// You can find below the list of available IDs with hints to help you
// identify the object you want to interact with.
// - portletId : the ID of the object portlet to which the message should be sent.
// - message : the message to send. This must be a list of strings and / or numbers.
//
// Examples :
// - sending a message to a bang node of ID 'n_0_1' :
// sendMsgToWebPd('n_0_1', '0', ['bang'])
// - sending a message to a number object of ID 'n_0_2' :
// sendMsgToWebPd('n_0_2', '0', [123])
//
const sendMsgToWebPd = (nodeId, portletId, message) => {
webpdNode.port.postMessage({
type: 'io:messageReceiver',
payload: {
nodeId,
portletId,
message,
},
})
}
// Here is an index of objects IDs to which you can send messages, with hints so you can find the right ID.
// Note that by default only GUI objects (bangs, sliders, etc ...) are available.${renderIoMessageReceiversOrSenders(engineMetadata.settings.io.messageReceivers, webPdMetadata,
// Render controls
(node, portletId, layout) => `
// - nodeId "${node.id}" portletId "${portletId}"
// * type "${node.type}"
// * position ${layout.x} ${layout.y}${layout.label ? `
// * label "${layout.label}"` : ''}
`,
// Render send/receive
(node, portletId) => `
// - nodeId "${node.id}" portletId "${portletId}"
// * type "send"
// * send "${node.args.busName}"
`,
// Render if empty io specs
`
// EMPTY (did you place a GUI object or send object in your patch ?)
`)}
// ------------- 3. SENDING MESSAGES FROM THE PATCH TO JAVASCRIPT
// Use the function receiveMsgFromWebPd to receive a message from an object inside your patch.
//
// Parameters :
// - nodeId: the ID of the object that is sending a message.
// This ID is a string that has been assigned by WebPd at compilation.
// You can find below the list of available IDs with hints to help you
// identify the object you want to interact with.
// - portletId : the ID of the object portlet that is sending the message.
// - message : the message that was sent. It is a list of strings and / or numbers.
const receiveMsgFromWebPd = (nodeId, portletId, message) => {${renderIoMessageReceiversOrSenders(engineMetadata.settings.io.messageSenders, webPdMetadata,
// Render controls
(node, portletId, layout) => `
if (nodeId === "${node.id}" && portletId === "${portletId}") {
console.log('Message received from :\\n'
+ '\t* nodeId "${node.id}" portletId "${portletId}"\\n'
+ '\t* type "${node.type}"\\n'
+ '\t* position ${layout.x} ${layout.y}\\n'${layout.label ? `
+ '\t* label "${layout.label}"'` : ''}
)
}`,
// Render send/receive
(node, portletId) => `
if (nodeId === "${node.id}" && portletId === "${portletId}") {
console.log('Message received from :\\n'
+ '\t* nodeId "${node.id}" portletId "${portletId}"\\n'
+ '\t* type "receive"\\n'
+ '\t* receive "${node.args.busName}"'
)
}`,
// Render if empty io specs
`
// /!\ there seems to be no message senders in the patch.
// Add a GUI object or a send object in your patch to be able to receive messages.
`)}
}
</script>
</body>
</html>`,
};
return generatedApp;
};
const renderIoMessageReceiversOrSenders = (ioMessageSpecs, webPdMetadata, renderControl, renderSendReceive, emptyString) => {
if (Object.keys(ioMessageSpecs).length) {
const indexedPdGuiNodes = {};
traversePdGui(webPdMetadata.pdGui, (pdGuiNode) => {
if (pdGuiNode.nodeClass === 'control') {
indexedPdGuiNodes[pdGuiNode.nodeId] = pdGuiNode;
}
});
return Object.entries(ioMessageSpecs)
.flatMap(([nodeId, portletIds]) => portletIds.map((portletId) => {
const node = getNode(webPdMetadata.graph, nodeId);
if (node.type === 'send' || node.type === 'receive') {
return renderSendReceive(node, portletId);
}
const pdGuiNode = indexedPdGuiNodes[nodeId];
if (!pdGuiNode) {
return '';
}
else if (pdGuiNode.nodeClass === 'control') {
const pdNode = webPdMetadata.pdNodes[pdGuiNode.patchId][pdGuiNode.pdNodeId];
return renderControl(node, portletId, pdNode.layout);
}
else {
return '';
}
}))
.join('');
}
else {
return emptyString;
}
};
export { WEBPD_RUNTIME_FILENAME, buildApp as default };