Add LFOs and envelopes

This commit is contained in:
Aria 2025-09-27 12:48:37 -07:00
parent 8c3ee6db6b
commit 917d01c47d
5 changed files with 247 additions and 34 deletions

View file

@ -97,6 +97,36 @@ export const getParamADSR = (
param[ramp](min, end + release);
};
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
debugger;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,

View file

@ -9,7 +9,7 @@ import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, effectSend } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
@ -29,13 +29,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@ -314,28 +307,6 @@ export function connectToDestination(input, channels) {
controller.output.connectToDestination(input, channels);
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
@ -682,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
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

View file

@ -1,11 +1,12 @@
import { clamp } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import {
applyFM,
destroyAudioWorkletNode,
gainNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,

View file

@ -4,6 +4,7 @@ import {
destroyAudioWorkletNode,
getADSRValues,
getFrequencyFromValue,
getLfo,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
@ -235,7 +236,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
const { tableUrl, label } = getTableInfo(value, tables);
const payload = await loadWavetableFrames(tableUrl, label, frameLen);
const holdEnd = t + duration;
const envEnd = holdEnd + release + 0.01;
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
@ -258,6 +260,37 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
}
const posADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease];
const warpADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
if (posADSRParams.some((p) => p !== undefined)) {
const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams);
getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, 0, 1, t, holdEnd, 'linear');
} else {
const posLFO = getLfo(ac, t, endWithRelease, {
frequency: value.wtPosRate,
depth: value.wtPosDepth,
shape: value.wtPosShape,
skew: value.wtPosSkew,
dcoffset: value.wtPosDCOffset ?? 0,
});
posLFO.connect(positionParam);
}
if (posADSRParams.some((p) => p !== undefined)) {
const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams);
getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, 0, 1, t, holdEnd, 'linear');
} else {
const warpLFO = getLfo(ac, t, endWithRelease, {
frequency: value.wtWarpRate,
depth: value.wtWarpDepth,
shape: value.wtWarpShape,
skew: value.wtWarpSkew,
dcoffset: value.wtWarpDCOffset ?? 0,
});
warpLFO.connect(warpParam);
}
const vibratoOscillator = getVibratoOscillator(source.detune, value, t);
const envGain = ac.createGain();
const node = source.connect(envGain);
@ -271,6 +304,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
node.disconnect();
warpLFO.disconnect();
posLFO.disconnect();
onended();
},
t,