nodePools.mjs - Helper functions related to pooling and re-using audio nodes
Copyright (C) 2025 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
8import { releaseAudioNode } from './helpers.mjs';
10const nodePools = new Map(); 11const POOL_KEY = Symbol('nodePoolKey'); 12 13export const isPoolable = (node) => !!node[POOL_KEY]; 14 15export const clearNodePool = () => { 16 for (const pool of nodePools) { 17 for (const node of pool) { 18 if (node instanceof AudioNode) { 19 releaseAudioNode(node); 20 } 21 } 22 } 23 nodePools.clear(); 24}; 25 26const getNodeTime = (node) => { 27 return node.context?.currentTime ?? 0; 28}; 29 30const getParams = (node) => { 31 const params = new Set(); 32 node.parameters?.forEach((param) => params.add(param)); 33 const visited = new Set(); // prioritize deepest definition 34 let proto = node; 35 // Move up the prototype chain 36 while (proto !== Object.prototype) { 37 for (const key of Object.getOwnPropertyNames(proto)) { 38 if (visited.has(key)) continue; 39 visited.add(key); 40 const value = node[key]; 41 if (value instanceof AudioParam) { 42 params.add(value); 43 } 44 } 45 proto = Object.getPrototypeOf(proto); 46 } 47 return params; 48}; 49 50export const releaseNodeToPool = (node) => { 51 node.disconnect(); 52 if (node instanceof AudioScheduledSourceNode) { 53 // not reusable 54 return; 55 } 56 const key = node[POOL_KEY]; 57 if (key == null) return; 58 const now = getNodeTime(node); 59 getParams(node).forEach((param) => param.cancelScheduledValues(now)); 60 const pool = nodePools.get(key) ?? []; 61 pool.push(new WeakRef(node)); 62 nodePools.set(key, pool); 63};
Audio worklets are given a grace period to survive (return true) after
being released. This concludes at time end + 0.5. We test here whether we are
within some safe distance of that (end + 0.45) and if so, permit the node to be
released. This helps to prevent race conditions between node termination and node
re-use
Attempt to get node from the pool. If this fails, fall back to building it with the factory
80export const getNodeFromPool = (key, factory) => { 81 const pool = nodePools.get(key) ?? []; 82 let node; 83 let found = false; 84 while (pool.length) { 85 const ref = pool.pop(); 86 node = ref?.deref(); 87 if (node != null && isNodeAlive(node)) { 88 found = true; 89 break; 90 } 91 } 92 if (!found) { 93 node = factory(); 94 } 95 node[POOL_KEY] = key; 96 return node; 97};