mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-20 04:27:05 -04:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79e0503474 | |||
| 3a3d8299b8 | |||
| e0a61b7f1e | |||
| 5647bd84f9 | |||
| 971cfe15fc | |||
| be8aebff92 | |||
| 9db821e50f | |||
| 89247d92db | |||
| c828adce03 | |||
| 7e45389db3 | |||
| 601eaebb93 | |||
| 6b72e421cd | |||
| f3f7824111 | |||
| 29ee90f994 | |||
| 837b87e13a | |||
| d07134dc5f | |||
| 93a38003ed | |||
| bb0ea3b4ad | |||
| 99e4855545 | |||
| 555949366e | |||
| 93f7265f5c |
@@ -1622,40 +1622,6 @@ export const { unison } = registerControl('unison');
|
||||
*
|
||||
*/
|
||||
export const { spread } = registerControl('spread');
|
||||
|
||||
/**
|
||||
* Controls how much the detune is concentrated outwards or inwards. A value of 0 will be
|
||||
* linear spacing of voices; -1 will be concentrated outwards (inverse power law); 1 will be concentrated inwards
|
||||
*
|
||||
* @name dpow
|
||||
* @synonyms detunepower
|
||||
* @param {number | Pattern} power between -1 and 1
|
||||
*
|
||||
*/
|
||||
export const { dpow, detunepower } = registerControl('dpow', 'detunepower');
|
||||
|
||||
/**
|
||||
* Controls how much the central voices are emphasized in a detuned oscillator. -1 will emphasize detuned voices;
|
||||
* 1 will emphasize central voices (i.e. either the raw frequency if `unison` is odd or the central two detuned frequencies
|
||||
* if it is even); 0 will be the default blending
|
||||
*
|
||||
* @name dblend
|
||||
* @synonyms detuneblend
|
||||
* @param {number | Pattern} blend between -1 and 1
|
||||
*
|
||||
*/
|
||||
export const { dblend, detuneblend } = registerControl('dblend', 'detuneblend');
|
||||
|
||||
/**
|
||||
* Sets the stacking mode of detuned oscillators
|
||||
*
|
||||
* @name dstack
|
||||
* @synonyms detunestack
|
||||
* @param {number | Pattern} mode mode index starting at 0
|
||||
*
|
||||
*/
|
||||
export const { dstack, detunestack } = registerControl('dstack', 'detunestack');
|
||||
|
||||
/**
|
||||
* Set dryness of reverb. See `room` and `size` for more information about reverb.
|
||||
*
|
||||
|
||||
@@ -3687,3 +3687,26 @@ Pattern.prototype.phases = function (list) {
|
||||
export const phases = (list) => {
|
||||
return _ensureListPattern(list).as('phases');
|
||||
};
|
||||
|
||||
/**
|
||||
* Establishes a signal chain. Can be called in sequence like pat.chain(...).chain(...) and so forth
|
||||
* and/or in a single .chain(..., ..., etc) call. The arguments to `chain` are _patterns_ which each act like
|
||||
* a self-contained pattern and follow the normal [signal chain](https://strudel.cc/learn/effects/).
|
||||
*
|
||||
* If multiple sound generators are present within the chain, they will be mixed in at the location where
|
||||
* they are declared.
|
||||
*
|
||||
* @name chain
|
||||
* @memberof Pattern
|
||||
* @param {Pattern | Pattern[]} patterns Patterns to combine into a single chain
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.chain = function (...pats) {
|
||||
pats = pats.map(reify);
|
||||
return this.withValue((v) => (vEff) => {
|
||||
const currChain = v.chain ?? [];
|
||||
return { ...v, chain: currChain.concat(vEff) };
|
||||
}).appLeft(parray(pats));
|
||||
};
|
||||
|
||||
export const chain = (pats) => pure({}).chain(pats);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core';
|
||||
import {
|
||||
cleanupNodes,
|
||||
getAudioContext,
|
||||
registerSound,
|
||||
getParamADSR,
|
||||
@@ -161,22 +162,17 @@ export function registerSoundfonts() {
|
||||
const node = bufferSource.connect(envGain);
|
||||
const holdEnd = time + duration;
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear');
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
// pitch envelope
|
||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (releaseTime) => {};
|
||||
bufferSource.onended = () => {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
cleanupNodes([bufferSource, vibratoOscillator, node]);
|
||||
onended();
|
||||
};
|
||||
return { node, stop };
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
return { node, stop: bufferSource.stop };
|
||||
},
|
||||
{ type: 'soundfont', prebake: true, fonts },
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { clamp, midiToFreq, nanFallback, noteToMidi } from './util.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
@@ -518,30 +518,13 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
return Number(freq);
|
||||
};
|
||||
|
||||
export const destroyAudioWorkletNode = (node) => {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
node.disconnect();
|
||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||
export const cleanupNode = (node, time) => {
|
||||
if (node == null) return;
|
||||
node.disconnect?.();
|
||||
node.parameters?.get('end')?.setValueAtTime(0, 0);
|
||||
node.stop?.(time);
|
||||
};
|
||||
|
||||
export const getDetuner = (detune = 0, power = 0) => {
|
||||
if (detune <= 0) {
|
||||
return () => 0;
|
||||
}
|
||||
const curve = (x) => {
|
||||
if (power === 0) {
|
||||
return x;
|
||||
}
|
||||
const a = Math.abs(x),
|
||||
s = Math.sign(x);
|
||||
if (power > 0) {
|
||||
return s * Math.pow(a, 1 + 4 * power);
|
||||
}
|
||||
return s * (1 - Math.pow(1 - a, 1 - 4 * power));
|
||||
};
|
||||
return (t) => {
|
||||
return curve(2 * t - 1) * detune;
|
||||
};
|
||||
export const cleanupNodes = (nodes, time) => {
|
||||
nodes.forEach((n) => cleanupNode(n, time));
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { getADSRValues, cleanupNodes, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
@@ -319,19 +319,11 @@ 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);
|
||||
bufferSource.onended = function () {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
out.disconnect();
|
||||
const stop = () => {
|
||||
cleanupNodes([bufferSource, vibratoOscillator, node]);
|
||||
onended();
|
||||
};
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (endTime) => {
|
||||
bufferSource.stop(endTime);
|
||||
};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
const handle = { node: out, stop };
|
||||
|
||||
// cut groups
|
||||
if (cut !== undefined) {
|
||||
|
||||
+344
-235
@@ -7,9 +7,22 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import { _mod, clamp, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import {
|
||||
cleanupNode,
|
||||
cleanupNodes,
|
||||
createFilter,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getWorklet,
|
||||
effectSend,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
@@ -159,6 +172,9 @@ let defaultDefaultValues = {
|
||||
i: 1,
|
||||
velocity: 1,
|
||||
fft: 8,
|
||||
tremolodepth: 1,
|
||||
tremolophase: 0,
|
||||
drive: 0.69,
|
||||
};
|
||||
|
||||
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
||||
@@ -391,44 +407,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
// destructure
|
||||
let {
|
||||
tremolo,
|
||||
tremolosync,
|
||||
tremolodepth = 1,
|
||||
tremoloskew,
|
||||
tremolophase = 0,
|
||||
tremoloshape,
|
||||
s = getDefaultValue('s'),
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
postgain = getDefaultValue('postgain'),
|
||||
density = getDefaultValue('density'),
|
||||
duckorbit,
|
||||
duckonset,
|
||||
duckattack,
|
||||
duckdepth,
|
||||
djf,
|
||||
// filters
|
||||
fanchor = getDefaultValue('fanchor'),
|
||||
release = 0,
|
||||
|
||||
//phaser
|
||||
phaserrate: phaser,
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
phasersweep,
|
||||
phasercenter,
|
||||
//
|
||||
coarse,
|
||||
|
||||
crush,
|
||||
dry,
|
||||
shape,
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
distortvol = getDefaultValue('distortvol'),
|
||||
distorttype = getDefaultValue('distorttype'),
|
||||
pan,
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
@@ -443,18 +432,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
irspeed,
|
||||
irbegin,
|
||||
i = getDefaultValue('i'),
|
||||
velocity = getDefaultValue('velocity'),
|
||||
analyze, // analyser wet
|
||||
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
||||
compressor: compressorThreshold,
|
||||
compressorRatio,
|
||||
compressorKnee,
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
chain: subChains = [],
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
||||
if (value.wtPosSynced != null) {
|
||||
value.wtPosRate /= cps;
|
||||
}
|
||||
|
||||
if (value.wtWarpSynced != null) {
|
||||
value.wtWarpRate /= cps;
|
||||
}
|
||||
|
||||
const orbitChannels = mapChannelNumbers(
|
||||
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
|
||||
);
|
||||
@@ -465,18 +457,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
||||
}
|
||||
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
postgain = applyGainCurve(postgain);
|
||||
shapevol = applyGainCurve(shapevol);
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
|
||||
const end = t + hapDuration;
|
||||
const endWithRelease = end + release;
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
|
||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
|
||||
@@ -489,8 +471,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
activeSoundSources.delete(chainID);
|
||||
}
|
||||
|
||||
let audioNodes = [];
|
||||
|
||||
if (['-', '~', '_'].includes(s)) {
|
||||
return;
|
||||
}
|
||||
@@ -498,201 +478,318 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
}
|
||||
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, value, hapDuration, cps);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const onEnded = () => {
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
||||
const end = t + hapDuration;
|
||||
let fullRelease = release;
|
||||
for (const ch of subChains) {
|
||||
fullRelease = Math.max(fullRelease, ch.release ?? 0);
|
||||
}
|
||||
const fullEndWithRelease = end + fullRelease + 0.02;
|
||||
// The following ensures we run through the value once as normal _and then_ through all the subchains
|
||||
subChains = [value, ...subChains];
|
||||
let prev;
|
||||
for (const ch of subChains) {
|
||||
const audioNodes = [];
|
||||
const chain = [];
|
||||
ch.duration = hapDuration;
|
||||
const endWithRelease = end + Math.max(release, ch.release ?? 0);
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, ch, hapDuration, cps);
|
||||
chain.push(sourceNode);
|
||||
} else if (ch.s !== undefined && getSound(ch.s)) {
|
||||
const { onTrigger } = getSound(ch.s);
|
||||
const soundHandle = await onTrigger(t, ch, () => {}, cps);
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
||||
}
|
||||
chain.push(sourceNode);
|
||||
} else if (ch.s !== undefined) {
|
||||
throw new Error(`sound ${ch.s} not found! Is it loaded?`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
}
|
||||
if (!sourceNode) {
|
||||
// if onTrigger does not return anything, we will just silently skip
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
return;
|
||||
}
|
||||
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
// filter
|
||||
const ftype = getFilterType(value.ftype);
|
||||
|
||||
if (value.cutoff !== undefined) {
|
||||
const lpMap = {
|
||||
frequency: 'cutoff',
|
||||
q: 'resonance',
|
||||
attack: 'lpattack',
|
||||
decay: 'lpdecay',
|
||||
sustain: 'lpsustain',
|
||||
release: 'lprelease',
|
||||
env: 'lpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'lprate',
|
||||
sync: 'lpsync',
|
||||
depth: 'lpdepth',
|
||||
shape: 'lpshape',
|
||||
dcoffset: 'lpdc',
|
||||
skew: 'lpskew',
|
||||
};
|
||||
const lpParams = pickAndRename(value, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
let lp = () => createFilter(ac, t, end, lpParams, cps);
|
||||
chain.push(lp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(lp());
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (value.hcutoff !== undefined) {
|
||||
const hpMap = {
|
||||
frequency: 'hcutoff',
|
||||
q: 'hresonance',
|
||||
attack: 'hpattack',
|
||||
decay: 'hpdecay',
|
||||
sustain: 'hpsustain',
|
||||
release: 'hprelease',
|
||||
env: 'hpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'hprate',
|
||||
sync: 'hpsync',
|
||||
depth: 'hpdepth',
|
||||
shape: 'hpshape',
|
||||
dcoffset: 'hpdc',
|
||||
skew: 'hpskew',
|
||||
};
|
||||
const hpParams = pickAndRename(value, hpMap);
|
||||
hpParams.type = 'highpass';
|
||||
let hp = () => createFilter(ac, t, end, hpParams, cps);
|
||||
chain.push(hp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(hp());
|
||||
const prevExists = prev !== undefined;
|
||||
const sourceExists = sourceNode !== undefined;
|
||||
if (!prevExists && !sourceExists) {
|
||||
// if there are no signals, we will just silently continue
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
continue;
|
||||
} else if (prevExists && !sourceExists) {
|
||||
// just FX -- apply ADSR now
|
||||
const adsrParams = [ch.attack, ch.decay, ch.sustain, ch.release];
|
||||
if (adsrParams.some((v) => v != null)) {
|
||||
const [attack, decay, sustain, release] = getADSRValues(adsrParams, 'linear', [0.001, 0.05, 0.6, 0.01]);
|
||||
const adsrNode = gainNode(1);
|
||||
chain.push(adsrNode);
|
||||
getParamADSR(adsrNode.gain, attack, decay, sustain, release, 0, 1, t, t + hapDuration, 'linear');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value.bandf !== undefined) {
|
||||
const bpMap = {
|
||||
frequency: 'bandf',
|
||||
q: 'bandq',
|
||||
attack: 'bpattack',
|
||||
decay: 'bpdecay',
|
||||
sustain: 'bpsustain',
|
||||
release: 'bprelease',
|
||||
env: 'bpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'bprate',
|
||||
sync: 'bpsync',
|
||||
depth: 'bpdepth',
|
||||
shape: 'bpshape',
|
||||
dcoffset: 'bpdc',
|
||||
skew: 'bpskew',
|
||||
};
|
||||
const bpParams = pickAndRename(value, bpMap);
|
||||
bpParams.type = 'bandpass';
|
||||
let bp = () => createFilter(ac, t, end, bpParams, cps);
|
||||
chain.push(bp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(bp());
|
||||
}
|
||||
}
|
||||
|
||||
if (vowel !== undefined) {
|
||||
const vowelFilter = ac.createVowelFilter(vowel);
|
||||
chain.push(vowelFilter);
|
||||
}
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||
|
||||
if (tremolosync != null) {
|
||||
tremolo = cps * tremolosync;
|
||||
}
|
||||
|
||||
if (value.wtPosSynced != null) {
|
||||
value.wtPosRate /= cps;
|
||||
}
|
||||
|
||||
if (value.wtWarpSynced != null) {
|
||||
value.wtWarpRate /= cps;
|
||||
}
|
||||
|
||||
if (tremolo !== undefined) {
|
||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||
const gain = Math.max(1 - tremolodepth, 0);
|
||||
const amGain = new GainNode(ac, { gain });
|
||||
|
||||
const time = cycle / cps;
|
||||
const lfo = getLfo(ac, t, endWithRelease, {
|
||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
||||
frequency: tremolo,
|
||||
depth: tremolodepth,
|
||||
time,
|
||||
dcoffset: 0,
|
||||
shape: tremoloshape,
|
||||
phaseoffset: tremolophase,
|
||||
min: 0,
|
||||
max: 1,
|
||||
curve: 1.5,
|
||||
});
|
||||
lfo.connect(amGain.gain);
|
||||
chain.push(amGain);
|
||||
}
|
||||
|
||||
compressorThreshold !== undefined &&
|
||||
chain.push(
|
||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
||||
webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
activeSoundSources.delete(chainID);
|
||||
},
|
||||
0,
|
||||
fullEndWithRelease,
|
||||
);
|
||||
let {
|
||||
gain = getDefaultValue('gain'),
|
||||
velocity = getDefaultValue('velocity'),
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distorttype = getDefaultValue('distorttype'),
|
||||
distortvol = getDefaultValue('distortvol'),
|
||||
tremolodepth = getDefaultValue('tremolodepth'),
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
delay = getDefaultValue('delay'),
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
} = ch;
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
gain = applyGainCurve(gain);
|
||||
velocity = applyGainCurve(velocity);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
shapevol = applyGainCurve(shapevol);
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
ch.drive = ch.drive ?? getDefaultValue('drive');
|
||||
ch.fanchor = ch.fanchor ?? getDefaultValue('fanchor');
|
||||
ch.stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: ch.stretch }));
|
||||
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
// phaser
|
||||
if (phaser !== undefined && phaserdepth > 0) {
|
||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
chain.push(phaserFX);
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
//filter
|
||||
const ftype = getFilterType(ch.ftype);
|
||||
if (ch.cutoff !== undefined) {
|
||||
const lpMap = {
|
||||
frequency: 'cutoff',
|
||||
q: 'resonance',
|
||||
attack: 'lpattack',
|
||||
decay: 'lpdecay',
|
||||
sustain: 'lpsustain',
|
||||
release: 'lprelease',
|
||||
env: 'lpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'lprate',
|
||||
sync: 'lpsync',
|
||||
depth: 'lpdepth',
|
||||
shape: 'lpshape',
|
||||
dcoffset: 'lpdc',
|
||||
skew: 'lpskew',
|
||||
};
|
||||
const lpParams = pickAndRename(ch, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
let lp = () => createFilter(ac, t, end, lpParams, cps);
|
||||
chain.push(lp());
|
||||
if (ch.ftype === '24db') {
|
||||
chain.push(lp());
|
||||
}
|
||||
}
|
||||
|
||||
if (ch.hcutoff !== undefined) {
|
||||
const hpMap = {
|
||||
frequency: 'hcutoff',
|
||||
q: 'hresonance',
|
||||
attack: 'hpattack',
|
||||
decay: 'hpdecay',
|
||||
sustain: 'hpsustain',
|
||||
release: 'hprelease',
|
||||
env: 'hpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'hprate',
|
||||
sync: 'hpsync',
|
||||
depth: 'hpdepth',
|
||||
shape: 'hpshape',
|
||||
dcoffset: 'hpdc',
|
||||
skew: 'hpskew',
|
||||
};
|
||||
const hpParams = pickAndRename(ch, hpMap);
|
||||
hpParams.type = 'highpass';
|
||||
let hp = () => createFilter(ac, t, end, hpParams, cps);
|
||||
chain.push(hp());
|
||||
if (ch.ftype === '24db') {
|
||||
chain.push(hp());
|
||||
}
|
||||
}
|
||||
|
||||
if (ch.bandf !== undefined) {
|
||||
const bpMap = {
|
||||
frequency: 'bandf',
|
||||
q: 'bandq',
|
||||
attack: 'bpattack',
|
||||
decay: 'bpdecay',
|
||||
sustain: 'bpsustain',
|
||||
release: 'bprelease',
|
||||
env: 'bpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'bprate',
|
||||
sync: 'bpsync',
|
||||
depth: 'bpdepth',
|
||||
shape: 'bpshape',
|
||||
dcoffset: 'bpdc',
|
||||
skew: 'bpskew',
|
||||
};
|
||||
const bpParams = pickAndRename(ch, bpMap);
|
||||
bpParams.type = 'bandpass';
|
||||
let bp = () => createFilter(ac, t, end, bpParams, cps);
|
||||
chain.push(bp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(bp());
|
||||
}
|
||||
}
|
||||
|
||||
if (ch.vowel !== undefined) {
|
||||
const vowelFilter = ac.createVowelFilter(ch.vowel);
|
||||
chain.push(vowelFilter);
|
||||
}
|
||||
|
||||
// effects
|
||||
ch.coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse: ch.coarse }));
|
||||
ch.crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush: ch.crush }));
|
||||
ch.shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape: ch.shape, postgain: shapevol }));
|
||||
ch.distort !== undefined && chain.push(getDistortion(ch.distort, distortvol, distorttype));
|
||||
|
||||
let tremolo = ch.tremolo;
|
||||
if (ch.tremolosync != null) {
|
||||
tremolo = cps * ch.tremolosync;
|
||||
}
|
||||
|
||||
if (tremolo !== undefined) {
|
||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||
const gain = Math.max(1 - tremolodepth, 0);
|
||||
const amGain = new GainNode(ac, { gain });
|
||||
|
||||
const time = cycle / cps;
|
||||
const lfo = getLfo(ac, t, endWithRelease, {
|
||||
skew: ch.tremoloskew ?? (ch.tremoloshape != null ? 0.5 : 1),
|
||||
frequency: tremolo,
|
||||
depth: tremolodepth,
|
||||
time,
|
||||
dcoffset: 0,
|
||||
shape: ch.tremoloshape,
|
||||
phaseoffset: ch.tremolophase,
|
||||
min: 0,
|
||||
max: 1,
|
||||
curve: 1.5,
|
||||
});
|
||||
lfo.connect(amGain.gain);
|
||||
chain.push(amGain);
|
||||
}
|
||||
|
||||
ch.compressor !== undefined &&
|
||||
chain.push(
|
||||
getCompressor(
|
||||
ac,
|
||||
ch.compressor,
|
||||
ch.compressorRatio,
|
||||
ch.compressorKnee,
|
||||
ch.compressorAttack,
|
||||
ch.compressorRelease,
|
||||
),
|
||||
);
|
||||
|
||||
// panning
|
||||
if (ch.pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * ch.pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
// phaser
|
||||
if (ch.phaserrate !== undefined && phaserdepth > 0) {
|
||||
const phaserFX = getPhaser(t, endWithRelease, ch.phaserrate, phaserdepth, ch.phasercenter, ch.phasersweep);
|
||||
chain.push(phaserFX);
|
||||
}
|
||||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const dry = gainNode(1);
|
||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||
const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
const wetDelay = gainNode(delay);
|
||||
const dryDelay = gainNode(ch.dry ?? 1);
|
||||
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
dry.connect(dryDelay).connect(sum);
|
||||
dry.connect(delayNode).connect(wetDelay).connect(sum);
|
||||
const stop = () => cleanupNodes([dryDelay, delayNode, dry, wetDelay]);
|
||||
chain.push({ input: dry, output: sum, disconnect: stop });
|
||||
}
|
||||
// reverb
|
||||
if (ch.room > 0) {
|
||||
let roomIR;
|
||||
if (ch.ir !== undefined) {
|
||||
let url;
|
||||
let sample = getSound(ch.ir);
|
||||
if (Array.isArray(sample)) {
|
||||
url = sample.data.samples[ch.i % sample.data.samples.length];
|
||||
} else if (typeof sample === 'object') {
|
||||
url = Object.values(sample.data.samples).flat()[ch.i % Object.values(sample.data.samples).length];
|
||||
}
|
||||
roomIR = await loadBuffer(url, ac, ch.ir, 0);
|
||||
}
|
||||
const dry = gainNode(1);
|
||||
const reverbNode = ac.createReverb(
|
||||
ch.roomsize,
|
||||
ch.roomfade,
|
||||
ch.roomlp,
|
||||
ch.roomdim,
|
||||
roomIR,
|
||||
ch.irspeed,
|
||||
ch.irbegin,
|
||||
);
|
||||
const wetReverb = gainNode(ch.room);
|
||||
const dryReverb = gainNode(ch.dry ?? 1);
|
||||
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
dry.connect(dryReverb).connect(sum);
|
||||
dry.connect(reverbNode).connect(wetReverb).connect(sum);
|
||||
const stop = () => cleanupNodes([dryReverb, reverbNode, dry, wetReverb]);
|
||||
chain.push({ input: dry, output: sum, disconnect: stop });
|
||||
}
|
||||
if (prevExists) {
|
||||
const prevOut = prev.output ?? prev;
|
||||
if (sourceExists) {
|
||||
// mix with the previous output
|
||||
const mixer = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
prevOut.connect(mixer);
|
||||
chain.push(mixer);
|
||||
} else {
|
||||
prevOut.connect(chain[0]);
|
||||
}
|
||||
}
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => {
|
||||
const lastC = last.output ?? last;
|
||||
const currentC = current.input ?? current;
|
||||
lastC.connect(currentC);
|
||||
return current.output ?? current;
|
||||
}, chain[0]);
|
||||
audioNodes.push(...chain);
|
||||
prev = chain[chain.length - 1];
|
||||
}
|
||||
const outerAudioNodes = [];
|
||||
const outerChain = [prev];
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
outerChain.push(post);
|
||||
|
||||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
orbitBus.sendDelay(post, delay);
|
||||
const send = orbitBus.sendDelay(post, delay);
|
||||
outerAudioNodes.push(send);
|
||||
}
|
||||
// reverb
|
||||
if (room > 0) {
|
||||
@@ -709,7 +806,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
const send = orbitBus.sendReverb(post, room);
|
||||
audioNodes.push(send);
|
||||
outerAudioNodes.push(send);
|
||||
}
|
||||
|
||||
if (djf != null) {
|
||||
@@ -720,20 +817,32 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
if (analyze) {
|
||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||
const analyserSend = effectSend(post, analyserNode, 1);
|
||||
audioNodes.push(analyserSend);
|
||||
outerAudioNodes.push(analyserSend);
|
||||
}
|
||||
if (dry != null) {
|
||||
dry = applyGainCurve(dry);
|
||||
const dryGain = new GainNode(ac, { gain: dry });
|
||||
chain.push(dryGain);
|
||||
outerChain.push(dryGain);
|
||||
orbitBus.connectToOutput(dryGain);
|
||||
} else {
|
||||
orbitBus.connectToOutput(post);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
audioNodes = audioNodes.concat(chain);
|
||||
outerChain.slice(1).reduce((last, current) => {
|
||||
const lastC = last.output ?? last;
|
||||
const currentC = current.input ?? current;
|
||||
lastC.connect(currentC);
|
||||
return current.output ?? current;
|
||||
}, outerChain[0]);
|
||||
outerAudioNodes.push(...outerChain);
|
||||
webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
outerAudioNodes.forEach((n) => n?.disconnect?.());
|
||||
},
|
||||
0,
|
||||
fullEndWithRelease,
|
||||
);
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||
|
||||
@@ -82,7 +82,7 @@ export class Orbit {
|
||||
}
|
||||
|
||||
sendDelay(node, amount) {
|
||||
effectSend(node, this.delayNode, amount);
|
||||
return effectSend(node, this.delayNode, amount);
|
||||
}
|
||||
|
||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { registerSound, soundMap } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
destroyAudioWorkletNode,
|
||||
cleanupNodes,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
@@ -47,33 +47,21 @@ export function registerSynthSounds() {
|
||||
'linear',
|
||||
[0.001, 0.05, 0.6, 0.01],
|
||||
);
|
||||
|
||||
let sound = getOscillator(s, t, value);
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
const { node: o, stop } = getOscillator(s, t, value);
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
cleanupNodes([o, g]);
|
||||
onended();
|
||||
};
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
const holdEnd = t + duration;
|
||||
const node = o.connect(g).connect(gainNode(1));
|
||||
const holdEnd = t + value.duration;
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
triggerRelease?.(envEnd);
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
stop,
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
@@ -113,12 +101,7 @@ export function registerSynthSounds() {
|
||||
const mix = gainNode(mixGain);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
sat.disconnect();
|
||||
noise.node.disconnect();
|
||||
noiseGain.disconnect();
|
||||
mix.disconnect();
|
||||
cleanupNodes([o, g, sat, noise.node, noiseGain]);
|
||||
onended();
|
||||
};
|
||||
|
||||
@@ -136,13 +119,10 @@ export function registerSynthSounds() {
|
||||
mix.gain.linearRampToValueAtTime(0, end);
|
||||
|
||||
o.stop(end);
|
||||
noise.stop(end);
|
||||
|
||||
return {
|
||||
node,
|
||||
stop: (endTime) => {
|
||||
o.stop(endTime);
|
||||
},
|
||||
stop: o.stop,
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
@@ -174,44 +154,34 @@ export function registerSynthSounds() {
|
||||
begin,
|
||||
end,
|
||||
freqspread: detune,
|
||||
voices,
|
||||
panspread,
|
||||
power: value.dpow,
|
||||
blend: value.dblend,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
processorOptions: {
|
||||
voices,
|
||||
stackmode: value.dstack,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
const node = o.connect(gainNode(1));
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, begin, holdend, 'linear');
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
cleanupNodes([o, fm, vibratoOscillator]);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
node,
|
||||
stop: timeoutNode.stop,
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
@@ -264,30 +234,21 @@ export function registerSynthSounds() {
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
|
||||
o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency });
|
||||
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
const node = o.connect(gainNode(1));
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
envGain.disconnect();
|
||||
cleanupNodes([node, o]);
|
||||
onended();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
node,
|
||||
stop: timeoutNode.stop,
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
@@ -337,34 +298,26 @@ export function registerSynthSounds() {
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
const node = o.connect(gainNode(1));
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
let lfo;
|
||||
if (pwsweep != 0) {
|
||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
||||
lfo.connect(o.parameters.get('pulsewidth'));
|
||||
}
|
||||
let timeoutNode = webAudioTimeout(
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(o);
|
||||
destroyAudioWorkletNode(lfo);
|
||||
envGain.disconnect();
|
||||
cleanupNodes([node, o, lfo, fm, vibratoOscillator]);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
node,
|
||||
stop: timeoutNode.stop,
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'synth' },
|
||||
@@ -385,31 +338,26 @@ export function registerSynthSounds() {
|
||||
let { density } = value;
|
||||
sound = getNoiseOscillator(s, t, density);
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
let { node: o, stop } = sound;
|
||||
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
const holdEnd = t + duration;
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
o.onended = () => {
|
||||
cleanupNodes([node, o, g]);
|
||||
onended();
|
||||
};
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
triggerRelease?.(envEnd);
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
stop,
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
@@ -497,17 +445,14 @@ export function getOscillator(s, t, value) {
|
||||
if (noise) {
|
||||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
const stop = (time) => {
|
||||
fmModulator.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
o.stop(time);
|
||||
};
|
||||
return {
|
||||
node: noiseMix?.node || o,
|
||||
stop: (time) => {
|
||||
fmModulator.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
o.stop(time);
|
||||
},
|
||||
triggerRelease: (time) => {
|
||||
// envGain?.stop(time);
|
||||
},
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getCommonSampleInfo } from './util.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
applyParameterModulators,
|
||||
destroyAudioWorkletNode,
|
||||
cleanupNodes,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getParamADSR,
|
||||
@@ -234,19 +234,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
power: value.dpow,
|
||||
blend: value.dblend,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
processorOptions: {
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
stackmode: value.dstack,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
warpmode,
|
||||
},
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
);
|
||||
source.port.postMessage({ type: 'table', payload });
|
||||
if (ac.currentTime > t) {
|
||||
@@ -325,19 +318,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
fm?.stop();
|
||||
node.disconnect();
|
||||
wtPosModulators?.disconnect();
|
||||
wtWarpModulators?.disconnect();
|
||||
cleanupNodes([source, vibratoOscillator, fm, node, wtPosModulators, wtWarpModulators]);
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
envEnd,
|
||||
);
|
||||
handle.stop = (time) => {
|
||||
timeoutNode.stop(time);
|
||||
};
|
||||
handle.stop = timeoutNode.stop;
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm, getDetuner } from './helpers.mjs';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
|
||||
const blockSize = 128;
|
||||
const PI = Math.PI;
|
||||
@@ -28,6 +28,16 @@ const fast_tanh = (x) => {
|
||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||
};
|
||||
|
||||
// Optimized per-voice detuner which precomputes constants
|
||||
const getDetuner = (unison, detune) => {
|
||||
if (unison < 2) {
|
||||
return (_voiceIdx) => 0;
|
||||
}
|
||||
const scale = detune / (unison - 1);
|
||||
const center = detune * 0.5;
|
||||
return (voiceIdx) => voiceIdx * scale - center;
|
||||
};
|
||||
|
||||
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||
return frequency * Math.pow(2, detune / 12);
|
||||
};
|
||||
@@ -444,68 +454,11 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('distort-processor', DistortProcessor);
|
||||
|
||||
const INTERVALS = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
[1, 2, 4],
|
||||
[1, 2, 4, 8],
|
||||
[1, 2, 3],
|
||||
[1, 1.5, 2.4],
|
||||
[1, 2.5],
|
||||
[1, 1.782],
|
||||
[1, 2.4],
|
||||
[1, 1.5, 2.5],
|
||||
[1, 1.667],
|
||||
[1, 2.25],
|
||||
];
|
||||
const _getIntervals = (mode = 0) => {
|
||||
return INTERVALS[mode] ?? INTERVALS[0];
|
||||
};
|
||||
|
||||
const initUnisonProcessor = (processor, processorOptions) => {
|
||||
processor.intervals = _getIntervals(processorOptions.stackmode);
|
||||
processor.intervalGains = processor.intervals.map((i) => 1 / i ** 0.5); // reduce volume of upper harmonics
|
||||
processor.intNorm = Math.sqrt(1 / processor.intervalGains.reduce((acc, i) => acc + i ** 2, 0));
|
||||
processor.numIntervals = processor.intervals.length;
|
||||
processor.voices = processorOptions.voices;
|
||||
processor.isDetuned = processor.voices > 1;
|
||||
processor.isCenter = (idx) => {
|
||||
if (!processor.isDetuned) return true;
|
||||
if (processor.voices % 2 === 1) {
|
||||
return idx === (processor.voices - 1) >> 1;
|
||||
} else {
|
||||
const right = processor.voices >> 1;
|
||||
const left = right - 1;
|
||||
return idx === left || idx === right;
|
||||
}
|
||||
};
|
||||
const totalVoices = processor.voices * processor.numIntervals;
|
||||
processor.blendGains = new Array(processor.voices).fill(1);
|
||||
const phaserand = processorOptions.phaserand ?? 1;
|
||||
processor.phases = new Array(totalVoices).fill(0).map(() => Math.random() * phaserand);
|
||||
return totalVoices;
|
||||
};
|
||||
|
||||
const getUnisonData = (processor, params, i) => {
|
||||
const detune = pv(params.detune, i);
|
||||
const freqspread = pv(params.freqspread, i);
|
||||
const blend = pv(params.blend, i) * 0.95;
|
||||
const blendGains = processor.blendGains.map((_, idx) => (processor.isCenter(idx) ? 1 - blend : 1 + blend));
|
||||
const blendNorm = 1 / Math.sqrt(blendGains.reduce((acc, g) => acc + g ** 2, 0));
|
||||
const normalizer = blendNorm * processor.intNorm;
|
||||
const power = pv(params.power, i);
|
||||
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
|
||||
const detuner = getDetuner(freqspread, power);
|
||||
const inv = processor.voices > 1 ? 1 / (processor.voices - 1) : 0;
|
||||
return { blendGains, normalizer, detuner, inv, freq };
|
||||
};
|
||||
|
||||
// SUPERSAW
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor({ processorOptions }) {
|
||||
constructor() {
|
||||
super();
|
||||
initUnisonProcessor(this, processorOptions);
|
||||
this.phase = [];
|
||||
}
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
@@ -515,17 +468,20 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
min: Number.EPSILON,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'panspread',
|
||||
defaultValue: 0.4,
|
||||
@@ -542,17 +498,12 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'blend',
|
||||
defaultValue: 0,
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
name: 'power',
|
||||
defaultValue: 0,
|
||||
min: -1,
|
||||
max: 1,
|
||||
name: 'voices',
|
||||
defaultValue: 5,
|
||||
min: 1,
|
||||
automationRate: 'k-rate',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -561,35 +512,39 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
// this.port.postMessage({ type: 'onended' });
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0]; // k-rate
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, params, i);
|
||||
const detune = pv(params.detune, i);
|
||||
const freqspread = pv(params.freqspread, i);
|
||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||
let gainL = Math.sqrt(1 - panspread) * normalizer;
|
||||
let gainR = Math.sqrt(panspread) * normalizer;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
let gainL = Math.sqrt(1 - panspread);
|
||||
let gainR = Math.sqrt(panspread);
|
||||
let freq = pv(params.frequency, i);
|
||||
// Main detuning
|
||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
// Individual voice detuning
|
||||
const freqVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||
const bG = blendGains[n];
|
||||
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||
const g = bG * this.intervalGains[idx];
|
||||
const freqStack = freqVoice * this.intervals[idx];
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = ffrac(freqStack * INVSR);
|
||||
const voiceNum = n * this.numIntervals + idx;
|
||||
const v = waveshapes.sawblep(this.phases[voiceNum], dt);
|
||||
output[0][i] += v * gainL * g;
|
||||
output[1][i] += v * gainR * g;
|
||||
let pn = this.phases[voiceNum] + dt;
|
||||
if (pn >= 1.0) pn -= 1.0;
|
||||
this.phases[voiceNum] = pn;
|
||||
// invert right and left gain
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
}
|
||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = ffrac(freqVoice * INVSR);
|
||||
this.phase[n] = this.phase[n] ?? Math.random();
|
||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||
|
||||
output[0][i] += v * gainL;
|
||||
output[1][i] += v * gainR;
|
||||
|
||||
let pn = this.phase[n] + dt;
|
||||
if (pn >= 1.0) pn -= 1.0;
|
||||
this.phase[n] = pn;
|
||||
// invert right and left gain
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -1169,24 +1124,24 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warpMode', defaultValue: 0 },
|
||||
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
|
||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||
{ name: 'blend', defaultValue: 0, min: -1, max: 1 },
|
||||
{ name: 'power', defaultValue: 0, min: -1, max: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
constructor({ processorOptions }) {
|
||||
super();
|
||||
initUnisonProcessor(this, processorOptions);
|
||||
this.warpmode = processorOptions.warpmode;
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'table') {
|
||||
const key = payload.key;
|
||||
this.frameLen = payload.frameLen;
|
||||
this.baseThreshold = this.frameLen >>> 3; // used for mipmaps
|
||||
if (!tablesCache[key]) {
|
||||
const tables = [payload.frames];
|
||||
let table = tables[0];
|
||||
@@ -1354,13 +1309,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||
const numTables = this.tables.length;
|
||||
let level = 0,
|
||||
threshold = this.baseThreshold;
|
||||
while (level + 1 < numTables && approxHarm < threshold) {
|
||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
threshold *= 2;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
@@ -1379,41 +1331,50 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
}
|
||||
const voices = parameters.voices[0]; // k-rate
|
||||
for (let i = 0; i < outL.length; i++) {
|
||||
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, parameters, i);
|
||||
const detune = pv(parameters.detune, i);
|
||||
const freqspread = pv(parameters.freqspread, i);
|
||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
const frac = idx - fIdx;
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const panspread = this.isDetuned ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
let gainL = Math.sqrt(0.5 - 0.5 * panspread) * normalizer;
|
||||
let gainR = Math.sqrt(0.5 + 0.5 * panspread) * normalizer;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
const fVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||
const bG = blendGains[n];
|
||||
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||
const g = bG * this.intervalGains[idx];
|
||||
const fStack = fVoice * this.intervals[idx];
|
||||
const dt = fStack * INVSR;
|
||||
const voiceNum = n * this.numIntervals + idx;
|
||||
const level = this._chooseMip(dt);
|
||||
const table = this.tables[level];
|
||||
// warp phase then sample
|
||||
const ph = this._warpPhase(this.phases[voiceNum], warpAmount, this.warpmode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = s0 + (s1 - s0) * frac;
|
||||
if (this.warpmode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
}
|
||||
outL[i] += s * gainL * g;
|
||||
outR[i] += s * gainR * g;
|
||||
this.phases[voiceNum] = ffrac(this.phases[voiceNum] + dt);
|
||||
// invert right and left gain
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
if (isOdd) {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
||||
const dPhase = fVoice * INVSR;
|
||||
const level = this._chooseMip(dPhase);
|
||||
const table = this.tables[level];
|
||||
|
||||
// warp phase then sample
|
||||
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = s0 + (s1 - s0) * frac;
|
||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
}
|
||||
outL[i] += s * gainL * normalizer;
|
||||
outR[i] += s * gainR * normalizer;
|
||||
this.phase[n] = ffrac(this.phase[n] + dPhase);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { buildSamples } from './zzfx_fork.mjs';
|
||||
import { cleanupNode } from './helpers.mjs';
|
||||
|
||||
export const getZZFX = (value, t) => {
|
||||
let {
|
||||
@@ -84,7 +85,7 @@ export function registerZZFXSounds() {
|
||||
(t, value, onended) => {
|
||||
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
cleanupNode(o);
|
||||
onended();
|
||||
};
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user