mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 05:33:13 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c08bd1014 | |||
| e1fc5eec5b | |||
| 07c1a9eb23 | |||
| c0423c1757 | |||
| acd1f9e691 | |||
| 6429b0cb96 |
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
@@ -269,30 +270,20 @@ 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...
|
// still not too sure about how this could be used more generally...
|
||||||
export function drywet(dry, wet, wetAmount = 0) {
|
export function drywet(dry, wet, wetAmount = 0) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
|
const ag = getAudioGraph();
|
||||||
if (!wetAmount) {
|
if (!wetAmount) {
|
||||||
return dry;
|
return dry;
|
||||||
}
|
}
|
||||||
let dry_gain = ac.createGain();
|
let dry_gain = ac.createGain();
|
||||||
let wet_gain = ac.createGain();
|
let wet_gain = ac.createGain();
|
||||||
dry.connect(dry_gain);
|
ag.connect(dry, dry_gain);
|
||||||
wet.connect(wet_gain);
|
ag.connect(wet, wet_gain);
|
||||||
dry_gain.gain.value = wetfade(wetAmount);
|
dry_gain.gain.value = wetfade(wetAmount);
|
||||||
wet_gain.gain.value = wetfade(1 - wetAmount);
|
wet_gain.gain.value = wetfade(1 - wetAmount);
|
||||||
let mix = ac.createGain();
|
const mix = ac.createGain();
|
||||||
dry_gain.connect(mix);
|
ag.connect(dry_gain, mix);
|
||||||
wet_gain.connect(mix);
|
ag.connect(wet_gain, mix);
|
||||||
return {
|
return { node: mix };
|
||||||
node: mix,
|
|
||||||
teardown: () => {
|
|
||||||
releaseAudioNode(dry_gain);
|
|
||||||
releaseAudioNode(wet_gain);
|
|
||||||
// it is not the responsability of drywet
|
|
||||||
// to call `releaseAudioNode` on
|
|
||||||
// the 2 external args dry and wet
|
|
||||||
dry.disconnect(dry_gain);
|
|
||||||
wet.disconnect(wet_gain);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let curves = ['linear', 'exponential'];
|
let curves = ['linear', 'exponential'];
|
||||||
@@ -320,17 +311,14 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
const { vibmod = 0.5, vib } = value;
|
const { vibmod = 0.5, vib } = value;
|
||||||
let vibratoOscillator;
|
let vibratoOscillator;
|
||||||
if (vib > 0) {
|
if (vib > 0) {
|
||||||
|
const ag = getAudioGraph();
|
||||||
vibratoOscillator = getAudioContext().createOscillator();
|
vibratoOscillator = getAudioContext().createOscillator();
|
||||||
vibratoOscillator.frequency.value = vib;
|
vibratoOscillator.frequency.value = vib;
|
||||||
const gain = getAudioContext().createGain();
|
const gain = getAudioContext().createGain();
|
||||||
// Vibmod is the amount of vibrato, in semitones
|
// Vibmod is the amount of vibrato, in semitones
|
||||||
gain.gain.value = vibmod * 100;
|
gain.gain.value = vibmod * 100;
|
||||||
vibratoOscillator.connect(gain);
|
ag.connect(vibratoOscillator, gain);
|
||||||
gain.connect(param);
|
ag.connect(gain, param);
|
||||||
onceEnded(vibratoOscillator, () => {
|
|
||||||
releaseAudioNode(gain);
|
|
||||||
releaseAudioNode(vibratoOscillator);
|
|
||||||
});
|
|
||||||
vibratoOscillator.start(t);
|
vibratoOscillator.start(t);
|
||||||
return vibratoOscillator;
|
return vibratoOscillator;
|
||||||
}
|
}
|
||||||
@@ -386,7 +374,7 @@ const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
|
|||||||
|
|
||||||
export function applyFM(param, value, begin) {
|
export function applyFM(param, value, begin) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const toStop = []; // fm oscillators we will expose `stop` for
|
const ag = getAudioGraph();
|
||||||
const fms = {};
|
const fms = {};
|
||||||
// Matrix
|
// Matrix
|
||||||
for (let i = 1; i <= 8; i++) {
|
for (let i = 1; i <= 8; i++) {
|
||||||
@@ -413,8 +401,6 @@ export function applyFM(param, value, begin) {
|
|||||||
if (!fms[idx]) {
|
if (!fms[idx]) {
|
||||||
const idxS = idx === 1 ? '' : idx;
|
const idxS = idx === 1 ? '' : idx;
|
||||||
const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
|
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}`]);
|
const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
|
||||||
let output = osc;
|
let output = osc;
|
||||||
if (adsr.some((v) => v !== undefined)) {
|
if (adsr.some((v) => v !== undefined)) {
|
||||||
@@ -434,15 +420,13 @@ export function applyFM(param, value, begin) {
|
|||||||
holdEnd,
|
holdEnd,
|
||||||
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
||||||
);
|
);
|
||||||
toCleanup.push(envGain);
|
output = ag.connect(osc, envGain);
|
||||||
output = osc.connect(envGain);
|
|
||||||
}
|
}
|
||||||
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
fms[idx] = { input: osc.frequency, output, freq };
|
||||||
}
|
}
|
||||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
const { input, output, freq } = fms[idx];
|
||||||
const g = gainNode(amt * freq);
|
const g = gainNode(amt * freq);
|
||||||
io.push(isMod ? output.connect(g) : input);
|
io.push(isMod ? ag.connect(output, g) : input);
|
||||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
|
||||||
}
|
}
|
||||||
if (!io[1]) {
|
if (!io[1]) {
|
||||||
logger(
|
logger(
|
||||||
@@ -451,12 +435,9 @@ export function applyFM(param, value, begin) {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
io[0].connect(io[1]);
|
ag.connect(io[0], io[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saturation curves
|
// Saturation curves
|
||||||
@@ -568,61 +549,3 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|||||||
freq *= Math.pow(2, octave);
|
freq *= Math.pow(2, octave);
|
||||||
return Number(freq);
|
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) {
|
|
||||||
// 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)));
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ 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/>.
|
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 './superdough.mjs';
|
|
||||||
export * from './sampler.mjs';
|
|
||||||
export * from './helpers.mjs';
|
|
||||||
export * from './synth.mjs';
|
|
||||||
export * from './zzfx.mjs';
|
|
||||||
export * from './logger.mjs';
|
|
||||||
export * from './dspworklet.mjs';
|
|
||||||
export * from './audioContext.mjs';
|
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 './synth.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
export * from './zzfx.mjs';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs';
|
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
|
||||||
|
import { drywet } from './helpers.mjs';
|
||||||
|
|
||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
@@ -63,14 +64,17 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNoiseMix(inputNode, wet, t) {
|
export function getNoiseMix(inputNode, wet, t) {
|
||||||
|
const ag = getAudioGraph();
|
||||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
const { subGraph, output } = ag.asSubGraph(() => {
|
||||||
|
return drywet(inputNode, noiseOscillator.node, wet);
|
||||||
|
});
|
||||||
onceEnded(noiseOscillator.node, () => {
|
onceEnded(noiseOscillator.node, () => {
|
||||||
releaseAudioNode(noiseOscillator.node);
|
releaseAudioNode(noiseOscillator.node);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
node: noiseMix.node,
|
node: output.node,
|
||||||
stop: (time) => noiseOscillator?.stop(time),
|
stop: (time) => noiseOscillator?.stop(time),
|
||||||
teardown: noiseMix.teardown,
|
teardown: subGraph.release,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { onceEnded, releaseAudioNode } from './audioGraph.mjs';
|
||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import {
|
|
||||||
getADSRValues,
|
|
||||||
getParamADSR,
|
|
||||||
getPitchEnvelope,
|
|
||||||
getVibratoOscillator,
|
|
||||||
onceEnded,
|
|
||||||
releaseAudioNode,
|
|
||||||
} from './helpers.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|||||||
@@ -9,20 +9,12 @@ import './reverb.mjs';
|
|||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import {
|
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||||
createFilter,
|
|
||||||
gainNode,
|
|
||||||
getCompressor,
|
|
||||||
getDistortion,
|
|
||||||
getLfo,
|
|
||||||
getWorklet,
|
|
||||||
effectSend,
|
|
||||||
releaseAudioNode,
|
|
||||||
} from './helpers.mjs';
|
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { releaseAudioNode } from './audioGraph.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, soundMap } from './superdough.mjs';
|
import { registerSound, soundMap } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { getAudioGraph, onceEnded, releaseAudioNode } from './audioGraph.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
gainNode,
|
gainNode,
|
||||||
@@ -12,8 +13,6 @@ import {
|
|||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
noises,
|
noises,
|
||||||
onceEnded,
|
|
||||||
releaseAudioNode,
|
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
@@ -478,34 +477,25 @@ export function getOscillator(s, t, value, onended) {
|
|||||||
}
|
}
|
||||||
// set frequency
|
// set frequency
|
||||||
o.frequency.value = getFrequencyFromValue(value);
|
o.frequency.value = getFrequencyFromValue(value);
|
||||||
|
const ag = getAudioGraph();
|
||||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
const { subGraph, output } = ag.asSubGraph(() => {
|
||||||
|
getVibratoOscillator(o.detune, value, t);
|
||||||
// pitch envelope
|
// pitch envelope
|
||||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||||
const fmModulator = applyFM(o.frequency, value, t);
|
applyFM(o.frequency, value, t);
|
||||||
|
let noiseMix;
|
||||||
let noiseMix;
|
if (noise) {
|
||||||
if (noise) {
|
noiseMix = getNoiseMix(o, noise, t);
|
||||||
noiseMix = getNoiseMix(o, noise, t);
|
}
|
||||||
}
|
return { node: noiseMix?.node || o };
|
||||||
|
|
||||||
onceEnded(o, () => {
|
|
||||||
noiseMix?.teardown();
|
|
||||||
releaseAudioNode(o);
|
|
||||||
releaseAudioNode(noiseMix?.node);
|
|
||||||
onended();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onceEnded(o, () => subGraph.release());
|
||||||
o.start(t);
|
o.start(t);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix?.node || o,
|
node: output.node,
|
||||||
stop: (time) => {
|
stop: (time) => o.stop(time),
|
||||||
fmModulator.stop(time);
|
|
||||||
vibratoOscillator?.stop(time);
|
|
||||||
noiseMix?.stop(time);
|
|
||||||
o.stop(time);
|
|
||||||
},
|
|
||||||
triggerRelease: (time) => {
|
triggerRelease: (time) => {
|
||||||
// envGain?.stop(time);
|
// envGain?.stop(time);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { releaseAudioNode } from './audioGraph.mjs';
|
||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
releaseAudioNode,
|
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//import { ZZFX } from 'zzfx';
|
//import { ZZFX } from 'zzfx';
|
||||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
|
||||||
import { registerSound } from './superdough.mjs';
|
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { onceEnded, releaseAudioNode } from './audioGraph.mjs';
|
||||||
|
import { registerSound } from './superdough.mjs';
|
||||||
|
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
import { onceEnded, releaseAudioNode } from './helpers.mjs';
|
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
let {
|
let {
|
||||||
|
|||||||
Reference in New Issue
Block a user