Compare commits

..

5 Commits

Author SHA1 Message Date
Switch Angel AKA Jade Rose ae202ac5bb Merge branch 'main' into glossing/highlighting 2025-12-08 06:21:27 +01:00
Aria a82ebbeef6 Remove unused import 2025-11-28 21:56:04 -06:00
Aria d4a1b571fd Throttle in draw 2025-11-28 21:55:00 -06:00
Aria d44f6d3ded Throttle fps 2025-11-28 20:39:22 -06:00
Aria 3b8f309a22 Don't activate draw when highlighting setting off 2025-11-28 20:12:20 -06:00
12 changed files with 188 additions and 229 deletions
+19 -2
View File
@@ -162,6 +162,7 @@ export class StrudelMirror {
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
this.hasPainters = false;
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -177,7 +178,7 @@ export class StrudelMirror {
onToggle: (started) => {
replOptions?.onToggle?.(started);
if (started) {
this.drawer.start(this.repl.scheduler);
this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
if (this.solo) {
// stop other repls when this one is started
document.dispatchEvent(
@@ -207,9 +208,10 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.hasPainters = options.pattern.getPainters().length > 0;
// if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
const drawTime = this.hasPainters ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler);
@@ -237,6 +239,7 @@ export class StrudelMirror {
cmEditor.style.backgroundColor = 'transparent';
}
const settings = codemirrorSettings.get();
this.isPatternHighlightingEnabled = parseBooleans(settings.isPatternHighlightingEnabled);
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
@@ -321,8 +324,17 @@ export class StrudelMirror {
flash(this.editor, ms);
}
highlight(haps, time) {
if (!this.isPatternHighlightingEnabled) {
return;
}
highlightMiniLocations(this.editor, time, haps);
}
shouldAnimate() {
return (
this.repl.scheduler.started &&
(this.isPatternHighlightingEnabled || this.hasPainters || this.onDraw !== this.draw)
);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
@@ -343,6 +355,11 @@ export class StrudelMirror {
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
if (key === 'isPatternHighlightingEnabled') {
this.isPatternHighlightingEnabled = value;
this.drawer.stop();
this.shouldAnimate() && this.drawer.start(this.repl.scheduler);
}
if (key === 'theme') {
activateTheme(value);
}
+8
View File
@@ -6,6 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
const ANIMATE_INTERVAL = 1000 / 15; // 15 FPS
export const getDrawContext = (id = 'test-canvas', options) => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
let canvas = document.querySelector('#' + id);
@@ -112,7 +114,13 @@ export class Framer {
}
start() {
const self = this;
let lastTime = 0;
let frame = requestAnimationFrame(function updateHighlights(time) {
if (time - lastTime < ANIMATE_INTERVAL) {
frame = requestAnimationFrame(updateHighlights);
return;
}
lastTime = time;
try {
self.onFrame(time);
} catch (err) {
+3 -3
View File
@@ -7,7 +7,6 @@ import {
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from '@strudel/webaudio';
import gm from './gm.mjs';
@@ -173,8 +172,9 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd);
const stop = (releaseTime) => {};
onceEnded(bufferSource, () => {
releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator);
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
onended();
});
return { node, stop };
-143
View File
@@ -1,143 +0,0 @@
/*
audioGraph.mjs - Shadow web audio graph used for managing connections
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audioGraph.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/>.
*/
import { logger } from './logger.mjs';
// This helper should be used instead of the `node.onended = callback` pattern
// It adds a mechanism to help minimize gc retention
export const onceEnded = (node, callback) => {
const onended = callback;
node.onended = function cleanup() {
onended && onended();
this.onended = null;
};
};
export const releaseAudioNode = (node) => {
if (node == null) return;
// check we received an AudioNode
if (!(node instanceof AudioNode)) {
throw new Error('releaseAudioNode can only release an AudioNode');
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
node.disconnect();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
node.start(node.context.currentTime + 5); // will never happen
node.stop();
}
}
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
// while `end` is not native to the web audio API, it is common practice in superdough
// to use that param in the worklets to trigger returning false from the processor
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
// Once the `anchor` node has ended, release all nodes in `toCleanup`
export const cleanupOnEnd = (anchor, toCleanup) => {
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
};
class Edge {
constructor(from, to) {
this.from = new WeakRef(from);
this.to = new WeakRef(to);
this.subGraphs = new Set();
}
disconnect() {
const from = this.from.deref();
const to = this.to.deref();
from && to && from.disconnect(to);
}
release() {
const from = this.from.deref();
if (from instanceof AudioNode) {
releaseAudioNode(from);
}
}
}
let audioGraph;
class AudioGraph {
constructor(id) {
this.id = id;
this.activeSubGraphs = [];
this.subGraphs = {};
this.edges = [];
this.subGraphCounter = 0;
}
connect(from, to) {
const edge = new Edge(from, to);
for (const subGraph of this.activeSubGraphs) {
// Track which subgraphs it's in
edge.subGraphs.add(subGraph.id);
// Add to the subgraph's `edges`
subGraph.edges.push(edge);
// Add to this' `edges`
this.edges.push(edge);
}
// Make the actual connection
return from.connect(to);
}
// Introduces a context wherein all connections will be added to both this graph
// and the subgraph and all edges will be tagged with the subgraph for tracking
asSubGraph(fn) {
const subGraphID = `${this.id}_${this.subGraphCounter}`;
this.subGraphCounter++;
const subGraph = new AudioGraph(subGraphID);
this.subGraphs[subGraphID] = subGraph;
this.activeSubGraphs.push(subGraph);
try {
return { subGraph, output: fn() };
} finally {
this.activeSubGraphs.pop();
}
}
// Disconnects all from-to connections (rather than naked `from.disconnect()`)
disconnect() {
this.edges.forEach((edge) => edge.disconnect());
this.edges = [];
}
// Release this entire graph (nodes will be fully disconnected, stopped, etc)
release() {
this.edges.forEach((edge) => edge.release());
this.edges = [];
}
}
export const getAudioGraph = () => {
if (audioGraph === undefined) {
audioGraph = new AudioGraph(0);
}
return audioGraph;
};
+90 -18
View File
@@ -1,5 +1,4 @@
import { getAudioContext } from './audioContext.mjs';
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
import { logger } from './logger.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
@@ -190,7 +189,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return lfo;
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, start, end, params, cps, cycle) {
let {
@@ -270,20 +269,27 @@ let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5);
// still not too sure about how this could be used more generally...
export function drywet(dry, wet, wetAmount = 0) {
const ac = getAudioContext();
const ag = getAudioGraph();
if (!wetAmount) {
return dry;
}
let dry_gain = ac.createGain();
let wet_gain = ac.createGain();
ag.connect(dry, dry_gain);
ag.connect(wet, wet_gain);
dry.connect(dry_gain);
wet.connect(wet_gain);
dry_gain.gain.value = wetfade(wetAmount);
wet_gain.gain.value = wetfade(1 - wetAmount);
const mix = ac.createGain();
ag.connect(dry_gain, mix);
ag.connect(wet_gain, mix);
return { node: mix };
let mix = ac.createGain();
dry_gain.connect(mix);
wet_gain.connect(mix);
return {
node: mix,
onended: () => {
dry_gain.disconnect(mix);
wet_gain.disconnect(mix);
dry.disconnect(dry_gain);
wet.disconnect(wet_gain);
},
};
}
let curves = ['linear', 'exponential'];
@@ -311,14 +317,17 @@ export function getVibratoOscillator(param, value, t) {
const { vibmod = 0.5, vib } = value;
let vibratoOscillator;
if (vib > 0) {
const ag = getAudioGraph();
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
ag.connect(vibratoOscillator, gain);
ag.connect(gain, param);
vibratoOscillator.connect(gain);
gain.connect(param);
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t);
return vibratoOscillator;
}
@@ -374,7 +383,7 @@ const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
export function applyFM(param, value, begin) {
const ac = getAudioContext();
const ag = getAudioGraph();
const toStop = []; // fm oscillators we will expose `stop` for
const fms = {};
// Matrix
for (let i = 1; i <= 8; i++) {
@@ -401,6 +410,8 @@ export function applyFM(param, value, begin) {
if (!fms[idx]) {
const idxS = idx === 1 ? '' : idx;
const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
toStop.push(osc);
const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
let output = osc;
if (adsr.some((v) => v !== undefined)) {
@@ -420,13 +431,15 @@ export function applyFM(param, value, begin) {
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
output = ag.connect(osc, envGain);
toCleanup.push(envGain);
output = osc.connect(envGain);
}
fms[idx] = { input: osc.frequency, output, freq };
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
}
const { input, output, freq } = fms[idx];
const { input, output, freq, osc, toCleanup } = fms[idx];
const g = gainNode(amt * freq);
io.push(isMod ? ag.connect(output, g) : input);
io.push(isMod ? output.connect(g) : input);
cleanupOnEnd(osc, [...toCleanup, g]);
}
if (!io[1]) {
logger(
@@ -435,9 +448,12 @@ export function applyFM(param, value, begin) {
);
continue;
}
ag.connect(io[0], io[1]);
io[0].connect(io[1]);
}
}
return {
stop: (t) => toStop.forEach((m) => m?.stop(t)),
};
}
// Saturation curves
@@ -549,3 +565,59 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
freq *= Math.pow(2, octave);
return Number(freq);
};
// This helper should be used instead of the `node.onended = callback` pattern
// It adds a mechanism to help minimize gc retention
export const onceEnded = (node, callback) => {
const onended = callback;
node.onended = function cleanup() {
onended && onended();
this.onended = null;
};
};
export const releaseAudioNode = (node) => {
if (node == null) return;
// check we received an AudioNode
if (!(node instanceof AudioNode)) {
throw new Error('releaseAudioNode can only release an AudioNode');
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect
node.disconnect();
// make sure all AudioScheduledSourceNodes are in a stopped state
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
if (node instanceof AudioScheduledSourceNode) {
if (node.onended && node.onended.name !== 'cleanup') {
logger(
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
);
}
try {
node.stop();
} catch (e) {
// At the stage, `start` was not called on the node
// but an `onended` callback releasing resources may exist
// and we want it to fire :
// - we force a start/stop cycle so that `onended` gets called
// - we `lock` the node so that no-one can start it
node.start(node.context.currentTime + 5); // will never happen
node.stop();
}
}
// https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing
// An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]]
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
// Once the `anchor` node has ended, release all nodes in `toCleanup`
export const cleanupOnEnd = (anchor, toCleanup) => {
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
};
+6 -7
View File
@@ -4,13 +4,12 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
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/>.
*/
export * from './audioContext.mjs';
export * from './audioGraph.mjs';
export * from './dspworklet.mjs';
export * from './helpers.mjs';
export * from './logger.mjs';
export * from './sampler.mjs';
export * from './superdough.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
export * from './wavetable.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './audioContext.mjs';
export * from './wavetable.mjs';
+4 -11
View File
@@ -1,6 +1,5 @@
import { getAudioContext } from './audioContext.mjs';
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
import { drywet } from './helpers.mjs';
import { getAudioContext } from './audioContext.mjs';
let noiseCache = {};
@@ -64,17 +63,11 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
}
export function getNoiseMix(inputNode, wet, t) {
const ag = getAudioGraph();
const noiseOscillator = getNoiseOscillator('pink', t);
const { subGraph, output } = ag.asSubGraph(() => {
return drywet(inputNode, noiseOscillator.node, wet);
});
onceEnded(noiseOscillator.node, () => {
releaseAudioNode(noiseOscillator.node);
});
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
noiseOscillator.node.onended = () => noiseMix.onended();
return {
node: output.node,
node: noiseMix.node,
stop: (time) => noiseOscillator?.stop(time),
teardown: subGraph.release,
};
}
+13 -7
View File
@@ -1,8 +1,14 @@
import { getAudioContext } from './audioContext.mjs';
import { onceEnded, releaseAudioNode } from './audioGraph.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from './helpers.mjs';
import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
@@ -317,10 +323,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
node.connect(out);
onceEnded(bufferSource, function () {
releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator);
releaseAudioNode(node);
releaseAudioNode(out);
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
out.disconnect();
onended();
});
let envEnd = holdEnd + release + 0.01;
+1 -2
View File
@@ -14,7 +14,6 @@ import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs';
import { releaseAudioNode } from './audioGraph.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -506,7 +505,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const onEnded = () => {
audioNodes.forEach((n) => releaseAudioNode(n));
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded, cps);
+37 -29
View File
@@ -1,7 +1,6 @@
import { clamp } from './util.mjs';
import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
import {
applyFM,
gainNode,
@@ -13,6 +12,7 @@ import {
getVibratoOscillator,
getWorklet,
noises,
releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -52,7 +52,7 @@ export function registerSynthSounds() {
const g = gainNode(0.3);
let sound = getOscillator(s, t, value, () => {
releaseAudioNode(g);
g.disconnect();
onended();
});
@@ -110,15 +110,15 @@ export function registerSynthSounds() {
const mix = gainNode(mixGain);
onceEnded(o, () => {
releaseAudioNode(o);
releaseAudioNode(g);
releaseAudioNode(sat);
releaseAudioNode(noise.node);
releaseAudioNode(noiseGain);
releaseAudioNode(mix);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
});
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
@@ -384,11 +384,11 @@ export function registerSynthSounds() {
const { duration } = value;
onceEnded(o, () => {
releaseAudioNode(o);
releaseAudioNode(g);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
});
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
@@ -477,25 +477,33 @@ export function getOscillator(s, t, value, onended) {
}
// set frequency
o.frequency.value = getFrequencyFromValue(value);
const ag = getAudioGraph();
const { subGraph, output } = ag.asSubGraph(() => {
getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
applyFM(o.frequency, value, t);
let noiseMix;
if (noise) {
noiseMix = getNoiseMix(o, noise, t);
}
return { node: noiseMix?.node || o };
});
onceEnded(o, () => subGraph.release());
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix;
if (noise) {
noiseMix = getNoiseMix(o, noise, t);
}
o.onended = () => {
noiseMix || o.disconnect();
noiseMix?.node.disconnect();
onended();
};
o.start(t);
return {
node: output.node,
stop: (time) => o.stop(time),
node: noiseMix?.node || o,
stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
o.stop(time);
},
triggerRelease: (time) => {
// envGain?.stop(time);
},
+4 -4
View File
@@ -1,4 +1,3 @@
import { releaseAudioNode } from './audioGraph.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import {
@@ -10,6 +9,7 @@ import {
getPitchEnvelope,
getVibratoOscillator,
getWorklet,
releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -320,10 +320,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
ac,
() => {
releaseAudioNode(source);
releaseAudioNode(vibratoOscillator);
vibratoOscillator?.stop();
fm?.stop();
releaseAudioNode(wtPosModulators);
releaseAudioNode(wtWarpModulators);
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
+3 -3
View File
@@ -1,9 +1,9 @@
//import { ZZFX } from 'zzfx';
import { getAudioContext } from './audioContext.mjs';
import { onceEnded, releaseAudioNode } from './audioGraph.mjs';
import { registerSound } from './superdough.mjs';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import { buildSamples } from './zzfx_fork.mjs';
import { onceEnded, releaseAudioNode } from './helpers.mjs';
export const getZZFX = (value, t) => {
let {