Switch to tracking grace period in node pool; add negative begin and ends
This commit is contained in:
parent
1eb657e6fe
commit
1f1f3288a6
4 changed files with 53 additions and 75 deletions
|
|
@ -7,10 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
|
|
||||||
const nodePools = new Map();
|
const nodePools = new Map();
|
||||||
const POOL_KEY = Symbol('nodePoolKey');
|
const POOL_KEY = Symbol('nodePoolKey');
|
||||||
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
|
|
||||||
|
|
||||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||||
|
|
||||||
|
const getNodeTime = (node) => {
|
||||||
|
return node.context?.currentTime ?? 0;
|
||||||
|
};
|
||||||
|
|
||||||
const getParams = (node) => {
|
const getParams = (node) => {
|
||||||
const params = new Set();
|
const params = new Set();
|
||||||
node.parameters?.forEach((param) => params.add(param));
|
node.parameters?.forEach((param) => params.add(param));
|
||||||
|
|
@ -37,32 +40,43 @@ export const releaseNodeToPool = (node) => {
|
||||||
// not reusable
|
// not reusable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node[IS_WORKLET_DEAD]) {
|
|
||||||
// Worklet already terminated, don't pool it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = node[POOL_KEY];
|
const key = node[POOL_KEY];
|
||||||
if (key == null) return;
|
if (key == null) return;
|
||||||
const now = node.context?.currentTime ?? 0;
|
const now = getNodeTime(node);
|
||||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||||
const pool = nodePools.get(key) ?? [];
|
const pool = nodePools.get(key) ?? [];
|
||||||
pool.push(new WeakRef(node));
|
pool.push(new WeakRef(node));
|
||||||
nodePools.set(key, pool);
|
nodePools.set(key, pool);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
// 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
|
||||||
|
const isNodeAlive = (node) => {
|
||||||
|
// Skip check if node is not a worklet
|
||||||
|
if (!(node instanceof AudioWorkletNode)) return true;
|
||||||
|
const now = getNodeTime(node);
|
||||||
|
const end = node?.parameters?.get('end').value ?? 0;
|
||||||
|
return now < end + 0.45;
|
||||||
|
};
|
||||||
|
|
||||||
// Attempt to get node from the pool. If this fails, fall back
|
// Attempt to get node from the pool. If this fails, fall back
|
||||||
// to building it with the factory
|
// to building it with the factory
|
||||||
export const getNodeFromPool = (key, factory) => {
|
export const getNodeFromPool = (key, factory) => {
|
||||||
const pool = nodePools.get(key) ?? [];
|
const pool = nodePools.get(key) ?? [];
|
||||||
let node;
|
let node;
|
||||||
|
let found = false;
|
||||||
while (pool.length) {
|
while (pool.length) {
|
||||||
const ref = pool.pop();
|
const ref = pool.pop();
|
||||||
node = ref?.deref();
|
node = ref?.deref();
|
||||||
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
if (node != null && isNodeAlive(node)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (node == null || node[IS_WORKLET_DEAD]) {
|
if (!found) {
|
||||||
node = factory();
|
node = factory();
|
||||||
}
|
}
|
||||||
node[POOL_KEY] = key;
|
node[POOL_KEY] = key;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
|
|
@ -185,13 +185,6 @@ export function registerSynthSounds() {
|
||||||
param.setValueAtTime(target, now);
|
param.setValueAtTime(target, now);
|
||||||
});
|
});
|
||||||
o.port.postMessage({ type: 'initialize' });
|
o.port.postMessage({ type: 'initialize' });
|
||||||
o.port.onmessage = (e) => {
|
|
||||||
if (e.data.type === 'died') {
|
|
||||||
markWorkletAsDead(o);
|
|
||||||
o.port.postMessage({ type: 'diedACK' });
|
|
||||||
}
|
|
||||||
o.port.onmessage = null;
|
|
||||||
};
|
|
||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
releaseAudioNode,
|
releaseAudioNode,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export const Warpmode = Object.freeze({
|
export const Warpmode = Object.freeze({
|
||||||
|
|
@ -251,13 +251,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||||
param.setValueAtTime(target, now);
|
param.setValueAtTime(target, now);
|
||||||
});
|
});
|
||||||
source.port.postMessage({ type: 'initialize', payload });
|
source.port.postMessage({ type: 'initialize', payload });
|
||||||
source.port.onmessage = (e) => {
|
|
||||||
if (e.data.type === 'died') {
|
|
||||||
markWorkletAsDead(source);
|
|
||||||
source.port.postMessage({ type: 'diedACK' });
|
|
||||||
}
|
|
||||||
source.port.onmessage = null;
|
|
||||||
};
|
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -463,20 +463,11 @@ registerProcessor('distort-processor', DistortProcessor);
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.isAlive = true; // used internally to prevent multiple death messages
|
|
||||||
// diedACK is used for the main thread to acknowledge that the worklet has died
|
|
||||||
// This is so that we don't risk simultaneously killing the worklet (`return false`)
|
|
||||||
// and pooling the worklet (main thread) before the async `died` message hits
|
|
||||||
// the main thread
|
|
||||||
this.diedACK = false;
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'initialize') {
|
if (type === 'initialize') {
|
||||||
this.initialize(payload);
|
this.initialize(payload);
|
||||||
}
|
}
|
||||||
if (type === 'diedACK') {
|
|
||||||
this.diedACK = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
@ -487,16 +478,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'begin',
|
name: 'begin',
|
||||||
defaultValue: 0,
|
defaultValue: -1,
|
||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
min: -1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'end',
|
name: 'end',
|
||||||
defaultValue: 0,
|
defaultValue: -1,
|
||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
min: -1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -531,19 +522,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(_input, outputs, params) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime >= params.end[0] + 0.5) {
|
const begin = params.begin[0];
|
||||||
// Outside of grace period - should terminate
|
const end = params.end[0];
|
||||||
if (this.isAlive) {
|
const beginDefined = begin >= 0;
|
||||||
this.port.postMessage({ type: 'died' });
|
const endDefined = end >= 0;
|
||||||
this.isAlive = false;
|
// We give a 0.5s grace period (for node pooling) before termination
|
||||||
}
|
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||||
if (this.diedACK || currentTime >= params.end[1] + 1) {
|
const ended = endDefined && currentTime >= end;
|
||||||
return false;
|
const notStarted = currentTime <= begin;
|
||||||
}
|
if (shouldTerminate) {
|
||||||
return true;
|
return false;
|
||||||
}
|
} else if (ended || notStarted || !beginDefined) {
|
||||||
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
|
||||||
// Inside of grace period or not yet started
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
|
|
@ -1159,8 +1148,8 @@ const tablesCache = {};
|
||||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||||
{ name: 'detune', defaultValue: 0 },
|
{ name: 'detune', defaultValue: 0 },
|
||||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
|
|
@ -1175,20 +1164,11 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.isAlive = true; // used internally to prevent multiple death messages
|
|
||||||
// diedACK is used for the main thread to acknowledge that the worklet has died
|
|
||||||
// This is so that we don't risk simultaneously killing the worklet (`return false`)
|
|
||||||
// and pooling the worklet (main thread) before the async `died` message hits
|
|
||||||
// the main thread
|
|
||||||
this.diedACK = false;
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'initialize') {
|
if (type === 'initialize') {
|
||||||
this.initialize(payload);
|
this.initialize(payload);
|
||||||
}
|
}
|
||||||
if (type === 'diedACK') {
|
|
||||||
this.diedACK = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
@ -1351,19 +1331,17 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
if (currentTime >= parameters.end[0] + 0.5) {
|
const begin = parameters.begin[0];
|
||||||
// Outside of grace period - should terminate
|
const end = parameters.end[0];
|
||||||
if (this.isAlive) {
|
const beginDefined = begin >= 0;
|
||||||
this.port.postMessage({ type: 'died' });
|
const endDefined = end >= 0;
|
||||||
this.isAlive = false;
|
// We give a 0.5s grace period (for node pooling) before termination
|
||||||
}
|
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||||
if (this.diedACK || currentTime >= parameters.end[1] + 1) {
|
const ended = endDefined && currentTime >= end;
|
||||||
return false;
|
const notStarted = currentTime <= begin;
|
||||||
}
|
if (shouldTerminate) {
|
||||||
return true;
|
return false;
|
||||||
}
|
} else if (ended || notStarted || !beginDefined) {
|
||||||
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
|
||||||
// Inside of grace period or not yet started
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const outL = outputs[0][0];
|
const outL = outputs[0][0];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue