jevstrudel.git / packages / superdough / nodePools.mjs

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

70const isNodeAlive = (node) => {
71  // Skip check if node is not a worklet
72  if (!(node instanceof AudioWorkletNode)) return true;
73  const now = getNodeTime(node);
74  const end = node?.parameters?.get('end').value ?? 0;
75  return now < end + 0.45;
76};

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};